nxtgauge-frontend-solid/src/components/dashboard/CustomerRequirementsPage.tsx

366 lines
12 KiB
TypeScript
Raw Normal View History

/**
* 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, createSignal, onMount } from "solid-js";
import { BTN_GHOST, BTN_ORANGE, CARD, INPUT, LABEL } from "~/components/DashboardShell";
const API = "/api/gateway";
type RequirementItem = {
id: string;
title: string;
description?: string | null;
status?: string;
budget_inr?: number | null;
area?: string | null;
location?: string | null;
created_at?: string;
};
async function apiFetch(path: string, opts?: RequestInit) {
return fetch(`${API}${path}`, {
...opts,
credentials: "include",
headers: { "Content-Type": "application/json", ...(opts?.headers ?? {}) },
});
}
export default function CustomerRequirementsPage() {
const [requirements, setRequirements] = createSignal<RequirementItem[]>([]);
const [loading, setLoading] = createSignal(true);
const [busyId, setBusyId] = createSignal<string | null>(null);
const [saving, setSaving] = createSignal(false);
const [msg, setMsg] = createSignal("");
const [err, setErr] = createSignal("");
const [form, setForm] = createSignal({
title: "",
description: "",
budget_min: "",
budget_max: "",
area: "",
location: "",
});
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<typeof form>, value: string) =>
setForm((prev) => ({ ...prev, [key]: value }));
const createRequirement = async () => {
setSaving(true);
setMsg("");
setErr("");
try {
const payload = {
title: form().title.trim(),
description: form().description.trim() || undefined,
budget_inr:
form().budget_min || form().budget_max
? Number(form().budget_min) || Number(form().budget_max)
: undefined,
area: form().area.trim() || undefined,
location: form().city.trim() || 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: "",
});
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 (
<div style={{ display: "grid", gap: "14px", "max-width": "980px" }}>
<div style={CARD}>
<p style={{ margin: "0", "font-size": "22px", "font-weight": "800", color: "#0D0D2A" }}>
My Requirements
</p>
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "#6B7280" }}>
Create requirements. They move to verification first, then final approval.
</p>
</div>
<div style={CARD}>
<p
style={{
margin: "0 0 10px",
"font-size": "16px",
"font-weight": "700",
color: "#111827",
}}
>
Post New Requirement
</p>
<div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "12px" }}>
<div style={{ "grid-column": "1 / -1" }}>
<label style={LABEL}>Title</label>
<input
value={form().title}
onInput={(e) => setField("title", e.currentTarget.value)}
style={INPUT}
placeholder="Wedding photographer in Chennai"
/>
</div>
<div style={{ "grid-column": "1 / -1" }}>
<label style={LABEL}>Description</label>
<textarea
rows={3}
value={form().description}
onInput={(e) => setField("description", e.currentTarget.value)}
style={{ ...INPUT, height: "auto", padding: "10px 12px", resize: "vertical" }}
placeholder="Describe what you need"
/>
</div>
<div>
<label style={LABEL}>Budget Min</label>
<input
value={form().budget_min}
onInput={(e) => setField("budget_min", e.currentTarget.value)}
style={INPUT}
placeholder="10000"
/>
</div>
<div>
<label style={LABEL}>Budget Max</label>
<input
value={form().budget_max}
onInput={(e) => setField("budget_max", e.currentTarget.value)}
style={INPUT}
placeholder="50000"
/>
</div>
<div>
<label style={LABEL}>Area</label>
<input
value={form().area}
onInput={(e) => setField("area", e.currentTarget.value)}
style={INPUT}
placeholder="T Nagar"
/>
</div>
<div>
<label style={LABEL}>Location</label>
<input
value={form().location}
onInput={(e) => setField("location", e.currentTarget.value)}
style={INPUT}
placeholder="Chennai"
/>
</div>
</div>
<div style={{ display: "flex", "justify-content": "flex-end", "margin-top": "12px" }}>
<button
type="button"
onClick={createRequirement}
disabled={saving() || !form().title.trim()}
style={{ ...BTN_ORANGE, opacity: saving() ? "0.7" : "1" }}
>
{saving() ? "Posting..." : "Post Requirement"}
</button>
</div>
</div>
<Show when={msg()}>
<div
style={{
...CARD,
border: "1px solid #BBF7D0",
background: "#ECFDF5",
padding: "12px 14px",
color: "#065F46",
"font-size": "13px",
"font-weight": "600",
}}
>
{msg()}
</div>
</Show>
<Show when={err()}>
<div
style={{
...CARD,
border: "1px solid #FECACA",
background: "#FEF2F2",
padding: "12px 14px",
color: "#B91C1C",
"font-size": "13px",
"font-weight": "600",
}}
>
{err()}
</div>
</Show>
<div style={CARD}>
<div
style={{
display: "flex",
"justify-content": "space-between",
"align-items": "center",
"margin-bottom": "10px",
}}
>
<p style={{ margin: "0", "font-size": "16px", "font-weight": "700", color: "#111827" }}>
My Requirement List
</p>
<button type="button" onClick={loadRequirements} style={BTN_GHOST}>
Refresh
</button>
</div>
<Show when={loading()}>
<p style={{ margin: "0", color: "#9CA3AF", "font-size": "13px" }}>
Loading requirements...
</p>
</Show>
<Show when={!loading() && requirements().length === 0}>
<p style={{ margin: "0", color: "#6B7280", "font-size": "13px" }}>
No requirements found.
</p>
</Show>
<Show when={!loading() && requirements().length > 0}>
<div style={{ display: "grid", gap: "10px" }}>
<For each={requirements()}>
{(row) => (
<div
style={{
border: "1px solid #E5E7EB",
"border-radius": "12px",
padding: "12px",
background: "#FCFCFD",
}}
>
<div
style={{
display: "flex",
"justify-content": "space-between",
gap: "10px",
"flex-wrap": "wrap",
}}
>
<div>
<p
style={{
margin: "0",
"font-size": "14px",
"font-weight": "800",
color: "#111827",
}}
>
{row.title}
</p>
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
{row.location || row.city || "—"} {row.area ? `${row.area}` : ""}{" "}
{row.created_at
? `${new Date(row.created_at).toLocaleString("en-IN")}`
: ""}
</p>
</div>
<span
style={{
display: "inline-flex",
height: "24px",
"align-items": "center",
padding: "0 10px",
"border-radius": "999px",
background: "#EEF2FF",
color: "#3730A3",
"font-size": "11px",
"font-weight": "700",
}}
>
{String(row.status || "DRAFT").replace(/_/g, " ")}
</span>
</div>
<p style={{ margin: "8px 0 0", "font-size": "13px", color: "#374151" }}>
{row.description || "No description added."}
</p>
<div
style={{ display: "flex", "justify-content": "flex-end", "margin-top": "10px" }}
>
<button
type="button"
onClick={() => submitRequirement(row.id)}
disabled={busyId() === row.id}
style={{
...BTN_ORANGE,
height: "32px",
"font-size": "12px",
padding: "0 12px",
opacity: busyId() === row.id ? "0.7" : "1",
}}
>
{busyId() === row.id ? "Submitting..." : "Submit for Verification"}
</button>
</div>
</div>
)}
</For>
</div>
</Show>
</div>
</div>
);
}