mirror of
https://github.com/EdiFarcas/Car-Fuel-Tracking-App.git
synced 2026-06-22 07:00:55 +03:00
Hybrid cars implementation 1.0(Without stats)
This commit is contained in:
@@ -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;
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user