Files
fiberops-web/app/(app)/settings/page.tsx

801 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import {
Building2, CreditCard, Map, Wifi, Users, ChevronRight,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { Table, TableHead, TableBody, TableRow, Th, Td, EmptyState } from "@/components/ui/Table";
import { Badge } from "@/components/ui/Badge";
import { Modal } from "@/components/ui/Modal";
import { formatCurrency } from "@/lib/utils";
import api from "@/lib/api";
// ─── Types ─────────────────────────────────────────────────────────────────────
interface Tenant {
id: string;
name: string;
slug: string;
address?: string;
phone?: string;
email?: string;
settings?: TenantBillingSettings | null;
}
interface TenantBillingSettings {
billingDay?: number;
lateFeeAmount?: number;
lateFeeGraceDays?: number;
}
interface Plan {
id: string;
name: string;
description?: string;
type: string;
speedDownMbps: number;
speedUpMbps: number;
monthlyPrice: number | string;
isActive: boolean;
}
// ─── Sub-page: Tenant ─────────────────────────────────────────────────────────
function TenantSettings() {
const [name, setName] = useState("");
const [address, setAddress] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [loaded, setLoaded] = useState(false);
const { isLoading } = useQuery<Tenant>({
queryKey: ["tenant-me"],
queryFn: async () => {
const res = await api.get<Tenant>("/api/v1/tenants/me");
return res.data;
},
select: (data) => {
if (!loaded) {
setName(data.name ?? "");
setAddress(data.address ?? "");
setEmail(data.email ?? "");
setPhone(data.phone ?? "");
setLoaded(true);
}
return data;
},
});
const saveMutation = useMutation({
mutationFn: async () => {
await api.patch("/api/v1/tenants/me", { name, address, email, phone });
},
onSuccess: () => toast.success("Tenant settings saved"),
onError: () => toast.error("Failed to save. Endpoint may not be available yet."),
});
if (isLoading) {
return <div className="h-40 animate-pulse bg-gray-100 rounded-xl" />;
}
return (
<Card>
<CardHeader><CardTitle>Business Information</CardTitle></CardHeader>
<CardContent>
<form
onSubmit={(e) => { e.preventDefault(); saveMutation.mutate(); }}
className="space-y-4 max-w-lg"
>
<Input
label="Business Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Input
label="Address"
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
<Input
label="Contact Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input
label="Phone"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
{saveMutation.isError && (
<p className="text-sm text-orange-600 bg-orange-50 rounded-lg px-3 py-2">
Save endpoint not available yet changes not persisted.
</p>
)}
<Button type="submit" isLoading={saveMutation.isPending}>
Save Changes
</Button>
</form>
</CardContent>
</Card>
);
}
// ─── Sub-page: Billing Settings ───────────────────────────────────────────────
interface FullBillingSettings {
billingDay?: number;
gracePeriodDays?: number;
lateFeeAmount?: string | number;
lateFeePercent?: string | number;
lateFeeGraceDays?: number;
currency?: string;
}
function BillingSettings() {
const [fields, setFields] = useState({ billingDay: "1", gracePeriodDays: "5", lateFeeAmount: "0", lateFeePercent: "0", lateFeeGraceDays: "0", currency: "PHP" });
const [loaded, setLoaded] = useState(false);
const { isLoading } = useQuery<FullBillingSettings>({
queryKey: ["tenant-billing-settings"],
queryFn: async () => {
const res = await api.get<FullBillingSettings>("/api/v1/tenants/me/settings");
return res.data ?? {};
},
select: (data) => {
if (!loaded && data) {
setFields({
billingDay: String(data.billingDay ?? 1),
gracePeriodDays: String(data.gracePeriodDays ?? 5),
lateFeeAmount: String(data.lateFeeAmount ?? 0),
lateFeePercent: String(data.lateFeePercent ?? 0),
lateFeeGraceDays: String(data.lateFeeGraceDays ?? 0),
currency: data.currency ?? "PHP",
});
setLoaded(true);
}
return data;
},
});
const saveMutation = useMutation({
mutationFn: async () => {
await api.patch("/api/v1/tenants/me/settings", {
billingDay: parseInt(fields.billingDay),
gracePeriodDays: parseInt(fields.gracePeriodDays),
lateFeeAmount: parseFloat(fields.lateFeeAmount),
lateFeePercent: parseFloat(fields.lateFeePercent),
lateFeeGraceDays: parseInt(fields.lateFeeGraceDays),
currency: fields.currency,
});
},
onSuccess: () => toast.success("Billing settings saved"),
onError: () => toast.error("Failed to save billing settings"),
});
const set = (key: keyof typeof fields) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) =>
setFields(f => ({ ...f, [key]: e.target.value }));
if (isLoading) return <div className="h-40 animate-pulse bg-gray-100 rounded-xl" />;
return (
<Card>
<CardHeader><CardTitle>Billing Configuration</CardTitle></CardHeader>
<CardContent>
<form onSubmit={(e) => { e.preventDefault(); saveMutation.mutate(); }} className="space-y-4 max-w-lg">
<Input label="Billing Day (128)" type="number" min="1" max="28"
value={fields.billingDay} onChange={set("billingDay")}
hint="Day of month invoices are generated" />
<Input label="Grace Period Days" type="number" min="0"
value={fields.gracePeriodDays} onChange={set("gracePeriodDays")}
hint="Days after billing day before account is flagged overdue" />
<div className="grid grid-cols-2 gap-3">
<Input label="Late Fee Amount (₱)" type="number" min="0" step="0.01"
value={fields.lateFeeAmount} onChange={set("lateFeeAmount")} />
<Input label="Late Fee % (0 = disabled)" type="number" min="0" max="100" step="0.01"
value={fields.lateFeePercent} onChange={set("lateFeePercent")} />
</div>
<Input label="Late Fee Grace Days" type="number" min="0"
value={fields.lateFeeGraceDays} onChange={set("lateFeeGraceDays")}
hint="Days after due date before late fee applies" />
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Currency</label>
<select className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={fields.currency} onChange={set("currency")}>
<option value="PHP">PHP Philippine Peso</option>
<option value="USD">USD US Dollar</option>
</select>
</div>
<Button type="submit" isLoading={saveMutation.isPending}>Save Billing Settings</Button>
</form>
</CardContent>
</Card>
);
}
// ─── Sub-page: Areas & Zones ──────────────────────────────────────────────────
interface Area {
id: string;
name: string;
zones?: Zone[];
}
interface Zone {
id: string;
name: string;
areaId: string;
}
function AreasSettings() {
const [showAddArea, setShowAddArea] = useState(false);
const [showAddZone, setShowAddZone] = useState(false);
const [areaName, setAreaName] = useState("");
const [zoneName, setZoneName] = useState("");
const [zoneAreaId, setZoneAreaId] = useState("");
const { data: areas, isLoading, refetch } = useQuery<Area[]>({
queryKey: ["areas"],
queryFn: async () => {
try {
const res = await api.get<Area[] | { data: Area[] }>("/api/v1/areas");
const d = res.data;
return Array.isArray(d) ? d : (d as { data: Area[] }).data ?? [];
} catch {
return [];
}
},
});
const addAreaMutation = useMutation({
mutationFn: async () => {
await api.post("/api/v1/areas", { name: areaName });
},
onSuccess: () => {
toast.success("Area added");
setAreaName("");
setShowAddArea(false);
refetch();
},
onError: () => toast.error("Failed to add area"),
});
const addZoneMutation = useMutation({
mutationFn: async () => {
await api.post("/api/v1/zones", { name: zoneName, areaId: zoneAreaId });
},
onSuccess: () => {
toast.success("Zone added");
setZoneName("");
setZoneAreaId("");
setShowAddZone(false);
refetch();
},
onError: () => toast.error("Failed to add zone"),
});
const areaList = areas ?? [];
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Areas & Zones</CardTitle>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={() => setShowAddArea(true)}>
+ Area
</Button>
<Button size="sm" variant="outline" onClick={() => setShowAddZone(true)}>
+ Zone
</Button>
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHead>
<TableRow>
<Th>Area Name</Th>
<Th>Zones</Th>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
Array.from({ length: 3 }).map((_, i) => (
<TableRow key={i}>
<Td><div className="h-4 w-32 animate-pulse bg-gray-100 rounded" /></Td>
<Td><div className="h-4 w-24 animate-pulse bg-gray-100 rounded" /></Td>
</TableRow>
))
) : areaList.length === 0 ? (
<EmptyState message="No areas configured" />
) : (
areaList.map((a) => (
<TableRow key={a.id}>
<Td className="font-medium">{a.name}</Td>
<Td className="text-sm text-gray-500">
{a.zones?.length
? a.zones.map((z) => z.name).join(", ")
: <span className="text-gray-300">No zones</span>}
</Td>
</TableRow>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Add Area Modal */}
<Modal isOpen={showAddArea} onClose={() => setShowAddArea(false)} title="Add Area">
<form onSubmit={(e) => { e.preventDefault(); addAreaMutation.mutate(); }} className="space-y-4">
<Input
label="Area Name"
value={areaName}
onChange={(e) => setAreaName(e.target.value)}
placeholder="e.g. North Sector"
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => setShowAddArea(false)}>Cancel</Button>
<Button type="submit" size="sm" isLoading={addAreaMutation.isPending} disabled={!areaName.trim()}>
Add Area
</Button>
</div>
</form>
</Modal>
{/* Add Zone Modal */}
<Modal isOpen={showAddZone} onClose={() => setShowAddZone(false)} title="Add Zone">
<form onSubmit={(e) => { e.preventDefault(); addZoneMutation.mutate(); }} className="space-y-4">
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Area</label>
<select
className="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
value={zoneAreaId}
onChange={(e) => setZoneAreaId(e.target.value)}
>
<option value="">Select area</option>
{areaList.map((a) => (
<option key={a.id} value={a.id}>{a.name}</option>
))}
</select>
</div>
<Input
label="Zone Name"
value={zoneName}
onChange={(e) => setZoneName(e.target.value)}
placeholder="e.g. Zone 1"
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => setShowAddZone(false)}>Cancel</Button>
<Button
type="submit"
size="sm"
isLoading={addZoneMutation.isPending}
disabled={!zoneName.trim() || !zoneAreaId}
>
Add Zone
</Button>
</div>
</form>
</Modal>
</div>
);
}
// ─── Sub-page: Plans ──────────────────────────────────────────────────────────
function PlansSettings() {
const [showAdd, setShowAdd] = useState(false);
const [planName, setPlanName] = useState("");
const [planType, setPlanType] = useState("POSTPAID");
const [speedDown, setSpeedDown] = useState("");
const [speedUp, setSpeedUp] = useState("");
const [price, setPrice] = useState("");
const [description, setDescription] = useState("");
const [editPlan, setEditPlan] = useState<Plan | null>(null);
const [editForm, setEditForm] = useState({ name: "", type: "POSTPAID", speedDown: "", speedUp: "", price: "", description: "" });
const { data, isLoading, refetch } = useQuery<Plan[]>({
queryKey: ["plans"],
queryFn: async () => {
try {
const res = await api.get<Plan[] | { data: Plan[] }>("/api/v1/plans?includeInactive=true");
const d = res.data;
return Array.isArray(d) ? d : (d as { data: Plan[] }).data ?? [];
} catch {
return [];
}
},
});
const addPlanMutation = useMutation({
mutationFn: async () => {
await api.post("/api/v1/plans", {
name: planName,
type: planType,
speedDownMbps: parseInt(speedDown),
speedUpMbps: parseInt(speedUp),
monthlyPrice: parseFloat(price),
description: description || undefined,
});
},
onSuccess: () => {
toast.success("Plan created");
resetAdd();
setShowAdd(false);
refetch();
},
onError: () => toast.error("Failed to create plan"),
});
const toggleActiveMutation = useMutation({
mutationFn: async ({ id, isActive }: { id: string; isActive: boolean }) => {
await api.patch(`/api/v1/plans/${id}`, { isActive: !isActive });
},
onSuccess: () => {
toast.success("Plan updated");
refetch();
},
onError: () => toast.error("Failed to update plan"),
});
const editPlanMutation = useMutation({
mutationFn: async () => {
if (!editPlan) return;
await api.patch(`/api/v1/plans/${editPlan.id}`, {
name: editForm.name,
type: editForm.type,
speedDownMbps: parseInt(editForm.speedDown),
speedUpMbps: parseInt(editForm.speedUp),
monthlyPrice: parseFloat(editForm.price),
description: editForm.description || undefined,
});
},
onSuccess: () => {
toast.success("Plan updated");
setEditPlan(null);
refetch();
},
onError: () => toast.error("Failed to update plan"),
});
function openEdit(p: Plan) {
setEditPlan(p);
setEditForm({
name: p.name,
type: p.type,
speedDown: String(p.speedDownMbps),
speedUp: String(p.speedUpMbps),
price: String(Number(p.monthlyPrice)),
description: p.description ?? "",
});
}
function resetAdd() {
setPlanName(""); setPlanType("POSTPAID");
setSpeedDown(""); setSpeedUp(""); setPrice(""); setDescription("");
}
const plans = data ?? [];
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Plans</CardTitle>
<Button size="sm" onClick={() => setShowAdd(true)}>
+ Add Plan
</Button>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHead>
<TableRow>
<Th>Name</Th>
<Th>Type</Th>
<Th>Speed</Th>
<Th>Price</Th>
<Th>Status</Th>
<Th>Actions</Th>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
Array.from({ length: 3 }).map((_, i) => (
<TableRow key={i}>
{[1,2,3,4,5,6].map((j) => (
<Td key={j}><div className="h-4 w-20 animate-pulse bg-gray-100 rounded" /></Td>
))}
</TableRow>
))
) : plans.length === 0 ? (
<EmptyState message="No plans configured" />
) : (
plans.map((p) => (
<TableRow key={p.id}>
<Td className="font-medium">{p.name}</Td>
<Td><Badge variant="muted">{p.type}</Badge></Td>
<Td className="text-sm">{p.speedDownMbps}/{p.speedUpMbps} Mbps</Td>
<Td className="font-semibold">{formatCurrency(Number(p.monthlyPrice))}</Td>
<Td>
<Badge variant={p.isActive ? "success" : "muted"}>
{p.isActive ? "Active" : "Inactive"}
</Badge>
</Td>
<Td>
<div className="flex items-center gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => openEdit(p)}
>
Edit
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => toggleActiveMutation.mutate({ id: p.id, isActive: p.isActive })}
>
{p.isActive ? "Archive" : "Restore"}
</Button>
</div>
</Td>
</TableRow>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Edit Plan Modal */}
<Modal isOpen={!!editPlan} onClose={() => setEditPlan(null)} title="Edit Plan" className="max-w-lg">
<form onSubmit={(e) => { e.preventDefault(); editPlanMutation.mutate(); }} className="space-y-4">
<Input label="Plan Name" value={editForm.name} onChange={(e) => setEditForm(f => ({ ...f, name: e.target.value }))} />
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Type</label>
<select
className="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
value={editForm.type}
onChange={(e) => setEditForm(f => ({ ...f, type: e.target.value }))}
>
<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" min="1" value={editForm.speedDown} onChange={(e) => setEditForm(f => ({ ...f, speedDown: e.target.value }))} />
<Input label="Upload (Mbps)" type="number" min="1" value={editForm.speedUp} onChange={(e) => setEditForm(f => ({ ...f, speedUp: e.target.value }))} />
</div>
<Input label="Monthly Price (₱)" type="number" min="0" step="0.01" value={editForm.price} onChange={(e) => setEditForm(f => ({ ...f, price: e.target.value }))} />
<Input label="Description (optional)" value={editForm.description} onChange={(e) => setEditForm(f => ({ ...f, description: e.target.value }))} />
<div className="flex justify-end gap-2 pt-1">
<Button type="button" variant="outline" size="sm" onClick={() => setEditPlan(null)}>Cancel</Button>
<Button type="submit" size="sm" isLoading={editPlanMutation.isPending}>Save Changes</Button>
</div>
</form>
</Modal>
{/* Add Plan Modal */}
<Modal isOpen={showAdd} onClose={() => { setShowAdd(false); resetAdd(); }} title="Add Plan" className="max-w-lg">
<form onSubmit={(e) => { e.preventDefault(); addPlanMutation.mutate(); }} className="space-y-4">
<Input label="Plan Name" value={planName} onChange={(e) => setPlanName(e.target.value)} placeholder="e.g. Basic 10 Mbps" />
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Type</label>
<select
className="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
value={planType}
onChange={(e) => setPlanType(e.target.value)}
>
<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" min="1" value={speedDown} onChange={(e) => setSpeedDown(e.target.value)} />
<Input label="Upload (Mbps)" type="number" min="1" value={speedUp} onChange={(e) => setSpeedUp(e.target.value)} />
</div>
<Input label="Monthly Price (₱)" type="number" min="0" step="0.01" value={price} onChange={(e) => setPrice(e.target.value)} />
<Input label="Description (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
<div className="flex justify-end gap-2 pt-1">
<Button type="button" variant="outline" size="sm" onClick={() => { setShowAdd(false); resetAdd(); }}>Cancel</Button>
<Button
type="submit"
size="sm"
isLoading={addPlanMutation.isPending}
disabled={!planName.trim() || !speedDown || !speedUp || !price}
>
Create Plan
</Button>
</div>
</form>
</Modal>
</div>
);
}
// ─── Sub-page: Users ──────────────────────────────────────────────────────────
interface UserItem {
id: string; firstName: string; lastName: string; email: string;
phone?: string; isActive: boolean;
roleAssignments?: { role: string }[];
}
const ROLES = ["ADMIN", "STAFF", "COLLECTOR", "TECHNICIAN"];
function UsersSettings() {
const qc = useQueryClient();
const [showAdd, setShowAdd] = useState(false);
const [form, setForm] = useState({ firstName: "", lastName: "", email: "", password: "", phone: "", role: "STAFF" });
const { data, isLoading, refetch } = useQuery<UserItem[]>({
queryKey: ["users-settings"],
queryFn: async () => {
const res = await api.get<UserItem[]>("/api/v1/users");
return Array.isArray(res.data) ? res.data : [];
},
});
const createUser = useMutation({
mutationFn: async () => {
await api.post("/api/v1/users", {
firstName: form.firstName, lastName: form.lastName,
email: form.email, password: form.password,
phone: form.phone || undefined, role: form.role,
});
},
onSuccess: () => {
toast.success("User created!");
setShowAdd(false);
setForm({ firstName: "", lastName: "", email: "", password: "", phone: "", role: "STAFF" });
refetch();
},
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to create user"),
});
const toggleActive = useMutation({
mutationFn: async ({ id, isActive }: { id: string; isActive: boolean }) => {
await api.patch(`/api/v1/users/${id}`, { isActive: !isActive });
},
onSuccess: () => { toast.success("User updated"); refetch(); },
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed"),
});
const users = data ?? [];
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Users ({users.length})</CardTitle>
<Button size="sm" onClick={() => setShowAdd(true)}>+ Add User</Button>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHead>
<TableRow><Th>Name</Th><Th>Email</Th><Th>Role</Th><Th>Status</Th><Th>Actions</Th></TableRow>
</TableHead>
<TableBody>
{isLoading ? (
Array.from({ length: 3 }).map((_, i) => (
<TableRow key={i}>{[1,2,3,4,5].map(j => <Td key={j}><div className="h-4 w-20 animate-pulse bg-gray-100 rounded" /></Td>)}</TableRow>
))
) : users.length === 0 ? (
<EmptyState message="No users found" />
) : users.map(u => {
const role = u.roleAssignments?.[0]?.role ?? "—";
return (
<TableRow key={u.id}>
<Td className="font-medium">{u.firstName} {u.lastName}<div className="text-xs text-gray-400">{u.phone ?? ""}</div></Td>
<Td className="text-sm text-gray-600">{u.email}</Td>
<Td><Badge variant={role === "ADMIN" ? "danger" : role === "STAFF" ? "default" as any : "muted"}>{role}</Badge></Td>
<Td><Badge variant={u.isActive ? "success" : "muted"}>{u.isActive ? "Active" : "Inactive"}</Badge></Td>
<Td>
<Button size="sm" variant="ghost" onClick={() => toggleActive.mutate({ id: u.id, isActive: u.isActive })}>
{u.isActive ? "Deactivate" : "Activate"}
</Button>
</Td>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
<Modal isOpen={showAdd} onClose={() => setShowAdd(false)} title="Add New User">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3">
<Input label="First Name *" value={form.firstName} onChange={e => setForm(f => ({ ...f, firstName: e.target.value }))} />
<Input label="Last Name *" value={form.lastName} onChange={e => setForm(f => ({ ...f, lastName: e.target.value }))} />
</div>
<Input label="Email *" type="email" value={form.email} onChange={e => setForm(f => ({ ...f, email: e.target.value }))} />
<Input label="Password *" type="password" value={form.password} onChange={e => setForm(f => ({ ...f, password: e.target.value }))} hint="Minimum 8 characters" />
<Input label="Phone" value={form.phone} onChange={e => setForm(f => ({ ...f, phone: e.target.value }))} />
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Role *</label>
<select className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={form.role} onChange={e => setForm(f => ({ ...f, role: e.target.value }))}>
{ROLES.map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowAdd(false)}>Cancel</Button>
<Button onClick={() => createUser.mutate()} isLoading={createUser.isPending}
disabled={!form.firstName || !form.lastName || !form.email || form.password.length < 8}>
Create User
</Button>
</div>
</div>
</Modal>
</div>
);
}
// ─── Main Page ─────────────────────────────────────────────────────────────────
const SUB_NAV = [
{ key: "tenant", label: "Tenant", icon: Building2 },
{ key: "billing", label: "Billing", icon: CreditCard },
{ key: "areas", label: "Areas & Zones", icon: Map },
{ key: "plans", label: "Plans", icon: Wifi },
{ key: "users", label: "Users", icon: Users },
] as const;
type SubPage = typeof SUB_NAV[number]["key"];
export default function SettingsPage() {
const [active, setActive] = useState<SubPage>("tenant");
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
<p className="text-sm text-gray-500">Manage your ISP configuration</p>
</div>
<div className="flex gap-6">
{/* Left sub-nav */}
<aside className="w-48 flex-shrink-0">
<nav className="space-y-0.5">
{SUB_NAV.map(({ key, label, icon: Icon }) => (
<button
key={key}
onClick={() => setActive(key)}
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
active === key
? "bg-blue-50 text-blue-700"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
}`}
>
<Icon className="h-4 w-4 flex-shrink-0" />
<span className="flex-1 text-left">{label}</span>
{active === key && <ChevronRight className="h-3 w-3 opacity-50" />}
</button>
))}
</nav>
</aside>
{/* Content */}
<div className="flex-1 min-w-0">
{active === "tenant" && <TenantSettings />}
{active === "billing" && <BillingSettings />}
{active === "areas" && <AreasSettings />}
{active === "plans" && <PlansSettings />}
{active === "users" && <UsersSettings />}
</div>
</div>
</div>
);
}