jobs modify
This commit is contained in:
parent
4bb3c94784
commit
00c4a30f6b
615
app/admin/jobs/[id]/edit/page.tsx
Normal file
615
app/admin/jobs/[id]/edit/page.tsx
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
'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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
|||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import AdminLogo from '../../../components/AdminLogo';
|
||||
import Notification from '../../../components/Notification';
|
||||
|
||||
interface Customer {
|
||||
id: number;
|
||||
|
|
@ -29,6 +30,7 @@ export default function AddJobPage() {
|
|||
const [selectedCustomerId, setSelectedCustomerId] = useState<string>('');
|
||||
const [selectedVehicleId, setSelectedVehicleId] = useState<string>('');
|
||||
const [metadata, setMetadata] = useState('');
|
||||
const [mileage, setMileage] = useState('');
|
||||
|
||||
// Search state
|
||||
const [customerSearch, setCustomerSearch] = useState('');
|
||||
|
|
@ -51,6 +53,9 @@ export default function AddJobPage() {
|
|||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
|
||||
|
||||
// Notification state
|
||||
const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' | 'info' } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const adminLoggedIn = sessionStorage.getItem('adminLoggedIn');
|
||||
if (adminLoggedIn !== 'true') {
|
||||
|
|
@ -116,13 +121,14 @@ export default function AddJobPage() {
|
|||
setShowAddCustomer(false);
|
||||
setCustomerSearch('');
|
||||
setShowCustomerResults(false);
|
||||
setNotification({ message: 'Customer created successfully!', type: 'success' });
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Failed to create customer: ${error.error || error.details}`);
|
||||
setNotification({ message: `Failed to create customer: ${error.error || error.details}`, type: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating customer:', error);
|
||||
alert('Failed to create customer');
|
||||
setNotification({ message: 'Failed to create customer', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -158,13 +164,14 @@ export default function AddJobPage() {
|
|||
setShowAddVehicle(false);
|
||||
setVehicleSearch('');
|
||||
setShowVehicleResults(false);
|
||||
setNotification({ message: 'Vehicle created successfully!', type: 'success' });
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Failed to create vehicle: ${error.error || error.details}`);
|
||||
setNotification({ message: `Failed to create vehicle: ${error.error || error.details}`, type: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating vehicle:', error);
|
||||
alert('Failed to create vehicle');
|
||||
setNotification({ message: 'Failed to create vehicle', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -172,12 +179,12 @@ export default function AddJobPage() {
|
|||
e.preventDefault();
|
||||
|
||||
if (!selectedCustomerId) {
|
||||
alert('Please select a customer');
|
||||
setNotification({ message: 'Please select a customer', type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedVehicleId) {
|
||||
alert('Please select a vehicle');
|
||||
setNotification({ message: 'Please select a vehicle', type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -190,20 +197,23 @@ export default function AddJobPage() {
|
|||
body: JSON.stringify({
|
||||
vehicleId: selectedVehicleId,
|
||||
metadata: metadata || null,
|
||||
mileage: mileage ? parseInt(mileage) : null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
alert('Job created successfully!');
|
||||
router.push('/admin/dashboard');
|
||||
setNotification({ message: 'Job created successfully!', type: 'success' });
|
||||
setTimeout(() => {
|
||||
router.push('/admin/dashboard');
|
||||
}, 1500);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`Failed to create job: ${error.error || error.details}`);
|
||||
setNotification({ message: `Failed to create job: ${error.error || error.details}`, type: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating job:', error);
|
||||
alert('Failed to create job');
|
||||
setNotification({ message: 'Failed to create job', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -431,6 +441,21 @@ export default function AddJobPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Mileage */}
|
||||
<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>
|
||||
|
||||
{/* Metadata */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
|
||||
|
|
@ -656,7 +681,14 @@ export default function AddJobPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{notification && (
|
||||
<Notification
|
||||
message={notification.message}
|
||||
type={notification.type}
|
||||
onClose={() => setNotification(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ interface ServiceRecord {
|
|||
vehicle: string;
|
||||
metadata: string | null;
|
||||
status: number;
|
||||
mileage: number | null;
|
||||
Vehicles?: Vehicle;
|
||||
}
|
||||
|
||||
|
|
@ -279,6 +280,17 @@ export default function JobsListPage() {
|
|||
{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 && (
|
||||
|
|
@ -296,12 +308,20 @@ export default function JobsListPage() {
|
|||
<div className="text-xs text-white/40 font-mono">
|
||||
Service Record ID: {job.id}
|
||||
</div>
|
||||
<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 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>
|
||||
))}
|
||||
|
|
@ -342,15 +362,23 @@ export default function JobsListPage() {
|
|||
Job Details
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedJob(null);
|
||||
setJobActions([]);
|
||||
}}
|
||||
className="text-white/60 hover:text-white text-2xl transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<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 */}
|
||||
|
|
@ -409,6 +437,17 @@ export default function JobsListPage() {
|
|||
{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 && (
|
||||
|
|
@ -451,82 +490,60 @@ export default function JobsListPage() {
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{jobActions.map((action) => (
|
||||
<div
|
||||
key={action.id}
|
||||
className="rounded-lg border border-white/20 bg-black/50 p-4 hover:border-white/40 hover:bg-white/5 transition-all"
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Action Type
|
||||
<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>
|
||||
<div className="text-white font-mono font-semibold">
|
||||
{action.action_type}
|
||||
</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(action.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{action.part_type && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Part Type
|
||||
</div>
|
||||
<div className="text-white font-mono">
|
||||
{action.part_type}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.part_id && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Part ID
|
||||
</div>
|
||||
<div className="text-white font-mono">
|
||||
{action.part_id}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.labour_fee > 0 && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Labour Fee
|
||||
</div>
|
||||
<div className="text-white font-mono">
|
||||
₹{action.labour_fee}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.custom_price > 0 && action.custom_price !== -1 && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Custom Price
|
||||
</div>
|
||||
<div className="text-white font-mono">
|
||||
₹{action.custom_price}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t border-white/10">
|
||||
<div className="text-xs text-white/40 font-mono">
|
||||
Action ID: {action.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
78
app/api/admin/jobs/[id]/actions/[actionId]/route.ts
Normal file
78
app/api/admin/jobs/[id]/actions/[actionId]/route.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { supabaseAdmin } from '@/lib/supabase';
|
||||
|
||||
// PUT /api/admin/jobs/[id]/actions/[actionId] - Update a job action
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string; actionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { actionId } = await params;
|
||||
const jobActionId = parseInt(actionId);
|
||||
const body = await request.json();
|
||||
const { action_type, part_type, part_id, custom_price, labour_fee } = body;
|
||||
|
||||
const updateData: any = {};
|
||||
if (action_type !== undefined) updateData.action_type = action_type;
|
||||
if (part_type !== undefined) updateData.part_type = part_type || null;
|
||||
if (part_id !== undefined) updateData.part_id = part_id || null;
|
||||
if (custom_price !== undefined) updateData.custom_price = custom_price;
|
||||
if (labour_fee !== undefined) updateData.labour_fee = labour_fee;
|
||||
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from('JobActions')
|
||||
.update(updateData)
|
||||
.eq('id', jobActionId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update job action', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Job action updated successfully', data });
|
||||
} catch (error) {
|
||||
console.error('Error updating job action:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update job action' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/admin/jobs/[id]/actions/[actionId] - Delete a job action
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string; actionId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { actionId } = await params;
|
||||
const jobActionId = parseInt(actionId);
|
||||
|
||||
const { error } = await supabaseAdmin
|
||||
.from('JobActions')
|
||||
.delete()
|
||||
.eq('id', jobActionId);
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete job action', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Job action deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting job action:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete job action' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,3 +34,45 @@ export async function GET(
|
|||
}
|
||||
}
|
||||
|
||||
// POST /api/admin/jobs/[id]/actions - Create a new job action
|
||||
export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const serviceRecordId = parseInt(id);
|
||||
const body = await request.json();
|
||||
const { action_type, part_type, part_id, custom_price, labour_fee } = body;
|
||||
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from('JobActions')
|
||||
.insert({
|
||||
service_record: serviceRecordId,
|
||||
action_type: action_type || '',
|
||||
part_type: part_type || null,
|
||||
part_id: part_id || null,
|
||||
custom_price: custom_price !== undefined ? custom_price : -1,
|
||||
labour_fee: labour_fee !== undefined ? labour_fee : 0,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create job action', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Job action created successfully', data }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Error creating job action:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create job action' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
80
app/api/admin/jobs/[id]/route.ts
Normal file
80
app/api/admin/jobs/[id]/route.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { supabaseAdmin } from '@/lib/supabase';
|
||||
|
||||
// GET /api/admin/jobs/[id] - Get a single service record
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const serviceRecordId = parseInt(id);
|
||||
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from('ServiceRecords')
|
||||
.select(`
|
||||
*,
|
||||
Vehicles (*)
|
||||
`)
|
||||
.eq('id', serviceRecordId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch service record', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching service record:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch service record' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/admin/jobs/[id] - Update service record
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const serviceRecordId = parseInt(id);
|
||||
const body = await request.json();
|
||||
const { status, metadata, mileage } = body;
|
||||
|
||||
const updateData: any = {};
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (metadata !== undefined) updateData.metadata = metadata || null;
|
||||
if (mileage !== undefined) updateData.mileage = mileage || null;
|
||||
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from('ServiceRecords')
|
||||
.update(updateData)
|
||||
.eq('id', serviceRecordId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update service record', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Service record updated successfully', data });
|
||||
} catch (error) {
|
||||
console.error('Error updating service record:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update service record' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ import { supabaseAdmin } from '@/lib/supabase';
|
|||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { vehicleId, metadata } = body;
|
||||
const { vehicleId, metadata, mileage } = body;
|
||||
|
||||
// Create the ServiceRecord (status defaults to 0 = pending)
|
||||
const { data: serviceRecord, error: recordError } = await supabaseAdmin
|
||||
|
|
@ -13,6 +13,7 @@ export async function POST(request: Request) {
|
|||
.insert({
|
||||
vehicle: vehicleId,
|
||||
metadata: metadata || null,
|
||||
mileage: mileage || null,
|
||||
status: 0, // Pending
|
||||
})
|
||||
.select()
|
||||
|
|
|
|||
82
app/components/Notification.tsx
Normal file
82
app/components/Notification.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface NotificationProps {
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info';
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function Notification({ message, type, onClose }: NotificationProps) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(onClose, 300); // Wait for fade out animation
|
||||
}, 3000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [onClose]);
|
||||
|
||||
const bgColor = type === 'success'
|
||||
? 'bg-green-500/20 border-green-500/50'
|
||||
: type === 'error'
|
||||
? 'bg-red-500/20 border-red-500/50'
|
||||
: 'bg-cyan-500/20 border-cyan-500/50';
|
||||
|
||||
const textColor = type === 'success'
|
||||
? 'text-green-400'
|
||||
: type === 'error'
|
||||
? 'text-red-400'
|
||||
: 'text-cyan-400';
|
||||
|
||||
const icon = type === 'success' ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : type === 'error' ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-4 right-4 z-50 transition-all duration-300 ${
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'
|
||||
}`}
|
||||
>
|
||||
<div className={`relative rounded-lg border-2 ${bgColor} bg-black/95 backdrop-blur-sm p-4 shadow-[0_0_20px_rgba(255,255,255,0.1)] neon-border-glow min-w-[300px] max-w-md`}>
|
||||
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-white/5 to-transparent"></div>
|
||||
<div className="relative flex items-start gap-3">
|
||||
<div className={`flex-shrink-0 ${textColor}`}>
|
||||
{icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className={`font-mono text-sm font-medium ${textColor}`}>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(onClose, 300);
|
||||
}}
|
||||
className={`flex-shrink-0 ${textColor} opacity-60 hover:opacity-100 transition-opacity`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ interface ServiceRecord {
|
|||
vehicle: string;
|
||||
metadata: string | null;
|
||||
status: number;
|
||||
mileage: number | null;
|
||||
Vehicles?: Vehicle;
|
||||
}
|
||||
|
||||
|
|
@ -217,6 +218,17 @@ export default function VehicleStatusPage() {
|
|||
{new Date(record.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{record.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">
|
||||
{record.mileage.toLocaleString()} km
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{record.metadata && (
|
||||
|
|
@ -332,6 +344,17 @@ export default function VehicleStatusPage() {
|
|||
{new Date(selectedRecord.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedRecord.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">
|
||||
{selectedRecord.mileage.toLocaleString()} km
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedRecord.metadata && (
|
||||
|
|
@ -374,82 +397,60 @@ export default function VehicleStatusPage() {
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{jobActions.map((action) => (
|
||||
<div
|
||||
key={action.id}
|
||||
className="rounded-lg border border-white/20 bg-black/50 p-4 hover:border-white/40 hover:bg-white/5 transition-all"
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Action Type
|
||||
<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>
|
||||
<div className="text-white font-mono font-semibold">
|
||||
{action.action_type}
|
||||
</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(action.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{action.part_type && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Part Type
|
||||
</div>
|
||||
<div className="text-white font-mono">
|
||||
{action.part_type}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.part_id && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Part ID
|
||||
</div>
|
||||
<div className="text-white font-mono">
|
||||
{action.part_id}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.labour_fee > 0 && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Labour Fee
|
||||
</div>
|
||||
<div className="text-white font-mono">
|
||||
₹{action.labour_fee}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.custom_price > 0 && action.custom_price !== -1 && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-white/60 font-mono uppercase tracking-wider">
|
||||
Custom Price
|
||||
</div>
|
||||
<div className="text-white font-mono">
|
||||
₹{action.custom_price}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t border-white/10">
|
||||
<div className="text-xs text-white/40 font-mono">
|
||||
Action ID: {action.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user