diff --git a/prisma/migrations/20250709194039_hybrid_update/migration.sql b/prisma/migrations/20250709194039_hybrid_update/migration.sql new file mode 100644 index 0000000..dc5cbae --- /dev/null +++ b/prisma/migrations/20250709194039_hybrid_update/migration.sql @@ -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; diff --git a/prisma/migrations/20250709205035_delete_update/migration.sql b/prisma/migrations/20250709205035_delete_update/migration.sql new file mode 100644 index 0000000..e2a2d84 --- /dev/null +++ b/prisma/migrations/20250709205035_delete_update/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4c98ab7..3775af4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,27 +23,27 @@ model Car { make String // Manufacturer (e.g. "BMW") model String // Model (e.g. "320i") year Int // Year (e.g. 2019) - fuelType FuelType + fuelTypes FuelType[] // Changed from single fuelType to array for hybrid support fillUps FillUp[] mileage MileageEntry[] } - model FillUp { id String @id @default(cuid()) - car Car @relation(fields: [carId], references: [id]) + car Car @relation(fields: [carId], references: [id], onDelete: Cascade) carId String mileage Int liters Float cost Float currency Currency date DateTime @default(now()) + fuelType FuelType // Add fuelType to each fill-up } model MileageEntry { id String @id @default(cuid()) - car Car @relation(fields: [carId], references: [id]) + car Car @relation(fields: [carId], references: [id], onDelete: Cascade) carId String mileage Int date DateTime @default(now()) diff --git a/src/app/api/activity/route.ts b/src/app/api/activity/route.ts index 0d5fb64..7ad9424 100644 --- a/src/app/api/activity/route.ts +++ b/src/app/api/activity/route.ts @@ -39,6 +39,7 @@ export async function GET(req: Request) { cost: f.cost, currency: f.currency, date: f.date, + fuelType: f.fuelType, // Fix: Prisma FillUp model includes fuelType })), ...mileageEntries.map(m => ({ type: 'mileage', diff --git a/src/app/api/cars/[carId]/route.ts b/src/app/api/cars/[carId]/route.ts index 63ce1f8..cebfd19 100644 --- a/src/app/api/cars/[carId]/route.ts +++ b/src/app/api/cars/[carId]/route.ts @@ -12,7 +12,7 @@ export async function GET(req: Request, { params }: { params: { carId: string } const car = await prisma.car.findFirst({ where: { id: carId, - user: { email: session.user?.email! }, + user: { email: session.user?.email ?? undefined }, }, select: { id: true, @@ -20,10 +20,30 @@ export async function GET(req: Request, { params }: { params: { carId: string } make: true, model: true, year: true, - fuelType: true, + fuelTypes: true, }, }); if (!car) return NextResponse.json({ message: 'Car not found' }, { status: 404 }); 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' }); +} diff --git a/src/app/api/cars/route.ts b/src/app/api/cars/route.ts index 55c922a..335052f 100644 --- a/src/app/api/cars/route.ts +++ b/src/app/api/cars/route.ts @@ -7,14 +7,14 @@ export async function POST(req: Request) { if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); 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 }); } const user = await prisma.user.findUnique({ - where: { email: session.user?.email! }, + where: { email: session.user?.email ?? undefined }, }); if (!user) { @@ -27,7 +27,7 @@ export async function POST(req: Request) { make, model, year: parseInt(year), - fuelType, + fuelTypes: { set: fuelTypes }, userId: user.id, }, }); diff --git a/src/app/api/fillups/route.ts b/src/app/api/fillups/route.ts index 7780697..7d74bac 100644 --- a/src/app/api/fillups/route.ts +++ b/src/app/api/fillups/route.ts @@ -26,15 +26,15 @@ export async function POST(req: Request) { const session = await getSession(); if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); - const { carId, mileage, liters, cost, currency } = await req.json(); - if (!carId || !mileage || !liters || !cost || !currency) { + const { carId, mileage, liters, cost, currency, fuelType } = await req.json(); + if (!carId || !mileage || !liters || !cost || !currency || !fuelType) { return NextResponse.json({ message: 'Missing fields' }, { status: 400 }); } const car = await prisma.car.findFirst({ where: { 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), currency, carId, + fuelType, }, }); diff --git a/src/app/dashboard/cars/[carId]/fillups/page.tsx b/src/app/dashboard/cars/[carId]/fillups/page.tsx index 29d1da4..e756f97 100644 --- a/src/app/dashboard/cars/[carId]/fillups/page.tsx +++ b/src/app/dashboard/cars/[carId]/fillups/page.tsx @@ -11,6 +11,7 @@ interface FillUp { cost: number; currency: string; date: string; + fuelType: string; // Add fuelType to FillUp interface } const currencies = ['EUR', 'USD', 'RON', 'GBP']; @@ -18,12 +19,13 @@ const currencies = ['EUR', 'USD', 'RON', 'GBP']; export default function FillUpsPage() { const { carId } = useParams(); const [fillups, setFillups] = useState([]); - const [carFuelType, setCarFuelType] = useState(undefined); + const [carFuelTypes, setCarFuelTypes] = useState([]); const [form, setForm] = useState({ mileage: '', liters: '', cost: '', currency: 'EUR', + fuelType: '', }); const [error, setError] = useState(''); @@ -34,11 +36,11 @@ export default function FillUpsPage() { .then(setFillups); }, [carId]); - // Fetch car fuel type + // Fetch car fuel types useEffect(() => { fetch(`/api/cars/${carId}`) .then((res) => res.json()) - .then((car) => setCarFuelType(car?.fuelType)); + .then((car) => setCarFuelTypes(car?.fuelTypes || [])); }, [carId]); const handleChange = (e: React.ChangeEvent) => { @@ -49,14 +51,14 @@ export default function FillUpsPage() { e.preventDefault(); const res = await fetch('/api/fillups', { method: 'POST', - body: JSON.stringify({ ...form, carId }), + body: JSON.stringify({ ...form, carId, fuelType: form.fuelType || carFuelTypes[0] }), headers: { 'Content-Type': 'application/json' }, }); if (res.ok) { const newFill = await res.json(); setFillups((prev) => [newFill, ...prev]); - setForm({ mileage: '', liters: '', cost: '', currency: 'EUR' }); + setForm({ mileage: '', liters: '', cost: '', currency: 'EUR', fuelType: carFuelTypes[0] || '' }); setError(''); } else { const data = await res.json(); @@ -64,8 +66,8 @@ export default function FillUpsPage() { } }; - // Only render fill-ups when carFuelType is loaded - if (carFuelType === undefined) { + // Only render fill-ups when carFuelTypes is loaded + if (!carFuelTypes.length) { return (

Fuel Fill-Ups

@@ -95,21 +97,39 @@ export default function FillUpsPage() {
{ + 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} 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)]" /> - {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?'; + })()}
@@ -141,6 +161,25 @@ export default function FillUpsPage() { ))}
+ {carFuelTypes.length > 1 && ( +
+ + +
+ )} + {carFuelTypes.length === 1 && ( + + )} @@ -149,7 +188,7 @@ export default function FillUpsPage() {
    {fillups.map((fill) => ( - + ))}
diff --git a/src/app/dashboard/cars/[carId]/page.tsx b/src/app/dashboard/cars/[carId]/page.tsx index df211ed..0d1e9a9 100644 --- a/src/app/dashboard/cars/[carId]/page.tsx +++ b/src/app/dashboard/cars/[carId]/page.tsx @@ -3,6 +3,7 @@ import { prisma } from '@/lib/prisma'; import { redirect } from 'next/navigation'; import Link from 'next/link'; import { FaCarSide, FaGasPump, FaCalendarAlt, FaIndustry, FaBolt } from 'react-icons/fa'; +import DeleteCarButton from '@/components/DeleteCarButton'; interface CarDetailPageProps { params: { @@ -15,13 +16,21 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) { const session = await getSession(); if (!session) redirect('/auth/login'); - const userEmail = session.user?.email!; + const userEmail = session.user?.email || ''; const car = await prisma.car.findFirst({ where: { id: resolvedParams.carId, 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) { return
Car not found or unauthorized access.
; @@ -44,8 +53,15 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) {

{car.name} {car.year} + {Array.isArray(car.fuelTypes) && car.fuelTypes.length > 1 ? ( + + {fuelMeta[car.fuelTypes[0]]?.icon}{car.fuelTypes[0]} + {fuelMeta[car.fuelTypes[1]]?.icon}{car.fuelTypes[1]} + + ) : Array.isArray(car.fuelTypes) && car.fuelTypes.length === 1 ? ( + {fuelMeta[car.fuelTypes[0]]?.icon}{car.fuelTypes[0]} + ) : null}

- {fuelMeta[car.fuelType]?.icon}{car.fuelType} {/* Car Details */}
@@ -65,9 +81,12 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) { {car.year}
- {fuelMeta[car.fuelType]?.icon} - Fuel: - {car.fuelType} + {Array.isArray(car.fuelTypes) && car.fuelTypes.length > 0 + ? car.fuelTypes.map((ft: string) => ( + {fuelMeta[ft]?.icon}{ft} + )) + : null + }
{/* Actions */} @@ -90,6 +109,7 @@ export default async function CarDetailPage({ params }: CarDetailPageProps) { > 📊 View Stats + diff --git a/src/app/dashboard/cars/new/page.tsx b/src/app/dashboard/cars/new/page.tsx index 2bbcc81..cabf0e4 100644 --- a/src/app/dashboard/cars/new/page.tsx +++ b/src/app/dashboard/cars/new/page.tsx @@ -12,16 +12,33 @@ export default function AddCarPage() { make: '', model: '', year: '', - fuelType: 'GASOLINE', + fuelTypes: ['GASOLINE'], // default to one selected }); const [error, setError] = useState(''); const handleChange = (e: React.ChangeEvent) => { - 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) => { e.preventDefault(); + if (!form.fuelTypes.length) { + setError('Please select at least one fuel type.'); + return; + } const res = await fetch('/api/cars', { method: 'POST', body: JSON.stringify(form), @@ -97,18 +114,22 @@ export default function AddCarPage() { />
- - + {type} + ))} - +
+ + ); +} diff --git a/src/components/RecentActivity.tsx b/src/components/RecentActivity.tsx index 2decfe9..90ec400 100644 --- a/src/components/RecentActivity.tsx +++ b/src/components/RecentActivity.tsx @@ -10,6 +10,7 @@ interface ActivityItem { cost?: number; currency?: string; date: string; + fuelType?: string; // Add fuelType for fill-ups } export default function RecentActivity() { @@ -46,7 +47,7 @@ export default function RecentActivity() {
{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`} {' '}on {new Date(item.date).toLocaleDateString()}
diff --git a/src/components/StatsPageClient.tsx b/src/components/StatsPageClient.tsx index fe3c742..c80ddc3 100644 --- a/src/components/StatsPageClient.tsx +++ b/src/components/StatsPageClient.tsx @@ -17,20 +17,28 @@ interface FillUp { cost: number; currency: string; 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 }) { const [range, setRange] = useState(30); const [units, setUnits] = useState('metric'); + // Hybrid: fuel type selection + const allFuelTypes = Array.from(new Set(fillUps.map(f => f.fuelType).filter(Boolean))); + const [selectedFuelType, setSelectedFuelType] = useState('ALL'); + const filteredByFuel = useMemo(() => { + if (selectedFuelType === 'ALL') return fillUps; + return fillUps.filter(f => f.fuelType === selectedFuelType); + }, [fillUps, selectedFuelType]); const now = useMemo(() => new Date(), []); const filtered = useMemo(() => { - if (!range) return fillUps; - return fillUps.filter(f => { + if (!range) return filteredByFuel; + return filteredByFuel.filter(f => { const d = typeof f.date === 'string' ? new Date(f.date) : f.date; return d >= new Date(now.getTime() - (range ?? 30) * 24 * 60 * 60 * 1000); }); - }, [fillUps, range, now]); + }, [filteredByFuel, range, now]); if (error) { 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) - const fuelType = fillUps[0]?.car?.fuelType; + const fuelType = filtered[0]?.fuelType; const isElectric = fuelType === 'ELECTRIC'; // If electric, always use metric units const displayUnits = isElectric ? 'metric' : units; @@ -186,6 +194,18 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false 📊 Fuel Stats
+ {allFuelTypes.length > 1 && ( + + )} {!isElectric && ( )} @@ -225,7 +245,8 @@ export default function StatsPageClient({ fillUps, carId, error, loading = false - + +