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

616 lines
27 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useRouter, useParams } from 'next/navigation';
import Link from 'next/link';
import AdminLogo from '../../../../components/AdminLogo';
import Notification from '../../../../components/Notification';
interface JobAction {
id: number;
action_type: string;
part_type: string | null;
part_id: string | null;
custom_price: number;
labour_fee: number;
created_at: string;
service_record: number;
}
interface Vehicle {
id: string;
make: string;
model: string;
capacity: number | null;
yom: number | null;
owner: number | null;
}
interface ServiceRecord {
id: number;
created_at: string;
vehicle: string;
metadata: string | null;
status: number;
Vehicles?: Vehicle;
}
export default function EditJobPage() {
const router = useRouter();
const params = useParams();
const jobId = params?.id as string;
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [serviceRecord, setServiceRecord] = useState<ServiceRecord | null>(null);
const [jobActions, setJobActions] = useState<JobAction[]>([]);
// Form state
const [status, setStatus] = useState<number>(0);
const [metadata, setMetadata] = useState('');
const [mileage, setMileage] = useState('');
const [showAddAction, setShowAddAction] = useState(false);
const [editingAction, setEditingAction] = useState<JobAction | null>(null);
// Notification state
const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' | 'info' } | null>(null);
// New action form state
const [newActionType, setNewActionType] = useState('');
const [newPartType, setNewPartType] = useState('');
const [newPartId, setNewPartId] = useState('');
const [newCustomPrice, setNewCustomPrice] = useState('');
const [newLabourFee, setNewLabourFee] = useState('');
useEffect(() => {
const adminLoggedIn = sessionStorage.getItem('adminLoggedIn');
if (adminLoggedIn !== 'true') {
router.push('/admin');
return;
}
setIsAuthenticated(true);
if (jobId) {
loadJobData();
}
}, [jobId, router]);
const loadJobData = async () => {
try {
setLoading(true);
const [recordResponse, actionsResponse] = await Promise.all([
fetch(`/api/admin/jobs/${jobId}`),
fetch(`/api/admin/jobs/${jobId}/actions`)
]);
if (recordResponse.ok) {
const recordData = await recordResponse.json();
setServiceRecord(recordData);
setStatus(recordData.status);
setMetadata(recordData.metadata || '');
setMileage(recordData.mileage ? recordData.mileage.toString() : '');
}
if (actionsResponse.ok) {
const actionsData = await actionsResponse.json();
setJobActions(actionsData);
}
} catch (error) {
console.error('Error loading job data:', error);
} finally {
setLoading(false);
}
};
const handleSaveServiceRecord = async () => {
try {
setSaving(true);
const response = await fetch(`/api/admin/jobs/${jobId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status,
metadata: metadata || null,
mileage: mileage ? parseInt(mileage) : null,
}),
});
if (response.ok) {
// Reload data
await loadJobData();
setNotification({ message: 'Service record updated successfully!', type: 'success' });
} else {
const error = await response.json();
setNotification({ message: `Failed to update: ${error.error}`, type: 'error' });
}
} catch (error) {
console.error('Error saving service record:', error);
setNotification({ message: 'Failed to save changes', type: 'error' });
} finally {
setSaving(false);
}
};
const handleAddAction = async () => {
try {
setSaving(true);
const response = await fetch(`/api/admin/jobs/${jobId}/actions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action_type: newActionType,
part_type: newPartType || null,
part_id: newPartId || null,
custom_price: newCustomPrice ? parseInt(newCustomPrice) : -1,
labour_fee: newLabourFee ? parseInt(newLabourFee) : 0,
}),
});
if (response.ok) {
await loadJobData();
setShowAddAction(false);
// Reset form
setNewActionType('');
setNewPartType('');
setNewPartId('');
setNewCustomPrice('');
setNewLabourFee('');
setNotification({ message: 'Job action added successfully!', type: 'success' });
} else {
const error = await response.json();
setNotification({ message: `Failed to add action: ${error.error}`, type: 'error' });
}
} catch (error) {
console.error('Error adding action:', error);
setNotification({ message: 'Failed to add action', type: 'error' });
} finally {
setSaving(false);
}
};
const handleEditAction = (action: JobAction) => {
setEditingAction(action);
setNewActionType(action.action_type);
setNewPartType(action.part_type || '');
setNewPartId(action.part_id || '');
setNewCustomPrice(action.custom_price > 0 && action.custom_price !== -1 ? action.custom_price.toString() : '');
setNewLabourFee(action.labour_fee > 0 ? action.labour_fee.toString() : '');
setShowAddAction(true);
};
const handleUpdateAction = async () => {
if (!editingAction) return;
try {
setSaving(true);
const response = await fetch(`/api/admin/jobs/${jobId}/actions/${editingAction.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action_type: newActionType,
part_type: newPartType || null,
part_id: newPartId || null,
custom_price: newCustomPrice ? parseInt(newCustomPrice) : -1,
labour_fee: newLabourFee ? parseInt(newLabourFee) : 0,
}),
});
if (response.ok) {
await loadJobData();
setShowAddAction(false);
setEditingAction(null);
// Reset form
setNewActionType('');
setNewPartType('');
setNewPartId('');
setNewCustomPrice('');
setNewLabourFee('');
setNotification({ message: 'Job action updated successfully!', type: 'success' });
} else {
const error = await response.json();
setNotification({ message: `Failed to update action: ${error.error}`, type: 'error' });
}
} catch (error) {
console.error('Error updating action:', error);
setNotification({ message: 'Failed to update action', type: 'error' });
} finally {
setSaving(false);
}
};
const handleDeleteAction = async (actionId: number) => {
if (!confirm('Are you sure you want to delete this job action?')) return;
try {
setSaving(true);
const response = await fetch(`/api/admin/jobs/${jobId}/actions/${actionId}`, {
method: 'DELETE',
});
if (response.ok) {
await loadJobData();
setNotification({ message: 'Job action deleted successfully!', type: 'success' });
} else {
const error = await response.json();
setNotification({ message: `Failed to delete action: ${error.error}`, type: 'error' });
}
} catch (error) {
console.error('Error deleting action:', error);
setNotification({ message: 'Failed to delete action', type: 'error' });
} finally {
setSaving(false);
}
};
const cancelEdit = () => {
setShowAddAction(false);
setEditingAction(null);
setNewActionType('');
setNewPartType('');
setNewPartId('');
setNewCustomPrice('');
setNewLabourFee('');
};
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';
}
};
if (!isAuthenticated) {
return null;
}
if (loading) {
return (
<>
<AdminLogo />
<div className="relative min-h-screen bg-black tech-grid scanlines">
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<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...</p>
</div>
</div>
</div>
</>
);
}
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">
Edit Job #{serviceRecord?.id}
</h1>
</div>
<Link
href="/admin/jobs"
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 Jobs
</Link>
</div>
{/* Vehicle Info */}
{serviceRecord?.Vehicles && (
<div className="mb-6 rounded-lg border-2 border-white/20 bg-black/50 p-4">
<div className="mb-2 text-xl font-bold text-white font-mono">
{serviceRecord.Vehicles.id}
</div>
<div className="text-white/80 font-mono">
{serviceRecord.Vehicles.make} {serviceRecord.Vehicles.model}
{serviceRecord.Vehicles.capacity && ` (${serviceRecord.Vehicles.capacity}cc)`}
</div>
</div>
)}
{/* Service Record Edit Section */}
<div className="mb-8 space-y-4 rounded-lg border-2 border-white/20 bg-black/50 p-6">
<h2 className="mb-4 text-xl font-bold text-white uppercase tracking-wider">
Service Record Details
</h2>
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Status <span className="text-red-500">*</span>
</label>
<select
value={status}
onChange={(e) => setStatus(parseInt(e.target.value))}
className="w-full rounded-lg border-2 border-white/20 bg-black/50 px-4 py-3 font-mono text-white focus:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/30 neon-border transition-all"
>
<option value={0}>Pending</option>
<option value={1}>Active</option>
<option value={2}>Waiting for Customer Response</option>
<option value={3}>Waiting for Parts</option>
<option value={10}>Completed</option>
</select>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Mileage (km)
</label>
<input
type="number"
value={mileage}
onChange={(e) => setMileage(e.target.value)}
placeholder="Enter vehicle mileage..."
min="0"
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>
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Metadata / Notes
</label>
<textarea
value={metadata}
onChange={(e) => setMetadata(e.target.value)}
placeholder="Enter notes or metadata..."
rows={3}
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>
</div>
<button
onClick={handleSaveServiceRecord}
disabled={saving}
className="rounded-lg border border-white/30 bg-white/5 px-6 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 disabled:opacity-50"
>
{saving ? 'Saving...' : 'Save Changes'}
</button>
</div>
{/* Job Actions Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-white uppercase tracking-wider">
Job Actions
</h2>
{!showAddAction && (
<button
onClick={() => setShowAddAction(true)}
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"
>
+ Add Action
</button>
)}
</div>
{/* Add/Edit Action Form */}
{showAddAction && (
<div className="rounded-lg border-2 border-white/20 bg-black/50 p-6">
<h3 className="mb-4 text-lg font-bold text-white uppercase tracking-wider">
{editingAction ? 'Edit Job Action' : 'Add New Job Action'}
</h3>
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Action Type <span className="text-red-500">*</span>
</label>
<input
type="text"
value={newActionType}
onChange={(e) => setNewActionType(e.target.value)}
placeholder="e.g., Oil Change, Tire Replacement..."
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"
required
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Part Type
</label>
<input
type="text"
value={newPartType}
onChange={(e) => setNewPartType(e.target.value)}
placeholder="e.g., Engine Oil, Brake Pad..."
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>
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Part ID
</label>
<input
type="text"
value={newPartId}
onChange={(e) => setNewPartId(e.target.value)}
placeholder="Part identifier..."
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>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Custom Price (.)
</label>
<input
type="number"
value={newCustomPrice}
onChange={(e) => setNewCustomPrice(e.target.value)}
placeholder="0"
min="0"
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>
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Labour Fee (.)
</label>
<input
type="number"
value={newLabourFee}
onChange={(e) => setNewLabourFee(e.target.value)}
placeholder="0"
min="0"
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>
</div>
<div className="flex gap-2">
<button
onClick={editingAction ? handleUpdateAction : handleAddAction}
disabled={saving || !newActionType.trim()}
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 disabled:opacity-50"
>
{saving ? 'Saving...' : editingAction ? 'Update Action' : 'Add Action'}
</button>
<button
onClick={cancelEdit}
disabled={saving}
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 font-mono uppercase tracking-wider disabled:opacity-50"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Job Actions Table */}
{jobActions.length === 0 ? (
<div className="rounded-lg border border-white/10 bg-black/50 p-8 text-center">
<p className="text-white/60 font-mono uppercase tracking-wider">
No job actions found. Add one to get started.
</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<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>
<th className="px-4 py-3 text-left text-xs font-bold text-white uppercase tracking-wider font-mono">Actions</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>
<td className="px-4 py-3">
<div className="flex gap-2">
<button
onClick={() => handleEditAction(action)}
disabled={saving}
className="rounded-lg border border-white/30 bg-white/5 px-3 py-1 text-xs font-medium text-white transition-all hover:bg-white/10 hover:border-white/50 font-mono uppercase tracking-wider disabled:opacity-50"
>
Edit
</button>
<button
onClick={() => handleDeleteAction(action.id)}
disabled={saving}
className="rounded-lg border border-red-500/50 bg-red-500/10 px-3 py-1 text-xs font-medium text-red-400 transition-all hover:bg-red-500/20 hover:border-red-500 font-mono uppercase tracking-wider disabled:opacity-50"
>
Delete
</button>
</div>
</td>
</tr>
);
})}
<tr className="border-t-2 border-white/30 bg-black/70 font-bold">
<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>
<td className="px-4 py-4"></td>
</tr>
<tr className="bg-black/70">
<td colSpan={7} 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>
</div>
{notification && (
<Notification
message={notification.message}
type={notification.type}
onClose={() => setNotification(null)}
/>
)}
</>
);
}