This commit is contained in:
Sewmina 2025-11-06 00:25:08 +05:30
parent 595fbcff0a
commit 4bb3c94784
19 changed files with 2420 additions and 16 deletions

View File

@ -0,0 +1,115 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import AdminLogo from '../../components/AdminLogo';
export default function AdminDashboard() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const router = useRouter();
useEffect(() => {
// Check if admin is logged in
const adminLoggedIn = sessionStorage.getItem('adminLoggedIn');
if (adminLoggedIn !== 'true') {
router.push('/admin');
return;
}
setIsAuthenticated(true);
}, [router]);
const handleLogout = () => {
sessionStorage.removeItem('adminLoggedIn');
router.push('/admin');
};
if (!isAuthenticated) {
return null;
}
const adminActions = [
{
id: 'add-job',
title: 'Add New Job',
description: 'Create a new service job for a vehicle',
href: '/admin/jobs/add',
},
{
id: 'view-jobs',
title: 'View All Jobs',
description: 'Browse and search all service jobs',
href: '/admin/jobs',
},
// Add more actions here in the future
];
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-4xl 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-8 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">
Admin Dashboard
</h1>
</div>
<button
onClick={handleLogout}
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)]"
>
Logout
</button>
</div>
<div className="mb-6">
<h2 className="mb-4 text-xl font-bold text-white uppercase tracking-wider">
Admin Actions
</h2>
<div className="grid gap-4 md:grid-cols-2">
{adminActions.map((action) => (
<Link
key={action.id}
href={action.href}
className="group relative 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="mb-2 flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-white group-hover:animate-pulse"></div>
<h3 className="text-lg font-bold text-white uppercase tracking-wider">
{action.title}
</h3>
</div>
<p className="text-sm text-white/60 font-mono">
{action.description}
</p>
</Link>
))}
</div>
</div>
<div className="mt-6 text-center">
<Link
href="/"
className="text-sm text-white/60 hover:text-white transition-colors font-mono uppercase tracking-wider"
>
Back to Home
</Link>
</div>
</div>
</div>
</div>
</div>
</>
);
}

663
app/admin/jobs/add/page.tsx Normal file
View File

@ -0,0 +1,663 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import AdminLogo from '../../../components/AdminLogo';
interface Customer {
id: number;
name: string;
address: string;
tier: number;
}
interface Vehicle {
id: string;
make: string;
model: string;
capacity: number;
yom: number;
owner: number | null;
}
export default function AddJobPage() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const router = useRouter();
// Form state
const [selectedCustomerId, setSelectedCustomerId] = useState<string>('');
const [selectedVehicleId, setSelectedVehicleId] = useState<string>('');
const [metadata, setMetadata] = useState('');
// Search state
const [customerSearch, setCustomerSearch] = useState('');
const [vehicleSearch, setVehicleSearch] = useState('');
const [showCustomerResults, setShowCustomerResults] = useState(false);
const [showVehicleResults, setShowVehicleResults] = useState(false);
// Inline creation state
const [showAddCustomer, setShowAddCustomer] = useState(false);
const [showAddVehicle, setShowAddVehicle] = useState(false);
const [newCustomerName, setNewCustomerName] = useState('');
const [newCustomerAddress, setNewCustomerAddress] = useState('');
const [newVehicleId, setNewVehicleId] = useState('');
const [newVehicleMake, setNewVehicleMake] = useState('');
const [newVehicleModel, setNewVehicleModel] = useState('');
const [newVehicleCapacity, setNewVehicleCapacity] = useState('');
const [newVehicleYom, setNewVehicleYom] = useState('');
// Mock data - replace with API calls
const [customers, setCustomers] = useState<Customer[]>([]);
const [vehicles, setVehicles] = useState<Vehicle[]>([]);
useEffect(() => {
const adminLoggedIn = sessionStorage.getItem('adminLoggedIn');
if (adminLoggedIn !== 'true') {
router.push('/admin');
return;
}
setIsAuthenticated(true);
loadCustomers();
loadVehicles();
}, [router]);
const loadCustomers = async () => {
try {
const response = await fetch('/api/admin/customers');
if (response.ok) {
const data = await response.json();
setCustomers(data);
} else {
console.error('Failed to load customers');
}
} catch (error) {
console.error('Error loading customers:', error);
}
};
const loadVehicles = async () => {
try {
const response = await fetch('/api/admin/vehicles');
if (response.ok) {
const data = await response.json();
setVehicles(data);
} else {
console.error('Failed to load vehicles');
}
} catch (error) {
console.error('Error loading vehicles:', error);
}
};
const handleAddCustomer = async () => {
if (!newCustomerName.trim()) return;
try {
const response = await fetch('/api/admin/customers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newCustomerName,
address: newCustomerAddress,
tier: 0,
}),
});
if (response.ok) {
const result = await response.json();
const newCustomer = result.customer;
setCustomers([...customers, newCustomer]);
setSelectedCustomerId(newCustomer.id.toString());
setNewCustomerName('');
setNewCustomerAddress('');
setShowAddCustomer(false);
setCustomerSearch('');
setShowCustomerResults(false);
} else {
const error = await response.json();
alert(`Failed to create customer: ${error.error || error.details}`);
}
} catch (error) {
console.error('Error creating customer:', error);
alert('Failed to create customer');
}
};
const handleAddVehicle = async () => {
if (!newVehicleId.trim() || !newVehicleMake.trim() || !newVehicleModel.trim()) return;
try {
const response = await fetch('/api/admin/vehicles', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: newVehicleId.toUpperCase(),
make: newVehicleMake,
model: newVehicleModel,
capacity: parseInt(newVehicleCapacity) || 200,
yom: parseInt(newVehicleYom) || new Date().getFullYear(),
owner: selectedCustomerId ? parseInt(selectedCustomerId) : null,
}),
});
if (response.ok) {
const result = await response.json();
const newVehicle = result.vehicle;
setVehicles([...vehicles, newVehicle]);
setSelectedVehicleId(newVehicle.id);
setNewVehicleId('');
setNewVehicleMake('');
setNewVehicleModel('');
setNewVehicleCapacity('');
setNewVehicleYom('');
setShowAddVehicle(false);
setVehicleSearch('');
setShowVehicleResults(false);
} else {
const error = await response.json();
alert(`Failed to create vehicle: ${error.error || error.details}`);
}
} catch (error) {
console.error('Error creating vehicle:', error);
alert('Failed to create vehicle');
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedCustomerId) {
alert('Please select a customer');
return;
}
if (!selectedVehicleId) {
alert('Please select a vehicle');
return;
}
try {
const response = await fetch('/api/admin/jobs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
vehicleId: selectedVehicleId,
metadata: metadata || null,
}),
});
if (response.ok) {
const result = await response.json();
alert('Job created successfully!');
router.push('/admin/dashboard');
} else {
const error = await response.json();
alert(`Failed to create job: ${error.error || error.details}`);
}
} catch (error) {
console.error('Error creating job:', error);
alert('Failed to create job');
}
};
const selectedCustomer = customers.find(c => c.id.toString() === selectedCustomerId);
const selectedVehicle = vehicles.find(v => v.id === selectedVehicleId);
const filteredCustomers = customers.filter(customer =>
customer.name.toLowerCase().includes(customerSearch.toLowerCase()) ||
(customer.address && customer.address.toLowerCase().includes(customerSearch.toLowerCase()))
);
const filteredVehicles = vehicles
.filter(v => !selectedCustomerId || v.owner === parseInt(selectedCustomerId))
.filter(vehicle =>
vehicle.id.toLowerCase().includes(vehicleSearch.toLowerCase()) ||
vehicle.make.toLowerCase().includes(vehicleSearch.toLowerCase()) ||
vehicle.model.toLowerCase().includes(vehicleSearch.toLowerCase())
);
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.customer-search-container') && !target.closest('.vehicle-search-container')) {
setShowCustomerResults(false);
setShowVehicleResults(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
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-4xl 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">
Add New Job
</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>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Customer Selection */}
<div className="relative customer-search-container">
<div className="mb-2 flex items-center justify-between">
<label className="block text-sm font-medium text-white uppercase tracking-wider">
Customer <span className="text-red-500">*</span>
</label>
<button
type="button"
onClick={() => {
setNewCustomerName(customerSearch);
setShowAddCustomer(true);
}}
className="text-xs font-mono uppercase tracking-wider border border-cyan-500/50 bg-cyan-500/10 px-2 py-1 rounded text-cyan-400 transition-all hover:border-cyan-500 hover:bg-cyan-500/20 hover:text-cyan-300 hover:shadow-[0_0_10px_rgba(0,191,255,0.4)]"
>
+ Add New
</button>
</div>
{selectedCustomer ? (
<div className="mb-2 flex items-center justify-between rounded-lg border-2 border-white/30 bg-black/50 px-4 py-2">
<span className="text-white font-mono">
{selectedCustomer.name} {selectedCustomer.address ? `- ${selectedCustomer.address}` : ''}
</span>
<button
type="button"
onClick={() => {
setSelectedCustomerId('');
setSelectedVehicleId('');
setCustomerSearch('');
}}
className="text-white/60 hover:text-white"
>
</button>
</div>
) : (
<>
<input
type="text"
value={customerSearch}
onChange={(e) => {
setCustomerSearch(e.target.value);
setShowCustomerResults(true);
}}
onFocus={() => setShowCustomerResults(true)}
placeholder="Search customer by name or address..."
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"
/>
{showCustomerResults && customerSearch && filteredCustomers.length > 0 && (
<div className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border-2 border-white/20 bg-black/95 backdrop-blur-sm">
{filteredCustomers.map((customer) => (
<button
key={customer.id}
type="button"
onClick={() => {
setSelectedCustomerId(customer.id.toString());
setSelectedVehicleId('');
setCustomerSearch('');
setShowCustomerResults(false);
}}
className="w-full px-4 py-3 text-left text-white hover:bg-white/10 font-mono transition-colors border-b border-white/10 last:border-b-0"
>
<div className="font-semibold">{customer.name}</div>
{customer.address && (
<div className="text-sm text-white/60">{customer.address}</div>
)}
</button>
))}
</div>
)}
{showCustomerResults && customerSearch && filteredCustomers.length === 0 && (
<div className="absolute z-10 mt-1 w-full rounded-lg border-2 border-white/20 bg-black/95 backdrop-blur-sm px-4 py-3 text-white/60 font-mono text-sm">
No customers found
</div>
)}
</>
)}
</div>
{/* Vehicle Selection */}
<div className="relative vehicle-search-container">
<div className="mb-2 flex items-center justify-between">
<label className="block text-sm font-medium text-white uppercase tracking-wider">
Vehicle <span className="text-red-500">*</span>
</label>
<button
type="button"
onClick={() => {
setNewVehicleId(vehicleSearch);
setShowAddVehicle(true);
}}
className="text-xs font-mono uppercase tracking-wider border border-cyan-500/50 bg-cyan-500/10 px-2 py-1 rounded text-cyan-400 transition-all hover:border-cyan-500 hover:bg-cyan-500/20 hover:text-cyan-300 hover:shadow-[0_0_10px_rgba(0,191,255,0.4)]"
>
+ Add New
</button>
</div>
{selectedVehicle ? (
<div className="mb-2 flex items-center justify-between rounded-lg border-2 border-white/30 bg-black/50 px-4 py-2">
<span className="text-white font-mono">
{selectedVehicle.id} - {selectedVehicle.make} {selectedVehicle.model}
</span>
<button
type="button"
onClick={() => {
setSelectedVehicleId('');
setVehicleSearch('');
}}
className="text-white/60 hover:text-white"
>
</button>
</div>
) : (
<>
<input
type="text"
value={vehicleSearch}
onChange={(e) => {
setVehicleSearch(e.target.value);
setShowVehicleResults(true);
}}
onFocus={() => setShowVehicleResults(true)}
placeholder={selectedCustomerId ? "Search vehicles for selected customer..." : "Search vehicle by number, make, or model..."}
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"
disabled={!selectedCustomerId}
/>
{!selectedCustomerId && (
<p className="mt-1 text-xs text-white/60 font-mono">
Please select a customer first
</p>
)}
{showVehicleResults && vehicleSearch && filteredVehicles.length > 0 && (
<div className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border-2 border-white/20 bg-black/95 backdrop-blur-sm">
{filteredVehicles.map((vehicle) => (
<button
key={vehicle.id}
type="button"
onClick={() => {
setSelectedVehicleId(vehicle.id);
setVehicleSearch('');
setShowVehicleResults(false);
}}
className="w-full px-4 py-3 text-left text-white hover:bg-white/10 font-mono transition-colors border-b border-white/10 last:border-b-0"
>
<div className="font-semibold">{vehicle.id}</div>
<div className="text-sm text-white/60">
{vehicle.make} {vehicle.model} {vehicle.capacity ? `(${vehicle.capacity}cc)` : ''}
</div>
</button>
))}
</div>
)}
{showVehicleResults && vehicleSearch && filteredVehicles.length === 0 && (
<div className="absolute z-10 mt-1 w-full rounded-lg border-2 border-white/20 bg-black/95 backdrop-blur-sm px-4 py-3 text-white/60 font-mono text-sm">
No vehicles found
</div>
)}
</>
)}
</div>
{/* Metadata */}
<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)}
rows={4}
placeholder="Additional notes 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>
<div className="flex gap-4 pt-4">
<Link
href="/admin/dashboard"
className="flex-1 rounded-lg border-2 border-white/30 bg-black/50 px-6 py-3 text-center font-medium uppercase tracking-wider text-white transition-all hover:bg-white/10 hover:border-white/50 hover:shadow-[0_0_15px_rgba(255,255,255,0.3)]"
>
Cancel
</Link>
<button
type="submit"
className="flex-1 rounded-lg border-2 border-white/30 bg-white/5 px-6 py-3 font-bold uppercase tracking-wider text-white transition-all hover:bg-white/10 hover:border-white/50 hover:text-gray-200 hover:shadow-[0_0_20px_rgba(255,255,255,0.3)] focus:outline-none focus:ring-2 focus:ring-white/30"
>
Create Job
</button>
</div>
</form>
</div>
</div>
</div>
{/* Add Customer Modal */}
{showAddCustomer && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4">
<div className="relative w-full max-w-md rounded-lg border-2 border-white/30 bg-black/95 p-6 neon-border-glow">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-bold text-white uppercase tracking-wider">
Add New Customer
</h2>
<button
onClick={() => {
setShowAddCustomer(false);
setNewCustomerName('');
setNewCustomerAddress('');
}}
className="text-white/60 hover:text-white"
>
</button>
</div>
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Name <span className="text-red-500">*</span>
</label>
<input
type="text"
value={newCustomerName}
onChange={(e) => setNewCustomerName(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"
required
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Address
</label>
<input
type="text"
value={newCustomerAddress}
onChange={(e) => setNewCustomerAddress(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"
/>
</div>
<div className="flex gap-4">
<button
type="button"
onClick={() => {
setShowAddCustomer(false);
setNewCustomerName('');
setNewCustomerAddress('');
}}
className="flex-1 rounded-lg border-2 border-white/30 bg-black/50 px-4 py-2 text-white transition-all hover:bg-white/10"
>
Cancel
</button>
<button
type="button"
onClick={handleAddCustomer}
className="flex-1 rounded-lg border-2 border-white/30 bg-white/5 px-4 py-2 font-bold text-white transition-all hover:bg-white/10"
>
Add Customer
</button>
</div>
</div>
</div>
</div>
)}
{/* Add Vehicle Modal */}
{showAddVehicle && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4">
<div className="relative w-full max-w-md rounded-lg border-2 border-white/30 bg-black/95 p-6 neon-border-glow">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-bold text-white uppercase tracking-wider">
Add New Vehicle
</h2>
<button
onClick={() => {
setShowAddVehicle(false);
setNewVehicleId('');
setNewVehicleMake('');
setNewVehicleModel('');
setNewVehicleCapacity('');
setNewVehicleYom('');
}}
className="text-white/60 hover:text-white"
>
</button>
</div>
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Vehicle Number <span className="text-red-500">*</span>
</label>
<input
type="text"
value={newVehicleId}
onChange={(e) => setNewVehicleId(e.target.value.toUpperCase())}
placeholder="ABC-1234"
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"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Make <span className="text-red-500">*</span>
</label>
<input
type="text"
value={newVehicleMake}
onChange={(e) => setNewVehicleMake(e.target.value)}
placeholder="Honda"
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"
required
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Model <span className="text-red-500">*</span>
</label>
<input
type="text"
value={newVehicleModel}
onChange={(e) => setNewVehicleModel(e.target.value)}
placeholder="Civic"
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"
required
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Capacity (cc)
</label>
<input
type="number"
value={newVehicleCapacity}
onChange={(e) => setNewVehicleCapacity(e.target.value)}
placeholder="200"
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"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-white uppercase tracking-wider">
Year of Manufacture
</label>
<input
type="number"
value={newVehicleYom}
onChange={(e) => setNewVehicleYom(e.target.value)}
placeholder={new Date().getFullYear().toString()}
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"
/>
</div>
</div>
{selectedCustomerId && (
<div className="rounded-lg border border-white/20 bg-white/5 p-3">
<p className="text-xs text-white/60 font-mono">
Vehicle will be linked to selected customer
</p>
</div>
)}
<div className="flex gap-4">
<button
type="button"
onClick={() => {
setShowAddVehicle(false);
setNewVehicleId('');
setNewVehicleMake('');
setNewVehicleModel('');
setNewVehicleCapacity('');
setNewVehicleYom('');
}}
className="flex-1 rounded-lg border-2 border-white/30 bg-black/50 px-4 py-2 text-white transition-all hover:bg-white/10"
>
Cancel
</button>
<button
type="button"
onClick={handleAddVehicle}
className="flex-1 rounded-lg border-2 border-white/30 bg-white/5 px-4 py-2 font-bold text-white transition-all hover:bg-white/10"
>
Add Vehicle
</button>
</div>
</div>
</div>
</div>
)}
</div>
</>
);
}

540
app/admin/jobs/page.tsx Normal file
View File

@ -0,0 +1,540 @@
'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;
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>
</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>
<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>
)}
{!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>
<button
onClick={() => {
setSelectedJob(null);
setJobActions([]);
}}
className="text-white/60 hover:text-white text-2xl transition-colors"
>
</button>
</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>
</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="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>
<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>
))}
</div>
)}
</div>
</div>
</div>
</div>
)}
</>
);
}

97
app/admin/page.tsx Normal file
View File

@ -0,0 +1,97 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import AdminLogo from '../components/AdminLogo';
export default function AdminLogin() {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
if (password === '111222') {
// Store admin session
sessionStorage.setItem('adminLoggedIn', 'true');
router.push('/admin/dashboard');
} else {
setError('Invalid password');
setPassword('');
}
};
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 flex min-h-screen max-w-md flex-col items-center justify-center px-4 py-12">
<div className="relative w-full 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-2 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">
Admin Login
</h1>
</div>
<p className="mb-6 text-sm text-white/60 font-mono">
ENTER ADMIN PASSWORD TO CONTINUE
</p>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label
htmlFor="password"
className="mb-2 block text-sm font-medium text-white uppercase tracking-wider"
>
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
setError('');
}}
placeholder="Enter password"
className="w-full rounded-lg border-2 border-white/20 bg-black/50 px-4 py-3 text-lg 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
/>
{error && (
<p className="mt-2 text-sm text-red-500 font-mono">{error}</p>
)}
</div>
<button
type="submit"
className="w-full rounded-lg border-2 border-white/30 bg-white/5 px-6 py-3 text-lg font-bold uppercase tracking-wider text-white transition-all hover:bg-white/10 hover:border-white/50 hover:text-gray-200 hover:shadow-[0_0_20px_rgba(255,255,255,0.3)] focus:outline-none focus:ring-2 focus:ring-white/30"
>
Login
</button>
</form>
<div className="mt-6 text-center">
<Link
href="/"
className="text-sm text-white/60 hover:text-white transition-colors font-mono uppercase tracking-wider"
>
Back to Home
</Link>
</div>
</div>
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,66 @@
import { NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
// POST /api/admin/customers - Create a new customer
export async function POST(request: Request) {
try {
const body = await request.json();
const { name, address, tier } = body;
const { data, error } = await supabaseAdmin
.from('Customers')
.insert({
name: name,
address: address || '',
tier: tier || 0,
})
.select()
.single();
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Failed to create customer', details: error.message },
{ status: 500 }
);
}
return NextResponse.json(
{ message: 'Customer created successfully', customer: data },
{ status: 201 }
);
} catch (error) {
console.error('Error creating customer:', error);
return NextResponse.json(
{ error: 'Failed to create customer' },
{ status: 500 }
);
}
}
// GET /api/admin/customers - Get all customers
export async function GET() {
try {
const { data, error } = await supabaseAdmin
.from('Customers')
.select('*')
.order('registered_at', { ascending: false });
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Failed to fetch customers', details: error.message },
{ status: 500 }
);
}
return NextResponse.json(data || []);
} catch (error) {
console.error('Error fetching customers:', error);
return NextResponse.json(
{ error: 'Failed to fetch customers' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,36 @@
import { NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
// GET /api/admin/jobs/[id]/actions - Get all job actions for a service record
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const serviceRecordId = id;
const { data, error } = await supabaseAdmin
.from('JobActions')
.select('*')
.eq('service_record', serviceRecordId)
.order('created_at', { ascending: false });
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Failed to fetch job actions', details: error.message },
{ status: 500 }
);
}
return NextResponse.json(data || []);
} catch (error) {
console.error('Error fetching job actions:', error);
return NextResponse.json(
{ error: 'Failed to fetch job actions' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,70 @@
import { NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
// POST /api/admin/jobs - Create a new job (ServiceRecord)
export async function POST(request: Request) {
try {
const body = await request.json();
const { vehicleId, metadata } = body;
// Create the ServiceRecord (status defaults to 0 = pending)
const { data: serviceRecord, error: recordError } = await supabaseAdmin
.from('ServiceRecords')
.insert({
vehicle: vehicleId,
metadata: metadata || null,
status: 0, // Pending
})
.select()
.single();
if (recordError) {
console.error('Supabase error creating ServiceRecord:', recordError);
return NextResponse.json(
{ error: 'Failed to create service record', details: recordError.message },
{ status: 500 }
);
}
return NextResponse.json(
{ message: 'Job created successfully', serviceRecord },
{ status: 201 }
);
} catch (error) {
console.error('Error creating job:', error);
return NextResponse.json(
{ error: 'Failed to create job' },
{ status: 500 }
);
}
}
// GET /api/admin/jobs - Get all jobs
export async function GET() {
try {
const { data, error } = await supabaseAdmin
.from('ServiceRecords')
.select(`
*,
Vehicles (*)
`)
.order('created_at', { ascending: false });
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Failed to fetch jobs', details: error.message },
{ status: 500 }
);
}
return NextResponse.json(data || []);
} catch (error) {
console.error('Error fetching jobs:', error);
return NextResponse.json(
{ error: 'Failed to fetch jobs' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,69 @@
import { NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
// POST /api/admin/vehicles - Create a new vehicle
export async function POST(request: Request) {
try {
const body = await request.json();
const { id, make, model, capacity, yom, owner } = body;
const { data, error } = await supabaseAdmin
.from('Vehicles')
.insert({
id: typeof id === 'string' ? id.toUpperCase() : id,
make: make || 'tvs',
model: model || 'rtr 200',
capacity: capacity || 200,
yom: yom || new Date().getFullYear(),
owner: owner || null,
})
.select()
.single();
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Failed to create vehicle', details: error.message },
{ status: 500 }
);
}
return NextResponse.json(
{ message: 'Vehicle created successfully', vehicle: data },
{ status: 201 }
);
} catch (error) {
console.error('Error creating vehicle:', error);
return NextResponse.json(
{ error: 'Failed to create vehicle' },
{ status: 500 }
);
}
}
// GET /api/admin/vehicles - Get all vehicles
export async function GET() {
try {
const { data, error } = await supabaseAdmin
.from('Vehicles')
.select('*')
.order('registered_at', { ascending: false });
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Failed to fetch vehicles', details: error.message },
{ status: 500 }
);
}
return NextResponse.json(data || []);
} catch (error) {
console.error('Error fetching vehicles:', error);
return NextResponse.json(
{ error: 'Failed to fetch vehicles' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,35 @@
import { NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
// GET /api/vehicle/[vehicleId]/actions/[recordId] - Get all job actions for a service record
export async function GET(
request: Request,
{ params }: { params: Promise<{ vehicleId: string; recordId: string }> }
) {
try {
const { recordId } = await params;
const { data, error } = await supabaseAdmin
.from('JobActions')
.select('*')
.eq('service_record', parseInt(recordId))
.order('created_at', { ascending: false });
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Failed to fetch job actions', details: error.message },
{ status: 500 }
);
}
return NextResponse.json(data || []);
} catch (error) {
console.error('Error fetching job actions:', error);
return NextResponse.json(
{ error: 'Failed to fetch job actions' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,39 @@
import { NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';
// GET /api/vehicle/[vehicleId]/records - Get all service records for a vehicle
export async function GET(
request: Request,
{ params }: { params: Promise<{ vehicleId: string }> }
) {
try {
const { vehicleId } = await params;
const decodedVehicleId = decodeURIComponent(vehicleId);
const { data, error } = await supabaseAdmin
.from('ServiceRecords')
.select(`
*,
Vehicles (*)
`)
.eq('vehicle', decodedVehicleId.toUpperCase())
.order('created_at', { ascending: false });
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Failed to fetch service records', details: error.message },
{ status: 500 }
);
}
return NextResponse.json(data || []);
} catch (error) {
console.error('Error fetching service records:', error);
return NextResponse.json(
{ error: 'Failed to fetch service records' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,28 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
export default function AdminLogo() {
return (
<div className="relative w-full border-b border-white/20 bg-black/95 backdrop-blur-sm">
<div className="absolute inset-0 tech-grid opacity-20"></div>
<div className="relative mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
<Link href="/" className="flex items-center group">
<div className="relative">
<Image
src="/100kmphlogo.png"
alt="100kmph Logo"
width={100}
height={100}
className="rounded"
priority
/>
<div className="absolute inset-0 rounded bg-white/10 blur-md group-hover:bg-white/20 transition-all"></div>
</div>
</Link>
</div>
</div>
);
}

View File

@ -1,11 +1,18 @@
'use client';
import { usePathname } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
export default function Header() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const pathname = usePathname();
// Hide header on admin pages
if (pathname?.startsWith('/admin')) {
return null;
}
return (
<header className="relative w-full border-b border-white/20 bg-black/95 backdrop-blur-sm">
@ -17,8 +24,8 @@ export default function Header() {
<Image
src="/100kmphlogo.png"
alt="100kmph Logo"
width={100}
height={100}
width={120}
height={120}
className="rounded"
priority
/>
@ -67,4 +74,3 @@ export default function Header() {
</header>
);
}

View File

@ -12,11 +12,8 @@ export default function Home() {
const handleGuestLogin = (e: React.FormEvent) => {
e.preventDefault();
if (vehicleNumber.trim()) {
// TODO: Handle guest login logic
// For now, just show an alert or navigate to status page
console.log('Checking status for vehicle:', vehicleNumber);
// You can navigate to a status page here
// router.push(`/status/${vehicleNumber}`);
// Navigate to vehicle status page
router.push(`/vehicle/${encodeURIComponent(vehicleNumber.trim().toUpperCase())}`);
}
};

View File

@ -0,0 +1,463 @@
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
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 ServiceRecord {
id: number;
created_at: string;
vehicle: string;
metadata: string | null;
status: number;
Vehicles?: Vehicle;
}
export default function VehicleStatusPage() {
const router = useRouter();
const params = useParams();
const vehicleId = params?.vehicleId as string;
const [serviceRecords, setServiceRecords] = useState<ServiceRecord[]>([]);
const [loading, setLoading] = useState(true);
const [selectedRecord, setSelectedRecord] = useState<ServiceRecord | null>(null);
const [jobActions, setJobActions] = useState<JobAction[]>([]);
const [loadingActions, setLoadingActions] = useState(false);
useEffect(() => {
if (vehicleId) {
loadServiceRecords();
}
}, [vehicleId]);
// Close modal when clicking outside or pressing Escape
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape' && selectedRecord) {
setSelectedRecord(null);
setJobActions([]);
}
};
if (selectedRecord) {
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [selectedRecord]);
const loadServiceRecords = async () => {
try {
setLoading(true);
const response = await fetch(`/api/vehicle/${encodeURIComponent(vehicleId)}/records`);
if (response.ok) {
const data = await response.json();
setServiceRecords(data);
} else {
console.error('Failed to load service records');
}
} catch (error) {
console.error('Error loading service records:', error);
} finally {
setLoading(false);
}
};
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 (record: ServiceRecord) => {
setSelectedRecord(record);
setLoadingActions(true);
try {
const response = await fetch(`/api/vehicle/${encodeURIComponent(vehicleId)}/actions/${record.id}`);
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 vehicle = serviceRecords[0]?.Vehicles;
return (
<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">
Service Status
</h1>
</div>
<Link
href="/"
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 Home
</Link>
</div>
{/* Vehicle Info */}
{vehicle && (
<div className="mb-8 rounded-lg border-2 border-white/20 bg-black/50 p-6">
<div className="mb-4 text-center">
<div className="mb-2 text-2xl font-bold text-white font-mono">
{vehicle.id}
</div>
<div className="text-white/80 font-mono">
{vehicle.make} {vehicle.model}
{vehicle.capacity && ` (${vehicle.capacity}cc)`}
{vehicle.yom && ` - ${vehicle.yom}`}
</div>
</div>
</div>
)}
{/* Service Records 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 Service Records...</p>
</div>
) : serviceRecords.length === 0 ? (
<div className="text-center py-12">
<p className="text-white/60 font-mono uppercase tracking-wider">
No service records found for vehicle {vehicleId}
</p>
</div>
) : (
<div className="space-y-4">
{serviceRecords.map((record) => (
<div
key={record.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">
Service Record ID
</div>
<div className="text-white font-mono font-semibold">
#{record.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(record.status)}`}>
{getStatusLabel(record.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(record.created_at).toLocaleString()}
</div>
</div>
</div>
{record.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">
{record.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">
Record ID: {record.id}
</div>
<button
onClick={() => handleShowDetails(record)}
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>
)}
{!loading && serviceRecords.length > 0 && (
<div className="mt-6 text-center">
<p className="text-white/60 font-mono text-sm uppercase tracking-wider">
{serviceRecords.length} service record{serviceRecords.length !== 1 ? 's' : ''} found
</p>
</div>
)}
</div>
</div>
</div>
{/* Details Modal */}
{selectedRecord && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 p-4 overflow-y-auto"
onClick={() => {
setSelectedRecord(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">
Service Record Details
</h2>
</div>
<button
onClick={() => {
setSelectedRecord(null);
setJobActions([]);
}}
className="text-white/60 hover:text-white text-2xl transition-colors"
>
</button>
</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">
#{selectedRecord.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(selectedRecord.status)}`}>
{getStatusLabel(selectedRecord.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">
{selectedRecord.Vehicles?.id || 'N/A'}
</div>
<div className="text-sm text-white/70 font-mono">
{selectedRecord.Vehicles?.make || 'N/A'} {selectedRecord.Vehicles?.model || ''}
{selectedRecord.Vehicles?.capacity && ` (${selectedRecord.Vehicles.capacity}cc)`}
</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(selectedRecord.created_at).toLocaleString()}
</div>
</div>
</div>
{selectedRecord.metadata && (
<div>
<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 bg-black/50 rounded-lg p-4 border border-white/10">
{selectedRecord.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="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>
<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>
))}
</div>
)}
</div>
</div>
</div>
</div>
)}
</div>
);
}

44
db_structure_sql Normal file
View File

@ -0,0 +1,44 @@
-- WARNING: This schema is for context only and is not meant to be run.
-- Table order and constraints may not be valid for execution.
CREATE TABLE public.Customers (
id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
registered_at timestamp with time zone NOT NULL DEFAULT now(),
name character varying,
address character varying DEFAULT ''::character varying,
tier smallint DEFAULT '0'::smallint,
CONSTRAINT Customers_pkey PRIMARY KEY (id)
);
CREATE TABLE public.JobActions (
id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
action_type character varying NOT NULL,
part_type character varying,
part_id character varying,
custom_price bigint DEFAULT '-1'::bigint,
labour_fee bigint DEFAULT '0'::bigint,
service_record bigint,
CONSTRAINT JobActions_pkey PRIMARY KEY (id),
CONSTRAINT JobActions_service_record_fkey FOREIGN KEY (service_record) REFERENCES public.ServiceRecords(id)
);
CREATE TABLE public.ServiceRecords (
id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
created_at timestamp with time zone NOT NULL DEFAULT now(),
vehicle character varying,
metadata character varying,
status smallint DEFAULT '0'::smallint,
CONSTRAINT ServiceRecords_pkey PRIMARY KEY (id),
CONSTRAINT ServiceRecords_vehicle_fkey FOREIGN KEY (vehicle) REFERENCES public.Vehicles(id)
);
CREATE TABLE public.Vehicles (
id character varying NOT NULL DEFAULT 'SDR-5000'::character varying,
registered_at timestamp with time zone NOT NULL DEFAULT now(),
make character varying DEFAULT 'tvs'::character varying,
model character varying DEFAULT 'rtr 200'::character varying,
capacity integer DEFAULT 200,
yom integer DEFAULT 2018,
metadata character varying,
owner bigint,
CONSTRAINT Vehicles_pkey PRIMARY KEY (id),
CONSTRAINT Vehicles_owner_fkey FOREIGN KEY (owner) REFERENCES public.Customers(id)
);

21
lib/supabase.ts Normal file
View File

@ -0,0 +1,21 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY!;
if (!supabaseUrl || !supabaseKey) {
throw new Error('Missing Supabase environment variables');
}
// Client for public operations (uses anon key)
export const supabase = createClient(supabaseUrl, supabaseKey);
// Admin client for server-side operations (bypasses RLS)
// Use service role key if available, otherwise fallback to anon key
const adminKey = process.env.SUPABASE_SERVICE_ROLE_KEY || supabaseKey;
export const supabaseAdmin = createClient(supabaseUrl, adminKey, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});

118
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "100kmph",
"version": "0.1.0",
"dependencies": {
"@supabase/supabase-js": "^2.79.0",
"next": "16.0.1",
"react": "19.2.0",
"react-dom": "19.2.0"
@ -1195,6 +1196,85 @@
"dev": true,
"license": "MIT"
},
"node_modules/@supabase/auth-js": {
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.79.0.tgz",
"integrity": "sha512-p2GKvdbF9d/6C+dtS6iNcSicPr6eUfkvovD60HWlWsD+oOjC483DzFWrzGjNpBwnswhfMRP8Qn3rYA0VWaOfjw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.79.0.tgz",
"integrity": "sha512-WaiU6b+Z+ZfJOjFhpMKdajt42weiFUrA6TVW5oGd6WfPGajFiKZJJIAvuK0g7KDKaYowtQrOo5+Ais+PcuZ1qA==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.79.0.tgz",
"integrity": "sha512-2i8EFm3/49ecjt6dk/TGVROBbtOmhryiC4NL3u0FBIrm2hqj+FvbELv1jjM6r+a6abnh+uzIV/bFsWHAa/k3/w==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.79.0.tgz",
"integrity": "sha512-foaZujNBycAqLizUcuLyyFyDitfPnEMVO4CiKXNwaMCDVMoVX4QR6n4gpJLUC5BGzc20Mte6vSJLbk4MN90Prw==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.79.0.tgz",
"integrity": "sha512-PLSeKX1/BZhGWCT972w4TvVOCcw/xh4TsowtUBiZvPx4OdHT7dB1q0DXKwVUfKbWk5UUC+6XAq4ZU/ZCtdgn6w==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.79.0.tgz",
"integrity": "sha512-x9ndEaBSwoRnFOOZGhh2CeV69Uz4B/EOSGCbKysDhTiYakiCAdDXaNuLPluviKU/Aot+F7BglXZDZ0YJ3GpGrw==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.79.0",
"@supabase/functions-js": "2.79.0",
"@supabase/postgrest-js": "2.79.0",
"@supabase/realtime-js": "2.79.0",
"@supabase/storage-js": "2.79.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -1511,12 +1591,17 @@
"version": "20.19.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
@ -1537,6 +1622,15 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz",
@ -6296,7 +6390,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {
@ -6490,6 +6583,27 @@
"node": ">=0.10.0"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -9,18 +9,19 @@
"lint": "eslint"
},
"dependencies": {
"@supabase/supabase-js": "^2.79.0",
"next": "16.0.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"next": "16.0.1"
"react-dom": "19.2.0"
},
"devDependencies": {
"typescript": "^5",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "16.0.1"
"eslint-config-next": "16.0.1",
"tailwindcss": "^4",
"typescript": "^5"
}
}

0
supabase_config Normal file
View File