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

Merged
kibin merged 11 commits from feat/web-sprint-250-262 into main 2026-04-01 08:09:45 +00:00
Showing only changes of commit 95bbb198b5 - Show all commits

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>