Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
37
README.md
@ -1,37 +0,0 @@
|
|||||||
# AFG_CARS - Supreme Automotive Marketplace
|
|
||||||
|
|
||||||
An enterprise-level car dealership management system built for professional, offline-ready deployment.
|
|
||||||
|
|
||||||
## 👥 Admin Credentials
|
|
||||||
**Access Link:** [Admin Panel](setup.php) (Initial setup & login dashboard)
|
|
||||||
|
|
||||||
- **Username:** `admin`
|
|
||||||
- **Password:** `admin123`
|
|
||||||
- **Role:** Super Admin (Full system control)
|
|
||||||
|
|
||||||
## 🏢 System Architecture (Multi-Page)
|
|
||||||
The application follows a professional Multi-Page Architecture (MPA) where each section is a standalone `.php` file for better performance and SEO:
|
|
||||||
|
|
||||||
- **Home (`index.php`):** Featured showcase, luxury hero section, testimonials, and branch locations.
|
|
||||||
- **Marketplace (`marketplace.php`):** Complete vehicle inventory with dynamic filters and installment previews.
|
|
||||||
- **Work (`work.php`):** Detailed step-by-step guides for buying and selling processes.
|
|
||||||
- **About (`about.php`):** Company history, mission, and business statistics.
|
|
||||||
- **Contact (`contact.php`):** Professional inquiry form and regional office contacts.
|
|
||||||
|
|
||||||
## 🛠 Features
|
|
||||||
- **Supreme Design:** 100% custom Dark-Themed CSS with Gold accents (No Bootstrap/CDN).
|
|
||||||
- **Offline Ready:** All assets (images, fonts, scripts) are stored locally for XAMPP deployment.
|
|
||||||
- **Multi-Branch:** Integrated data management for Kabul, Herat, Mazar, and Kandahar.
|
|
||||||
- **Installment System:** Automated calculation logic for monthly payments.
|
|
||||||
- **Enterprise RBAC:** Prepared structure for Guests, Dealers, and Admins.
|
|
||||||
|
|
||||||
## 🚀 Installation
|
|
||||||
1. Place the project folder inside your `htdocs` directory.
|
|
||||||
2. Ensure MySQL/MariaDB is running.
|
|
||||||
3. Open `http://localhost/your-folder-name/setup.php` in your browser.
|
|
||||||
4. Click **"Run Automated Setup"** to initialize the database and seed 20 premium cars.
|
|
||||||
|
|
||||||
---
|
|
||||||
**Developed by:** Mohammad Sadiq
|
|
||||||
**Project Type:** University Final-Year Enterprise System
|
|
||||||
**Environment:** PHP 8.x + MariaDB (100% Offline)
|
|
||||||
58
about.php
@ -1,58 +0,0 @@
|
|||||||
<?php
|
|
||||||
// about.php
|
|
||||||
require_once 'includes/auth.php';
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
|
||||||
<div class="grid" style="grid-template-columns: 1fr 1fr; gap: 50px; align-items: center;">
|
|
||||||
<div>
|
|
||||||
<h1>About AFG CARS</h1>
|
|
||||||
<p class="mb-5" style="color: var(--primary); font-weight: bold;">Your trusted partner for premium automotive solutions in Afghanistan.</p>
|
|
||||||
<p style="margin-bottom: 20px;">Founded in 2010, AFG CARS has established itself as the leading car dealership in Afghanistan. We specialize in importing and selling high-quality vehicles from top global manufacturers.</p>
|
|
||||||
<p>Our mission is to provide our customers with reliable, luxury vehicles at competitive prices, backed by exceptional customer service and flexible financing options.</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="card" style="padding: 40px; text-align: center; background: rgba(255,255,255,0.05);">
|
|
||||||
<div class="grid" style="grid-template-columns: 1fr 1fr; gap: 20px;">
|
|
||||||
<div>
|
|
||||||
<h3 style="color: var(--primary); font-size: 2.5rem;">10+</h3>
|
|
||||||
<p>Years Experience</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 style="color: var(--primary); font-size: 2.5rem;">5000+</h3>
|
|
||||||
<p>Cars Sold</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 style="color: var(--primary); font-size: 2.5rem;">4</h3>
|
|
||||||
<p>Major Branches</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 style="color: var(--primary); font-size: 2.5rem;">100%</h3>
|
|
||||||
<p>Satisfaction</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-5">
|
|
||||||
<h2 class="text-center mb-5">Our Core Values</h2>
|
|
||||||
<div class="grid" style="grid-template-columns: repeat(3, 1fr);">
|
|
||||||
<div class="card" style="padding: 20px; text-align: center;">
|
|
||||||
<h3 style="color: var(--primary);">Integrity</h3>
|
|
||||||
<p>We believe in transparent pricing and honest dealings with every customer.</p>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="padding: 20px; text-align: center;">
|
|
||||||
<h3 style="color: var(--primary);">Quality</h3>
|
|
||||||
<p>Every vehicle undergoes a rigorous 150-point inspection before sale.</p>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="padding: 20px; text-align: center;">
|
|
||||||
<h3 style="color: var(--primary);">Service</h3>
|
|
||||||
<p>Our relationship doesn't end at the sale; we provide ongoing support.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
// Handle Form Submission
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$name = $_POST['name'];
|
|
||||||
$location = $_POST['location'];
|
|
||||||
$phone = $_POST['phone'];
|
|
||||||
$email = $_POST['email'];
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO branches (name, location, phone, email) VALUES (?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$name, $location, $phone, $email]);
|
|
||||||
|
|
||||||
// Log activity
|
|
||||||
$adminId = $_SESSION['user_id'];
|
|
||||||
$pdo->prepare("INSERT INTO activity_logs (user_id, action) VALUES (?, 'Added new branch: $name')")->execute([$adminId]);
|
|
||||||
|
|
||||||
echo "<div style='padding: 1rem; background: #4caf50; color: white; margin-bottom: 1rem;'>Branch Added Successfully</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete Branch
|
|
||||||
if (isset($_GET['delete'])) {
|
|
||||||
$id = $_GET['delete'];
|
|
||||||
$pdo->prepare("DELETE FROM branches WHERE id = ?")->execute([$id]);
|
|
||||||
echo "<div style='padding: 1rem; background: #f44336; color: white; margin-bottom: 1rem;'>Branch Deleted</div>";
|
|
||||||
}
|
|
||||||
|
|
||||||
$branches = $pdo->query("SELECT * FROM branches ORDER BY created_at DESC")->fetchAll();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>Branch Management</h1>
|
|
||||||
<button onclick="document.getElementById('addBranchModal').style.display='block'" class="btn-sm btn-primary">Add New Branch</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-grid" style="grid-template-columns: 1fr;">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Location</th>
|
|
||||||
<th>Contact</th>
|
|
||||||
<th>Created At</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($branches as $branch): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?php echo htmlspecialchars($branch['name']); ?></td>
|
|
||||||
<td><?php echo htmlspecialchars($branch['location']); ?></td>
|
|
||||||
<td>
|
|
||||||
<?php echo htmlspecialchars($branch['phone']); ?><br>
|
|
||||||
<small><?php echo htmlspecialchars($branch['email']); ?></small>
|
|
||||||
</td>
|
|
||||||
<td><?php echo $branch['created_at']; ?></td>
|
|
||||||
<td>
|
|
||||||
<a href="?delete=<?php echo $branch['id']; ?>" class="btn-sm btn-danger" onclick="return confirm('Delete this branch?')">Delete</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Simple Modal for Adding Branch -->
|
|
||||||
<div id="addBranchModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); z-index:1000;">
|
|
||||||
<div style="background:var(--card-bg); width:400px; margin: 100px auto; padding:2rem; border-radius:8px; border:1px solid var(--border-color);">
|
|
||||||
<h2 style="margin-bottom:1rem; color:var(--text-primary);">Add Branch</h2>
|
|
||||||
<form method="POST">
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<label style="display:block; color:var(--text-secondary); margin-bottom:0.5rem;">Branch Name</label>
|
|
||||||
<input type="text" name="name" required style="width:100%; padding:0.8rem; background:var(--bg-color); border:1px solid var(--border-color); color:var(--text-primary);">
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<label style="display:block; color:var(--text-secondary); margin-bottom:0.5rem;">Location</label>
|
|
||||||
<input type="text" name="location" required style="width:100%; padding:0.8rem; background:var(--bg-color); border:1px solid var(--border-color); color:var(--text-primary);">
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<label style="display:block; color:var(--text-secondary); margin-bottom:0.5rem;">Phone</label>
|
|
||||||
<input type="text" name="phone" style="width:100%; padding:0.8rem; background:var(--bg-color); border:1px solid var(--border-color); color:var(--text-primary);">
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<label style="display:block; color:var(--text-secondary); margin-bottom:0.5rem;">Email</label>
|
|
||||||
<input type="email" name="email" style="width:100%; padding:0.8rem; background:var(--bg-color); border:1px solid var(--border-color); color:var(--text-primary);">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn-sm btn-primary" style="width:100%;">Create Branch</button>
|
|
||||||
<button type="button" onclick="document.getElementById('addBranchModal').style.display='none'" class="btn-sm" style="width:100%; margin-top:0.5rem; background:transparent; border:1px solid var(--border-color);">Cancel</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Auto-open modal if action=add
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
if (urlParams.get('action') === 'add') {
|
|
||||||
document.getElementById('addBranchModal').style.display = 'block';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
106
admin/cars.php
@ -1,106 +0,0 @@
|
|||||||
<?php
|
|
||||||
// admin/cars.php
|
|
||||||
require_once '../includes/auth.php';
|
|
||||||
require_once '../includes/middleware.php';
|
|
||||||
requireAdmin();
|
|
||||||
require_once '../includes/header.php';
|
|
||||||
global $pdo;
|
|
||||||
|
|
||||||
$msg = '';
|
|
||||||
|
|
||||||
// Handle Delete
|
|
||||||
if (isset($_GET['delete'])) {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM cars WHERE id = ?");
|
|
||||||
$stmt->execute([$_GET['delete']]);
|
|
||||||
$msg = "Car deleted successfully.";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Add
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$brand = $_POST['brand'];
|
|
||||||
$model = $_POST['model'];
|
|
||||||
$year = $_POST['year'];
|
|
||||||
$price = $_POST['price'];
|
|
||||||
$branch_id = $_POST['branch_id'];
|
|
||||||
|
|
||||||
// Use a random placeholder from the 20 generated ones for demo purposes
|
|
||||||
$random_img = rand(1, 20);
|
|
||||||
$image_path = "assets/images/cars/car{$random_img}.jpg";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO cars (brand, model, year, price, branch_id, status, image_path, is_featured) VALUES (?, ?, ?, ?, ?, 'available', ?, 0)");
|
|
||||||
$stmt->execute([$brand, $model, $year, $price, $branch_id, $image_path]);
|
|
||||||
$msg = "Car added successfully.";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch Cars
|
|
||||||
$cars = $pdo->query("SELECT cars.*, branches.city FROM cars LEFT JOIN branches ON cars.branch_id = branches.id ORDER BY created_at DESC")->fetchAll();
|
|
||||||
$branches = $pdo->query("SELECT * FROM branches")->fetchAll();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<div class="sidebar">
|
|
||||||
<h3 class="mb-5" style="color: var(--primary);">Admin Panel</h3>
|
|
||||||
<a href="index.php">Dashboard</a>
|
|
||||||
<a href="cars.php" class="active">Manage Cars</a>
|
|
||||||
<a href="users.php">Manage Users</a>
|
|
||||||
<a href="branches.php">Manage Branches</a>
|
|
||||||
<a href="sales.php">Sales & Installments</a>
|
|
||||||
<a href="../logout.php" class="mt-5" style="color: #e63946;">Logout</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-content">
|
|
||||||
<h1>Manage Cars</h1>
|
|
||||||
|
|
||||||
<?php if ($msg): ?>
|
|
||||||
<div class="alert alert-success" style="background: rgba(42, 157, 143, 0.2); color: #2a9d8f; padding: 10px; margin-bottom: 20px;">
|
|
||||||
<?= htmlspecialchars($msg) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Add Car Form -->
|
|
||||||
<div class="card mb-5" style="padding: 20px;">
|
|
||||||
<h3>Add New Car</h3>
|
|
||||||
<form method="POST" class="grid" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
|
||||||
<input type="text" name="brand" placeholder="Brand" class="form-control" required>
|
|
||||||
<input type="text" name="model" placeholder="Model" class="form-control" required>
|
|
||||||
<input type="number" name="year" placeholder="Year" class="form-control" required>
|
|
||||||
<input type="number" name="price" placeholder="Price" class="form-control" required>
|
|
||||||
<select name="branch_id" class="form-control" required>
|
|
||||||
<option value="">Select Branch</option>
|
|
||||||
<?php foreach ($branches as $b): ?>
|
|
||||||
<option value="<?= $b['id'] ?>"><?= htmlspecialchars($b['city']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="btn">Add Car</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="padding: 20px;">
|
|
||||||
<h3>Inventory List</h3>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Car</th>
|
|
||||||
<th>Price</th>
|
|
||||||
<th>Branch</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
<?php foreach ($cars as $car): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= $car['id'] ?></td>
|
|
||||||
<td><?= htmlspecialchars($car['year'] . ' ' . $car['brand'] . ' ' . $car['model']) ?></td>
|
|
||||||
<td>$<?= number_format((float)$car['price']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($car['city'] ?? 'N/A') ?></td>
|
|
||||||
<td><?= htmlspecialchars(ucfirst($car['status'])) ?></td>
|
|
||||||
<td>
|
|
||||||
<a href="?delete=<?= $car['id'] ?>" style="color: #e63946;" onclick="return confirm('Are you sure?')">Delete</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once '../includes/footer.php'; ?>
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
<?php
|
|
||||||
// admin/index.php
|
|
||||||
require_once '../includes/auth.php';
|
|
||||||
require_once '../includes/middleware.php';
|
|
||||||
requireAdmin();
|
|
||||||
require_once '../includes/header.php';
|
|
||||||
global $pdo;
|
|
||||||
|
|
||||||
// Fetch Stats
|
|
||||||
$total_cars = $pdo->query("SELECT COUNT(*) FROM cars")->fetchColumn();
|
|
||||||
$total_users = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
|
||||||
$total_sales = $pdo->query("SELECT COUNT(*) FROM sales")->fetchColumn();
|
|
||||||
$pending_inquiries = $pdo->query("SELECT COUNT(*) FROM inquiries")->fetchColumn();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<div class="sidebar">
|
|
||||||
<h3 class="mb-5" style="color: var(--primary);">Admin Panel</h3>
|
|
||||||
<a href="index.php" class="active">Dashboard</a>
|
|
||||||
<a href="cars.php">Manage Cars</a>
|
|
||||||
<a href="users.php">Manage Users</a>
|
|
||||||
<a href="branches.php">Manage Branches</a>
|
|
||||||
<a href="sales.php">Sales & Installments</a>
|
|
||||||
<a href="../logout.php" class="mt-5" style="color: #e63946;">Logout</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-content">
|
|
||||||
<h1>Dashboard</h1>
|
|
||||||
<p class="mb-5">Welcome back, <?= htmlspecialchars(getUserName()) ?></p>
|
|
||||||
|
|
||||||
<div class="grid" style="grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 40px;">
|
|
||||||
<div class="card" style="padding: 20px; text-align: center;">
|
|
||||||
<h3>Total Cars</h3>
|
|
||||||
<div class="stat-number" style="font-size: 2rem; color: var(--primary); font-weight: bold;"><?= $total_cars ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="padding: 20px; text-align: center;">
|
|
||||||
<h3>Total Users</h3>
|
|
||||||
<div class="stat-number" style="font-size: 2rem; color: var(--primary); font-weight: bold;"><?= $total_users ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="padding: 20px; text-align: center;">
|
|
||||||
<h3>Sales</h3>
|
|
||||||
<div class="stat-number" style="font-size: 2rem; color: var(--primary); font-weight: bold;"><?= $total_sales ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="card" style="padding: 20px; text-align: center;">
|
|
||||||
<h3>Inquiries</h3>
|
|
||||||
<div class="stat-number" style="font-size: 2rem; color: var(--primary); font-weight: bold;"><?= $pending_inquiries ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="padding: 20px;">
|
|
||||||
<h3>Recent Inquiries</h3>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Message</th>
|
|
||||||
</tr>
|
|
||||||
<?php
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT * FROM inquiries ORDER BY created_at DESC LIMIT 5");
|
|
||||||
while ($row = $stmt->fetch()):
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td><?= date('M d, Y', strtotime($row['created_at'])) ?></td>
|
|
||||||
<td><?= htmlspecialchars($row['name']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($row['email']) ?></td>
|
|
||||||
<td><?= htmlspecialchars(substr($row['message'], 0, 50)) ?>...</td>
|
|
||||||
</tr>
|
|
||||||
<?php endwhile;
|
|
||||||
} catch(Exception $e) { echo "<tr><td colspan='4'>No data</td></tr>"; }
|
|
||||||
?>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once '../includes/footer.php'; ?>
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
$dealers = $pdo->query("SELECT * FROM users WHERE role = 'Dealer' ORDER BY created_at DESC")->fetchAll();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>Dealer Management</h1>
|
|
||||||
<a href="users.php?role=Dealer" class="btn-sm btn-primary">Manage All Users</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-grid" style="grid-template-columns: 1fr;">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Performance (Sales)</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($dealers as $dealer): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?php echo htmlspecialchars($dealer['username']); ?></td>
|
|
||||||
<td><?php echo htmlspecialchars($dealer['email']); ?></td>
|
|
||||||
<td><span style="padding:0.2rem 0.5rem; background:rgba(76, 175, 80, 0.2); color:#4caf50; border-radius:4px;">Active</span></td>
|
|
||||||
<td>$0.00 (0 Sales)</td> <!-- Placeholder for now -->
|
|
||||||
<td>
|
|
||||||
<a href="users.php?edit=<?php echo $dealer['id']; ?>" class="btn-sm" style="background:var(--accent-color); color:black;">Edit</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($dealers)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" style="text-align:center; color:var(--text-secondary); padding:2rem;">No dealers found. Create one in the Users section.</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
</main>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../../includes/auth.php';
|
|
||||||
requireAdmin();
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Admin Dashboard</title>
|
|
||||||
<link rel="stylesheet" href="/assets/css/style.css">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
.admin-layout { display: flex; min-height: 100vh; }
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background: var(--surface-color);
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: fixed;
|
|
||||||
height: 100vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 250px;
|
|
||||||
padding: 2rem;
|
|
||||||
background: var(--bg-color);
|
|
||||||
}
|
|
||||||
.sidebar-brand {
|
|
||||||
color: var(--accent-color);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.nav-link {
|
|
||||||
display: block;
|
|
||||||
padding: 0.8rem 1rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.nav-link:hover, .nav-link.active {
|
|
||||||
background: rgba(212, 175, 55, 0.1);
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
.card-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.stat-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
.stat-value { font-size: 2rem; font-weight: 700; color: var(--text-primary); margin-bottom: 0.5rem; }
|
|
||||||
.stat-label { color: var(--text-secondary); font-size: 0.9rem; }
|
|
||||||
.data-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
.data-table th, .data-table td {
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
.data-table th {
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
color: var(--accent-color);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.data-table tr:hover { background: rgba(255,255,255,0.02); }
|
|
||||||
.btn-sm {
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.btn-danger { background: rgba(255, 68, 68, 0.2); color: #ff4444; }
|
|
||||||
.btn-danger:hover { background: rgba(255, 68, 68, 0.3); }
|
|
||||||
.btn-primary { background: var(--accent-color); color: var(--bg-color); font-weight: 600; }
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
h1 { font-size: 1.8rem; color: var(--text-primary); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="admin-layout">
|
|
||||||
<aside class="sidebar">
|
|
||||||
<a href="/admin/dashboard.php" class="sidebar-brand">AFG_CARS ADMIN</a>
|
|
||||||
<nav>
|
|
||||||
<a href="/admin/dashboard.php" class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'dashboard.php' ? 'active' : ''; ?>">Dashboard</a>
|
|
||||||
<a href="/admin/users.php" class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'users.php' ? 'active' : ''; ?>">Users</a>
|
|
||||||
<a href="/admin/branches.php" class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'branches.php' ? 'active' : ''; ?>">Branches</a>
|
|
||||||
<a href="/admin/dealers.php" class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'dealers.php' ? 'active' : ''; ?>">Dealers</a>
|
|
||||||
<a href="/admin/cars.php" class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'cars.php' ? 'active' : ''; ?>">Cars Inventory</a>
|
|
||||||
<a href="/admin/sales.php" class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'sales.php' ? 'active' : ''; ?>">Sales & Installments</a>
|
|
||||||
<a href="/admin/reports.php" class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'reports.php' ? 'active' : ''; ?>">Reports</a>
|
|
||||||
<a href="/logout.php" class="nav-link" style="margin-top: auto; color: #ff4444;">Logout</a>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
<main class="main-content">
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<?php header('Location: dashboard.php'); ?>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
?>
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>Reports & Analytics</h1>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center; padding: 4rem; background: var(--card-bg); border: 1px solid var(--border-color); border-radius: var(--border-radius);">
|
|
||||||
<i class="fas fa-chart-pie" style="font-size: 3rem; color: var(--accent-color); margin-bottom: 1rem;"></i>
|
|
||||||
<h3>Enterprise Reporting</h3>
|
|
||||||
<p style="color: var(--text-secondary);">Financial reports, dealer performance, and inventory turnover analytics will be available here.</p>
|
|
||||||
</div>
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
?>
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>Sales & Installments</h1>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center; padding: 4rem; background: var(--card-bg); border: 1px solid var(--border-color); border-radius: var(--border-radius);">
|
|
||||||
<i class="fas fa-file-invoice-dollar" style="font-size: 3rem; color: var(--accent-color); margin-bottom: 1rem;"></i>
|
|
||||||
<h3>Sales Records System</h3>
|
|
||||||
<p style="color: var(--text-secondary);">No sales recorded yet. Once sales are made, they will appear here along with installment tracking.</p>
|
|
||||||
</div>
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
145
admin/users.php
@ -1,145 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
if (isset($_POST['delete_user'])) {
|
|
||||||
$id = $_POST['user_id'];
|
|
||||||
if ($id != $_SESSION['user_id']) {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
echo "<div style='padding: 1rem; background: #f44336; color: white;'>User Deleted</div>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Add User
|
|
||||||
if (isset($_POST['add_user'])) {
|
|
||||||
$username = $_POST['username'];
|
|
||||||
$email = $_POST['email'];
|
|
||||||
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
|
|
||||||
$role = $_POST['role'];
|
|
||||||
|
|
||||||
// Check if exists
|
|
||||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
|
|
||||||
$stmt->execute([$username, $email]);
|
|
||||||
if ($stmt->fetch()) {
|
|
||||||
echo "<div style='padding: 1rem; background: #f44336; color: white;'>User already exists</div>";
|
|
||||||
} else {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$username, $email, $password, $role]);
|
|
||||||
echo "<div style='padding: 1rem; background: #4caf50; color: white;'>User Added Successfully</div>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['update_role'])) {
|
|
||||||
$id = $_POST['user_id'];
|
|
||||||
$role = $_POST['role'];
|
|
||||||
if ($id != $_SESSION['user_id']) { // Prevent changing own role to something lower accidentally
|
|
||||||
$stmt = $pdo->prepare("UPDATE users SET role = ? WHERE id = ?");
|
|
||||||
$stmt->execute([$role, $id]);
|
|
||||||
echo "<div style='padding: 1rem; background: #4caf50; color: white;'>User Role Updated</div>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->query("SELECT * FROM users ORDER BY created_at DESC");
|
|
||||||
$users = $stmt->fetchAll();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>User Management</h1>
|
|
||||||
<button onclick="document.getElementById('addUserModal').style.display='block'" class="btn-sm btn-primary">Add New User</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Joined</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($users as $user): ?>
|
|
||||||
<tr>
|
|
||||||
<td>#<?php echo $user['id']; ?></td>
|
|
||||||
<td>
|
|
||||||
<?php echo htmlspecialchars($user['username']); ?><br>
|
|
||||||
<small style="color:var(--text-secondary);"><?php echo htmlspecialchars($user['email'] ?? ''); ?></small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<?php if ($user['id'] == $_SESSION['user_id']): ?>
|
|
||||||
<span class="badge badge-admin"><?php echo htmlspecialchars($user['role']); ?></span>
|
|
||||||
<?php else: ?>
|
|
||||||
<form method="POST" style="display:inline;">
|
|
||||||
<input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
|
|
||||||
<select name="role" onchange="this.form.submit()" style="padding:0.2rem; border-radius:4px; background:var(--bg-color); color:var(--text-primary); border:1px solid var(--border-color);">
|
|
||||||
<?php
|
|
||||||
$roles = ['Guest','Customer','Dealer','Employee','Manager','Admin','Super Admin'];
|
|
||||||
foreach ($roles as $r) {
|
|
||||||
$selected = ($user['role'] === $r) ? 'selected' : '';
|
|
||||||
echo "<option value='$r' $selected>$r</option>";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</select>
|
|
||||||
<input type="hidden" name="update_role" value="1">
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td><?php echo date('M j, Y', strtotime($user['created_at'])); ?></td>
|
|
||||||
<td>
|
|
||||||
<?php if ($user['id'] != $_SESSION['user_id']): ?>
|
|
||||||
<form method="POST" onsubmit="return confirm('Are you sure?');" style="display: inline;">
|
|
||||||
<input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
|
|
||||||
<button type="submit" name="delete_user" class="btn-sm btn-danger">Delete</button>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
|
|
||||||
<!-- Add User Modal -->
|
|
||||||
<div id="addUserModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); z-index:1000;">
|
|
||||||
<div style="background:var(--card-bg); width:400px; margin: 100px auto; padding:2rem; border-radius:8px; border:1px solid var(--border-color);">
|
|
||||||
<h2 style="margin-bottom:1rem; color:var(--text-primary);">Add User</h2>
|
|
||||||
<form method="POST">
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<label style="display:block; color:var(--text-secondary); margin-bottom:0.5rem;">Username</label>
|
|
||||||
<input type="text" name="username" required style="width:100%; padding:0.8rem; background:var(--bg-color); border:1px solid var(--border-color); color:var(--text-primary);">
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<label style="display:block; color:var(--text-secondary); margin-bottom:0.5rem;">Email</label>
|
|
||||||
<input type="email" name="email" required style="width:100%; padding:0.8rem; background:var(--bg-color); border:1px solid var(--border-color); color:var(--text-primary);">
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<label style="display:block; color:var(--text-secondary); margin-bottom:0.5rem;">Password</label>
|
|
||||||
<input type="password" name="password" required style="width:100%; padding:0.8rem; background:var(--bg-color); border:1px solid var(--border-color); color:var(--text-primary);">
|
|
||||||
</div>
|
|
||||||
<div style="margin-bottom:1rem;">
|
|
||||||
<label style="display:block; color:var(--text-secondary); margin-bottom:0.5rem;">Role</label>
|
|
||||||
<select name="role" style="width:100%; padding:0.8rem; background:var(--bg-color); border:1px solid var(--border-color); color:var(--text-primary);">
|
|
||||||
<option value="Customer">Customer</option>
|
|
||||||
<option value="Dealer">Dealer</option>
|
|
||||||
<option value="Manager">Manager</option>
|
|
||||||
<option value="Admin">Admin</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" name="add_user" class="btn-sm btn-primary" style="width:100%;">Create User</button>
|
|
||||||
<button type="button" onclick="document.getElementById('addUserModal').style.display='none'" class="btn-sm" style="width:100%; margin-top:0.5rem; background:transparent; border:1px solid var(--border-color);">Cancel</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
if (urlParams.get('action') === 'add') {
|
|
||||||
document.getElementById('addUserModal').style.display = 'block';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
493
ai/LocalAIApi.php
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
<?php
|
||||||
|
// LocalAIApi — proxy client for the Responses API.
|
||||||
|
// Usage (async: auto-polls status until ready):
|
||||||
|
// require_once __DIR__ . '/ai/LocalAIApi.php';
|
||||||
|
// $response = LocalAIApi::createResponse([
|
||||||
|
// 'input' => [
|
||||||
|
// ['role' => 'system', 'content' => 'You are a helpful assistant.'],
|
||||||
|
// ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
|
||||||
|
// ],
|
||||||
|
// ]);
|
||||||
|
// if (!empty($response['success'])) {
|
||||||
|
// // response['data'] contains full payload, e.g.:
|
||||||
|
// // {
|
||||||
|
// // "id": "resp_xxx",
|
||||||
|
// // "status": "completed",
|
||||||
|
// // "output": [
|
||||||
|
// // {"type": "reasoning", "summary": []},
|
||||||
|
// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
|
||||||
|
// // ]
|
||||||
|
// // }
|
||||||
|
// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...)
|
||||||
|
// }
|
||||||
|
// Poll settings override:
|
||||||
|
// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]);
|
||||||
|
|
||||||
|
class LocalAIApi
|
||||||
|
{
|
||||||
|
/** @var array<string,mixed>|null */
|
||||||
|
private static ?array $configCache = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature compatible with the OpenAI Responses API.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.).
|
||||||
|
* @param array<string,mixed> $options Extra options (timeout, verify_tls, headers, path, project_uuid).
|
||||||
|
* @return array{
|
||||||
|
* success:bool,
|
||||||
|
* status?:int,
|
||||||
|
* data?:mixed,
|
||||||
|
* error?:string,
|
||||||
|
* response?:mixed,
|
||||||
|
* message?:string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function createResponse(array $params, array $options = []): array
|
||||||
|
{
|
||||||
|
$cfg = self::config();
|
||||||
|
$payload = $params;
|
||||||
|
|
||||||
|
if (empty($payload['input']) || !is_array($payload['input'])) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'input_missing',
|
||||||
|
'message' => 'Parameter "input" is required and must be an array.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($payload['model']) || $payload['model'] === '') {
|
||||||
|
$payload['model'] = $cfg['default_model'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$initial = self::request($options['path'] ?? null, $payload, $options);
|
||||||
|
if (empty($initial['success'])) {
|
||||||
|
return $initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async flow: if backend returns ai_request_id, poll status until ready
|
||||||
|
$data = $initial['data'] ?? null;
|
||||||
|
if (is_array($data) && isset($data['ai_request_id'])) {
|
||||||
|
$aiRequestId = $data['ai_request_id'];
|
||||||
|
$pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; // seconds
|
||||||
|
$pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; // seconds
|
||||||
|
return self::awaitResponse($aiRequestId, [
|
||||||
|
'timeout' => $pollTimeout,
|
||||||
|
'interval' => $pollInterval,
|
||||||
|
'headers' => $options['headers'] ?? [],
|
||||||
|
'timeout_per_call' => $options['timeout'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snake_case alias for createResponse (matches the provided example).
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $params
|
||||||
|
* @param array<string,mixed> $options
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function create_response(array $params, array $options = []): array
|
||||||
|
{
|
||||||
|
return self::createResponse($params, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a raw request to the AI proxy.
|
||||||
|
*
|
||||||
|
* @param string $path Endpoint (may be an absolute URL).
|
||||||
|
* @param array<string,mixed> $payload JSON payload.
|
||||||
|
* @param array<string,mixed> $options Additional request options.
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function request(?string $path = null, array $payload = [], array $options = []): array
|
||||||
|
{
|
||||||
|
$cfg = self::config();
|
||||||
|
|
||||||
|
$projectUuid = $cfg['project_uuid'];
|
||||||
|
if (empty($projectUuid)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'project_uuid_missing',
|
||||||
|
'message' => 'PROJECT_UUID is not defined; aborting AI request.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultPath = $cfg['responses_path'] ?? null;
|
||||||
|
$resolvedPath = $path ?? ($options['path'] ?? $defaultPath);
|
||||||
|
if (empty($resolvedPath)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'project_id_missing',
|
||||||
|
'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = self::buildUrl($resolvedPath, $cfg['base_url']);
|
||||||
|
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||||
|
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||||
|
if ($timeout <= 0) {
|
||||||
|
$timeout = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
||||||
|
$verifyTls = array_key_exists('verify_tls', $options)
|
||||||
|
? (bool) $options['verify_tls']
|
||||||
|
: $baseVerifyTls;
|
||||||
|
|
||||||
|
$projectHeader = $cfg['project_header'];
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Accept: application/json',
|
||||||
|
];
|
||||||
|
$headers[] = $projectHeader . ': ' . $projectUuid;
|
||||||
|
if (!empty($options['headers']) && is_array($options['headers'])) {
|
||||||
|
foreach ($options['headers'] as $header) {
|
||||||
|
if (is_string($header) && $header !== '') {
|
||||||
|
$headers[] = $header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) {
|
||||||
|
$payload['project_uuid'] = $projectUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||||
|
if ($body === false) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'json_encode_failed',
|
||||||
|
'message' => 'Failed to encode request body to JSON.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll AI request status until ready or timeout.
|
||||||
|
*
|
||||||
|
* @param int|string $aiRequestId
|
||||||
|
* @param array<string,mixed> $options
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function awaitResponse($aiRequestId, array $options = []): array
|
||||||
|
{
|
||||||
|
$cfg = self::config();
|
||||||
|
|
||||||
|
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds
|
||||||
|
$interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds
|
||||||
|
if ($interval <= 0) {
|
||||||
|
$interval = 5;
|
||||||
|
}
|
||||||
|
$perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null;
|
||||||
|
|
||||||
|
$deadline = time() + max($timeout, $interval);
|
||||||
|
$headers = $options['headers'] ?? [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$statusResp = self::fetchStatus($aiRequestId, [
|
||||||
|
'headers' => $headers,
|
||||||
|
'timeout' => $perCallTimeout,
|
||||||
|
]);
|
||||||
|
if (!empty($statusResp['success'])) {
|
||||||
|
$data = $statusResp['data'] ?? [];
|
||||||
|
if (is_array($data)) {
|
||||||
|
$statusValue = $data['status'] ?? null;
|
||||||
|
if ($statusValue === 'success') {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'status' => 200,
|
||||||
|
'data' => $data['response'] ?? $data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($statusValue === 'failed') {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'status' => 500,
|
||||||
|
'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed',
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return $statusResp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time() >= $deadline) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'timeout',
|
||||||
|
'message' => 'Timed out waiting for AI response.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
sleep($interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch status for queued AI request.
|
||||||
|
*
|
||||||
|
* @param int|string $aiRequestId
|
||||||
|
* @param array<string,mixed> $options
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function fetchStatus($aiRequestId, array $options = []): array
|
||||||
|
{
|
||||||
|
$cfg = self::config();
|
||||||
|
$projectUuid = $cfg['project_uuid'];
|
||||||
|
if (empty($projectUuid)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'project_uuid_missing',
|
||||||
|
'message' => 'PROJECT_UUID is not defined; aborting status check.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusPath = self::resolveStatusPath($aiRequestId, $cfg);
|
||||||
|
$url = self::buildUrl($statusPath, $cfg['base_url']);
|
||||||
|
|
||||||
|
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
||||||
|
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
||||||
|
if ($timeout <= 0) {
|
||||||
|
$timeout = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
||||||
|
$verifyTls = array_key_exists('verify_tls', $options)
|
||||||
|
? (bool) $options['verify_tls']
|
||||||
|
: $baseVerifyTls;
|
||||||
|
|
||||||
|
$projectHeader = $cfg['project_header'];
|
||||||
|
$headers = [
|
||||||
|
'Accept: application/json',
|
||||||
|
$projectHeader . ': ' . $projectUuid,
|
||||||
|
];
|
||||||
|
if (!empty($options['headers']) && is_array($options['headers'])) {
|
||||||
|
foreach ($options['headers'] as $header) {
|
||||||
|
if (is_string($header) && $header !== '') {
|
||||||
|
$headers[] = $header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::sendCurl($url, 'GET', null, $headers, $timeout, $verifyTls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract plain text from a Responses API payload.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $response Result of LocalAIApi::createResponse|request.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function extractText(array $response): string
|
||||||
|
{
|
||||||
|
$payload = $response['data'] ?? $response;
|
||||||
|
if (!is_array($payload)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($payload['output']) && is_array($payload['output'])) {
|
||||||
|
$combined = '';
|
||||||
|
foreach ($payload['output'] as $item) {
|
||||||
|
if (!isset($item['content']) || !is_array($item['content'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($item['content'] as $block) {
|
||||||
|
if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) {
|
||||||
|
$combined .= $block['text'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($combined !== '') {
|
||||||
|
return $combined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($payload['choices'][0]['message']['content'])) {
|
||||||
|
return (string) $payload['choices'][0]['message']['content'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to decode JSON emitted by the model (handles markdown fences).
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $response
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
public static function decodeJsonFromResponse(array $response): ?array
|
||||||
|
{
|
||||||
|
$text = self::extractText($response);
|
||||||
|
if ($text === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($text, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stripped = preg_replace('/^```json|```$/m', '', trim($text));
|
||||||
|
if ($stripped !== null && $stripped !== $text) {
|
||||||
|
$decoded = json_decode($stripped, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration from ai/config.php.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private static function config(): array
|
||||||
|
{
|
||||||
|
if (self::$configCache === null) {
|
||||||
|
$configPath = __DIR__ . '/config.php';
|
||||||
|
if (!file_exists($configPath)) {
|
||||||
|
throw new RuntimeException('AI config file not found: ai/config.php');
|
||||||
|
}
|
||||||
|
$cfg = require $configPath;
|
||||||
|
if (!is_array($cfg)) {
|
||||||
|
throw new RuntimeException('Invalid AI config format: expected array');
|
||||||
|
}
|
||||||
|
self::$configCache = $cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$configCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an absolute URL from base_url and a path.
|
||||||
|
*/
|
||||||
|
private static function buildUrl(string $path, string $baseUrl): string
|
||||||
|
{
|
||||||
|
$trimmed = trim($path);
|
||||||
|
if ($trimmed === '') {
|
||||||
|
return $baseUrl;
|
||||||
|
}
|
||||||
|
if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) {
|
||||||
|
return $trimmed;
|
||||||
|
}
|
||||||
|
if ($trimmed[0] === '/') {
|
||||||
|
return $baseUrl . $trimmed;
|
||||||
|
}
|
||||||
|
return $baseUrl . '/' . $trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve status path based on configured responses_path and ai_request_id.
|
||||||
|
*
|
||||||
|
* @param int|string $aiRequestId
|
||||||
|
* @param array<string,mixed> $cfg
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function resolveStatusPath($aiRequestId, array $cfg): string
|
||||||
|
{
|
||||||
|
$basePath = $cfg['responses_path'] ?? '';
|
||||||
|
$trimmed = rtrim($basePath, '/');
|
||||||
|
if ($trimmed === '') {
|
||||||
|
return '/ai-request/' . rawurlencode((string)$aiRequestId) . '/status';
|
||||||
|
}
|
||||||
|
if (substr($trimmed, -11) !== '/ai-request') {
|
||||||
|
$trimmed .= '/ai-request';
|
||||||
|
}
|
||||||
|
return $trimmed . '/' . rawurlencode((string)$aiRequestId) . '/status';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared CURL sender for GET/POST requests.
|
||||||
|
*
|
||||||
|
* @param string $url
|
||||||
|
* @param string $method
|
||||||
|
* @param string|null $body
|
||||||
|
* @param array<int,string> $headers
|
||||||
|
* @param int $timeout
|
||||||
|
* @param bool $verifyTls
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array
|
||||||
|
{
|
||||||
|
if (!function_exists('curl_init')) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'curl_missing',
|
||||||
|
'message' => 'PHP cURL extension is missing. Install or enable it on the VM.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||||
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls);
|
||||||
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
|
||||||
|
curl_setopt($ch, CURLOPT_FAILONERROR, false);
|
||||||
|
|
||||||
|
$upper = strtoupper($method);
|
||||||
|
if ($upper === 'POST') {
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? '');
|
||||||
|
} else {
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$responseBody = curl_exec($ch);
|
||||||
|
if ($responseBody === false) {
|
||||||
|
$error = curl_error($ch) ?: 'Unknown cURL error';
|
||||||
|
curl_close($ch);
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'curl_error',
|
||||||
|
'message' => $error,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$decoded = null;
|
||||||
|
if ($responseBody !== '' && $responseBody !== null) {
|
||||||
|
$decoded = json_decode($responseBody, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
$decoded = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status >= 200 && $status < 300) {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'status' => $status,
|
||||||
|
'data' => $decoded ?? $responseBody,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorMessage = 'AI proxy request failed';
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage;
|
||||||
|
} elseif (is_string($responseBody) && $responseBody !== '') {
|
||||||
|
$errorMessage = $responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'status' => $status,
|
||||||
|
'error' => $errorMessage,
|
||||||
|
'response' => $decoded ?? $responseBody,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy alias for backward compatibility with the previous class name.
|
||||||
|
if (!class_exists('OpenAIService')) {
|
||||||
|
class_alias(LocalAIApi::class, 'OpenAIService');
|
||||||
|
}
|
||||||
52
ai/config.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
// OpenAI proxy configuration (workspace scope).
|
||||||
|
// Reads values from environment variables or executor/.env.
|
||||||
|
|
||||||
|
$projectUuid = getenv('PROJECT_UUID');
|
||||||
|
$projectId = getenv('PROJECT_ID');
|
||||||
|
|
||||||
|
if (
|
||||||
|
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
||||||
|
($projectId === false || $projectId === null || $projectId === '')
|
||||||
|
) {
|
||||||
|
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||||
|
if ($envPath && is_readable($envPath)) {
|
||||||
|
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if ($line === '' || $line[0] === '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!str_contains($line, '=')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$value = trim($value, "\"' ");
|
||||||
|
if (getenv($key) === false || getenv($key) === '') {
|
||||||
|
putenv("{$key}={$value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$projectUuid = getenv('PROJECT_UUID');
|
||||||
|
$projectId = getenv('PROJECT_ID');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
|
||||||
|
$projectId = ($projectId === false) ? null : $projectId;
|
||||||
|
|
||||||
|
$baseUrl = 'https://flatlogic.com';
|
||||||
|
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'base_url' => $baseUrl,
|
||||||
|
'responses_path' => $responsesPath,
|
||||||
|
'project_id' => $projectId,
|
||||||
|
'project_uuid' => $projectUuid,
|
||||||
|
'project_header' => 'project-uuid',
|
||||||
|
'default_model' => 'gpt-5-mini',
|
||||||
|
'timeout' => 30,
|
||||||
|
'verify_tls' => true,
|
||||||
|
];
|
||||||
@ -1,228 +0,0 @@
|
|||||||
/* AFG CARS Enterprise Style - 100% Offline */
|
|
||||||
:root {
|
|
||||||
--primary: #e63946;
|
|
||||||
--secondary: #1d3557;
|
|
||||||
--accent: #457b9d;
|
|
||||||
--light: #f1faee;
|
|
||||||
--dark: #0f172a;
|
|
||||||
--glass-bg: rgba(255, 255, 255, 0.05);
|
|
||||||
--glass-border: rgba(255, 255, 255, 0.1);
|
|
||||||
--shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--dark);
|
|
||||||
color: var(--light);
|
|
||||||
line-height: 1.6;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
a { text-decoration: none; color: inherit; transition: 0.3s; }
|
|
||||||
ul { list-style: none; }
|
|
||||||
|
|
||||||
/* Components */
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.btn:hover { background: #c1121f; transform: translateY(-2px); }
|
|
||||||
.btn-outline { background: transparent; border: 2px solid var(--primary); }
|
|
||||||
.btn-outline:hover { background: var(--primary); }
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navbar */
|
|
||||||
.navbar {
|
|
||||||
background: rgba(15, 23, 42, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border-bottom: 1px solid var(--glass-border);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
padding: 15px 0;
|
|
||||||
}
|
|
||||||
.navbar .container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.logo { font-size: 1.5rem; font-weight: bold; color: var(--primary); }
|
|
||||||
.nav-links { display: flex; gap: 20px; }
|
|
||||||
.nav-links a:hover { color: var(--primary); }
|
|
||||||
|
|
||||||
/* Hero */
|
|
||||||
.hero {
|
|
||||||
height: 80vh;
|
|
||||||
background: linear-gradient(rgba(0,0,0,0.6), rgba(0,0,0,0.6)), url('../images/hero.jpg');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.hero h1 { font-size: 3rem; margin-bottom: 20px; }
|
|
||||||
.hero p { font-size: 1.2rem; margin-bottom: 30px; opacity: 0.9; }
|
|
||||||
|
|
||||||
/* Cards */
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 30px;
|
|
||||||
margin: 50px 0;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: var(--glass-bg);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
transition: 0.3s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.card:hover { transform: translateY(-5px); box-shadow: var(--shadow); }
|
|
||||||
.card img { width: 100%; height: 200px; object-fit: cover; }
|
|
||||||
.card-body { padding: 20px; flex-grow: 1; display: flex; flex-direction: column; }
|
|
||||||
.card-title { font-size: 1.2rem; margin-bottom: 10px; }
|
|
||||||
.card-price { color: var(--primary); font-size: 1.1rem; font-weight: bold; margin-bottom: 10px; }
|
|
||||||
.card-meta { font-size: 0.9rem; color: #ccc; margin-bottom: 15px; }
|
|
||||||
.card-actions { margin-top: auto; }
|
|
||||||
.badge {
|
|
||||||
background: var(--accent);
|
|
||||||
padding: 3px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
.form-group { margin-bottom: 15px; }
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.form-control:focus { outline: none; border-color: var(--primary); }
|
|
||||||
|
|
||||||
/* Auth Box */
|
|
||||||
.auth-box {
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 50px auto;
|
|
||||||
padding: 40px;
|
|
||||||
background: var(--glass-bg);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Admin Dashboard */
|
|
||||||
.dashboard-container {
|
|
||||||
display: flex;
|
|
||||||
min-height: calc(100vh - 60px);
|
|
||||||
}
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background: rgba(0,0,0,0.3);
|
|
||||||
padding: 20px;
|
|
||||||
border-right: 1px solid var(--glass-border);
|
|
||||||
}
|
|
||||||
.sidebar a {
|
|
||||||
display: block;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.sidebar a.active, .sidebar a:hover {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.main-content { padding: 30px; flex-grow: 1; overflow-y: auto; }
|
|
||||||
.stat-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.stat-card {
|
|
||||||
background: var(--glass-bg);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
}
|
|
||||||
.stat-number { font-size: 2rem; font-weight: bold; color: var(--primary); }
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
.table-responsive { overflow-x: auto; }
|
|
||||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
|
||||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid var(--glass-border); }
|
|
||||||
th { background: rgba(255,255,255,0.05); }
|
|
||||||
tr:hover { background: rgba(255,255,255,0.02); }
|
|
||||||
|
|
||||||
/* Installment Calculator */
|
|
||||||
.calculator {
|
|
||||||
background: var(--glass-bg);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-top: 30px;
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
footer {
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
padding: 40px 0;
|
|
||||||
margin-top: auto;
|
|
||||||
border-top: 1px solid var(--glass-border);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Helpers */
|
|
||||||
.text-center { text-align: center; }
|
|
||||||
.mt-5 { margin-top: 3rem; }
|
|
||||||
.mb-5 { margin-bottom: 3rem; }
|
|
||||||
.text-danger { color: #e63946; }
|
|
||||||
.text-success { color: #2a9d8f; }
|
|
||||||
|
|
||||||
/* Filter Bar */
|
|
||||||
.filter-bar {
|
|
||||||
background: var(--glass-bg);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.filter-bar select, .filter-bar input {
|
|
||||||
padding: 8px;
|
|
||||||
background: rgba(0,0,0,0.3);
|
|
||||||
border: 1px solid var(--glass-border);
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB |
118
car_detail.php
@ -1,118 +0,0 @@
|
|||||||
<?php
|
|
||||||
// car_detail.php
|
|
||||||
require_once 'includes/auth.php';
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
global $pdo;
|
|
||||||
|
|
||||||
$id = $_GET['id'] ?? 0;
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM cars WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
$car = $stmt->fetch();
|
|
||||||
|
|
||||||
if (!$car) {
|
|
||||||
die("<div class='container mt-5'><h1>Car not found</h1><a href='marketplace.php'>Back to Marketplace</a></div>");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch Branch Info
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM branches WHERE id = ?");
|
|
||||||
$stmt->execute([$car['branch_id']]);
|
|
||||||
$branch = $stmt->fetch();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
|
||||||
<a href="marketplace.php" class="btn-outline" style="margin-bottom: 20px; display: inline-block;">← Back to Marketplace</a>
|
|
||||||
|
|
||||||
<div class="grid" style="grid-template-columns: 2fr 1fr; gap: 40px;">
|
|
||||||
<!-- Left Column: Image & Desc -->
|
|
||||||
<div>
|
|
||||||
<div class="card" style="padding: 0; margin-bottom: 20px;">
|
|
||||||
<img src="<?= htmlspecialchars($car['image_path'] ?? $car['image_url'] ?? '') ?>" alt="<?= htmlspecialchars($car['brand']) ?>" style="height: auto; max-height: 500px;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="padding: 30px;">
|
|
||||||
<h2>Vehicle Description</h2>
|
|
||||||
<p><?= nl2br(htmlspecialchars($car['description'])) ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: Details & Actions -->
|
|
||||||
<div>
|
|
||||||
<div class="card" style="padding: 30px;">
|
|
||||||
<h1 style="color: var(--primary); margin-bottom: 10px;">
|
|
||||||
<?= htmlspecialchars($car['year'] . ' ' . $car['brand'] . ' ' . $car['model']) ?>
|
|
||||||
</h1>
|
|
||||||
<div style="font-size: 2rem; font-weight: bold; margin-bottom: 20px;">
|
|
||||||
$<?= number_format((float)$car['price']) ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table style="margin-bottom: 30px;">
|
|
||||||
<tr><th>Mileage</th><td><?= number_format((float)$car['mileage']) ?> km</td></tr>
|
|
||||||
<tr><th>Fuel</th><td><?= htmlspecialchars($car['fuel_type']) ?></td></tr>
|
|
||||||
<tr><th>Transmission</th><td><?= htmlspecialchars($car['transmission']) ?></td></tr>
|
|
||||||
<tr><th>Status</th><td><?= htmlspecialchars(ucfirst($car['status'])) ?></td></tr>
|
|
||||||
<tr><th>Location</th><td><?= htmlspecialchars($branch['city'] ?? 'Unknown') ?></td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<a href="contact.php?inquiry=<?= $car['id'] ?>" class="btn" style="width: 100%; margin-bottom: 10px;">Request to Buy</a>
|
|
||||||
<button onclick="document.getElementById('calc-section').scrollIntoView({behavior: 'smooth'})" class="btn-outline" style="width: 100%;">Calculate Installments</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="padding: 20px; margin-top: 20px;">
|
|
||||||
<h3>Branch Contact</h3>
|
|
||||||
<p><strong><?= htmlspecialchars($branch['name'] ?? '') ?></strong></p>
|
|
||||||
<p><?= htmlspecialchars($branch['address'] ?? '') ?></p>
|
|
||||||
<p><?= htmlspecialchars($branch['phone'] ?? '') ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Installment Calculator for this car -->
|
|
||||||
<div id="calc-section" class="card mt-5" style="padding: 40px;">
|
|
||||||
<h2>Installment Calculator</h2>
|
|
||||||
<div class="grid" style="grid-template-columns: 1fr 1fr 1fr;">
|
|
||||||
<div>
|
|
||||||
<label>Vehicle Price</label>
|
|
||||||
<input type="text" class="form-control" value="$<?= number_format((float)$car['price']) ?>" disabled>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Down Payment ($)</label>
|
|
||||||
<input type="number" id="downPayment" class="form-control" value="<?= (float)$car['price'] * 0.2 ?>" oninput="calc()">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Term</label>
|
|
||||||
<select id="term" class="form-control" onchange="calc()">
|
|
||||||
<option value="12">12 Months</option>
|
|
||||||
<option value="24" selected>24 Months</option>
|
|
||||||
<option value="36">36 Months</option>
|
|
||||||
<option value="48">48 Months</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 20px; font-size: 1.5rem; text-align: center;">
|
|
||||||
Estimated Monthly Payment: <span id="monthlyPayment" style="color: var(--primary); font-weight: bold;">---</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function calc() {
|
|
||||||
const price = <?= (float)$car['price'] ?>;
|
|
||||||
const down = parseFloat(document.getElementById('downPayment').value) || 0;
|
|
||||||
const months = parseInt(document.getElementById('term').value);
|
|
||||||
|
|
||||||
const principal = price - down;
|
|
||||||
if (principal < 0) {
|
|
||||||
document.getElementById('monthlyPayment').innerText = "Down payment exceeds price!";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const interest = 0.05; // 5% flat
|
|
||||||
const total = principal * (1 + interest);
|
|
||||||
const monthly = total / months;
|
|
||||||
|
|
||||||
document.getElementById('monthlyPayment').innerText = "$" + monthly.toFixed(2);
|
|
||||||
}
|
|
||||||
calc(); // Run on load
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
86
contact.php
@ -1,86 +0,0 @@
|
|||||||
<?php
|
|
||||||
// contact.php
|
|
||||||
require_once 'includes/auth.php';
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
global $pdo;
|
|
||||||
|
|
||||||
$msg = '';
|
|
||||||
$inquiry_car = null;
|
|
||||||
|
|
||||||
if (isset($_GET['inquiry'])) {
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM cars WHERE id = ?");
|
|
||||||
$stmt->execute([$_GET['inquiry']]);
|
|
||||||
$inquiry_car = $stmt->fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
// Process form (Demo only - just save to DB or show success)
|
|
||||||
$name = $_POST['name'];
|
|
||||||
$email = $_POST['email'];
|
|
||||||
$message = $_POST['message'];
|
|
||||||
$car_id = $_POST['car_id'] ?? null;
|
|
||||||
|
|
||||||
// Use the built-in mail service if desired, or just save to DB
|
|
||||||
// For this "100% offline" task, saving to DB is better.
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO inquiries (car_id, name, email, message) VALUES (?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$car_id, $name, $email, $message]);
|
|
||||||
|
|
||||||
$msg = "Thank you! Your message has been sent. We will contact you shortly.";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
|
||||||
<h1>Contact Us</h1>
|
|
||||||
<p class="mb-5">We are here to help you find your dream car.</p>
|
|
||||||
|
|
||||||
<?php if ($msg): ?>
|
|
||||||
<div style="background: rgba(42, 157, 143, 0.2); color: #2a9d8f; padding: 20px; border-radius: 10px; margin-bottom: 30px;">
|
|
||||||
<?= htmlspecialchars($msg) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 50px;">
|
|
||||||
<div>
|
|
||||||
<form method="POST">
|
|
||||||
<?php if ($inquiry_car): ?>
|
|
||||||
<div style="background: rgba(255,255,255,0.05); padding: 15px; border-radius: 5px; margin-bottom: 20px;">
|
|
||||||
<strong>Inquiry about:</strong> <?= htmlspecialchars($inquiry_car['year'] . ' ' . $inquiry_car['brand'] . ' ' . $inquiry_car['model']) ?>
|
|
||||||
<input type="hidden" name="car_id" value="<?= $inquiry_car['id'] ?>">
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Your Name</label>
|
|
||||||
<input type="text" name="name" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Email Address</label>
|
|
||||||
<input type="email" name="email" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Message</label>
|
|
||||||
<textarea name="message" class="form-control" rows="5" required><?= $inquiry_car ? "I am interested in this vehicle. Please provide more details." : "" ?></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn">Send Message</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3>Head Office</h3>
|
|
||||||
<p class="mb-5">Shar-e-Naw, Kabul, Afghanistan</p>
|
|
||||||
|
|
||||||
<h3>Phone</h3>
|
|
||||||
<p class="mb-5">+93 700 000 000</p>
|
|
||||||
|
|
||||||
<h3>Email</h3>
|
|
||||||
<p class="mb-5">info@afgcars.af</p>
|
|
||||||
|
|
||||||
<h3>Hours</h3>
|
|
||||||
<p>Sat - Thu: 8:00 AM - 6:00 PM</p>
|
|
||||||
<p>Friday: Closed</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
@ -1,22 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
// db/config.php
|
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||||
|
|
||||||
// Database Configuration
|
|
||||||
define('DB_HOST', '127.0.0.1');
|
define('DB_HOST', '127.0.0.1');
|
||||||
define('DB_NAME', 'app_38474');
|
define('DB_NAME', 'app_38474');
|
||||||
define('DB_USER', 'app_38474');
|
define('DB_USER', 'app_38474');
|
||||||
define('DB_PASS', '31621ed0-58d1-46b5-b7d0-9eb1e1abacf7');
|
define('DB_PASS', '31621ed0-58d1-46b5-b7d0-9eb1e1abacf7');
|
||||||
|
|
||||||
try {
|
function db() {
|
||||||
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS);
|
static $pdo;
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
if (!$pdo) {
|
||||||
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||||
} catch (PDOException $e) {
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
die("Database Connection Failed: " . $e->getMessage());
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global helper function if needed, but we can just use $pdo
|
|
||||||
function getDB() {
|
|
||||||
global $pdo;
|
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
-- Database Schema for AFG_CARS Enterprise System
|
|
||||||
SET FOREIGN_KEY_CHECKS=0;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `installments`;
|
|
||||||
DROP TABLE IF EXISTS `sales`;
|
|
||||||
DROP TABLE IF EXISTS `inquiries`;
|
|
||||||
DROP TABLE IF EXISTS `cars`;
|
|
||||||
DROP TABLE IF EXISTS `branches`;
|
|
||||||
DROP TABLE IF EXISTS `users`;
|
|
||||||
|
|
||||||
-- Users Table
|
|
||||||
CREATE TABLE `users` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`name` VARCHAR(100) NOT NULL,
|
|
||||||
`email` VARCHAR(100) NOT NULL UNIQUE,
|
|
||||||
`password` VARCHAR(255) NOT NULL,
|
|
||||||
`role` ENUM('admin', 'user') DEFAULT 'user',
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Branches Table
|
|
||||||
CREATE TABLE `branches` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`name` VARCHAR(100) NOT NULL,
|
|
||||||
`city` VARCHAR(50) NOT NULL,
|
|
||||||
`address` TEXT,
|
|
||||||
`phone` VARCHAR(20)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Cars Table
|
|
||||||
CREATE TABLE `cars` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`branch_id` INT,
|
|
||||||
`brand` VARCHAR(50) NOT NULL,
|
|
||||||
`model` VARCHAR(50) NOT NULL,
|
|
||||||
`year` INT NOT NULL,
|
|
||||||
`price` DECIMAL(10, 2) NOT NULL,
|
|
||||||
`mileage` INT DEFAULT 0,
|
|
||||||
`fuel_type` VARCHAR(20) DEFAULT 'Petrol',
|
|
||||||
`transmission` VARCHAR(20) DEFAULT 'Automatic',
|
|
||||||
`description` TEXT,
|
|
||||||
`image_path` VARCHAR(255),
|
|
||||||
`status` ENUM('available', 'sold') DEFAULT 'available',
|
|
||||||
`is_featured` BOOLEAN DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (`branch_id`) REFERENCES `branches`(`id`) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Sales Table
|
|
||||||
CREATE TABLE `sales` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`car_id` INT NOT NULL,
|
|
||||||
`user_id` INT NOT NULL,
|
|
||||||
`sale_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`sale_price` DECIMAL(10, 2) NOT NULL,
|
|
||||||
FOREIGN KEY (`car_id`) REFERENCES `cars`(`id`),
|
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Installments Table
|
|
||||||
CREATE TABLE `installments` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`car_id` INT NOT NULL,
|
|
||||||
`user_id` INT NOT NULL,
|
|
||||||
`total_amount` DECIMAL(10, 2) NOT NULL,
|
|
||||||
`monthly_payment` DECIMAL(10, 2) NOT NULL,
|
|
||||||
`months` INT NOT NULL,
|
|
||||||
`status` ENUM('pending', 'approved', 'rejected', 'completed') DEFAULT 'pending',
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (`car_id`) REFERENCES `cars`(`id`),
|
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Inquiries/Contact Table
|
|
||||||
CREATE TABLE `inquiries` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`car_id` INT DEFAULT NULL,
|
|
||||||
`user_id` INT DEFAULT NULL,
|
|
||||||
`name` VARCHAR(100),
|
|
||||||
`email` VARCHAR(100),
|
|
||||||
`message` TEXT,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (`car_id`) REFERENCES `cars`(`id`) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS=1;
|
|
||||||
@ -1,148 +0,0 @@
|
|||||||
/*M!999999\- enable the sandbox mode */
|
|
||||||
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
|
|
||||||
--
|
|
||||||
-- Host: 127.0.0.1 Database: app_38474
|
|
||||||
-- ------------------------------------------------------
|
|
||||||
-- Server version 10.11.14-MariaDB-0+deb12u2
|
|
||||||
|
|
||||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
|
||||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
|
||||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
|
||||||
/*!40101 SET NAMES utf8mb4 */;
|
|
||||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
|
||||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
|
||||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
|
||||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
|
||||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
|
||||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Table structure for table `branches`
|
|
||||||
--
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `branches`;
|
|
||||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
|
||||||
/*!40101 SET character_set_client = utf8mb4 */;
|
|
||||||
CREATE TABLE `branches` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`name` varchar(100) NOT NULL,
|
|
||||||
`city` varchar(100) NOT NULL,
|
|
||||||
`address` varchar(255) DEFAULT NULL,
|
|
||||||
`phone` varchar(50) DEFAULT NULL,
|
|
||||||
`hours` varchar(100) DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `branches`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `branches` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `branches` DISABLE KEYS */;
|
|
||||||
INSERT INTO `branches` VALUES
|
|
||||||
(1,'Kabul Main','Kabul','Shar-e-Naw, Kabul','+93 700 111 222','08:00 AM - 06:00 PM'),
|
|
||||||
(2,'Herat Branch','Herat','Main Road, Herat','+93 700 333 444','08:30 AM - 05:30 PM'),
|
|
||||||
(3,'Mazar Center','Mazar-i-Sharif','Balkh Street, Mazar','+93 700 555 666','08:00 AM - 05:00 PM'),
|
|
||||||
(4,'Kandahar Hub','Kandahar','Airport Road, Kandahar','+93 700 777 888','09:00 AM - 04:00 PM');
|
|
||||||
/*!40000 ALTER TABLE `branches` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Table structure for table `cars`
|
|
||||||
--
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `cars`;
|
|
||||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
|
||||||
/*!40101 SET character_set_client = utf8mb4 */;
|
|
||||||
CREATE TABLE `cars` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`vin` varchar(50) NOT NULL,
|
|
||||||
`brand` varchar(100) NOT NULL,
|
|
||||||
`model` varchar(100) NOT NULL,
|
|
||||||
`year` int(11) NOT NULL,
|
|
||||||
`price` decimal(15,2) NOT NULL,
|
|
||||||
`mileage` int(11) NOT NULL,
|
|
||||||
`transmission` varchar(50) DEFAULT NULL,
|
|
||||||
`fuel_type` varchar(50) DEFAULT NULL,
|
|
||||||
`status` enum('Available','Reserved','Sold') DEFAULT 'Available',
|
|
||||||
`branch_id` int(11) DEFAULT NULL,
|
|
||||||
`is_featured` tinyint(1) DEFAULT 0,
|
|
||||||
`image_url` varchar(255) DEFAULT NULL,
|
|
||||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `vin` (`vin`),
|
|
||||||
KEY `branch_id` (`branch_id`),
|
|
||||||
CONSTRAINT `cars_ibfk_1` FOREIGN KEY (`branch_id`) REFERENCES `branches` (`id`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `cars`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `cars` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `cars` DISABLE KEYS */;
|
|
||||||
INSERT INTO `cars` VALUES
|
|
||||||
(1,'VIN0000000001','Tesla','Model X',2023,66020.00,8568,'Automatic','Hybrid','Available',4,1,'assets/images/cars/car1.jpg','2026-02-16 17:44:23'),
|
|
||||||
(2,'VIN0000000002','BMW','X7',2020,114269.00,8462,'Automatic','Hybrid','Available',4,1,'assets/images/cars/car2.jpg','2026-02-16 17:44:23'),
|
|
||||||
(3,'VIN0000000003','Mercedes-Benz','E-Class',2022,176551.00,14978,'Automatic','Gasoline','Available',1,1,'assets/images/cars/car3.jpg','2026-02-16 17:44:23'),
|
|
||||||
(4,'VIN0000000004','Tesla','Model S',2021,129987.00,5263,'Automatic','Gasoline','Available',1,1,'assets/images/cars/car4.jpg','2026-02-16 17:44:23'),
|
|
||||||
(5,'VIN0000000005','BMW','X7',2020,71914.00,2277,'Automatic','Hybrid','Available',4,1,'assets/images/cars/car5.jpg','2026-02-16 17:44:23'),
|
|
||||||
(6,'VIN0000000006','Land Rover','Defender',2024,53857.00,13809,'Automatic','Hybrid','Available',2,1,'assets/images/cars/car6.jpg','2026-02-16 17:44:23'),
|
|
||||||
(7,'VIN0000000007','Lexus','RX 350',2020,50362.00,8212,'Automatic','Gasoline','Available',3,1,'assets/images/cars/car7.jpg','2026-02-16 17:44:23'),
|
|
||||||
(8,'VIN0000000008','Lexus','LX 600',2020,157026.00,192,'Automatic','Hybrid','Available',1,1,'assets/images/cars/car8.jpg','2026-02-16 17:44:23'),
|
|
||||||
(9,'VIN0000000009','Lexus','ES 350',2022,149865.00,8073,'Automatic','Hybrid','Available',4,0,'assets/images/cars/car9.jpg','2026-02-16 17:44:23'),
|
|
||||||
(10,'VIN0000000010','Lexus','LX 600',2022,136955.00,14064,'Automatic','Hybrid','Available',2,0,'assets/images/cars/car10.jpg','2026-02-16 17:44:23'),
|
|
||||||
(11,'VIN0000000011','Toyota','Land Cruiser',2021,116083.00,12545,'Automatic','Gasoline','Available',1,0,'assets/images/cars/car11.jpg','2026-02-16 17:44:23'),
|
|
||||||
(12,'VIN0000000012','Mercedes-Benz','S-Class',2023,177233.00,2170,'Automatic','Hybrid','Available',2,0,'assets/images/cars/car12.jpg','2026-02-16 17:44:23'),
|
|
||||||
(13,'VIN0000000013','BMW','X7',2022,176162.00,2440,'Automatic','Hybrid','Available',3,0,'assets/images/cars/car13.jpg','2026-02-16 17:44:23'),
|
|
||||||
(14,'VIN0000000014','Porsche','Cayenne',2020,113840.00,13614,'Automatic','Gasoline','Available',4,0,'assets/images/cars/car14.jpg','2026-02-16 17:44:23'),
|
|
||||||
(15,'VIN0000000015','Tesla','Model S',2021,53508.00,10674,'Automatic','Gasoline','Available',4,0,'assets/images/cars/car15.jpg','2026-02-16 17:44:23'),
|
|
||||||
(16,'VIN0000000016','BMW','7 Series',2020,108898.00,5346,'Automatic','Hybrid','Available',4,0,'assets/images/cars/car16.jpg','2026-02-16 17:44:23'),
|
|
||||||
(17,'VIN0000000017','Mercedes-Benz','S-Class',2020,56037.00,12766,'Automatic','Hybrid','Available',3,0,'assets/images/cars/car17.jpg','2026-02-16 17:44:23'),
|
|
||||||
(18,'VIN0000000018','Toyota','RAV4',2024,150289.00,8590,'Automatic','Hybrid','Available',1,0,'assets/images/cars/car18.jpg','2026-02-16 17:44:23'),
|
|
||||||
(19,'VIN0000000019','Land Rover','Defender',2021,73639.00,1603,'Automatic','Hybrid','Available',2,0,'assets/images/cars/car19.jpg','2026-02-16 17:44:23'),
|
|
||||||
(20,'VIN0000000020','Toyota','Camry',2021,76785.00,13561,'Automatic','Hybrid','Available',3,0,'assets/images/cars/car20.jpg','2026-02-16 17:44:23');
|
|
||||||
/*!40000 ALTER TABLE `cars` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Table structure for table `users`
|
|
||||||
--
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `users`;
|
|
||||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
|
||||||
/*!40101 SET character_set_client = utf8mb4 */;
|
|
||||||
CREATE TABLE `users` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`username` varchar(100) NOT NULL,
|
|
||||||
`password` varchar(255) NOT NULL,
|
|
||||||
`role` enum('Guest','Customer','Dealer','Employee','Manager','Admin','Super Admin') DEFAULT 'Customer',
|
|
||||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `username` (`username`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Dumping data for table `users`
|
|
||||||
--
|
|
||||||
|
|
||||||
LOCK TABLES `users` WRITE;
|
|
||||||
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
|
|
||||||
INSERT INTO `users` VALUES
|
|
||||||
(1,'admin','$2y$10$QCCJTMtWMNo4VDc5azbJ2evY2xFdNkuJKrJPgrzWMp55mH6bTv1we','Super Admin','2026-02-16 17:44:23');
|
|
||||||
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
|
|
||||||
UNLOCK TABLES;
|
|
||||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
|
||||||
|
|
||||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
|
||||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
|
||||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
|
||||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
|
||||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
|
||||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
|
||||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
|
||||||
|
|
||||||
-- Dump completed on 2026-02-17 5:33:17
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
-- Branches Table
|
|
||||||
CREATE TABLE IF NOT EXISTS `branches` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`name` VARCHAR(100) NOT NULL,
|
|
||||||
`location` VARCHAR(255) NOT NULL,
|
|
||||||
`phone` VARCHAR(20),
|
|
||||||
`email` VARCHAR(100),
|
|
||||||
`manager_id` INT,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Car Images Table (Multiple Images)
|
|
||||||
CREATE TABLE IF NOT EXISTS `car_images` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`car_id` INT NOT NULL,
|
|
||||||
`image_url` VARCHAR(255) NOT NULL,
|
|
||||||
`is_primary` TINYINT(1) DEFAULT 0,
|
|
||||||
FOREIGN KEY (`car_id`) REFERENCES `cars`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Sales Table
|
|
||||||
CREATE TABLE IF NOT EXISTS `sales` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`car_id` INT NOT NULL,
|
|
||||||
`buyer_id` INT NOT NULL,
|
|
||||||
`seller_id` INT, -- Dealer or Admin who sold it
|
|
||||||
`sale_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`final_price` DECIMAL(15, 2) NOT NULL,
|
|
||||||
`payment_method` ENUM('Cash', 'Installment') DEFAULT 'Cash',
|
|
||||||
`status` ENUM('Pending', 'Completed', 'Cancelled') DEFAULT 'Pending',
|
|
||||||
FOREIGN KEY (`car_id`) REFERENCES `cars`(`id`),
|
|
||||||
FOREIGN KEY (`buyer_id`) REFERENCES `users`(`id`)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Installments Table
|
|
||||||
CREATE TABLE IF NOT EXISTS `installments` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`sale_id` INT NOT NULL,
|
|
||||||
`total_amount` DECIMAL(15, 2) NOT NULL,
|
|
||||||
`paid_amount` DECIMAL(15, 2) DEFAULT 0.00,
|
|
||||||
`monthly_payment` DECIMAL(15, 2) NOT NULL,
|
|
||||||
`due_date` DATE,
|
|
||||||
`status` ENUM('Active', 'Completed', 'Defaulted') DEFAULT 'Active',
|
|
||||||
FOREIGN KEY (`sale_id`) REFERENCES `sales`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Activity Logs
|
|
||||||
CREATE TABLE IF NOT EXISTS `activity_logs` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`user_id` INT,
|
|
||||||
`action` VARCHAR(255) NOT NULL,
|
|
||||||
`details` TEXT,
|
|
||||||
`ip_address` VARCHAR(45),
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Notifications
|
|
||||||
CREATE TABLE IF NOT EXISTS `notifications` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`user_id` INT NOT NULL,
|
|
||||||
`message` TEXT NOT NULL,
|
|
||||||
`is_read` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Update Cars Table
|
|
||||||
ALTER TABLE `cars` ADD COLUMN IF NOT EXISTS `dealer_id` INT DEFAULT NULL;
|
|
||||||
ALTER TABLE `cars` ADD COLUMN IF NOT EXISTS `installment_available` TINYINT(1) DEFAULT 0;
|
|
||||||
|
|
||||||
-- Update Users Table (Ensure role column is correct - strictly speaking it already exists but this is safe)
|
|
||||||
-- ALTER TABLE `users` MODIFY COLUMN `role` ENUM('Guest','Customer','Dealer','Employee','Manager','Admin','Super Admin') DEFAULT 'Customer';
|
|
||||||
@ -1 +0,0 @@
|
|||||||
2026-02-17 08:38:35
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
// includes/auth.php
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
|
|
||||||
// Check if user is logged in
|
|
||||||
function isLoggedIn() {
|
|
||||||
return isset($_SESSION['user_id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current user role
|
|
||||||
function getUserRole() {
|
|
||||||
return $_SESSION['user_role'] ?? 'guest';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current user name
|
|
||||||
function getUserName() {
|
|
||||||
return $_SESSION['user_name'] ?? 'Guest';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login function
|
|
||||||
function login($email, $password) {
|
|
||||||
global $pdo;
|
|
||||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
|
|
||||||
$stmt->execute([$email]);
|
|
||||||
$user = $stmt->fetch();
|
|
||||||
|
|
||||||
if ($user && password_verify($password, $user['password'])) {
|
|
||||||
$_SESSION['user_id'] = $user['id'];
|
|
||||||
$_SESSION['user_name'] = $user['name'];
|
|
||||||
$_SESSION['user_email'] = $user['email'];
|
|
||||||
$_SESSION['user_role'] = $user['role'];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout function
|
|
||||||
function logout() {
|
|
||||||
session_destroy();
|
|
||||||
header("Location: login.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
// includes/footer.php
|
|
||||||
?>
|
|
||||||
<footer>
|
|
||||||
<div class="container">
|
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); text-align: left;">
|
|
||||||
<div>
|
|
||||||
<h3 style="color: var(--primary); margin-bottom: 15px;">AFG CARS</h3>
|
|
||||||
<p>Afghanistan's premier automotive marketplace. Luxury, reliability, and excellence in every drive.</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 style="margin-bottom: 15px;">Quick Links</h3>
|
|
||||||
<ul style="line-height: 2;">
|
|
||||||
<li><a href="/index.php">Home</a></li>
|
|
||||||
<li><a href="/marketplace.php">Marketplace</a></li>
|
|
||||||
<li><a href="/work.php">How it Works</a></li>
|
|
||||||
<li><a href="/about.php">About Us</a></li>
|
|
||||||
<li><a href="/contact.php">Contact</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 style="margin-bottom: 15px;">Contact Info</h3>
|
|
||||||
<p>Shar-e-Naw, Kabul, Afghanistan</p>
|
|
||||||
<p>+93 700 000 000</p>
|
|
||||||
<p>info@afgcars.af</p>
|
|
||||||
<p>Sat - Thu: 8:00 AM - 6:00 PM</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 50px; padding-top: 20px; border-top: 1px solid var(--glass-border); text-align: center; color: #888;">
|
|
||||||
<p>© <?= date('Y') ?> AFG CARS - Elite Dealership System.</p>
|
|
||||||
<p style="font-size: 0.8rem; margin-top: 5px;">100% Offline | Enterprise Ready</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
// includes/header.php
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
|
||||||
session_start();
|
|
||||||
}
|
|
||||||
require_once __DIR__ . '/auth.php';
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>AFG CARS Enterprise</title>
|
|
||||||
<link rel="stylesheet" href="/assets/css/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<nav class="navbar">
|
|
||||||
<div class="container">
|
|
||||||
<a href="/index.php" class="logo">AFG CARS</a>
|
|
||||||
<div class="nav-links">
|
|
||||||
<a href="/index.php">Home</a>
|
|
||||||
<a href="/marketplace.php">Marketplace</a>
|
|
||||||
<a href="/about.php">About</a>
|
|
||||||
<a href="/work.php">Work</a>
|
|
||||||
<a href="/contact.php">Contact</a>
|
|
||||||
|
|
||||||
<?php if (isLoggedIn()): ?>
|
|
||||||
<?php if (getUserRole() === 'admin'): ?>
|
|
||||||
<a href="/admin/index.php" class="btn">Admin</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<a href="#" class="btn-outline">My Account</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a href="/logout.php" style="color: #e63946;">Logout</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<a href="/login.php" class="btn">Login</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
|
|
||||||
function ensure_db_setup() {
|
|
||||||
$flagFile = __DIR__ . '/../db/setup_done.flag';
|
|
||||||
if (file_exists($flagFile)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$db = db();
|
|
||||||
|
|
||||||
// Schema Creation (IF NOT EXISTS to be safe)
|
|
||||||
$db->exec("SET FOREIGN_KEY_CHECKS = 0");
|
|
||||||
|
|
||||||
// Branches
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS branches (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
city VARCHAR(100) NOT NULL,
|
|
||||||
address VARCHAR(255),
|
|
||||||
phone VARCHAR(50),
|
|
||||||
hours VARCHAR(100)
|
|
||||||
)");
|
|
||||||
|
|
||||||
// Users
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
username VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
email VARCHAR(100) UNIQUE,
|
|
||||||
password VARCHAR(255) NOT NULL,
|
|
||||||
role ENUM('Guest', 'Customer', 'Dealer', 'Employee', 'Manager', 'Admin', 'Super Admin') DEFAULT 'Customer',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)");
|
|
||||||
|
|
||||||
// Cars
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS cars (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
vin VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
brand VARCHAR(100) NOT NULL,
|
|
||||||
model VARCHAR(100) NOT NULL,
|
|
||||||
year INT NOT NULL,
|
|
||||||
price DECIMAL(15, 2) NOT NULL,
|
|
||||||
mileage INT NOT NULL,
|
|
||||||
transmission VARCHAR(50),
|
|
||||||
fuel_type VARCHAR(50),
|
|
||||||
status ENUM('Available', 'Reserved', 'Sold') DEFAULT 'Available',
|
|
||||||
branch_id INT,
|
|
||||||
dealer_id INT DEFAULT NULL,
|
|
||||||
installment_available BOOLEAN DEFAULT 0,
|
|
||||||
is_featured BOOLEAN DEFAULT 0,
|
|
||||||
image_url VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (branch_id) REFERENCES branches(id),
|
|
||||||
FOREIGN KEY (dealer_id) REFERENCES users(id)
|
|
||||||
)");
|
|
||||||
|
|
||||||
// Car Images
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS car_images (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
car_id INT NOT NULL,
|
|
||||||
image_path VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (car_id) REFERENCES cars(id) ON DELETE CASCADE
|
|
||||||
)");
|
|
||||||
|
|
||||||
// Reviews
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS reviews (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
car_id INT NOT NULL,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
rating INT NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
|
||||||
comment TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (car_id) REFERENCES cars(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
)");
|
|
||||||
|
|
||||||
// Sales
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS sales (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
car_id INT NOT NULL,
|
|
||||||
amount DECIMAL(15, 2) NOT NULL,
|
|
||||||
sale_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
status ENUM('Pending', 'Completed', 'Cancelled') DEFAULT 'Pending',
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (car_id) REFERENCES cars(id)
|
|
||||||
)");
|
|
||||||
|
|
||||||
// Installments
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS installments (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
sale_id INT NOT NULL,
|
|
||||||
total_amount DECIMAL(15, 2) NOT NULL,
|
|
||||||
paid_amount DECIMAL(15, 2) DEFAULT 0,
|
|
||||||
monthly_payment DECIMAL(15, 2) NOT NULL,
|
|
||||||
status ENUM('Active', 'Completed', 'Overdue') DEFAULT 'Active',
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (sale_id) REFERENCES sales(id) ON DELETE CASCADE
|
|
||||||
)");
|
|
||||||
|
|
||||||
// Activity Logs
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS activity_logs (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT,
|
|
||||||
action VARCHAR(255) NOT NULL,
|
|
||||||
ip_address VARCHAR(50),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
||||||
)");
|
|
||||||
|
|
||||||
// Notifications
|
|
||||||
$db->exec("CREATE TABLE IF NOT EXISTS notifications (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
is_read BOOLEAN DEFAULT 0,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
)");
|
|
||||||
|
|
||||||
$db->exec("SET FOREIGN_KEY_CHECKS = 1");
|
|
||||||
|
|
||||||
// Seeding (Only if empty)
|
|
||||||
$stmt = $db->query("SELECT COUNT(*) FROM branches");
|
|
||||||
if ($stmt->fetchColumn() == 0) {
|
|
||||||
$branches = [
|
|
||||||
['Kabul Main', 'Kabul', 'Shar-e-Naw, Kabul', '+93 700 111 222', '08:00 AM - 06:00 PM'],
|
|
||||||
['Herat Branch', 'Herat', 'Main Road, Herat', '+93 700 333 444', '08:30 AM - 05:30 PM'],
|
|
||||||
['Mazar Center', 'Mazar-i-Sharif', 'Balkh Street, Mazar', '+93 700 555 666', '08:00 AM - 05:00 PM'],
|
|
||||||
['Kandahar Hub', 'Kandahar', 'Airport Road, Kandahar', '+93 700 777 888', '09:00 AM - 04:00 PM']
|
|
||||||
];
|
|
||||||
$stmt = $db->prepare("INSERT INTO branches (name, city, address, phone, hours) VALUES (?, ?, ?, ?, ?)");
|
|
||||||
foreach ($branches as $branch) {
|
|
||||||
$stmt->execute($branch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $db->query("SELECT COUNT(*) FROM cars");
|
|
||||||
if ($stmt->fetchColumn() == 0) {
|
|
||||||
$brands = ['Toyota', 'Lexus', 'Mercedes-Benz', 'BMW', 'Audi', 'Land Rover', 'Porsche', 'Tesla'];
|
|
||||||
$models = [
|
|
||||||
'Toyota' => ['Camry', 'Land Cruiser', 'Corolla', 'RAV4'],
|
|
||||||
'Lexus' => ['LX 600', 'RX 350', 'ES 350'],
|
|
||||||
'Mercedes-Benz' => ['S-Class', 'G-Wagon', 'E-Class'],
|
|
||||||
'BMW' => ['X7', 'X5', '7 Series'],
|
|
||||||
'Audi' => ['Q8', 'A8', 'RS7'],
|
|
||||||
'Land Rover' => ['Defender', 'Range Rover'],
|
|
||||||
'Porsche' => ['911 Carrera', 'Cayenne'],
|
|
||||||
'Tesla' => ['Model S', 'Model X']
|
|
||||||
];
|
|
||||||
|
|
||||||
$stmt = $db->prepare("INSERT INTO cars (vin, brand, model, year, price, mileage, transmission, fuel_type, branch_id, is_featured, image_url, installment_available) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
|
|
||||||
for ($i = 1; $i <= 20; $i++) {
|
|
||||||
$brand = $brands[array_rand($brands)];
|
|
||||||
$model = $models[$brand][array_rand($models[$brand])];
|
|
||||||
$year = rand(2020, 2024);
|
|
||||||
$price = rand(45000, 180000);
|
|
||||||
$mileage = rand(0, 15000);
|
|
||||||
$branch_id = rand(1, 4);
|
|
||||||
$is_featured = ($i <= 8) ? 1 : 0;
|
|
||||||
$installment_available = rand(0, 1);
|
|
||||||
$image_url = "assets/images/cars/car{$i}.jpg";
|
|
||||||
$vin = "VIN" . str_pad((string)$i, 10, "0", STR_PAD_LEFT);
|
|
||||||
|
|
||||||
$stmt->execute([
|
|
||||||
$vin, $brand, $model, $year, $price, $mileage,
|
|
||||||
'Automatic', rand(0,1) ? 'Gasoline' : 'Hybrid',
|
|
||||||
$branch_id, $is_featured, $image_url, $installment_available
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $db->query("SELECT COUNT(*) FROM users");
|
|
||||||
if ($stmt->fetchColumn() == 0) {
|
|
||||||
$stmt = $db->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
|
|
||||||
$stmt->execute(['admin', password_hash('admin123', PASSWORD_DEFAULT), 'Super Admin']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create flag file to prevent re-running on every request
|
|
||||||
file_put_contents($flagFile, date('Y-m-d H:i:s'));
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log("DB Setup Failed: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
// includes/middleware.php
|
|
||||||
|
|
||||||
require_once __DIR__ . '/auth.php';
|
|
||||||
|
|
||||||
function requireLogin() {
|
|
||||||
if (!isLoggedIn()) {
|
|
||||||
header("Location: /login.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireAdmin() {
|
|
||||||
requireLogin();
|
|
||||||
if (getUserRole() !== 'admin') {
|
|
||||||
header("Location: /index.php"); // Or dashboard
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/auth.php';
|
|
||||||
|
|
||||||
function requireRole($allowed_roles) {
|
|
||||||
if (!is_array($allowed_roles)) {
|
|
||||||
$allowed_roles = [$allowed_roles];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLoggedIn()) {
|
|
||||||
header('Location: /login.php');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user_role = $_SESSION['role'] ?? 'Guest';
|
|
||||||
|
|
||||||
if (!in_array($user_role, $allowed_roles)) {
|
|
||||||
// Redirect based on their actual role or to home
|
|
||||||
switch ($user_role) {
|
|
||||||
case 'Admin':
|
|
||||||
case 'Super Admin':
|
|
||||||
header('Location: /admin/index.php');
|
|
||||||
break;
|
|
||||||
case 'Dealer':
|
|
||||||
header('Location: /dealer/index.php');
|
|
||||||
break;
|
|
||||||
case 'Customer':
|
|
||||||
case 'Buyer': // Assuming 'Buyer' is the role name from prompt
|
|
||||||
header('Location: /buyer/index.php');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
header('Location: /index.php');
|
|
||||||
}
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasRole($role) {
|
|
||||||
return isset($_SESSION['role']) && $_SESSION['role'] === $role;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSuperAdmin() {
|
|
||||||
return hasRole('Super Admin');
|
|
||||||
}
|
|
||||||
255
index.php
@ -1,117 +1,150 @@
|
|||||||
<?php
|
<?php
|
||||||
// index.php
|
declare(strict_types=1);
|
||||||
require_once 'includes/auth.php';
|
@ini_set('display_errors', '1');
|
||||||
require_once 'includes/header.php';
|
@error_reporting(E_ALL);
|
||||||
global $pdo;
|
@date_default_timezone_set('UTC');
|
||||||
|
|
||||||
// Fetch Featured Cars
|
$phpVersion = PHP_VERSION;
|
||||||
try {
|
$now = date('Y-m-d H:i:s');
|
||||||
$stmt = $pdo->query("SELECT * FROM cars WHERE is_featured = 1 LIMIT 6");
|
|
||||||
$featured_cars = $stmt->fetchAll();
|
|
||||||
|
|
||||||
// Fetch branches for selector
|
|
||||||
$branches = $pdo->query("SELECT * FROM branches")->fetchAll();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$featured_cars = [];
|
|
||||||
$branches = [];
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
|
<!doctype html>
|
||||||
<!-- Hero Section -->
|
<html lang="en">
|
||||||
<section class="hero">
|
<head>
|
||||||
<div class="container">
|
<meta charset="utf-8" />
|
||||||
<h1>Find Your Dream Car</h1>
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<p>Premium Vehicles. Flexible Installments. Nationwide Service.</p>
|
<title>New Style</title>
|
||||||
<a href="marketplace.php" class="btn">Browse Inventory</a>
|
<?php
|
||||||
</div>
|
// Read project preview data from environment
|
||||||
</section>
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
<!-- Installment Calculator Section -->
|
?>
|
||||||
<section class="container mt-5">
|
<?php if ($projectDescription): ?>
|
||||||
<h2 class="text-center">Installment Calculator</h2>
|
<!-- Meta description -->
|
||||||
<div class="calculator">
|
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
|
<!-- Open Graph meta tags -->
|
||||||
<div class="form-group">
|
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
<label>Car Price ($)</label>
|
<!-- Twitter meta tags -->
|
||||||
<input type="number" id="calcPrice" class="form-control" placeholder="30000" value="30000">
|
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
</div>
|
<?php endif; ?>
|
||||||
<div class="form-group">
|
<?php if ($projectImageUrl): ?>
|
||||||
<label>Down Payment ($)</label>
|
<!-- Open Graph image -->
|
||||||
<input type="number" id="calcDown" class="form-control" placeholder="5000" value="5000">
|
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
</div>
|
<!-- Twitter image -->
|
||||||
<div class="form-group">
|
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
<label>Loan Term</label>
|
<?php endif; ?>
|
||||||
<select id="calcMonths" class="form-control">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<option value="12">12 Months</option>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<option value="24" selected>24 Months</option>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<option value="36">36 Months</option>
|
<style>
|
||||||
<option value="48">48 Months</option>
|
:root {
|
||||||
</select>
|
--bg-color-start: #6a11cb;
|
||||||
</div>
|
--bg-color-end: #2575fc;
|
||||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
--text-color: #ffffff;
|
||||||
<button onclick="calculateInstallment()" class="btn" style="width: 100%;">Calculate</button>
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
</div>
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
</div>
|
}
|
||||||
<div id="calcResult" class="mt-5 text-center" style="font-size: 1.5rem; font-weight: bold; color: var(--primary);">
|
body {
|
||||||
Monthly Payment: $1,145.83
|
margin: 0;
|
||||||
</div>
|
font-family: 'Inter', sans-serif;
|
||||||
</div>
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
</section>
|
color: var(--text-color);
|
||||||
|
display: flex;
|
||||||
<!-- Featured Cars -->
|
justify-content: center;
|
||||||
<section class="container mt-5">
|
align-items: center;
|
||||||
<h2 class="text-center mb-5">Featured Vehicles</h2>
|
min-height: 100vh;
|
||||||
<div class="grid">
|
text-align: center;
|
||||||
<?php foreach ($featured_cars as $car): ?>
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||||
|
animation: bg-pan 20s linear infinite;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
@keyframes bg-pan {
|
||||||
|
0% { background-position: 0% 0%; }
|
||||||
|
100% { background-position: 100% 100%; }
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg-color);
|
||||||
|
border: 1px solid var(--card-border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.loader {
|
||||||
|
margin: 1.25rem auto 1.25rem;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px; height: 1px;
|
||||||
|
padding: 0; margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap; border: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<img src="<?= htmlspecialchars($car['image_path'] ?? $car['image_url'] ?? '') ?>" alt="<?= htmlspecialchars($car['brand']) ?>">
|
<h1>Analyzing your requirements and generating your website…</h1>
|
||||||
<div class="card-body">
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<h3 class="card-title"><?= htmlspecialchars($car['brand'] . ' ' . $car['model']) ?></h3>
|
<span class="sr-only">Loading…</span>
|
||||||
<div class="card-price">$<?= number_format((float)$car['price']) ?></div>
|
|
||||||
<div class="card-meta">
|
|
||||||
<span><?= $car['year'] ?></span> •
|
|
||||||
<span><?= number_format((float)$car['mileage']) ?> km</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
<a href="car_detail.php?id=<?= $car['id'] ?>" class="btn btn-outline" style="width:100%">View Details</a>
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
|
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
<footer>
|
||||||
<?php endforeach; ?>
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
</div>
|
</footer>
|
||||||
</section>
|
</body>
|
||||||
|
</html>
|
||||||
<!-- Branch Selector -->
|
|
||||||
<section class="container mt-5 mb-5">
|
|
||||||
<h2 class="text-center mb-5">Visit Our Branches</h2>
|
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
|
|
||||||
<?php foreach ($branches as $branch): ?>
|
|
||||||
<div class="card" style="padding: 20px; text-align: center;">
|
|
||||||
<h3 style="color: var(--primary); margin-bottom: 10px;"><?= htmlspecialchars($branch['city']) ?></h3>
|
|
||||||
<p style="margin-bottom: 5px;"><?= htmlspecialchars($branch['name']) ?></p>
|
|
||||||
<p style="color: #aaa;"><?= htmlspecialchars($branch['phone']) ?></p>
|
|
||||||
<a href="contact.php?branch=<?= $branch['id'] ?>" class="btn-outline" style="margin-top: 15px; display: inline-block;">View Map</a>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function calculateInstallment() {
|
|
||||||
const price = parseFloat(document.getElementById('calcPrice').value);
|
|
||||||
const down = parseFloat(document.getElementById('calcDown').value);
|
|
||||||
const months = parseInt(document.getElementById('calcMonths').value);
|
|
||||||
|
|
||||||
if (price && months) {
|
|
||||||
const principal = price - (down || 0);
|
|
||||||
const interestRate = 0.05; // 5% flat rate
|
|
||||||
const total = principal * (1 + interestRate);
|
|
||||||
const monthly = total / months;
|
|
||||||
|
|
||||||
document.getElementById('calcResult').innerText =
|
|
||||||
`Monthly Payment: $${monthly.toFixed(2)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
|
|||||||
64
login.php
@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
// login.php
|
|
||||||
require_once 'includes/auth.php';
|
|
||||||
|
|
||||||
if (isLoggedIn()) {
|
|
||||||
header("Location: index.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$email = $_POST['email'] ?? '';
|
|
||||||
$password = $_POST['password'] ?? '';
|
|
||||||
|
|
||||||
if (login($email, $password)) {
|
|
||||||
if (getUserRole() === 'admin') {
|
|
||||||
header("Location: admin/index.php");
|
|
||||||
} else {
|
|
||||||
header("Location: index.php");
|
|
||||||
}
|
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
$error = "Invalid email or password";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Login - AFG CARS</title>
|
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
|
||||||
</head>
|
|
||||||
<body style="display: flex; align-items: center; justify-content: center; min-height: 100vh;">
|
|
||||||
|
|
||||||
<div class="auth-box">
|
|
||||||
<h1 class="logo mb-5">AFG CARS</h1>
|
|
||||||
<h2 class="mb-5">Login</h2>
|
|
||||||
|
|
||||||
<?php if ($error): ?>
|
|
||||||
<div style="background: rgba(230, 57, 70, 0.2); color: #e63946; padding: 10px; border-radius: 5px; margin-bottom: 20px;">
|
|
||||||
<?= htmlspecialchars($error) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<form method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="email" name="email" class="form-control" placeholder="Email Address" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="password" name="password" class="form-control" placeholder="Password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn" style="width: 100%;">Sign In</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p class="mt-5">
|
|
||||||
<a href="index.php" style="color: #aaa;">Back to Home</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'includes/auth.php';
|
|
||||||
logout();
|
|
||||||
235
mail/MailService.php
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
<?php
|
||||||
|
// Minimal mail service for the workspace app (VM).
|
||||||
|
// Usage:
|
||||||
|
// require_once __DIR__ . '/MailService.php';
|
||||||
|
// // Generic:
|
||||||
|
// MailService::sendMail($to, $subject, $htmlBody, $textBody = null, $opts = []);
|
||||||
|
// // Contact form helper:
|
||||||
|
// MailService::sendContactMessage($name, $email, $message, $to = null, $subject = 'New contact form');
|
||||||
|
|
||||||
|
class MailService
|
||||||
|
{
|
||||||
|
// Universal mail sender (no attachments by design)
|
||||||
|
public static function sendMail($to, string $subject, string $htmlBody, ?string $textBody = null, array $opts = [])
|
||||||
|
{
|
||||||
|
$cfg = self::loadConfig();
|
||||||
|
|
||||||
|
$autoload = __DIR__ . '/../vendor/autoload.php';
|
||||||
|
if (file_exists($autoload)) {
|
||||||
|
require_once $autoload;
|
||||||
|
}
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
@require_once 'libphp-phpmailer/autoload.php';
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||||
|
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||||
|
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||||
|
}
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
@require_once 'PHPMailer/src/Exception.php';
|
||||||
|
@require_once 'PHPMailer/src/SMTP.php';
|
||||||
|
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||||
|
}
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
@require_once 'PHPMailer/Exception.php';
|
||||||
|
@require_once 'PHPMailer/SMTP.php';
|
||||||
|
@require_once 'PHPMailer/PHPMailer.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||||
|
try {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $cfg['smtp_host'] ?? '';
|
||||||
|
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
|
||||||
|
$secure = $cfg['smtp_secure'] ?? 'tls';
|
||||||
|
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||||||
|
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
else $mail->SMTPSecure = false;
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $cfg['smtp_user'] ?? '';
|
||||||
|
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||||
|
|
||||||
|
$fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost');
|
||||||
|
$fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? 'App');
|
||||||
|
$mail->setFrom($fromEmail, $fromName);
|
||||||
|
if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$mail->addReplyTo($opts['reply_to']);
|
||||||
|
} elseif (!empty($cfg['reply_to'])) {
|
||||||
|
$mail->addReplyTo($cfg['reply_to']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipients
|
||||||
|
$toList = [];
|
||||||
|
if ($to) {
|
||||||
|
if (is_string($to)) $toList = array_map('trim', explode(',', $to));
|
||||||
|
elseif (is_array($to)) $toList = $to;
|
||||||
|
} elseif (!empty(getenv('MAIL_TO'))) {
|
||||||
|
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
|
||||||
|
}
|
||||||
|
$added = 0;
|
||||||
|
foreach ($toList as $addr) {
|
||||||
|
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { $mail->addAddress($addr); $added++; }
|
||||||
|
}
|
||||||
|
if ($added === 0) {
|
||||||
|
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((array)($opts['cc'] ?? []) as $cc) { if (filter_var($cc, FILTER_VALIDATE_EMAIL)) $mail->addCC($cc); }
|
||||||
|
foreach ((array)($opts['bcc'] ?? []) as $bcc){ if (filter_var($bcc, FILTER_VALIDATE_EMAIL)) $mail->addBCC($bcc); }
|
||||||
|
|
||||||
|
// Optional DKIM
|
||||||
|
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
|
||||||
|
$mail->DKIM_domain = $cfg['dkim_domain'];
|
||||||
|
$mail->DKIM_selector = $cfg['dkim_selector'];
|
||||||
|
$mail->DKIM_private = $cfg['dkim_private_key_path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->Subject = $subject;
|
||||||
|
$mail->Body = $htmlBody;
|
||||||
|
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
|
||||||
|
$ok = $mail->send();
|
||||||
|
return [ 'success' => $ok ];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static function loadConfig(): array
|
||||||
|
{
|
||||||
|
$configPath = __DIR__ . '/config.php';
|
||||||
|
if (!file_exists($configPath)) {
|
||||||
|
throw new \RuntimeException('Mail config not found. Copy mail/config.sample.php to mail/config.php and fill in credentials.');
|
||||||
|
}
|
||||||
|
$cfg = require $configPath;
|
||||||
|
if (!is_array($cfg)) {
|
||||||
|
throw new \RuntimeException('Invalid mail config format: expected array');
|
||||||
|
}
|
||||||
|
return $cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a contact message
|
||||||
|
// $to can be: a single email string, a comma-separated list, an array of emails, or null (fallback to MAIL_TO/MAIL_FROM)
|
||||||
|
public static function sendContactMessage(string $name, string $email, string $message, $to = null, string $subject = 'New contact form')
|
||||||
|
{
|
||||||
|
$cfg = self::loadConfig();
|
||||||
|
|
||||||
|
// Try Composer autoload if available (for PHPMailer)
|
||||||
|
$autoload = __DIR__ . '/../vendor/autoload.php';
|
||||||
|
if (file_exists($autoload)) {
|
||||||
|
require_once $autoload;
|
||||||
|
}
|
||||||
|
// Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer)
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
// Debian/Ubuntu package layout (libphp-phpmailer)
|
||||||
|
@require_once 'libphp-phpmailer/autoload.php';
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
@require_once 'libphp-phpmailer/src/Exception.php';
|
||||||
|
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||||
|
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||||
|
}
|
||||||
|
// Alternative layout (older PHPMailer package names)
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
@require_once 'PHPMailer/src/Exception.php';
|
||||||
|
@require_once 'PHPMailer/src/SMTP.php';
|
||||||
|
@require_once 'PHPMailer/src/PHPMailer.php';
|
||||||
|
}
|
||||||
|
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
@require_once 'PHPMailer/Exception.php';
|
||||||
|
@require_once 'PHPMailer/SMTP.php';
|
||||||
|
@require_once 'PHPMailer/PHPMailer.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$transport = $cfg['transport'] ?? 'smtp';
|
||||||
|
if ($transport === 'smtp' && class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
||||||
|
return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: attempt native mail() — works only if MTA is configured on the VM
|
||||||
|
return self::sendViaNativeMail($cfg, $name, $email, $message, $to, $subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function sendViaPHPMailer(array $cfg, string $name, string $email, string $body, $to, string $subject)
|
||||||
|
{
|
||||||
|
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||||
|
try {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $cfg['smtp_host'] ?? '';
|
||||||
|
$mail->Port = (int)($cfg['smtp_port'] ?? 587);
|
||||||
|
$secure = $cfg['smtp_secure'] ?? 'tls';
|
||||||
|
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS;
|
||||||
|
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
else $mail->SMTPSecure = false;
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $cfg['smtp_user'] ?? '';
|
||||||
|
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||||
|
|
||||||
|
$fromEmail = $cfg['from_email'] ?? 'no-reply@localhost';
|
||||||
|
$fromName = $cfg['from_name'] ?? 'App';
|
||||||
|
$mail->setFrom($fromEmail, $fromName);
|
||||||
|
|
||||||
|
// Use Reply-To for the user's email to avoid spoofing From
|
||||||
|
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$mail->addReplyTo($email, $name ?: $email);
|
||||||
|
}
|
||||||
|
if (!empty($cfg['reply_to'])) {
|
||||||
|
$mail->addReplyTo($cfg['reply_to']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destination: prefer dynamic recipients ($to), fallback to MAIL_TO; no silent FROM fallback
|
||||||
|
$toList = [];
|
||||||
|
if ($to) {
|
||||||
|
if (is_string($to)) {
|
||||||
|
// allow comma-separated list
|
||||||
|
$toList = array_map('trim', explode(',', $to));
|
||||||
|
} elseif (is_array($to)) {
|
||||||
|
$toList = $to;
|
||||||
|
}
|
||||||
|
} elseif (!empty(getenv('MAIL_TO'))) {
|
||||||
|
$toList = array_map('trim', explode(',', getenv('MAIL_TO')));
|
||||||
|
}
|
||||||
|
$added = 0;
|
||||||
|
foreach ($toList as $addr) {
|
||||||
|
if (filter_var($addr, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$mail->addAddress($addr);
|
||||||
|
$added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($added === 0) {
|
||||||
|
return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// DKIM (optional)
|
||||||
|
if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) {
|
||||||
|
$mail->DKIM_domain = $cfg['dkim_domain'];
|
||||||
|
$mail->DKIM_selector = $cfg['dkim_selector'];
|
||||||
|
$mail->DKIM_private = $cfg['dkim_private_key_path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->Subject = $subject;
|
||||||
|
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
$safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||||
|
$mail->Body = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
|
||||||
|
$mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}";
|
||||||
|
|
||||||
|
$ok = $mail->send();
|
||||||
|
return [ 'success' => $ok ];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function sendViaNativeMail(array $cfg, string $name, string $email, string $body, $to, string $subject)
|
||||||
|
{
|
||||||
|
$opts = ['reply_to' => $email];
|
||||||
|
$html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||||
|
return self::sendMail($to, $subject, $html, $body, $opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
mail/config.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
// Mail configuration sourced from environment variables.
|
||||||
|
// No secrets are stored here; the file just maps env -> config array for MailService.
|
||||||
|
|
||||||
|
function env_val(string $key, $default = null) {
|
||||||
|
$v = getenv($key);
|
||||||
|
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if critical vars are missing from process env, try to parse executor/.env
|
||||||
|
// This helps in web/Apache contexts where .env is not exported.
|
||||||
|
// Supports simple KEY=VALUE lines; ignores quotes and comments.
|
||||||
|
function load_dotenv_if_needed(array $keys): void {
|
||||||
|
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
||||||
|
if (empty($missing)) return;
|
||||||
|
static $loaded = false;
|
||||||
|
if ($loaded) return;
|
||||||
|
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
||||||
|
if ($envPath && is_readable($envPath)) {
|
||||||
|
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if ($line[0] === '#' || trim($line) === '') continue;
|
||||||
|
if (!str_contains($line, '=')) continue;
|
||||||
|
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||||
|
// Strip potential surrounding quotes
|
||||||
|
$v = trim($v, "\"' ");
|
||||||
|
// Do not override existing env
|
||||||
|
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
||||||
|
putenv("{$k}={$v}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load_dotenv_if_needed([
|
||||||
|
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS',
|
||||||
|
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO',
|
||||||
|
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transport = env_val('MAIL_TRANSPORT', 'smtp');
|
||||||
|
$smtp_host = env_val('SMTP_HOST');
|
||||||
|
$smtp_port = (int) env_val('SMTP_PORT', 587);
|
||||||
|
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
|
||||||
|
$smtp_user = env_val('SMTP_USER');
|
||||||
|
$smtp_pass = env_val('SMTP_PASS');
|
||||||
|
|
||||||
|
$from_email = env_val('MAIL_FROM', 'no-reply@localhost');
|
||||||
|
$from_name = env_val('MAIL_FROM_NAME', 'App');
|
||||||
|
$reply_to = env_val('MAIL_REPLY_TO');
|
||||||
|
|
||||||
|
$dkim_domain = env_val('DKIM_DOMAIN');
|
||||||
|
$dkim_selector = env_val('DKIM_SELECTOR');
|
||||||
|
$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'transport' => $transport,
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
'smtp_host' => $smtp_host,
|
||||||
|
'smtp_port' => $smtp_port,
|
||||||
|
'smtp_secure' => $smtp_secure,
|
||||||
|
'smtp_user' => $smtp_user,
|
||||||
|
'smtp_pass' => $smtp_pass,
|
||||||
|
|
||||||
|
// From / Reply-To
|
||||||
|
'from_email' => $from_email,
|
||||||
|
'from_name' => $from_name,
|
||||||
|
'reply_to' => $reply_to,
|
||||||
|
|
||||||
|
// DKIM (optional)
|
||||||
|
'dkim_domain' => $dkim_domain,
|
||||||
|
'dkim_selector' => $dkim_selector,
|
||||||
|
'dkim_private_key_path' => $dkim_private_key_path,
|
||||||
|
];
|
||||||
101
marketplace.php
@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
// marketplace.php
|
|
||||||
require_once 'includes/auth.php';
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
global $pdo;
|
|
||||||
|
|
||||||
// Fetch unique filter values
|
|
||||||
try {
|
|
||||||
$brands = $pdo->query("SELECT DISTINCT brand FROM cars ORDER BY brand")->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
$years = $pdo->query("SELECT DISTINCT year FROM cars ORDER BY year DESC")->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$brands = [];
|
|
||||||
$years = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build Query
|
|
||||||
$where = ["status = 'available'"];
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
if (!empty($_GET['brand'])) {
|
|
||||||
$where[] = "brand = ?";
|
|
||||||
$params[] = $_GET['brand'];
|
|
||||||
}
|
|
||||||
if (!empty($_GET['year'])) {
|
|
||||||
$where[] = "year = ?";
|
|
||||||
$params[] = $_GET['year'];
|
|
||||||
}
|
|
||||||
if (!empty($_GET['max_price'])) {
|
|
||||||
$where[] = "price <= ?";
|
|
||||||
$params[] = $_GET['max_price'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = "SELECT * FROM cars WHERE " . implode(" AND ", $where) . " ORDER BY created_at DESC";
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$cars = $stmt->fetchAll();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$cars = [];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
|
||||||
<h1>Marketplace</h1>
|
|
||||||
<p class="mb-5">Browse our premium selection of vehicles.</p>
|
|
||||||
|
|
||||||
<!-- Filter Bar -->
|
|
||||||
<form method="GET" class="filter-bar">
|
|
||||||
<select name="brand">
|
|
||||||
<option value="">All Brands</option>
|
|
||||||
<?php foreach ($brands as $b): ?>
|
|
||||||
<option value="<?= htmlspecialchars($b) ?>" <?= (isset($_GET['brand']) && $_GET['brand'] == $b) ? 'selected' : '' ?>>
|
|
||||||
<?= htmlspecialchars($b) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select name="year">
|
|
||||||
<option value="">All Years</option>
|
|
||||||
<?php foreach ($years as $y): ?>
|
|
||||||
<option value="<?= htmlspecialchars($y) ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
|
|
||||||
<?= htmlspecialchars($y) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<input type="number" name="max_price" placeholder="Max Price" value="<?= htmlspecialchars($_GET['max_price'] ?? '') ?>">
|
|
||||||
|
|
||||||
<button type="submit" class="btn">Filter</button>
|
|
||||||
<a href="marketplace.php" class="btn btn-outline" style="border: none; color: white;">Reset</a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="grid">
|
|
||||||
<?php if (count($cars) > 0): ?>
|
|
||||||
<?php foreach ($cars as $car): ?>
|
|
||||||
<div class="card">
|
|
||||||
<img src="<?= htmlspecialchars($car['image_path'] ?? $car['image_url'] ?? '') ?>" alt="<?= htmlspecialchars($car['brand']) ?>">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title">
|
|
||||||
<?= htmlspecialchars($car['brand'] . ' ' . $car['model']) ?>
|
|
||||||
<?php if ($car['is_featured']): ?>
|
|
||||||
<span class="badge" style="float: right;">Featured</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</h3>
|
|
||||||
<div class="card-price">$<?= number_format((float)$car['price']) ?></div>
|
|
||||||
<div class="card-meta">
|
|
||||||
<?= $car['year'] ?> • <?= number_format((float)$car['mileage']) ?> km • <?= $car['fuel_type'] ?>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<a href="car_detail.php?id=<?= $car['id'] ?>" class="btn btn-outline" style="width:100%">View Details</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<p>No cars found matching your criteria.</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||
106
setup.php
@ -1,106 +0,0 @@
|
|||||||
<?php
|
|
||||||
// setup.php - Enterprise Setup Script
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
|
|
||||||
echo "<h1>AFG CARS Enterprise Setup</h1>";
|
|
||||||
|
|
||||||
try {
|
|
||||||
global $pdo; // Assumes db/config.php creates $pdo
|
|
||||||
|
|
||||||
echo "<h3>1. Initializing Database Schema...</h3>";
|
|
||||||
|
|
||||||
// Read SQL file
|
|
||||||
$sql_file = __DIR__ . '/db/database.sql';
|
|
||||||
if (!file_exists($sql_file)) {
|
|
||||||
throw new Exception("Database SQL file not found at: $sql_file");
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql_content = file_get_contents($sql_file);
|
|
||||||
|
|
||||||
// Split into individual queries (basic splitting by semicolon)
|
|
||||||
// Note: This is a simple splitter and might break on complex stored procedures, but sufficient for this schema.
|
|
||||||
$queries = explode(';', $sql_content);
|
|
||||||
|
|
||||||
foreach ($queries as $query) {
|
|
||||||
$query = trim($query);
|
|
||||||
if (!empty($query)) {
|
|
||||||
$pdo->exec($query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo "<p style='color:green'>Schema imported successfully.</p>";
|
|
||||||
|
|
||||||
echo "<h3>2. Seeding Data...</h3>";
|
|
||||||
|
|
||||||
// Seed Users (Admin & Customer)
|
|
||||||
// Check if admin exists to avoid duplicates if re-run
|
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM users WHERE email = ?");
|
|
||||||
$stmt->execute(['admin@afgcars.com']);
|
|
||||||
if ($stmt->fetchColumn() == 0) {
|
|
||||||
$password = password_hash('admin123', PASSWORD_DEFAULT);
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO users (name, email, password, role) VALUES (?, ?, ?, ?)");
|
|
||||||
$stmt->execute(['Administrator', 'admin@afgcars.com', $password, 'admin']);
|
|
||||||
echo "<p>Admin user created (admin@afgcars.com / admin123)</p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt->execute(['John Doe', 'user@example.com', password_hash('user123', PASSWORD_DEFAULT), 'user']);
|
|
||||||
echo "<p>Demo user created (user@example.com / user123)</p>";
|
|
||||||
|
|
||||||
// Seed Branches
|
|
||||||
// Tables are fresh from schema import, so no need to truncate
|
|
||||||
$branches = [
|
|
||||||
['Kabul Main', 'Kabul', 'Shar-e-Naw, Kabul', '+93 700 111 222'],
|
|
||||||
['Herat Branch', 'Herat', 'Main Road, Herat', '+93 700 333 444'],
|
|
||||||
['Mazar Center', 'Mazar-i-Sharif', 'Balkh Street, Mazar', '+93 700 555 666'],
|
|
||||||
['Kandahar Hub', 'Kandahar', 'Airport Road, Kandahar', '+93 700 777 888']
|
|
||||||
];
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO branches (name, city, address, phone) VALUES (?, ?, ?, ?)");
|
|
||||||
foreach ($branches as $branch) {
|
|
||||||
$stmt->execute($branch);
|
|
||||||
}
|
|
||||||
echo "<p>Branches seeded.</p>";
|
|
||||||
|
|
||||||
// Seed Cars
|
|
||||||
// $pdo->exec("SET FOREIGN_KEY_CHECKS=0");
|
|
||||||
// $pdo->exec("TRUNCATE TABLE cars");
|
|
||||||
// $pdo->exec("SET FOREIGN_KEY_CHECKS=1");
|
|
||||||
|
|
||||||
$brands = ['Toyota', 'Lexus', 'Mercedes-Benz', 'BMW', 'Audi', 'Land Rover', 'Porsche', 'Tesla'];
|
|
||||||
$models = [
|
|
||||||
'Toyota' => ['Camry', 'Land Cruiser', 'Corolla', 'RAV4'],
|
|
||||||
'Lexus' => ['LX 600', 'RX 350', 'ES 350'],
|
|
||||||
'Mercedes-Benz' => ['S-Class', 'G-Wagon', 'E-Class'],
|
|
||||||
'BMW' => ['X7', 'X5', '7 Series'],
|
|
||||||
'Audi' => ['Q8', 'A8', 'RS7'],
|
|
||||||
'Land Rover' => ['Defender', 'Range Rover'],
|
|
||||||
'Porsche' => ['911 Carrera', 'Cayenne'],
|
|
||||||
'Tesla' => ['Model S', 'Model X']
|
|
||||||
];
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO cars (brand, model, year, price, mileage, fuel_type, transmission, description, image_path, branch_id, is_featured, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
|
|
||||||
for ($i = 1; $i <= 20; $i++) {
|
|
||||||
$brand = $brands[array_rand($brands)];
|
|
||||||
$model = $models[$brand][array_rand($models[$brand])];
|
|
||||||
$year = rand(2020, 2024);
|
|
||||||
$price = rand(25000, 150000);
|
|
||||||
$mileage = rand(0, 50000);
|
|
||||||
$fuel = rand(0, 1) ? 'Petrol' : 'Hybrid';
|
|
||||||
$desc = "Premium condition $brand $model. Full options, well maintained.";
|
|
||||||
$image = "assets/images/cars/car{$i}.jpg";
|
|
||||||
$branch_id = rand(1, 4);
|
|
||||||
$is_featured = ($i <= 6) ? 1 : 0; // First 6 are featured
|
|
||||||
|
|
||||||
$stmt->execute([
|
|
||||||
$brand, $model, $year, $price, $mileage, $fuel, 'Automatic',
|
|
||||||
$desc, $image, $branch_id, $is_featured, 'available'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
echo "<p>20 Demo cars seeded.</p>";
|
|
||||||
|
|
||||||
echo "<h3>Setup Complete!</h3>";
|
|
||||||
echo "<p><a href='index.php'>Go to Homepage</a></p>";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
die("<h3 style='color:red'>Setup Failed: " . $e->getMessage() . "</h3>");
|
|
||||||
}
|
|
||||||
46
work.php
@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
// work.php
|
|
||||||
require_once 'includes/auth.php';
|
|
||||||
require_once 'includes/header.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
|
||||||
<div class="text-center mb-5">
|
|
||||||
<h1>How It Works</h1>
|
|
||||||
<p>Your journey to owning a luxury car in 4 simple steps.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); text-align: center; gap: 30px;">
|
|
||||||
<div class="card" style="padding: 30px;">
|
|
||||||
<div style="font-size: 3rem; color: var(--primary); font-weight: bold; margin-bottom: 20px;">1</div>
|
|
||||||
<h3>Browse</h3>
|
|
||||||
<p>Explore our extensive inventory of premium vehicles online or visit one of our branches.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="padding: 30px;">
|
|
||||||
<div style="font-size: 3rem; color: var(--primary); font-weight: bold; margin-bottom: 20px;">2</div>
|
|
||||||
<h3>Select</h3>
|
|
||||||
<p>Choose your dream car and customize your payment plan using our installment calculator.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="padding: 30px;">
|
|
||||||
<div style="font-size: 3rem; color: var(--primary); font-weight: bold; margin-bottom: 20px;">3</div>
|
|
||||||
<h3>Apply</h3>
|
|
||||||
<p>Submit a request online or in-person. Our team will process your application quickly.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="padding: 30px;">
|
|
||||||
<div style="font-size: 3rem; color: var(--primary); font-weight: bold; margin-bottom: 20px;">4</div>
|
|
||||||
<h3>Drive</h3>
|
|
||||||
<p>Once approved, sign the paperwork and drive away in your new vehicle.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-5 card" style="padding: 40px; text-align: center;">
|
|
||||||
<h2>Ready to get started?</h2>
|
|
||||||
<p class="mb-5">Browse our marketplace to find your perfect car today.</p>
|
|
||||||
<a href="marketplace.php" class="btn">View Inventory</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php require_once 'includes/footer.php'; ?>
|
|
||||||