218 lines
9.2 KiB
TypeScript
218 lines
9.2 KiB
TypeScript
|
|
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_min?: number | null;
|
||
|
|
budget_max?: number | null;
|
||
|
|
area?: string | null;
|
||
|
|
city?: 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: '',
|
||
|
|
city: '',
|
||
|
|
});
|
||
|
|
|
||
|
|
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_min: form().budget_min ? Number(form().budget_min) : undefined,
|
||
|
|
budget_max: form().budget_max ? Number(form().budget_max) : undefined,
|
||
|
|
area: form().area.trim() || undefined,
|
||
|
|
city: 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: '', city: '' });
|
||
|
|
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}>City</label>
|
||
|
|
<input value={form().city} onInput={(e) => setField('city', 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.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>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|