ifixkart-admin/app/(admin)/services/page.tsx

211 lines
10 KiB
TypeScript

/**
* @page Service Jobs Dashboard ERP/CRM (`app/(admin)/services/page.tsx`)
* @purpose General dashboard queue for active scheduling service jobs, calendars, and technician tracking.
*/
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { adminService } from '@/services/api/adminService';
import {
Wrench,
Clock,
Calendar,
CheckCircle,
AlertCircle,
Loader2,
Plus,
Filter,
FileText,
UserCheck
} from 'lucide-react';
import { formatCurrency } from '@/lib/utils';
export default function AdminServicesDashboard() {
const [jobs, setJobs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [filterStatus, setFilterStatus] = useState<string>('ALL');
useEffect(() => {
loadJobs();
}, []);
const loadJobs = async () => {
setLoading(true);
setErrorMsg(null);
try {
const data = await adminService.listServiceJobs();
// Deduplicate by job_id to prevent React key conflicts
const seen = new Set<string>();
const unique = (data as any[]).filter(j => {
if (seen.has(j.job_id)) return false;
seen.add(j.job_id);
return true;
});
setJobs(unique);
} catch (err: any) {
setErrorMsg(err.message || 'Failed to load service job queues.');
} finally {
setLoading(false);
}
};
const filteredJobs = filterStatus === 'ALL'
? jobs
: jobs.filter(j => j.status === filterStatus);
if (loading) {
return (
<div className="w-full min-h-[400px] flex items-center justify-center">
<Loader2 className="w-8 h-8 text-[#e4382f] animate-spin" />
</div>
);
}
return (
<div className="space-y-6 font-sans text-xs">
{/* Header section */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-lg font-black text-[#1b2559] uppercase tracking-wider flex items-center gap-2">
<Wrench className="w-5 h-5 text-[#e4382f]" /> Service Jobs & Repairs
</h1>
<p className="text-[10px] text-gray-400 font-bold uppercase mt-1">
Manage online slot appointments and walk-in counter diagnostics
</p>
</div>
<div className="flex gap-2">
<Link
href="/services/intake"
className="bg-[#1b2559] hover:bg-[#1b2559]/90 text-white font-extrabold uppercase py-2.5 px-4 rounded-xl flex items-center gap-1.5 transition"
>
<Plus className="w-4 h-4" /> New Walk-In Intake
</Link>
</div>
</div>
{errorMsg && (
<div className="bg-red-50 border border-red-200 text-red-700 text-xs font-semibold px-4 py-3 rounded-2xl flex items-center gap-1.5">
<AlertCircle className="w-4 h-4 text-red-650" /> {errorMsg}
</div>
)}
{/* Filter and stats cards */}
<div className="bg-white border border-gray-200 rounded-3xl p-5 shadow-sm space-y-4">
<div className="flex justify-between items-center border-b border-gray-100 pb-3">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-gray-450" />
<span className="font-extrabold text-[#1b2559] uppercase text-[10px]">Filter Status Queue</span>
</div>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="bg-gray-50 border border-gray-200 rounded-lg p-1.5 outline-none font-bold text-[10px]"
>
<option value="ALL">ALL ACTIVE QUEUES</option>
<option value="BOOKING_PENDING">PENDING DEPOSIT</option>
<option value="BOOKED">BOOKED (AWAITING DEVICE)</option>
<option value="DEVICE_RECEIVED">DEVICE RECEIVED</option>
<option value="DEVICE_INTAKE">INTAKE COMPLETE</option>
<option value="INSPECTION_PENDING">AWAITING INSPECTION</option>
<option value="INSPECTION_COMPLETED">INSPECTION DONE</option>
<option value="QUOTE_SENT">QUOTE SENT</option>
<option value="QUOTE_ACCEPTED">QUOTE APPROVED</option>
<option value="REPAIR_IN_PROGRESS">IN REPAIR</option>
<option value="REPAIR_COMPLETED">REPAIR COMPLETE</option>
<option value="FINAL_PAYMENT_PENDING">PAYMENT REQUIRED</option>
<option value="CLOSED">CLOSED</option>
</select>
</div>
{/* Table list */}
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-gray-200 text-[10px] text-gray-450 uppercase tracking-wider font-extrabold">
<th className="pb-3">Job Reference</th>
<th className="pb-3">Customer</th>
<th className="pb-3">Device / Service</th>
<th className="pb-3">Price</th>
<th className="pb-3">Current Step</th>
<th className="pb-3">Intake Date</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-bold text-[#1b2559]">
{filteredJobs.length > 0 ? (
filteredJobs.map((job) => (
<tr key={job.job_id} className="hover:bg-gray-50/50 transition">
<td className="py-3.5 font-mono text-[10.5px]">
{job.job_no}
</td>
<td className="py-3.5">
<span className="block font-extrabold text-slate-100">{job.customer_name || 'Guest Customer'}</span>
<span className="text-[10px] text-slate-400 font-medium block mt-0.5">{job.customer_phone && job.customer_phone !== 'N/A' ? job.customer_phone : (job.customer_email && job.customer_email !== 'N/A' ? job.customer_email : '—')}</span>
</td>
<td className="py-3.5">
<span className="block font-extrabold">{job.device_brand} {job.device_model}</span>
<span className="text-[10px] text-gray-400 font-medium block mt-0.5">{job.service_name}</span>
</td>
<td className="py-3.5 text-gray-700">
{formatCurrency(job.base_price)}
</td>
<td className="py-3.5">
<select
value={job.status}
onChange={async (e) => {
const newStatus = e.target.value;
try {
await adminService.updateServiceJobStatus(job.job_id, newStatus);
setJobs(prevJobs =>
prevJobs.map(j =>
j.job_id === job.job_id ? { ...j, status: newStatus } : j
)
);
} catch (err: any) {
setErrorMsg(err.message || 'Failed to update status.');
}
}}
className={`px-2.5 py-1 text-[9px] font-black tracking-wider uppercase rounded-lg cursor-pointer outline-none transition ${
job.status === 'CLOSED' || job.status === 'REPAIR_COMPLETED' ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40' :
job.status === 'BOOKING_PENDING' ? 'bg-rose-500/20 text-rose-300 border border-rose-500/40' :
job.status === 'REPAIR_IN_PROGRESS' || job.status === 'INSPECTION_PENDING' || job.status === 'INSPECTION_COMPLETED' ? 'bg-amber-500/20 text-amber-300 border border-amber-500/40' :
job.status === 'QUOTE_SENT' || job.status === 'QUOTE_ACCEPTED' ? 'bg-purple-500/20 text-purple-300 border border-purple-500/40' :
'bg-sky-500/20 text-sky-300 border border-sky-500/40'
}`}
>
<option className="bg-[#131d38] text-slate-100 font-bold" value="BOOKING_PENDING">PENDING DEPOSIT</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="BOOKED">BOOKED (AWAITING DEVICE)</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="DEVICE_RECEIVED">DEVICE RECEIVED</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="DEVICE_INTAKE">INTAKE COMPLETE</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="INSPECTION_PENDING">AWAITING INSPECTION</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="INSPECTION_COMPLETED">INSPECTION DONE</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="QUOTE_SENT">QUOTE SENT</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="QUOTE_ACCEPTED">QUOTE APPROVED</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="REPAIR_IN_PROGRESS">IN REPAIR</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="REPAIR_COMPLETED">REPAIR COMPLETE</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="FINAL_PAYMENT_PENDING">PAYMENT REQUIRED</option>
<option className="bg-[#131d38] text-slate-100 font-bold" value="CLOSED">CLOSED</option>
</select>
</td>
<td className="py-3.5 font-mono text-[10px] text-gray-400">
{new Date(job.created_at).toLocaleDateString()}
</td>
</tr>
))
) : (
<tr>
<td colSpan={6} className="py-8 text-center text-gray-450">
No active service jobs match this queue filter.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
}