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:
109
src/routes/+page.svelte
Normal file
109
src/routes/+page.svelte
Normal file
@ -0,0 +1,109 @@
|
||||
<script>
|
||||
import { locale, t, localized, formatMoney } from '$lib/i18n/store.js';
|
||||
|
||||
export let data;
|
||||
$: lang = $locale;
|
||||
$: ({ stats, lowStock, movements } = data);
|
||||
</script>
|
||||
|
||||
<h1>{$t('dashboard.title')}</h1>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card stat">
|
||||
<div class="label">{$t('dashboard.total_skus')}</div>
|
||||
<div class="value">{stats.total}</div>
|
||||
</div>
|
||||
<div class="card stat">
|
||||
<div class="label">{$t('dashboard.low_stock')}</div>
|
||||
<div class="value" class:warn={stats.lowStock > 0}>{stats.lowStock}</div>
|
||||
</div>
|
||||
<div class="card stat">
|
||||
<div class="label">{$t('dashboard.inventory_value')}</div>
|
||||
<div class="value">
|
||||
{formatMoney(stats.inventoryValueDirams, lang)}
|
||||
<span class="cur">{$t('common.currency_short')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card quick">
|
||||
<strong>{$t('dashboard.quick_actions')}</strong>
|
||||
<a href="/parts/new">{$t('nav.new_part')}</a>
|
||||
<a href="/movements/new">{$t('nav.new_movement')}</a>
|
||||
<a href="/parts">{$t('nav.parts')}</a>
|
||||
</div>
|
||||
|
||||
<h2>{$t('dashboard.low_stock_list')}</h2>
|
||||
{#if lowStock.length === 0}
|
||||
<p class="muted">{$t('common.none')}</p>
|
||||
{:else}
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each lowStock as p}
|
||||
<tr>
|
||||
<td><a href="/parts/{p.id}">{p.sku}</a></td>
|
||||
<td>{localized(p, 'name', lang)}</td>
|
||||
<td class="num"><span class="pill low">{p.quantity_on_hand}</span></td>
|
||||
<td class="num">{p.reorder_level}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<h2>{$t('dashboard.recent_movements')}</h2>
|
||||
{#if movements.length === 0}
|
||||
<p class="muted">{$t('movements.no_movements')}</p>
|
||||
{:else}
|
||||
<table>
|
||||
<thead>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each movements as m}
|
||||
<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 class="num">{m.quantity}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.stat .label { color: #6b7388; font-size: 0.85rem; }
|
||||
.stat .value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.25rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.stat .value.warn { color: #b8443f; }
|
||||
.stat .cur { font-size: 0.85rem; color: #6b7388; margin-left: 0.25rem; }
|
||||
|
||||
.quick { display: flex; align-items: center; gap: 1rem; margin: 1rem 0; }
|
||||
.quick strong { margin-right: auto; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user