feat: web sprint 250-262 — plans/clients/tickets/audit/profile/settings/leads/reports (#26)

This commit was merged in pull request #26.
This commit is contained in:
2026-04-01 08:09:44 +00:00
parent 34549fc4b6
commit b07e2faee5
10 changed files with 772 additions and 237 deletions

View File

@@ -528,7 +528,22 @@ export default function ClientDetailPage() {
{/* Profile */}
{activeTab === "profile" && (
<Card>
<CardHeader><CardTitle>Client Profile</CardTitle></CardHeader>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Client Profile</CardTitle>
<div className="flex gap-2 flex-wrap">
<Button size="sm" variant="outline" onClick={() => router.push(`/tickets?clientId=${client.id}`)}>
<Ticket className="h-3.5 w-3.5 mr-1" /> View Tickets
</Button>
<Button size="sm" variant="outline" onClick={() => router.push(`/invoices?clientId=${client.id}`)}>
<FileText className="h-3.5 w-3.5 mr-1" /> View Invoices
</Button>
<Button size="sm" variant="outline" onClick={() => router.push(`/payments?clientId=${client.id}`)}>
<CreditCard className="h-3.5 w-3.5 mr-1" /> View Payments
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{[

View File

@@ -53,11 +53,21 @@ export default function ClientsPage() {
queryFn: async () => { const r = await api.get<Area[]>("/api/v1/areas"); return r.data; },
});
const { data: plans = [] } = useQuery<Plan[]>({
queryKey: ["plans"],
queryFn: async () => { const r = await api.get<Plan[]>("/api/v1/plans"); return Array.isArray(r.data) ? r.data : (r.data as any).data ?? []; },
// Fetch only active plans, filter client-side by billing type
const { data: allActivePlans = [] } = useQuery<(Plan & { type: string; isActive: boolean })[]>({
queryKey: ["plans", "active"],
queryFn: async () => {
const r = await api.get("/api/v1/plans?isActive=true");
const raw = Array.isArray(r.data) ? r.data : (r.data as any).data ?? [];
return raw;
},
});
// Filter plans by selected billing type
const filteredPlans = allActivePlans.filter(p =>
!form.billingType || p.type === form.billingType
);
const createClient = useMutation({
mutationFn: async () => {
const res = await api.post("/api/v1/clients", {
@@ -186,10 +196,11 @@ export default function ClientsPage() {
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-gray-700">Billing Type</label>
<select className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={form.billingType} onChange={e => setForm(f => ({ ...f, billingType: e.target.value }))}>
value={form.billingType} onChange={e => setForm(f => ({ ...f, billingType: e.target.value, planId: "" }))}>
<option value="POSTPAID">Postpaid</option>
<option value="PREPAID">Prepaid</option>
</select>
<p className="text-xs text-gray-400">Plan list filters to match this type</p>
</div>
</div>
<div className="flex flex-col gap-1.5">
@@ -197,8 +208,11 @@ export default function ClientsPage() {
<select className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={form.planId} onChange={e => setForm(f => ({ ...f, planId: e.target.value }))}>
<option value=""> Select plan </option>
{plans.map(p => <option key={p.id} value={p.id}>{p.name} {Number(p.monthlyPrice).toLocaleString()}/mo</option>)}
{filteredPlans.map(p => <option key={p.id} value={p.id}>{p.name} {Number(p.monthlyPrice).toLocaleString()}/mo</option>)}
</select>
{filteredPlans.length === 0 && form.billingType && (
<p className="text-xs text-amber-600">No active {form.billingType.toLowerCase()} plans available.</p>
)}
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" onClick={() => setShowAdd(false)}>Cancel</Button>