fix(m4): plans page - correct API fields (speedDownMbps/speedUpMbps/monthlyPrice)

This commit is contained in:
root
2026-03-26 10:11:16 +00:00
parent eebdf21505
commit b0c68ae562
2 changed files with 98 additions and 79 deletions

View File

@@ -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<string, "success" | "muted"> = {
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<Plan[]>({
queryKey: ["plans", search],
// GET /plans returns a plain array (not paginated)
const { data: allPlans = [], isLoading, isError, refetch } = useQuery<Plan[]>({
queryKey: ["plans"],
queryFn: async () => {
const params = new URLSearchParams({ limit: "100" });
if (search) params.set("search", search);
const res = await api.get<Plan[]>(`/api/v1/plans?${params}`);
return res.data;
const res = await api.get<Plan[] | { data: Plan[] }>("/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() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Plans</h1>
<p className="text-sm text-gray-500 mt-1">{filtered.length} total plans</p>
<p className="text-sm text-gray-500 mt-1">{allPlans.length} total plans</p>
</div>
<div className="flex gap-2">
<Button onClick={() => refetch()} variant="outline" size="sm" data-testid="btn-refresh">
@@ -169,6 +167,7 @@ export default function PlansPage() {
<Th>Type</Th>
<Th>Speed (Down/Up)</Th>
<Th>Monthly Price</Th>
<Th>Status</Th>
<Th>Description</Th>
<Th></Th>
</TableRow>
@@ -177,29 +176,34 @@ export default function PlansPage() {
{isLoading ? (
Array.from({ length: 5 }).map((_, i) => (
<TableRow key={i}>
<Td colSpan={6}><div className="h-4 bg-gray-100 rounded animate-pulse" /></Td>
<Td colSpan={7}><div className="h-4 bg-gray-100 rounded animate-pulse" /></Td>
</TableRow>
))
) : isError ? (
<TableRow>
<Td colSpan={6}>
<Td colSpan={7}>
<p className="text-center py-6 text-red-400 text-sm">
Failed to load plans.{" "}
<button onClick={() => refetch()} className="underline">Retry</button>
</p>
</Td>
</TableRow>
) : filtered.length === 0 ? (
<EmptyState colSpan={6} message="No plans found" icon={<Package size={24} />} />
) : plans.length === 0 ? (
<EmptyState colSpan={7} message="No plans found" icon={<Package size={24} />} />
) : (
filtered.map(plan => (
plans.map(plan => (
<TableRow key={plan.id} data-testid="plan-row">
<Td className="font-medium">{plan.name}</Td>
<Td>
<Badge variant={typeVariant[plan.type] ?? "muted"}>{plan.type}</Badge>
</Td>
<Td className="font-mono text-sm">{plan.speedDownMbps}/{plan.speedUpMbps} Mbps</Td>
<Td className="font-semibold">{formatCurrency(plan.monthlyPrice)}</Td>
<Td className="font-semibold">{formatCurrency(Number(plan.monthlyPrice))}</Td>
<Td>
<Badge variant={plan.isActive ? "success" : "muted"}>
{plan.isActive ? "Active" : "Inactive"}
</Badge>
</Td>
<Td className="text-gray-500 text-sm max-w-xs truncate">{plan.description ?? "—"}</Td>
<Td>
<div className="flex gap-2 justify-end">
@@ -231,7 +235,7 @@ export default function PlansPage() {
{/* Create Plan Modal */}
<Modal isOpen={showCreate} onClose={() => { setShowCreate(false); setCreateForm({ ...emptyForm }); }} title="Add Plan" className="max-w-md">
<div className="space-y-4">
<div className="space-y-4" data-testid="modal-create-plan">
<Input
label="Plan Name"
value={createForm.name}
@@ -247,24 +251,24 @@ export default function PlansPage() {
onChange={e => setCreateForm(f => ({ ...f, type: e.target.value as "PREPAID" | "POSTPAID" }))}
data-testid="select-plan-type"
>
<option value="PREPAID">PREPAID</option>
<option value="POSTPAID">POSTPAID</option>
<option value="PREPAID">PREPAID</option>
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label="Download (Mbps)"
type="number"
value={createForm.speedDown}
onChange={e => 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"
/>
<Input
label="Upload (Mbps)"
type="number"
value={createForm.speedUp}
onChange={e => 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 */}
<Modal isOpen={!!editPlan} onClose={() => setEditPlan(null)} title={`Edit Plan: ${editPlan?.name ?? ""}`} className="max-w-md">
<div className="space-y-4">
<div className="space-y-4" data-testid="modal-edit-plan">
<Input
label="Plan Name"
value={editForm.name}
@@ -316,23 +320,23 @@ export default function PlansPage() {
onChange={e => setEditForm(f => ({ ...f, type: e.target.value as "PREPAID" | "POSTPAID" }))}
data-testid="select-edit-type"
>
<option value="PREPAID">PREPAID</option>
<option value="POSTPAID">POSTPAID</option>
<option value="PREPAID">PREPAID</option>
</select>
</div>
<div className="grid grid-cols-2 gap-3">
<Input
label="Download (Mbps)"
type="number"
value={editForm.speedDown}
onChange={e => 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"
/>
<Input
label="Upload (Mbps)"
type="number"
value={editForm.speedUp}
onChange={e => 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"
/>
</div>
@@ -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 */}
<Modal isOpen={!!deletePlan} onClose={() => setDeletePlan(null)} title="Delete Plan" className="max-w-sm">
<div className="space-y-4">
<div className="space-y-4" data-testid="modal-delete-plan">
<p className="text-sm text-gray-600">
Are you sure you want to delete <strong>{deletePlan?.name}</strong>? This action cannot be undone.
</p>