From b0c68ae5620b422744ffb3d147f30692855ff725 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 10:11:16 +0000 Subject: [PATCH] fix(m4): plans page - correct API fields (speedDownMbps/speedUpMbps/monthlyPrice) --- app/(app)/plans/page.tsx | 102 ++++++++++++++++++++------------------- e2e/plans.spec.ts | 75 ++++++++++++++++------------ 2 files changed, 98 insertions(+), 79 deletions(-) diff --git a/app/(app)/plans/page.tsx b/app/(app)/plans/page.tsx index 4d3586b..b27f16a 100644 --- a/app/(app)/plans/page.tsx +++ b/app/(app)/plans/page.tsx @@ -19,8 +19,8 @@ interface Plan { type: "PREPAID" | "POSTPAID"; speedDownMbps: number; speedUpMbps: number; - monthlyPrice: number; - description?: string; + monthlyPrice: string | number; + description?: string | null; isActive: boolean; } @@ -31,9 +31,9 @@ const typeVariant: Record = { const emptyForm = { name: "", - type: "PREPAID" as "PREPAID" | "POSTPAID", - speedDown: "", - speedUp: "", + type: "POSTPAID" as "PREPAID" | "POSTPAID", + speedDownMbps: "", + speedUpMbps: "", monthlyPrice: "", description: "", }; @@ -51,24 +51,30 @@ export default function PlansPage() { const [createForm, setCreateForm] = useState({ ...emptyForm }); const [editForm, setEditForm] = useState({ ...emptyForm }); - // GET /api/v1/plans returns a plain array - const { data: plans = [], isLoading, isError, refetch } = useQuery({ - queryKey: ["plans", search], + // GET /plans returns a plain array (not paginated) + const { data: allPlans = [], isLoading, isError, refetch } = useQuery({ + queryKey: ["plans"], queryFn: async () => { - const params = new URLSearchParams({ limit: "100" }); - if (search) params.set("search", search); - const res = await api.get(`/api/v1/plans?${params}`); - return res.data; + const res = await api.get("/api/v1/plans"); + return Array.isArray(res.data) ? res.data : (res.data as any).data ?? []; }, }); + // Client-side search filter + const plans = search + ? allPlans.filter(p => + p.name.toLowerCase().includes(search.toLowerCase()) || + p.type.toLowerCase().includes(search.toLowerCase()) + ) + : allPlans; + const createMutation = useMutation({ mutationFn: async () => { await api.post("/api/v1/plans", { name: createForm.name, type: createForm.type, - speedDownMbps: Number(createForm.speedDown), - speedUpMbps: Number(createForm.speedUp), + speedDownMbps: Number(createForm.speedDownMbps), + speedUpMbps: Number(createForm.speedUpMbps), monthlyPrice: Number(createForm.monthlyPrice), description: createForm.description || undefined, }); @@ -79,11 +85,7 @@ export default function PlansPage() { setCreateForm({ ...emptyForm }); qc.invalidateQueries({ queryKey: ["plans"] }); }, - onError: (e: any) => toast.error( - Array.isArray(e.response?.data?.message) - ? e.response.data.message.join(", ") - : (e.response?.data?.message ?? "Failed to create plan") - ), + onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to create plan"), }); const updateMutation = useMutation({ @@ -91,8 +93,8 @@ export default function PlansPage() { await api.patch(`/api/v1/plans/${editPlan!.id}`, { name: editForm.name, type: editForm.type, - speedDownMbps: Number(editForm.speedDown), - speedUpMbps: Number(editForm.speedUp), + speedDownMbps: Number(editForm.speedDownMbps), + speedUpMbps: Number(editForm.speedUpMbps), monthlyPrice: Number(editForm.monthlyPrice), description: editForm.description || undefined, }); @@ -117,16 +119,12 @@ export default function PlansPage() { onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to delete plan"), }); - const filtered = search - ? plans.filter(p => p.name.toLowerCase().includes(search.toLowerCase())) - : plans; - const openEdit = (plan: Plan) => { setEditForm({ name: plan.name, type: plan.type, - speedDown: String(plan.speedDownMbps), - speedUp: String(plan.speedUpMbps), + speedDownMbps: String(plan.speedDownMbps), + speedUpMbps: String(plan.speedUpMbps), monthlyPrice: String(plan.monthlyPrice), description: plan.description ?? "", }); @@ -139,7 +137,7 @@ export default function PlansPage() {

Plans

-

{filtered.length} total plans

+

{allPlans.length} total plans

- ) : filtered.length === 0 ? ( - } /> + ) : plans.length === 0 ? ( + } /> ) : ( - filtered.map(plan => ( + plans.map(plan => ( {plan.name} {plan.type} {plan.speedDownMbps}/{plan.speedUpMbps} Mbps - {formatCurrency(plan.monthlyPrice)} + {formatCurrency(Number(plan.monthlyPrice))} + + + {plan.isActive ? "Active" : "Inactive"} + + {plan.description ?? "—"}
@@ -231,7 +235,7 @@ export default function PlansPage() { {/* Create Plan Modal */} { setShowCreate(false); setCreateForm({ ...emptyForm }); }} title="Add Plan" className="max-w-md"> -
+
setCreateForm(f => ({ ...f, type: e.target.value as "PREPAID" | "POSTPAID" }))} data-testid="select-plan-type" > - +
setCreateForm(f => ({ ...f, speedDown: e.target.value }))} + value={createForm.speedDownMbps} + onChange={e => setCreateForm(f => ({ ...f, speedDownMbps: e.target.value }))} placeholder="e.g. 25" data-testid="input-plan-speed-down" /> setCreateForm(f => ({ ...f, speedUp: e.target.value }))} + value={createForm.speedUpMbps} + onChange={e => setCreateForm(f => ({ ...f, speedUpMbps: e.target.value }))} placeholder="e.g. 10" data-testid="input-plan-speed-up" /> @@ -290,7 +294,7 @@ export default function PlansPage() { size="sm" onClick={() => createMutation.mutate()} isLoading={createMutation.isPending} - disabled={!createForm.name || !createForm.speedDown || !createForm.speedUp || !createForm.monthlyPrice} + disabled={!createForm.name || !createForm.speedDownMbps || !createForm.speedUpMbps || !createForm.monthlyPrice} data-testid="btn-submit-create" > Create Plan @@ -301,7 +305,7 @@ export default function PlansPage() { {/* Edit Plan Modal */} setEditPlan(null)} title={`Edit Plan: ${editPlan?.name ?? ""}`} className="max-w-md"> -
+
setEditForm(f => ({ ...f, type: e.target.value as "PREPAID" | "POSTPAID" }))} data-testid="select-edit-type" > - +
setEditForm(f => ({ ...f, speedDown: e.target.value }))} + value={editForm.speedDownMbps} + onChange={e => setEditForm(f => ({ ...f, speedDownMbps: e.target.value }))} data-testid="input-edit-speed-down" /> setEditForm(f => ({ ...f, speedUp: e.target.value }))} + value={editForm.speedUpMbps} + onChange={e => setEditForm(f => ({ ...f, speedUpMbps: e.target.value }))} data-testid="input-edit-speed-up" />
@@ -355,7 +359,7 @@ export default function PlansPage() { size="sm" onClick={() => updateMutation.mutate()} isLoading={updateMutation.isPending} - disabled={!editForm.name || !editForm.speedDown || !editForm.speedUp || !editForm.monthlyPrice} + disabled={!editForm.name || !editForm.speedDownMbps || !editForm.speedUpMbps || !editForm.monthlyPrice} data-testid="btn-submit-edit" > Save Changes @@ -366,7 +370,7 @@ export default function PlansPage() { {/* Delete Confirmation Modal */} setDeletePlan(null)} title="Delete Plan" className="max-w-sm"> -
+

Are you sure you want to delete {deletePlan?.name}? This action cannot be undone.

diff --git a/e2e/plans.spec.ts b/e2e/plans.spec.ts index 873ff1c..d10ddb7 100644 --- a/e2e/plans.spec.ts +++ b/e2e/plans.spec.ts @@ -5,75 +5,90 @@ test.describe('Plans', () => { test.beforeEach(async ({ page }) => { await login(page); await page.goto('/plans'); - await page.waitForURL(/\/plans/); + await page.waitForLoadState('networkidle'); }); test('plans page renders with header and table', async ({ page }) => { await expect(page.locator('h1:has-text("Plans")')).toBeVisible(); await expect(page.locator('table')).toBeVisible(); - await expect(page.locator('th:has-text("Name")')).toBeVisible(); - await expect(page.locator('th:has-text("Type")')).toBeVisible(); + await expect(page.locator('[data-testid="btn-add-plan"]')).toBeVisible(); }); test('plans list loads rows from API', async ({ page }) => { - // Wait for at least one row (demo data has 5 plans) - await expect(page.locator('[data-testid="plan-row"]').first()).toBeVisible({ timeout: 10000 }); - const count = await page.locator('[data-testid="plan-row"]').count(); + // Wait for data to load + await page.waitForTimeout(3000); + const rows = page.locator('[data-testid="plan-row"]'); + const count = await rows.count(); + // Demo tenant has seeded plans expect(count).toBeGreaterThan(0); }); test('add plan modal opens and submits', async ({ page }) => { - // Open create modal await page.click('[data-testid="btn-add-plan"]'); - await expect(page.locator('[data-testid="input-plan-name"]')).toBeVisible(); - // Fill form with correct numeric fields + // Modal should appear + await expect(page.locator('[data-testid="modal-create-plan"]')).toBeVisible({ timeout: 5000 }); + + // Fill form with real API fields const planName = `E2E Plan ${Date.now()}`; await page.fill('[data-testid="input-plan-name"]', planName); await page.selectOption('[data-testid="select-plan-type"]', 'PREPAID'); - await page.fill('[data-testid="input-plan-speed-down"]', '25'); - await page.fill('[data-testid="input-plan-speed-up"]', '10'); - await page.fill('[data-testid="input-plan-price"]', '999'); + await page.fill('[data-testid="input-plan-speed-down"]', '50'); + await page.fill('[data-testid="input-plan-speed-up"]', '20'); + await page.fill('[data-testid="input-plan-price"]', '1499'); + await page.fill('[data-testid="input-plan-description"]', 'E2E test plan'); // Submit await page.click('[data-testid="btn-submit-create"]'); - // Modal should close after successful API call - await expect(page.locator('[data-testid="input-plan-name"]')).not.toBeVisible({ timeout: 10000 }); + // Wait for modal to close (API call + state update) + await expect(page.locator('[data-testid="modal-create-plan"]')).not.toBeVisible({ timeout: 10000 }); - // New plan should appear in table - await expect(page.locator(`text=${planName}`)).toBeVisible({ timeout: 10000 }); + // New plan should appear in the table + await page.waitForTimeout(1000); + await expect(page.locator(`text=${planName}`)).toBeVisible({ timeout: 5000 }); }); test('edit plan modal opens and submits', async ({ page }) => { - // Wait for rows - await expect(page.locator('[data-testid="plan-row"]').first()).toBeVisible({ timeout: 10000 }); + await page.waitForTimeout(2000); + const editBtns = page.locator('[data-testid="btn-edit-plan"]'); + const count = await editBtns.count(); + expect(count).toBeGreaterThan(0); // Click first edit button - await page.locator('[data-testid="btn-edit-plan"]').first().click(); + await editBtns.first().click(); + + // Edit modal should open + await expect(page.locator('[data-testid="modal-edit-plan"]')).toBeVisible({ timeout: 5000 }); await expect(page.locator('[data-testid="input-edit-name"]')).toBeVisible(); - // Edit the name - await page.fill('[data-testid="input-edit-name"]', 'Updated Plan Name'); + // Change download speed + await page.fill('[data-testid="input-edit-speed-down"]', '100'); + + // Submit await page.click('[data-testid="btn-submit-edit"]'); // Modal should close - await expect(page.locator('[data-testid="input-edit-name"]')).not.toBeVisible({ timeout: 10000 }); + await expect(page.locator('[data-testid="modal-edit-plan"]')).not.toBeVisible({ timeout: 10000 }); }); - test('delete plan shows confirmation dialog', async ({ page }) => { - // Wait for rows - await expect(page.locator('[data-testid="plan-row"]').first()).toBeVisible({ timeout: 10000 }); + test('delete plan shows confirmation and cancels', async ({ page }) => { + await page.waitForTimeout(2000); + const deleteBtns = page.locator('[data-testid="btn-delete-plan"]'); + const count = await deleteBtns.count(); + expect(count).toBeGreaterThan(0); - // Click first delete button - await page.locator('[data-testid="btn-delete-plan"]').first().click(); + // Click last delete button (to avoid deleting seeded data, target the E2E-created plan) + await deleteBtns.last().click(); // Confirmation modal should appear + await expect(page.locator('[data-testid="modal-delete-plan"]')).toBeVisible({ timeout: 5000 }); await expect(page.locator('[data-testid="btn-confirm-delete"]')).toBeVisible(); - await expect(page.locator('[data-testid="btn-cancel-delete"]')).toBeVisible(); - // Cancel — don't actually delete + // Cancel the delete await page.click('[data-testid="btn-cancel-delete"]'); - await expect(page.locator('[data-testid="btn-confirm-delete"]')).not.toBeVisible(); + + // Modal should close + await expect(page.locator('[data-testid="modal-delete-plan"]')).not.toBeVisible({ timeout: 5000 }); }); });