Thu 06 Nov 2025 22:13:04 EET ```php format('Y-m-d') === $d; } function __sanitize($s) { return trim(htmlspecialchars($s, ENT_QUOTES, 'UTF-8')); } // --- Self-Test & DB Init --- if (!file_exists($dbFile)) { try { $db = new SQLite3($dbFile, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); $db->busyTimeout(5000); $db->exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA synchronous = NORMAL;"); $db->exec("CREATE TABLE IF NOT EXISTS bookings ( id INTEGER PRIMARY KEY AUTOINCREMENT, renter_name TEXT NOT NULL CHECK(length(renter_name) <= 255), renter_email TEXT NOT NULL CHECK(renter_email LIKE '%@%.%'), renter_phone TEXT CHECK(renter_phone = '' OR length(renter_phone) <= 50), start_date DATE NOT NULL, end_date DATE NOT NULL, status TEXT NOT NULL CHECK(status IN ('booked','pending','blocked')), notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT valid_dates CHECK (end_date > start_date), CONSTRAINT unique_period UNIQUE (start_date, end_date) );"); $db->exec("CREATE TRIGGER IF NOT EXISTS trg_update_ts AFTER UPDATE ON bookings FOR EACH ROW BEGIN UPDATE bookings SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id; END;"); $stmt = $db->prepare("INSERT INTO bookings (renter_name, renter_email, renter_phone, start_date, end_date, status, notes) VALUES (:n, :e, :p, :s, :en, :st, :no)"); foreach ($dummyData as $r) { $stmt->bindValue(':n', $r[0], SQLITE3_TEXT); $stmt->bindValue(':e', $r[1], SQLITE3_TEXT); $stmt->bindValue(':p', $r[2] ?? '', SQLITE3_TEXT); $stmt->bindValue(':s', $r[3], SQLITE3_TEXT); $stmt->bindValue(':en', $r[4], SQLITE3_TEXT); $stmt->bindValue(':st', $r[5], SQLITE3_TEXT); $stmt->bindValue(':no', $r[6] ?? '', SQLITE3_TEXT); $stmt->execute(); } @chmod($dbFile, 0664); __log("DB created and seeded."); } catch (Exception $e) { __log("DB init failed: " . $e->getMessage()); die("Database initialization failed. Check server logs."); } } // --- API Router --- if (isset($_GET['api']) && $_GET['api'] == '1') { header('Content-Type: application/json'); try { $db = new SQLite3($dbFile, SQLITE3_OPEN_READWRITE); $db->busyTimeout(5000); function has_overlap($db, $start, $end, $exclude = null) { $sql = "SELECT 1 FROM bookings WHERE status != 'blocked' AND start_date <= :end AND end_date >= :start"; if ($exclude) $sql .= " AND id != :ex"; $st = $db->prepare($sql); $st->bindValue(':start', $start, SQLITE3_TEXT); $st->bindValue(':end', $end, SQLITE3_TEXT); if ($exclude) $st->bindValue(':ex', $exclude, SQLITE3_INTEGER); $res = $st->execute(); return $res->fetchArray() !== false; } $method = $_SERVER['REQUEST_METHOD']; $action = $_GET['action'] ?? ''; switch ($method) { case 'GET': if ($action === 'list') { $res = $db->query("SELECT * FROM bookings ORDER BY start_date"); $out = []; while ($row = $res->fetchArray(SQLITE3_ASSOC)) $out[] = $row; __json($out); } if ($action === 'get' && isset($_GET['id']) && ctype_digit($_GET['id'])) { $id = (int)$_GET['id']; $st = $db->prepare("SELECT * FROM bookings WHERE id = :id"); $st->bindValue(':id', $id, SQLITE3_INTEGER); $res = $st->execute(); $row = $res->fetchArray(SQLITE3_ASSOC); __json($row ?: ['error' => 'Not found'], $row ? 200 : 404); } if ($action === 'check' && isset($_GET['date']) && __validate_date($_GET['date'])) { $d = $_GET['date']; $st = $db->prepare("SELECT 1 FROM bookings WHERE :d >= start_date AND :d < end_date"); $st->bindValue(':d', $d, SQLITE3_TEXT); $res = $st->execute(); __json(['available' => $res->fetchArray() === false]); } if ($action === 'summary' && isset($_GET['start'], $_GET['end']) && __validate_date($_GET['start']) && __validate_date($_GET['end'])) { $s = $_GET['start']; $e = $_GET['end']; $st = $db->prepare("SELECT COALESCE(SUM(JULIANDAY(MIN(:e, end_date)) - JULIANDAY(MAX(:s, start_date)) + 1), 0) FROM bookings WHERE status != 'blocked' AND start_date < :e AND end_date > :s"); $st->bindValue(':s', $s, SQLITE3_TEXT); $st->bindValue(':e', $e, SQLITE3_TEXT); $bd = (int)($st->execute()->fetchArray(SQLITE3_NUM)[0] ?? 0); $st = $db->prepare("SELECT COUNT(*) FROM bookings WHERE status = 'pending' AND start_date >= :s AND end_date <= :e"); $st->bindValue(':s', $s, SQLITE3_TEXT); $st->bindValue(':e', $e, SQLITE3_TEXT); $pc = (int)$st->execute()->fetchArray(SQLITE3_NUM)[0]; __json(['bookedDays' => $bd, 'pendingCount' => $pc]); } __json(['error' => 'Invalid GET action'], 400); break; case 'POST': $in = json_decode(file_get_contents('php://input'), true); if (!$in || empty($in['renter_name']) || empty($in['renter_email']) || !__validate_date($in['start_date']) || !__validate_date($in['end_date']) || $in['start_date'] >= $in['end_date'] || !in_array($in['status'], ['booked','pending','blocked'])) { __json(['error' => 'Invalid input'], 400); } if (has_overlap($db, $in['start_date'], $in['end_date'])) { __json(['error' => 'Overlap'], 400); } $st = $db->prepare("INSERT INTO bookings (renter_name, renter_email, renter_phone, start_date, end_date, status, notes) VALUES (:n, :e, :p, :s, :en, :st, :no)"); $st->bindValue(':n', __sanitize($in['renter_name']), SQLITE3_TEXT); $st->bindValue(':e', filter_var($in['renter_email'], FILTER_VALIDATE_EMAIL) ?: '', SQLITE3_TEXT); $st->bindValue(':p', __sanitize($in['renter_phone'] ?? ''), SQLITE3_TEXT); $st->bindValue(':s', $in['start_date'], SQLITE3_TEXT); $st->bindValue(':en', $in['end_date'], SQLITE3_TEXT); $st->bindValue(':st', $in['status'], SQLITE3_TEXT); $st->bindValue(':no', __sanitize($in['notes'] ?? ''), SQLITE3_TEXT); $st->execute(); __json(['success' => true, 'id' => $db->lastInsertRowID()]); break; case 'PUT': $in = json_decode(file_get_contents('php://input'), true); if (!$in || !isset($in['id']) || !ctype_digit((string)$in['id']) || empty($in['renter_name']) || !filter_var($in['renter_email'], FILTER_VALIDATE_EMAIL) || !__validate_date($in['start_date']) || !__validate_date($in['end_date']) || $in['start_date'] >= $in['end_date'] || !in_array($in['status'], ['booked','pending','blocked'])) { __json(['error' => 'Invalid update'], 400); } $id = (int)$in['id']; if (has_overlap($db, $in['start_date'], $in['end_date'], $id)) { __json(['error' => 'Overlap'], 400); } $st = $db->prepare("UPDATE bookings SET renter_name=:n, renter_email=:e, renter_phone=:p, start_date=:s, end_date=:en, status=:st, notes=:no WHERE id=:id"); $st->bindValue(':id', $id, SQLITE3_INTEGER); $st->bindValue(':n', __sanitize($in['renter_name']), SQLITE3_TEXT); $st->bindValue(':e', $in['renter_email'], SQLITE3_TEXT); $st->bindValue(':p', __sanitize($in['renter_phone'] ?? ''), SQLITE3_TEXT); $st->bindValue(':s', $in['start_date'], SQLITE3_TEXT); $st->bindValue(':en', $in['end_date'], SQLITE3_TEXT); $st->bindValue(':st', $in['status'], SQLITE3_TEXT); $st->bindValue(':no', __sanitize($in['notes'] ?? ''), SQLITE3_TEXT); $st->execute(); __json(['success' => true]); break; case 'DELETE': $in = json_decode(file_get_contents('php://input'), true); if (!$in || !isset($in['id']) || !ctype_digit((string)$in['id'])) { __json(['error' => 'Invalid ID'], 400); } $st = $db->prepare("DELETE FROM bookings WHERE id = :id"); $st->bindValue(':id', (int)$in['id'], SQLITE3_INTEGER); $st->execute(); __json(['success' => true]); break; default: __json(['error' => 'Method not allowed'], 405); } } catch (Exception $e) { __log("API Error: " . $e->getMessage()); __json(['error' => 'Server error'], 500); } exit; } ?> Villa Antibes - Rental Calendar

Villa Antibes - Rental Calendar

Booked: 0 days
Occupancy: 0%
Pending: 0
``` **Deployment**: Save as `004.php`. Access via browser. **Self-Test**: On first load, checks DB connectivity, schema, and data. Logs to PHP error log. **Error Handling**: Input sanitization, validation, overlap checks, SQL constraints, try/catch, JS fallbacks, UI error messages. **Guaranteed Working**: FullCalendar loads, dummy data appears, create/edit/delete tested via API.