Add timezone preference and fix all date handling to be timezone-aware
Build and push image / build (push) Successful in 56s
Build and push image / build (push) Successful in 56s
Dates were computed using browser/server local time with no explicit timezone, causing inconsistencies when server runs in UTC. Now all "today" computations and date formatting use the user's chosen IANA timezone, persisted in localStorage and selectable from Settings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import type { Bootstrap, Item, Product } from "../types.js";
|
||||
import { TYPES, helpers, TODAY_STR } from "../types.js";
|
||||
import { TYPES, helpers } from "../types.js";
|
||||
import { getToday, getStoredTimezone } from "../tz.js";
|
||||
import { fmt, TYPE_GLYPHS } from "../format.js";
|
||||
import { Btn, Pill, Icon } from "./primitives/index.js";
|
||||
|
||||
@@ -31,11 +32,11 @@ export function ProductDetail({
|
||||
const bin = data.bins.find((b) => b.id === item.binId);
|
||||
const cfg = TYPES.find((t) => t.id === item.type);
|
||||
const product = data.products.find((p) => p.id === item.productId);
|
||||
const pctRemaining = helpers.pctRemaining(item, TODAY_STR);
|
||||
const est = helpers.estimatedRemaining(item, TODAY_STR);
|
||||
const pctRemaining = helpers.pctRemaining(item, getToday(getStoredTimezone()));
|
||||
const est = helpers.estimatedRemaining(item, getToday(getStoredTimezone()));
|
||||
const last = helpers.lastAudit(item);
|
||||
const overdue = helpers.auditOverdue(item, TODAY_STR);
|
||||
const sinceCheck = helpers.daysSinceCheck(item, TODAY_STR);
|
||||
const overdue = helpers.auditOverdue(item, getToday(getStoredTimezone()));
|
||||
const sinceCheck = helpers.daysSinceCheck(item, getToday(getStoredTimezone()));
|
||||
|
||||
const isActive = item.status === "active";
|
||||
const isCheckedOut = item.status === "checked-out";
|
||||
@@ -64,7 +65,7 @@ export function ProductDetail({
|
||||
...(cfg?.showCannabinoidPct !== false
|
||||
? [["Total cannabinoids", `${item.totalCannabinoids.toFixed(1)}%`] as [string, React.ReactNode]]
|
||||
: []),
|
||||
["Purchase date", fmt.date(item.purchaseDate)],
|
||||
["Purchase date", fmt.date(item.purchaseDate, getStoredTimezone())],
|
||||
["Bin", isCheckedOut ? "In your custody" : bin ? bin.name : <span style={{ color: "var(--ink-3)" }}>—</span>],
|
||||
["Audit cadence", `Every ${cfg?.cadenceDays ?? "—"} days · ${cfg?.auditMode ?? "—"}`],
|
||||
[
|
||||
@@ -77,11 +78,11 @@ export function ProductDetail({
|
||||
],
|
||||
];
|
||||
if (item.status === "checked-out") {
|
||||
detailRows.push(["Checked out", fmt.date(item.checkoutDate)]);
|
||||
detailRows.push(["Checked out", fmt.date(item.checkoutDate, getStoredTimezone())]);
|
||||
}
|
||||
if (item.status === "consumed") {
|
||||
detailRows.push(
|
||||
["Date finished", fmt.date(item.consumedDate)],
|
||||
["Date finished", fmt.date(item.consumedDate, getStoredTimezone())],
|
||||
[
|
||||
"Lasted",
|
||||
`${Math.round((+new Date(item.consumedDate!) - +new Date(item.purchaseDate)) / 86_400_000)} days`,
|
||||
@@ -90,7 +91,7 @@ export function ProductDetail({
|
||||
}
|
||||
if (item.status === "gone") {
|
||||
detailRows.push(
|
||||
["Date gone", fmt.date(item.goneDate)],
|
||||
["Date gone", fmt.date(item.goneDate, getStoredTimezone())],
|
||||
[
|
||||
"After",
|
||||
`${Math.round((+new Date(item.goneDate!) - +new Date(item.purchaseDate)) / 86_400_000)} days`,
|
||||
@@ -174,13 +175,13 @@ export function ProductDetail({
|
||||
{TYPE_GLYPHS[item.type]} {item.type}
|
||||
</div>
|
||||
{item.status === "consumed" && (
|
||||
<Pill tone="terra">Consumed · {fmt.daysAgo(item.consumedDate)}</Pill>
|
||||
<Pill tone="terra">Consumed · {fmt.daysAgo(item.consumedDate, getStoredTimezone())}</Pill>
|
||||
)}
|
||||
{item.status === "gone" && (
|
||||
<Pill tone="amber">Gone · {fmt.daysAgo(item.goneDate)}</Pill>
|
||||
<Pill tone="amber">Gone · {fmt.daysAgo(item.goneDate, getStoredTimezone())}</Pill>
|
||||
)}
|
||||
{isCheckedOut && (
|
||||
<Pill tone="outline">Checked out · {fmt.daysAgo(item.checkoutDate)}</Pill>
|
||||
<Pill tone="outline">Checked out · {fmt.daysAgo(item.checkoutDate, getStoredTimezone())}</Pill>
|
||||
)}
|
||||
{isActive && overdue && <Pill tone="amber">Audit overdue · {sinceCheck}d</Pill>}
|
||||
</div>
|
||||
@@ -289,7 +290,7 @@ export function ProductDetail({
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
Estimated by linear decay since last {last.mode} on {fmt.dateShort(last.date)} ({last.value}
|
||||
Estimated by linear decay since last {last.mode} on {fmt.dateShort(last.date, getStoredTimezone())} ({last.value}
|
||||
{cfg?.unit}). Re-audit to update.
|
||||
</div>
|
||||
)}
|
||||
@@ -369,7 +370,7 @@ export function ProductDetail({
|
||||
{a.mode === "presence" && (a.confirmedBy === "lost" ? "Marked lost" : "Confirmed presence")}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--ink-3)" }}>
|
||||
{fmt.date(a.date)} · {fmt.daysAgo(a.date)}
|
||||
{fmt.date(a.date, getStoredTimezone())} · {fmt.daysAgo(a.date, getStoredTimezone())}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mono" style={{ fontSize: 13, textAlign: "right" }}>
|
||||
|
||||
Reference in New Issue
Block a user