/** * Customer Requirements Page - Wired to real backend APIs * Endpoints: * GET /api/customers/requirements - List customer's requirements * POST /api/customers/requirements - Create new requirement * PATCH /api/customers/requirements/:id - Update requirement * DELETE /api/customers/requirements/:id - Delete requirement */ import { For, Show, createMemo, createSignal, onMount } from "solid-js"; import { BTN_GHOST, BTN_PRIMARY, CARD, INPUT, LABEL } from "~/components/DashboardShell"; const API = "/api"; type RequirementItem = { id: string; title: string; description?: string | null; status?: string; budget_inr?: number | null; area?: string | null; location?: string | null; tags?: string[] | null; created_at?: string; }; type SortKey = "newest" | "budget_desc" | "budget_asc" | "title_asc"; async function apiFetch(path: string, opts?: RequestInit) { const token = typeof window !== "undefined" ? window.sessionStorage.getItem("nxtgauge_access_token") || "" : ""; const cleanPath = path.startsWith("/api/") ? path.slice(4) : path; return fetch(`${API}${cleanPath}`, { ...opts, credentials: "include", headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(opts?.headers ?? {}), }, }); } export default function CustomerRequirementsPage() { const [requirements, setRequirements] = createSignal([]); const [loading, setLoading] = createSignal(true); const [busyId, setBusyId] = createSignal(null); const [saving, setSaving] = createSignal(false); const [msg, setMsg] = createSignal(""); const [err, setErr] = createSignal(""); const [search, setSearch] = createSignal(""); const [sortBy, setSortBy] = createSignal("newest"); const [activeTag, setActiveTag] = createSignal(""); const [form, setForm] = createSignal({ title: "", description: "", budget_min: "", budget_max: "", area: "", location: "", tags: "", profession_key: "", preferred_date: "", }); const rowTags = (row: RequirementItem) => Array.isArray(row.tags) ? row.tags.map((tag) => String(tag || "").trim()).filter(Boolean) : []; const availableTags = createMemo(() => { const tags = new Set(); for (const row of requirements()) { for (const tag of rowTags(row)) tags.add(tag); } return Array.from(tags).sort((a, b) => a.localeCompare(b)); }); const filteredSortedRows = createMemo(() => { const q = search().trim().toLowerCase(); const tag = activeTag().trim().toLowerCase(); const next = requirements().filter((row) => { const tags = rowTags(row); const matchesTag = !tag || tags.some((t) => t.toLowerCase() === tag); if (!matchesTag) return false; if (!q) return true; return ( String(row.title || "").toLowerCase().includes(q) || String(row.description || "").toLowerCase().includes(q) || String(row.location || "").toLowerCase().includes(q) || String(row.area || "").toLowerCase().includes(q) || tags.some((t) => t.toLowerCase().includes(q)) ); }); next.sort((a, b) => { if (sortBy() === "budget_desc") return Number(b.budget_inr || 0) - Number(a.budget_inr || 0); if (sortBy() === "budget_asc") return Number(a.budget_inr || 0) - Number(b.budget_inr || 0); if (sortBy() === "title_asc") return String(a.title || "").localeCompare(String(b.title || "")); return new Date(String(b.created_at || 0)).getTime() - new Date(String(a.created_at || 0)).getTime(); }); return next; }); const loadRequirements = async () => { setLoading(true); setErr(""); try { const res = await apiFetch("/api/customers/requirements?page=1&limit=100"); const payload = await res.json().catch(() => ({})); if (!res.ok) { setErr(payload.error || payload.message || "Failed to load requirements."); setRequirements([]); return; } setRequirements(Array.isArray(payload?.data) ? payload.data : []); } catch { setErr("Network error while loading requirements."); } finally { setLoading(false); } }; onMount(loadRequirements); const setField = (key: keyof ReturnType, value: string) => setForm((prev) => ({ ...prev, [key]: value })); const createRequirement = async () => { setSaving(true); setMsg(""); setErr(""); try { const payload = { profession_key: form().profession_key, title: form().title.trim(), description: form().description.trim() || "", budget: form().budget_min || form().budget_max ? Number(form().budget_min) || Number(form().budget_max) : undefined, area: form().area.trim() || undefined, location: form().location.trim() || "", tags: form().tags.split(",").map((t) => t.trim()).filter(Boolean), preferred_date: form().preferred_date || undefined, }; const res = await apiFetch("/api/customers/requirements", { method: "POST", body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (!res.ok) { setErr(data.error || data.message || "Failed to create requirement."); return; } setMsg("Requirement created."); setForm({ title: "", description: "", budget_min: "", budget_max: "", area: "", location: "", tags: "", profession_key: "", preferred_date: "", }); await loadRequirements(); } catch { setErr("Network error while creating requirement."); } finally { setSaving(false); } }; const submitRequirement = async (id: string) => { setBusyId(id); setMsg(""); setErr(""); try { const res = await apiFetch(`/api/customers/requirements/${id}/submit`, { method: "POST" }); const data = await res.json().catch(() => ({})); if (!res.ok) { setErr(data.error || data.message || "Failed to submit requirement."); return; } setMsg("Requirement submitted to verification."); await loadRequirements(); } catch { setErr("Network error while submitting requirement."); } finally { setBusyId(null); } }; return (

My Requirements

Create requirements. They move to verification first, then final approval.

{requirements().length} of 2 free requirements used. Additional requirements may require a paid plan.

Post New Requirement

setField("title", e.currentTarget.value)} style={INPUT} placeholder="Wedding photographer in Chennai" />