#include <WiFi.
h>
#include <WebServer.h>
#include <ESP32Servo.h>
#include <ArduinoJson.h>
#include <SPIFFS.h>
#include <vector>
#include <string>
// WiFi credentials
const char* ssid = "A";
const char* password = "55667799";
// Create WebServer object on port 80
WebServer server(80);
// Servo motors
Servo entryServo;
Servo exitServo;
const int carEnterPin = 0; // G3 - Entry IR Sensor
const int carExitPin = 4; // G4 - Exit IR Sensor
const int slot1Pin = 5; // G15 - Slot 1 IR Sensor
const int slot2Pin = 18; // G19 - Slot 2 IR Sensor
const int slot3Pin = 19; // G22 - Slot 3 IR Sensor
const int slot4Pin = 21; // G21 - Slot 4 IR Sensor
const int slot5Pin = 22; // G13 - Slot 5 IR Sensor
const int slot6Pin = 23; // G12 - Slot 6 IR Sensor
const int entryServoPin = 13; // G23 - Tower Pro SG90 Entry Servo
const int exitServoPin = 12; // G34 - Tower Pro SG90 Exit Servo
const int buzzerPin = 14; // G14 - Status/Alert Buzzer
const int ledPin = 15; // G18 - System Status LED
// Constants - Optimized for Tower Pro SG90 Servo Motors
const int CLOSE_ANGLE = 0; // 0 degrees for closed position
const int OPEN_ANGLE = 90; // 90 degrees for open position
const unsigned long SENSOR_DEBOUNCE = 200;
const unsigned long GATE_OPEN_DURATION = 4000; // 4 seconds gate open time
const int TOTAL_SLOTS = 6;
const int PRICE_PER_HOUR = 50;
// SG90 Servo specifications
const int SERVO_MIN_PULSE = 500; // Minimum pulse width in microseconds
const int SERVO_MAX_PULSE = 2500; // Maximum pulse width in microseconds
// Gate status variables
bool entryGateOpen = false;
bool exitGateOpen = false;
bool allSlotsFull = false;
unsigned long entryGateOpenTime = 0;
unsigned long exitGateOpenTime = 0;
// Updated Slot status structure with better timing management
struct ParkingSlot {
bool occupied;
bool reserved;
unsigned long occupiedTime;
unsigned long totalTime;
bool lastState;
String vehicleNumber;
String phoneNumber;
unsigned long reservedDuration; // in seconds
unsigned long reservationTime; // when the reservation was made
unsigned long expectedArrival; // expected arrival timestamp
};
ParkingSlot slots[TOTAL_SLOTS] = {
{false, false, 0, 0, false, "", "", 0, 0, 0},
{false, false, 0, 0, false, "", "", 0, 0, 0},
{false, false, 0, 0, false, "", "", 0, 0, 0},
{false, false, 0, 0, false, "", "", 0, 0, 0},
{false, false, 0, 0, false, "", "", 0, 0, 0},
{false, false, 0, 0, false, "", "", 0, 0, 0}
};
// Sensor pins array for easy iteration
const int slotPins[TOTAL_SLOTS] = {slot1Pin, slot2Pin, slot3Pin, slot4Pin,
slot5Pin, slot6Pin};
// Previous sensor states for edge detection
bool lastEntryState = false;
bool lastExitState = false;
// Debouncing variables
unsigned long lastSensorRead = 0;
unsigned long lastDataUpdate = 0;
unsigned long lastLedBlink = 0;
bool ledState = false;
// Statistics
int totalCarsToday = 0;
float averageParkingTime = 0;
unsigned long systemStartTime = 0;
unsigned long totalRevenue = 0;
std::vector<std::string> recentActivity;
// Own feature: Simulated users, but for simplicity, assume single user
String currentUserPhone = "";
void setup() {
Serial.begin(115200);
delay(1000);
// Initialize SPIFFS
if(!SPIFFS.begin(true)){
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
// Initialize pins
pinMode(carEnterPin, INPUT_PULLUP);
pinMode(carExitPin, INPUT_PULLUP);
pinMode(buzzerPin, OUTPUT);
pinMode(ledPin, OUTPUT);
for(int i = 0; i < TOTAL_SLOTS; i++) {
pinMode(slotPins[i], INPUT_PULLUP);
}
// Attach Tower Pro SG90 servos with proper pulse width settings
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
entryServo.setPeriodHertz(50); // Standard 50Hz for SG90
exitServo.setPeriodHertz(50); // Standard 50Hz for SG90
entryServo.attach(entryServoPin, SERVO_MIN_PULSE, SERVO_MAX_PULSE);
exitServo.attach(exitServoPin, SERVO_MIN_PULSE, SERVO_MAX_PULSE);
// Initialize servos to closed position
entryServo.write(CLOSE_ANGLE);
exitServo.write(CLOSE_ANGLE);
// Connect to WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
// Blink LED while connecting
digitalWrite(ledPin, !digitalRead(ledPin));
}
Serial.println();
Serial.print("Connected to WiFi. IP Address: ");
Serial.println(WiFi.localIP());
// Initialize web server routes
setupWebServer();
// Start server
server.begin();
Serial.println("Web server started");
systemStartTime = millis();
// Initialize previous states
delay(500);
lastEntryState = !digitalRead(carEnterPin);
lastExitState = !digitalRead(carExitPin);
for(int i = 0; i < TOTAL_SLOTS; i++) {
slots[i].lastState = !digitalRead(slotPins[i]);
}
// System ready - LED solid ON, welcome beep
digitalWrite(ledPin, HIGH);
playWelcomeBeep();
Serial.println("Smart Parking System with 6 slots initialized");
Serial.println("Hardware: 38-pin ESP32 with Tower Pro SG90 servos");
Serial.println("Pin Configuration:");
Serial.println(" Entry IR: G2, Exit IR: G4");
Serial.println(" Slots IR: G5, G18, G19, G21, G22, G23");
Serial.println(" Entry Servo: G13, Exit Servo: G12");
Serial.println(" Buzzer: G14 (Alerts/Status), LED: G15 (System Status)");
Serial.println("LED Usage: Solid=Ready, Blink=Activity, Fast Blink=Full
Parking");
Serial.println("Buzzer Usage: 1 beep=Car detected, 2 beeps=Gate open, 3
beeps=Parking full");
Serial.println("Access web interface at: http://" + WiFi.localIP().toString());
}
void loop() {
server.handleClient();
// Check sensors with debouncing
if (millis() - lastSensorRead > SENSOR_DEBOUNCE) {
checkSensors();
updateParkingTimes();
lastSensorRead = millis();
}
// Update statistics every 5 seconds
if (millis() - lastDataUpdate > 5000) {
updateStatistics();
lastDataUpdate = millis();
}
// Handle LED status indication
updateLedStatus();
// Auto-close gates after timeout
handleGateTimeouts();
}
// Updated checkSensors function with better timing logic
void checkSensors() {
// Read sensors (inverted for IR sensors)
bool entryState = !digitalRead(carEnterPin);
bool exitState = !digitalRead(carExitPin);
// Check current time
unsigned long currentTime = millis();
// Check if all slots are full (considering reservations)
allSlotsFull = true;
for(int i = 0; i < TOTAL_SLOTS; i++) {
if(!slots[i].occupied && !slots[i].reserved) {
allSlotsFull = false;
break;
}
}
// Handle entry gate
if (entryState && !entryGateOpen && !allSlotsFull) {
Serial.println("Car at entry - Opening gate");
entryServo.write(OPEN_ANGLE);
entryGateOpen = true;
entryGateOpenTime = millis();
playBeep(2);
} else if (entryState && allSlotsFull && !entryGateOpen) {
playBeep(3);
Serial.println("Parking Full - Entry denied");
}
// Handle exit gate
if (exitState && !exitGateOpen) {
Serial.println("Car at exit - Opening gate");
exitServo.write(OPEN_ANGLE);
exitGateOpen = true;
exitGateOpenTime = millis();
playBeep(2);
}
// Handle parking slots with reservation timing
for(int i = 0; i < TOTAL_SLOTS; i++) {
bool currentState = !digitalRead(slotPins[i]);
if (currentState != slots[i].lastState) {
if (currentState && !slots[i].occupied) {
// Car entered slot
slots[i].occupied = true;
slots[i].occupiedTime = currentTime;
if (slots[i].reserved) {
Serial.println("Slot " + String(i+1) + ": OCCUPIED (Reserved booking
activated)");
// Start the timer for reserved duration
} else {
Serial.println("Slot " + String(i+1) + ": OCCUPIED (Walk-in customer)");
}
playBeep(1);
recentActivity.push_back(std::string("Slot ") + std::to_string(i+1) + "
occupied");
if (recentActivity.size() > 5)
recentActivity.erase(recentActivity.begin());
} else if (!currentState && slots[i].occupied) {
// Car left slot
unsigned long parkedTime = currentTime - slots[i].occupiedTime;
if (slots[i].reserved && slots[i].reservedDuration > 0) {
// Check if parking time exceeded reserved duration
unsigned long reservedTimeMs = slots[i].reservedDuration * 1000;
if (parkedTime > reservedTimeMs) {
// Calculate penalty for overstaying
unsigned long overTime = parkedTime - reservedTimeMs;
float penalty = (overTime / 60000.0) * 10; // ₹10 per minute penalty
totalRevenue += penalty;
Serial.println("Slot " + String(i+1) + ": VACATED (Overstayed by " +
formatTime(overTime / 1000) + ", Penalty: ₹" +
String(penalty) + ")");
} else {
Serial.println("Slot " + String(i+1) + ": VACATED (On time)");
}
}
slots[i].occupied = false;
slots[i].reserved = false; // Release reservation
slots[i].reservedDuration = 0;
slots[i].reservationTime = 0;
slots[i].expectedArrival = 0;
slots[i].vehicleNumber = "";
slots[i].phoneNumber = "";
if(slots[i].occupiedTime > 0) {
slots[i].totalTime += parkedTime;
if (!slots[i].reserved) {
// Add revenue for walk-in customer
totalRevenue += (parkedTime / 3600000.0) * PRICE_PER_HOUR;
}
}
Serial.println("Slot " + String(i+1) + ": AVAILABLE (Parked for " +
formatTime(parkedTime / 1000) + ")");
playBeep(1);
recentActivity.push_back(std::string("Slot ") + std::to_string(i+1) + "
available");
if (recentActivity.size() > 5)
recentActivity.erase(recentActivity.begin());
}
slots[i].lastState = currentState;
}
// Auto-release reservations that exceed expected arrival time by 30 minutes
if (slots[i].reserved && !slots[i].occupied && slots[i].expectedArrival > 0) {
if (currentTime > (slots[i].expectedArrival + (30 * 60 * 1000))) { // 30
minutes grace period
Serial.println("Slot " + String(i+1) + ": Reservation auto-cancelled (No-
show)");
slots[i].reserved = false;
slots[i].reservedDuration = 0;
slots[i].reservationTime = 0;
slots[i].expectedArrival = 0;
slots[i].vehicleNumber = "";
slots[i].phoneNumber = "";
// Partial refund (50% penalty for no-show)
totalRevenue -= (slots[i].reservedDuration / 3600) * PRICE_PER_HOUR * 0.5;
recentActivity.push_back(std::string("Slot ") + std::to_string(i+1) + "
reservation cancelled (no-show)");
if (recentActivity.size() > 5)
recentActivity.erase(recentActivity.begin());
}
}
}
}
void handleGateTimeouts() {
// Auto-close entry gate after timeout
if (entryGateOpen && (millis() - entryGateOpenTime > GATE_OPEN_DURATION)) {
bool entryState = !digitalRead(carEnterPin);
if (!entryState) { // No car detected at entry
Serial.println("Entry gate auto-closing");
entryServo.write(CLOSE_ANGLE);
entryGateOpen = false;
totalCarsToday++;
} else {
// Extend timeout if car still present
entryGateOpenTime = millis();
}
}
// Auto-close exit gate after timeout
if (exitGateOpen && (millis() - exitGateOpenTime > GATE_OPEN_DURATION)) {
bool exitState = !digitalRead(carExitPin);
if (!exitState) { // No car detected at exit
Serial.println("Exit gate auto-closing");
exitServo.write(CLOSE_ANGLE);
exitGateOpen = false;
} else {
// Extend timeout if car still present
exitGateOpenTime = millis();
}
}
}
void updateLedStatus() {
unsigned long currentTime = millis();
if (allSlotsFull) {
// Fast blink when parking is full
if (currentTime - lastLedBlink > 250) {
ledState = !ledState;
digitalWrite(ledPin, ledState);
lastLedBlink = currentTime;
}
} else if (entryGateOpen || exitGateOpen) {
// Slow blink when gates are active
if (currentTime - lastLedBlink > 500) {
ledState = !ledState;
digitalWrite(ledPin, ledState);
lastLedBlink = currentTime;
}
} else {
// Solid ON when system is ready and available
digitalWrite(ledPin, HIGH);
ledState = true;
}
}
void updateParkingTimes() {
for(int i = 0; i < TOTAL_SLOTS; i++) {
if(slots[i].occupied && slots[i].occupiedTime > 0) {
// Update current parking time for occupied slots
}
}
}
void updateStatistics() {
// Calculate average parking time
unsigned long totalTimeSum = 0;
int completedParkings = 0;
for(int i = 0; i < TOTAL_SLOTS; i++) {
if(slots[i].totalTime > 0) {
totalTimeSum += slots[i].totalTime;
completedParkings++;
}
}
if(completedParkings > 0) {
averageParkingTime = (float)totalTimeSum / completedParkings / 1000.0 /
60.0; // in minutes
}
}
void playBeep(int times) {
for(int i = 0; i < times; i++) {
digitalWrite(buzzerPin, HIGH);
delay(100);
digitalWrite(buzzerPin, LOW);
delay(100);
}
}
void playWelcomeBeep() {
// Special startup sequence
for(int i = 0; i < 3; i++) {
digitalWrite(buzzerPin, HIGH);
delay(150);
digitalWrite(buzzerPin, LOW);
delay(50);
}
}
String formatTime(unsigned long seconds) {
unsigned long hours = seconds / 3600;
unsigned long minutes = (seconds % 3600) / 60;
unsigned long secs = seconds % 60;
String timeStr = "";
if(hours > 0) timeStr += String(hours) + "h ";
if(minutes > 0) timeStr += String(minutes) + "m ";
timeStr += String(secs) + "s";
return timeStr;
}
String getCurrentTime() {
unsigned long uptime = (millis() - systemStartTime) / 1000;
unsigned long hours = uptime / 3600;
unsigned long minutes = (uptime % 3600) / 60;
unsigned long secs = uptime % 60;
return String(hours) + ":" + (minutes < 10 ? "0" : "") + String(minutes) + ":" +
(secs < 10 ? "0" : "") + String(secs) + " PM"; // Simulate time
}
void setupWebServer() {
// Serve main page
server.on("/", handleRoot);
// API endpoints
server.on("/api/status", handleStatus);
server.on("/api/reset", HTTP_POST, handleReset);
server.on("/api/book", HTTP_POST, handleBook);
server.on("/api/report", HTTP_POST, handleReport);
server.on("/api/cancel", HTTP_POST, handleCancel);
// Serve static files
server.onNotFound(handleNotFound);
}
void handleRoot() {
String html = generateWebPage();
server.send(200, "text/html", html);
}
// Updated handleStatus function to include timing information
void handleStatus() {
DynamicJsonDocument doc(1024);
// System status
doc["timestamp"] = millis();
doc["totalSlots"] = TOTAL_SLOTS;
int physicalOccupied = 0;
int reserved = 0;
for(int i = 0; i < TOTAL_SLOTS; i++) {
if(slots[i].occupied) physicalOccupied++;
if(slots[i].reserved) reserved++;
}
doc["occupiedSlots"] = physicalOccupied;
doc["reservedSlots"] = reserved;
doc["availableSlots"] = TOTAL_SLOTS - physicalOccupied - reserved;
doc["allSlotsFull"] = allSlotsFull;
doc["averageParkingTime"] = averageParkingTime;
doc["revenue"] = totalRevenue;
doc["occupancyRate"] = ((physicalOccupied + reserved) * 100) / TOTAL_SLOTS;
doc["lastUpdated"] = getCurrentTime();
// Slot details with enhanced timing
JsonArray slotsArray = doc.createNestedArray("slots");
for(int i = 0; i < TOTAL_SLOTS; i++) {
JsonObject slot = slotsArray.createNestedObject();
slot["id"] = i + 1;
slot["occupied"] = slots[i].occupied;
slot["reserved"] = slots[i].reserved;
slot["reservedDuration"] = slots[i].reservedDuration; // in seconds
slot["reservationTime"] = slots[i].reservationTime;
slot["expectedArrival"] = slots[i].expectedArrival;
if(slots[i].occupied && slots[i].occupiedTime > 0) {
slot["currentTime"] = (millis() - slots[i].occupiedTime) / 1000;
slot["occupiedTimestamp"] = slots[i].occupiedTime;
} else {
slot["currentTime"] = 0;
slot["occupiedTimestamp"] = 0;
}
slot["totalTime"] = slots[i].totalTime / 1000;
slot["vehicleNumber"] = slots[i].vehicleNumber;
}
// Recent activity
JsonArray activityArray = doc.createNestedArray("recentActivity");
for(auto& act : recentActivity) {
activityArray.add(act);
}
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
void handleReset() {
// Reset statistics
totalCarsToday = 0;
averageParkingTime = 0;
totalRevenue = 0;
systemStartTime = millis();
for(int i = 0; i < TOTAL_SLOTS; i++) {
slots[i].totalTime = 0;
}
recentActivity.clear();
server.send(200, "text/plain", "Statistics reset");
Serial.println("Statistics reset via web interface");
}
// Updated handleBook function with arrival time parsing
void handleBook() {
if(server.hasArg("slot") && server.hasArg("duration") && server.hasArg("vehicle")
&& server.hasArg("phone") && server.hasArg("arrival")) {
int slotId = server.arg("slot").toInt() - 1;
unsigned long duration = server.arg("duration").toInt() * 3600; // hours to
seconds
String vehicle = server.arg("vehicle");
String phone = server.arg("phone");
String arrivalStr = server.arg("arrival");
if(slotId >= 0 && slotId < TOTAL_SLOTS && !slots[slotId].occupied && !
slots[slotId].reserved) {
slots[slotId].reserved = true;
slots[slotId].reservedDuration = duration;
slots[slotId].vehicleNumber = vehicle;
slots[slotId].phoneNumber = phone;
slots[slotId].reservationTime = millis();
// Parse arrival time (simplified - in real implementation, parse ISO string)
// For now, set expected arrival to current time + 15 minutes as example
slots[slotId].expectedArrival = millis() + (15 * 60 * 1000);
totalRevenue += (duration / 3600) * PRICE_PER_HOUR; // Prepaid
recentActivity.push_back(vehicle.c_str() + (std::string(" booked slot ") +
std::to_string(slotId + 1)));
if (recentActivity.size() > 5) recentActivity.erase(recentActivity.begin());
server.send(200, "text/plain", "Slot reserved successfully");
Serial.println("Slot " + String(slotId + 1) + " reserved for " + vehicle + "
(" + String(duration/3600) + "h)");
} else {
server.send(400, "text/plain", "Cannot reserve this slot");
}
} else {
server.send(400, "text/plain", "Missing parameters");
}
}
void handleCancel() {
if(server.hasArg("slot")) {
int slotId = server.arg("slot").toInt() - 1;
if(slotId >= 0 && slotId < TOTAL_SLOTS && slots[slotId].reserved && !
slots[slotId].occupied) {
totalRevenue -= (slots[slotId].reservedDuration / 3600) * PRICE_PER_HOUR; //
Refund simulation
slots[slotId].reserved = false;
slots[slotId].reservedDuration = 0;
slots[slotId].reservationTime = 0;
slots[slotId].expectedArrival = 0;
slots[slotId].vehicleNumber = "";
slots[slotId].phoneNumber = "";
recentActivity.push_back(std::string("Slot ") + std::to_string(slotId + 1) +
" reservation cancelled");
if (recentActivity.size() > 5) recentActivity.erase(recentActivity.begin());
server.send(200, "text/plain", "Reservation cancelled");
} else {
server.send(400, "text/plain", "Cannot cancel");
}
} else {
server.send(400, "text/plain", "Missing slot");
}
}
void handleReport() {
if(server.hasArg("message")) {
String message = server.arg("message");
Serial.println("User Report: " + message);
server.send(200, "text/plain", "Message sent");
} else {
server.send(400, "text/plain", "Missing message");
}
}
void handleNotFound() {
server.send(404, "text/plain", "404: Not Found");
}
String generateWebPage() {
String html = "<!DOCTYPE html><html><head>";
html += "<meta charset='utf-8'><meta name='viewport' content='width=device-width,
initial-scale=1'>";
html += "<title>Parking Dashboard</title>";
html += "<style>";
html += "* { box-sizing: border-box; margin: 0; padding: 0; }";
html += "body { background: #1a1d28; color: #fff; font-family: 'Segoe UI', sans-
serif; padding: 20px; }";
html += ".header { display: flex; justify-content: space-between; align-items:
center; margin-bottom: 20px; }";
html += ".header h1 { font-size: 1.5em; }";
html += ".header p { opacity: 0.7; font-size: 0.9em; }";
html += ".update-info { font-size: 0.8em; opacity: 0.7; }";
html += ".refresh { background: none; border: none; color: #fff; cursor: pointer;
}";
html += ".stats { display: grid; grid-template-columns: repeat(4, 1fr); gap:
10px; margin-bottom: 20px; }";
html += ".stat-card { background: #2a2f3d; padding: 10px; border-radius: 8px;
text-align: left; }";
html += ".stat-card .icon { font-size: 1.5em; margin-bottom: 5px; }";
html += ".stat-card .value { font-size: 1.2em; font-weight: bold; }";
html += ".stat-card .change { font-size: 0.8em; color: #28a745; }";
html += ".legend { display: flex; align-items: center; gap: 10px; margin-bottom:
10px; font-size: 0.9em; }";
html += ".dot { width: 10px; height: 10px; border-radius: 50%; }";
html += ".green { background: #28a745; } .red { background: #dc3545; } .orange
{ background: #ffc107; }";
html += ".parking-section { margin-bottom: 10px; }";
html += ".parking-section h3 { font-size: 1em; }";
html += ".slots-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap:
10px; margin-bottom: 20px; }";
html += ".slot-card { background: #2a2f3d; padding: 10px; border-radius: 8px;
text-align: center; position: relative; cursor: pointer; transition: all 0.3s ease;
}";
html += ".slot-card:hover { transform: translateY(-2px); }";
html += ".slot-card.available:hover { box-shadow: 0 4px 8px rgba(40, 167, 69,
0.3); }";
html += ".slot-card .dot { position: absolute; top: 10px; left: 10px; }";
html += ".slot-card .name { font-weight: bold; margin-bottom: 5px; }";
html += ".slot-card .status { font-size: 0.8em; opacity: 0.8; margin-bottom: 3px;
}";
html += ".slot-card .timer { font-size: 0.7em; color: #ffc107; margin-bottom:
3px; }";
html += ".slot-card .price { font-size: 0.8em; color: #28a745; }";
html += ".recent-activity { background: #2a2f3d; padding: 10px; border-radius:
8px; margin-bottom: 20px; }";
html += ".recent-activity h3 { font-size: 1em; margin-bottom: 10px; }";
html += ".activity-item { display: flex; justify-content: space-between; font-
size: 0.8em; opacity: 0.8; margin-bottom: 5px; }";
html += ".quick-actions { background: #2a2f3d; padding: 10px; border-radius: 8px;
}";
html += ".quick-actions h3 { font-size: 1em; margin-bottom: 10px; }";
html += ".action-btn { background: #3a3f4d; color: #fff; border: none; padding:
10px; border-radius: 5px; width: 100%; margin-bottom: 10px; cursor: pointer; text-
align: left; }";
html += ".modal { display: none; position: fixed; z-index: 1; left: 0; top: 0;
width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);
padding-top: 60px; }";
html += ".modal-content { background-color: #2a2f3d; margin: 5% auto; padding:
20px; border-radius: 8px; width: 80%; max-width: 500px; }";
html += ".modal-header { display: flex; align-items: center; margin-bottom: 20px;
}";
html += ".modal-header .back-btn { background: none; border: none; color: #fff;
font-size: 1.5em; cursor: pointer; margin-right: 10px; }";
html += ".modal-header h2 { flex: 1; text-align: center; }";
html += ".steps { display: flex; justify-content: center; align-items: center;
gap: 10px; margin-bottom: 20px; }";
html += ".step { width: 30px; height: 30px; border-radius: 50%; background:
#6c757d; text-align: center; line-height: 30px; color: #fff; font-weight: bold; }";
html += ".step.active { background: #007bff; }";
html += ".step.completed { background: #28a745; }";
html += ".step-line { width: 40px; height: 2px; background: #6c757d; }";
html += ".step-line.completed { background: #28a745; }";
html += ".step-content { display: none; }";
html += ".step-content.active { display: block; }";
html += ".slot-info { background: #3a3f4d; padding: 15px; border-radius: 8px;
margin-bottom: 20px; display: flex; align-items: center; gap: 15px; }";
html += ".slot-info .icon { font-size: 2.5em; color: #28a745; }";
html += ".slot-info .details { flex: 1; }";
html += ".slot-info .status { background: #28a745; color: #fff; padding: 3px 8px;
border-radius: 3px; font-size: 0.8em; }";
html += ".form-group { margin-bottom: 15px; }";
html += ".form-group label { display: block; font-size: 0.9em; margin-bottom:
5px; color: #ccc; }";
html += ".form-group input, .form-group select { width: 100%; padding: 12px;
border: none; border-radius: 5px; background: #3a3f4d; color: #fff; font-size: 1em;
}";
html += ".form-group input:focus, .form-group select:focus { outline: 2px solid
#007bff; }";
html += ".prediction-box { background: #3a3f4d; padding: 12px; border-radius:
8px; margin-top: 10px; }";
html += ".prediction-box .label { font-size: 0.8em; color: #ccc; margin-bottom:
5px; }";
html += ".prediction-box .value { font-size: 1.1em; color: #28a745; font-weight:
bold; }";
html += ".payment-methods { display: grid; grid-template-columns: repeat(2, 1fr);
gap: 10px; margin-bottom: 20px; }";
html += ".payment-method { background: #3a3f4d; padding: 15px; border-radius:
8px; text-align: center; cursor: pointer; transition: all 0.3s ease; }";
html += ".payment-method:hover { background: #4a4f5d; }";
html += ".payment-method.active { background: #007bff; }";
html += ".total-amount { display: flex; justify-content: space-between; align-
items: center; background: #3a3f4d; padding: 15px; border-radius: 8px; margin-
bottom: 20px; }";
html += ".total-amount .value { color: #28a745; font-weight: bold; font-size:
1.2em; }";
html += ".buttons { display: flex; gap: 10px; }";
html += ".buttons button { flex: 1; padding: 12px; border: none; border-radius:
5px; cursor: pointer; font-size: 1em; }";
html += ".back-btn { background: #6c757d; color: #fff; }";
html += ".next-btn { background: #007bff; color: #fff; }";
html += ".pay-btn { background: linear-gradient(to right, #007bff, #6f42c1);
color: #fff; }";
html += ".my-bookings { background: #2a2f3d; padding: 10px; border-radius: 8px;
display: none; }";
html += ".my-bookings h3 { font-size: 1em; margin-bottom: 10px; }";
html += ".booking-item { background: #3a3f4d; padding: 10px; border-radius: 5px;
margin-bottom: 10px; display: flex; justify-content: space-between; align-items:
center; }";
html += ".cancel-btn { background: #dc3545; color: #fff; border: none; padding:
5px 10px; border-radius: 3px; cursor: pointer; }";
html += ".report-modal .modal-content { max-width: 400px; }";
html += ".report-modal textarea { width: 100%; height: 120px; padding: 12px;
border: none; border-radius: 5px; background: #3a3f4d; color: #fff; margin-bottom:
15px; resize: vertical; }";
html += ".report-modal textarea:focus { outline: 2px solid #007bff; }";
html += "@media (max-width: 768px) { .stats { grid-template-columns: repeat(2,
1fr); } .slots-grid { grid-template-columns: repeat(2, 1fr); } .payment-methods
{ grid-template-columns: 1fr; } }";
html += "</style></head><body>";
// Header
html += "<div class='header'>";
html += "<div>";
html += "<h1>Parking Dashboard</h1>";
html += "<p>Real-time parking management system</p>";
html += "</div>";
html += "<div class='update-info'>";
html += "Last updated: <span id='last-updated'></span> <button class='refresh'
onclick='updateData()'>⭮ Refresh</button>";
html += "</div>";
html += "</div>";
// Stats
html += "<div class='stats'>";
html += "<div class='stat-card'><div class='icon'>🚗</div><div class='value'
id='available-slots'>0/6</div>Available Slots<div class='change' id='available-
change'>+0 from last hour</div></div>";
html += "<div class='stat-card'><div class='icon'>👥</div><div class='value'
id='active-bookings'>0</div>Active Bookings<div class='change' id='bookings-
change'>0 ending soon</div></div>";
html += "<div class='stat-card'><div class='icon'>📈</div><div class='value'
id='occupancy-rate'>0%</div>Occupancy Rate<div class='change' id='occupancy-
change'>0% change today</div></div>";
html += "<div class='stat-card'><div class='icon'>💰</div><div class='value'
id='revenue'>₹0</div>Revenue Today<div class='change'
id='revenue-change'>+0%</div></div>";
html += "</div>";
// Legend
html += "<div class='legend'>";
html += "<span class='dot green'></span> Available <span class='dot red'></span>
Occupied <span class='dot orange'></span> Reserved <i>Live Updates</i>";
html += "</div>";
// Parking Slots (Removed booking button)
html += "<div class='parking-section'>";
html += "<h3> Parking Slots - Level 1 - Ground Floor</h3>";
html += "</div>";
html += "<div class='slots-grid'>";
for(int i = 0; i < TOTAL_SLOTS; i++) {
html += "<div id='slot" + String(i+1) + "' class='slot-card'>";
html += "<span class='dot'></span>";
html += "<div class='name'>A" + String(i+1) + "</div>";
html += "<div class='status'>Available</div>";
html += "<div class='timer' id='timer" + String(i+1) + "'></div>";
html += "<div class='price'>₹50/hr</div>";
html += "</div>";
}
html += "</div>";
// Recent Activity
html += "<div class='recent-activity'>";
html += "<h3>🔔 Recent Activity</h3>";
html += "<div id='activity-list'></div>";
html += "</div>";
// Quick Actions (Removed New Booking button)
html += "<div class='quick-actions'>";
html += "<h3>Quick Actions</h3>";
html += "<button class='action-btn' onclick='toggleMyBookings()'>📋 My
Bookings</button>";
html += "<button class='action-btn' onclick='openReportModal()'>📝 Report
Issue</button>";
html += "</div>";
// My Bookings
html += "<div class='my-bookings' id='my-bookings'>";
html += "<h3>My Bookings</h3>";
html += "<div id='bookings-list'></div>";
html += "</div>";
// 3-Step Booking Modal
html += "<div id='bookingModal' class='modal'>";
html += "<div class='modal-content'>";
html += "<div class='modal-header'>";
html += "<button class='back-btn' onclick='closeBookingModal()'>←</button>";
html += "<h2>Book Parking Slot</h2>";
html += "</div>";
// Steps indicator
html += "<div class='steps'>";
html += "<div class='step active' id='step1'>1</div>";
html += "<div class='step-line' id='line1'></div>";
html += "<div class='step' id='step2'>2</div>";
html += "<div class='step-line' id='line2'></div>";
html += "<div class='step' id='step3'>3</div>";
html += "</div>";
// Step 1: Slot Selection and Basic Details
html += "<div class='step-content active' id='step-content-1'>";
html += "<div class='slot-info'>";
html += "<div class='icon'>🚗</div>";
html += "<div class='details'>";
html += "<div class='name' id='selected-slot'>Select a Slot</div>";
html += "<div>Ground Floor - Section A</div>";
html += "<div class='price'>₹50/hour</div>";
html += "<div class='status'>Available</div>";
html += "</div>";
html += "</div>";
html += "<div class='form-group'>";
html += "<label>Parking Duration</label>";
html += "<select id='duration' onchange='updatePredictions()'>";
html += "<option value='1'>1 hour - ₹50</option>";
html += "<option value='2'>2 hours - ₹100</option>";
html += "<option value='3'>3 hours - ₹150</option>";
html += "<option value='4'>4 hours - ₹200</option>";
html += "<option value='6'>6 hours - ₹300</option>";
html += "<option value='8'>8 hours - ₹400</option>";
html += "</select>";
html += "</div>";
html += "<div class='form-group'>";
html += "<label>Expected Arrival Time</label>";
html += "<input type='datetime-local' id='arrival-time'
onchange='updatePredictions()'>";
html += "</div>";
html += "<div class='prediction-box'>";
html += "<div class='label'>Predicted Exit Time:</div>";
html += "<div class='value' id='predicted-exit'>--:--</div>";
html += "</div>";
html += "<div class='buttons'>";
html += "<button class='back-btn' onclick='closeBookingModal()'>Cancel</button>";
html += "<button class='next-btn' onclick='nextStep(1)'>Next</button>";
html += "</div>";
html += "</div>";
// Step 2: Vehicle Details
html += "<div class='step-content' id='step-content-2'>";
html += "<div class='form-group'>";
html += "<label>Vehicle Number</label>";
html += "<input type='text' id='vehicle-number' placeholder='MH01AB1234'
style='text-transform: uppercase;'>";
html += "</div>";
html += "<div class='form-group'>";
html += "<label>Phone Number</label>";
html += "<input type='text' id='phone-number' placeholder='+91 9876543210'>";
html += "</div>";
html += "<div class='buttons'>";
html += "<button class='back-btn' onclick='previousStep(2)'>Back</button>";
html += "<button class='next-btn' onclick='nextStep(2)'>Next</button>";
html += "</div>";
html += "</div>";
// Step 3: Payment
html += "<div class='step-content' id='step-content-3'>";
html += "<div class='form-group'>";
html += "<label>Payment Method</label>";
html += "<div class='payment-methods'>";
html += "<div class='payment-method active' data-method='upi'>📱 UPI
Payment</div>";
html += "<div class='payment-method' data-method='card'>💳 Credit/Debit
Card</div>";
html += "<div class='payment-method' data-method='net'>🏦 Net Banking</div>";
html += "<div class='payment-method' data-method='wallet'>👛 Digital
Wallet</div>";
html += "</div>";
html += "</div>";
html += "<div class='total-amount'>";
html += "<div>Total Amount: <span id='total-hours'>1 hour x
₹50/hour</span></div>";
html += "<div class='value' id='total-value'>₹50</div>";
html += "</div>";
html += "<div class='buttons'>";
html += "<button class='back-btn' onclick='previousStep(3)'>Back</button>";
html += "<button class='pay-btn' onclick='payAndBook()'>Pay <span id='pay-
amount'>₹50</span></button>";
html += "</div>";
html += "</div>";
html += "</div>";
html += "</div>";
// Report Modal with back button
html += "<div id='reportModal' class='modal report-modal'>";
html += "<div class='modal-content'>";
html += "<div class='modal-header'>";
html += "<button class='back-btn' onclick='closeReportModal()'>←</button>";
html += "<h2>Report Issue</h2>";
html += "</div>";
html += "<textarea id='reportMessage' placeholder='Describe the issue you are
facing...'></textarea>";
html += "<div class='buttons'>";
html += "<button class='back-btn' onclick='closeReportModal()'>Cancel</button>";
html += "<button class='pay-btn' onclick='sendReport()'>Send Report</button>";
html += "</div>";
html += "<p id='reportStatus' style='margin-top: 10px; text-align: center; color:
#28a745;'></p>";
html += "</div>";
html += "</div>";
html += "<script>";
html += "let selectedSlot = null; let selectedMethod = 'upi'; let totalAmount =
50; let currentStep = 1;";
// Step navigation functions
html += "function nextStep(step) {";
html += " if (step === 1 && (!selectedSlot || !document.getElementById('arrival-
time').value)) {";
html += " alert('Please select a slot and arrival time'); return;";
html += " }";
html += " if (step === 2 && (!document.getElementById('vehicle-number').value
|| !document.getElementById('phone-number').value)) {";
html += " alert('Please fill all vehicle details'); return;";
html += " }";
html += " document.getElementById('step-content-' +
step).classList.remove('active');";
html += " document.getElementById('step' + step).classList.remove('active');";
html += " document.getElementById('step' + step).classList.add('completed');";
html += " document.getElementById('line' + step).classList.add('completed');";
html += " currentStep = step + 1;";
html += " document.getElementById('step-content-' +
currentStep).classList.add('active');";
html += " document.getElementById('step' +
currentStep).classList.add('active');";
html += "}";
html += "function previousStep(step) {";
html += " document.getElementById('step-content-' +
step).classList.remove('active');";
html += " document.getElementById('step' + step).classList.remove('active');";
html += " currentStep = step - 1;";
html += " document.getElementById('step-content-' +
currentStep).classList.add('active');";
html += " document.getElementById('step' +
currentStep).classList.remove('completed');";
html += " document.getElementById('step' +
currentStep).classList.add('active');";
html += " if (currentStep > 1) document.getElementById('line' +
currentStep).classList.remove('completed');";
html += "}";
// Prediction function
html += "function updatePredictions() {";
html += " let arrivalTime = document.getElementById('arrival-time').value;";
html += " let duration = parseInt(document.getElementById('duration').value);";
html += " if (arrivalTime && duration) {";
html += " let arrival = new Date(arrivalTime);";
html += " let exit = new Date(arrival.getTime() + (duration * 60 * 60 * 1000));";
html += " document.getElementById('predicted-exit').innerText =
exit.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' });";
html += " totalAmount = duration * 50;";
html += " document.getElementById('total-hours').innerText = duration + ' hours x
₹50/hour';";
html += " document.getElementById('total-value').innerText = '₹' + totalAmount;";
html += " document.getElementById('pay-amount').innerText = '₹' + totalAmount;";
html += " }";
html += "}";
// Initialize arrival time to current time and set minimum date
html += "function initializeArrivalTime() {";
html += " let now = new Date();";
html += " now.setMinutes(now.getMinutes() - now.getTimezoneOffset());";
html += " let arrivalInput = document.getElementById('arrival-time');";
html += " arrivalInput.value = now.toISOString().slice(0, 16);";
html += " arrivalInput.min = now.toISOString().slice(0, 16);"; // Prevent past
dates
html += " updatePredictions();";
html += "}";
html += "function openBookingModal() {";
html += " currentStep = 1;";
html += " document.querySelectorAll('.step').forEach(s =>
s.classList.remove('active', 'completed'));";
html += " document.querySelectorAll('.step-line').forEach(l =>
l.classList.remove('completed'));";
html += " document.querySelectorAll('.step-content').forEach(sc =>
sc.classList.remove('active'));";
html += " document.getElementById('step1').classList.add('active');";
html += " document.getElementById('step-content-1').classList.add('active');";
html += " initializeArrivalTime();";
html += " document.getElementById('bookingModal').style.display = 'block';";
html += "}";
html += "function closeBookingModal() {";
html += " document.getElementById('bookingModal').style.display = 'none';";
html += " selectedSlot = null;";
html += " document.getElementById('selected-slot').innerText = 'Select a Slot';";
html += "}";
html += "function openReportModal()
{ document.getElementById('reportModal').style.display = 'block'; }";
html += "function closeReportModal()
{ document.getElementById('reportModal').style.display = 'none';
document.getElementById('reportMessage').value = '';
document.getElementById('reportStatus').innerText = ''; }";
html += "function toggleMyBookings() { document.getElementById('my-
bookings').style.display = document.getElementById('my-bookings').style.display ===
'block' ? 'none' : 'block'; }";
// Payment method selection
html += "document.querySelectorAll('.payment-method').forEach(m => {";
html += " m.addEventListener('click', () => {";
html += " document.querySelectorAll('.payment-method').forEach(mm =>
mm.classList.remove('active'));";
html += " m.classList.add('active');";
html += " selectedMethod = m.dataset.method;";
html += " });";
html += "});";
html += "function payAndBook() {";
html += " let duration = document.getElementById('duration').value;";
html += " let vehicle = document.getElementById('vehicle-
number').value.toUpperCase();";
html += " let phone = document.getElementById('phone-number').value;";
html += " let arrival = document.getElementById('arrival-time').value;";
html += " if (selectedSlot && duration && vehicle && phone && arrival) {";
html += " let arrivalDate = new Date(arrival);";
html += " if (arrivalDate < new Date()) {";
html += " alert('Arrival time cannot be in the past!'); return;";
html += " }";
html += " fetch('/api/book', { method: 'POST', headers: { 'Content-Type':
'application/x-www-form-urlencoded' }, body: 'slot=' + selectedSlot + '&duration='
+ duration + '&vehicle=' + encodeURIComponent(vehicle) + '&phone=' +
encodeURIComponent(phone) + '&arrival=' + encodeURIComponent(arrival) })";
html += " .then(res => res.text()).then(data => { alert('Booking confirmed!
Payment of ₹' + totalAmount + ' processed.\\nSlot: A' + selectedSlot + '\\nVehicle:
' + vehicle + '\\nDuration: ' + duration + ' hours'); closeBookingModal();
updateData(); });";
html += " } else { alert('Please complete all steps and ensure arrival time is
not in the past'); }";
html += "}";
// Timer calculation function with booking status
html += "function calculateRemainingTime(occupiedTime, reservedDuration,
isOccupied, arrivalTime) {";
html += " let now = Date.now();";
html += " if (!isOccupied && reservedDuration > 0) {";
html += " return 'Booked - Awaiting';";
html += " }";
html += " if (isOccupied && occupiedTime && reservedDuration) {";
html += " let endTime = occupiedTime + (reservedDuration * 1000);";
html += " let remaining = endTime - now;";
html += " if (remaining <= 0) return 'Time Expired';";
html += " let hours = Math.floor(remaining / (60 * 60 * 1000));";
html += " let minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 *
1000));";
html += " let seconds = Math.floor((remaining % (60 * 1000)) / 1000);";
html += " if (hours > 0) return hours + 'h ' + minutes + 'm left';";
html += " if (minutes > 0) return minutes + 'm ' + seconds + 's left';";
html += " return seconds + 's left';";
html += " }";
html += " return '';";
html += "}";
html += "function updateData() {";
html += " fetch('/api/status')";
html += " .then(res => res.json())";
html += " .then(data => {";
html += " document.getElementById('available-slots').innerText =
data.availableSlots + '/' + data.totalSlots;";
html += " document.getElementById('active-bookings').innerText =
data.occupiedSlots + data.reservedSlots;";
html += " document.getElementById('occupancy-rate').innerText =
data.occupancyRate + '%';";
html += " document.getElementById('revenue').innerText = '₹' + data.revenue;";
html += " document.getElementById('last-updated').innerText = data.lastUpdated;";
// Update activity list
html += " let activityList = document.getElementById('activity-list');
activityList.innerHTML = '';";
html += " data.recentActivity.forEach(act => { let item =
document.createElement('div'); item.className = 'activity-item'; item.innerText =
act; activityList.appendChild(item); });";
// Update bookings list
html += " let bookingsList = document.getElementById('bookings-list');
bookingsList.innerHTML = '';";
// Update slots with enhanced timer functionality and status management
html += " for (let i = 0; i < 6; i++) {";
html += " let slotData = data.slots[i];";
html += " let slotDiv = document.getElementById('slot' + (i + 1));";
html += " let dot = slotDiv.querySelector('.dot');";
html += " let status = slotDiv.querySelector('.status');";
html += " let timer = document.getElementById('timer' + (i + 1));";
html += " let occupiedTime = slotData.occupied ? (data.timestamp -
(slotData.currentTime * 1000)) : 0;";
html += " let remainingTime = calculateRemainingTime(occupiedTime,
slotData.reservedDuration, slotData.occupied, 0);";
html += " if (slotData.occupied && slotData.reserved) {";
html += " if (remainingTime === 'Time Expired') {";
html += " dot.className = 'dot red'; status.innerText = 'Overstaying';";
html += " timer.innerText = 'Time Expired - ₹10/min penalty';";
html += " } else {";
html += " dot.className = 'dot red'; status.innerText = 'Occupied';";
html += " timer.innerText = remainingTime;";
html += " }";
html += " slotDiv.classList.remove('available'); slotDiv.onclick = null;";
html += " }";
html += " else if (slotData.occupied && !slotData.reserved) {";
html += " dot.className = 'dot red'; status.innerText = 'Occupied
(Unreserved)';";
html += " timer.innerText = 'No booking found';";
html += " slotDiv.classList.remove('available'); slotDiv.onclick = null;";
html += " }";
html += " else if (!slotData.occupied && slotData.reserved) {";
html += " dot.className = 'dot orange'; status.innerText = 'Booked';";
html += " timer.innerText = 'Awaiting arrival';";
html += " slotDiv.classList.remove('available'); slotDiv.onclick = null;";
html += " }";
html += " else {";
html += " dot.className = 'dot green'; status.innerText = 'Available';";
html += " slotDiv.classList.add('available'); timer.innerText = '';";
html += " slotDiv.onclick = () => { selectedSlot = i + 1;
document.getElementById('selected-slot').innerText = 'Slot A' + (i + 1);
openBookingModal(); };";
html += " }";
// Add to bookings list if reserved
html += " if (slotData.reserved) {";
html += " let bookingItem = document.createElement('div'); bookingItem.className
= 'booking-item';";
html += " bookingItem.innerHTML = '<div>Slot A' + (i + 1) + ' - ' +
slotData.vehicleNumber + '</div><button class=\"cancel-btn\"
onclick=\"cancelBooking(' + (i + 1) + ')\">Cancel</button>';";
html += " bookingsList.appendChild(bookingItem);";
html += " }";
html += " }";
html += " });";
html += "}";
html += "function cancelBooking(slot) {";
html += " if (confirm('Are you sure you want to cancel this booking?')) {";
html += " fetch('/api/cancel', { method: 'POST', headers: { 'Content-Type':
'application/x-www-form-urlencoded' }, body: 'slot=' + slot })";
html += " .then(res => res.text()).then(data => { alert(data);
updateData(); });";
html += " }";
html += "}";
html += "function sendReport() {";
html += " let msg = document.getElementById('reportMessage').value.trim();";
html += " if (msg) {";
html += " fetch('/api/report', { method: 'POST', headers: { 'Content-Type':
'application/x-www-form-urlencoded' }, body: 'message=' +
encodeURIComponent(msg) })";
html += " .then(res => res.text()).then(data => {";
html += " document.getElementById('reportStatus').innerText = 'Report sent
successfully!';";
html += " setTimeout(() => { closeReportModal(); }, 2000);";
html += " });";
html += " } else {";
html += " alert('Please enter a message');";
html += " }";
html += "}";
// Auto-update data every 1 second for real-time timer updates
html += "setInterval(updateData, 1000); updateData();";
html += "</script>";
html += "</body></html>";
return html;
}