Soft-delete parts and hide SKU/location/description from the UI
Deleting a part used to be impossible. Hard delete would cascade
stock_movements (FK ON DELETE CASCADE) and orphan invoice_lines, losing
the audit trail. Instead, the part detail page now has a Delete button
that flips active=0; listParts and categoriesWithParts filter on active,
but historical joins (recentMovements, linesFor, topSellingParts) stay
unfiltered so old movements and invoices still render the part name.
The existing active checkbox on the detail page doubles as a reactivate
switch.
SKU, location, and description fields are removed from every UI surface
(forms, /parts table, dashboard, movement/invoice pickers, invoice line
labels, top-sellers report). None were load-bearing — barcode + name +
category already cover lookup. The SKU column is kept in the DB
(NOT NULL UNIQUE) and auto-stamped server-side as `SKU-{id}` after
insert, so the change is reversible without a migration. updatePart no
longer writes SKU, freezing it after creation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -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)",
|
||||
|
||||
@ -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": "Арзиш (бо нархи фурӯш)",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t('parts.sku')}</th>
|
||||
<th>{$t('parts.name')}</th>
|
||||
<th class="num">{$t('parts.quantity_on_hand')}</th>
|
||||
<th class="num">{$t('parts.reorder_level')}</th>
|
||||
@ -24,8 +23,7 @@
|
||||
<tbody>
|
||||
{#each lowStock as p}
|
||||
<tr>
|
||||
<td><a href="/parts/{p.id}">{p.sku}</a></td>
|
||||
<td>{localized(p, 'name', lang)}</td>
|
||||
<td><a href="/parts/{p.id}">{localized(p, 'name', lang)}</a></td>
|
||||
<td class="num"><span class="pill low">{p.quantity_on_hand}</span></td>
|
||||
<td class="num">{p.reorder_level}</td>
|
||||
</tr>
|
||||
@ -43,7 +41,6 @@
|
||||
<tr>
|
||||
<th>{$t('movements.created_at')}</th>
|
||||
<th>{$t('movements.type')}</th>
|
||||
<th>{$t('parts.sku')}</th>
|
||||
<th>{$t('parts.name')}</th>
|
||||
<th class="num">{$t('movements.quantity')}</th>
|
||||
</tr>
|
||||
@ -53,8 +50,7 @@
|
||||
<tr>
|
||||
<td>{m.created_at}</td>
|
||||
<td><span class="pill">{$t('movements.type_' + m.movement_type)}</span></td>
|
||||
<td><a href="/parts/{m.part_id}">{m.sku}</a></td>
|
||||
<td>{localized(m, 'name', lang)}</td>
|
||||
<td><a href="/parts/{m.part_id}">{localized(m, 'name', lang)}</a></td>
|
||||
<td class="num">{m.quantity}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
@ -80,7 +80,6 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t('parts.sku')}</th>
|
||||
<th>{$t('parts.name')}</th>
|
||||
<th class="num">{$t('reports.units_sold')}</th>
|
||||
<th class="num">{$t('reports.sale')}</th>
|
||||
@ -91,8 +90,7 @@
|
||||
<tbody>
|
||||
{#each topParts as p}
|
||||
<tr>
|
||||
<td><a href="/parts/{p.id}">{p.sku}</a></td>
|
||||
<td>{localized(p, 'name', lang)}</td>
|
||||
<td><a href="/parts/{p.id}">{localized(p, 'name', lang)}</a></td>
|
||||
<td class="num">{p.units_sold}</td>
|
||||
<td class="num">
|
||||
{formatMoney(p.sale_dirams, lang)}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -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 @@
|
||||
<option value="">—</option>
|
||||
{#each visibleParts as p}
|
||||
<option value={String(p.id)}>
|
||||
{p.sku} — {localized(p, 'name', lang)} ({$t('parts.quantity_on_hand')}: {p.quantity_on_hand})
|
||||
{localized(p, 'name', lang)} ({$t('parts.quantity_on_hand')}: {p.quantity_on_hand})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
@ -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 @@
|
||||
<option value="">—</option>
|
||||
{#each visibleParts as p}
|
||||
<option value={String(p.id)}>
|
||||
{p.sku} — {localized(p, 'name', lang)} ({$t('parts.quantity_on_hand')}: {p.quantity_on_hand})
|
||||
{localized(p, 'name', lang)} ({$t('parts.quantity_on_hand')}: {p.quantity_on_hand})
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><button class="th-btn" on:click={() => sortBy('sku')}>{$t('parts.sku')} {arrow('sku')}</button></th>
|
||||
<th><button class="th-btn" on:click={() => sortBy(lang === 'tg' ? 'name_tg' : 'name_en')}>{$t('parts.name')} {arrow(lang === 'tg' ? 'name_tg' : 'name_en')}</button></th>
|
||||
<th>{$t('parts.category')}</th>
|
||||
<th class="num"><button class="th-btn" on:click={() => sortBy('quantity_on_hand')}>{$t('parts.quantity_on_hand')} {arrow('quantity_on_hand')}</button></th>
|
||||
@ -110,9 +109,8 @@
|
||||
<tbody>
|
||||
{#each parts as p}
|
||||
<tr>
|
||||
<td><a href="/parts/{p.id}">{p.sku}</a></td>
|
||||
<td>
|
||||
{localized(p, 'name', lang)}
|
||||
<a href="/parts/{p.id}">{localized(p, 'name', lang)}</a>
|
||||
{#if !hasTranslation(p, 'name', lang)}
|
||||
<em class="missing">{$t('common.missing_translation')}</em>
|
||||
{/if}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { enhance } from '$app/forms';
|
||||
import { locale, t, localized, formatMoney } from '$lib/i18n/store.js';
|
||||
|
||||
export let data;
|
||||
@ -6,6 +7,12 @@
|
||||
$: lang = $locale;
|
||||
$: ({ part, categories, movements } = data);
|
||||
|
||||
function confirmDelete(event) {
|
||||
const name = localized(part, 'name', lang) || String(part.id);
|
||||
const message = $t('parts.delete_confirm').replace('{name}', name);
|
||||
if (!confirm(message)) event.preventDefault();
|
||||
}
|
||||
|
||||
$: errors = form?.errors ?? {};
|
||||
$: values = form?.values ?? {};
|
||||
|
||||
@ -17,7 +24,7 @@
|
||||
</script>
|
||||
|
||||
<div class="page-head">
|
||||
<h1>{$t('parts.edit')}: {part.sku}</h1>
|
||||
<h1>{$t('parts.edit')}: {localized(part, 'name', lang) || part.id}</h1>
|
||||
<a href="/parts" class="muted">← {$t('common.back')}</a>
|
||||
</div>
|
||||
|
||||
@ -27,13 +34,7 @@
|
||||
|
||||
<div class="layout">
|
||||
<section>
|
||||
<form class="stack" method="POST">
|
||||
<label>
|
||||
{$t('parts.sku')} *
|
||||
<input name="sku" required value={values.sku ?? part.sku} />
|
||||
{#if errors.sku}<span class="field-error">{$t(errors.sku)}</span>{/if}
|
||||
</label>
|
||||
|
||||
<form class="stack" method="POST" action="?/update">
|
||||
<div class="row">
|
||||
<label>
|
||||
{$t('parts.name_en')}
|
||||
@ -83,27 +84,10 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>
|
||||
{$t('parts.location')}
|
||||
<input name="location" value={values.location ?? part.location ?? ''} />
|
||||
</label>
|
||||
<label>
|
||||
{$t('parts.barcode')}
|
||||
<input name="barcode" value={values.barcode ?? part.barcode ?? ''} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label>
|
||||
{$t('parts.description_en')}
|
||||
<textarea name="description_en">{values.description_en ?? part.description_en ?? ''}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
{$t('parts.description_tg')}
|
||||
<textarea name="description_tg">{values.description_tg ?? part.description_tg ?? ''}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
{$t('parts.barcode')}
|
||||
<input name="barcode" value={values.barcode ?? part.barcode ?? ''} />
|
||||
</label>
|
||||
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="active" value="1"
|
||||
@ -116,6 +100,10 @@
|
||||
<a class="btn-link" href="/movements/new?part_id={part.id}">+ {$t('nav.new_movement')}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="?/delete" class="delete-form" use:enhance on:submit={confirmDelete}>
|
||||
<button type="submit" class="danger">{$t('common.delete')}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<aside>
|
||||
@ -186,6 +174,11 @@
|
||||
.btn-link:hover { background: #00553e; color: #fff; }
|
||||
.checkbox { display: flex; align-items: center; gap: 0.4rem; }
|
||||
.field-error { color: #8a1f1b; font-size: 0.8rem; }
|
||||
.delete-form {
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eef0f5;
|
||||
}
|
||||
|
||||
.qty { font-size: 2rem; font-weight: 700; margin: 0.25rem 0; }
|
||||
.qty.low { color: #b8443f; }
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { createPart, getPartBySku, listCategories } from '$lib/server/parts.js';
|
||||
import { createPart, listCategories } from '$lib/server/parts.js';
|
||||
import { recordMovement } from '$lib/server/movements.js';
|
||||
|
||||
export function load() {
|
||||
@ -13,10 +13,6 @@ export const actions = {
|
||||
const errors = validate(data);
|
||||
if (errors) return fail(400, { errors, values: data });
|
||||
|
||||
if (getPartBySku(data.sku.trim())) {
|
||||
return fail(400, { errors: { sku: 'parts.errors.sku_taken' }, values: data });
|
||||
}
|
||||
|
||||
// Save the part with quantity 0, then record an opening "in" movement
|
||||
// if the user supplied an initial quantity. This keeps quantity changes
|
||||
// funneled exclusively through stock_movements.
|
||||
@ -38,7 +34,6 @@ export const actions = {
|
||||
|
||||
function validate(d) {
|
||||
const errors = {};
|
||||
if (!d.sku || !d.sku.trim()) errors.sku = 'parts.errors.sku_required';
|
||||
if ((!d.name_en || !d.name_en.trim()) && (!d.name_tg || !d.name_tg.trim())) {
|
||||
errors.name = 'parts.errors.name_required';
|
||||
}
|
||||
|
||||
@ -17,12 +17,6 @@
|
||||
{/if}
|
||||
|
||||
<form class="stack" method="POST">
|
||||
<label>
|
||||
{$t('parts.sku')} *
|
||||
<input name="sku" required value={values.sku ?? ''} />
|
||||
{#if errors.sku}<span class="field-error">{$t(errors.sku)}</span>{/if}
|
||||
</label>
|
||||
|
||||
<div class="row">
|
||||
<label>
|
||||
{$t('parts.name_en')}
|
||||
@ -74,24 +68,8 @@
|
||||
<input name="quantity_on_hand" type="number" min="0" step="1" value={values.quantity_on_hand ?? 0} />
|
||||
</label>
|
||||
<label>
|
||||
{$t('parts.location')}
|
||||
<input name="location" value={values.location ?? ''} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
{$t('parts.barcode')}
|
||||
<input name="barcode" value={values.barcode ?? ''} />
|
||||
</label>
|
||||
|
||||
<div class="row">
|
||||
<label>
|
||||
{$t('parts.description_en')}
|
||||
<textarea name="description_en">{values.description_en ?? ''}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
{$t('parts.description_tg')}
|
||||
<textarea name="description_tg">{values.description_tg ?? ''}</textarea>
|
||||
{$t('parts.barcode')}
|
||||
<input name="barcode" value={values.barcode ?? ''} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user