diff --git a/src/App.tsx b/src/App.tsx index 7b6e955..103344e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,6 @@ import { createEntries, createEntry, deleteEntry as deleteEntryDocument, - isDuplicateDraft, listEntries, updateEntry, } from "./lib/appwriteEntries"; @@ -52,7 +51,7 @@ import { } from "./lib/userLimits"; import { createExcelExport, downloadBlob, parseExcelImport } from "./lib/excel"; import { buildEntryDerived, groupByDay, groupByFlavour, groupByWeek, humanDateTime, makeId } from "./lib/metrics"; -import { exportPayload, parseImport } from "./lib/storage"; +import { buildJsonImportPreview, exportPayload, parseImport } from "./lib/storage"; import { useToasts } from "./lib/toasts"; import type { AppView, @@ -547,18 +546,18 @@ function App() { } } - async function confirmExcelImport() { + async function confirmImportPreview() { if (!user || !importPreview) return; const drafts = importPreview.rows .filter((row) => row.entry && !row.errors.length && !row.duplicate) .map((row) => row.entry as EntryDraft); if (!drafts.length) { - setNotice("No valid new Excel rows to import."); + setNotice("No valid new rows to import."); return; } - setBusyAction("confirm-excel-import"); + setBusyAction("confirm-import"); setSyncError(""); importInFlightRef.current = true; setImportProgress({ done: 0, total: drafts.length }); @@ -566,7 +565,7 @@ function App() { const saved = await createEntries(user.$id, drafts, (done, total) => setImportProgress({ done, total })); setEntries((current) => sortEntries([...saved, ...current])); setImportPreview(null); - setNotice(`${saved.length} Excel row${saved.length === 1 ? "" : "s"} saved to Appwrite.`); + setNotice(`${saved.length} row${saved.length === 1 ? "" : "s"} saved to Appwrite.`); } catch (error) { setSyncError(appwriteErrorMessage(error)); } finally { @@ -588,18 +587,12 @@ function App() { setSyncError(""); try { const drafts = parseImport(await file.text()); - const uniqueDrafts = drafts.filter((draft) => !isDuplicateDraft(entries, draft)); - if (!uniqueDrafts.length) { - setNotice("No new JSON entries found."); - return; - } - importInFlightRef.current = true; - const saved = await createEntries(user.$id, uniqueDrafts.map((draft) => ({ ...draft, source: "json" }))); - setEntries((current) => sortEntries([...saved, ...current])); setNotice(`${saved.length} JSON entr${saved.length === 1 ? "y" : "ies"} saved to Appwrite.`); + const preview = buildJsonImportPreview(drafts, entries, file.name); + setImportPreview(preview); + setNotice(`${preview.rows.length} JSON row${preview.rows.length === 1 ? "" : "s"} parsed for review.`); } catch (error) { setSyncError(error instanceof Error ? error.message : "JSON import failed."); } finally { - importInFlightRef.current = false; if (jsonFileInputRef.current) jsonFileInputRef.current.value = ""; setBusyAction(null); } @@ -810,11 +803,11 @@ function App() { /> setImportPreview(null)} - onConfirm={() => void confirmExcelImport()} + onConfirm={() => void confirmImportPreview()} />
-

Excel import

+

+ {preview.fileName.toLowerCase().endsWith(".json") ? "JSON import" : "Excel import"} +

Preview rows

diff --git a/src/components/LimitsSettingsForm.tsx b/src/components/LimitsSettingsForm.tsx index 4d48085..3e74037 100644 --- a/src/components/LimitsSettingsForm.tsx +++ b/src/components/LimitsSettingsForm.tsx @@ -142,7 +142,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett value={canInput} onChange={(event) => handleCanInputChange(event.target.value)} /> - Leave empty to remove. Counts use BST calendar days. + Leave empty to remove. Counts use device-local calendar days. {previewParts.length ? ( diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index c246cb4..d3e8860 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -61,7 +61,7 @@ export function SettingsView({ return (
- + entry.importKey || makeImportKey(entry))); + const rows: ImportPreviewRow[] = drafts.map((draft, index) => { + const key = makeImportKey({ + ...draft, + dateTime: new Date(draft.dateTime).toISOString(), + notes: draft.notes ?? "", + store: draft.store ?? "", + }); + const duplicate = seen.has(key); + if (!duplicate) seen.add(key); + return { + rowNumber: index + 1, + entry: draft, + errors: [], + duplicate, + duplicateReason: duplicate ? "Matches an existing or earlier imported row." : undefined, + }; + }); + return { fileName, rows }; +} + function coerceEntryDraft(value: unknown): EntryDraft | null { if (!value || typeof value !== "object") return null; const entry = value as Partial; diff --git a/src/lib/userLimits.ts b/src/lib/userLimits.ts index 0997bb3..da08095 100644 --- a/src/lib/userLimits.ts +++ b/src/lib/userLimits.ts @@ -1,6 +1,6 @@ import type { EntryDraft, LimitCheckResult, LimitViolation, RedBullEntry, UserLimits } from "../types"; import { getBstHour } from "./greeting"; -import { currency, spendFor, sum } from "./metrics"; +import { currency, formatDateKey, spendFor, sum } from "./metrics"; export const DEFAULT_LIMITS: UserLimits = {}; @@ -59,25 +59,11 @@ export function mergePrefsWithLimits( } export function formatBstDateKey(date = new Date()) { - return new Intl.DateTimeFormat("en-CA", { - timeZone: "Europe/London", - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(date); + return formatDateKey(date); } export function getBstMinutes(date = new Date()) { - const parts = new Intl.DateTimeFormat("en-GB", { - timeZone: "Europe/London", - hour: "numeric", - minute: "numeric", - hour12: false, - }).formatToParts(date); - - const hour = Number(parts.find((part) => part.type === "hour")?.value ?? 0); - const minute = Number(parts.find((part) => part.type === "minute")?.value ?? 0); - return hour * 60 + minute; + return date.getHours() * 60 + date.getMinutes(); } export function parseStopTimeMinutes(stopTime: string) { @@ -101,7 +87,7 @@ export function formatStopTimeLabel(stopTime: string) { }).format(date); } -function entriesTodayBst(entries: RedBullEntry[], ref = new Date()) { +function entriesToday(entries: RedBullEntry[], ref = new Date()) { const key = formatBstDateKey(ref); return entries.filter((entry) => formatBstDateKey(new Date(entry.dateTime)) === key); } @@ -111,7 +97,7 @@ function spendForDraft(draft: EntryDraft) { } function todayTotals(entries: RedBullEntry[], excludeEntryId?: string, ref = new Date()) { - const todayEntries = entriesTodayBst(entries, ref).filter((entry) => entry.id !== excludeEntryId); + const todayEntries = entriesToday(entries, ref).filter((entry) => entry.id !== excludeEntryId); return { todayCans: sum(todayEntries, (entry) => entry.cans), todaySpend: sum(todayEntries, spendFor), @@ -171,7 +157,7 @@ export function limitStatusMessage( if (violations.includes("cans") && limits.dailyCanLimit != null) { lines.push( - `This would bring you to ${check.projectedCans.toFixed(1)}/${limits.dailyCanLimit} cans today (BST).`, + `This would bring you to ${check.projectedCans.toFixed(1)}/${limits.dailyCanLimit} cans today.`, ); } @@ -182,7 +168,7 @@ export function limitStatusMessage( } if (violations.includes("stopTime") && limits.stopTime) { - lines.push(`You're past your stop time (${formatStopTimeLabel(limits.stopTime)} BST).`); + lines.push(`You're past your stop time (${formatStopTimeLabel(limits.stopTime)}).`); } return lines.join(" ");