admin: fix timestamps, per-day stats, reset, delete drinks/bars
- store created_at as UTC ISO; display/group in Europe/Berlin - stats grouped by business day (sales night past midnight, 5am cutoff) - add "Statistik zurücksetzen" - allow deleting drinks/tresen (refused if referenced by sales) - CSV exports local wall-clock time
This commit is contained in:
parent
22577a0a65
commit
40cdc98f82
4 changed files with 210 additions and 9 deletions
78
server/src/time.ts
Normal file
78
server/src/time.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Time handling for the sales day.
|
||||
//
|
||||
// Timestamps are stored in the DB as UTC ISO strings (`new Date().toISOString()`).
|
||||
// For display and for grouping into business days we convert to a fixed local
|
||||
// timezone (default Europe/Berlin) so the numbers match the bar's wall clock
|
||||
// regardless of where the server runs.
|
||||
|
||||
export const TZ = process.env.WUTZ_TZ ?? 'Europe/Berlin';
|
||||
|
||||
// A sale at e.g. 03:00 still belongs to the previous night's business day.
|
||||
// Anything before this local hour counts towards the day before.
|
||||
export const BUSINESS_DAY_CUTOFF_HOUR = Number(process.env.WUTZ_DAY_CUTOFF_HOUR ?? 5);
|
||||
|
||||
/**
|
||||
* Parse a value stored in `created_at`. New rows are UTC ISO strings (with `Z`),
|
||||
* but legacy rows written by SQLite's CURRENT_TIMESTAMP look like
|
||||
* `YYYY-MM-DD HH:MM:SS` and are also UTC — normalise both to a Date.
|
||||
*/
|
||||
export function parseDbTime(s: string): Date {
|
||||
if (/[zZ]|[+-]\d{2}:\d{2}$/.test(s)) return new Date(s);
|
||||
return new Date(s.replace(' ', 'T') + 'Z');
|
||||
}
|
||||
|
||||
interface LocalParts {
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
hour: number;
|
||||
minute: number;
|
||||
second: number;
|
||||
}
|
||||
|
||||
function localParts(d: Date, tz: string = TZ): LocalParts {
|
||||
const fmt = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: tz,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
const parts: Record<string, string> = {};
|
||||
for (const p of fmt.formatToParts(d)) {
|
||||
if (p.type !== 'literal') parts[p.type] = p.value;
|
||||
}
|
||||
return {
|
||||
year: Number(parts.year),
|
||||
month: Number(parts.month),
|
||||
day: Number(parts.day),
|
||||
hour: Number(parts.hour === '24' ? '0' : parts.hour),
|
||||
minute: Number(parts.minute),
|
||||
second: Number(parts.second),
|
||||
};
|
||||
}
|
||||
|
||||
/** Local wall-clock time as `YYYY-MM-DD HH:MM:SS` for CSV / display. */
|
||||
export function formatLocal(d: Date, tz: string = TZ): string {
|
||||
const p = localParts(d, tz);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${p.year}-${pad(p.month)}-${pad(p.day)} ${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The business day a timestamp belongs to, as `YYYY-MM-DD`.
|
||||
* Hours before BUSINESS_DAY_CUTOFF_HOUR are attributed to the previous day.
|
||||
*/
|
||||
export function businessDay(d: Date, tz: string = TZ): string {
|
||||
const p = localParts(d, tz);
|
||||
// Build a date from the local Y-M-D and roll back a day when before cutoff.
|
||||
const date = new Date(Date.UTC(p.year, p.month - 1, p.day));
|
||||
if (p.hour < BUSINESS_DAY_CUTOFF_HOUR) {
|
||||
date.setUTCDate(date.getUTCDate() - 1);
|
||||
}
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
||||
}
|
||||
Loading…
Reference in a new issue