Hybrid cars implementation 1.0(Without stats)

This commit is contained in:
EdiFarcas
2025-07-10 00:09:58 +03:00
parent b3b916f52b
commit cf1f78280c
14 changed files with 247 additions and 50 deletions
@@ -0,0 +1,18 @@
/*
Warnings:
- You are about to drop the column `fuelType` on the `Car` table. All the data in the column will be lost.
- Added the required column `fuelType` to the `FillUp` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Car" DROP COLUMN "fuelType",
ADD COLUMN "fuelTypes" "FuelType"[];
-- AlterTable: add as nullable first
ALTER TABLE "FillUp" ADD COLUMN "fuelType" "FuelType";
-- Set default for existing rows (choose the most common, e.g. GASOLINE)
UPDATE "FillUp" SET "fuelType" = 'GASOLINE' WHERE "fuelType" IS NULL;
-- Make the column required
ALTER TABLE "FillUp" ALTER COLUMN "fuelType" SET NOT NULL;
@@ -0,0 +1,11 @@
-- DropForeignKey
ALTER TABLE "FillUp" DROP CONSTRAINT "FillUp_carId_fkey";
-- DropForeignKey
ALTER TABLE "MileageEntry" DROP CONSTRAINT "MileageEntry_carId_fkey";
-- AddForeignKey
ALTER TABLE "FillUp" ADD CONSTRAINT "FillUp_carId_fkey" FOREIGN KEY ("carId") REFERENCES "Car"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MileageEntry" ADD CONSTRAINT "MileageEntry_carId_fkey" FOREIGN KEY ("carId") REFERENCES "Car"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+4 -4
View File
@@ -23,27 +23,27 @@ model Car {
make String // Manufacturer (e.g. "BMW") make String // Manufacturer (e.g. "BMW")
model String // Model (e.g. "320i") model String // Model (e.g. "320i")
year Int // Year (e.g. 2019) year Int // Year (e.g. 2019)
fuelType FuelType fuelTypes FuelType[] // Changed from single fuelType to array for hybrid support
fillUps FillUp[] fillUps FillUp[]
mileage MileageEntry[] mileage MileageEntry[]
} }
model FillUp { model FillUp {
id String @id @default(cuid()) id String @id @default(cuid())
car Car @relation(fields: [carId], references: [id]) car Car @relation(fields: [carId], references: [id], onDelete: Cascade)
carId String carId String
mileage Int mileage Int
liters Float liters Float
cost Float cost Float
currency Currency currency Currency
date DateTime @default(now()) date DateTime @default(now())
fuelType FuelType // Add fuelType to each fill-up
} }
model MileageEntry { model MileageEntry {
id String @id @default(cuid()) id String @id @default(cuid())
car Car @relation(fields: [carId], references: [id]) car Car @relation(fields: [carId], references: [id], onDelete: Cascade)
carId String carId String
mileage Int mileage Int
date DateTime @default(now()) date DateTime @default(now())
+1
View File
@@ -39,6 +39,7 @@ export async function GET(req: Request) {
cost: f.cost, cost: f.cost,
currency: f.currency, currency: f.currency,
date: f.date, date: f.date,
fuelType: f.fuelType, // Fix: Prisma FillUp model includes fuelType
})), })),
...mileageEntries.map(m => ({ ...mileageEntries.map(m => ({
type: 'mileage', type: 'mileage',
+22 -2
View File
@@ -12,7 +12,7 @@ export async function GET(req: Request, { params }: { params: { carId: string }
const car = await prisma.car.findFirst({ const car = await prisma.car.findFirst({
where: { where: {
id: carId, id: carId,
user: { email: session.user?.email! }, user: { email: session.user?.email ?? undefined },
}, },
select: { select: {
id: true, id: true,
@@ -20,10 +20,30 @@ export async function GET(req: Request, { params }: { params: { carId: string }
make: true, make: true,
model: true, model: true,
year: true, year: true,
fuelType: true, fuelTypes: true,
}, },
}); });
if (!car) return NextResponse.json({ message: 'Car not found' }, { status: 404 }); if (!car) return NextResponse.json({ message: 'Car not found' }, { status: 404 });
return NextResponse.json(car); return NextResponse.json(car);
} }
export async function DELETE(req: Request, { params }: { params: { carId: string } }) {
const session = await getSession();
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
const carId = params.carId;
if (!carId) return NextResponse.json({ message: 'Missing carId' }, { status: 400 });
// Only allow deleting user's own car
const car = await prisma.car.findFirst({
where: {
id: carId,
user: { email: session.user?.email ?? undefined },
},
});
if (!car) return NextResponse.json({ message: 'Car not found' }, { status: 404 });
await prisma.car.delete({ where: { id: carId } });
return NextResponse.json({ message: 'Car deleted' });
}
+4 -4
View File
@@ -7,14 +7,14 @@ export async function POST(req: Request) {
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
const body = await req.json(); const body = await req.json();
const { name, make, model, year, fuelType } = body; const { name, make, model, year, fuelTypes } = body;
if (!name || !make || !model || !year || !fuelType) { if (!name || !make || !model || !year || !fuelTypes || !Array.isArray(fuelTypes) || fuelTypes.length === 0) {
return NextResponse.json({ message: 'Missing fields' }, { status: 400 }); return NextResponse.json({ message: 'Missing fields' }, { status: 400 });
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { email: session.user?.email! }, where: { email: session.user?.email ?? undefined },
}); });
if (!user) { if (!user) {
@@ -27,7 +27,7 @@ export async function POST(req: Request) {
make, make,
model, model,
year: parseInt(year), year: parseInt(year),
fuelType, fuelTypes: { set: fuelTypes },
userId: user.id, userId: user.id,
}, },
}); });
+4 -3
View File
@@ -26,15 +26,15 @@ export async function POST(req: Request) {
const session = await getSession(); const session = await getSession();
if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
const { carId, mileage, liters, cost, currency } = await req.json(); const { carId, mileage, liters, cost, currency, fuelType } = await req.json();
if (!carId || !mileage || !liters || !cost || !currency) { if (!carId || !mileage || !liters || !cost || !currency || !fuelType) {
return NextResponse.json({ message: 'Missing fields' }, { status: 400 }); return NextResponse.json({ message: 'Missing fields' }, { status: 400 });
} }
const car = await prisma.car.findFirst({ const car = await prisma.car.findFirst({
where: { where: {
id: carId, id: carId,
user: { email: session.user?.email! }, user: { email: session.user?.email ?? undefined },
}, },
}); });
@@ -47,6 +47,7 @@ export async function POST(req: Request) {
cost: parseFloat(cost), cost: parseFloat(cost),
currency, currency,
carId, carId,
fuelType,
}, },
}); });
+50 -11
View File
@@ -11,6 +11,7 @@ interface FillUp {
cost: number; cost: number;
currency: string; currency: string;
date: string; date: string;
fuelType: string; // Add fuelType to FillUp interface
} }
const currencies = ['EUR', 'USD', 'RON', 'GBP']; const currencies = ['EUR', 'USD', 'RON', 'GBP'];
@@ -18,12 +19,13 @@ const currencies = ['EUR', 'USD', 'RON', 'GBP'];
export default function FillUpsPage() { export default function FillUpsPage() {
const { carId } = useParams(); const { carId } = useParams();
const [fillups, setFillups] = useState<FillUp[]>([]); const [fillups, setFillups] = useState<FillUp[]>([]);
const [carFuelType, setCarFuelType] = useState<string | undefined>(undefined); const [carFuelTypes, setCarFuelTypes] = useState<string[]>([]);
const [form, setForm] = useState({ const [form, setForm] = useState({
mileage: '', mileage: '',
liters: '', liters: '',
cost: '', cost: '',
currency: 'EUR', currency: 'EUR',
fuelType: '',
}); });
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -34,11 +36,11 @@ export default function FillUpsPage() {
.then(setFillups); .then(setFillups);
}, [carId]); }, [carId]);
// Fetch car fuel type // Fetch car fuel types
useEffect(() => { useEffect(() => {
fetch(`/api/cars/${carId}`) fetch(`/api/cars/${carId}`)
.then((res) => res.json()) .then((res) => res.json())
.then((car) => setCarFuelType(car?.fuelType)); .then((car) => setCarFuelTypes(car?.fuelTypes || []));
}, [carId]); }, [carId]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
@@ -49,14 +51,14 @@ export default function FillUpsPage() {
e.preventDefault(); e.preventDefault();
const res = await fetch('/api/fillups', { const res = await fetch('/api/fillups', {
method: 'POST', method: 'POST',
body: JSON.stringify({ ...form, carId }), body: JSON.stringify({ ...form, carId, fuelType: form.fuelType || carFuelTypes[0] }),
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });
if (res.ok) { if (res.ok) {
const newFill = await res.json(); const newFill = await res.json();
setFillups((prev) => [newFill, ...prev]); setFillups((prev) => [newFill, ...prev]);
setForm({ mileage: '', liters: '', cost: '', currency: 'EUR' }); setForm({ mileage: '', liters: '', cost: '', currency: 'EUR', fuelType: carFuelTypes[0] || '' });
setError(''); setError('');
} else { } else {
const data = await res.json(); const data = await res.json();
@@ -64,8 +66,8 @@ export default function FillUpsPage() {
} }
}; };
// Only render fill-ups when carFuelType is loaded // Only render fill-ups when carFuelTypes is loaded
if (carFuelType === undefined) { if (!carFuelTypes.length) {
return ( return (
<main className="max-w-2xl mx-auto p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow"> <main className="max-w-2xl mx-auto p-8 space-y-8 bg-[var(--muted)] rounded-xl shadow">
<h1 className="text-2xl font-bold text-[var(--primary)]">Fuel Fill-Ups</h1> <h1 className="text-2xl font-bold text-[var(--primary)]">Fuel Fill-Ups</h1>
@@ -95,21 +97,39 @@ export default function FillUpsPage() {
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label htmlFor="liters" className="font-medium text-[var(--primary)]"> <label htmlFor="liters" className="font-medium text-[var(--primary)]">
{carFuelType === 'ELECTRIC' ? 'Kilowatt-hours' : 'Liters'} {(() => {
const fuel = carFuelTypes.length > 1 ? (form.fuelType || carFuelTypes[0]) : carFuelTypes[0];
if (fuel === 'ELECTRIC') return 'Kilowatt-hours';
if (fuel === 'LPG') return 'Liters (LPG)';
if (fuel === 'GASOLINE') return 'Liters (Gasoline)';
if (fuel === 'DIESEL') return 'Liters (Diesel)';
return 'Liters';
})()}
</label> </label>
<input <input
id="liters" id="liters"
name="liters" name="liters"
type="number" type="number"
step="0.01" step="0.01"
placeholder={carFuelType === 'ELECTRIC' ? 'e.g. 45.5' : 'e.g. 45.5'} placeholder={(() => {
const fuel = carFuelTypes.length > 1 ? (form.fuelType || carFuelTypes[0]) : carFuelTypes[0];
if (fuel === 'ELECTRIC') return 'e.g. 45.5';
return 'e.g. 45.5';
})()}
value={form.liters} value={form.liters}
onChange={handleChange} onChange={handleChange}
required required
className="border border-[var(--border)] px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--primary)] bg-[var(--muted)] text-[var(--foreground)]" className="border border-[var(--border)] px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--primary)] bg-[var(--muted)] text-[var(--foreground)]"
/> />
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
{carFuelType === 'ELECTRIC' ? 'How many kilowatt-hours did you charge?' : 'How many liters did you fill?'} {(() => {
const fuel = carFuelTypes.length > 1 ? (form.fuelType || carFuelTypes[0]) : carFuelTypes[0];
if (fuel === 'ELECTRIC') return 'How many kilowatt-hours did you charge?';
if (fuel === 'LPG') return 'How many liters of LPG did you fill?';
if (fuel === 'GASOLINE') return 'How many liters of gasoline did you fill?';
if (fuel === 'DIESEL') return 'How many liters of diesel did you fill?';
return 'How many liters did you fill?';
})()}
</span> </span>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@@ -141,6 +161,25 @@ export default function FillUpsPage() {
))} ))}
</select> </select>
</div> </div>
{carFuelTypes.length > 1 && (
<div className="flex flex-col gap-1 col-span-full">
<label className="font-medium text-[var(--primary)]">Fuel Type</label>
<select
name="fuelType"
value={form.fuelType || carFuelTypes[0]}
onChange={handleChange}
required
className="border border-[var(--border)] px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--primary)] bg-[var(--muted)] text-[var(--foreground)]"
>
{carFuelTypes.map((type) => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>
)}
{carFuelTypes.length === 1 && (
<input type="hidden" name="fuelType" value={carFuelTypes[0]} />
)}
<button type="submit" className="col-span-full bg-[var(--secondary)] text-white py-2 rounded-lg font-semibold shadow hover:bg-green-700 transition mt-2"> <button type="submit" className="col-span-full bg-[var(--secondary)] text-white py-2 rounded-lg font-semibold shadow hover:bg-green-700 transition mt-2">
Add Fill-Up Add Fill-Up
</button> </button>
@@ -149,7 +188,7 @@ export default function FillUpsPage() {
<ul className="space-y-3"> <ul className="space-y-3">
{fillups.map((fill) => ( {fillups.map((fill) => (
<FillUpCard key={fill.id} fill={{ ...fill, car: { fuelType: carFuelType } }} /> <FillUpCard key={fill.id} fill={{ ...fill, car: { fuelType: fill.fuelType } }} />
))} ))}
</ul> </ul>
</main> </main>
+26 -6
View File
@@ -3,6 +3,7 @@ import { prisma } from '@/lib/prisma';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { FaCarSide, FaGasPump, FaCalendarAlt, FaIndustry, FaBolt } from 'react-icons/fa'; import { FaCarSide, FaGasPump, FaCalendarAlt, FaIndustry, FaBolt } from 'react-icons/fa';
import DeleteCarButton from '@/components/DeleteCarButton';
interface CarDetailPageProps { interface CarDetailPageProps {
params: { params: {
@@ -15,13 +16,21 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) {
const session = await getSession(); const session = await getSession();
if (!session) redirect('/auth/login'); if (!session) redirect('/auth/login');
const userEmail = session.user?.email!; const userEmail = session.user?.email || '';
const car = await prisma.car.findFirst({ const car = await prisma.car.findFirst({
where: { where: {
id: resolvedParams.carId, id: resolvedParams.carId,
user: { email: userEmail }, user: { email: userEmail },
}, },
}); select: {
id: true,
name: true,
make: true,
model: true,
year: true,
fuelTypes: true,
},
}) as { id: string; name: string; make: string; model: string; year: number; fuelTypes?: string[] };
if (!car) { if (!car) {
return <div className="p-8 text-center text-red-500 text-lg font-semibold">Car not found or unauthorized access.</div>; return <div className="p-8 text-center text-red-500 text-lg font-semibold">Car not found or unauthorized access.</div>;
@@ -44,8 +53,15 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) {
<h1 className="text-4xl font-extrabold text-[var(--primary)] leading-tight tracking-tight flex items-center gap-3"> <h1 className="text-4xl font-extrabold text-[var(--primary)] leading-tight tracking-tight flex items-center gap-3">
{car.name} {car.name}
<span className="inline-block px-3 py-1 rounded-full text-base font-bold bg-gray-200 text-gray-700 ml-2">{car.year}</span> <span className="inline-block px-3 py-1 rounded-full text-base font-bold bg-gray-200 text-gray-700 ml-2">{car.year}</span>
{Array.isArray(car.fuelTypes) && car.fuelTypes.length > 1 ? (
<span className="ml-2 flex items-center">
<span className={`px-2 py-1 rounded-l-full text-xs font-bold ${fuelMeta[car.fuelTypes[0]]?.color || 'bg-gray-100 text-gray-700'}`} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0, borderRight: '1px solid #ccc' }}>{fuelMeta[car.fuelTypes[0]]?.icon}{car.fuelTypes[0]}</span>
<span className={`px-2 py-1 rounded-r-full text-xs font-bold ${fuelMeta[car.fuelTypes[1]]?.color || 'bg-gray-100 text-gray-700'}`} style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}>{fuelMeta[car.fuelTypes[1]]?.icon}{car.fuelTypes[1]}</span>
</span>
) : Array.isArray(car.fuelTypes) && car.fuelTypes.length === 1 ? (
<span className={`ml-2 px-2 py-1 rounded-full text-xs font-bold ${fuelMeta[car.fuelTypes[0]]?.color || 'bg-gray-100 text-gray-700'}`}>{fuelMeta[car.fuelTypes[0]]?.icon}{car.fuelTypes[0]}</span>
) : null}
</h1> </h1>
<span className={`mt-2 px-4 py-1 rounded-full text-sm font-bold flex items-center gap-1 ${fuelMeta[car.fuelType]?.color || 'bg-gray-100 text-gray-700'}`}>{fuelMeta[car.fuelType]?.icon}{car.fuelType}</span>
</div> </div>
{/* Car Details */} {/* Car Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 px-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8 px-8">
@@ -65,9 +81,12 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) {
<span className="ml-1 text-gray-400 font-mono text-lg">{car.year}</span> <span className="ml-1 text-gray-400 font-mono text-lg">{car.year}</span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{fuelMeta[car.fuelType]?.icon} {Array.isArray(car.fuelTypes) && car.fuelTypes.length > 0
<span className="font-semibold text-[var(--foreground)]">Fuel:</span> ? car.fuelTypes.map((ft: string) => (
<span className={`ml-1 px-2 py-1 rounded-full text-xs font-bold ${fuelMeta[car.fuelType]?.color || 'bg-gray-100 text-gray-700'}`}>{car.fuelType}</span> <span key={ft} className={`ml-1 px-2 py-1 rounded-full text-xs font-bold flex items-center gap-1 ${fuelMeta[ft]?.color || 'bg-gray-100 text-gray-700'}`}>{fuelMeta[ft]?.icon}{ft}</span>
))
: null
}
</div> </div>
</div> </div>
{/* Actions */} {/* Actions */}
@@ -90,6 +109,7 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) {
> >
📊 View Stats 📊 View Stats
</Link> </Link>
<DeleteCarButton carId={car.id} />
</div> </div>
</div> </div>
</main> </main>
+33 -12
View File
@@ -12,16 +12,33 @@ export default function AddCarPage() {
make: '', make: '',
model: '', model: '',
year: '', year: '',
fuelType: 'GASOLINE', fuelTypes: ['GASOLINE'], // default to one selected
}); });
const [error, setError] = useState(''); const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setForm({ ...form, [e.target.name]: e.target.value }); const { name, value, type, checked } = e.target;
if (name === 'fuelTypes') {
setForm((prev) => {
let updated = prev.fuelTypes;
if (checked) {
updated = [...prev.fuelTypes, value];
} else {
updated = prev.fuelTypes.filter((t: string) => t !== value);
}
return { ...prev, fuelTypes: updated };
});
} else {
setForm({ ...form, [name]: value });
}
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!form.fuelTypes.length) {
setError('Please select at least one fuel type.');
return;
}
const res = await fetch('/api/cars', { const res = await fetch('/api/cars', {
method: 'POST', method: 'POST',
body: JSON.stringify(form), body: JSON.stringify(form),
@@ -97,18 +114,22 @@ export default function AddCarPage() {
/> />
</div> </div>
<div> <div>
<label htmlFor="fuelType" className="block mb-1 font-semibold text-[var(--foreground)]">Fuel Type</label> <label className="block mb-1 font-semibold text-[var(--foreground)]">Fuel Types</label>
<select <div className="flex flex-wrap gap-4">
id="fuelType"
name="fuelType"
value={form.fuelType}
onChange={handleChange}
className="w-full border border-[var(--border)] px-4 py-3 rounded-lg bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
>
{fuelTypes.map((type) => ( {fuelTypes.map((type) => (
<option key={type} value={type}>{type}</option> <label key={type} className="flex items-center gap-2">
<input
type="checkbox"
name="fuelTypes"
value={type}
checked={form.fuelTypes.includes(type)}
onChange={handleChange}
className="accent-[var(--primary)]"
/>
{type}
</label>
))} ))}
</select> </div>
</div> </div>
<button <button
type="submit" type="submit"
+21 -1
View File
@@ -19,12 +19,32 @@ export default function CarCard({ car }: { car: any }) {
} }
} }
// Show all fuel types as badges if car.fuelTypes is an array
let fuelBadges = null;
if (Array.isArray(car.fuelTypes) && car.fuelTypes.length > 0) {
if (car.fuelTypes.length === 1) {
fuelBadges = (
<span className={`ml-2 px-2 py-1 rounded-full text-xs font-bold ${getFuelBadgeColor(car.fuelTypes[0])}`}>{car.fuelTypes[0]}</span>
);
} else {
// Show both, but visually split: half badge for each
fuelBadges = (
<span className="ml-2 flex items-center">
<span className={`px-2 py-1 rounded-l-full text-xs font-bold ${getFuelBadgeColor(car.fuelTypes[0])}`} style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0, borderRight: '1px solid #ccc' }}>{car.fuelTypes[0]}</span>
<span className={`px-2 py-1 rounded-r-full text-xs font-bold ${getFuelBadgeColor(car.fuelTypes[1])}`} style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}>{car.fuelTypes[1]}</span>
</span>
);
}
} else if (car.fuelType) {
fuelBadges = <span className={`ml-2 px-2 py-1 rounded-full text-xs font-bold ${getFuelBadgeColor(car.fuelType)}`}>{car.fuelType}</span>;
}
return ( return (
<Link href={`/dashboard/cars/${car.id}`}> <Link href={`/dashboard/cars/${car.id}`}>
<div className="border border-[var(--border)] rounded-xl p-4 shadow-sm hover:shadow-md transition bg-[var(--muted)]"> <div className="border border-[var(--border)] rounded-xl p-4 shadow-sm hover:shadow-md transition bg-[var(--muted)]">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-[var(--primary)]">{car.name}</h2> <h2 className="text-xl font-bold text-[var(--primary)]">{car.name}</h2>
<span className={`ml-2 px-2 py-1 rounded-full text-xs font-bold ${getFuelBadgeColor(car.fuelType)}`}>{car.fuelType}</span> <span className="flex flex-wrap gap-1">{fuelBadges}</span>
</div> </div>
<p className="text-[var(--foreground)]/80">{car.make} {car.model} ({car.year})</p> <p className="text-[var(--foreground)]/80">{car.make} {car.model} ({car.year})</p>
<div className="mt-3 text-sm text-[var(--foreground)]/80"> <div className="mt-3 text-sm text-[var(--foreground)]/80">
+24
View File
@@ -0,0 +1,24 @@
"use client";
export default function DeleteCarButton({ carId }: { carId: string }) {
const handleDelete = async (e: React.FormEvent) => {
e.preventDefault();
if (!confirm('Are you sure you want to delete this car? This will delete all fill-ups and mileage logs for this car.')) return;
const res = await fetch(`/api/cars/${carId}`, { method: 'DELETE' });
if (res.ok) {
window.location.href = '/dashboard';
} else {
alert('Failed to delete car.');
}
};
return (
<form onSubmit={handleDelete}>
<button
type="submit"
className="flex items-center gap-2 bg-red-600 text-white px-6 py-3 rounded-lg font-bold shadow-lg hover:bg-red-800 hover:scale-[1.03] transition-all"
>
🗑 Delete Car
</button>
</form>
);
}
+2 -1
View File
@@ -10,6 +10,7 @@ interface ActivityItem {
cost?: number; cost?: number;
currency?: string; currency?: string;
date: string; date: string;
fuelType?: string; // Add fuelType for fill-ups
} }
export default function RecentActivity() { export default function RecentActivity() {
@@ -46,7 +47,7 @@ export default function RecentActivity() {
</div> </div>
<div className="text-sm text-[var(--foreground)]/60"> <div className="text-sm text-[var(--foreground)]/60">
{item.type === 'fillup' {item.type === 'fillup'
? `${item.liters} L, ${item.mileage} km${item.cost ? `, ${item.cost} ${item.currency}` : ''}` ? `${item.liters} ${item.fuelType === 'ELECTRIC' ? 'kWh' : item.fuelType === 'LPG' ? 'L (LPG)' : item.fuelType === 'GASOLINE' ? 'L (Gasoline)' : item.fuelType === 'DIESEL' ? 'L (Diesel)' : 'L'}, ${item.mileage} km${item.cost ? `, ${item.cost} ${item.currency}` : ''}`
: `${item.mileage} km`} : `${item.mileage} km`}
{' '}on {new Date(item.date).toLocaleDateString()} {' '}on {new Date(item.date).toLocaleDateString()}
</div> </div>
+27 -6
View File
@@ -17,20 +17,28 @@ interface FillUp {
cost: number; cost: number;
currency: string; currency: string;
date: string | Date; date: string | Date;
car?: { fuelType?: string }; car?: { fuelType?: string; fuelTypes?: string[] };
fuelType?: string;
} }
export default function StatsPageClient({ fillUps, carId, error, loading = false }: { fillUps: FillUp[]; carId: string; error?: string; loading?: boolean }) { export default function StatsPageClient({ fillUps, carId, error, loading = false }: { fillUps: FillUp[]; carId: string; error?: string; loading?: boolean }) {
const [range, setRange] = useState<number | null>(30); const [range, setRange] = useState<number | null>(30);
const [units, setUnits] = useState<Units>('metric'); const [units, setUnits] = useState<Units>('metric');
// Hybrid: fuel type selection
const allFuelTypes = Array.from(new Set(fillUps.map(f => f.fuelType).filter(Boolean)));
const [selectedFuelType, setSelectedFuelType] = useState<string>('ALL');
const filteredByFuel = useMemo(() => {
if (selectedFuelType === 'ALL') return fillUps;
return fillUps.filter(f => f.fuelType === selectedFuelType);
}, [fillUps, selectedFuelType]);
const now = useMemo(() => new Date(), []); const now = useMemo(() => new Date(), []);
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!range) return fillUps; if (!range) return filteredByFuel;
return fillUps.filter(f => { return filteredByFuel.filter(f => {
const d = typeof f.date === 'string' ? new Date(f.date) : f.date; const d = typeof f.date === 'string' ? new Date(f.date) : f.date;
return d >= new Date(now.getTime() - (range ?? 30) * 24 * 60 * 60 * 1000); return d >= new Date(now.getTime() - (range ?? 30) * 24 * 60 * 60 * 1000);
}); });
}, [fillUps, range, now]); }, [filteredByFuel, range, now]);
if (error) { if (error) {
return ( return (
@@ -174,7 +182,7 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
]; ];
// Get fuel type from first fill-up (all fill-ups for a car have the same type) // Get fuel type from first fill-up (all fill-ups for a car have the same type)
const fuelType = fillUps[0]?.car?.fuelType; const fuelType = filtered[0]?.fuelType;
const isElectric = fuelType === 'ELECTRIC'; const isElectric = fuelType === 'ELECTRIC';
// If electric, always use metric units // If electric, always use metric units
const displayUnits = isElectric ? 'metric' : units; const displayUnits = isElectric ? 'metric' : units;
@@ -186,6 +194,18 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
<span role="img" aria-label="stats">📊</span> Fuel Stats <span role="img" aria-label="stats">📊</span> Fuel Stats
</h1> </h1>
<div className="flex flex-col sm:flex-row sm:items-center gap-2 mb-2 w-full justify-center"> <div className="flex flex-col sm:flex-row sm:items-center gap-2 mb-2 w-full justify-center">
{allFuelTypes.length > 1 && (
<select
value={selectedFuelType}
onChange={e => setSelectedFuelType(e.target.value)}
className="border border-[var(--border)] px-2 py-1 rounded-lg bg-[var(--background)] text-[var(--foreground)] font-semibold"
>
<option value="ALL">All Fuels</option>
{allFuelTypes.map(ft => (
<option key={ft} value={ft}>{ft}</option>
))}
</select>
)}
{!isElectric && ( {!isElectric && (
<UnitsToggle units={units} setUnits={setUnits} disabled={isElectric} /> <UnitsToggle units={units} setUnits={setUnits} disabled={isElectric} />
)} )}
@@ -225,7 +245,8 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false
<StatCard label={`Total Fuel Used`} value={`${totalLiters.toFixed(2)} ${isElectric ? 'kWh' : (displayUnits === 'imperial' ? 'gal' : 'L')}`} icon="⛽" color="secondary" info={isElectric ? 'Total kilowatt-hours used in selected period.' : (displayUnits === 'imperial' ? 'Total gallons used in selected period.' : 'Total liters used in selected period.')} /> <StatCard label={`Total Fuel Used`} value={`${totalLiters.toFixed(2)} ${isElectric ? 'kWh' : (displayUnits === 'imperial' ? 'gal' : 'L')}`} icon="⛽" color="secondary" info={isElectric ? 'Total kilowatt-hours used in selected period.' : (displayUnits === 'imperial' ? 'Total gallons used in selected period.' : 'Total liters used in selected period.')} />
<StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${filtered[0].currency}`} icon="💸" color="accent" info="Sum of all fill-up costs in selected period." /> <StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${filtered[0].currency}`} icon="💸" color="accent" info="Sum of all fill-up costs in selected period." />
<StatCard label="Average Consumption" value={`${avgConsumption.toFixed(2)} ${isElectric ? 'kWh / 100km' : (displayUnits === 'imperial' ? 'MPG' : 'L / 100km')}`} icon="📏" color="primary" info={isElectric ? 'Kilowatt-hours per 100km (lower is better).' : (displayUnits === 'imperial' ? 'Miles per gallon (higher is better).' : 'Liters per 100km (lower is better).')} /> <StatCard label="Average Consumption" value={`${avgConsumption.toFixed(2)} ${isElectric ? 'kWh / 100km' : (displayUnits === 'imperial' ? 'MPG' : 'L / 100km')}`} icon="📏" color="primary" info={isElectric ? 'Kilowatt-hours per 100km (lower is better).' : (displayUnits === 'imperial' ? 'Miles per gallon (higher is better).' : 'Liters per 100km (lower is better).')} />
<StatCard label={`Average Cost / ${displayUnits === 'imperial' ? 'mi' : 'km'}`} value={`${costPerDist.toFixed(2)} ${filtered[0].currency}`} icon="💰" color="secondary" info={`Average fuel cost per ${displayUnits === 'imperial' ? 'mile' : 'kilometer'}.`} /> <StatCard label="Cost per Distance" value={`${costPerDist.toFixed(2)} ${filtered[0].currency} / ${displayUnits === 'imperial' ? 'mi' : 'km'}`} icon="💰" color="secondary" info="Fuel cost per unit of distance." />
<StatCard label="Fill-Up Count" value={`${filtered.length}`} icon="⛽️" color="accent" info="Total number of fill-ups in selected period." />
</div> </div>
</div> </div>
</div> </div>