Compare commits
9 Commits
259f8d4b8f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0314e87c3f | |||
| 32331d4cf8 | |||
| a85979731f | |||
| 45ef55e13e | |||
| 0000748fe0 | |||
| a287d26b93 | |||
| d4cba18017 | |||
| 66e15dee1f | |||
| aac71becfc |
3
.gitignore
vendored
3
.gitignore
vendored
@ -12,4 +12,5 @@ backups/
|
|||||||
*.sw?
|
*.sw?
|
||||||
.session.vim
|
.session.vim
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
~*
|
*~
|
||||||
|
/*.bat
|
||||||
|
|||||||
@ -6,16 +6,6 @@ chcp 65001 >nul
|
|||||||
setlocal
|
setlocal
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
|
|
||||||
if exist "%ProgramFiles%\nodejs\node.exe" (
|
|
||||||
echo Node.js уже установлен в %ProgramFiles%\nodejs.
|
|
||||||
echo Пропускаем установку.
|
|
||||||
goto :done
|
|
||||||
)
|
|
||||||
if exist "%ProgramFiles(x86)%\nodejs\node.exe" (
|
|
||||||
echo Node.js уже установлен в %ProgramFiles(x86)%\nodejs.
|
|
||||||
echo Пропускаем установку.
|
|
||||||
goto :done
|
|
||||||
)
|
|
||||||
|
|
||||||
set "MSI=node-v16.20.2-x64.msi"
|
set "MSI=node-v16.20.2-x64.msi"
|
||||||
if /i "%PROCESSOR_ARCHITECTURE%"=="x86" if not defined PROCESSOR_ARCHITEW6432 set "MSI=node-v16.20.2-x86.msi"
|
if /i "%PROCESSOR_ARCHITECTURE%"=="x86" if not defined PROCESSOR_ARCHITEW6432 set "MSI=node-v16.20.2-x86.msi"
|
||||||
|
|||||||
@ -146,7 +146,6 @@
|
|||||||
"active_skus": "Active parts",
|
"active_skus": "Active parts",
|
||||||
"units_on_hand": "Units on hand",
|
"units_on_hand": "Units on hand",
|
||||||
"cost_value": "Value (at cost)",
|
"cost_value": "Value (at cost)",
|
||||||
"sale_value": "Value (at sale)",
|
|
||||||
"low_stock": "Low stock",
|
"low_stock": "Low stock",
|
||||||
"out_of_stock": "Out of stock",
|
"out_of_stock": "Out of stock",
|
||||||
"top_parts": "Top selling parts",
|
"top_parts": "Top selling parts",
|
||||||
|
|||||||
@ -85,3 +85,20 @@ export function formatMoney(dirams, lang = 'en') {
|
|||||||
const s = n.toFixed(2);
|
const s = n.toFixed(2);
|
||||||
return lang === 'tg' ? s.replace('.', ',') : s;
|
return lang === 'tg' ? s.replace('.', ',') : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTs(utcStr) {
|
||||||
|
if (!utcStr) return '';
|
||||||
|
|
||||||
|
const normalized = String(utcStr).trim().replace(' ', 'T');
|
||||||
|
const utcDate = new Date(`${normalized}Z`);
|
||||||
|
if (Number.isNaN(utcDate.getTime())) return utcStr;
|
||||||
|
|
||||||
|
const tajikDate = new Date(utcDate.getTime() + 5 * 60 * 60 * 1000);
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
|
||||||
|
return [
|
||||||
|
pad(tajikDate.getUTCDate()),
|
||||||
|
pad(tajikDate.getUTCMonth() + 1),
|
||||||
|
tajikDate.getUTCFullYear()
|
||||||
|
].join('.') + ` ${pad(tajikDate.getUTCHours())}:${pad(tajikDate.getUTCMinutes())}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -146,7 +146,6 @@
|
|||||||
"active_skus": "Қисмҳои фаъол",
|
"active_skus": "Қисмҳои фаъол",
|
||||||
"units_on_hand": "Дар анбор",
|
"units_on_hand": "Дар анбор",
|
||||||
"cost_value": "Арзиш (бо нархи харид)",
|
"cost_value": "Арзиш (бо нархи харид)",
|
||||||
"sale_value": "Арзиш (бо нархи фурӯш)",
|
|
||||||
"low_stock": "Захираи кам",
|
"low_stock": "Захираи кам",
|
||||||
"out_of_stock": "Тамом шуд",
|
"out_of_stock": "Тамом шуд",
|
||||||
"top_parts": "Қисмҳои серфурӯш",
|
"top_parts": "Қисмҳои серфурӯш",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { getDb } from './db.js';
|
import { getDb } from './db.js';
|
||||||
|
|
||||||
// All time windows are computed in local time using SQLite's `datetime('now', 'localtime')`.
|
// All time windows are computed in Tajikistan time (UTC+5) while timestamps are stored as UTC.
|
||||||
// Cost of goods (COG) and profit are computed against each part's current cost_price —
|
// Cost of goods (COG) and profit are computed against each part's current cost_price —
|
||||||
// the schema does not snapshot cost at sale time, so historical cost changes are not
|
// the schema does not snapshot cost at sale time, so historical cost changes are not
|
||||||
// reflected. Custom (non-inventory) lines contribute to sale revenue but have zero COG.
|
// reflected. Custom (non-inventory) lines contribute to sale revenue but have zero COG.
|
||||||
@ -29,9 +29,9 @@ function windowStats(dateClause) {
|
|||||||
export function salesSummary() {
|
export function salesSummary() {
|
||||||
return {
|
return {
|
||||||
all_time: windowStats(''),
|
all_time: windowStats(''),
|
||||||
today: windowStats(`date(saved_at, 'localtime') = date('now', 'localtime')`),
|
today: windowStats(`date(saved_at, '+5 hours') = date('now', '+5 hours')`),
|
||||||
week: windowStats(`date(saved_at, 'localtime') >= date('now', 'localtime', '-6 days')`),
|
week: windowStats(`date(saved_at, '+5 hours') >= date('now', '+5 hours', '-6 days')`),
|
||||||
month: windowStats(`strftime('%Y-%m', saved_at, 'localtime') = strftime('%Y-%m', 'now', 'localtime')`)
|
month: windowStats(`strftime('%Y-%m', saved_at, '+5 hours') = strftime('%Y-%m', 'now', '+5 hours')`)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { locale, t, localized } from '$lib/i18n/store.js';
|
import { locale, t, localized, formatTs } from '$lib/i18n/store.js';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
$: lang = $locale;
|
$: lang = $locale;
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{#each movements as m}
|
{#each movements as m}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{m.created_at}</td>
|
<td>{formatTs(m.created_at)}</td>
|
||||||
<td><span class="pill">{$t('movements.type_' + m.movement_type)}</span></td>
|
<td><span class="pill">{$t('movements.type_' + m.movement_type)}</span></td>
|
||||||
<td><a href="/parts/{m.part_id}">{localized(m, 'name', lang)}</a></td>
|
<td><a href="/parts/{m.part_id}">{localized(m, 'name', lang)}</a></td>
|
||||||
<td class="num">{m.quantity}</td>
|
<td class="num">{m.quantity}</td>
|
||||||
|
|||||||
@ -1,16 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { locale, t, localized, formatMoney } from '$lib/i18n/store.js';
|
import { locale, t, localized, formatMoney, formatTs } from '$lib/i18n/store.js';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
$: lang = $locale;
|
$: lang = $locale;
|
||||||
$: ({ sales, topParts, inventory, recentSales } = data);
|
$: ({ sales, topParts, inventory, recentSales } = data);
|
||||||
|
|
||||||
function formatWhen(iso) {
|
|
||||||
if (!iso) return '';
|
|
||||||
const d = new Date(iso.replace(' ', 'T') + 'Z');
|
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h2>{$t('reports.sales_heading')}</h2>
|
<h2>{$t('reports.sales_heading')}</h2>
|
||||||
@ -56,13 +50,7 @@
|
|||||||
<span class="cur">{$t('common.currency_short')}</span>
|
<span class="cur">{$t('common.currency_short')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card stat">
|
<div></div>
|
||||||
<div class="label">{$t('reports.sale_value')}</div>
|
|
||||||
<div class="value">
|
|
||||||
{formatMoney(inventory.sale_value_dirams, lang)}
|
|
||||||
<span class="cur">{$t('common.currency_short')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card stat">
|
<div class="card stat">
|
||||||
<div class="label">{$t('reports.low_stock')}</div>
|
<div class="label">{$t('reports.low_stock')}</div>
|
||||||
<div class="value" class:warn={inventory.lowStockCount > 0}>{inventory.lowStockCount}</div>
|
<div class="value" class:warn={inventory.lowStockCount > 0}>{inventory.lowStockCount}</div>
|
||||||
@ -128,7 +116,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{#each recentSales as s}
|
{#each recentSales as s}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{formatWhen(s.saved_at)}</td>
|
<td>{formatTs(s.saved_at)}</td>
|
||||||
<td class="num">{s.line_count}</td>
|
<td class="num">{s.line_count}</td>
|
||||||
<td class="num">
|
<td class="num">
|
||||||
{formatMoney(s.sale_dirams, lang)}
|
{formatMoney(s.sale_dirams, lang)}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { locale, t, localized, formatMoney } from '$lib/i18n/store.js';
|
import { locale, t, localized, formatMoney, formatTs } from '$lib/i18n/store.js';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
$: lang = $locale;
|
$: lang = $locale;
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<header class="head">
|
<header class="head">
|
||||||
<div>
|
<div>
|
||||||
<h1>{$t('invoices.saved_title')} #{invoice.id}</h1>
|
<h1>{$t('invoices.saved_title')} #{invoice.id}</h1>
|
||||||
<p class="muted">{invoice.saved_at}</p>
|
<p class="muted">{formatTs(invoice.saved_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/invoices/new" class="print-hide back">← {$t('invoices.new_another')}</a>
|
<a href="/invoices/new" class="print-hide back">← {$t('invoices.new_another')}</a>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { locale, t, localized, formatMoney } from '$lib/i18n/store.js';
|
import { locale, t, localized, formatMoney, formatTs } from '$lib/i18n/store.js';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
export let form;
|
export let form;
|
||||||
@ -117,8 +117,8 @@
|
|||||||
{$t('parts.reorder_level')}: {part.reorder_level}
|
{$t('parts.reorder_level')}: {part.reorder_level}
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="muted small">{$t('common.created')}: {part.created_at}</div>
|
<div class="muted small">{$t('common.created')}: {formatTs(part.created_at)}</div>
|
||||||
<div class="muted small">{$t('common.updated')}: {part.updated_at}</div>
|
<div class="muted small">{$t('common.updated')}: {formatTs(part.updated_at)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{$t('parts.recent_movements')}</h2>
|
<h2>{$t('parts.recent_movements')}</h2>
|
||||||
@ -137,7 +137,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{#each movements as m}
|
{#each movements as m}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{m.created_at}</td>
|
<td>{formatTs(m.created_at)}</td>
|
||||||
<td><span class="pill">{$t('movements.type_' + m.movement_type)}</span></td>
|
<td><span class="pill">{$t('movements.type_' + m.movement_type)}</span></td>
|
||||||
<td class="num">{m.quantity > 0 ? '+' : ''}{m.quantity}</td>
|
<td class="num">{m.quantity > 0 ? '+' : ''}{m.quantity}</td>
|
||||||
<td class="num">{m.unit_price != null ? formatMoney(m.unit_price, lang) : $t('common.none')}</td>
|
<td class="num">{m.unit_price != null ? formatMoney(m.unit_price, lang) : $t('common.none')}</td>
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<label>
|
<label>
|
||||||
{$t('parts.unit')}
|
{$t('parts.unit')}
|
||||||
<input name="unit" value={values.unit ?? 'pcs'} />
|
<input name="unit" value={values.unit ?? 'liter'} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{$t('parts.reorder_level')}
|
{$t('parts.reorder_level')}
|
||||||
|
|||||||
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Reference in New Issue
Block a user