first commit

This commit is contained in:
AnuragJangid18 2026-09-05 18:52:26 +05:30
parent e7365b06ac
commit a9e21cbe3c
9 changed files with 874 additions and 320 deletions

4
.env
View file

@ -21,3 +21,7 @@ DB_PORT=3306
DB_USER=root DB_USER=root
DB_PASSWORD=Z41kfdw8e8@sql DB_PASSWORD=Z41kfdw8e8@sql
DB_NAME=roadrentals DB_NAME=roadrentals
# Admin Credentials
ADMIN_ID=Himanshu Jaiswal
ADMIN_PASSWORD=Himan@1314

View file

@ -114,214 +114,7 @@ app.post("/api/bookings", async (req, res) => {
} }
}); });
// Pricing Engine & Car Data // --- DYNAMIC PRICING ENGINE (DB DRIVEN) ---
const VEHICLES = [
// LUXURY TIER
{
id: 1,
name: "Audi A6 Premium",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "audi_a6",
baseFarePerDay: 12000,
driverAllowancePerDay: 600,
extraKmRate: 45,
includedKmPerDay: 250,
badge: "Executive",
features: ["Leather Interior", "Dual Climate", "Premium Audio", "Sunroof"]
},
{
id: 2,
name: "BMW 7 Series",
type: "LUXURY",
seats: "4+1",
luggage: "3 Bags",
image: "bmw_7_series",
baseFarePerDay: 25000,
driverAllowancePerDay: 800,
extraKmRate: 85,
includedKmPerDay: 200,
badge: "VVIP Choice",
features: ["Rear-seat Entertainment", "Massage Seats", "Quiet Cabin", "Chauffeur Driven"]
},
{
id: 3,
name: "Mercedes S-Class",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "mercedes_s_class",
baseFarePerDay: 28000,
driverAllowancePerDay: 800,
extraKmRate: 90,
includedKmPerDay: 200,
badge: "Ultimate Luxury",
features: ["Ambient Lighting", "Burmester Sound", "Premium Chauffeur"]
},
// SUV/MUV TIER
{
id: 4,
name: "Toyota Fortuner 4x4",
type: "SUV",
seats: "6+1",
luggage: "4 Bags",
image: "fortuner",
baseFarePerDay: 6500,
driverAllowancePerDay: 500,
extraKmRate: 25,
includedKmPerDay: 300,
badge: "Advanture King",
features: ["Off-road capable", "Powerful AC", "Spacious", "Rugged Built"]
},
{
id: 5,
name: "Innova Crysta",
type: "MUV",
seats: "7+1",
luggage: "3 Bags",
image: "innova_crysta",
baseFarePerDay: 4500,
driverAllowancePerDay: 400,
extraKmRate: 18,
includedKmPerDay: 300,
badge: "Best Seller",
features: ["Clean Interiors", "Captain Seats", "Smooth Ride", "Carrier Available"]
},
{
id: 6,
name: "Toyota Innova",
type: "MUV",
seats: "7+1",
luggage: "2 Bags",
image: "innova",
baseFarePerDay: 3800,
driverAllowancePerDay: 400,
extraKmRate: 16,
includedKmPerDay: 300,
badge: "Value MUV",
features: ["Comfortable", "Spacious", "Economical for Family"]
},
// SEDAN TIER
{
id: 7,
name: "Toyota Camry",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "camry",
baseFarePerDay: 4200,
driverAllowancePerDay: 400,
extraKmRate: 18,
includedKmPerDay: 300,
badge: "Corporate Elite",
features: ["Hybrid Tech", "Quiet Ride", "Executive Space"]
},
{
id: 8,
name: "Toyota Etios",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "etios",
baseFarePerDay: 2800,
driverAllowancePerDay: 400,
extraKmRate: 13,
includedKmPerDay: 300,
badge: "Economic",
features: ["Spacious Boot", "Good Legroom", "Standard AC"]
},
{
id: 9,
name: "Swift Dzire",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "dzire",
baseFarePerDay: 2600,
driverAllowancePerDay: 400,
extraKmRate: 13,
includedKmPerDay: 300,
badge: "Economic Choice",
features: ["Popular", "Quick Maneuver", "Efficient AC"]
},
// GROUP TRAVEL
{
id: 11,
name: "Audi Q7 Luxury SUV",
type: "SUV",
seats: "6+1",
luggage: "4 Bags",
image: "audi_q7",
baseFarePerDay: 15000,
driverAllowancePerDay: 700,
extraKmRate: 55,
includedKmPerDay: 250,
badge: "Luxury SUV",
features: ["Quattro 4x4", "Panoramic Sunroof", "Premium Audio", "Heated Seats"]
},
{
id: 12,
name: "BMW 5 Series",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "bmw_5_series",
baseFarePerDay: 14000,
driverAllowancePerDay: 600,
extraKmRate: 50,
includedKmPerDay: 250,
badge: "Business Class",
features: ["Dynamic Drive", "Luxury Interiors", "Silent Hybrid", "Touchscreen Control"]
},
{
id: 13,
name: "Mercedes E-Class",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "mercedes_e_class",
baseFarePerDay: 14500,
driverAllowancePerDay: 600,
extraKmRate: 52,
includedKmPerDay: 250,
badge: "Timeless Elegance",
features: ["Soft Close Doors", "Ambient Light", "Professional Driver", "WIFI Enabled"]
},
{
id: 14,
name: "Toyota Corolla Altis",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "corolla_altis",
baseFarePerDay: 3800,
driverAllowancePerDay: 400,
extraKmRate: 16,
includedKmPerDay: 300,
badge: "Executive Sedan",
features: ["Durable", "Comfortable Suspension", "Clean Air Filter"]
}
];
// Lead Capture
app.post("/api/leads", async (req, res) => {
try {
const { name, mobile_number, tripType, pickupLocation, dropLocation } = req.body;
if (!name || !mobile_number) return res.status(400).json({ detail: "Name and Mobile are required" });
await pool.execute(
`INSERT INTO Leads (name, mobile_number, trip_type, pickup_location, drop_location) VALUES (?, ?, ?, ?, ?)`,
[name, mobile_number, tripType || null, pickupLocation || null, dropLocation || null]
);
res.status(201).json({ status: 1, message: "Lead captured" });
} catch (error) {
console.error("Lead Error:", error);
res.status(500).json({ detail: "Failed to capture lead" });
}
});
// Centralized Pricing Calculator
app.post("/api/pricing/calculate", async (req, res) => { app.post("/api/pricing/calculate", async (req, res) => {
try { try {
const { tripType, pickupDate, returnDate, packageType } = req.body; const { tripType, pickupDate, returnDate, packageType } = req.body;
@ -331,7 +124,12 @@ app.post("/api/pricing/calculate", async (req, res) => {
const diffTime = Math.abs(end - start); const diffTime = Math.abs(end - start);
const durationDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) || 1; const durationDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) || 1;
const results = VEHICLES.map(vehicle => { // Fetch ACTIVE vehicles from DB
const [rows] = await pool.execute("SELECT * FROM Vehicles WHERE isAvailable = TRUE");
const results = rows.map(vehicle => {
const features = typeof vehicle.features === 'string' ? JSON.parse(vehicle.features) : vehicle.features;
let totalBaseFare = vehicle.baseFarePerDay * durationDays; let totalBaseFare = vehicle.baseFarePerDay * durationDays;
let totalDriverAllowance = vehicle.driverAllowancePerDay * durationDays; let totalDriverAllowance = vehicle.driverAllowancePerDay * durationDays;
@ -343,11 +141,11 @@ app.post("/api/pricing/calculate", async (req, res) => {
totalDriverAllowance = 0; totalDriverAllowance = 0;
} }
// Special Case: Airport Transfers (Flat Rates) // Special Case: Airport Transfers
if (tripType === 'airport') { if (tripType === 'airport') {
if (vehicle.type === 'SEDAN') totalBaseFare = 1200; if (vehicle.type === 'SEDAN') totalBaseFare = 1200;
else if (vehicle.type === 'SUV') totalBaseFare = 2200; else if (vehicle.type === 'SUV') totalBaseFare = 2200;
else totalBaseFare = 1800; // MUV/Other else totalBaseFare = 1800;
totalDriverAllowance = 0; totalDriverAllowance = 0;
} }
@ -356,18 +154,19 @@ app.post("/api/pricing/calculate", async (req, res) => {
return { return {
...vehicle, ...vehicle,
features,
calc: { calc: {
days: durationDays, days: durationDays,
baseFare: totalBaseFare, baseFare: totalBaseFare,
driverAllowance: totalDriverAllowance, driverAllowance: totalDriverAllowance,
totalKmIncluded: vehicle.includedKmPerDay * (tripType === 'airport' ? 0.2 : durationDays), // Approx for airport totalKmIncluded: vehicle.includedKmPerDay * (tripType === 'airport' ? 0.2 : durationDays),
grandTotal, grandTotal,
advancePayment, advancePayment,
math: tripType === 'airport' || tripType === 'local' math: tripType === 'airport' || tripType === 'local'
? `Fixed ${tripType.charAt(0).toUpperCase() + tripType.slice(1)} Rate` ? `Fixed ${tripType.charAt(0).toUpperCase() + tripType.slice(1)} Rate`
: `(₹${vehicle.baseFarePerDay} x ${durationDays} Days) + ₹${totalDriverAllowance} Driver` : `(₹${vehicle.baseFarePerDay} x ${durationDays} Days) + ₹${totalDriverAllowance} Driver`
}, },
inclusions: ["AC", "Professional Driver", tripType === 'airport' ? "Airport Toll Included" : "First " + (vehicle.includedKmPerDay * durationDays) + " Kms"], inclusions: ["AC", "Professional Driver", tripType === 'airport' ? "Airport Toll Included" : "First " + (vehicle.includedKmPerDay * (tripType === 'airport' ? 1 : durationDays)) + " Kms"],
exclusions: ["Parking Charges", "Extra Waiting Time", "Inter-state Permits"] exclusions: ["Parking Charges", "Extra Waiting Time", "Inter-state Permits"]
}; };
}); });
@ -379,6 +178,86 @@ app.post("/api/pricing/calculate", async (req, res) => {
} }
}); });
// --- ADMIN API ENDPOINTS ---
// Secure Admin Login
app.post("/api/admin/login", async (req, res) => {
const { adminId, password } = req.body;
// Environment-based credentials for security
const VALID_ADMIN_ID = process.env.ADMIN_ID || "admin";
const VALID_ADMIN_PASS = process.env.ADMIN_PASSWORD || "admin123";
if (adminId === VALID_ADMIN_ID && password === VALID_ADMIN_PASS) {
res.json({ success: true, token: "secure_session_token_xyz_987" });
} else {
res.status(401).json({ detail: "Invalid Admin ID or Password" });
}
});
// Get Full Fleet for Management
app.get("/api/admin/vehicles", async (req, res) => {
try {
const [rows] = await pool.execute("SELECT * FROM Vehicles ORDER BY id DESC");
const cars = rows.map(v => ({
...v,
features: typeof v.features === 'string' ? JSON.parse(v.features) : v.features
}));
res.json(cars);
} catch (err) {
res.status(500).json({ detail: "Fail to fetch fleet" });
}
});
// Update Availability or Pricing
app.patch("/api/admin/vehicles/:id", async (req, res) => {
try {
const { id } = req.params;
const { baseFarePerDay, isAvailable } = req.body;
if (baseFarePerDay !== undefined) {
await pool.execute("UPDATE Vehicles SET baseFarePerDay = ? WHERE id = ?", [baseFarePerDay, id]);
}
if (isAvailable !== undefined) {
await pool.execute("UPDATE Vehicles SET isAvailable = ? WHERE id = ?", [isAvailable, id]);
}
res.json({ success: true, message: "Vehicle updated successfully" });
} catch (err) {
res.status(500).json({ detail: "Failed to update vehicle" });
}
});
// Add New Cab
app.post("/api/admin/vehicles", async (req, res) => {
try {
const v = req.body;
const features = JSON.stringify(v.features || []);
await pool.execute(
`INSERT INTO Vehicles (name, type, seats, luggage, image, baseFarePerDay, driverAllowancePerDay, extraKmRate, includedKmPerDay, badge, features)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[v.name, v.type, v.seats, v.luggage, v.image, v.baseFarePerDay, v.driverAllowancePerDay, v.extraKmRate, v.includedKmPerDay, v.badge, features]
);
res.status(201).json({ success: true, message: "New vehicle added" });
} catch (err) {
res.status(500).json({ detail: "Failed to add new vehicle" });
}
});
// Delete Cab
app.delete("/api/admin/vehicles/:id", async (req, res) => {
try {
const { id } = req.params;
await pool.execute("DELETE FROM Vehicles WHERE id = ?", [id]);
res.json({ success: true, message: "Vehicle removed" });
} catch (err) {
res.status(500).json({ detail: "Failed to delete vehicle" });
}
});
const PORT = 8000; const PORT = 8000;
app.listen(PORT, () => { app.listen(PORT, () => {

235
scripts/migrate_vehicles.js Normal file
View file

@ -0,0 +1,235 @@
import "dotenv/config";
import mysql from "mysql2/promise";
const vehicles = [
{
name: "Audi A6 Premium",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "audi_a6",
baseFarePerDay: 12000,
driverAllowancePerDay: 600,
extraKmRate: 45,
includedKmPerDay: 250,
badge: "Executive",
features: JSON.stringify(["Leather Interior", "Dual Climate", "Premium Audio", "Sunroof"])
},
{
name: "BMW 7 Series",
type: "LUXURY",
seats: "4+1",
luggage: "3 Bags",
image: "bmw_7_series",
baseFarePerDay: 25000,
driverAllowancePerDay: 800,
extraKmRate: 85,
includedKmPerDay: 200,
badge: "VVIP Choice",
features: JSON.stringify(["Rear-seat Entertainment", "Massage Seats", "Quiet Cabin", "Chauffeur Driven"])
},
{
name: "Mercedes S-Class",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "mercedes_s_class",
baseFarePerDay: 28000,
driverAllowancePerDay: 800,
extraKmRate: 90,
includedKmPerDay: 200,
badge: "Ultimate Luxury",
features: JSON.stringify(["Ambient Lighting", "Burmester Sound", "Premium Chauffeur"])
},
{
name: "Toyota Fortuner 4x4",
type: "SUV",
seats: "6+1",
luggage: "4 Bags",
image: "fortuner",
baseFarePerDay: 6500,
driverAllowancePerDay: 500,
extraKmRate: 25,
includedKmPerDay: 300,
badge: "Advanture King",
features: JSON.stringify(["Off-road capable", "Powerful AC", "Spacious", "Rugged Built"])
},
{
name: "Innova Crysta",
type: "MUV",
seats: "7+1",
luggage: "3 Bags",
image: "innova_crysta",
baseFarePerDay: 4500,
driverAllowancePerDay: 400,
extraKmRate: 18,
includedKmPerDay: 300,
badge: "Best Seller",
features: JSON.stringify(["Clean Interiors", "Captain Seats", "Smooth Ride", "Carrier Available"])
},
{
name: "Toyota Innova",
type: "MUV",
seats: "7+1",
luggage: "2 Bags",
image: "innova",
baseFarePerDay: 3800,
driverAllowancePerDay: 400,
extraKmRate: 16,
includedKmPerDay: 300,
badge: "Value MUV",
features: JSON.stringify(["Comfortable", "Spacious", "Economical for Family"])
},
{
name: "Toyota Camry",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "camry",
baseFarePerDay: 4200,
driverAllowancePerDay: 400,
extraKmRate: 18,
includedKmPerDay: 300,
badge: "Corporate Elite",
features: JSON.stringify(["Hybrid Tech", "Quiet Ride", "Executive Space"])
},
{
name: "Toyota Etios",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "etios",
baseFarePerDay: 2800,
driverAllowancePerDay: 400,
extraKmRate: 13,
includedKmPerDay: 300,
badge: "Economic",
features: JSON.stringify(["Spacious Boot", "Good Legroom", "Standard AC"])
},
{
name: "Swift Dzire",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "dzire",
baseFarePerDay: 2600,
driverAllowancePerDay: 400,
extraKmRate: 13,
includedKmPerDay: 300,
badge: "Economic Choice",
features: JSON.stringify(["Popular", "Quick Maneuver", "Efficient AC"])
},
{
name: "Audi Q7 Luxury SUV",
type: "SUV",
seats: "6+1",
luggage: "4 Bags",
image: "audi_q7",
baseFarePerDay: 15000,
driverAllowancePerDay: 700,
extraKmRate: 55,
includedKmPerDay: 250,
badge: "Luxury SUV",
features: JSON.stringify(["Quattro 4x4", "Panoramic Sunroof", "Premium Audio", "Heated Seats"])
},
{
name: "BMW 5 Series",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "bmw_5_series",
baseFarePerDay: 14000,
driverAllowancePerDay: 600,
extraKmRate: 50,
includedKmPerDay: 250,
badge: "Business Class",
features: JSON.stringify(["Dynamic Drive", "Luxury Interiors", "Silent Hybrid", "Touchscreen Control"])
},
{
name: "Mercedes E-Class",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "mercedes_e_class",
baseFarePerDay: 14500,
driverAllowancePerDay: 600,
extraKmRate: 52,
includedKmPerDay: 250,
badge: "Timeless Elegance",
features: JSON.stringify(["Soft Close Doors", "Ambient Light", "Professional Driver", "WIFI Enabled"])
},
{
name: "Toyota Corolla Altis",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "corolla_altis",
baseFarePerDay: 3800,
driverAllowancePerDay: 400,
extraKmRate: 16,
includedKmPerDay: 300,
badge: "Executive Sedan",
features: JSON.stringify(["Durable", "Comfortable Suspension", "Clean Air Filter"])
},
{
name: "Toyota Commuter",
type: "VAN",
seats: "12+1",
luggage: "6 Bags",
image: "toyota_commuter",
baseFarePerDay: 8500,
driverAllowancePerDay: 600,
extraKmRate: 28,
includedKmPerDay: 300,
badge: "Large Group",
features: JSON.stringify(["12 Seater", "High Ceiling", "Multiple Bags Space"])
}
];
async function runMigration() {
const pool = mysql.createPool({
host: process.env.DB_HOST || "localhost",
user: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || "roadrentals",
});
try {
console.log("Creating Vehicles table...");
await pool.execute(`
CREATE TABLE IF NOT EXISTS Vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(50),
seats VARCHAR(20),
luggage VARCHAR(50),
image VARCHAR(255),
baseFarePerDay INT,
driverAllowancePerDay INT,
extraKmRate INT,
includedKmPerDay INT,
badge VARCHAR(100),
features TEXT,
isAvailable BOOLEAN DEFAULT TRUE,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log("Seeding vehicles...");
for (const v of vehicles) {
await pool.execute(
`INSERT INTO Vehicles (name, type, seats, luggage, image, baseFarePerDay, driverAllowancePerDay, extraKmRate, includedKmPerDay, badge, features)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[v.name, v.type, v.seats, v.luggage, v.image, v.baseFarePerDay, v.driverAllowancePerDay, v.extraKmRate, v.includedKmPerDay, v.badge, v.features]
);
}
console.log("Migration successful!");
} catch (error) {
console.error("Migration failed:", error);
} finally {
await pool.end();
}
}
runMigration();

View file

@ -2,6 +2,8 @@ import { useState } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import SearchPage from "./pages/SearchPage"; import SearchPage from "./pages/SearchPage";
import AdminLogin from "./pages/Admin/AdminLogin";
import AdminDashboard from "./pages/Admin/AdminDashboard";
import AuthModal from "./components/Auth/AuthModal"; import AuthModal from "./components/Auth/AuthModal";
import "./index.css"; import "./index.css";
@ -31,6 +33,9 @@ function App() {
/> />
} }
/> />
<Route path="/admin" element={<AdminLogin />} />
<Route path="/admin/login" element={<AdminLogin />} />
<Route path="/admin/dashboard" element={<AdminDashboard />} />
</Routes> </Routes>
<AuthModal <AuthModal

View file

@ -1,3 +1,4 @@
import { Link } from "react-router-dom";
import Logo from "../Logo/Logo"; import Logo from "../Logo/Logo";
const Footer = () => { const Footer = () => {
@ -81,6 +82,11 @@ const Footer = () => {
<p className="text-gray-500 text-sm"> <p className="text-gray-500 text-sm">
Copyright © 2018-2025 road.rentals Tours & Travels. All Rights Reserved. Copyright © 2018-2025 road.rentals Tours & Travels. All Rights Reserved.
</p> </p>
<div className="flex items-center gap-4">
<Link to="/admin" className="text-xs text-gray-600 hover:text-indigo-400 opacity-60 hover:opacity-100 transition-all font-medium uppercase tracking-tighter">
Owner Login
</Link>
</div>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -0,0 +1,316 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Car, CreditCard, Power, Plus, Trash2, LayoutDashboard, Settings, LogOut, Search, MoreVertical, X, Check } from 'lucide-react';
const AdminDashboard = () => {
const [vehicles, setVehicles] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [editingId, setEditingId] = useState(null);
const [newCar, setNewCar] = useState({
name: '', type: 'SEDAN', seats: '4+1', luggage: '2 Bags', image: 'dzire',
baseFarePerDay: 2500, driverAllowancePerDay: 400, extraKmRate: 13,
includedKmPerDay: 300, badge: 'Standard', features: []
});
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem('adminToken');
if (!token) navigate('/admin/login');
else fetchVehicles();
}, []);
const fetchVehicles = async () => {
try {
const res = await fetch('/api/admin/vehicles');
const data = await res.json();
setVehicles(data);
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};
const toggleAvailability = async (id, currentStatus) => {
try {
await fetch(`/api/admin/vehicles/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isAvailable: !currentStatus })
});
fetchVehicles();
} catch (err) {
console.error(err);
}
};
const updatePrice = async (id, newPrice) => {
try {
await fetch(`/api/admin/vehicles/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ baseFarePerDay: parseInt(newPrice) })
});
setEditingId(null);
fetchVehicles();
} catch (err) {
console.error(err);
}
};
const deleteVehicle = async (id) => {
if (!window.confirm('Delete this vehicle from fleet?')) return;
try {
await fetch(`/api/admin/vehicles/${id}`, { method: 'DELETE' });
fetchVehicles();
} catch (err) {
console.error(err);
}
};
const handleAddVehicle = async (e) => {
e.preventDefault();
try {
await fetch('/api/admin/vehicles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newCar)
});
setShowAddModal(false);
fetchVehicles();
} catch (err) {
console.error(err);
}
};
const logout = () => {
localStorage.removeItem('adminToken');
navigate('/admin/login');
};
return (
<div className="min-h-screen bg-[#f8fafc] flex">
{/* Sidebar */}
<div className="w-64 bg-slate-900 text-white flex flex-col">
<div className="p-6 border-b border-white/10">
<h1 className="text-xl font-bold flex items-center gap-2">
<div className="w-8 h-8 bg-indigo-500 rounded-lg flex items-center justify-center">
<Car className="w-5 h-5" />
</div>
road.rentals
</h1>
<p className="text-slate-400 text-xs mt-1">Admin Panel v2.0</p>
</div>
<nav className="flex-1 p-4 space-y-2">
<button className="w-full flex items-center gap-3 px-4 py-3 bg-indigo-600 rounded-xl text-sm font-semibold text-white">
<LayoutDashboard className="w-5 h-5" />
Fleet Control
</button>
<button className="w-full flex items-center gap-3 px-4 py-3 text-slate-400 hover:bg-white/5 rounded-xl text-sm font-semibold transition-all">
<CreditCard className="w-5 h-5" />
Booking Reports
</button>
<button className="w-full flex items-center gap-3 px-4 py-3 text-slate-400 hover:bg-white/5 rounded-xl text-sm font-semibold transition-all">
<Settings className="w-5 h-5" />
System Config
</button>
</nav>
<div className="p-4 mt-auto">
<button
onClick={logout}
className="w-full flex items-center gap-3 px-4 py-3 text-red-400 hover:bg-red-500/10 rounded-xl text-sm font-semibold transition-all"
>
<LogOut className="w-5 h-5" />
Logout
</button>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col max-h-screen overflow-hidden">
<header className="bg-white border-b border-slate-200 px-8 py-4 flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-slate-800">Fleet Management</h2>
<p className="text-sm text-slate-500">Inventory control and dynamic pricing</p>
</div>
<button
onClick={() => setShowAddModal(true)}
className="bg-indigo-600 hover:bg-indigo-700 text-white px-5 py-2.5 rounded-xl text-sm font-bold shadow-lg shadow-indigo-100 flex items-center gap-2 transition-all"
>
<Plus className="w-5 h-5" />
Add New Vehicle
</button>
</header>
<main className="flex-1 p-8 overflow-y-auto">
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<p className="text-sm text-slate-500 font-semibold mb-1">Total Fleet</p>
<h3 className="text-28 font-bold text-slate-900">{vehicles.length} Cars</h3>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<p className="text-sm text-slate-400 font-semibold mb-1">Active Now</p>
<h3 className="text-28 font-bold text-green-600">{vehicles.filter(v => v.isAvailable).length} Cars</h3>
</div>
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
<p className="text-sm text-slate-400 font-semibold mb-1">Maintenance</p>
<h3 className="text-28 font-bold text-red-500">{vehicles.filter(v => !v.isAvailable).length} Cars</h3>
</div>
<div className="bg-indigo-600 p-6 rounded-2xl shadow-xl shadow-indigo-100 text-white">
<p className="text-sm text-white/70 font-semibold mb-1">Fleet Revenue</p>
<h3 className="text-28 font-bold">Dynamic</h3>
</div>
</div>
{/* Table */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50 border-b border-slate-200">
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Vehicle Details</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Type / Capacity</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Base Price / Day</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{isLoading ? (
<tr><td colSpan="5" className="p-10 text-center text-slate-400">Loading fleet data...</td></tr>
) : vehicles.map(vehicle => (
<tr key={vehicle.id} className="hover:bg-slate-50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-slate-100 rounded-lg overflow-hidden flex items-center justify-center">
<Car className="w-6 h-6 text-slate-400" />
</div>
<div>
<p className="font-bold text-slate-900">{vehicle.name}</p>
<p className="text-xs text-slate-500">{vehicle.badge || 'Standard'}</p>
</div>
</div>
</td>
<td className="px-6 py-4">
<span className="text-xs font-bold px-2 py-1 bg-slate-100 rounded-md text-slate-600 mr-2">{vehicle.type}</span>
<span className="text-sm text-slate-500 font-medium">{vehicle.seats} Seater</span>
</td>
<td className="px-6 py-4">
{editingId === vehicle.id ? (
<div className="flex items-center gap-2">
<input
type="number"
defaultValue={vehicle.baseFarePerDay}
className="w-20 px-2 py-1 border border-indigo-300 rounded outline-none"
onKeyDown={(e) => {
if (e.key === 'Enter') updatePrice(vehicle.id, e.target.value);
if (e.key === 'Escape') setEditingId(null);
}}
/>
</div>
) : (
<div
onClick={() => setEditingId(vehicle.id)}
className="font-bold text-slate-900 cursor-pointer hover:text-indigo-600"
>
{vehicle.baseFarePerDay}
</div>
)}
</td>
<td className="px-6 py-4">
<button
onClick={() => toggleAvailability(vehicle.id, vehicle.isAvailable)}
className={`flex items-center gap-2 text-xs font-bold px-3 py-1.5 rounded-full transition-all ${
vehicle.isAvailable
? 'bg-green-50 text-green-600 border border-green-100 hover:bg-green-100'
: 'bg-red-50 text-red-600 border border-red-100 hover:bg-red-100'
}`}
>
<Power className="w-3 h-3" />
{vehicle.isAvailable ? 'AVAILABLE' : 'OFF DUTY'}
</button>
</td>
<td className="px-6 py-4 text-right">
<button
onClick={() => deleteVehicle(vehicle.id)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-all"
>
<Trash2 className="w-5 h-5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</main>
</div>
{/* Add Cab Modal */}
{showAddModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-sm">
<div className="bg-white w-full max-w-xl rounded-2xl shadow-2xl overflow-hidden border border-slate-200">
<header className="px-8 py-6 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
<h3 className="text-xl font-bold text-slate-900">Add New Fleet Vehicle</h3>
<button onClick={() => setShowAddModal(false)} className="text-slate-400 hover:text-slate-600">
<X className="w-6 h-6" />
</button>
</header>
<form onSubmit={handleAddVehicle} className="p-8 space-y-6">
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Vehicle Name</label>
<input
required
className="w-full px-4 py-2.5 rounded-xl border border-slate-200 outline-none"
placeholder="e.g. Audi A8 L"
onChange={e => setNewCar({...newCar, name: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Category</label>
<select
className="w-full px-4 py-2.5 rounded-xl border border-slate-200 outline-none"
onChange={e => setNewCar({...newCar, type: e.target.value})}
>
<option value="LUXURY">Luxury</option>
<option value="SUV">SUV</option>
<option value="MUV">MUV</option>
<option value="SEDAN">Sedan</option>
<option value="VAN">Van</option>
</select>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Seats</label>
<input
className="w-full px-4 py-2.5 rounded-xl border border-slate-200 outline-none"
placeholder="e.g. 4+1"
onChange={e => setNewCar({...newCar, seats: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">Base Fare</label>
<input
type="number"
className="w-full px-4 py-2.5 rounded-xl border border-slate-200 outline-none"
placeholder="₹ per day"
onChange={e => setNewCar({...newCar, baseFarePerDay: parseInt(e.target.value)})}
/>
</div>
</div>
<button className="w-full bg-indigo-600 hover:bg-fuchsia-600 text-white font-bold py-4 rounded-xl shadow-lg transition-all">
Confirm & Add to Fleet
</button>
</form>
</div>
</div>
)}
</div>
);
};
export default AdminDashboard;

View file

@ -0,0 +1,109 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Lock, User, ShieldCheck, AlertCircle } from 'lucide-react';
const AdminLogin = () => {
const [adminId, setAdminId] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
const response = await fetch('/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ adminId, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('adminToken', data.token);
navigate('/admin/dashboard');
} else {
setError(data.detail || 'Invalid credentials');
}
} catch (err) {
setError('Connection failed. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8 border border-slate-100">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-indigo-600 rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-indigo-200">
<ShieldCheck className="w-8 h-8 text-white" />
</div>
<h1 className="text-24 font-bold text-slate-900">Admin Portal</h1>
<p className="text-slate-500 mt-2">Secure access for road.rentals owner</p>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-100 rounded-xl flex items-center gap-3 text-red-600 text-sm">
<AlertCircle className="w-5 h-5 flex-shrink-0" />
{error}
</div>
)}
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">Admin ID</label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="text"
value={adminId}
onChange={(e) => setAdminId(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-50/50 outline-none transition-all"
placeholder="Enter your Admin ID"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">Password</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-50/50 outline-none transition-all"
placeholder="••••••••"
required
/>
</div>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-4 rounded-xl transition-all shadow-lg shadow-indigo-200 flex items-center justify-center gap-2"
>
{isLoading ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
'Sign In to Dashboard'
)}
</button>
</form>
<p className="text-center text-slate-400 text-xs mt-8 font-medium uppercase tracking-wider">
Road.Rentals Official Admin System
</p>
</div>
</div>
);
};
export default AdminLogin;