feat(admin-ui): replace redirect aliases with applications and modules flows
This commit is contained in:
parent
19bacc5ab3
commit
931a1db71b
4 changed files with 523 additions and 18 deletions
20
src/app.css
20
src/app.css
|
|
@ -1214,6 +1214,26 @@ body {
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 60;
|
||||||
|
background: rgba(2, 6, 23, 0.55);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
width: min(560px, 100%);
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 24px 60px -28px rgba(15, 23, 42, 0.6);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.sub-card {
|
.sub-card {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,161 @@
|
||||||
import { onMount } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
import { useNavigate } from '@solidjs/router';
|
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
|
||||||
export default function ApplicationsAliasPage() {
|
const API = '/api/gateway';
|
||||||
const navigate = useNavigate();
|
|
||||||
onMount(() => navigate('/admin/jobs', { replace: true }));
|
type ApplicationRow = {
|
||||||
return <AdminShell><div class="card"><p class="notice">Redirecting to jobs management...</p></div></AdminShell>;
|
id: string;
|
||||||
|
requirementId: string;
|
||||||
|
professionalId: string;
|
||||||
|
status: string;
|
||||||
|
quote?: number;
|
||||||
|
message?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Requirement = {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
location?: string;
|
||||||
|
profession?: string;
|
||||||
|
customerId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApplicationsPayload = {
|
||||||
|
applications: ApplicationRow[];
|
||||||
|
requirements: Requirement[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchApplications(): Promise<ApplicationsPayload> {
|
||||||
|
const [appRes, reqRes] = await Promise.all([
|
||||||
|
fetch(`${API}/api/responses`),
|
||||||
|
fetch(`${API}/api/requirements`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const appData = appRes.ok ? await appRes.json() : {};
|
||||||
|
const reqData = reqRes.ok ? await reqRes.json() : {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
applications: appData?.responses || appData?.applications || [],
|
||||||
|
requirements: reqData?.requirements || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ApplicationsPage() {
|
||||||
|
const [refreshToken, setRefreshToken] = createSignal(0);
|
||||||
|
const [busyId, setBusyId] = createSignal('');
|
||||||
|
const [error, setError] = createSignal('');
|
||||||
|
|
||||||
|
const [payload] = createResource(refreshToken, fetchApplications);
|
||||||
|
const applications = createMemo(() => payload()?.applications || []);
|
||||||
|
const requirements = createMemo(() => payload()?.requirements || []);
|
||||||
|
|
||||||
|
async function updateStatus(id: string, status: string) {
|
||||||
|
setBusyId(id);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/api/responses/${id}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const message = await res.text();
|
||||||
|
throw new Error(message || 'Failed to update status');
|
||||||
|
}
|
||||||
|
setRefreshToken((value) => value + 1);
|
||||||
|
} catch (nextError: any) {
|
||||||
|
setError(nextError?.message || 'Failed to update status');
|
||||||
|
} finally {
|
||||||
|
setBusyId('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requirementFor(id: string): Requirement | undefined {
|
||||||
|
return requirements().find((item) => item.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusClass(status: string): string {
|
||||||
|
if (status === 'ACCEPTED') return 'status-approved';
|
||||||
|
if (status === 'REJECTED' || status === 'WITHDRAWN') return 'status-rejected';
|
||||||
|
return 'status-pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminShell>
|
||||||
|
<div class="page-hero-card">
|
||||||
|
<h1 class="page-title">Applications</h1>
|
||||||
|
<p class="page-subtitle">Review submitted applications and update acceptance status.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={payload.loading}>
|
||||||
|
<div class="card"><p class="notice">Loading applications...</p></div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={error()}>
|
||||||
|
<div class="card"><p class="error-note">{error()}</p></div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!payload.loading && applications().length === 0}>
|
||||||
|
<div class="card"><p class="notice">No applications found.</p></div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="list-grid" style="grid-template-columns:1fr;gap:14px">
|
||||||
|
<For each={applications()}>
|
||||||
|
{(app) => {
|
||||||
|
const req = createMemo(() => requirementFor(app.requirementId));
|
||||||
|
return (
|
||||||
|
<article class="card">
|
||||||
|
<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap">
|
||||||
|
<div>
|
||||||
|
<h2 style="margin:0;font-size:19px">{req()?.title || 'Unknown Requirement'}</h2>
|
||||||
|
<p class="notice" style="margin:6px 0 0">ID: {app.id}</p>
|
||||||
|
</div>
|
||||||
|
<span class={`status-pill ${statusClass(app.status)}`}>{app.status}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sub-card">
|
||||||
|
<p class="kv-label">Message</p>
|
||||||
|
<p class="kv-value" style="font-weight:500">{app.message || 'No message provided.'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-grid-2" style="margin-top:12px">
|
||||||
|
<div class="kv-item">
|
||||||
|
<p class="kv-label">Quote</p>
|
||||||
|
<p class="kv-value">₹ {app.quote || 0}</p>
|
||||||
|
</div>
|
||||||
|
<div class="kv-item">
|
||||||
|
<p class="kv-label">Category</p>
|
||||||
|
<p class="kv-value">{req()?.profession || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="kv-item">
|
||||||
|
<p class="kv-label">Location</p>
|
||||||
|
<p class="kv-value">{req()?.location || '—'}</p>
|
||||||
|
</div>
|
||||||
|
<div class="kv-item">
|
||||||
|
<p class="kv-label">Applied On</p>
|
||||||
|
<p class="kv-value">{app.createdAt ? new Date(app.createdAt).toLocaleDateString() : '—'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<Show when={app.status === 'SUBMITTED'}>
|
||||||
|
<button class="btn primary" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'ACCEPTED')}>Accept Bid</button>
|
||||||
|
<button class="btn danger" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'REJECTED')}>Decline</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={app.status === 'SHORTLISTED'}>
|
||||||
|
<button class="btn primary" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'ACCEPTED')}>Accept Bid</button>
|
||||||
|
<button class="btn danger" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'REJECTED')}>Decline</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={app.status === 'SUBMITTED' || app.status === 'SHORTLISTED'}>
|
||||||
|
<button class="btn" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'WITHDRAWN')}>Withdraw</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</AdminShell>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,225 @@
|
||||||
import { onMount } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
import { useNavigate } from '@solidjs/router';
|
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
|
||||||
export default function ModulesAliasPage() {
|
const API = '/api/gateway';
|
||||||
const navigate = useNavigate();
|
|
||||||
onMount(() => navigate('/admin/internal-dashboard-management', { replace: true }));
|
type ModuleRecord = {
|
||||||
return <AdminShell><div class="card"><p class="notice">Redirecting to dashboard module configuration...</p></div></AdminShell>;
|
id: string;
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
description?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModuleForm = {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_FORM: ModuleForm = { name: '', key: '', description: '', isActive: true };
|
||||||
|
|
||||||
|
async function fetchModules(): Promise<ModuleRecord[]> {
|
||||||
|
const res = await fetch(`${API}/api/modules`);
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const data = await res.json();
|
||||||
|
return Array.isArray(data) ? data : data?.modules || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModulesPage() {
|
||||||
|
const [refreshToken, setRefreshToken] = createSignal(0);
|
||||||
|
const [isModalOpen, setIsModalOpen] = createSignal(false);
|
||||||
|
const [editing, setEditing] = createSignal<ModuleRecord | null>(null);
|
||||||
|
const [form, setForm] = createSignal<ModuleForm>({ ...EMPTY_FORM });
|
||||||
|
const [error, setError] = createSignal('');
|
||||||
|
const [submitting, setSubmitting] = createSignal(false);
|
||||||
|
|
||||||
|
const [modules] = createResource(refreshToken, fetchModules);
|
||||||
|
const modalTitle = createMemo(() => editing() ? 'Edit Module' : 'Create Module');
|
||||||
|
|
||||||
|
function openModal(item?: ModuleRecord) {
|
||||||
|
if (item) {
|
||||||
|
setEditing(item);
|
||||||
|
setForm({
|
||||||
|
name: item.name || '',
|
||||||
|
key: item.key || '',
|
||||||
|
description: item.description || '',
|
||||||
|
isActive: Boolean(item.isActive),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setEditing(null);
|
||||||
|
setForm({ ...EMPTY_FORM });
|
||||||
|
}
|
||||||
|
setError('');
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setEditing(null);
|
||||||
|
setForm({ ...EMPTY_FORM });
|
||||||
|
setError('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const current = form();
|
||||||
|
if (!current.name.trim() || !current.key.trim()) {
|
||||||
|
setError('Name and key are required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const target = editing();
|
||||||
|
const method = target ? 'PATCH' : 'POST';
|
||||||
|
const url = target ? `${API}/api/modules/${target.id}` : `${API}/api/modules`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(current),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const message = await res.text();
|
||||||
|
throw new Error(message || 'Failed to save module');
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
setRefreshToken((value) => value + 1);
|
||||||
|
} catch (nextError: any) {
|
||||||
|
setError(nextError?.message || 'Failed to save module');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeModule(id: string) {
|
||||||
|
if (!confirm('Are you sure you want to delete this module?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/api/modules/${id}`, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Delete failed');
|
||||||
|
setRefreshToken((value) => value + 1);
|
||||||
|
} catch {
|
||||||
|
setError('Failed to delete module.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminShell>
|
||||||
|
<div class="page-hero-card page-actions">
|
||||||
|
<div>
|
||||||
|
<h1 class="page-title">Module Registry</h1>
|
||||||
|
<p class="page-subtitle">Manage internal module definitions and activation state.</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn primary" onClick={() => openModal()}>Add Module</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={error() && !isModalOpen()}>
|
||||||
|
<div class="card"><p class="error-note">{error()}</p></div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={modules.loading}>
|
||||||
|
<div class="card"><p class="notice">Loading modules...</p></div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="list-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th class="align-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Show when={(modules() || []).length > 0} fallback={
|
||||||
|
<tr><td colspan="5" class="notice" style="padding:16px">No modules found.</td></tr>
|
||||||
|
}>
|
||||||
|
<For each={modules() || []}>
|
||||||
|
{(item) => (
|
||||||
|
<tr>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td><code>{item.key}</code></td>
|
||||||
|
<td>{item.description || '—'}</td>
|
||||||
|
<td>
|
||||||
|
<span class={`status-pill ${item.isActive ? 'status-approved' : 'status-rejected'}`}>
|
||||||
|
{item.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="align-right">
|
||||||
|
<div class="table-actions">
|
||||||
|
<button class="btn" onClick={() => openModal(item)}>Edit</button>
|
||||||
|
<button class="btn danger" onClick={() => removeModule(item.id)}>Delete</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isModalOpen()}>
|
||||||
|
<div class="modal-backdrop">
|
||||||
|
<div class="modal">
|
||||||
|
<h2 style="margin-top:0">{modalTitle()}</h2>
|
||||||
|
<form onSubmit={submitForm}>
|
||||||
|
<div class="field">
|
||||||
|
<label>Name</label>
|
||||||
|
<input
|
||||||
|
value={form().name}
|
||||||
|
onInput={(event) => setForm((prev) => ({ ...prev, name: event.currentTarget.value }))}
|
||||||
|
placeholder="e.g. Job Board"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Key</label>
|
||||||
|
<input
|
||||||
|
value={form().key}
|
||||||
|
onInput={(event) => setForm((prev) => ({ ...prev, key: event.currentTarget.value }))}
|
||||||
|
placeholder="e.g. manage_jobs"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea
|
||||||
|
rows="3"
|
||||||
|
value={form().description}
|
||||||
|
onInput={(event) => setForm((prev) => ({ ...prev, description: event.currentTarget.value }))}
|
||||||
|
placeholder="Short description..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label style="display:flex;gap:8px;align-items:center;font-size:13px">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form().isActive}
|
||||||
|
onChange={(event) => setForm((prev) => ({ ...prev, isActive: event.currentTarget.checked }))}
|
||||||
|
/>
|
||||||
|
Active
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Show when={error()}>
|
||||||
|
<p class="error-note">{error()}</p>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="actions" style="justify-content:flex-end">
|
||||||
|
<button type="button" class="btn" onClick={closeModal}>Cancel</button>
|
||||||
|
<button type="submit" class="btn primary" disabled={submitting()}>
|
||||||
|
{editing() ? 'Save Changes' : 'Create'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</AdminShell>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,126 @@
|
||||||
import { onMount } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
import { useNavigate } from '@solidjs/router';
|
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
|
||||||
export default function ResponsesAliasPage() {
|
const API = '/api/gateway';
|
||||||
const navigate = useNavigate();
|
|
||||||
onMount(() => navigate('/admin/leads', { replace: true }));
|
type Requirement = {
|
||||||
return <AdminShell><div class="card"><p class="notice">Redirecting to lead responses management...</p></div></AdminShell>;
|
id: string;
|
||||||
|
customerId?: string;
|
||||||
|
title?: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponseRow = {
|
||||||
|
id: string;
|
||||||
|
requirementId: string;
|
||||||
|
professionalId: string;
|
||||||
|
professionalName?: string;
|
||||||
|
quote?: number;
|
||||||
|
message?: string;
|
||||||
|
status: string;
|
||||||
|
createdAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResponsesPayload = {
|
||||||
|
responses: ResponseRow[];
|
||||||
|
requirements: Requirement[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchResponses(): Promise<ResponsesPayload> {
|
||||||
|
const [resRes, reqRes] = await Promise.all([
|
||||||
|
fetch(`${API}/api/responses`),
|
||||||
|
fetch(`${API}/api/requirements`),
|
||||||
|
]);
|
||||||
|
const resData = resRes.ok ? await resRes.json() : {};
|
||||||
|
const reqData = reqRes.ok ? await reqRes.json() : {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
responses: resData?.responses || [],
|
||||||
|
requirements: reqData?.requirements || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ResponsesPage() {
|
||||||
|
const [refreshToken, setRefreshToken] = createSignal(0);
|
||||||
|
const [error, setError] = createSignal('');
|
||||||
|
const [busyId, setBusyId] = createSignal('');
|
||||||
|
|
||||||
|
const [payload] = createResource(refreshToken, fetchResponses);
|
||||||
|
const responses = createMemo(() => payload()?.responses || []);
|
||||||
|
const requirements = createMemo(() => payload()?.requirements || []);
|
||||||
|
|
||||||
|
function requirementTitle(id: string): string {
|
||||||
|
return requirements().find((item) => item.id === id)?.title || id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transition(id: string, status: string) {
|
||||||
|
setError('');
|
||||||
|
setBusyId(id);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/api/responses/${id}/status`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const message = await res.text();
|
||||||
|
throw new Error(message || 'Failed to update response');
|
||||||
|
}
|
||||||
|
setRefreshToken((value) => value + 1);
|
||||||
|
} catch (nextError: any) {
|
||||||
|
setError(nextError?.message || 'Failed to update response');
|
||||||
|
} finally {
|
||||||
|
setBusyId('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminShell>
|
||||||
|
<div class="page-hero-card">
|
||||||
|
<h1 class="page-title">Responses</h1>
|
||||||
|
<p class="page-subtitle">Track professional responses and move them through shortlist, accept, or reject states.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={payload.loading}>
|
||||||
|
<div class="card"><p class="notice">Loading responses...</p></div>
|
||||||
|
</Show>
|
||||||
|
<Show when={error()}>
|
||||||
|
<div class="card"><p class="error-note">{error()}</p></div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<Show when={!payload.loading && responses().length === 0} fallback={
|
||||||
|
<div class="list-grid" style="grid-template-columns:1fr;gap:12px">
|
||||||
|
<For each={responses()}>
|
||||||
|
{(row) => (
|
||||||
|
<div class="list-item">
|
||||||
|
<p style="margin:0;font-weight:700;color:#0f172a">{requirementTitle(row.requirementId)}</p>
|
||||||
|
<p class="notice" style="margin:6px 0 0">{row.message || 'No message'}</p>
|
||||||
|
<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap">
|
||||||
|
<span class="meta-chip">Professional: {row.professionalName || row.professionalId}</span>
|
||||||
|
<span class="meta-chip">Quote: {row.quote || 0}</span>
|
||||||
|
<span class={`status-pill ${row.status === 'ACCEPTED' ? 'status-approved' : row.status === 'REJECTED' ? 'status-rejected' : 'status-pending'}`}>{row.status}</span>
|
||||||
|
<Show when={row.createdAt}>
|
||||||
|
<span class="meta-chip">{new Date(row.createdAt!).toLocaleDateString()}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<Show when={row.status === 'SUBMITTED'}>
|
||||||
|
<button class="btn" disabled={busyId() === row.id} onClick={() => transition(row.id, 'SHORTLISTED')}>Shortlist</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={row.status === 'SUBMITTED' || row.status === 'SHORTLISTED'}>
|
||||||
|
<button class="btn primary" disabled={busyId() === row.id} onClick={() => transition(row.id, 'ACCEPTED')}>Accept</button>
|
||||||
|
<button class="btn danger" disabled={busyId() === row.id} onClick={() => transition(row.id, 'REJECTED')}>Reject</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<p class="notice">No responses yet.</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</AdminShell>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue