feat: stack related date fields and clarify share stock labels (#422)
* feat: stack related date fields and clarify share stock labels * test: cover stacked date pairs and share labels
This commit is contained in:
@@ -421,17 +421,27 @@ export function MobileEditModal({
|
||||
<span className="field-error">{fieldErrors.genericName}</span>
|
||||
)}
|
||||
</label>
|
||||
<label className="full">
|
||||
{t("form.medicationStartDate")}
|
||||
<DateInput
|
||||
value={form.medicationStartDate}
|
||||
onChange={(e) => onHandleValueChange("medicationStartDate", e.target.value)}
|
||||
placeholder={t("common.optional")}
|
||||
/>
|
||||
{!readOnlyMode && dateConsistencyError && (
|
||||
<span className="field-error">{dateConsistencyError}</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="full date-pair-group">
|
||||
<label className="date-pair-field">
|
||||
{t("form.medicationStartDate")}
|
||||
<DateInput
|
||||
value={form.medicationStartDate}
|
||||
onChange={(e) => onHandleValueChange("medicationStartDate", e.target.value)}
|
||||
placeholder={t("common.optional")}
|
||||
/>
|
||||
{!readOnlyMode && dateConsistencyError && (
|
||||
<span className="field-error">{dateConsistencyError}</span>
|
||||
)}
|
||||
</label>
|
||||
<label className="date-pair-field">
|
||||
{t("form.medicationEndDate")}
|
||||
<DateInput
|
||||
value={form.medicationEndDate}
|
||||
onChange={(e) => onHandleValueChange("medicationEndDate", e.target.value)}
|
||||
placeholder={t("common.optional")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="full">
|
||||
{t("form.packageType")}
|
||||
<select
|
||||
@@ -446,14 +456,6 @@ export function MobileEditModal({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="full">
|
||||
{t("form.medicationEndDate")}
|
||||
<DateInput
|
||||
value={form.medicationEndDate}
|
||||
onChange={(e) => onHandleValueChange("medicationEndDate", e.target.value)}
|
||||
placeholder={t("common.optional")}
|
||||
/>
|
||||
</label>
|
||||
{allowsPillFormSelection(form.packageType) && (
|
||||
<label className="full">
|
||||
{t("form.pillForm")}
|
||||
|
||||
@@ -585,14 +585,14 @@
|
||||
"name": "Name",
|
||||
"package": "Packung",
|
||||
"stock": "Bestand",
|
||||
"daysLeft": "Tage übrig",
|
||||
"daysLeft": "Geschätzte Resttage",
|
||||
"nextIntake": "Nächste Einnahme",
|
||||
"depletion": "Aufgebraucht",
|
||||
"priority": "Priorität"
|
||||
"priority": "Bestandswarnung"
|
||||
},
|
||||
"priority": {
|
||||
"normal": "Normal",
|
||||
"high": "Hoch"
|
||||
"normal": "Bestand ok",
|
||||
"high": "Niedriger Bestand"
|
||||
},
|
||||
"stock": {
|
||||
"of": "{{current}} von {{capacity}}"
|
||||
|
||||
@@ -585,14 +585,14 @@
|
||||
"name": "Name",
|
||||
"package": "Package",
|
||||
"stock": "Stock",
|
||||
"daysLeft": "Days left",
|
||||
"daysLeft": "Estimated days left",
|
||||
"nextIntake": "Next intake",
|
||||
"depletion": "Depletion",
|
||||
"priority": "Priority"
|
||||
"priority": "Stock alert"
|
||||
},
|
||||
"priority": {
|
||||
"normal": "Normal",
|
||||
"high": "High"
|
||||
"normal": "Stock OK",
|
||||
"high": "Low stock"
|
||||
},
|
||||
"stock": {
|
||||
"of": "{{current}} of {{capacity}}"
|
||||
|
||||
@@ -677,8 +677,10 @@ export function DashboardPage() {
|
||||
<span>{t("table.dailyConsumption")}</span>
|
||||
<span>{t("table.stockDetails")}</span>
|
||||
<span>{t("table.daysLeft")}</span>
|
||||
<span>{t("table.runsOut")}</span>
|
||||
<span>{t("table.expiry")}</span>
|
||||
<span className="date-pair-stack-header">
|
||||
<span className="date-pair-label">{t("table.runsOut")}</span>
|
||||
<span className="date-pair-label">{t("table.expiry")}</span>
|
||||
</span>
|
||||
<span>{t("table.status")}</span>
|
||||
</div>
|
||||
{coverage.all.map((row) => {
|
||||
@@ -806,15 +808,23 @@ export function DashboardPage() {
|
||||
<span data-label={t("table.daysLeft")} className={textClass}>
|
||||
{formatNumber(row.daysLeft)}
|
||||
</span>
|
||||
<span data-label={t("table.runsOut")}>{row.depletionDate ?? "-"}</span>
|
||||
<span data-label={t("table.expiry")} className={expiryClass}>
|
||||
{med?.expiryDate
|
||||
? new Date(med.expiryDate).toLocaleDateString(getSystemLocale(i18n.language), {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "2-digit",
|
||||
})
|
||||
: "-"}
|
||||
<span className="date-pair-stack">
|
||||
<span className="date-pair-entry">
|
||||
<span className="date-pair-label">{t("table.runsOut")}</span>
|
||||
<span className="date-pair-value">{row.depletionDate ?? "-"}</span>
|
||||
</span>
|
||||
<span className="date-pair-entry">
|
||||
<span className="date-pair-label">{t("table.expiry")}</span>
|
||||
<span className={`date-pair-value ${expiryClass}`}>
|
||||
{med?.expiryDate
|
||||
? new Date(med.expiryDate).toLocaleDateString(getSystemLocale(i18n.language), {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "2-digit",
|
||||
})
|
||||
: "-"}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span data-label={t("table.status")} className={status ? `status-chip ${status.className}` : ""}>
|
||||
{status ? t(status.label) : "-"}
|
||||
|
||||
@@ -1247,17 +1247,27 @@ export function MedicationsPage() {
|
||||
<span className="field-error">{fieldErrors.genericName}</span>
|
||||
)}
|
||||
</label>
|
||||
<label>
|
||||
{t("form.medicationStartDate")}
|
||||
<DateInput
|
||||
value={form.medicationStartDate}
|
||||
onChange={(e) => handleValueChange("medicationStartDate", e.target.value)}
|
||||
placeholder={t("common.optional")}
|
||||
/>
|
||||
{!readOnlyView && dateConsistencyError && (
|
||||
<span className="field-error">{dateConsistencyError}</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="full date-pair-group">
|
||||
<label className="date-pair-field">
|
||||
{t("form.medicationStartDate")}
|
||||
<DateInput
|
||||
value={form.medicationStartDate}
|
||||
onChange={(e) => handleValueChange("medicationStartDate", e.target.value)}
|
||||
placeholder={t("common.optional")}
|
||||
/>
|
||||
{!readOnlyView && dateConsistencyError && (
|
||||
<span className="field-error">{dateConsistencyError}</span>
|
||||
)}
|
||||
</label>
|
||||
<label className="date-pair-field">
|
||||
{t("form.medicationEndDate")}
|
||||
<DateInput
|
||||
value={form.medicationEndDate}
|
||||
onChange={(e) => handleValueChange("medicationEndDate", e.target.value)}
|
||||
placeholder={t("common.optional")}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
{t("form.packageType")}
|
||||
<select
|
||||
@@ -1272,14 +1282,6 @@ export function MedicationsPage() {
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.medicationEndDate")}
|
||||
<DateInput
|
||||
value={form.medicationEndDate}
|
||||
onChange={(e) => handleValueChange("medicationEndDate", e.target.value)}
|
||||
placeholder={t("common.optional")}
|
||||
/>
|
||||
</label>
|
||||
{allowsPillFormSelection(form.packageType) && (
|
||||
<label>
|
||||
{t("form.pillForm")}
|
||||
|
||||
@@ -244,8 +244,12 @@ export function SharedOverviewPage() {
|
||||
<th>{t("sharedOverview.columns.package")}</th>
|
||||
<th>{t("sharedOverview.columns.stock")}</th>
|
||||
<th>{t("sharedOverview.columns.daysLeft")}</th>
|
||||
<th>{t("sharedOverview.columns.nextIntake")}</th>
|
||||
<th>{t("sharedOverview.columns.depletion")}</th>
|
||||
<th>
|
||||
<div className="date-pair-stack-header">
|
||||
<span className="date-pair-label">{t("sharedOverview.columns.nextIntake")}</span>
|
||||
<span className="date-pair-label">{t("sharedOverview.columns.depletion")}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>{t("sharedOverview.columns.priority")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -276,8 +280,18 @@ export function SharedOverviewPage() {
|
||||
})}
|
||||
</td>
|
||||
<td>{medication.daysLeft === null ? "-" : medication.daysLeft}</td>
|
||||
<td>{formatDate(medication.nextIntakeDate, locale)}</td>
|
||||
<td>{formatDate(medication.depletionDate, locale)}</td>
|
||||
<td>
|
||||
<div className="date-pair-stack">
|
||||
<div className="date-pair-entry">
|
||||
<span className="date-pair-label">{t("sharedOverview.columns.nextIntake")}</span>
|
||||
<span className="date-pair-value">{formatDate(medication.nextIntakeDate, locale)}</span>
|
||||
</div>
|
||||
<div className="date-pair-entry">
|
||||
<span className="date-pair-label">{t("sharedOverview.columns.depletion")}</span>
|
||||
<span className="date-pair-value">{formatDate(medication.depletionDate, locale)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{medication.priority === null ? (
|
||||
"-"
|
||||
|
||||
+61
-1
@@ -1668,6 +1668,46 @@ textarea.auto-resize {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.date-pair-group {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.date-pair-field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.date-pair-stack,
|
||||
.date-pair-stack-header {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.date-pair-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.date-pair-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.date-pair-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.3;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.form-category {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
@@ -2785,7 +2825,7 @@ button.has-validation-error {
|
||||
|
||||
.table-8 .table-head,
|
||||
.table-8 .table-row {
|
||||
grid-template-columns: minmax(130px, 1.4fr) 90px 130px 70px 95px 95px 90px 95px;
|
||||
grid-template-columns: minmax(130px, 1.4fr) 90px 130px 70px 95px minmax(130px, 1.15fr) 95px;
|
||||
}
|
||||
|
||||
.email-sent-status {
|
||||
@@ -2884,6 +2924,26 @@ button.has-validation-error {
|
||||
flex-shrink: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.table-row > .date-pair-stack {
|
||||
gap: 0.45rem;
|
||||
}
|
||||
.table-row > .date-pair-stack::before {
|
||||
display: none;
|
||||
content: none;
|
||||
}
|
||||
.table-row > .date-pair-stack .date-pair-entry {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.table-row > .date-pair-stack .date-pair-label {
|
||||
margin-right: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.table-row > .date-pair-stack .date-pair-value {
|
||||
text-align: right;
|
||||
}
|
||||
/* First span (name cell) - centered horizontal layout */
|
||||
.table-row > span:first-child {
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
.shared-overview-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 860px;
|
||||
min-width: 780px;
|
||||
}
|
||||
|
||||
.shared-overview-table th,
|
||||
@@ -268,6 +268,11 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.shared-overview-table .date-pair-stack-header,
|
||||
.shared-overview-table .date-pair-stack {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.shared-schedule-page {
|
||||
padding: 1rem;
|
||||
|
||||
@@ -161,6 +161,18 @@ describe("MobileEditModal", () => {
|
||||
expect(screen.getByText(/form\.genericName/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("groups medication start and end date fields in one stacked date pair", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const datePairGroup = document.querySelector(".date-pair-group");
|
||||
expect(datePairGroup).toBeInTheDocument();
|
||||
|
||||
const dateFields = Array.from(datePairGroup?.querySelectorAll(".date-pair-field") ?? []);
|
||||
expect(dateFields).toHaveLength(2);
|
||||
expect(dateFields[0]).toHaveTextContent("form.medicationStartDate");
|
||||
expect(dateFields[1]).toHaveTextContent("form.medicationEndDate");
|
||||
});
|
||||
|
||||
it("renders packs input", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
|
||||
@@ -416,6 +416,33 @@ describe("DashboardPage", () => {
|
||||
expect(screen.getByText(/table\.daysLeft/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders runs-out and expiry as a stacked date pair in overview rows", () => {
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const headerPair = document.querySelector(".table-head .date-pair-stack-header");
|
||||
expect(headerPair).toBeInTheDocument();
|
||||
expect(headerPair).toHaveTextContent("table.runsOut");
|
||||
expect(headerPair).toHaveTextContent("table.expiry");
|
||||
|
||||
const rowPair = document.querySelector(".table-row .date-pair-stack");
|
||||
expect(rowPair).toBeInTheDocument();
|
||||
|
||||
const rowEntries = Array.from(rowPair?.querySelectorAll(".date-pair-entry") ?? []);
|
||||
expect(rowEntries).toHaveLength(2);
|
||||
expect(rowEntries[0]).toHaveTextContent("table.runsOut");
|
||||
expect(rowEntries[0]).toHaveTextContent("2025-02-15");
|
||||
expect(rowEntries[1]).toHaveTextContent("table.expiry");
|
||||
});
|
||||
|
||||
it("renders multiple cards", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
|
||||
@@ -219,6 +219,19 @@ describe("MedicationsPage", () => {
|
||||
expect(screen.getByText(/form\.genericName/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders medication start and end dates as one desktop date pair group", () => {
|
||||
renderPage();
|
||||
openNewMedicationForm();
|
||||
|
||||
const datePairGroup = document.querySelector(".date-pair-group");
|
||||
expect(datePairGroup).toBeInTheDocument();
|
||||
|
||||
const dateFields = Array.from(datePairGroup?.querySelectorAll(".date-pair-field") ?? []);
|
||||
expect(dateFields).toHaveLength(2);
|
||||
expect(dateFields[0]).toHaveTextContent("form.medicationStartDate");
|
||||
expect(dateFields[1]).toHaveTextContent("form.medicationEndDate");
|
||||
});
|
||||
|
||||
it("shows submit button in form mode", () => {
|
||||
renderPage();
|
||||
openNewMedicationForm();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import de from "../../i18n/de.json";
|
||||
import en from "../../i18n/en.json";
|
||||
import { SharedOverviewPage } from "../../pages/SharedOverviewPage";
|
||||
|
||||
function renderSharedOverview(path: string) {
|
||||
@@ -62,9 +64,37 @@ describe("SharedOverviewPage", () => {
|
||||
expect(screen.getAllByText("Acetylsalicylic Acid").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const headerPair = document.querySelector(".shared-overview-table .date-pair-stack-header");
|
||||
expect(headerPair).toBeInTheDocument();
|
||||
expect(headerPair).toHaveTextContent("sharedOverview.columns.nextIntake");
|
||||
expect(headerPair).toHaveTextContent("sharedOverview.columns.depletion");
|
||||
|
||||
const rowPair = document.querySelector(".shared-overview-table .date-pair-stack");
|
||||
expect(rowPair).toBeInTheDocument();
|
||||
|
||||
const rowEntries = Array.from(rowPair?.querySelectorAll(".date-pair-entry") ?? []);
|
||||
expect(rowEntries).toHaveLength(2);
|
||||
expect(rowEntries[0]).toHaveTextContent("sharedOverview.columns.nextIntake");
|
||||
expect(rowEntries[1]).toHaveTextContent("sharedOverview.columns.depletion");
|
||||
expect(screen.getAllByText("sharedOverview.columns.daysLeft")).toHaveLength(2);
|
||||
expect(screen.getByText("sharedOverview.columns.priority")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("sharedOverview.priority.normal")).toHaveLength(2);
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/share/abcdef0123456789/overview");
|
||||
});
|
||||
|
||||
it("keeps the updated shared overview stock wording in English and German", () => {
|
||||
expect(en.sharedOverview.columns.daysLeft).toBe("Estimated days left");
|
||||
expect(en.sharedOverview.columns.priority).toBe("Stock alert");
|
||||
expect(en.sharedOverview.priority.normal).toBe("Stock OK");
|
||||
expect(en.sharedOverview.priority.high).toBe("Low stock");
|
||||
|
||||
expect(de.sharedOverview.columns.daysLeft).toBe("Geschätzte Resttage");
|
||||
expect(de.sharedOverview.columns.priority).toBe("Bestandswarnung");
|
||||
expect(de.sharedOverview.priority.normal).toBe("Bestand ok");
|
||||
expect(de.sharedOverview.priority.high).toBe("Niedriger Bestand");
|
||||
});
|
||||
|
||||
it("renders not found state for missing token", async () => {
|
||||
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user