123 lines
4.9 KiB
TypeScript
123 lines
4.9 KiB
TypeScript
|
|
import { A, useParams } from '@solidjs/router';
|
||
|
|
import { createMemo, createResource, createSignal, Show } from 'solid-js';
|
||
|
|
import AdminShell from '~/components/AdminShell';
|
||
|
|
|
||
|
|
const API = '/api/gateway';
|
||
|
|
|
||
|
|
type ApprovalDetail = {
|
||
|
|
id: string;
|
||
|
|
requestType?: string;
|
||
|
|
type?: string;
|
||
|
|
requestStatus?: string;
|
||
|
|
status?: string;
|
||
|
|
priority?: number;
|
||
|
|
createdAt?: string;
|
||
|
|
created_at?: string;
|
||
|
|
requester?: { name?: string; email?: string };
|
||
|
|
requesterName?: string;
|
||
|
|
requesterEmail?: string;
|
||
|
|
requester_name?: string;
|
||
|
|
requester_email?: string;
|
||
|
|
payload?: unknown;
|
||
|
|
};
|
||
|
|
|
||
|
|
async function loadApproval(id: string): Promise<ApprovalDetail | null> {
|
||
|
|
try {
|
||
|
|
const res = await fetch(`${API}/api/admin/approvals/${id}`);
|
||
|
|
if (!res.ok) return null;
|
||
|
|
return res.json();
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function ApprovalDetailPage() {
|
||
|
|
const params = useParams();
|
||
|
|
const [approval, { refetch }] = createResource(() => params.id, loadApproval);
|
||
|
|
const [acting, setActing] = createSignal('');
|
||
|
|
const [error, setError] = createSignal('');
|
||
|
|
|
||
|
|
const status = createMemo(() => (approval()?.requestStatus || approval()?.status || 'PENDING').toUpperCase());
|
||
|
|
const requestType = createMemo(() => (approval()?.requestType || approval()?.type || 'OTHER').toUpperCase());
|
||
|
|
const requesterName = createMemo(() => approval()?.requester?.name || approval()?.requesterName || approval()?.requester_name || 'Unknown');
|
||
|
|
const requesterEmail = createMemo(() => approval()?.requester?.email || approval()?.requesterEmail || approval()?.requester_email || '—');
|
||
|
|
const submittedAt = createMemo(() => approval()?.createdAt || approval()?.created_at || '');
|
||
|
|
|
||
|
|
const act = async (nextStatus: 'APPROVED' | 'REJECTED' | 'CHANGES_REQUESTED') => {
|
||
|
|
try {
|
||
|
|
setActing(nextStatus);
|
||
|
|
setError('');
|
||
|
|
const res = await fetch(`${API}/api/admin/approvals/${params.id}`, {
|
||
|
|
method: 'PATCH',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ status: nextStatus }),
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error(`Failed to mark as ${nextStatus.toLowerCase()}`);
|
||
|
|
refetch();
|
||
|
|
} catch (err: any) {
|
||
|
|
setError(err.message || 'Failed to update approval status');
|
||
|
|
} finally {
|
||
|
|
setActing('');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AdminShell>
|
||
|
|
<div class="page-hero-card page-actions">
|
||
|
|
<div>
|
||
|
|
<h1 class="page-title">Approval Detail</h1>
|
||
|
|
<p class="page-subtitle">Review one approval request in detail and take action.</p>
|
||
|
|
</div>
|
||
|
|
<A class="btn" href="/admin/approval">Back to Approval List</A>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Show when={error()}>
|
||
|
|
<div class="error-box">{error()}</div>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
<Show when={approval.loading}>
|
||
|
|
<div class="card"><p class="notice">Loading approval...</p></div>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
<Show when={!approval.loading && !approval()}>
|
||
|
|
<div class="card"><p class="notice">Approval request not found.</p></div>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
<Show when={approval()}>
|
||
|
|
<div class="grid" style="margin-top:0">
|
||
|
|
<section class="card">
|
||
|
|
<h2 style="margin-bottom:8px">Request Summary</h2>
|
||
|
|
<p class="notice" style="margin:0"><strong>ID:</strong> {approval()!.id}</p>
|
||
|
|
<p class="notice" style="margin:8px 0 0"><strong>Type:</strong> {requestType()}</p>
|
||
|
|
<p class="notice" style="margin:8px 0 0"><strong>Status:</strong> {status()}</p>
|
||
|
|
<p class="notice" style="margin:8px 0 0"><strong>Priority:</strong> {approval()!.priority ?? '—'}</p>
|
||
|
|
<p class="notice" style="margin:8px 0 0"><strong>Submitted:</strong> {submittedAt() ? new Date(submittedAt()).toLocaleString() : '—'}</p>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="card">
|
||
|
|
<h2 style="margin-bottom:8px">Requester</h2>
|
||
|
|
<p class="notice" style="margin:0"><strong>Name:</strong> {requesterName()}</p>
|
||
|
|
<p class="notice" style="margin:8px 0 0"><strong>Email:</strong> {requesterEmail()}</p>
|
||
|
|
<div class="actions">
|
||
|
|
<button class="btn primary" type="button" disabled={!!acting()} onClick={() => act('APPROVED')}>
|
||
|
|
{acting() === 'APPROVED' ? 'Approving...' : 'Approve'}
|
||
|
|
</button>
|
||
|
|
<button class="btn" type="button" disabled={!!acting()} onClick={() => act('CHANGES_REQUESTED')}>
|
||
|
|
{acting() === 'CHANGES_REQUESTED' ? 'Updating...' : 'Request Changes'}
|
||
|
|
</button>
|
||
|
|
<button class="btn danger" type="button" disabled={!!acting()} onClick={() => act('REJECTED')}>
|
||
|
|
{acting() === 'REJECTED' ? 'Rejecting...' : 'Reject'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<section class="card" style="margin-top:16px">
|
||
|
|
<h2 style="margin-bottom:10px">Raw Payload</h2>
|
||
|
|
<pre class="json">{JSON.stringify(approval(), null, 2)}</pre>
|
||
|
|
</section>
|
||
|
|
</Show>
|
||
|
|
</AdminShell>
|
||
|
|
);
|
||
|
|
}
|