From 57377aeead56fd98a9bdbae347e785df7c0bc6a3 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sat, 27 Dec 2025 15:32:47 +0100 Subject: [PATCH] feat(blister-form): update blister form structure to include separate start date and time fields --- frontend/src/App.tsx | 56 ++++++++++++++++++++++++++++++++++----- frontend/src/i18n/de.json | 3 ++- frontend/src/i18n/en.json | 3 ++- frontend/src/styles.css | 53 +++++++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dea79cc..ed65206 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -42,7 +42,7 @@ type PlannerRow = { enough: boolean; }; -type FormBlister = { usage: string; every: string; start: string }; +type FormBlister = { usage: string; every: string; startDate: string; startTime: string }; type FormState = { name: string; @@ -59,7 +59,15 @@ type FormState = { blisters: FormBlister[]; }; -const defaultBlister = (): FormBlister => ({ usage: "1", every: "1", start: toInputValue(new Date().toISOString()) }); +const defaultBlister = (): FormBlister => { + const now = new Date(); + return { + usage: "1", + every: "1", + startDate: toDateValue(now), + startTime: toTimeValue(now) + }; +}; const defaultForm = (): FormState => ({ name: "", genericName: "", takenBy: "", packCount: "1", stripsPerPack: "1", tabsPerStrip: "1", looseTablets: "0", pillWeightMg: "", expiryDate: "", notes: "", intakeRemindersEnabled: false, blisters: [defaultBlister()] }); @@ -659,7 +667,12 @@ function AppContent() { expiryDate: med.expiryDate ? med.expiryDate.slice(0, 10) : "", notes: med.notes ?? "", intakeRemindersEnabled: med.intakeRemindersEnabled ?? false, - blisters: med.blisters.map((s) => ({ usage: String(s.usage), every: String(s.every), start: toInputValue(s.start) })), + blisters: med.blisters.map((s) => ({ + usage: String(s.usage), + every: String(s.every), + startDate: toDateValue(s.start), + startTime: toTimeValue(s.start) + })), }); } @@ -691,7 +704,11 @@ function AppContent() { expiryDate: form.expiryDate || null, notes: form.notes.trim() || null, intakeRemindersEnabled: form.intakeRemindersEnabled, - blisters: form.blisters.map((s) => ({ usage: Number(s.usage) || 0, every: Math.max(1, Number(s.every) || 1), start: toIsoString(s.start) })), + blisters: form.blisters.map((s) => ({ + usage: Number(s.usage) || 0, + every: Math.max(1, Number(s.every) || 1), + start: toIsoString(combineDateAndTime(s.startDate, s.startTime)) + })), }; const method = editingId ? "PUT" : "POST"; @@ -1349,8 +1366,12 @@ function AppContent() { setBlisterValue(idx, "every", e.target.value)} /> + {form.blisters.length > 1 && ( @@ -2246,6 +2267,29 @@ function toIsoString(value: string) { return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString(); } +function toDateValue(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + if (Number.isNaN(d.getTime())) { + const now = new Date(); + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + } + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + +function toTimeValue(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + if (Number.isNaN(d.getTime())) { + const now = new Date(); + return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; + } + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; +} + +function combineDateAndTime(dateStr: string, timeStr: string): string { + // Combine separate date and time strings into ISO format + return `${dateStr}T${timeStr}`; +} + function toInputValue(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) { diff --git a/frontend/src/i18n/de.json b/frontend/src/i18n/de.json index 935d813..5fceb4b 100644 --- a/frontend/src/i18n/de.json +++ b/frontend/src/i18n/de.json @@ -119,7 +119,8 @@ "everyDays": "Alle (Tage)", "every": "alle", "from": "ab", - "start": "Start (Datum/Uhrzeit)" + "startDate": "Datum", + "startTime": "Uhrzeit" } }, "planner": { diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 4681018..52a0937 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -121,7 +121,8 @@ "everyDays": "Every (days)", "every": "every", "from": "from", - "start": "Start (date/time)" + "startDate": "Date", + "startTime": "Time" } }, "planner": { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d9a033c..72a4ae7 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -280,9 +280,17 @@ body { .blister-pill { display: flex; gap: 0.4rem; flex-wrap: wrap; } .blister-row { display: flex; flex-direction: column; gap: 0.75rem; background: var(--bg-tertiary); border: 1px solid var(--border-primary); padding: 1rem; border-radius: 8px; margin-bottom: 0.65rem; transition: background 200ms ease; } [data-theme=\"light\"] .blister-row { background: var(--bg-tertiary); } -.blister-row .blister-inputs { display: grid; grid-template-columns: 1fr 1fr 2fr; gap: 1rem; align-items: end; } +.blister-row .blister-inputs { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 0.75rem; align-items: end; } .blister-row button { align-self: flex-end; width: auto; } .blister-row:last-child { margin-bottom: 0; } + +@media (max-width: 600px) { + .blister-row .blister-inputs { + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + } +} + .blisters h3 { margin: 0; } .gap { gap: 0.6rem; } @@ -960,6 +968,8 @@ textarea { display: flex; flex-direction: column; gap: 1.5rem; + max-width: 100%; + overflow: hidden; } .setting-row { @@ -973,6 +983,13 @@ textarea { gap: 1rem; } +@media (max-width: 480px) { + .setting-row { + padding: 0.75rem; + gap: 0.75rem; + } +} + .setting-row.inline { padding: 0; background: transparent; @@ -1282,6 +1299,30 @@ textarea { border-radius: 6px; } +/* Notification Matrix Mobile */ +@media (max-width: 480px) { + .notification-matrix { + margin: 0 -0.5rem; + border-radius: 8px; + } + + .matrix-header, + .matrix-row { + grid-template-columns: 1fr 60px 60px; + padding: 0.6rem 0.75rem; + gap: 0.25rem; + } + + .matrix-channel, + .matrix-header .matrix-label { + font-size: 0.7rem; + } + + .matrix-row .matrix-label { + font-size: 0.8rem; + } +} + /* Settings Grid - Two column layout */ .settings-grid { display: grid; @@ -1376,6 +1417,16 @@ textarea { background: var(--bg-secondary); } +@media (max-width: 480px) { + .channel-content { + padding: 0 0.75rem 1rem; + } + + .channel-toggle { + padding: 0.75rem 1rem; + } +} + .channel-config { padding-top: 1rem; }