first commit
This commit is contained in:
parent
e7365b06ac
commit
a9e21cbe3c
9 changed files with 874 additions and 320 deletions
4
.env
4
.env
|
|
@ -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
|
||||||
305
backend/index.js
305
backend/index.js
|
|
@ -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
235
scripts/migrate_vehicles.js
Normal 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();
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
316
src/pages/Admin/AdminDashboard.jsx
Normal file
316
src/pages/Admin/AdminDashboard.jsx
Normal 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;
|
||||||
109
src/pages/Admin/AdminLogin.jsx
Normal file
109
src/pages/Admin/AdminLogin.jsx
Normal 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;
|
||||||
|
|
@ -298,14 +298,14 @@ const SearchPage = ({ onLoginClick, user }) => {
|
||||||
<div className="flex items-center gap-4 mt-1.5">
|
<div className="flex items-center gap-4 mt-1.5">
|
||||||
<span className="text-[10px] font-black text-primary-600 bg-primary-50 px-2.5 py-1 rounded-lg uppercase tracking-widest">{car.type}</span>
|
<span className="text-[10px] font-black text-primary-600 bg-primary-50 px-2.5 py-1 rounded-lg uppercase tracking-widest">{car.type}</span>
|
||||||
<div className="flex items-center gap-3 text-gray-400">
|
<div className="flex items-center gap-3 text-gray-400">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Users className="w-3.5 h-3.5" />
|
<Users className="w-3.5 h-3.5" />
|
||||||
<span className="text-[11px] font-bold">{car.seats}</span>
|
<span className="text-[11px] font-bold">{car.seats}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Luggage className="w-3.5 h-3.5" />
|
<Luggage className="w-3.5 h-3.5" />
|
||||||
<span className="text-[11px] font-bold">{car.luggage}</span>
|
<span className="text-[11px] font-bold">{car.luggage}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -318,15 +318,15 @@ const SearchPage = ({ onLoginClick, user }) => {
|
||||||
{/* Transparency Math Block */}
|
{/* Transparency Math Block */}
|
||||||
<div className="bg-primary-50/50 rounded-2xl p-4 mb-6 border border-primary-100/30">
|
<div className="bg-primary-50/50 rounded-2xl p-4 mb-6 border border-primary-100/30">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-[10px] font-black text-primary-900 uppercase tracking-widest flex items-center gap-1.5">
|
<span className="text-[10px] font-black text-primary-900 uppercase tracking-widest flex items-center gap-1.5">
|
||||||
<Info className="w-3.5 h-3.5" />
|
<Info className="w-3.5 h-3.5" />
|
||||||
Transparent Breakdown
|
Transparent Breakdown
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] font-black text-primary-600">{car.calc.math}</span>
|
<span className="text-[11px] font-black text-primary-600">{car.calc.math}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest font-mono">20% Booking Advance</span>
|
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest font-mono">20% Booking Advance</span>
|
||||||
<span className="text-sm font-black text-primary-600">Pay ₹{car.calc.advancePayment} to Confirm</span>
|
<span className="text-sm font-black text-primary-600">Pay ₹{car.calc.advancePayment} to Confirm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -334,7 +334,7 @@ const SearchPage = ({ onLoginClick, user }) => {
|
||||||
<div className="grid grid-cols-2 gap-4 pb-6 border-b border-gray-50">
|
<div className="grid grid-cols-2 gap-4 pb-6 border-b border-gray-50">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] font-black text-green-600 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
<p className="text-[10px] font-black text-green-600 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
||||||
<CheckCircle2 className="w-3 h-3" /> Included
|
<CheckCircle2 className="w-3 h-3" /> Included
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
{car.inclusions.map((inc, i) => (
|
{car.inclusions.map((inc, i) => (
|
||||||
|
|
@ -346,7 +346,7 @@ const SearchPage = ({ onLoginClick, user }) => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] font-black text-red-500 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
<p className="text-[10px] font-black text-red-500 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
||||||
<X className="w-3 h-3" /> Excluded
|
<X className="w-3 h-3" /> Excluded
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
{car.exclusions.map((exc, i) => (
|
{car.exclusions.map((exc, i) => (
|
||||||
|
|
@ -385,61 +385,61 @@ const SearchPage = ({ onLoginClick, user }) => {
|
||||||
{/* Pricing Summary Sidebar */}
|
{/* Pricing Summary Sidebar */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="sticky top-28 space-y-6">
|
<div className="sticky top-28 space-y-6">
|
||||||
<div className="bg-white rounded-3xl shadow-sm border border-gray-100 p-8 overflow-hidden relative group">
|
<div className="bg-white rounded-3xl shadow-sm border border-gray-100 p-8 overflow-hidden relative group">
|
||||||
{/* Decorative element */}
|
{/* Decorative element */}
|
||||||
<div className="absolute -top-10 -right-10 w-32 h-32 bg-primary-50 rounded-full opacity-50 blur-3xl group-hover:bg-primary-100 transition-colors duration-500" />
|
<div className="absolute -top-10 -right-10 w-32 h-32 bg-primary-50 rounded-full opacity-50 blur-3xl group-hover:bg-primary-100 transition-colors duration-500" />
|
||||||
|
|
||||||
<h2 className="text-xl font-black text-dark-900 mb-8 flex items-center gap-3 relative z-10">
|
<h2 className="text-xl font-black text-dark-900 mb-8 flex items-center gap-3 relative z-10">
|
||||||
Terms & Policy
|
Terms & Policy
|
||||||
< ShieldCheck className="w-5 h-5 text-green-500" />
|
< ShieldCheck className="w-5 h-5 text-green-500" />
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-5 relative z-10">
|
<div className="space-y-5 relative z-10">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="mt-1 w-5 h-5 bg-green-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
<div className="mt-1 w-5 h-5 bg-green-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
<CheckCircle2 className="w-3 h-3 text-green-600" />
|
<CheckCircle2 className="w-3 h-3 text-green-600" />
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-black text-dark-900 mb-0.5">Toll & State Permits</p>
|
|
||||||
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">To be paid by traveller as per actuals during the trip.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3">
|
<div>
|
||||||
<div className="mt-1 w-5 h-5 bg-primary-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
<p className="text-xs font-black text-dark-900 mb-0.5">Toll & State Permits</p>
|
||||||
<Info className="w-3 h-3 text-primary-600" />
|
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">To be paid by traveller as per actuals during the trip.</p>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-black text-dark-900 mb-0.5">Extra Kilimoteres</p>
|
|
||||||
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">Charged at standard base rates mentioned on the car cards.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="mt-1 w-5 h-5 bg-orange-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
||||||
<Calendar className="w-3 h-3 text-orange-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-black text-dark-900 mb-0.5">Night Charge</p>
|
|
||||||
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">Driver night allowance applicable after 10:00 PM.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
<div className="mt-10 p-5 bg-gray-50 rounded-2xl border border-gray-100 text-center">
|
<div className="mt-1 w-5 h-5 bg-primary-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1.5">Need Custom Help?</p>
|
<Info className="w-3 h-3 text-primary-600" />
|
||||||
<a href="tel:+917975942520" className="text-sm font-black text-dark-900 hover:text-primary-600 transition-colors">+91 7975942520</a>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-black text-dark-900 mb-0.5">Extra Kilimoteres</p>
|
||||||
|
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">Charged at standard base rates mentioned on the car cards.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="mt-1 w-5 h-5 bg-orange-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||||
|
<Calendar className="w-3 h-3 text-orange-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-black text-dark-900 mb-0.5">Night Charge</p>
|
||||||
|
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">Driver night allowance applicable after 10:00 PM.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-dark-900 rounded-3xl p-8 text-white relative overflow-hidden shadow-2xl">
|
<div className="mt-10 p-5 bg-gray-50 rounded-2xl border border-gray-100 text-center">
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-primary-600/20 rounded-full blur-3xl" />
|
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1.5">Need Custom Help?</p>
|
||||||
<h3 className="text-lg font-black mb-4 flex items-center gap-2">
|
<a href="tel:+917975942520" className="text-sm font-black text-dark-900 hover:text-primary-600 transition-colors">+91 7975942520</a>
|
||||||
<ShieldCheck className="w-5 h-5 text-primary-400" />
|
</div>
|
||||||
Refund Guarantee
|
</div>
|
||||||
</h3>
|
|
||||||
<p className="text-xs font-bold text-gray-400 leading-relaxed">
|
<div className="bg-dark-900 rounded-3xl p-8 text-white relative overflow-hidden shadow-2xl">
|
||||||
Cancel up to 24 hours in advance and receive a full refund of your 20% booking deposit.
|
<div className="absolute top-0 right-0 w-32 h-32 bg-primary-600/20 rounded-full blur-3xl" />
|
||||||
</p>
|
<h3 className="text-lg font-black mb-4 flex items-center gap-2">
|
||||||
</div>
|
<ShieldCheck className="w-5 h-5 text-primary-400" />
|
||||||
|
Refund Guarantee
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs font-bold text-gray-400 leading-relaxed">
|
||||||
|
Cancel up to 24 hours in advance and receive a full refund of your 20% booking deposit.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue