feat(blister-form): update blister form structure to include separate start date and time fields

This commit is contained in:
Daniel Volz
2025-12-27 15:32:47 +01:00
parent 6b6c20bdc3
commit 57377aeead
4 changed files with 106 additions and 9 deletions
+50 -6
View File
@@ -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() {
<input type="number" min="1" value={s.every} onChange={(e) => setBlisterValue(idx, "every", e.target.value)} />
</label>
<label>
{t('form.blisters.start')}
<input type="datetime-local" step="60" value={s.start} onChange={(e) => setBlisterValue(idx, "start", e.target.value)} />
{t('form.blisters.startDate')}
<input type="date" value={s.startDate} onChange={(e) => setBlisterValue(idx, "startDate", e.target.value)} />
</label>
<label>
{t('form.blisters.startTime')}
<input type="time" value={s.startTime} onChange={(e) => setBlisterValue(idx, "startTime", e.target.value)} />
</label>
</div>
{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())) {
+2 -1
View File
@@ -119,7 +119,8 @@
"everyDays": "Alle (Tage)",
"every": "alle",
"from": "ab",
"start": "Start (Datum/Uhrzeit)"
"startDate": "Datum",
"startTime": "Uhrzeit"
}
},
"planner": {
+2 -1
View File
@@ -121,7 +121,8 @@
"everyDays": "Every (days)",
"every": "every",
"from": "from",
"start": "Start (date/time)"
"startDate": "Date",
"startTime": "Time"
}
},
"planner": {
+52 -1
View File
@@ -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;
}