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;
}