nxtgauge-admin-solid/src/routes/admin/support.tsx

374 lines
17 KiB
TypeScript
Raw Normal View History

import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
import { A } from '@solidjs/router';
const API = '';
type SupportCase = {
id: string;
title: string;
description: string;
type: 'platform_issue' | 'customer_query' | 'professional_query' | 'billing_issue' | 'lead_dispute';
priority: 'low' | 'medium' | 'high' | 'critical';
status: 'new' | 'in_progress' | 'waiting_for_user' | 'resolved' | 'closed';
requesterName?: string;
requesterEmail?: string;
updatedAt: string;
createdAt: string;
};
const STATUS_OPTIONS: SupportCase['status'][] = ['new', 'in_progress', 'waiting_for_user', 'resolved', 'closed'];
const TYPE_OPTIONS: SupportCase['type'][] = ['platform_issue', 'customer_query', 'professional_query', 'billing_issue', 'lead_dispute'];
const PRIORITY_OPTIONS: SupportCase['priority'][] = ['low', 'medium', 'high', 'critical'];
function formatValue(input: string): string {
return input.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
function typeBadgeStyle(type: string): string {
const map: Record<string, string> = {
platform_issue: 'background:#dbeafe;color:#1d4ed8',
customer_query: 'background:#dcfce7;color:#15803d',
billing_issue: 'background:#ffedd5;color:#c2410c',
lead_dispute: 'background:#fee2e2;color:#b91c1c',
professional_query: 'background:#f3e8ff;color:#7e22ce',
};
return map[type] || 'background:#f1f5f9;color:#475569';
}
function priorityBadgeStyle(priority: string): string {
const map: Record<string, string> = {
low: 'background:#f1f5f9;color:#475569',
medium: 'background:#dbeafe;color:#1d4ed8',
high: 'background:#ffedd5;color:#c2410c',
critical: 'background:#fee2e2;color:#b91c1c',
};
return map[priority] || 'background:#f1f5f9;color:#475569';
}
function statusBadgeStyle(status: string): string {
const map: Record<string, string> = {
new: 'background:#dbeafe;color:#1d4ed8',
in_progress: 'background:#ffedd5;color:#c2410c',
waiting_for_user: 'background:#fef9c3;color:#a16207',
resolved: 'background:#dcfce7;color:#15803d',
closed: 'background:#f1f5f9;color:#475569',
};
return map[status] || 'background:#f1f5f9;color:#475569';
}
const BADGE_STYLE = 'display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600';
async function loadAllCases(): Promise<SupportCase[]> {
try {
const res = await fetch(`${API}/api/admin/support-cases`);
if (!res.ok) throw new Error('Failed');
const data = await res.json();
return Array.isArray(data.cases) ? data.cases : Array.isArray(data) ? data : [];
} catch {
return [];
}
}
export default function SupportPage() {
const [activeTab, setActiveTab] = createSignal<'queue' | 'create'>('queue');
const [statusFilter, setStatusFilter] = createSignal<'all' | SupportCase['status']>('all');
const [refetchKey, setRefetchKey] = createSignal(0);
const [cases] = createResource(refetchKey, loadAllCases);
const refetch = () => setRefetchKey((k) => k + 1);
const filteredCases = createMemo(() => {
const all = cases() ?? [];
const sf = statusFilter();
if (sf === 'all') return all;
return all.filter((c) => c.status === sf);
});
const stats = createMemo(() => {
const all = cases() ?? [];
return {
newCount: all.filter((c) => c.status === 'new').length,
inProgressCount: all.filter((c) => c.status === 'in_progress').length,
waitingCount: all.filter((c) => c.status === 'waiting_for_user').length,
total: all.length,
};
});
// Create Case form state
const [fTitle, setFTitle] = createSignal('');
const [fDesc, setFDesc] = createSignal('');
const [fType, setFType] = createSignal<SupportCase['type']>('customer_query');
const [fPriority, setFPriority] = createSignal<SupportCase['priority']>('medium');
const [fRequesterName, setFRequesterName] = createSignal('');
const [fRequesterEmail, setFRequesterEmail] = createSignal('');
const [createLoading, setCreateLoading] = createSignal(false);
const [createSuccess, setCreateSuccess] = createSignal('');
const [createError, setCreateError] = createSignal('');
const resetForm = () => {
setFTitle('');
setFDesc('');
setFType('customer_query');
setFPriority('medium');
setFRequesterName('');
setFRequesterEmail('');
};
const handleCreate = async (e: Event) => {
e.preventDefault();
setCreateLoading(true);
setCreateSuccess('');
setCreateError('');
try {
const res = await fetch(`${API}/api/admin/support-cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: fTitle(),
description: fDesc(),
type: fType(),
priority: fPriority(),
requesterName: fRequesterName(),
requesterEmail: fRequesterEmail(),
}),
});
if (!res.ok) {
const d = await res.json().catch(() => ({}));
throw new Error((d as any).message || 'Failed to create case');
}
setCreateSuccess('Case created!');
resetForm();
refetch();
setActiveTab('queue');
} catch (err: any) {
setCreateError(err.message || 'Failed to create case');
} finally {
setCreateLoading(false);
}
};
const statCards = [
{ label: 'New', getValue: () => stats().newCount },
{ label: 'In Progress', getValue: () => stats().inProgressCount },
{ label: 'Waiting', getValue: () => stats().waitingCount },
{ label: 'Total', getValue: () => stats().total },
];
return (
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="bg-white border-b border-gray-200 px-6 py-4">
<h1 class="text-xl font-semibold text-gray-900">Support Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Handle platform issues and customer queries</p>
</div>
{/* Tabs */}
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
<button
type="button"
class={activeTab() === 'queue'
? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium'
: 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
onClick={() => setActiveTab('queue')}
>
Support Queue
</button>
<button
type="button"
class={activeTab() === 'create'
? 'py-3 border-b-2 border-orange-500 text-orange-600 text-sm font-medium'
: 'py-3 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium transition-colors'}
onClick={() => setActiveTab('create')}
>
Create Case
</button>
</div>
<div class="flex-1 p-6">
{/* Stats bar */}
<div style="display:flex;gap:12px;margin-bottom:16px">
<For each={statCards}>
{(card) => (
<div style="background:#f8f9fa;border:1px solid #e5e7eb;border-radius:8px;padding:12px 20px;text-align:center">
<div style="font-size:24px;font-weight:700">{card.getValue()}</div>
<div style="font-size:12px;color:#6b7280">{card.label}</div>
</div>
)}
</For>
</div>
{/* Support Queue Tab */}
<Show when={activeTab() === 'queue'}>
<div style="display:flex;flex-direction:column;gap:16px">
<div style="display:flex;align-items:center;justify-content:flex-end">
<select
value={statusFilter()}
onChange={(e) => setStatusFilter(e.currentTarget.value as typeof statusFilter extends () => infer R ? R : never)}
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
>
<option value="all">All statuses</option>
<For each={STATUS_OPTIONS}>
{(s) => <option value={s}>{formatValue(s)}</option>}
</For>
</select>
</div>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Issue</th>
<th>Type</th>
<th>Priority</th>
<th>Status</th>
<th>Requester</th>
<th>Updated At</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={cases.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!cases.loading && cases.error}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#b91c1c">Failed to load cases.</td></tr>
</Show>
<Show when={!cases.loading && !cases.error && filteredCases().length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">No support cases found.</td></tr>
</Show>
<Show when={!cases.loading && !cases.error && filteredCases().length > 0}>
<For each={filteredCases()}>
{(item) => (
<tr class="hover:bg-slate-50" style="cursor:pointer" onClick={() => {}}>
<td>
<div class="font-semibold text-slate-900">{item.title}</div>
<div style="font-size:12px;color:#64748b;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{item.description}</div>
</td>
<td>
<span style={`${BADGE_STYLE};${typeBadgeStyle(item.type)}`}>{formatValue(item.type)}</span>
</td>
<td>
<span style={`${BADGE_STYLE};${priorityBadgeStyle(item.priority)}`}>{formatValue(item.priority)}</span>
</td>
<td>
<span style={`${BADGE_STYLE};${statusBadgeStyle(item.status)}`}>{formatValue(item.status)}</span>
</td>
<td>
<div style="font-size:13px">{item.requesterName || '—'}</div>
<div style="font-size:11px;color:#64748b">{item.requesterEmail || ''}</div>
</td>
<td class="text-slate-500" style="font-size:12px">
{item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '—'}
</td>
<td>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/support/${item.id}`}>View</A>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</div>
</div>
</Show>
{/* Create Case Tab */}
<Show when={activeTab() === 'create'}>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:600px">
<h2 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1e293b">Create Support Case</h2>
<p style="margin:0 0 20px;font-size:13px;color:#64748b">
Create an internal support record for platform issues, customer concerns, or compensation-related reviews.
</p>
<Show when={createSuccess()}>
<div style="background:#dcfce7;border:1px solid #86efac;border-radius:6px;padding:10px 14px;margin-bottom:14px;font-size:14px;color:#15803d;font-weight:600">
{createSuccess()}
</div>
</Show>
<Show when={createError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{createError()}</div>
</Show>
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Title</label>
<input
type="text"
required
value={fTitle()}
onInput={(e) => setFTitle(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Description</label>
<textarea
required
rows="4"
value={fDesc()}
onInput={(e) => setFDesc(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical;box-sizing:border-box"
/>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Type</label>
<select
value={fType()}
onChange={(e) => setFType(e.currentTarget.value as SupportCase['type'])}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<For each={TYPE_OPTIONS}>
{(t) => <option value={t}>{formatValue(t)}</option>}
</For>
</select>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Priority</label>
<select
value={fPriority()}
onChange={(e) => setFPriority(e.currentTarget.value as SupportCase['priority'])}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
>
<For each={PRIORITY_OPTIONS}>
{(p) => <option value={p}>{formatValue(p)}</option>}
</For>
</select>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Requester Name</label>
<input
type="text"
value={fRequesterName()}
onInput={(e) => setFRequesterName(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Requester Email</label>
<input
type="email"
value={fRequesterEmail()}
onInput={(e) => setFRequesterEmail(e.currentTarget.value)}
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
</div>
<div>
<button class="btn-primary" type="submit" disabled={createLoading()}>
{createLoading() ? 'Creating...' : 'Create Support Case'}
</button>
</div>
</form>
</section>
</Show>
</div>
</div>
);
}