feat: frontend improvements - shared schedule, bottle type, settings UI, planner notifications (#146)
- Rewrite SharedSchedule to match DashboardPage rendering with time-based consumption - Add bottle package type support across all views (MedDetail, Refill, Planner, Dashboard) - Redesign settings page with colored threshold chips, validation, and stock reminder display - Add shareStockStatus toggle and send manual reminder button - Pill/pills singular/plural consistency across all views - Planner send notification via push (Shoutrrr) in addition to email - Stock overflow warning and past-missed day styling - Update README: bottles in Smart Inventory, push in Trip Planner, new ENV section - 708 passing frontend tests including new coverage for all changes
This commit is contained in:
@@ -15,6 +15,7 @@ const mockMedication: Medication = {
|
||||
id: 1,
|
||||
name: "Test Med",
|
||||
genericName: "Generic Name",
|
||||
packageType: "blister",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
@@ -385,3 +386,197 @@ describe("MedDetailModal with refill history", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("MedDetailModal intake schedule usage display", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("does not multiply usage by personCount when intakes have per-intake takenBy", () => {
|
||||
// Two people at medication level, but each intake has its own takenBy
|
||||
const med: Medication = {
|
||||
...mockMedication,
|
||||
takenBy: ["Alice", "Bob"],
|
||||
blisters: [
|
||||
{ usage: 1, every: 1, start: "2024-01-01T09:00:00" },
|
||||
{ usage: 1, every: 1, start: "2024-01-01T21:00:00" },
|
||||
],
|
||||
intakes: [
|
||||
{ usage: 1, every: 1, start: "2024-01-01T09:00:00", takenBy: "Alice", intakeRemindersEnabled: false },
|
||||
{ usage: 1, every: 1, start: "2024-01-01T21:00:00", takenBy: "Bob", intakeRemindersEnabled: false },
|
||||
],
|
||||
};
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
const usageElements = document.querySelectorAll(".med-schedule-usage");
|
||||
// Each intake should show "1 pill" (not "2 pills")
|
||||
usageElements.forEach((el) => {
|
||||
expect(el.textContent).toContain("1");
|
||||
expect(el.textContent).not.toMatch(/^2\b/);
|
||||
});
|
||||
});
|
||||
|
||||
it("multiplies usage by personCount for legacy blisters without per-intake takenBy", () => {
|
||||
// Two people at medication level, legacy blisters without intakes
|
||||
const med: Medication = {
|
||||
...mockMedication,
|
||||
takenBy: ["Alice", "Bob"],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-01-01T09:00:00" }],
|
||||
// No intakes array - legacy format
|
||||
};
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
const usageElements = document.querySelectorAll(".med-schedule-usage");
|
||||
// Legacy: 1 pill * 2 people = "2 pills"
|
||||
expect(usageElements.length).toBe(1);
|
||||
expect(usageElements[0].textContent).toContain("2");
|
||||
});
|
||||
|
||||
it("shows correct usage for single person with per-intake takenBy", () => {
|
||||
const med: Medication = {
|
||||
...mockMedication,
|
||||
takenBy: ["Alice"],
|
||||
pillWeightMg: 500,
|
||||
blisters: [{ usage: 2, every: 1, start: "2024-01-01T09:00:00" }],
|
||||
intakes: [{ usage: 2, every: 1, start: "2024-01-01T09:00:00", takenBy: "Alice", intakeRemindersEnabled: false }],
|
||||
};
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
const usageElements = document.querySelectorAll(".med-schedule-usage");
|
||||
expect(usageElements.length).toBe(1);
|
||||
// Should show "2 pills (1000 mg)" - usage=2, not multiplied
|
||||
expect(usageElements[0].textContent).toContain("2");
|
||||
expect(usageElements[0].textContent).toContain("1000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MedDetailModal stock overflow warning", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows warning icon when stock exceeds package capacity", () => {
|
||||
const overflowCoverage: Coverage = {
|
||||
name: "Test Med",
|
||||
medsLeft: 49,
|
||||
daysLeft: 49,
|
||||
depletionDate: "2024-03-01",
|
||||
depletionTime: Date.now() + 49 * 86400000,
|
||||
nextDose: null,
|
||||
};
|
||||
|
||||
render(<MedDetailModal {...defaultProps} coverage={{ all: [overflowCoverage] }} />);
|
||||
|
||||
// packageSize = 1 * 1 * 30 + 0 = 30, currentStock = 49 > 30
|
||||
const warningIcon = document.querySelector(".info-tooltip.tooltip-align-left.warning-text");
|
||||
expect(warningIcon).toBeInTheDocument();
|
||||
expect(warningIcon?.getAttribute("data-tooltip")).toBe("tooltips.stockExceedsCapacity");
|
||||
});
|
||||
|
||||
it("does not show warning icon when stock is within package capacity", () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
// packageSize = 30, currentStock = 25 < 30
|
||||
const warningIcon = document.querySelector(".info-tooltip.tooltip-align-left.warning-text");
|
||||
expect(warningIcon).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show warning icon when stock equals package capacity", () => {
|
||||
const exactCoverage: Coverage = {
|
||||
name: "Test Med",
|
||||
medsLeft: 30,
|
||||
daysLeft: 30,
|
||||
depletionDate: "2024-02-01",
|
||||
depletionTime: Date.now() + 30 * 86400000,
|
||||
nextDose: null,
|
||||
};
|
||||
|
||||
render(<MedDetailModal {...defaultProps} coverage={{ all: [exactCoverage] }} />);
|
||||
|
||||
// packageSize = 30, currentStock = 30 — equal, no warning
|
||||
const warningIcon = document.querySelector(".info-tooltip.tooltip-align-left.warning-text");
|
||||
expect(warningIcon).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MedDetailModal bottle package type", () => {
|
||||
const bottleMed: Medication = {
|
||||
id: 2,
|
||||
name: "Bottle Med",
|
||||
genericName: null,
|
||||
packageType: "bottle",
|
||||
packCount: 0,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 1,
|
||||
looseTablets: 80,
|
||||
totalPills: 100,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-01-01T09:00:00" }],
|
||||
updatedAt: null,
|
||||
expiryDate: null,
|
||||
notes: null,
|
||||
};
|
||||
|
||||
const bottleCoverage: Coverage = {
|
||||
name: "Bottle Med",
|
||||
medsLeft: 80,
|
||||
daysLeft: 80,
|
||||
depletionDate: "2024-06-01",
|
||||
depletionTime: Date.now() + 80 * 86400000,
|
||||
nextDose: null,
|
||||
};
|
||||
|
||||
const bottleProps = {
|
||||
...defaultProps,
|
||||
selectedMed: bottleMed,
|
||||
coverage: { all: [bottleCoverage] },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("does not show blister fields in stock info section", () => {
|
||||
render(<MedDetailModal {...bottleProps} />);
|
||||
|
||||
// Should show current stock
|
||||
expect(screen.getByText(/modal\.currentStock/i)).toBeInTheDocument();
|
||||
|
||||
// Should NOT show full blisters or open blister labels
|
||||
expect(screen.queryByText(/table\.fullBlisters/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/table\.openBlister/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows bottle type in package details section", () => {
|
||||
render(<MedDetailModal {...bottleProps} />);
|
||||
|
||||
// Should show package type as bottle
|
||||
expect(screen.getByText(/form\.packageTypeBottle/i)).toBeInTheDocument();
|
||||
|
||||
// Should show total capacity
|
||||
expect(screen.getByText(/form\.totalCapacity/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows pills-only refill modal for bottle type", () => {
|
||||
render(<MedDetailModal {...bottleProps} showRefillModal={true} />);
|
||||
|
||||
// Should show pills to add label
|
||||
expect(screen.getByText(/refill\.pillsToAdd/i)).toBeInTheDocument();
|
||||
|
||||
// Should NOT show packs label in refill
|
||||
const refillModal = document.querySelector(".refill-modal");
|
||||
// Packs label should not be present for bottle type
|
||||
expect(screen.queryByText("refill.packs")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows total pills input in edit stock modal for bottle type", () => {
|
||||
render(<MedDetailModal {...bottleProps} showEditStockModal={true} />);
|
||||
|
||||
// Should show total pills label
|
||||
expect(screen.getByText(/editStock\.totalPills/i)).toBeInTheDocument();
|
||||
|
||||
// Should NOT show full blisters or partial blister labels
|
||||
expect(screen.queryByText(/editStock\.fullBlisters/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/editStock\.partialBlisterPills/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -541,3 +541,52 @@ describe("MobileEditModal optional fields", () => {
|
||||
expect(toggle).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MobileEditModal bottle package type", () => {
|
||||
const bottleForm: FormState = {
|
||||
...defaultForm,
|
||||
packageType: "bottle",
|
||||
packCount: "0",
|
||||
blistersPerPack: "1",
|
||||
pillsPerBlister: "1",
|
||||
looseTablets: "80",
|
||||
totalPills: "100",
|
||||
};
|
||||
|
||||
it("shows pills-only refill form for bottle type when editing", () => {
|
||||
render(<MobileEditModal {...defaultProps} form={bottleForm} editingId={1} />);
|
||||
|
||||
// Should show "pillsToAdd" label for bottle
|
||||
expect(screen.getByText(/refill\.pillsToAdd/i)).toBeInTheDocument();
|
||||
|
||||
// Should NOT show "packs" label in refill section
|
||||
const refillSection = document.querySelector(".refill-section");
|
||||
expect(refillSection).toBeInTheDocument();
|
||||
expect(refillSection!.textContent).not.toContain("refill.packs");
|
||||
expect(refillSection!.textContent).not.toContain("refill.loosePills");
|
||||
});
|
||||
|
||||
it("shows packs and loose refill form for blister type when editing", () => {
|
||||
render(<MobileEditModal {...defaultProps} form={defaultForm} editingId={1} />);
|
||||
|
||||
// Should show "packs" and "loosePills" labels for blister
|
||||
const refillSection = document.querySelector(".refill-section");
|
||||
expect(refillSection).toBeInTheDocument();
|
||||
expect(refillSection!.textContent).toContain("refill.packs");
|
||||
expect(refillSection!.textContent).toContain("refill.loosePills");
|
||||
});
|
||||
|
||||
it("shows totalCapacity and currentPills fields for bottle form", () => {
|
||||
render(<MobileEditModal {...defaultProps} form={bottleForm} />);
|
||||
|
||||
// Should show total capacity field
|
||||
expect(screen.getByText(/form\.totalCapacity/i)).toBeInTheDocument();
|
||||
// Should show current pills field
|
||||
expect(screen.getByText(/form\.currentPills/i)).toBeInTheDocument();
|
||||
|
||||
// Should NOT show blister-specific fields
|
||||
expect(screen.queryByText("form.packs")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("form.blistersPerPack")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("form.pillsPerBlister")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user