Initial scaffold for AvtoAmbor parts inventory

SvelteKit 2 + Svelte 4 + adapter-node, SQLite via better-sqlite3 (WAL,
foreign keys on). Bilingual EN/Тоҷикӣ throughout, locale persisted in
localStorage.

Pages: dashboard (totals, low stock, recent movements), parts list with
search and sort, part create/edit, record movement (in/out/adjust with
smart unit-price and adjust-quantity prefill), suppliers list with
inline add.

Schema: categories, suppliers, parts (with _en/_tg name+description
columns, dirams for money), stock_movements with check on movement_type.
On-hand updates are done in JS inside a transaction with the movement
insert.

Dockerized dev: docker compose, named project, bind-mounted data/ for
DB persistence. Seed contains 6 categories, 4 suppliers, 31 realistic
parts (Lada / Nexia / Opel / Toyota bias).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
David Beccue
2026-05-16 07:05:24 +05:00
commit 05be5b03aa
37 changed files with 4617 additions and 0 deletions

View File

@ -0,0 +1,46 @@
import { fail, redirect } from '@sveltejs/kit';
import { createPart, getPartBySku, listCategories } from '$lib/server/parts.js';
import { recordMovement } from '$lib/server/movements.js';
export function load() {
return { categories: listCategories() };
}
export const actions = {
default: async ({ request }) => {
const form = await request.formData();
const data = Object.fromEntries(form);
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.
const initialQty = Number(data.quantity_on_hand || 0);
const id = createPart({ ...data, quantity_on_hand: 0 });
if (initialQty > 0) {
recordMovement({
part_id: id,
movement_type: 'in',
quantity: initialQty,
unit_price: data.cost_price,
reference: 'OPENING'
});
}
throw redirect(303, `/parts/${id}`);
}
};
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';
}
return Object.keys(errors).length ? errors : null;
}