diff --git a/src/lib/i18n/en.json b/src/lib/i18n/en.json
index ce614c8..e20f7aa 100644
--- a/src/lib/i18n/en.json
+++ b/src/lib/i18n/en.json
@@ -42,7 +42,7 @@
},
"dashboard": {
"title": "Dashboard",
- "total_skus": "Total SKUs",
+ "total_skus": "Total parts",
"low_stock": "At or below reorder level",
"inventory_value": "Inventory value (at cost)",
"low_stock_list": "Low stock",
@@ -68,11 +68,12 @@
"location": "Location",
"barcode": "Barcode",
"active": "Active",
- "search_placeholder": "Search by SKU, name, or barcode…",
+ "search_placeholder": "Search by name or barcode…",
"no_results": "No parts match your search.",
"all": "All",
"recent_movements": "Recent movements",
"initial_quantity": "Initial quantity",
+ "delete_confirm": "Deactivate part \"{name}\"? It will be hidden from lists, but movements and counts will be kept.",
"errors": {
"sku_required": "SKU is required.",
"name_required": "At least one name (English or Tajik) is required.",
@@ -142,7 +143,7 @@
"this_month": "This month",
"all_time": "All time",
"invoices": "invoices",
- "active_skus": "Active SKUs",
+ "active_skus": "Active parts",
"units_on_hand": "Units on hand",
"cost_value": "Value (at cost)",
"sale_value": "Value (at sale)",
diff --git a/src/lib/i18n/tg.json b/src/lib/i18n/tg.json
index 8539e29..e1aa969 100644
--- a/src/lib/i18n/tg.json
+++ b/src/lib/i18n/tg.json
@@ -42,7 +42,7 @@
},
"dashboard": {
"title": "Лавҳаи асосӣ",
- "total_skus": "Ҳамаи SKU-ҳо",
+ "total_skus": "Ҳамаи қисмҳо",
"low_stock": "Дар сатҳи фармоиш ё камтар",
"inventory_value": "Арзиши захира (бо нархи харид)",
"low_stock_list": "Захираи кам",
@@ -68,11 +68,12 @@
"location": "Ҷой",
"barcode": "Штрих-код",
"active": "Фаъол",
- "search_placeholder": "Ҷустуҷӯ аз рӯи SKU, ном ё штрих-код…",
+ "search_placeholder": "Ҷустуҷӯ аз рӯи ном ё штрих-код…",
"no_results": "Ҳеҷ қисм мувофиқат намекунад.",
"all": "Ҳама",
"recent_movements": "Ҳаракатҳои охирин",
"initial_quantity": "Шумораи аввала",
+ "delete_confirm": "Қисми «{name}»-ро ғайрифаъол мекунед? Дар рӯйхатҳо нишон дода намешавад, аммо ҳаракатҳо ва ҳисобҳо боқӣ мемонанд.",
"errors": {
"sku_required": "SKU зарур аст.",
"name_required": "Ҳадди ақалл як ном (англисӣ ё тоҷикӣ) зарур аст.",
@@ -142,7 +143,7 @@
"this_month": "Ин моҳ",
"all_time": "Тамоми давра",
"invoices": "фактура",
- "active_skus": "SKU-ҳои фаъол",
+ "active_skus": "Қисмҳои фаъол",
"units_on_hand": "Дар анбор",
"cost_value": "Арзиш (бо нархи харид)",
"sale_value": "Арзиш (бо нархи фурӯш)",
diff --git a/src/lib/server/parts.js b/src/lib/server/parts.js
index a861e19..8bc91bb 100644
--- a/src/lib/server/parts.js
+++ b/src/lib/server/parts.js
@@ -1,20 +1,21 @@
+import { randomUUID } from 'node:crypto';
import { getDb } from './db.js';
// Columns the user can sort the parts list by. Anything else is ignored.
const SORTABLE = new Set([
- 'sku', 'name_en', 'name_tg', 'quantity_on_hand',
+ 'name_en', 'name_tg', 'quantity_on_hand',
'sale_price', 'cost_price', 'reorder_level', 'updated_at'
]);
-export function listParts({ q = '', sort = 'sku', dir = 'asc', categoryIds = [] } = {}) {
+export function listParts({ q = '', sort = 'name_en', dir = 'asc', categoryIds = [] } = {}) {
const db = getDb();
- const col = SORTABLE.has(sort) ? sort : 'sku';
+ const col = SORTABLE.has(sort) ? sort : 'name_en';
const order = dir === 'desc' ? 'DESC' : 'ASC';
- const where = [];
+ const where = ['p.active = 1'];
const params = {};
if (q && q.trim()) {
- where.push(`(p.sku LIKE @q OR p.name_en LIKE @q OR p.name_tg LIKE @q OR p.barcode LIKE @q)`);
+ where.push(`(p.name_en LIKE @q OR p.name_tg LIKE @q OR p.barcode LIKE @q)`);
params.q = `%${q.trim()}%`;
}
if (categoryIds && categoryIds.length) {
@@ -22,7 +23,7 @@ export function listParts({ q = '', sort = 'sku', dir = 'asc', categoryIds = []
where.push(`p.category_id IN (${placeholders})`);
categoryIds.forEach((id, i) => { params[`cat${i}`] = id; });
}
- const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
+ const whereSql = `WHERE ${where.join(' AND ')}`;
const sql = `
SELECT p.*, c.name_en AS category_name_en, c.name_tg AS category_name_tg
@@ -59,6 +60,7 @@ export function categoriesWithParts() {
return getDb().prepare(`
SELECT c.* FROM categories c
JOIN parts p ON p.category_id = c.id
+ WHERE p.active = 1
GROUP BY c.id
ORDER BY c.sort_order, c.name_en
`).all();
@@ -66,7 +68,7 @@ export function categoriesWithParts() {
export function createPart(input) {
const db = getDb();
- const stmt = db.prepare(`
+ const insertStmt = db.prepare(`
INSERT INTO parts
(sku, name_en, name_tg, description_en, description_tg,
category_id, unit, cost_price, sale_price,
@@ -76,15 +78,28 @@ export function createPart(input) {
@category_id, @unit, @cost_price, @sale_price,
@quantity_on_hand, @reorder_level, @location, @barcode, @active)
`);
- const result = stmt.run(normalizePart(input));
- return result.lastInsertRowid;
+ const stampStmt = db.prepare(`UPDATE parts SET sku = 'SKU-' || id WHERE id = ?`);
+
+ // SKU is hidden from the UI; the user never types one. The column is still
+ // NOT NULL UNIQUE, so insert with a uuid placeholder and rewrite to SKU-{id}
+ // once we know the row id.
+ const tx = db.transaction((data) => {
+ const userSku = (data.sku || '').trim();
+ const sku = userSku || `__pending__${randomUUID()}`;
+ const result = insertStmt.run({ ...data, sku });
+ const id = result.lastInsertRowid;
+ if (!userSku) stampStmt.run(id);
+ return id;
+ });
+ return tx(normalizePart(input));
}
export function updatePart(id, input) {
const db = getDb();
+ // SKU is intentionally NOT updated here — it's hidden from the UI and frozen
+ // after creation (auto-stamped as `SKU-{id}` in createPart).
const stmt = db.prepare(`
UPDATE parts SET
- sku = @sku,
name_en = @name_en,
name_tg = @name_tg,
description_en = @description_en,
@@ -132,11 +147,17 @@ function toDirams(value) {
return Math.round(num * 100);
}
+export function deactivatePart(id) {
+ getDb()
+ .prepare(`UPDATE parts SET active = 0, updated_at = datetime('now') WHERE id = ?`)
+ .run(Number(id));
+}
+
export function lowStockParts(limit = 10) {
return getDb().prepare(`
SELECT * FROM parts
WHERE active = 1 AND quantity_on_hand <= reorder_level
- ORDER BY (quantity_on_hand - reorder_level) ASC, sku ASC
+ ORDER BY (quantity_on_hand - reorder_level) ASC, name_en ASC
LIMIT ?
`).all(limit);
}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 89ba308..31c624b 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -15,7 +15,6 @@
- | {$t('parts.sku')} |
{$t('parts.name')} |
{$t('parts.quantity_on_hand')} |
{$t('parts.reorder_level')} |
@@ -24,8 +23,7 @@
{#each lowStock as p}
- | {p.sku} |
- {localized(p, 'name', lang)} |
+ {localized(p, 'name', lang)} |
{p.quantity_on_hand} |
{p.reorder_level} |
@@ -43,7 +41,6 @@
| {$t('movements.created_at')} |
{$t('movements.type')} |
- {$t('parts.sku')} |
{$t('parts.name')} |
{$t('movements.quantity')} |
@@ -53,8 +50,7 @@
| {m.created_at} |
{$t('movements.type_' + m.movement_type)} |
- {m.sku} |
- {localized(m, 'name', lang)} |
+ {localized(m, 'name', lang)} |
{m.quantity} |
{/each}
diff --git a/src/routes/admin/reports/+page.svelte b/src/routes/admin/reports/+page.svelte
index 1d81da7..9e3483a 100644
--- a/src/routes/admin/reports/+page.svelte
+++ b/src/routes/admin/reports/+page.svelte
@@ -80,7 +80,6 @@
- | {$t('parts.sku')} |
{$t('parts.name')} |
{$t('reports.units_sold')} |
{$t('reports.sale')} |
@@ -91,8 +90,7 @@
{#each topParts as p}
- | {p.sku} |
- {localized(p, 'name', lang)} |
+ {localized(p, 'name', lang)} |
{p.units_sold} |
{formatMoney(p.sale_dirams, lang)}
diff --git a/src/routes/invoices/[id]/+page.svelte b/src/routes/invoices/[id]/+page.svelte
index 13ac0ea..31958d3 100644
--- a/src/routes/invoices/[id]/+page.svelte
+++ b/src/routes/invoices/[id]/+page.svelte
@@ -7,7 +7,7 @@
function lineLabel(line) {
if (line.affects_inventory === 0) return line.label;
- return `${line.part_sku} — ${localized({ name_en: line.part_name_en, name_tg: line.part_name_tg }, 'name', lang)}`;
+ return localized({ name_en: line.part_name_en, name_tg: line.part_name_tg }, 'name', lang);
}
diff --git a/src/routes/invoices/new/+page.svelte b/src/routes/invoices/new/+page.svelte
index 2c57c65..5dd52f0 100644
--- a/src/routes/invoices/new/+page.svelte
+++ b/src/routes/invoices/new/+page.svelte
@@ -19,7 +19,6 @@
const q = partSearch.trim().toLowerCase();
if (!q) return parts;
return parts.filter((p) =>
- (p.sku || '').toLowerCase().includes(q) ||
(p.name_en || '').toLowerCase().includes(q) ||
(p.name_tg || '').toLowerCase().includes(q) ||
(p.barcode || '').toLowerCase().includes(q)
@@ -59,7 +58,7 @@
function lineLabel(line) {
if (line.affects_inventory === 0) return line.label;
- return `${line.part_sku} — ${localized({ name_en: line.part_name_en, name_tg: line.part_name_tg }, 'name', lang)}`;
+ return localized({ name_en: line.part_name_en, name_tg: line.part_name_tg }, 'name', lang);
}
function confirmCancel(event) {
@@ -88,7 +87,7 @@
{#each visibleParts as p}
{/each}
diff --git a/src/routes/movements/new/+page.svelte b/src/routes/movements/new/+page.svelte
index ed157b4..0f1cab1 100644
--- a/src/routes/movements/new/+page.svelte
+++ b/src/routes/movements/new/+page.svelte
@@ -21,7 +21,6 @@
const q = partSearch.trim().toLowerCase();
if (!q) return parts;
return parts.filter((p) =>
- (p.sku || '').toLowerCase().includes(q) ||
(p.name_en || '').toLowerCase().includes(q) ||
(p.name_tg || '').toLowerCase().includes(q) ||
(p.barcode || '').toLowerCase().includes(q)
@@ -105,7 +104,7 @@
{#each visibleParts as p}
{/each}
diff --git a/src/routes/parts/+page.server.js b/src/routes/parts/+page.server.js
index 33babda..0e05928 100644
--- a/src/routes/parts/+page.server.js
+++ b/src/routes/parts/+page.server.js
@@ -2,7 +2,7 @@ import { listParts, categoriesWithParts } from '$lib/server/parts.js';
export function load({ url }) {
const q = url.searchParams.get('q') ?? '';
- const sort = url.searchParams.get('sort') ?? 'sku';
+ const sort = url.searchParams.get('sort') ?? 'name_en';
const dir = url.searchParams.get('dir') ?? 'asc';
const cat = url.searchParams.get('category') ?? '';
const categoryIds = cat
diff --git a/src/routes/parts/+page.svelte b/src/routes/parts/+page.svelte
index 07cfbc3..f1f1a5e 100644
--- a/src/routes/parts/+page.svelte
+++ b/src/routes/parts/+page.svelte
@@ -15,7 +15,7 @@
const params = new URLSearchParams();
if (qNext) params.set('q', qNext);
if (catsNext.length) params.set('category', catsNext.join(','));
- if (sortNext && sortNext !== 'sku') params.set('sort', sortNext);
+ if (sortNext && sortNext !== 'name_en') params.set('sort', sortNext);
if (dirNext && dirNext !== 'asc') params.set('dir', dirNext);
const target = '/parts' + (params.toString() ? '?' + params.toString() : '');
goto(target, { replaceState: true, keepFocus: true, noScroll: true });
@@ -99,7 +99,6 @@
- |
|
{$t('parts.category')} |
|
@@ -110,9 +109,8 @@
{#each parts as p}
- | {p.sku} |
- {localized(p, 'name', lang)}
+ {localized(p, 'name', lang)}
{#if !hasTranslation(p, 'name', lang)}
{$t('common.missing_translation')}
{/if}
diff --git a/src/routes/parts/[id]/+page.server.js b/src/routes/parts/[id]/+page.server.js
index 904aa46..c2274ed 100644
--- a/src/routes/parts/[id]/+page.server.js
+++ b/src/routes/parts/[id]/+page.server.js
@@ -1,5 +1,5 @@
import { error, fail, redirect } from '@sveltejs/kit';
-import { getPart, getPartBySku, listCategories, updatePart } from '$lib/server/parts.js';
+import { deactivatePart, getPart, listCategories, updatePart } from '$lib/server/parts.js';
import { recentMovementsForPart } from '$lib/server/movements.js';
export function load({ params }) {
@@ -14,21 +14,23 @@ export function load({ params }) {
}
export const actions = {
- default: async ({ request, params }) => {
+ update: async ({ request, params }) => {
const id = Number(params.id);
const form = await request.formData();
const data = Object.fromEntries(form);
const errors = {};
- if (!data.sku || !data.sku.trim()) errors.sku = 'parts.errors.sku_required';
if ((!data.name_en || !data.name_en.trim()) && (!data.name_tg || !data.name_tg.trim())) {
errors.name = 'parts.errors.name_required';
}
- const existing = getPartBySku(data.sku.trim());
- if (existing && existing.id !== id) errors.sku = 'parts.errors.sku_taken';
if (Object.keys(errors).length) return fail(400, { errors, values: data });
updatePart(id, data);
throw redirect(303, `/parts/${id}`);
+ },
+
+ delete: async ({ params }) => {
+ deactivatePart(Number(params.id));
+ throw redirect(303, '/parts');
}
};
diff --git a/src/routes/parts/[id]/+page.svelte b/src/routes/parts/[id]/+page.svelte
index 60c1c8c..1ec380e 100644
--- a/src/routes/parts/[id]/+page.svelte
+++ b/src/routes/parts/[id]/+page.svelte
@@ -1,4 +1,5 @@
- {$t('parts.edit')}: {part.sku}
+ {$t('parts.edit')}: {localized(part, 'name', lang) || part.id}
← {$t('common.back')}
@@ -27,13 +34,7 @@
+
+
| |