use tajik timezone for displaying dates

This commit is contained in:
2026-06-17 20:40:43 +05:00
parent 259f8d4b8f
commit a85979731f
7 changed files with 168 additions and 20 deletions

View File

@ -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())}`;
}

View File

@ -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')`)
}; };
} }

View File

@ -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>

View File

@ -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>
@ -128,7 +122,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)}

View File

@ -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>

View File

@ -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>

137
timestamp-tz.patch Normal file
View File

@ -0,0 +1,137 @@
Format timestamps as Asia/Dushanbe (UTC+5)
SQLite stores all timestamps as UTC via datetime('now'). The UI was
rendering them raw (e.g. "2026-05-18 15:42:03"), which is 5 hours
behind local time for Tajikistan.
Add formatTs() to the i18n store alongside formatMoney/localized.
It appends 'Z' so JS Date treats the SQLite string as UTC, then
formats with Intl using the Asia/Dushanbe timezone (ru-RU locale).
Replace all bare timestamp renders on:
- / (dashboard: recent movements created_at)
- /parts/[id] (part created_at, updated_at, movement history)
- /invoices/[id] (invoice saved_at on receipt)
- /admin/reports (recent sales saved_at; replaces local formatWhen)
Tested: UTC "2026-05-18 15:42:03" -> "18.05.2026, 20:42" (pass)
UTC "2026-05-18 00:30:00" -> "18.05.2026, 05:30" (pass)
null/empty/invalid input handled gracefully (pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
src/lib/i18n/store.js | 15 +++++++++++++++
src/routes/+page.svelte | 2 +-
src/routes/admin/reports/+page.svelte | 9 +--------
src/routes/invoices/[id]/+page.svelte | 2 +-
src/routes/parts/[id]/+page.svelte | 4 ++--
5 files changed, 20 insertions(+), 12 deletions(-)
diff --git a/src/lib/i18n/store.js b/src/lib/i18n/store.js
--- a/src/lib/i18n/store.js
+++ b/src/lib/i18n/store.js
@@ -1,3 +1,18 @@
+// formatTs: convert a SQLite UTC timestamp string ("YYYY-MM-DD HH:MM:SS")
+// to a human-readable local time in Asia/Dushanbe (UTC+5).
+// The appended 'Z' is critical — without it JS parses as local time.
+export function formatTs(utcStr) {
+ if (!utcStr) return '';
+ const d = new Date(utcStr.replace(' ', 'T') + 'Z');
+ if (isNaN(d.getTime())) return utcStr;
+ return d.toLocaleString('ru-RU', {
+ timeZone: 'Asia/Dushanbe',
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+}
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,6 +1,6 @@
<script>
- import { locale, t, localized } from '$lib/i18n/store.js';
+ import { locale, t, localized, formatTs } from '$lib/i18n/store.js';
export let data;
$: lang = $locale;
@@ -48,7 +48,7 @@
{#each movements as m}
<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><a href="/parts/{m.part_id}">{localized(m, 'name', lang)}</a></td>
<td class="num">{m.quantity}</td>
diff --git a/src/routes/admin/reports/+page.svelte b/src/routes/admin/reports/+page.svelte
--- a/src/routes/admin/reports/+page.svelte
+++ b/src/routes/admin/reports/+page.svelte
@@ -1,6 +1,6 @@
<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;
$: lang = $locale;
$: ({ 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>
@@ -139,7 +132,7 @@
{#each recentSales as s}
<tr>
- <td>{formatWhen(s.saved_at)}</td>
+ <td>{formatTs(s.saved_at)}</td>
<td class="num">{s.line_count}</td>
diff --git a/src/routes/invoices/[id]/+page.svelte b/src/routes/invoices/[id]/+page.svelte
--- a/src/routes/invoices/[id]/+page.svelte
+++ b/src/routes/invoices/[id]/+page.svelte
@@ -1,6 +1,6 @@
<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;
$: lang = $locale;
@@ -16,7 +16,7 @@
<header class="head">
<div>
<h1>{$t('invoices.saved_title')} #{invoice.id}</h1>
- <p class="muted">{invoice.saved_at}</p>
+ <p class="muted">{formatTs(invoice.saved_at)}</p>
</div>
diff --git a/src/routes/parts/[id]/+page.svelte b/src/routes/parts/[id]/+page.svelte
--- a/src/routes/parts/[id]/+page.svelte
+++ b/src/routes/parts/[id]/+page.svelte
@@ -1,7 +1,7 @@
<script>
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 form;
@@ -115,8 +115,8 @@
<hr />
- <div class="muted small">{$t('common.created')}: {part.created_at}</div>
- <div class="muted small">{$t('common.updated')}: {part.updated_at}</div>
+ <div class="muted small">{$t('common.created')}: {formatTs(part.created_at)}</div>
+ <div class="muted small">{$t('common.updated')}: {formatTs(part.updated_at)}</div>
</div>
@@ -131,7 +131,7 @@
{#each movements as m}
<tr>
- <td>{m.created_at}</td>
+ <td>{formatTs(m.created_at)}</td>
<td><span class="pill">{$t('movements.type_' + m.movement_type)}</span></td>