100kmph/app/admin/jobs/page.tsx
2025-11-06 01:24:38 +05:30

558 lines
24 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import AdminLogo from '../../components/AdminLogo';
interface JobAction {
id: number;
action_type: string;
part_type: string | null;
part_id: string | null;
custom_price: number;
labour_fee: number;
created_at: string;
}
interface Vehicle {
id: string;
make: string;
model: string;
capacity: number | null;
yom: number | null;
owner: number | null;
}
interface Customer {
id: number;
name: string;
address: string;
}
interface ServiceRecord {
id: number;
created_at: string;
vehicle: string;
metadata: string | null;
status: number;
mileage: number | null;
Vehicles?: Vehicle;
}
export default function JobsListPage() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const router = useRouter();
const [jobs, setJobs] = useState<ServiceRecord[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [customers, setCustomers] = useState<Customer[]>([]);
const [selectedJob, setSelectedJob] = useState<ServiceRecord | null>(null);
const [jobActions, setJobActions] = useState<JobAction[]>([]);
const [loadingActions, setLoadingActions] = useState(false);
useEffect(() => {
const adminLoggedIn = sessionStorage.getItem('adminLoggedIn');
if (adminLoggedIn !== 'true') {
router.push('/admin');
return;
}
setIsAuthenticated(true);
loadJobs();
loadCustomers();
}, [router]);
// Close modal when clicking outside or pressing Escape
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape' && selectedJob) {
setSelectedJob(null);
setJobActions([]);
}
};
if (selectedJob) {
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [selectedJob]);
const loadJobs = async () => {
try {
setLoading(true);
const response = await fetch('/api/admin/jobs');
if (response.ok) {
const data = await response.json();
setJobs(data);
} else {
console.error('Failed to load jobs');
}
} catch (error) {
console.error('Error loading jobs:', error);
} finally {
setLoading(false);
}
};
const loadCustomers = async () => {
try {
const response = await fetch('/api/admin/customers');
if (response.ok) {
const data = await response.json();
setCustomers(data);
}
} catch (error) {
console.error('Error loading customers:', error);
}
};
const getCustomerName = (ownerId: number | null | undefined) => {
if (!ownerId) return 'Unknown';
const customer = customers.find(c => c.id === ownerId);
return customer ? customer.name : 'Unknown';
};
const getStatusLabel = (status: number) => {
switch (status) {
case 0: return 'Pending';
case 1: return 'Active';
case 2: return 'Waiting for Customer Response';
case 3: return 'Waiting for Parts';
case 10: return 'Completed';
default: return 'Unknown';
}
};
const getStatusColor = (status: number) => {
switch (status) {
case 0: return 'text-white/60';
case 1: return 'text-cyan-400';
case 2: return 'text-yellow-400';
case 3: return 'text-orange-400';
case 10: return 'text-green-400';
default: return 'text-white/60';
}
};
const handleShowDetails = async (job: ServiceRecord) => {
setSelectedJob(job);
setLoadingActions(true);
try {
const response = await fetch(`/api/admin/jobs/${job.id}/actions`);
if (response.ok) {
const data = await response.json();
setJobActions(data);
} else {
console.error('Failed to load job actions');
setJobActions([]);
}
} catch (error) {
console.error('Error loading job actions:', error);
setJobActions([]);
} finally {
setLoadingActions(false);
}
};
const filteredJobs = jobs.filter(job => {
if (!searchQuery.trim()) return true;
const query = searchQuery.toLowerCase();
const vehicleId = job.Vehicles?.id?.toLowerCase() || '';
const vehicleMake = job.Vehicles?.make?.toLowerCase() || '';
const vehicleModel = job.Vehicles?.model?.toLowerCase() || '';
const metadata = job.metadata?.toLowerCase() || '';
const customerName = getCustomerName(job.Vehicles?.owner).toLowerCase();
const statusLabel = getStatusLabel(job.status).toLowerCase();
return (
vehicleId.includes(query) ||
vehicleMake.includes(query) ||
vehicleModel.includes(query) ||
metadata.includes(query) ||
customerName.includes(query) ||
statusLabel.includes(query)
);
});
if (!isAuthenticated) {
return null;
}
return (
<>
<AdminLogo />
<div className="relative min-h-screen bg-black tech-grid scanlines">
{/* Animated background elements */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute top-1/4 left-1/4 h-96 w-96 rounded-full bg-white/5 blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 right-1/4 h-96 w-96 rounded-full bg-white/5 blur-3xl animate-pulse delay-1000"></div>
</div>
<div className="relative mx-auto max-w-6xl px-4 py-12">
<div className="relative rounded-lg border-2 border-white/30 bg-black/80 backdrop-blur-sm p-8 neon-border-glow">
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-white/5 to-transparent"></div>
<div className="relative">
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-white animate-pulse"></div>
<h1 className="text-3xl font-bold text-white uppercase tracking-wider neon-glow">
All Jobs
</h1>
</div>
<Link
href="/admin/dashboard"
className="text-sm font-medium text-white/60 hover:text-white transition-colors font-mono uppercase tracking-wider border border-white/20 px-3 py-1 rounded hover:border-white/40 hover:bg-white/10"
>
Back to Dashboard
</Link>
</div>
{/* Search Bar */}
<div className="mb-6">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search by vehicle number, make, model, customer, status, or metadata..."
className="w-full rounded-lg border-2 border-white/20 bg-black/50 px-4 py-3 font-mono text-white placeholder-white/40 focus:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/30 neon-border transition-all"
/>
</div>
{/* Jobs List */}
{loading ? (
<div className="text-center py-12">
<div className="h-2 w-2 rounded-full bg-white animate-pulse mx-auto mb-4"></div>
<p className="text-white/60 font-mono uppercase tracking-wider">Loading Jobs...</p>
</div>
) : filteredJobs.length === 0 ? (
<div className="text-center py-12">
<p className="text-white/60 font-mono uppercase tracking-wider">
{searchQuery ? 'No jobs found matching your search' : 'No jobs found'}
</p>
</div>
) : (
<div className="space-y-4">
{filteredJobs.map((job) => (
<div
key={job.id}
className="rounded-lg border-2 border-white/20 bg-black/50 p-6 transition-all hover:border-white/40 hover:bg-white/5 hover:shadow-[0_0_15px_rgba(255,255,255,0.2)]"
>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Vehicle
</div>
<div className="text-white font-mono font-semibold">
{job.Vehicles?.id || 'N/A'}
</div>
<div className="text-sm text-white/70 font-mono">
{job.Vehicles?.make || 'N/A'} {job.Vehicles?.model || ''}
{job.Vehicles?.capacity && ` (${job.Vehicles.capacity}cc)`}
</div>
</div>
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Customer
</div>
<div className="text-white font-mono">
{getCustomerName(job.Vehicles?.owner)}
</div>
</div>
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Status
</div>
<div className={`font-mono font-semibold ${getStatusColor(job.status)}`}>
{getStatusLabel(job.status)}
</div>
</div>
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Created At
</div>
<div className="text-white font-mono text-sm">
{new Date(job.created_at).toLocaleString()}
</div>
</div>
{job.mileage !== null && (
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Mileage
</div>
<div className="text-white font-mono text-sm">
{job.mileage.toLocaleString()} km
</div>
</div>
)}
</div>
{job.metadata && (
<div className="mt-4 pt-4 border-t border-white/10">
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Notes
</div>
<div className="text-white/80 font-mono text-sm">
{job.metadata}
</div>
</div>
)}
<div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
<div className="text-xs text-white/40 font-mono">
Service Record ID: {job.id}
</div>
<div className="flex gap-2">
<button
onClick={() => router.push(`/admin/jobs/${job.id}/edit`)}
className="rounded-lg border border-white/30 bg-white/5 px-4 py-2 text-sm font-medium text-white transition-all hover:bg-white/10 hover:border-white/50 hover:shadow-[0_0_15px_rgba(255,255,255,0.3)] font-mono uppercase tracking-wider"
>
Edit
</button>
<button
onClick={() => handleShowDetails(job)}
className="rounded-lg border border-cyan-500/50 bg-cyan-500/10 px-4 py-2 text-sm font-medium text-cyan-400 transition-all hover:bg-cyan-500/20 hover:border-cyan-500 hover:shadow-[0_0_15px_rgba(0,191,255,0.4)] font-mono uppercase tracking-wider"
>
Show Details
</button>
</div>
</div>
</div>
))}
</div>
)}
{!loading && filteredJobs.length > 0 && (
<div className="mt-6 text-center">
<p className="text-white/60 font-mono text-sm uppercase tracking-wider">
Showing {filteredJobs.length} of {jobs.length} jobs
</p>
</div>
)}
</div>
</div>
</div>
</div>
{/* Job Details Modal */}
{selectedJob && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4 overflow-y-auto"
onClick={() => {
setSelectedJob(null);
setJobActions([]);
}}
>
<div
className="relative w-full max-w-4xl rounded-lg border-2 border-white/30 bg-black/95 backdrop-blur-sm p-8 neon-border-glow my-8"
onClick={(e) => e.stopPropagation()}
>
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-white/5 to-transparent"></div>
<div className="relative">
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-white animate-pulse"></div>
<h2 className="text-2xl font-bold text-white uppercase tracking-wider neon-glow">
Job Details
</h2>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => router.push(`/admin/jobs/${selectedJob.id}/edit`)}
className="rounded-lg border border-white/30 bg-white/5 px-4 py-2 text-sm font-medium text-white transition-all hover:bg-white/10 hover:border-white/50 hover:shadow-[0_0_15px_rgba(255,255,255,0.3)] font-mono uppercase tracking-wider"
>
Edit
</button>
<button
onClick={() => {
setSelectedJob(null);
setJobActions([]);
}}
className="text-white/60 hover:text-white text-2xl transition-colors"
>
</button>
</div>
</div>
{/* Service Record Details */}
<div className="mb-8 space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Service Record ID
</div>
<div className="text-white font-mono font-semibold">
#{selectedJob.id}
</div>
</div>
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Status
</div>
<div className={`font-mono font-semibold ${getStatusColor(selectedJob.status)}`}>
{getStatusLabel(selectedJob.status)}
</div>
</div>
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Vehicle Number
</div>
<div className="text-white font-mono font-semibold">
{selectedJob.Vehicles?.id || 'N/A'}
</div>
<div className="text-sm text-white/70 font-mono">
{selectedJob.Vehicles?.make || 'N/A'} {selectedJob.Vehicles?.model || ''}
{selectedJob.Vehicles?.capacity && ` (${selectedJob.Vehicles.capacity}cc)`}
</div>
</div>
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Customer
</div>
<div className="text-white font-mono">
{getCustomerName(selectedJob.Vehicles?.owner)}
</div>
{selectedJob.Vehicles?.owner && (
<div className="text-sm text-white/60 font-mono">
Customer ID: {selectedJob.Vehicles.owner}
</div>
)}
</div>
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Created At
</div>
<div className="text-white font-mono text-sm">
{new Date(selectedJob.created_at).toLocaleString()}
</div>
</div>
{selectedJob.mileage !== null && (
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Mileage
</div>
<div className="text-white font-mono text-sm">
{selectedJob.mileage.toLocaleString()} km
</div>
</div>
)}
</div>
{selectedJob.metadata && (
<div>
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
Metadata / Notes
</div>
<div className="text-white/80 font-mono text-sm bg-black/50 rounded-lg p-4 border border-white/10">
{selectedJob.metadata}
</div>
</div>
)}
</div>
{/* Job Actions List */}
<div className="border-t border-white/20 pt-6">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-cyan-500"></div>
<h3 className="text-xl font-bold text-white uppercase tracking-wider">
Job Actions
</h3>
</div>
<div className="text-sm text-white/60 font-mono">
{jobActions.length} action{jobActions.length !== 1 ? 's' : ''}
</div>
</div>
{loadingActions ? (
<div className="text-center py-8">
<div className="h-2 w-2 rounded-full bg-white animate-pulse mx-auto mb-2"></div>
<p className="text-white/60 font-mono text-sm uppercase tracking-wider">
Loading Actions...
</p>
</div>
) : jobActions.length === 0 ? (
<div className="text-center py-8 rounded-lg border border-white/10 bg-black/50">
<p className="text-white/60 font-mono uppercase tracking-wider">
No job actions found
</p>
</div>
) : (
<div className="overflow-x-auto max-h-96 overflow-y-auto">
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-black/95">
<tr className="border-b-2 border-white/20">
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider font-mono">Action Type</th>
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider font-mono">Part Type</th>
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider font-mono">Part ID</th>
<th className="px-4 py-3 text-right text-xs font-bold text-white uppercase tracking-wider font-mono">Labour Fee</th>
<th className="px-4 py-3 text-right text-xs font-bold text-white uppercase tracking-wider font-mono">Parts Price</th>
<th className="px-4 py-3 text-right text-xs font-bold text-white uppercase tracking-wider font-mono">Total</th>
</tr>
</thead>
<tbody>
{jobActions.map((action, index) => {
const partsPrice = action.custom_price > 0 && action.custom_price !== -1 ? action.custom_price : 0;
const rowTotal = action.labour_fee + partsPrice;
return (
<tr
key={action.id}
className={`border-b border-white/10 hover:bg-white/5 transition-colors ${index % 2 === 0 ? 'bg-black/30' : 'bg-black/50'}`}
>
<td className="px-4 py-3 text-white font-mono font-semibold">{action.action_type}</td>
<td className="px-4 py-3 text-white/80 font-mono">{action.part_type || '-'}</td>
<td className="px-4 py-3 text-white/80 font-mono">{action.part_id || '-'}</td>
<td className="px-4 py-3 text-right text-white font-mono">.{action.labour_fee || 0}</td>
<td className="px-4 py-3 text-right text-white font-mono">{partsPrice > 0 ? `රු.${partsPrice}` : '-'}</td>
<td className="px-4 py-3 text-right text-white font-mono font-semibold">.{rowTotal}</td>
</tr>
);
})}
<tr className="border-t-2 border-white/30 bg-black/70 font-bold sticky bottom-0">
<td colSpan={5} className="px-4 py-4 text-right text-white font-mono uppercase tracking-wider">
Total Cost:
</td>
<td className="px-4 py-4 text-right text-white font-mono text-lg font-bold">
.{jobActions.reduce((sum, action) => {
const partsPrice = action.custom_price > 0 && action.custom_price !== -1 ? action.custom_price : 0;
return sum + action.labour_fee + partsPrice;
}, 0)}
</td>
</tr>
<tr className="bg-black/70 sticky bottom-0">
<td colSpan={6} className="px-4 pb-2 text-right text-white/60 font-mono text-xs">
<div className="flex justify-end gap-4">
<span>Labour: රු.{jobActions.reduce((sum, action) => sum + action.labour_fee, 0)}</span>
<span>Parts: රු.{jobActions.reduce((sum, action) => {
const partsPrice = action.custom_price > 0 && action.custom_price !== -1 ? action.custom_price : 0;
return sum + partsPrice;
}, 0)}</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
</div>
)}
</>
);
}