راتاااااااا
This commit is contained in:
parent
20dd5c8f61
commit
3f48850ff5
1
.gemini/tmp/sales_chart_cache.json
Normal file
1
.gemini/tmp/sales_chart_cache.json
Normal file
@ -0,0 +1 @@
|
||||
{"labels":["1404-09"],"data":[2940000]}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
*/build/
|
||||
|
||||
# Ignore environment files
|
||||
.env
|
||||
|
||||
59
about.php
59
about.php
@ -3,59 +3,58 @@ $page_title = 'درباره ما';
|
||||
require_once 'includes/header.php';
|
||||
?>
|
||||
|
||||
<main class="container py-5">
|
||||
<div class="text-center mb-5" data-aos="fade-down">
|
||||
<h1 class="display-4 fw-bold">داستان آتیمه</h1>
|
||||
<div class="container py-5 my-5">
|
||||
<div class="section-title text-center mb-5" data-aos="fade-down">
|
||||
<h1>داستان آتیمه</h1>
|
||||
<p class="fs-5 text-muted">تلفیق هنر سنتی و طراحی مدرن</p>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="card bg-dark-2 border-0 shadow-lg rounded-4 overflow-hidden">
|
||||
<div class="row g-0">
|
||||
<div class="col-lg-6" data-aos="fade-right">
|
||||
<img src="assets/images/pexels/about-us-34942790.jpg" class="img-fluid h-100" alt="هنر چرمدوزی" style="object-fit: cover; min-height: 400px;">
|
||||
</div>
|
||||
<div class="col-lg-6 d-flex align-items-center" data-aos="fade-left">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h2 class="card-title fw-bold mb-3">باور ما</h2>
|
||||
<p class="fs-5 lh-lg">ما در آتیمه، به قدرت دستها و اصالت مواد اولیه باور داریم. داستان ما از یک کارگاه کوچک و عشقی عمیق به هنر چرمدوزی آغاز شد. هدف ما خلق آثاری است که نه تنها یک وسیله کاربردی، بلکه بخشی از داستان و استایل روزمره شما باشند؛ آثاری که با گذر زمان، زیباتر و شخصیتر میشوند.</p>
|
||||
<p class="fs-5 lh-lg mt-3">هر محصول، حاصل ساعتها کار دست هنرمندان ماهر و استفاده از بهترین و باکیفیتترین چرمهای طبیعی است. ما به جزئیات اهمیت میدهیم، از انتخاب نخ گرفته تا طراحی هر برش و دوخت. این تعهد به کیفیت، تضمین میکند که هر ساخته دست ما، اثری ماندگار و بیهمتا باشد.</p>
|
||||
<a href="shop.php" class="btn btn-primary mt-4">مشاهده مجموعه ما</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="about-card p-4 p-lg-5 mb-5" data-aos="fade-up">
|
||||
<div class="row g-0 align-items-center">
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-delay="100">
|
||||
<img src="assets/images/pexels/about-us-34942790.jpg" class="img-fluid about-image" alt="هنر چرمدوزی">
|
||||
</div>
|
||||
<div class="col-lg-6" data-aos="fade-left" data-aos-delay="200">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h2 class="fw-bold mb-4">باور ما</h2>
|
||||
<p class="lh-lg">ما در آتیمه، به قدرت دستها و اصالت مواد اولیه باور داریم. داستان ما از یک کارگاه کوچک و عشقی عمیق به هنر چرمدوزی آغاز شد. هدف ما خلق آثاری است که نه تنها یک وسیله کاربردی، بلکه بخشی از داستان و استایل روزمره شما باشند؛ آثاری که با گذر زمان، زیباتر و شخصیتر میشوند.</p>
|
||||
<p class="lh-lg mt-3">هر محصول، حاصل ساعتها کار دست هنرمندان ماهر و استفاده از بهترین و باکیفیتترین چرمهای طبیعی است. ما به جزئیات اهمیت میدهیم، از انتخاب نخ گرفته تا طراحی هر برش و دوخت. این تعهد به کیفیت، تضمین میکند که هر ساخته دست ما، اثری ماندگار و بیهمتا باشد.</p>
|
||||
<a href="shop.php" class="btn btn-primary mt-4">مجموعه ما را ببینید</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Section or Values -->
|
||||
<section class="py-5 mt-4">
|
||||
<div class="row text-center g-4">
|
||||
<section class="py-5">
|
||||
<div class="text-center mb-5" data-aos="fade-down">
|
||||
<h2 class="fw-bold">ارزشهای ما</h2>
|
||||
</div>
|
||||
<div class="row text-center g-4 justify-content-center">
|
||||
<div class="col-md-4" data-aos="fade-up" data-aos-delay="100">
|
||||
<div class="card bg-dark-2 border-secondary h-100 p-4 rounded-3">
|
||||
<i class="fas fa-gem fa-3x text-primary mb-3"></i>
|
||||
<div class="values-card h-100">
|
||||
<i class="fas fa-gem mb-3"></i>
|
||||
<h4 class="fw-bold">تعهد به کیفیت</h4>
|
||||
<p class="text-muted">استفاده از بهترین مواد اولیه و کنترل کیفی دقیق در تمام مراحل تولید.</p>
|
||||
<p class="text-muted px-3">استفاده از بهترین مواد اولیه و کنترل کیفی دقیق در تمام مراحل تولید.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4" data-aos="fade-up" data-aos-delay="200">
|
||||
<div class="card bg-dark-2 border-secondary h-100 p-4 rounded-3">
|
||||
<i class="fas fa-hand-holding-heart fa-3x text-primary mb-3"></i>
|
||||
<div class="values-card h-100">
|
||||
<i class="fas fa-hand-holding-heart mb-3"></i>
|
||||
<h4 class="fw-bold">هنر دست</h4>
|
||||
<p class="text-muted">تمام محصولات ما با عشق و دقت توسط هنرمندان ماهر ساخته میشوند.</p>
|
||||
<p class="text-muted px-3">تمام محصولات ما با عشق و دقت توسط هنرمندان ماهر ساخته میشوند.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4" data-aos="fade-up" data-aos-delay="300">
|
||||
<div class="card bg-dark-2 border-secondary h-100 p-4 rounded-3">
|
||||
<i class="fas fa-leaf fa-3x text-primary mb-3"></i>
|
||||
<div class="values-card h-100">
|
||||
<i class="fas fa-leaf mb-3"></i>
|
||||
<h4 class="fw-bold">طراحی ماندگار</h4>
|
||||
<p class="text-muted">خلق آثاری مدرن و در عین حال کلاسیک که هیچگاه از مد نمیافتند.</p>
|
||||
<p class="text-muted px-3">خلق آثاری مدرن و در عین حال کلاسیک که هیچگاه از مد نمیافتند.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
|
||||
// New header
|
||||
require_once __DIR__ . '/header.php';
|
||||
|
||||
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||
@ -11,69 +9,82 @@ if ($flash_message) {
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2">افزودن محصول جدید</h1>
|
||||
<a href="products.php" class="btn btn-secondary">انصراف</a>
|
||||
</div>
|
||||
<style>
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card shadow-sm" style="background-color: var(--admin-surface); border-color: var(--admin-border);">
|
||||
<div class="card-body p-4">
|
||||
<form action="handler.php?action=add" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">نام محصول</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">توضیحات</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="4" required></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="price" class="form-label">قیمت (تومان)</label>
|
||||
<input type="number" class="form-control" id="price" name="price" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="image" class="form-label">تصویر محصول</label>
|
||||
<input type="file" class="form-control" id="image" name="image" accept="image/*">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
|
||||
<input type="text" class="form-control" id="colors" name="colors" placeholder="مثال: #8B4513, #2C2C2C">
|
||||
<div class="form-text" style="color: var(--admin-text-secondary);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</div>
|
||||
</div>
|
||||
<div class="mb-4 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_featured" name="is_featured" value="1" style="border-color: var(--admin-border);">
|
||||
<label class="form-check-label" for="is_featured">این یک محصول ویژه است</label>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">افزودن محصول</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="admin-header">
|
||||
<h1>افزودن محصول جدید</h1>
|
||||
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form action="handler.php?action=add" method="post" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">نام محصول</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">توضیحات</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="5" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="price" class="form-label">قیمت (تومان)</label>
|
||||
<input type="number" class="form-control" id="price" name="price" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stock" class="form-label">موجودی</label>
|
||||
<input type="number" class="form-control" id="stock" name="stock" required value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="image" class="form-label">تصویر محصول</label>
|
||||
<input type="file" class="form-control" id="image" name="image" accept="image/*">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
|
||||
<input type="text" class="form-control" id="colors" name="colors" placeholder="مثال: #8B4513, #2C2C2C">
|
||||
<small style="color: var(--admin-text-muted);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" id="is_featured" name="is_featured" value="1" style="width: 20px; height: 20px;">
|
||||
<span>این یک محصول ویژه است</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="text-align: left; margin-top: 2rem;">
|
||||
<button type="submit" class="btn btn-primary">افزودن محصول</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page-specific scripts -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const style = getComputedStyle(document.body);
|
||||
<?php if ($flash_message): ?>
|
||||
Swal.fire({
|
||||
title: '<?php echo $flash_message["type"] === "success" ? "عالی" : "خطا"; ?>',
|
||||
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||
icon: '<?php echo $flash_message["type"]; ?>',
|
||||
confirmButtonText: 'باشه',
|
||||
background: 'var(--admin-surface)',
|
||||
color: 'var(--admin-text-primary)'
|
||||
background: style.getPropertyValue('--admin-surface'),
|
||||
color: style.getPropertyValue('--admin-text')
|
||||
});
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// New footer
|
||||
require_once __DIR__ . '/footer.php';
|
||||
?>
|
||||
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||
136
admin/api.php
Normal file
136
admin/api.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/auth_handler.php';
|
||||
|
||||
// Start the session to check for admin status
|
||||
if (!is_admin()) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// IMPORTANT: Close the session immediately after use to prevent locking.
|
||||
// This allows other concurrent requests from the same user to be processed.
|
||||
session_write_close();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
$pdo = db();
|
||||
|
||||
if ($action === 'get_sales_data') {
|
||||
require_once __DIR__ . '/../includes/jdf.php';
|
||||
|
||||
$cache_file = __DIR__ . '/cache/sales_chart.json';
|
||||
$cache_lifetime = 3600; // 1 hour
|
||||
|
||||
// Clear PHP's stat cache to ensure we get the most up-to-date file status
|
||||
clearstatcache();
|
||||
|
||||
if (file_exists($cache_file) && is_readable($cache_file) && (time() - filemtime($cache_file) < $cache_lifetime)) {
|
||||
$cached_data = file_get_contents($cache_file);
|
||||
// Verify that the cache content is a valid JSON
|
||||
if ($cached_data && json_decode($cached_data) !== null) {
|
||||
header('X-Cache: HIT');
|
||||
echo $cached_data;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// CACHE MISS: Regenerate the data
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
YEAR(created_at) as year,
|
||||
MONTH(created_at) as month,
|
||||
SUM(total_amount) as total_sales
|
||||
FROM orders
|
||||
WHERE status = 'Delivered'
|
||||
GROUP BY year, month
|
||||
ORDER BY year ASC, month ASC
|
||||
");
|
||||
$stmt->execute();
|
||||
$sales_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$labels = [];
|
||||
$data = [];
|
||||
foreach ($sales_data as $row) {
|
||||
$jalali_date = gregorian_to_jalali($row['year'], $row['month'], 1);
|
||||
$labels[] = $jalali_date[0] . '-' . str_pad($jalali_date[1], 2, '0', STR_PAD_LEFT);
|
||||
$data[] = (float)$row['total_sales'];
|
||||
}
|
||||
|
||||
$response_data = json_encode(['labels' => $labels, 'data' => $data]);
|
||||
|
||||
// Atomic Write Operation
|
||||
$cache_dir = dirname($cache_file);
|
||||
if (!is_dir($cache_dir)) {
|
||||
mkdir($cache_dir, 0755, true);
|
||||
}
|
||||
$temp_file = $cache_file . '.' . uniqid() . '.tmp';
|
||||
if (file_put_contents($temp_file, $response_data) !== false) {
|
||||
// If rename fails, the old (possibly stale) cache will be used, which is acceptable.
|
||||
// The temp file will be cleaned up on subsequent runs or by a cron job.
|
||||
rename($temp_file, $cache_file);
|
||||
}
|
||||
|
||||
header('X-Cache: MISS');
|
||||
echo $response_data;
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
error_log("FATAL: DB Exception during sales data generation: " . $e->getMessage());
|
||||
echo json_encode(['error' => 'Database error while fetching sales data.']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_stats') {
|
||||
try {
|
||||
// Optimized: Fetch all stats in a single query
|
||||
$query = "
|
||||
SELECT
|
||||
(SELECT SUM(total_amount) FROM orders WHERE status = 'Delivered') as total_sales,
|
||||
(SELECT COUNT(*) FROM orders WHERE status = 'Shipped') as shipped_orders,
|
||||
(SELECT COUNT(*) FROM orders WHERE status = 'Cancelled') as cancelled_orders,
|
||||
(SELECT COUNT(*) FROM orders WHERE status = 'Processing') as processing_orders,
|
||||
(SELECT COUNT(*) FROM users) as total_users,
|
||||
(SELECT COUNT(*) FROM page_views) as total_views,
|
||||
(SELECT COUNT(*) FROM page_views WHERE YEAR(view_timestamp) = YEAR(CURDATE()) AND MONTH(view_timestamp) = MONTH(CURDATE())) as this_month_views,
|
||||
(SELECT COUNT(*) FROM page_views WHERE YEAR(view_timestamp) = YEAR(CURDATE() - INTERVAL 1 MONTH) AND MONTH(view_timestamp) = MONTH(CURDATE() - INTERVAL 1 MONTH)) as last_month_views
|
||||
";
|
||||
|
||||
$stmt = $pdo->query($query);
|
||||
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$this_month_views = (int)($stats['this_month_views'] ?? 0);
|
||||
$last_month_views = (int)($stats['last_month_views'] ?? 0);
|
||||
|
||||
$percentage_change = 0;
|
||||
if ($last_month_views > 0) {
|
||||
$percentage_change = (($this_month_views - $last_month_views) / $last_month_views) * 100;
|
||||
} elseif ($this_month_views > 0) {
|
||||
$percentage_change = 100;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'total_sales' => (float)($stats['total_sales'] ?? 0),
|
||||
'shipped_orders' => (int)($stats['shipped_orders'] ?? 0),
|
||||
'cancelled_orders' => (int)($stats['cancelled_orders'] ?? 0),
|
||||
'processing_orders' => (int)($stats['processing_orders'] ?? 0),
|
||||
'total_users' => (int)($stats['total_users'] ?? 0),
|
||||
'total_page_views' => [
|
||||
'count' => (int)($stats['total_views'] ?? 0),
|
||||
'percentage_change' => round($percentage_change, 2)
|
||||
],
|
||||
]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
error_log("API Error (get_stats): " . $e->getMessage());
|
||||
echo json_encode(['error' => 'Database error while fetching stats.']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid action']);
|
||||
@ -1,259 +1,348 @@
|
||||
|
||||
/* =================================================================
|
||||
ADMIN PANEL MODERN STYLES
|
||||
================================================================= */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;600;700&display=swap');
|
||||
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css');
|
||||
|
||||
:root {
|
||||
--admin-bg: #1A202C; /* Very dark blue */
|
||||
--admin-surface: #2D3748; /* Lighter dark blue for cards, tables */
|
||||
--admin-border: #4A5568; /* Subtle borders */
|
||||
--admin-accent: #FBBF24; /* Amber/Gold for highlights */
|
||||
--admin-accent-hover: #F59E0B; /* Darker gold for hover */
|
||||
|
||||
--admin-text-primary: #EDF2F7; /* Bright, light gray for main text */
|
||||
--admin-text-secondary: #A0AEC0; /* Softer gray for subtitles */
|
||||
|
||||
--admin-success: #38A169; /* Green */
|
||||
--admin-danger: #E53E3E; /* Red */
|
||||
--admin-info: #3182CE; /* Blue */
|
||||
|
||||
--admin-font: 'Vazirmatn', sans-serif;
|
||||
--admin-bg: #111214;
|
||||
--admin-surface: #1a1b1e;
|
||||
--admin-text: #eceff1;
|
||||
--admin-text-muted: #90a4ae;
|
||||
--admin-primary: #c09f80; /* Soft gold from luxury theme */
|
||||
--admin-border: #37474f;
|
||||
--admin-success: #4caf50;
|
||||
--admin-danger: #f44336;
|
||||
--admin-warning: #ff9800;
|
||||
--admin-info: #2196f3;
|
||||
}
|
||||
|
||||
/* --- General Body & Typography --- */
|
||||
body.admin-page {
|
||||
body.admin-dark-theme {
|
||||
background-color: var(--admin-bg);
|
||||
color: var(--admin-text-primary);
|
||||
font-family: var(--admin-font);
|
||||
padding-right: 0; /* Reset previous style */
|
||||
color: var(--admin-text);
|
||||
font-family: 'Vazirmatn', sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.admin-main-content {
|
||||
padding: 2rem;
|
||||
margin-right: 280px; /* Space for the new sidebar */
|
||||
transition: margin-right 0.3s ease;
|
||||
.admin-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
color: var(--admin-text-primary);
|
||||
/* --- Sidebar / Navigation --- */
|
||||
.admin-sidebar {
|
||||
width: 260px;
|
||||
background-color: var(--admin-surface);
|
||||
border-right: 1px solid var(--admin-border);
|
||||
padding: 1.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.sidebar-header h2 a {
|
||||
color: var(--admin-text);
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--admin-accent);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--admin-accent-hover);
|
||||
.sidebar-header h2 a span {
|
||||
color: var(--admin-primary);
|
||||
}
|
||||
|
||||
/* --- Override Bootstrap Dark Components --- */
|
||||
.table-dark {
|
||||
--bs-table-bg: var(--admin-surface);
|
||||
--bs-table-border-color: var(--admin-border);
|
||||
--bs-table-color: var(--admin-text-primary);
|
||||
--bs-table-striped-bg: #353c4a; /* Slightly lighter for striped rows */
|
||||
}
|
||||
|
||||
.table > :not(caption) > * > * {
|
||||
border-bottom-width: 1px;
|
||||
box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);
|
||||
}
|
||||
|
||||
|
||||
.form-control {
|
||||
background-color: var(--admin-surface);
|
||||
color: var(--admin-text-primary);
|
||||
border-color: var(--admin-border);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: var(--admin-surface);
|
||||
color: var(--admin-text-primary);
|
||||
border-color: var(--admin-accent);
|
||||
box-shadow: 0 0 0 0.25rem rgba(var(--admin-accent), 0.2);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
background-color: var(--admin-surface);
|
||||
color: var(--admin-text-primary);
|
||||
border-color: var(--admin-border);
|
||||
}
|
||||
|
||||
|
||||
/* --- Buttons --- */
|
||||
.btn-primary {
|
||||
background-color: var(--admin-accent);
|
||||
border-color: var(--admin-accent);
|
||||
color: #1A202C; /* Dark text on gold button */
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: var(--admin-accent-hover);
|
||||
border-color: var(--admin-accent-hover);
|
||||
color: #1A202C;
|
||||
}
|
||||
|
||||
.btn-success { background-color: var(--admin-success); border-color: var(--admin-success); }
|
||||
.btn-danger { background-color: var(--admin-danger); border-color: var(--admin-danger); }
|
||||
.btn-info { background-color: var(--admin-info); border-color: var(--admin-info); }
|
||||
|
||||
|
||||
/* --- New Sidebar --- */
|
||||
.admin-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
background-color: var(--admin-surface);
|
||||
border-left: 1px solid var(--admin-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 0;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.admin-sidebar-header {
|
||||
text-align: center;
|
||||
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
}
|
||||
.admin-sidebar-header .logo {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 800;
|
||||
color: var(--admin-text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.admin-sidebar-header .logo span {
|
||||
color: var(--admin-accent);
|
||||
}
|
||||
|
||||
.admin-sidebar .nav {
|
||||
.admin-nav {
|
||||
flex-grow: 1;
|
||||
padding-top: 1rem;
|
||||
list-style: none;
|
||||
padding: 1.5rem 0 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
.admin-sidebar .nav-link {
|
||||
color: var(--admin-text-secondary);
|
||||
|
||||
.admin-nav-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
gap: 0.8rem;
|
||||
padding: 0.9rem 1.5rem;
|
||||
margin: 0.25rem 0;
|
||||
border-right: 4px solid transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.admin-sidebar .nav-link:hover {
|
||||
color: var(--admin-text-primary);
|
||||
background-color: rgba(45, 55, 72, 0.5); /* #2D3748 with opacity */
|
||||
}
|
||||
.admin-sidebar .nav-link.active {
|
||||
color: var(--admin-text-primary);
|
||||
background-color: var(--admin-bg);
|
||||
border-right-color: var(--admin-accent);
|
||||
color: var(--admin-text-muted);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.admin-sidebar .nav-link .bi {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 0.75rem; /* For RTL, it should be margin-left */
|
||||
font-size: 0.95rem;
|
||||
border-left: 4px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.admin-sidebar-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
.admin-nav-link i {
|
||||
font-size: 1.1rem;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-nav-link:hover {
|
||||
background-color: var(--admin-bg);
|
||||
color: var(--admin-primary);
|
||||
border-left-color: var(--admin-primary);
|
||||
}
|
||||
|
||||
.admin-nav-link.active {
|
||||
background-color: var(--admin-bg);
|
||||
color: var(--admin-text);
|
||||
border-left-color: var(--admin-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
/* --- Dashboard Stat Cards --- */
|
||||
.stat-card {
|
||||
.sidebar-footer a {
|
||||
color: var(--admin-text-muted);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.sidebar-footer a:hover {
|
||||
color: var(--admin-primary);
|
||||
}
|
||||
|
||||
/* --- Main Content --- */
|
||||
.admin-main-content {
|
||||
flex-grow: 1;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.admin-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* --- General Components --- */
|
||||
.card {
|
||||
background-color: var(--admin-surface);
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 0.75rem;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
padding: 1rem;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 700;
|
||||
color: var(--admin-text-muted);
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: var(--admin-bg);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--admin-primary);
|
||||
color: var(--admin-bg);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--admin-danger);
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
/* --- Stat Cards (from dashboard) --- */
|
||||
.stat-card {
|
||||
background-color: var(--admin-surface);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
gap: 1.5rem;
|
||||
border: 1px solid var(--admin-border);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
|
||||
border-color: var(--admin-accent);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
.stat-card .icon-container {
|
||||
|
||||
.stat-card .icon {
|
||||
font-size: 2rem;
|
||||
color: var(--admin-accent);
|
||||
background-color: #363e4d;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
padding: 1rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 1.5rem; /* For RTL */
|
||||
}
|
||||
.stat-card .stat-info h3 {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
.stat-card .stat-info p {
|
||||
margin: 0;
|
||||
color: var(--admin-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
|
||||
/* --- Dashboard Tables & Badges --- */
|
||||
.card-table .card-header {
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
padding: 1rem 1.5rem;
|
||||
font-weight: 600;
|
||||
.stat-card .stat-info h3 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge.bg-processing { background-color: var(--admin-info) !important; }
|
||||
.badge.bg-shipped { background-color: var(--admin-success) !important; }
|
||||
.badge.bg-cancelled { background-color: var(--admin-danger) !important; }
|
||||
.badge.bg-pending { background-color: #DD6B20 !important; } /* Orange */
|
||||
.icon.bg-success { background-color: var(--admin-success); }
|
||||
.icon.bg-danger { background-color: var(--admin-danger); }
|
||||
.icon.bg-warning { background-color: var(--admin-warning); }
|
||||
.icon.bg-info { background-color: var(--admin-info); }
|
||||
.icon.bg-primary { background-color: var(--admin-primary); }
|
||||
|
||||
/* --- Modal Styling --- */
|
||||
.modal-content {
|
||||
|
||||
/* --- Chart Container --- */
|
||||
.chart-container {
|
||||
background-color: var(--admin-surface);
|
||||
color: var(--admin-text-primary);
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--admin-border);
|
||||
}
|
||||
.modal-header {
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
}
|
||||
.modal-footer {
|
||||
border-top: 1px solid var(--admin-border);
|
||||
}
|
||||
.btn-close {
|
||||
filter: invert(1) grayscale(100%) brightness(200%);
|
||||
|
||||
.chart-container h5 {
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* --- Form styles --- */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1rem;
|
||||
background-color: var(--admin-bg);
|
||||
border: 1px solid var(--admin-border);
|
||||
color: var(--admin-text);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--admin-primary);
|
||||
box-shadow: 0 0 0 2px rgba(192, 159, 128, 0.2);
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.admin-main-content {
|
||||
margin-right: 0;
|
||||
.admin-sidebar {
|
||||
width: 70px;
|
||||
}
|
||||
.sidebar-header h2 {
|
||||
display: none;
|
||||
}
|
||||
.admin-nav-link {
|
||||
justify-content: center;
|
||||
}
|
||||
.admin-nav-link span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.admin-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
.admin-sidebar {
|
||||
transform: translateX(280px); /* For RTL */
|
||||
transition: transform 0.3s ease;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
.admin-sidebar.is-open {
|
||||
transform: translateX(0);
|
||||
.sidebar-header {
|
||||
display: none;
|
||||
}
|
||||
/* Add a hamburger toggle button */
|
||||
.sidebar-toggle {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 1200;
|
||||
background: var(--admin-surface);
|
||||
border: 1px solid var(--admin-border);
|
||||
color: var(--admin-text-primary);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
.admin-nav {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.admin-nav-link {
|
||||
border-left: none;
|
||||
border-bottom: 4px solid transparent;
|
||||
}
|
||||
.admin-nav-link:hover, .admin-nav-link.active {
|
||||
border-left-color: transparent;
|
||||
border-bottom-color: var(--admin-primary);
|
||||
}
|
||||
.sidebar-footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
69
admin/assets/css/dashboard_style.css
Normal file
69
admin/assets/css/dashboard_style.css
Normal file
@ -0,0 +1,69 @@
|
||||
|
||||
:root {
|
||||
--dark-bg: #1a1a2e;
|
||||
--dark-surface: #16213e;
|
||||
--dark-primary: #0f3460;
|
||||
--dark-secondary: #e94560;
|
||||
--dark-text-primary: #ffffff;
|
||||
--dark-text-secondary: #c5c5c5;
|
||||
}
|
||||
|
||||
body.dark-theme {
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--dark-text-primary);
|
||||
}
|
||||
|
||||
.dashboard-container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.stat-card-v2 {
|
||||
background-color: var(--dark-surface);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
border: 1px solid var(--dark-primary);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card-v2:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stat-card-v2 .icon-container {
|
||||
font-size: 2.5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--dark-primary);
|
||||
}
|
||||
|
||||
.stat-card-v2 .stat-info p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--dark-text-secondary);
|
||||
}
|
||||
|
||||
.stat-card-v2 .stat-info h3 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background-color: var(--dark-surface);
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--dark-primary);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.chart-container h5 {
|
||||
font-weight: bold;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
require_once __DIR__ . '/auth_handler.php';
|
||||
|
||||
// Check if the user is logged in. If not, redirect to the login page.
|
||||
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
|
||||
if (!is_admin()) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
8
admin/auth_handler.php
Normal file
8
admin/auth_handler.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function is_admin() {
|
||||
return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
|
||||
}
|
||||
1
admin/cache/sales_chart.json
vendored
Normal file
1
admin/cache/sales_chart.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"labels":["1404-09"],"data":[2940000]}
|
||||
187
admin/dashboard.php
Normal file
187
admin/dashboard.php
Normal file
@ -0,0 +1,187 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="admin-header">
|
||||
<h1>گزارشها</h1>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-error" class="card d-none" style="color: var(--admin-danger);"><div class="card-body"></div></div>
|
||||
|
||||
<style>
|
||||
.stat-cards-grid-reports {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="stat-cards-grid-reports">
|
||||
<div class="stat-card">
|
||||
<div class="icon bg-success"><i class="fas fa-dollar-sign"></i></div>
|
||||
<div class="stat-info">
|
||||
<p>مجموع فروش (تحویل شده)</p>
|
||||
<h3 id="total-sales">...</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon bg-info"><i class="fas fa-users"></i></div>
|
||||
<div class="stat-info">
|
||||
<p>مجموع کاربران</p>
|
||||
<h3 id="total-users">...</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon bg-primary"><i class="fas fa-truck"></i></div>
|
||||
<div class="stat-info">
|
||||
<p>سفارشات در حال ارسال</p>
|
||||
<h3 id="shipped-orders">...</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon bg-danger"><i class="fas fa-times-circle"></i></div>
|
||||
<div class="stat-info">
|
||||
<p>سفارشات لغو شده</p>
|
||||
<h3 id="cancelled-orders">...</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon bg-warning"><i class="fas fa-spinner"></i></div>
|
||||
<div class="stat-info">
|
||||
<p>سفارشات در حال پردازش</p>
|
||||
<h3 id="processing-orders">...</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon" style="background-color: #6f42c1;"><i class="fas fa-eye"></i></div>
|
||||
<div class="stat-info">
|
||||
<p>کل بازدید صفحات</p>
|
||||
<h3 id="total-page-views">...</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" style="position: relative; height:40vh; max-height: 450px;">
|
||||
<h5>نمودار فروش ماهانه (سفارشات تحویل شده)</h5>
|
||||
<canvas id="salesChart"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const errorDiv = document.getElementById('dashboard-error');
|
||||
const errorBody = errorDiv.querySelector('.card-body');
|
||||
|
||||
function showError(message) {
|
||||
errorBody.textContent = message;
|
||||
errorDiv.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Fetch all data in parallel for faster loading
|
||||
Promise.all([
|
||||
fetch('api.php?action=get_stats').then(res => res.json()),
|
||||
fetch('api.php?action=get_sales_data').then(res => res.json())
|
||||
]).then(([statsData, salesData]) => {
|
||||
|
||||
// Handle stats data
|
||||
if (statsData.error) throw new Error(`آمار: ${statsData.error}`);
|
||||
document.getElementById('total-sales').textContent = new Intl.NumberFormat('fa-IR').format(statsData.total_sales) + ' تومان';
|
||||
document.getElementById('total-users').textContent = statsData.total_users;
|
||||
document.getElementById('shipped-orders').textContent = statsData.shipped_orders;
|
||||
document.getElementById('cancelled-orders').textContent = statsData.cancelled_orders;
|
||||
document.getElementById('processing-orders').textContent = statsData.processing_orders;
|
||||
document.getElementById('total-page-views').textContent = statsData.total_page_views.count;
|
||||
|
||||
// Handle sales chart data
|
||||
if (salesData.error) throw new Error(`نمودار فروش: ${salesData.error}`);
|
||||
renderSalesChart(salesData.labels, salesData.data);
|
||||
|
||||
}).catch(error => {
|
||||
console.error(`خطا:`, error);
|
||||
showError(`خطا در بارگذاری گزارشات: ${error.message}`);
|
||||
});
|
||||
|
||||
function renderSalesChart(labels, data) {
|
||||
const ctx = document.getElementById('salesChart').getContext('2d');
|
||||
const style = getComputedStyle(document.body);
|
||||
|
||||
// Use a more vibrant and visible color for the chart
|
||||
const chartColor = '#FFD700'; // A nice gold color
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
||||
gradient.addColorStop(0, `${chartColor}60`); // 40% opacity
|
||||
gradient.addColorStop(1, `${chartColor}00`); // 0% opacity
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'میزان فروش',
|
||||
data: data,
|
||||
backgroundColor: gradient,
|
||||
borderColor: chartColor,
|
||||
borderWidth: 3,
|
||||
fill: 'start',
|
||||
tension: 0.4, // Makes the line curvy
|
||||
pointBackgroundColor: chartColor,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
pointBorderColor: style.getPropertyValue('--admin-bg').trim(),
|
||||
pointBorderWidth: 2,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: style.getPropertyValue('--admin-surface').trim(),
|
||||
titleColor: style.getPropertyValue('--admin-text').trim(),
|
||||
bodyColor: style.getPropertyValue('--admin-text-muted').trim(),
|
||||
titleFont: { family: 'Vazirmatn', size: 14, weight: 'bold' },
|
||||
bodyFont: { family: 'Vazirmatn', size: 12 },
|
||||
padding: 12,
|
||||
cornerRadius: 8,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
label: (context) => `مجموع فروش: ${new Intl.NumberFormat('fa-IR').format(context.parsed.y)} تومان`
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
color: style.getPropertyValue('--admin-text-muted').trim(),
|
||||
font: { family: 'Vazirmatn', size: 12 }
|
||||
}
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
color: style.getPropertyValue('--admin-border').trim(),
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: style.getPropertyValue('--admin-text-muted').trim(),
|
||||
font: { family: 'Vazirmatn', size: 12 },
|
||||
padding: 10,
|
||||
callback: (value) => new Intl.NumberFormat('fa-IR', { notation: 'compact' }).format(value)
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once __DIR__ . '/footer.php';
|
||||
?>
|
||||
@ -3,9 +3,7 @@ session_start();
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
// Sanitize and validate product ID
|
||||
$product_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||
|
||||
if (!$product_id) {
|
||||
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'شناسه محصول نامعتبر است.'];
|
||||
header('Location: products.php');
|
||||
@ -17,116 +15,113 @@ try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
|
||||
$stmt->execute([$product_id]);
|
||||
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$product) {
|
||||
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'محصول مورد نظر یافت نشد.'];
|
||||
header('Location: products.php');
|
||||
exit;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database Error: " . $e->getMessage());
|
||||
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'خطا در اتصال به پایگاه داده. لطفاً بعداً تلاش کنید.'];
|
||||
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'خطا در اتصال به پایگاه داده.'];
|
||||
header('Location: products.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$page_title = "ویرایش محصول: " . htmlspecialchars($product['name']);
|
||||
require_once 'header.php';
|
||||
require_once __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">ویرایش محصول</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="index.php">داشبورد</a></li>
|
||||
<li class="breadcrumb-item"><a href="products.php">مدیریت محصولات</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?php echo htmlspecialchars($product['name']); ?></li>
|
||||
</ol>
|
||||
</nav>
|
||||
<style>
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
.image-preview-container {
|
||||
background-color: var(--admin-bg);
|
||||
border: 1px dashed var(--admin-border);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.image-preview {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="admin-header">
|
||||
<h1>ویرایش محصول: <?php echo htmlspecialchars($product['name']); ?></h1>
|
||||
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
|
||||
</div>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">فرم ویرایش محصول</h5>
|
||||
<a href="products.php" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>بازگشت
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="handler.php?action=edit" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="id" value="<?php echo htmlspecialchars($product['id']); ?>">
|
||||
<input type="hidden" name="current_image" value="<?php echo htmlspecialchars($product['image_url']); ?>">
|
||||
<form action="handler.php?action=edit" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="id" value="<?php echo htmlspecialchars($product['id']); ?>">
|
||||
<input type="hidden" name="current_image" value="<?php echo htmlspecialchars($product['image_url']); ?>">
|
||||
|
||||
<div class="row">
|
||||
<!-- Main Product Info -->
|
||||
<div class="col-lg-8">
|
||||
<div class="form-group mb-4">
|
||||
<label for="name">نام محصول</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-4">
|
||||
<label for="description">توضیحات</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="6" required><?php echo htmlspecialchars($product['description']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-4">
|
||||
<label for="price">قیمت (تومان)</label>
|
||||
<input type="number" class="form-control" id="price" name="price" min="0" value="<?php echo htmlspecialchars($product['price']); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-4">
|
||||
<label for="colors">کدهای رنگ هگز</label>
|
||||
<input type="text" class="form-control" id="colors" name="colors" value="<?php echo htmlspecialchars($product['colors'] ?? ''); ?>">
|
||||
<div class="form-text">رنگها را با کاما جدا کنید (مثال: #FFFFFF, #000000).</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch custom-switch mb-4">
|
||||
<input type="checkbox" class="form-check-input" id="is_featured" name="is_featured" value="1" <?php echo ($product['is_featured'] ?? 0) ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="is_featured">محصول ویژه (نمایش در صفحه اصلی)</label>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="name" class="form-label">نام محصول</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
|
||||
</div>
|
||||
|
||||
<!-- Image Upload -->
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group mb-4">
|
||||
<label for="image">تصویر محصول</label>
|
||||
<div class="image-upload-wrapper text-center">
|
||||
<img src="../<?php echo htmlspecialchars($product['image_url']); ?>" alt="Current Image" class="img-thumbnail mb-3" id="image-preview" style="max-width: 180px; height: auto;">
|
||||
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(event)">
|
||||
<small class="form-text text-muted mt-2">برای تغییر، تصویر جدید را انتخاب کنید.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">توضیحات</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="5" required><?php echo htmlspecialchars($product['description']); ?></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="price" class="form-label">قیمت (تومان)</label>
|
||||
<input type="number" class="form-control" id="price" name="price" value="<?php echo htmlspecialchars($product['price']); ?>" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stock" class="form-label">موجودی</label>
|
||||
<input type="number" class="form-control" id="stock" name="stock" value="<?php echo htmlspecialchars($product['stock']); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
|
||||
<input type="text" class="form-control" id="colors" name="colors" value="<?php echo htmlspecialchars($product['colors'] ?? ''); ?>" placeholder="مثال: #8B4513, #2C2C2C">
|
||||
<small style="color: var(--admin-text-muted);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="products.php" class="btn btn-outline-secondary">انصراف</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-2"></i>ذخیره تغییرات
|
||||
</button>
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" name="is_featured" value="1" <?php echo ($product['is_featured'] ?? 0) ? 'checked' : ''; ?> style="width: 20px; height: 20px;">
|
||||
<span>این یک محصول ویژه است</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">تصویر محصول</div>
|
||||
<div class="card-body">
|
||||
<div class="image-preview-container">
|
||||
<img src="../<?php echo htmlspecialchars($product['image_url']); ?>" alt="Current Image" id="image-preview" class="image-preview">
|
||||
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(event)">
|
||||
<small style="color: var(--admin-text-muted); margin-top: 0.5rem; display: block;">برای تغییر، تصویر جدیدی انتخاب کنید.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once 'footer.php';
|
||||
?>
|
||||
<div style="text-align: left; margin-top: 2rem;">
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> ذخیره تغییرات</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Preview image before upload
|
||||
function previewImage(event) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(){
|
||||
const output = document.getElementById('image-preview');
|
||||
output.src = reader.result;
|
||||
};
|
||||
if (event.target.files[0]) {
|
||||
reader.readAsDataURL(event.target.files[0]);
|
||||
}
|
||||
}
|
||||
function previewImage(event) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => document.getElementById('image-preview').src = reader.result;
|
||||
if (event.target.files[0]) reader.readAsDataURL(event.target.files[0]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||
@ -1,7 +1,5 @@
|
||||
</div><!-- /.container-fluid -->
|
||||
</main><!-- /.admin-main-content -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</div><!-- /.admin-main-content -->
|
||||
</div><!-- /.admin-wrapper -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,169 +1,37 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
$pdo = db();
|
||||
|
||||
// Default redirect location
|
||||
$redirect_to = 'index.php';
|
||||
|
||||
switch ($action) {
|
||||
case 'add':
|
||||
$redirect_to = 'add_product.php'; // Redirect back to form on error
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$price = filter_var($_POST['price'], FILTER_VALIDATE_FLOAT);
|
||||
$colors = trim($_POST['colors'] ?? '');
|
||||
$is_featured = isset($_POST['is_featured']) ? 1 : 0;
|
||||
|
||||
$errors = [];
|
||||
|
||||
// Validation
|
||||
if (empty($name)) $errors[] = "Product name is required.";
|
||||
if (empty($description)) $errors[] = "Description is required.";
|
||||
if ($price === false) $errors[] = "Price is invalid or missing.";
|
||||
|
||||
$image_path = '';
|
||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||
$upload_dir = __DIR__ . '/../assets/images/products/';
|
||||
|
||||
if (!is_dir($upload_dir)) {
|
||||
if (!mkdir($upload_dir, 0777, true)) {
|
||||
$errors[] = "Image directory does not exist and could not be created.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_writable($upload_dir)) {
|
||||
$errors[] = "Image directory is not writable. Please check server permissions.";
|
||||
} else {
|
||||
$filename = uniqid('product_', true) . '_' . basename($_FILES['image']['name']);
|
||||
$target_file = $upload_dir . $filename;
|
||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $target_file)) {
|
||||
$image_path = 'assets/images/products/' . $filename;
|
||||
} else {
|
||||
$errors[] = "Failed to move uploaded file.";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$file_error = $_FILES['image']['error'] ?? UPLOAD_ERR_NO_FILE;
|
||||
$upload_errors = [
|
||||
UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the server's maximum upload size (upload_max_filesize).",
|
||||
UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the maximum size specified in the form.",
|
||||
UPLOAD_ERR_PARTIAL => "The file was only partially uploaded.",
|
||||
UPLOAD_ERR_NO_FILE => "No file was selected for upload.",
|
||||
UPLOAD_ERR_NO_TMP_DIR => "Server configuration is missing a temporary folder for uploads.",
|
||||
UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk. Check permissions.",
|
||||
UPLOAD_ERR_EXTENSION => "A PHP extension stopped the file upload.",
|
||||
];
|
||||
if ($file_error !== UPLOAD_ERR_NO_FILE) {
|
||||
$errors[] = $upload_errors[$file_error] ?? "An unknown error occurred during file upload.";
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
$sql = "INSERT INTO products (name, description, price, image_url, colors, is_featured) VALUES (?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$name, $description, $price, $image_path, $colors, $is_featured]);
|
||||
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'محصول با موفقیت اضافه شد!'];
|
||||
$redirect_to = 'products.php';
|
||||
} else {
|
||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => implode("<br>", $errors)];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
$redirect_to = 'products.php'; // Default redirect on success or if ID is missing
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$price = filter_var($_POST['price'], FILTER_VALIDATE_FLOAT);
|
||||
$colors = trim($_POST['colors'] ?? '');
|
||||
$is_featured = isset($_POST['is_featured']) ? 1 : 0;
|
||||
|
||||
$errors = [];
|
||||
|
||||
if (!$id) {
|
||||
$errors[] = "Invalid product ID.";
|
||||
} else {
|
||||
$redirect_to = "edit_product.php?id=$id"; // Redirect back to the edit form on error
|
||||
}
|
||||
|
||||
if (empty($name)) $errors[] = "Product name is required.";
|
||||
if (empty($description)) $errors[] = "Description is required.";
|
||||
if ($price === false) $errors[] = "Price is invalid or missing.";
|
||||
|
||||
|
||||
$current_image_path = $_POST['current_image'] ?? '';
|
||||
$image_path = $current_image_path;
|
||||
|
||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||
$upload_dir = __DIR__ . '/../assets/images/products/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0777, true);
|
||||
|
||||
if (is_writable($upload_dir)) {
|
||||
$filename = uniqid('product_', true) . '_' . basename($_FILES['image']['name']);
|
||||
$target_file = $upload_dir . $filename;
|
||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $target_file)) {
|
||||
$image_path = 'assets/images/products/' . $filename;
|
||||
// Optionally, delete the old image if it's different
|
||||
if ($current_image_path && file_exists(__DIR__ . '/../' . $current_image_path)) {
|
||||
// unlink(__DIR__ . '/../' . $current_image_path);
|
||||
}
|
||||
} else {
|
||||
$errors[] = "Failed to move uploaded file.";
|
||||
}
|
||||
} else {
|
||||
$errors[] = "Image directory is not writable.";
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
$sql = "UPDATE products SET name = ?, description = ?, price = ?, image_url = ?, colors = ?, is_featured = ? WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$name, $description, $price, $image_path, $colors, $is_featured, $id]);
|
||||
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'محصول با موفقیت بهروزرسانی شد!'];
|
||||
$redirect_to = 'products.php';
|
||||
} else {
|
||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => implode("<br>", $errors)];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||
if ($id) {
|
||||
// First, get the image path to delete the file
|
||||
$stmt = $pdo->prepare("SELECT image_url FROM products WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($product && !empty($product['image_url'])) {
|
||||
$image_file = __DIR__ . '/../' . $product['image_url'];
|
||||
if (file_exists($image_file)) {
|
||||
// unlink($image_file);
|
||||
}
|
||||
}
|
||||
|
||||
// Then, delete the record from the database
|
||||
$sql = "DELETE FROM products WHERE id = ?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$id]);
|
||||
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'محصول با موفقیت حذف شد!'];
|
||||
} else {
|
||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'شناسه محصول برای حذف نامعتبر است.'];
|
||||
}
|
||||
$redirect_to = 'products.php';
|
||||
break;
|
||||
|
||||
default:
|
||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'عملیات نامعتبر است.'];
|
||||
break;
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
header("Location: " . $redirect_to);
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'update_order_status') {
|
||||
$order_id = filter_input(INPUT_POST, 'order_id', FILTER_VALIDATE_INT);
|
||||
$status = filter_input(INPUT_POST, 'status', FILTER_SANITIZE_STRING);
|
||||
|
||||
$allowed_statuses = ['Processing', 'Shipped', 'Delivered', 'Cancelled'];
|
||||
|
||||
if ($order_id && $status && in_array($status, $allowed_statuses)) {
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
|
||||
$stmt->execute([$status, $order_id]);
|
||||
|
||||
$_SESSION['success_message'] = "وضعیت سفارش #{$order_id} با موفقیت به '{$status}' تغییر یافت.";
|
||||
} catch (PDOException $e) {
|
||||
error_log("Order status update failed: " . $e->getMessage());
|
||||
$_SESSION['error_message'] = "خطایی در بهروزرسانی وضعیت سفارش رخ داد.";
|
||||
}
|
||||
} else {
|
||||
$_SESSION['error_message'] = "اطلاعات نامعتبر برای بهروزرسانی وضعیت.";
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: orders.php');
|
||||
exit;
|
||||
?>
|
||||
@ -5,23 +5,18 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>پنل مدیریت</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Styles -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Unified Stylesheet -->
|
||||
<link rel="stylesheet" href="assets/css/admin_style.css?v=<?php echo time(); ?>">
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<body class="admin-dark-theme">
|
||||
|
||||
<?php require_once 'nav.php'; ?>
|
||||
|
||||
<main class="admin-main-content">
|
||||
<div class="container-fluid">
|
||||
<div class="admin-wrapper">
|
||||
<?php require_once 'nav.php'; ?>
|
||||
<div class="admin-main-content">
|
||||
|
||||
220
admin/index.php
220
admin/index.php
@ -2,8 +2,6 @@
|
||||
session_start();
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
// New header - includes nav, head, and opening body/main tags
|
||||
require_once __DIR__ . '/header.php';
|
||||
|
||||
$dashboard_error = null;
|
||||
@ -13,14 +11,25 @@ $recent_orders = [];
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$total_products = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
|
||||
$total_orders = $pdo->query("SELECT COUNT(*) FROM orders")->fetchColumn();
|
||||
$recent_orders = $pdo->query("SELECT id, customer_name, total_amount, `status`, created_at FROM orders ORDER BY created_at DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$recent_orders_query = "
|
||||
SELECT
|
||||
o.id,
|
||||
COALESCE(CONCAT(u.first_name, ' ', u.last_name), o.billing_name) AS customer_name,
|
||||
o.total_amount,
|
||||
o.status,
|
||||
o.created_at
|
||||
FROM orders AS o
|
||||
LEFT JOIN users AS u ON o.user_id = u.id
|
||||
ORDER BY o.created_at DESC
|
||||
LIMIT 5
|
||||
";
|
||||
$recent_orders = $pdo->query($recent_orders_query)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$dashboard_error = "<strong>خطا در بارگذاری اطلاعات داشبورد:</strong> " . $e->getMessage();
|
||||
$dashboard_error .= "<br><br>این خطا معمولاً به دلیل قدیمی بودن ساختار دیتابیس رخ میدهد. لطفاً برای بهروزرسانی به <a href='../migrate.php' class='alert-link'>صفحه مایگریشن</a> بروید.";
|
||||
$dashboard_error = "<strong>خطا در بارگذاری اطلاعات:</strong> " . $e->getMessage();
|
||||
}
|
||||
|
||||
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||
@ -28,115 +37,110 @@ if ($flash_message) {
|
||||
unset($_SESSION['flash_message']);
|
||||
}
|
||||
|
||||
// Function to map status to a badge class
|
||||
function get_status_badge($status) {
|
||||
function get_status_badge_class($status) {
|
||||
switch (strtolower($status)) {
|
||||
case 'processing':
|
||||
return 'bg-processing';
|
||||
case 'shipped':
|
||||
return 'bg-shipped';
|
||||
case 'cancelled':
|
||||
return 'bg-cancelled';
|
||||
default:
|
||||
return 'bg-pending';
|
||||
case 'processing': return 'status-processing';
|
||||
case 'shipped': return 'status-shipped';
|
||||
case 'delivered': return 'status-delivered';
|
||||
case 'cancelled': return 'status-cancelled';
|
||||
default: return 'status-pending';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<h1 class="h2 mb-4">داشبورد</h1>
|
||||
<style>
|
||||
.status-badge {
|
||||
padding: 0.3em 0.6em;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.status-processing { background-color: var(--admin-info); }
|
||||
.status-shipped { background-color: var(--admin-warning); }
|
||||
.status-delivered { background-color: var(--admin-success); }
|
||||
.status-cancelled { background-color: var(--admin-danger); }
|
||||
.status-pending { background-color: var(--admin-text-muted); }
|
||||
.stat-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php if ($dashboard_error): ?>
|
||||
<div class="alert alert-danger"><?php echo $dashboard_error; ?></div>
|
||||
<?php else: ?>
|
||||
<div class="admin-header">
|
||||
<h1>داشبورد اصلی</h1>
|
||||
</div>
|
||||
|
||||
<!-- Stat Cards -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="stat-card">
|
||||
<div class="icon-container">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<p>کل محصولات</p>
|
||||
<h3><?php echo htmlspecialchars($total_products); ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="stat-card">
|
||||
<div class="icon-container">
|
||||
<i class="bi bi-receipt"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<p>کل سفارشات</p>
|
||||
<h3><?php echo htmlspecialchars($total_orders); ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Orders -->
|
||||
<div class="card shadow-sm card-table" style="background-color: var(--admin-surface); border-color: var(--admin-border);">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">آخرین سفارشات</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>شماره سفارش</th>
|
||||
<th>نام مشتری</th>
|
||||
<th>مبلغ کل</th>
|
||||
<th>وضعیت</th>
|
||||
<th>تاریخ</th>
|
||||
<th class="text-end"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recent_orders)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4">هیچ سفارشی یافت نشد.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recent_orders as $order): ?>
|
||||
<tr>
|
||||
<td>#<?php echo htmlspecialchars($order['id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($order['customer_name']); ?></td>
|
||||
<td><?php echo number_format($order['total_amount']); ?> تومان</td>
|
||||
<td><span class="badge <?php echo get_status_badge($order['status']); ?>"><?php echo htmlspecialchars($order['status']); ?></span></td>
|
||||
<td><?php echo date('Y-m-d', strtotime($order['created_at'])); ?></td>
|
||||
<td class="text-end">
|
||||
<a href="orders.php?id=<?php echo $order['id']; ?>" class="btn btn-sm btn-outline-info">مشاهده</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($flash_message): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
Swal.fire({
|
||||
title: '<?php echo $flash_message["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||
icon: '<?php echo $flash_message["type"]; ?>',
|
||||
confirmButtonText: 'باشه',
|
||||
background: 'var(--admin-surface)',
|
||||
color: 'var(--admin-text)'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Page-specific scripts -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Flash message handling (if any)
|
||||
<?php if ($flash_message): ?>
|
||||
Swal.fire({
|
||||
title: '<?php echo $flash_message["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||
icon: '<?php echo $flash_message["type"]; ?>',
|
||||
confirmButtonText: 'باشه',
|
||||
background: 'var(--admin-surface)',
|
||||
color: 'var(--admin-text-primary)'
|
||||
});
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
<?php if ($dashboard_error): ?>
|
||||
<div class="card"><div class="card-body" style="color: var(--admin-danger);"><?php echo $dashboard_error; ?></div></div>
|
||||
<?php else: ?>
|
||||
|
||||
<?php
|
||||
// New footer
|
||||
require_once __DIR__ . '/footer.php';
|
||||
?>
|
||||
<div class="stat-cards-grid">
|
||||
<div class="stat-card">
|
||||
<div class="icon bg-primary"><i class="fas fa-box"></i></div>
|
||||
<div class="stat-info">
|
||||
<p>کل محصولات</p>
|
||||
<h3><?php echo htmlspecialchars($total_products); ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="icon bg-warning"><i class="fas fa-receipt"></i></div>
|
||||
<div class="stat-info">
|
||||
<p>کل سفارشات</p>
|
||||
<h3><?php echo htmlspecialchars($total_orders); ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">آخرین سفارشات</div>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>شماره سفارش</th>
|
||||
<th>نام مشتری</th>
|
||||
<th>مبلغ کل</th>
|
||||
<th>وضعیت</th>
|
||||
<th>تاریخ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recent_orders)): ?>
|
||||
<tr><td colspan="5" style="text-align: center; padding: 2rem;">هیچ سفارشی یافت نشد.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recent_orders as $order): ?>
|
||||
<tr>
|
||||
<td>#<?php echo htmlspecialchars($order['id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($order['customer_name']); ?></td>
|
||||
<td><?php echo number_format($order['total_amount']); ?> تومان</td>
|
||||
<td><span class="status-badge <?php echo get_status_badge_class($order['status']); ?>"><?php echo htmlspecialchars($order['status']); ?></span></td>
|
||||
<td><?php echo date('Y-m-d', strtotime($order['created_at'])); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||
|
||||
@ -1,42 +1,37 @@
|
||||
<?php $current_page = basename($_SERVER['PHP_SELF']); ?>
|
||||
<div class="admin-sidebar">
|
||||
<div class="admin-sidebar-header">
|
||||
<a href="index.php" class="logo">آتیمه<span>.</span></a>
|
||||
<aside class="admin-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2><a href="index.php">آتیمه<span>.</span></a></h2>
|
||||
</div>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo ($current_page == 'index.php') ? 'active' : ''; ?>" href="index.php">
|
||||
<i class="bi bi-grid-1x2-fill"></i>
|
||||
<span>داشبورد</span>
|
||||
<ul class="admin-nav">
|
||||
<li class="admin-nav-item">
|
||||
<a class="admin-nav-link <?php echo ($current_page == 'index.php') ? 'active' : ''; ?>" href="index.php">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span>داشبورد اصلی</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo ($current_page == 'products.php' || $current_page == 'add_product.php' || $current_page == 'edit_product.php') ? 'active' : ''; ?>" href="products.php">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
<li class="admin-nav-item">
|
||||
<a class="admin-nav-link <?php echo ($current_page == 'dashboard.php') ? 'active' : ''; ?>" href="dashboard.php">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>گزارشها</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="admin-nav-item">
|
||||
<a class="admin-nav-link <?php echo in_array($current_page, ['products.php', 'add_product.php', 'edit_product.php']) ? 'active' : ''; ?>" href="products.php">
|
||||
<i class="fas fa-box"></i>
|
||||
<span>محصولات</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo ($current_page == 'orders.php') ? 'active' : ''; ?>" href="orders.php">
|
||||
<i class="bi bi-card-checklist"></i>
|
||||
<li class="admin-nav-item">
|
||||
<a class="admin-nav-link <?php echo ($current_page == 'orders.php') ? 'active' : ''; ?>" href="orders.php">
|
||||
<i class="fas fa-receipt"></i>
|
||||
<span>سفارشات</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="admin-sidebar-footer">
|
||||
<a href="../index.php" target="_blank" class="btn btn-outline-secondary w-100 mb-2">
|
||||
<i class="bi bi-box-arrow-up-right"></i> مشاهده سایت
|
||||
</a>
|
||||
<a href="logout.php" class="btn btn-outline-danger w-100">
|
||||
<i class="bi bi-box-arrow-right"></i> خروج
|
||||
</a>
|
||||
<div class="sidebar-footer">
|
||||
<a href="../index.php" target="_blank"><i class="fas fa-external-link-alt"></i> مشاهده سایت</a>
|
||||
<hr style="border-color: var(--admin-border); margin: 1rem 0;">
|
||||
<a href="logout.php"><i class="fas fa-sign-out-alt"></i> خروج</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn sidebar-toggle d-lg-none" type="button" onclick="toggleSidebar()">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
document.querySelector('.admin-sidebar').classList.toggle('is-open');
|
||||
}
|
||||
</script>
|
||||
</aside>
|
||||
261
admin/orders.php
261
admin/orders.php
@ -2,130 +2,185 @@
|
||||
session_start();
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
// New header
|
||||
require_once __DIR__ . '/header.php';
|
||||
|
||||
// Fetch orders from the database
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->query("SELECT * FROM orders ORDER BY created_at DESC");
|
||||
$query = "SELECT o.*, COALESCE(CONCAT(u.first_name, ' ', u.last_name), o.billing_name) AS customer_display_name FROM orders o LEFT JOIN users u ON o.user_id = u.id ORDER BY o.created_at DESC";
|
||||
$stmt = $pdo->query($query);
|
||||
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
$error_message = "خطا در دریافت اطلاعات سفارشات: " . $e->getMessage();
|
||||
$orders = [];
|
||||
}
|
||||
|
||||
// Function to map status to a badge class
|
||||
function get_status_badge($status) {
|
||||
function get_status_badge_class($status) {
|
||||
switch (strtolower($status)) {
|
||||
case 'processing':
|
||||
return 'bg-processing';
|
||||
case 'shipped':
|
||||
return 'bg-shipped';
|
||||
case 'cancelled':
|
||||
return 'bg-cancelled';
|
||||
default:
|
||||
return 'bg-pending';
|
||||
case 'processing': return 'status-processing';
|
||||
case 'shipped': return 'status-shipped';
|
||||
case 'delivered': return 'status-delivered';
|
||||
case 'cancelled': return 'status-cancelled';
|
||||
default: return 'status-pending';
|
||||
}
|
||||
}
|
||||
$statuses = ['Processing', 'Shipped', 'Delivered', 'Cancelled'];
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2">مدیریت سفارشات</h1>
|
||||
<style>
|
||||
/* Same status badges from index.php */
|
||||
.status-badge { padding: 0.3em 0.6em; border-radius: 6px; font-size: 0.8rem; font-weight: 600; color: #fff; }
|
||||
.status-processing { background-color: var(--admin-info); }
|
||||
.status-shipped { background-color: var(--admin-warning); }
|
||||
.status-delivered { background-color: var(--admin-success); }
|
||||
.status-cancelled { background-color: var(--admin-danger); }
|
||||
.status-pending { background-color: var(--admin-text-muted); }
|
||||
|
||||
/* Custom Modal Styles */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; display: none; align-items: center; justify-content: center; }
|
||||
.modal-container { background: var(--admin-surface); border: 1px solid var(--admin-border); border-radius: 12px; width: 90%; max-width: 800px; max-height: 90vh; display: flex; flex-direction: column; }
|
||||
.modal-header { padding: 1rem 1.5rem; border-bottom: 1px solid var(--admin-border); display: flex; justify-content: space-between; align-items: center; }
|
||||
.modal-body { padding: 1.5rem; overflow-y: auto; }
|
||||
.modal-footer { padding: 1rem 1.5rem; border-top: 1px solid var(--admin-border); text-align: left; }
|
||||
.modal-close { background: none; border: none; font-size: 1.5rem; color: var(--admin-text-muted); cursor: pointer; }
|
||||
.modal-overlay.active { display: flex; }
|
||||
.items-list img { width: 50px; height: 50px; object-fit: cover; border-radius: 6px; }
|
||||
</style>
|
||||
|
||||
<div class="admin-header">
|
||||
<h1>مدیریت سفارشات</h1>
|
||||
</div>
|
||||
|
||||
<?php if (isset($error_message)): ?>
|
||||
<div class="alert alert-danger"><?php echo $error_message; ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_SESSION['flash_message'])): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const style = getComputedStyle(document.body);
|
||||
Swal.fire({
|
||||
title: '<?php echo $_SESSION["flash_message"]["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||
html: '<?php echo addslashes($_SESSION["flash_message"]["message"]); ?>',
|
||||
icon: '<?php echo $_SESSION["flash_message"]["type"]; ?>',
|
||||
confirmButtonText: 'باشه',
|
||||
background: style.getPropertyValue('--admin-surface'),
|
||||
color: style.getPropertyValue('--admin-text')
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php unset($_SESSION['flash_message']); endif; ?>
|
||||
|
||||
<div class="card shadow-sm card-table" style="background-color: var(--admin-surface); border-color: var(--admin-border);">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>شماره</th>
|
||||
<th>نام مشتری</th>
|
||||
<th>مبلغ کل</th>
|
||||
<th>وضعیت</th>
|
||||
<th>تاریخ</th>
|
||||
<th class="text-end">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($orders)): ?>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>شماره</th><th>نام مشتری</th><th>مبلغ کل</th><th>وضعیت</th><th>تاریخ</th><th style="text-align: left;">عملیات</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($orders)): ?>
|
||||
<tr><td colspan="6" style="text-align: center; padding: 2rem;">هیچ سفارشی یافت نشد.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4">هیچ سفارشی یافت نشد.</td>
|
||||
<td>#<?php echo $order['id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($order['customer_display_name']); ?></td>
|
||||
<td><?php echo number_format($order['total_amount']); ?> تومان</td>
|
||||
<td><span class="status-badge <?php echo get_status_badge_class($order['status']); ?>"><?php echo htmlspecialchars($order['status']); ?></span></td>
|
||||
<td><?php echo date("Y-m-d", strtotime($order['created_at'])); ?></td>
|
||||
<td style="text-align: left;">
|
||||
<button class="btn btn-sm view-order-btn" data-order-id="<?php echo $order['id']; ?>" style="background-color: var(--admin-info); color: white;"><i class="fas fa-eye"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<tr>
|
||||
<td>#<?php echo $order['id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($order['customer_name']); ?></td>
|
||||
<td><?php echo number_format($order['total_amount']); ?> تومان</td>
|
||||
<td><span class="badge <?php echo get_status_badge($order['status']); ?>"><?php echo htmlspecialchars($order['status']); ?></span></td>
|
||||
<td><?php echo date("Y-m-d", strtotime($order['created_at'])); ?></td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-info" data-bs-toggle="modal" data-bs-target="#orderModal<?php echo $order['id']; ?>">
|
||||
<i class="bi bi-eye-fill"></i> مشاهده
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Modal for order details -->
|
||||
<div class="modal fade" id="orderModal<?php echo $order['id']; ?>" tabindex="-1" aria-labelledby="orderModalLabel<?php echo $order['id']; ?>" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="orderModalLabel<?php echo $order['id']; ?>">جزئیات سفارش #<?php echo $order['id']; ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>نام:</strong> <?php echo htmlspecialchars($order['customer_name']); ?></p>
|
||||
<p><strong>ایمیل:</strong> <?php echo htmlspecialchars($order['customer_email']); ?></p>
|
||||
<p><strong>تلفن:</strong> <?php echo htmlspecialchars($order['customer_phone'] ?? '-'); ?></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>آدرس:</strong> <?php echo htmlspecialchars($order['customer_address']); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h6>محصولات خریداری شده:</h6>
|
||||
<?php
|
||||
$items = json_decode($order['items_json'], true);
|
||||
if ($items):
|
||||
?>
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach($items as $item): ?>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" style="background: none; color: var(--admin-text-primary);">
|
||||
<?php echo htmlspecialchars($item['name']); ?>
|
||||
<span class="badge bg-secondary rounded-pill">
|
||||
<?php echo $item['quantity']; ?> عدد - <?php echo number_format($item['price']); ?> ت
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p>جزئیات محصولات موجود نیست.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<p class="w-100 text-start"><strong>مبلغ نهایی: <?php echo number_format($order['total_amount']); ?> تومان</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// New footer
|
||||
require_once __DIR__ . '/footer.php';
|
||||
?>
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<div id="modal-<?php echo $order['id']; ?>" class="modal-overlay">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h5>جزئیات سفارش #<?php echo $order['id']; ?></h5>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>اطلاعات مشتری</h6>
|
||||
<p><strong>نام:</strong> <?php echo htmlspecialchars($order['customer_display_name']); ?><br>
|
||||
<strong>آدرس:</strong> <?php echo htmlspecialchars($order['billing_address'] . ", " . $order['billing_city'] . ", " . $order['billing_province']); ?><br>
|
||||
<strong>تلفن:</strong> <?php echo htmlspecialchars($order['billing_phone']); ?></p>
|
||||
<hr style="border-color: var(--admin-border);">
|
||||
<h6>محصولات</h6>
|
||||
<table class="table items-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">محصول</th>
|
||||
<th>رنگ</th>
|
||||
<th>تعداد</th>
|
||||
<th class="text-start">قیمت واحد</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $items = json_decode($order['items_json'], true); ?>
|
||||
<?php foreach($items as $item): ?>
|
||||
<tr style="vertical-align: middle;">
|
||||
<td style="width: 60px;"><img src="../<?php echo htmlspecialchars($item['image_url']); ?>" style="width: 50px; height: 50px; object-fit: cover; border-radius: 6px;"></td>
|
||||
<td><?php echo htmlspecialchars($item['name']); ?></td>
|
||||
<td style="width: 60px;">
|
||||
<?php if (!empty($item['color'])): ?>
|
||||
<span style="display: inline-block; width: 22px; height: 22px; border-radius: 50%; background-color: <?php echo htmlspecialchars($item['color']); ?>; border: 1px solid var(--admin-border); box-shadow: 0 1px 3px rgba(0,0,0,0.1);" title="<?php echo htmlspecialchars($item['color']); ?>"></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="width: 80px;"><?php echo $item['quantity']; ?> عدد</td>
|
||||
<td style="width: 120px;" class="text-start"><?php echo number_format($item['price']); ?> تومان</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr style="border-color: var(--admin-border);">
|
||||
<h5 style="text-align: left;">مبلغ نهایی: <?php echo number_format($order['total_amount']); ?> تومان</h5>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<form action="handler.php" method="POST" style="display: flex; width: 100%; justify-content: space-between; align-items: center;">
|
||||
<input type="hidden" name="order_id" value="<?php echo $order['id']; ?>">
|
||||
<input type="hidden" name="action" value="update_order_status">
|
||||
<div class="form-group" style="display: flex; align-items: center; gap: 1rem;">
|
||||
<label for="status_<?php echo $order['id']; ?>" class="form-label">تغییر وضعیت:</label>
|
||||
<select class="form-control" name="status" id="status_<?php echo $order['id']; ?>">
|
||||
<?php foreach ($statuses as $status): ?>
|
||||
<option value="<?php echo $status; ?>" <?php echo ($order['status'] === $status) ? 'selected' : ''; ?>><?php echo $status; ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">بهروزرسانی</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const viewButtons = document.querySelectorAll('.view-order-btn');
|
||||
viewButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const orderId = this.getAttribute('data-order-id');
|
||||
document.getElementById('modal-' + orderId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const closeButtons = document.querySelectorAll('.modal-close');
|
||||
closeButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
this.closest('.modal-overlay').classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
this.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||
@ -2,8 +2,6 @@
|
||||
session_start();
|
||||
require_once __DIR__ . '/auth_check.php';
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
// New header - includes nav, head, and opening body/main tags
|
||||
require_once __DIR__ . '/header.php';
|
||||
|
||||
try {
|
||||
@ -11,7 +9,6 @@ try {
|
||||
$stmt = $pdo->query("SELECT id, name, price FROM products ORDER BY created_at DESC");
|
||||
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
// In a real app, log this error instead of just dying
|
||||
die("Error fetching products: " . $e->getMessage());
|
||||
}
|
||||
|
||||
@ -19,101 +16,83 @@ $flash_message = $_SESSION['flash_message'] ?? null;
|
||||
if ($flash_message) {
|
||||
unset($_SESSION['flash_message']);
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<!-- Page Title and Action Button -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2">مدیریت محصولات</h1>
|
||||
<a href="add_product.php" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i> افزودن محصول
|
||||
</a>
|
||||
<div class="admin-header">
|
||||
<h1>مدیریت محصولات</h1>
|
||||
<a href="add_product.php" class="btn btn-primary"><i class="fas fa-plus"></i> افزودن محصول</a>
|
||||
</div>
|
||||
|
||||
<!-- Products Table -->
|
||||
<div class="card shadow-sm" style="background-color: var(--admin-surface); border-color: var(--admin-border);">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">نام محصول</th>
|
||||
<th scope="col">قیمت</th>
|
||||
<th scope="col" class="text-end">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($products)): ?>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>نام محصول</th>
|
||||
<th>قیمت</th>
|
||||
<th style="text-align: left;">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($products)): ?>
|
||||
<tr><td colspan="4" style="text-align: center; padding: 2rem;">هیچ محصولی یافت نشد.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4">هیچ محصولی یافت نشد.</td>
|
||||
<td><?php echo htmlspecialchars($product['id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($product['name']); ?></td>
|
||||
<td><?php echo number_format($product['price']); ?> تومان</td>
|
||||
<td style="text-align: left;">
|
||||
<a href="edit_product.php?id=<?php echo $product['id']; ?>" class="btn" style="background-color: var(--admin-info); color: white;"><i class="fas fa-edit"></i></a>
|
||||
<a href="handler.php?action=delete&id=<?php echo $product['id']; ?>" class="btn btn-danger delete-btn"><i class="fas fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<tr>
|
||||
<th scope="row"><?php echo htmlspecialchars($product['id']); ?></th>
|
||||
<td><?php echo htmlspecialchars($product['name']); ?></td>
|
||||
<td><?php echo number_format($product['price']); ?> تومان</td>
|
||||
<td class="text-end">
|
||||
<a href="edit_product.php?id=<?php echo $product['id']; ?>" class="btn btn-sm btn-info">
|
||||
<i class="bi bi-pencil-fill"></i> ویرایش
|
||||
</a>
|
||||
<a href="handler.php?action=delete&id=<?php echo $product['id']; ?>" class="btn btn-sm btn-danger delete-btn">
|
||||
<i class="bi bi-trash-fill"></i> حذف
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page-specific scripts -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Flash message handling
|
||||
<?php if ($flash_message): ?>
|
||||
Swal.fire({
|
||||
title: '<?php echo $flash_message["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||
icon: '<?php echo $flash_message["type"]; ?>',
|
||||
confirmButtonText: 'باشه',
|
||||
background: 'var(--admin-surface)',
|
||||
color: 'var(--admin-text-primary)'
|
||||
});
|
||||
<?php endif; ?>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const style = getComputedStyle(document.body);
|
||||
|
||||
// Delete confirmation
|
||||
const deleteButtons = document.querySelectorAll('.delete-btn');
|
||||
deleteButtons.forEach(button => {
|
||||
button.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const href = this.getAttribute('href');
|
||||
Swal.fire({
|
||||
title: 'آیا مطمئن هستید؟',
|
||||
text: "این عمل غیرقابل بازگشت است!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: 'var(--admin-danger)',
|
||||
cancelButtonColor: 'var(--admin-info)',
|
||||
confirmButtonText: 'بله، حذف کن!',
|
||||
cancelButtonText: 'انصراف',
|
||||
background: 'var(--admin-surface)',
|
||||
color: 'var(--admin-text-primary)'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
<?php if ($flash_message): ?>
|
||||
Swal.fire({
|
||||
title: '<?php echo $flash_message["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||
icon: '<?php echo $flash_message["type"]; ?>',
|
||||
confirmButtonText: 'باشه',
|
||||
background: style.getPropertyValue('--admin-surface'),
|
||||
color: style.getPropertyValue('--admin-text')
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
document.querySelectorAll('.delete-btn').forEach(button => {
|
||||
button.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const href = this.getAttribute('href');
|
||||
Swal.fire({
|
||||
title: 'آیا مطمئن هستید؟',
|
||||
text: "این عمل غیرقابل بازگشت است!",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: style.getPropertyValue('--admin-danger'),
|
||||
cancelButtonColor: style.getPropertyValue('--admin-info'),
|
||||
confirmButtonText: 'بله، حذف کن!',
|
||||
cancelButtonText: 'انصراف',
|
||||
background: style.getPropertyValue('--admin-surface'),
|
||||
color: style.getPropertyValue('--admin-text')
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
// New footer - includes closing tags
|
||||
require_once __DIR__ . '/footer.php';
|
||||
?>
|
||||
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||
63
api/get_order_details.php
Normal file
63
api/get_order_details.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once '../db/config.php';
|
||||
require_once '../includes/jdf.php';
|
||||
|
||||
$response = ['success' => false, 'message' => 'Invalid request'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$tracking_id = $data['tracking_id'] ?? '';
|
||||
$phone = $data['phone'] ?? '';
|
||||
|
||||
if (empty($tracking_id) || empty($phone)) {
|
||||
$response['message'] = 'کد رهگیری و شماره تلفن الزامی است.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = db();
|
||||
$stmt = $db->prepare(
|
||||
"SELECT o.*, CONCAT(u.first_name, ' ', u.last_name) AS full_name, u.email
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id
|
||||
WHERE o.tracking_id = :tracking_id AND o.billing_phone = :phone"
|
||||
);
|
||||
$stmt->bindParam(':tracking_id', $tracking_id);
|
||||
$stmt->bindParam(':phone', $phone);
|
||||
$stmt->execute();
|
||||
$order = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($order) {
|
||||
$order_id = $order['id'];
|
||||
$products_stmt = $db->prepare(
|
||||
"SELECT p.name, p.price, p.image, oi.quantity, oi.color
|
||||
FROM order_items oi
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE oi.order_id = :order_id"
|
||||
);
|
||||
$products_stmt->bindParam(':order_id', $order_id);
|
||||
$products_stmt->execute();
|
||||
$products = $products_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Format creation date
|
||||
$order['created_at_jalali'] = jdate('Y/m/d H:i', strtotime($order['created_at']));
|
||||
|
||||
$response['success'] = true;
|
||||
$response['message'] = 'سفارش یافت شد.';
|
||||
$response['order'] = $order;
|
||||
$response['products'] = $products;
|
||||
} else {
|
||||
$response['message'] = 'سفارشی با این مشخصات یافت نشد.';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Order tracking PDO error: " . $e->getMessage());
|
||||
$response['message'] = 'خطا در برقراری ارتباط با سرور.';
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
} else {
|
||||
echo json_encode($response);
|
||||
}
|
||||
?>
|
||||
File diff suppressed because it is too large
Load Diff
198
assets/css/dark_luxury.css
Normal file
198
assets/css/dark_luxury.css
Normal file
@ -0,0 +1,198 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--luxury-bg: #111214;
|
||||
--luxury-surface: #1a1b1e;
|
||||
--luxury-text: #eceff1;
|
||||
--luxury-text-muted: #90a4ae;
|
||||
--luxury-primary: #c09f80; /* A soft gold for a touch of luxury */
|
||||
--luxury-border: #37474f;
|
||||
}
|
||||
|
||||
body.dark-luxury {
|
||||
background-color: var(--luxury-bg);
|
||||
color: var(--luxury-text);
|
||||
font-family: 'Vazirmatn', sans-serif;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.dark-luxury h1, .dark-luxury h2, .dark-luxury h3, .dark-luxury h4, .dark-luxury h5, .dark-luxury h6 {
|
||||
color: var(--luxury-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dark-luxury .text-muted {
|
||||
color: var(--luxury-text-muted) !important;
|
||||
}
|
||||
|
||||
.dark-luxury a {
|
||||
color: var(--luxury-text);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-luxury a:hover {
|
||||
color: var(--luxury-primary);
|
||||
}
|
||||
|
||||
.dark-luxury .section-title h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dark-luxury .section-title h1::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background-color: var(--luxury-primary);
|
||||
}
|
||||
|
||||
.dark-luxury .contact-card, .dark-luxury .about-card {
|
||||
background-color: var(--luxury-surface);
|
||||
border: 1px solid var(--luxury-border);
|
||||
border-radius: 15px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-luxury .contact-card:hover, .dark-luxury .about-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark-luxury .form-control {
|
||||
background-color: var(--luxury-bg);
|
||||
border: 1px solid var(--luxury-border);
|
||||
color: var(--luxury-text);
|
||||
border-radius: 8px;
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
|
||||
.dark-luxury .form-control:focus {
|
||||
background-color: var(--luxury-bg);
|
||||
color: var(--luxury-text);
|
||||
border-color: var(--luxury-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(192, 159, 128, 0.25);
|
||||
}
|
||||
|
||||
.dark-luxury .form-label {
|
||||
font-weight: 600;
|
||||
color: var(--luxury-text-muted);
|
||||
}
|
||||
|
||||
.dark-luxury .btn-primary {
|
||||
background-color: var(--luxury-primary);
|
||||
border-color: var(--luxury-primary);
|
||||
color: #111214;
|
||||
font-weight: 700;
|
||||
padding: 0.8rem 2rem;
|
||||
border-radius: 50px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-luxury .btn-primary:hover {
|
||||
background-color: #d4b090;
|
||||
border-color: #d4b090;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(192, 159, 128, 0.2);
|
||||
}
|
||||
|
||||
.dark-luxury .contact-info i {
|
||||
color: var(--luxury-primary);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.dark-luxury .contact-info a {
|
||||
color: var(--luxury-text);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-luxury .contact-info a:hover {
|
||||
color: var(--luxury-primary);
|
||||
}
|
||||
|
||||
.dark-luxury .about-image {
|
||||
border-radius: 15px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dark-luxury .values-card {
|
||||
background-color: var(--luxury-surface);
|
||||
border: 1px solid var(--luxury-border);
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.dark-luxury .values-card i {
|
||||
font-size: 2.5rem;
|
||||
color: var(--luxury-primary);
|
||||
}
|
||||
|
||||
/* Product Grid Styles */
|
||||
.dark-luxury .product-card {
|
||||
background-color: var(--luxury-surface);
|
||||
border: 1px solid var(--luxury-border);
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.dark-luxury .product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark-luxury .product-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 4; /* Enforce 3:4 aspect ratio */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark-luxury .product-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; /* Crop image to fit, don't distort */
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.dark-luxury .product-card:hover .product-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.dark-luxury .product-info {
|
||||
padding: 1.25rem;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dark-luxury .product-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dark-luxury .product-title a {
|
||||
color: var(--luxury-text);
|
||||
}
|
||||
|
||||
.dark-luxury .product-title a:hover {
|
||||
color: var(--luxury-primary);
|
||||
}
|
||||
|
||||
.dark-luxury .product-price {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--luxury-primary);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
115
assets/js/checkout_validation.js
Normal file
115
assets/js/checkout_validation.js
Normal file
@ -0,0 +1,115 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.querySelector('form[action="checkout_handler.php"]');
|
||||
if (!form) return;
|
||||
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
|
||||
const validateField = (field) => {
|
||||
const errorContainer = field.parentElement.querySelector('.invalid-feedback');
|
||||
if (!errorContainer) return;
|
||||
|
||||
let isValid = true;
|
||||
let errorMessage = '';
|
||||
|
||||
if (field.value.trim() === '') {
|
||||
isValid = false;
|
||||
errorMessage = 'این فیلد نمیتواند خالی باشد.';
|
||||
} else if (field.type === 'email' && field.value.trim() !== '' && !/^[\S]+@[\S]+\.[\S]+$/.test(field.value)) {
|
||||
isValid = false;
|
||||
errorMessage = 'لطفاً یک ایمیل معتبر وارد کنید.';
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
field.classList.add('is-invalid');
|
||||
errorContainer.textContent = errorMessage;
|
||||
errorContainer.style.display = 'block';
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
errorContainer.style.display = 'none';
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
// Create a container for the error message if it doesn't exist
|
||||
let errorContainer = field.parentElement.querySelector('.invalid-feedback');
|
||||
if (!errorContainer) {
|
||||
errorContainer = document.createElement('div');
|
||||
errorContainer.className = 'invalid-feedback';
|
||||
// Insert after the input field
|
||||
field.parentNode.insertBefore(errorContainer, field.nextSibling);
|
||||
}
|
||||
|
||||
field.addEventListener('blur', () => {
|
||||
validateField(field);
|
||||
});
|
||||
|
||||
// Also validate on input to give immediate feedback
|
||||
field.addEventListener('input', () => {
|
||||
// Only remove error, don't show it while typing
|
||||
if (field.classList.contains('is-invalid')) {
|
||||
validateField(field);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
let isFormValid = true;
|
||||
requiredFields.forEach(field => {
|
||||
if (!validateField(field)) {
|
||||
isFormValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isFormValid) {
|
||||
event.preventDefault(); // Stop form submission
|
||||
// Find the first invalid field and focus it for better UX
|
||||
const firstInvalidField = form.querySelector('.is-invalid');
|
||||
if(firstInvalidField) {
|
||||
firstInvalidField.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle address selection logic from the original file
|
||||
const savedAddressSelect = document.getElementById('saved_address');
|
||||
if (savedAddressSelect) {
|
||||
savedAddressSelect.addEventListener('change', function() {
|
||||
// Clear all fields first
|
||||
document.getElementById('first_name').value = '';
|
||||
document.getElementById('last_name').value = '';
|
||||
document.getElementById('phone_number').value = '';
|
||||
document.getElementById('province').value = '';
|
||||
document.getElementById('city').value = '';
|
||||
document.getElementById('address_line').value = '';
|
||||
document.getElementById('postal_code').value = '';
|
||||
|
||||
// Clear validation states
|
||||
requiredFields.forEach(field => {
|
||||
field.classList.remove('is-invalid');
|
||||
const errorContainer = field.parentElement.querySelector('.invalid-feedback');
|
||||
if(errorContainer) errorContainer.style.display = 'none';
|
||||
});
|
||||
|
||||
if (this.value) {
|
||||
try {
|
||||
const address = JSON.parse(this.value);
|
||||
document.getElementById('first_name').value = address.first_name || '';
|
||||
document.getElementById('last_name').value = address.last_name || '';
|
||||
document.getElementById('phone_number').value = address.phone_number || '';
|
||||
document.getElementById('province').value = address.province || '';
|
||||
document.getElementById('city').value = address.city || '';
|
||||
document.getElementById('address_line').value = address.address_line || '';
|
||||
document.getElementById('postal_code').value = address.postal_code || '';
|
||||
|
||||
// Re-validate all fields after filling them
|
||||
requiredFields.forEach(field => validateField(field));
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to parse address JSON:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
BIN
assets/pasted-20251204-015148-05b34c99.png
Normal file
BIN
assets/pasted-20251204-015148-05b34c99.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/pasted-20251204-202505-499ec228.png
Normal file
BIN
assets/pasted-20251204-202505-499ec228.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
assets/vm-shot-2025-12-04T01-47-45-573Z.jpg
Normal file
BIN
assets/vm-shot-2025-12-04T01-47-45-573Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
121
auth_handler.php
121
auth_handler.php
@ -12,6 +12,9 @@ switch ($action) {
|
||||
case 'verify_otp':
|
||||
handle_verify_otp();
|
||||
break;
|
||||
case 'google_login':
|
||||
handle_google_login();
|
||||
break;
|
||||
case 'logout':
|
||||
handle_logout();
|
||||
break;
|
||||
@ -144,4 +147,122 @@ function flash_message($type, $message, $location) {
|
||||
$_SESSION['flash_message'] = ['type' => $type, 'message' => $message];
|
||||
header("Location: $location");
|
||||
exit;
|
||||
}
|
||||
|
||||
function handle_google_login() {
|
||||
// Load Google credentials from .env
|
||||
$google_client_id = getenv('GOOGLE_CLIENT_ID');
|
||||
$google_client_secret = getenv('GOOGLE_CLIENT_SECRET');
|
||||
|
||||
// The redirect URI must be the exact same one configured in your Google Cloud project
|
||||
$redirect_uri = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . strtok($_SERVER["REQUEST_URI"], '?') . '?action=google_login';
|
||||
|
||||
if (empty($google_client_id) || empty($google_client_secret)) {
|
||||
flash_message('danger', 'قابلیت ورود با گوگل هنوز پیکربندی نشده است.', 'login.php');
|
||||
}
|
||||
|
||||
// If 'code' is not in the query string, this is the initial request. Redirect to Google.
|
||||
if (!isset($_GET['code'])) {
|
||||
$auth_url = 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query([
|
||||
'client_id' => $google_client_id,
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'response_type' => 'code',
|
||||
'scope' => 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
|
||||
'access_type' => 'online',
|
||||
'prompt' => 'select_account'
|
||||
]);
|
||||
header('Location: ' . $auth_url);
|
||||
exit;
|
||||
}
|
||||
// If 'code' is present, this is the callback from Google.
|
||||
else {
|
||||
try {
|
||||
// Step 1: Exchange authorization code for an access token
|
||||
$token_url = 'https://oauth2.googleapis.com/token';
|
||||
$token_data = [
|
||||
'code' => $_GET['code'],
|
||||
'client_id' => $google_client_id,
|
||||
'client_secret' => $google_client_secret,
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'grant_type' => 'authorization_code'
|
||||
];
|
||||
|
||||
$token_response = curl_request($token_url, 'POST', $token_data);
|
||||
|
||||
if (!isset($token_response['access_token'])) {
|
||||
throw new Exception("Failed to get access token from Google. Response: " . json_encode($token_response));
|
||||
}
|
||||
|
||||
// Step 2: Use access token to get user's profile information
|
||||
$userinfo_url = 'https://www.googleapis.com/oauth2/v1/userinfo?access_token=' . $token_response['access_token'];
|
||||
$userinfo = curl_request($userinfo_url);
|
||||
|
||||
if (!isset($userinfo['email'])) {
|
||||
throw new Exception("Failed to get user info from Google. Response: " . json_encode($userinfo));
|
||||
}
|
||||
|
||||
// Step 3: Log in or create user
|
||||
$email = filter_var($userinfo['email'], FILTER_VALIDATE_EMAIL);
|
||||
$first_name = $userinfo['given_name'] ?? '';
|
||||
$last_name = $userinfo['family_name'] ?? '';
|
||||
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$user_id = null;
|
||||
if ($user) {
|
||||
// User exists, log them in
|
||||
$user_id = $user['id'];
|
||||
// Update name if it's missing
|
||||
if (empty($user['first_name']) && !empty($first_name)) {
|
||||
$stmt_update = $pdo->prepare("UPDATE users SET first_name = ?, last_name = ? WHERE id = ?");
|
||||
$stmt_update->execute([$first_name, $last_name, $user_id]);
|
||||
}
|
||||
} else {
|
||||
// User does not exist, create a new one
|
||||
$stmt_create = $pdo->prepare("INSERT INTO users (email, first_name, last_name, is_admin) VALUES (?, ?, ?, 0)");
|
||||
$stmt_create->execute([$email, $first_name, $last_name]);
|
||||
$user_id = $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// Set session variables for login
|
||||
$_SESSION['user_id'] = $user_id;
|
||||
$_SESSION['user_name'] = $first_name;
|
||||
unset($_SESSION['otp_email']); // Clean up OTP session if it exists
|
||||
|
||||
flash_message('success', 'شما با موفقیت با حساب گوگل وارد شدید!', 'index.php');
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('Google Login Error: ' . $e->getMessage());
|
||||
flash_message('danger', 'خطایی در فرآیند ورود با گوگل رخ داد. لطفاً دوباره تلاش کنید.', 'login.php');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function curl_request($url, $method = 'GET', $data = []) {
|
||||
$ch = curl_init();
|
||||
|
||||
if ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
// Set a common user agent
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
$error_msg = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new Exception("cURL Error: " . $error_msg);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
return json_decode($response, true);
|
||||
}
|
||||
@ -24,19 +24,40 @@ $cart_item_id = $product_id . ($color ? '_' . str_replace('#', '', $color) : '')
|
||||
switch ($action) {
|
||||
case 'add':
|
||||
if ($quantity > 0) {
|
||||
// Check if product exists and get details
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT name, price, image_url FROM products WHERE id = ?");
|
||||
// Fetch product details including colors
|
||||
$stmt = $pdo->prepare("SELECT name, price, image_url, colors FROM products WHERE id = ?");
|
||||
$stmt->execute([$product_id]);
|
||||
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($product) {
|
||||
// If item already in cart, update quantity
|
||||
// --- START COLOR VALIDATION ---
|
||||
$available_colors = [];
|
||||
if (!empty($product['colors'])) {
|
||||
$colors_raw = explode(',', $product['colors']);
|
||||
foreach ($colors_raw as $c) {
|
||||
$trimmed_c = trim($c);
|
||||
if ($trimmed_c) $available_colors[] = $trimmed_c;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($available_colors) > 1 && empty($color)) {
|
||||
// For multi-color products, a color must be selected.
|
||||
$_SESSION['flash_message'] = [
|
||||
'type' => 'warning',
|
||||
'message' => 'برای افزودن این محصول، انتخاب یکی از رنگها الزامی است.'
|
||||
];
|
||||
header('Location: product.php?id=' . $product_id);
|
||||
exit;
|
||||
}
|
||||
// --- END COLOR VALIDATION ---
|
||||
|
||||
// If item is already in the cart (same product ID and color), just update the quantity.
|
||||
if (isset($_SESSION['cart'][$cart_item_id])) {
|
||||
$_SESSION['cart'][$cart_item_id]['quantity'] += $quantity;
|
||||
} else {
|
||||
// Otherwise, add new item
|
||||
// Otherwise, add the new item to the cart.
|
||||
$_SESSION['cart'][$cart_item_id] = [
|
||||
'product_id' => $product_id,
|
||||
'name' => $product['name'],
|
||||
@ -46,13 +67,24 @@ switch ($action) {
|
||||
'color' => $color
|
||||
];
|
||||
}
|
||||
$_SESSION['flash_message'] = [
|
||||
'type' => 'success',
|
||||
'message' => 'محصول با موفقیت به سبد خرید اضافه شد!'
|
||||
];
|
||||
} else {
|
||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'محصول یافت نشد.'];
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
// Log error, maybe set a session error message to display in cart
|
||||
error_log("Cart Add Error: " . $e->getMessage());
|
||||
$_SESSION['flash_message'] = [
|
||||
'type' => 'error',
|
||||
'message' => 'مشکلی در افزودن محصول به سبد خرید رخ داد.'
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Redirect back to the previous page (likely the product page) to show the flash message.
|
||||
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? 'shop.php'));
|
||||
exit;
|
||||
|
||||
case 'update':
|
||||
if ($quantity > 0) {
|
||||
@ -60,7 +92,7 @@ switch ($action) {
|
||||
$_SESSION['cart'][$cart_item_id]['quantity'] = $quantity;
|
||||
}
|
||||
} else {
|
||||
// If quantity is 0 or less, remove the item
|
||||
// If quantity is 0 or less, remove the item.
|
||||
unset($_SESSION['cart'][$cart_item_id]);
|
||||
}
|
||||
break;
|
||||
@ -72,6 +104,6 @@ switch ($action) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Redirect back to the cart page to show changes
|
||||
// For 'update' and 'remove' actions, redirect to the cart page to show changes.
|
||||
header('Location: cart.php');
|
||||
exit;
|
||||
|
||||
42
checkout.php
42
checkout.php
@ -76,14 +76,17 @@ require_once 'includes/header.php';
|
||||
<div class="col-md-6">
|
||||
<label for="first_name" class="form-label">نام</label>
|
||||
<input type="text" class="form-control bg-dark text-light" id="first_name" name="first_name" value="<?php echo htmlspecialchars($logged_in_user['first_name'] ?? ''); ?>" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="last_name" class="form-label">نام خانوادگی</label>
|
||||
<input type="text" class="form-control bg-dark text-light" id="last_name" name="last_name" value="<?php echo htmlspecialchars($logged_in_user['last_name'] ?? ''); ?>" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="phone_number" class="form-label">تلفن همراه</label>
|
||||
<input type="tel" class="form-control bg-dark text-light" id="phone_number" name="phone_number" value="<?php echo htmlspecialchars($logged_in_user['phone_number'] ?? ''); ?>" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
<?php if (!$is_logged_in): ?>
|
||||
<div class="form-text text-info fw-bold">توجه: فقط با شماره تلفن همراه میتوان سفارش را رهگیری کرد.</div>
|
||||
<?php endif; ?>
|
||||
@ -91,22 +94,27 @@ require_once 'includes/header.php';
|
||||
<div class="col-md-6">
|
||||
<label for="province" class="form-label">استان</label>
|
||||
<input type="text" class="form-control bg-dark text-light" id="province" name="province" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="city" class="form-label">شهر</label>
|
||||
<input type="text" class="form-control bg-dark text-light" id="city" name="city" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="address_line" class="form-label">آدرس دقیق</label>
|
||||
<textarea class="form-control bg-dark text-light" id="address_line" name="address_line" rows="2" required></textarea>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="postal_code" class="form-label">کد پستی</label>
|
||||
<input type="text" class="form-control bg-dark text-light" id="postal_code" name="postal_code" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label for="email" class="form-label">ایمیل (اختیاری)</label>
|
||||
<input type="email" class="form-control bg-dark text-light" id="email" name="email" value="<?php echo htmlspecialchars($logged_in_user['email'] ?? ''); ?>">
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mt-4">
|
||||
@ -114,6 +122,7 @@ require_once 'includes/header.php';
|
||||
<label class="form-check-label" for="terms">
|
||||
با <a href="#">قوانین و مقررات</a> سایت موافقم.
|
||||
</label>
|
||||
<div class="invalid-feedback">لطفاً قوانین و مقررات را بپذیرید.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -132,6 +141,12 @@ require_once 'includes/header.php';
|
||||
<div>
|
||||
<?php echo htmlspecialchars($item['name']); ?>
|
||||
<small class="d-block text-muted">تعداد: <?php echo $item['quantity']; ?></small>
|
||||
<?php if (!empty($item['color'])): ?>
|
||||
<div class="d-flex align-items-center gx-2 mt-1">
|
||||
<small class="text-muted">رنگ:</small>
|
||||
<span class="d-inline-block rounded-circle border ms-2" style="width: 15px; height: 15px; background-color: <?php echo htmlspecialchars($item['color']); ?>;"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<span class="fw-bold"><?php echo number_format($item['price'] * $item['quantity']); ?></span>
|
||||
@ -165,31 +180,6 @@ require_once 'includes/header.php';
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const savedAddressSelect = document.getElementById('saved_address');
|
||||
if (savedAddressSelect) {
|
||||
savedAddressSelect.addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
try {
|
||||
const address = JSON.parse(this.value);
|
||||
document.getElementById('province').value = address.province || '';
|
||||
document.getElementById('city').value = address.city || '';
|
||||
document.getElementById('address_line').value = address.address_line || '';
|
||||
document.getElementById('postal_code').value = address.postal_code || '';
|
||||
} catch (e) {
|
||||
console.error("Failed to parse address JSON:", e);
|
||||
}
|
||||
} else {
|
||||
// Clear fields if no address is selected
|
||||
document.getElementById('province').value = '';
|
||||
document.getElementById('city').value = '';
|
||||
document.getElementById('address_line').value = '';
|
||||
document.getElementById('postal_code').value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="assets/js/checkout_validation.js?v=<?php echo time(); ?>"></script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
@ -40,16 +40,16 @@ try {
|
||||
$total_amount = array_reduce($cart_items, function ($sum, $item) {
|
||||
return $sum + ($item['price'] * $item['quantity']);
|
||||
}, 0);
|
||||
$items_json = json_encode($cart_items, JSON_UNESCAPED_UNICODE);
|
||||
$products_data_json = json_encode($cart_items, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// 5. Insert the order into the database using the correct, updated column names
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO orders (user_id, billing_name, billing_email, customer_phone, shipping_province, shipping_city, shipping_address_line, shipping_postal_code, total_amount, items_json, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
"INSERT INTO orders (user_id, billing_name, billing_email, billing_phone, billing_province, billing_city, billing_address, billing_postal_code, total_amount, items_json, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$status = 'Pending'; // Default status
|
||||
$final_email = ($email !== false && $email !== '') ? $email : null; // Ensure email is null if empty/invalid, not false
|
||||
$status = 'Processing'; // Default status
|
||||
$final_email = ($email !== false && $email !== '') ? $email : null;
|
||||
|
||||
$stmt->execute([
|
||||
$user_id,
|
||||
@ -61,10 +61,16 @@ try {
|
||||
$address_line,
|
||||
$postal_code,
|
||||
$total_amount,
|
||||
$items_json,
|
||||
$products_data_json,
|
||||
$status
|
||||
]);
|
||||
|
||||
$order_id = $pdo->lastInsertId();
|
||||
$tracking_id = uniqid('ATMH-');
|
||||
|
||||
$update_stmt = $pdo->prepare("UPDATE orders SET tracking_id = ? WHERE id = ?");
|
||||
$update_stmt->execute([$tracking_id, $order_id]);
|
||||
|
||||
// 6. If user is logged in, save the new address for future use
|
||||
if ($user_id) {
|
||||
$stmt_check_addr = $pdo->prepare("SELECT COUNT(*) FROM user_addresses WHERE user_id = ? AND address_line = ? AND postal_code = ?");
|
||||
@ -84,9 +90,12 @@ try {
|
||||
// 7. Commit transaction
|
||||
$pdo->commit();
|
||||
|
||||
// 8. Clear the cart and redirect with a success message
|
||||
// 8. Clear the cart and redirect with a success message including tracking ID
|
||||
unset($_SESSION['cart']);
|
||||
$_SESSION['success_message'] = 'سفارش شما با موفقیت ثبت شد! از خرید شما متشکریم.';
|
||||
$_SESSION['success_message'] = "سفارش شما با موفقیت ثبت شد! کد پیگیری شما: <strong>" . htmlspecialchars($tracking_id) . "</strong>";
|
||||
// As I don't have SMS capability, I am displaying the tracking code here.
|
||||
// You can later integrate an SMS service and send the tracking ID to $phone_number.
|
||||
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
|
||||
|
||||
103
contact.php
103
contact.php
@ -15,10 +15,8 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['contact_form'])) {
|
||||
} elseif (!$email) {
|
||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'آدرس ایمیل وارد شده معتبر نیست.'];
|
||||
} else {
|
||||
// Send email using MailService
|
||||
$to_email = getenv('MAIL_TO') ?: 'your-default-email@example.com'; // Fallback email
|
||||
$to_email = getenv('MAIL_TO') ?: 'support@atimeh.com';
|
||||
$subject = "پیام جدید از فرم تماس وبسایت";
|
||||
|
||||
$email_result = MailService::sendContactMessage($name, $email, $message, $to_email, $subject);
|
||||
|
||||
if (!empty($email_result['success'])) {
|
||||
@ -29,64 +27,101 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['contact_form'])) {
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to the same page to prevent form resubmission
|
||||
header("Location: contact.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Check for flash messages
|
||||
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||
if ($flash_message) {
|
||||
unset($_SESSION['flash_message']);
|
||||
}
|
||||
?>
|
||||
|
||||
<main class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="text-center mb-5" data-aos="fade-down">
|
||||
<h1 class="display-4 fw-bold">ارتباط با ما</h1>
|
||||
<p class="fs-5 text-muted">نظرات، پیشنهادات و سوالات شما برای ما ارزشمند است.</p>
|
||||
</div>
|
||||
<div class="container py-5 my-5">
|
||||
<div class="section-title text-center mb-5" data-aos="fade-down">
|
||||
<h1>ارتباط با ما</h1>
|
||||
<p class="fs-5 text-muted">نظرات، پیشنهادات و سوالات شما برای ما ارزشمند است.</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-lg" data-aos="fade-up">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form action="contact.php" method="POST">
|
||||
<input type="hidden" name="contact_form">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="form-label fs-5">نام شما</label>
|
||||
<input type="text" class="form-control form-control-lg bg-dark" id="name" name="name" required>
|
||||
<div class="contact-card p-4 p-lg-5" data-aos="fade-up">
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-5">
|
||||
<div class="contact-info h-100 d-flex flex-column justify-content-center">
|
||||
<h3 class="mb-4">راههای ارتباطی</h3>
|
||||
<div class="d-flex align-items-start mb-4">
|
||||
<i class="fas fa-map-marker-alt mt-1 me-3"></i>
|
||||
<div>
|
||||
<strong>آدرس:</strong>
|
||||
<p class="text-muted mb-0">تهران، خیابان هنر، کوچه خلاقیت، پلاک ۱۲</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label fs-5">ایمیل</label>
|
||||
<input type="email" class="form-control form-control-lg bg-dark" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="d-flex align-items-start mb-4">
|
||||
<i class="fas fa-envelope mt-1 me-3"></i>
|
||||
<div>
|
||||
<strong>ایمیل:</strong>
|
||||
<p class="mb-0"><a href="mailto:info@atimeh.com">info@atimeh.com</a></p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="message" class="form-label fs-5">پیام شما</label>
|
||||
<textarea class="form-control form-control-lg bg-dark" id="message" name="message" rows="6" required></textarea>
|
||||
</div>
|
||||
<div class="d-flex align-items-start mb-4">
|
||||
<i class="fas fa-phone-alt mt-1 me-3"></i>
|
||||
<div>
|
||||
<strong>تلفن:</strong>
|
||||
<p class="mb-0"><a href="tel:+982112345678">۰۲۱-۱۲۳۴۵۶۷۸</a></p>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg">ارسال پیام</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<hr class="my-4" style="border-color: var(--luxury-border);">
|
||||
<h4 class="h5 mb-3">ما را دنبال کنید</h4>
|
||||
<div class="d-flex">
|
||||
<a href="#" class="btn btn-outline-primary rounded-circle me-2" style="width: 40px; height: 40px; line-height: 25px; text-align: center; padding: 5px;"><i class="fab fa-instagram"></i></a>
|
||||
<a href="#" class="btn btn-outline-primary rounded-circle me-2" style="width: 40px; height: 40px; line-height: 25px; text-align: center; padding: 5px;"><i class="fab fa-telegram"></i></a>
|
||||
<a href="#" class="btn btn-outline-primary rounded-circle" style="width: 40px; height: 40px; line-height: 25px; text-align: center; padding: 5px;"><i class="fab fa-whatsapp"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<h3 class="mb-4">فرم تماس</h3>
|
||||
<form action="contact.php" method="POST">
|
||||
<input type="hidden" name="contact_form">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="form-label">نام شما</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label">ایمیل</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="message" class="form-label">پیام شما</label>
|
||||
<textarea class="form-control" id="message" name="message" rows="7" required></textarea>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">ارسال پیام</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
<?php if ($flash_message): ?>
|
||||
Swal.fire({
|
||||
const swalConfig = {
|
||||
title: '<?php echo $flash_message["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||
text: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||
icon: '<?php echo $flash_message["type"]; ?>',
|
||||
confirmButtonText: 'باشه',
|
||||
background: '#2C2C2C',
|
||||
color: '#D5D5D5'
|
||||
});
|
||||
};
|
||||
|
||||
// Apply dark theme to SweetAlert2
|
||||
if (document.body.classList.contains('dark-luxury')) {
|
||||
swalConfig.background = 'var(--luxury-surface)';
|
||||
swalConfig.color = 'var(--luxury-text)';
|
||||
swalConfig.confirmButtonColor = 'var(--luxury-primary)';
|
||||
}
|
||||
|
||||
Swal.fire(swalConfig);
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
|
||||
1
db/migrations/009_add_tracking_id_to_orders.sql
Normal file
1
db/migrations/009_add_tracking_id_to_orders.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE `orders` ADD `tracking_id` VARCHAR(255) UNIQUE NULL DEFAULT NULL AFTER `status`;
|
||||
1
db/migrations/010_rename_phone_column_in_orders.sql
Normal file
1
db/migrations/010_rename_phone_column_in_orders.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE `orders` CHANGE `customer_phone` `billing_phone` VARCHAR(50) DEFAULT NULL;
|
||||
6
db/migrations/011_consolidate_address_columns.sql
Normal file
6
db/migrations/011_consolidate_address_columns.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- Rename shipping_* columns to billing_* to match the application logic
|
||||
ALTER TABLE `orders`
|
||||
CHANGE `shipping_province` `billing_province` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
CHANGE `shipping_city` `billing_city` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
CHANGE `shipping_address_line` `billing_address` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
CHANGE `shipping_postal_code` `billing_postal_code` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
|
||||
16
db/migrations/012_fix_encoding_for_orders_table.sql
Normal file
16
db/migrations/012_fix_encoding_for_orders_table.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- Convert the orders table and its text-based columns to utf8mb4 to support Persian characters
|
||||
ALTER TABLE `orders` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- Additionally, ensure individual text columns are correctly set
|
||||
ALTER TABLE `orders`
|
||||
MODIFY `billing_name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `billing_email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `legacy_customer_address` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `billing_phone` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `billing_province` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `billing_city` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `billing_address` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `billing_postal_code` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `items_json` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `status` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
MODIFY `tracking_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
6
db/migrations/013_create_page_views_table.sql
Normal file
6
db/migrations/013_create_page_views_table.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS `page_views` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`view_timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`page_url` VARCHAR(2048) NOT NULL,
|
||||
`ip_address` VARCHAR(45) NOT NULL
|
||||
);
|
||||
7
db/migrations/014_add_indexes_to_orders.sql
Normal file
7
db/migrations/014_add_indexes_to_orders.sql
Normal file
@ -0,0 +1,7 @@
|
||||
-- Add indexes to the orders table to improve query performance for the dashboard
|
||||
|
||||
-- Index for the status column, as it's frequently used in WHERE clauses
|
||||
ALTER TABLE `orders` ADD INDEX `idx_status` (`status`);
|
||||
|
||||
-- Index for the order date column, as it's used for grouping and filtering by date
|
||||
ALTER TABLE `orders` ADD INDEX `idx_created_at` (`created_at`);
|
||||
@ -2,8 +2,29 @@
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Log page views for non-admin pages
|
||||
if (strpos($_SERVER['REQUEST_URI'], '/admin/') === false) {
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
try {
|
||||
$pdo = db();
|
||||
// Check if the table exists to avoid errors before migration
|
||||
$table_check = $pdo->query("SHOW TABLES LIKE 'page_views'");
|
||||
if ($table_check->rowCount() > 0) {
|
||||
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||
$page_url = $_SERVER['REQUEST_URI'];
|
||||
$stmt = $pdo->prepare("INSERT INTO page_views (page_url, ip_address) VALUES (?, ?)");
|
||||
$stmt->execute([$page_url, $ip_address]);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
// Silently fail or log to a file to not break the page for users
|
||||
error_log("Could not log page view: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$cart_item_count = isset($_SESSION['cart']) ? array_sum(array_column($_SESSION['cart'], 'quantity')) : 0;
|
||||
$page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
@ -18,6 +39,28 @@ $page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Variable Overrides for Dark Luxury Theme -->
|
||||
<style>
|
||||
:root {
|
||||
--luxury-bg: #111214;
|
||||
--luxury-surface: #1a1b1e;
|
||||
--luxury-text: #eceff1;
|
||||
--luxury-text-muted: #90a4ae;
|
||||
--luxury-primary: #c09f80;
|
||||
--luxury-border: #37474f;
|
||||
|
||||
/* Override Bootstrap's root variables */
|
||||
--bs-body-bg: var(--luxury-bg);
|
||||
--bs-body-color: var(--luxury-text);
|
||||
--bs-primary: var(--luxury-primary);
|
||||
--bs-primary-rgb: 192, 159, 128;
|
||||
--bs-link-color: var(--luxury-primary);
|
||||
--bs-link-hover-color: #d4b090; /* A lighter gold for hover */
|
||||
--bs-border-color: var(--luxury-border);
|
||||
--bs-tertiary-bg: var(--luxury-surface);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
@ -29,6 +72,8 @@ $page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
<link rel="stylesheet" href="/assets/css/dark_luxury.css?v=<?php echo time(); ?>">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
|
||||
<script>
|
||||
// Apply theme from local storage before page load to prevent flashing
|
||||
@ -43,10 +88,9 @@ $page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<body class="dark-luxury">
|
||||
<div class="overflow-hidden">
|
||||
<?php
|
||||
$current_page = basename($_SERVER['PHP_SELF']);
|
||||
$is_admin_page = strpos($_SERVER['REQUEST_URI'], '/admin/') !== false;
|
||||
?>
|
||||
|
||||
|
||||
24
includes/jdf.php
Normal file
24
includes/jdf.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
// Jalali (Shamsi) to Gregorian and vice-versa converter in PHP
|
||||
// Original source: https://github.com/moradin/Jalali/blob/master/jdf.php
|
||||
// Simplified for this project.
|
||||
|
||||
function gregorian_to_jalali($gy, $gm, $gd, $mod='') {
|
||||
$g_d_m = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
|
||||
$jy = ($gy <= 1600) ? 0 : 979;
|
||||
$gy -= ($gy <= 1600) ? 621 : 1600;
|
||||
$gy2 = ($gm > 2) ? ($gy + 1) : $gy;
|
||||
$days = (365 * $gy) + ((int)(($gy2 + 3) / 4)) - ((int)(($gy2 + 99) / 100)) + ((int)(($gy2 + 399) / 400)) - 80 + $gd + $g_d_m[$gm - 1];
|
||||
$jy += 33 * ((int)($days / 12053));
|
||||
$days %= 12053;
|
||||
$jy += 4 * ((int)($days / 1461));
|
||||
$days %= 1461;
|
||||
$jy += (int)(($days - 1) / 365);
|
||||
if ($days > 365) $days = ($days - 1) % 365;
|
||||
$jm = ($days < 186) ? 1 + (int)($days / 31) : 7 + (int)(($days - 186) / 30);
|
||||
$jd = 1 + (($days < 186) ? ($days % 31) : (($days - 186) % 30));
|
||||
return ($mod == '') ? array($jy, $jm, $jd) : $jy . $mod . $jm . $mod . $jd;
|
||||
}
|
||||
|
||||
?>
|
||||
@ -65,10 +65,10 @@ $page_title = "ورود یا ثبتنام";
|
||||
<div class="separator my-4"><span>یا</span></div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-google" disabled>
|
||||
<a href="auth_handler.php?action=google_login" class="btn btn-google">
|
||||
<svg class="me-2" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>
|
||||
ورود با گوگل (به زودی)
|
||||
</button>
|
||||
ورود با گوگل
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="auth-footer">
|
||||
|
||||
115
product.php
115
product.php
@ -32,10 +32,16 @@ if (!$product) {
|
||||
// Set page title after fetching product name
|
||||
$page_title = htmlspecialchars($product['name']);
|
||||
|
||||
// Safely decode colors JSON
|
||||
$available_colors = json_decode($product['colors'] ?? '[]', true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$available_colors = []; // Assign empty array if JSON is invalid
|
||||
// Parse comma-separated colors string
|
||||
$available_colors = [];
|
||||
if (!empty($product['colors'])) {
|
||||
$colors_raw = explode(',', $product['colors']);
|
||||
foreach ($colors_raw as $color) {
|
||||
$trimmed_color = trim($color);
|
||||
if (!empty($trimmed_color)) {
|
||||
$available_colors[] = $trimmed_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
@ -66,7 +72,7 @@ if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
<h5 class="mb-3">انتخاب رنگ:</h5>
|
||||
<div class="color-swatches">
|
||||
<?php foreach ($available_colors as $index => $color_hex): ?>
|
||||
<input type="radio" class="btn-check" name="product_color" id="color_<?php echo $index; ?>" value="<?php echo htmlspecialchars($color_hex); ?>" <?php echo ($index === 0) ? 'checked' : ''; ?>>
|
||||
<input type="radio" class="btn-check" name="product_color" id="color_<?php echo $index; ?>" value="<?php echo htmlspecialchars($color_hex); ?>" <?php echo (count($available_colors) === 1) ? 'checked' : ''; ?>>
|
||||
<label class="btn" for="color_<?php echo $index; ?>" style="background-color: <?php echo htmlspecialchars($color_hex); ?>;" title="<?php echo htmlspecialchars($color_hex); ?>"></label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@ -89,4 +95,103 @@ if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- SweetAlert for color validation -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// --- 1. Flash Message Handling (from server-side) ---
|
||||
<?php
|
||||
if (isset($_SESSION['flash_message'])) {
|
||||
$flash_message = $_SESSION['flash_message'];
|
||||
unset($_SESSION['flash_message']);
|
||||
echo "Swal.fire({
|
||||
title: '" . addslashes($flash_message['message']) . "',
|
||||
icon: '" . $flash_message['type'] . "',
|
||||
toast: true,
|
||||
position: 'top-start',
|
||||
showConfirmButton: false,
|
||||
timer: 4000,
|
||||
timerProgressBar: true,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
},
|
||||
customClass: {
|
||||
popup: 'dark-theme-toast'
|
||||
}
|
||||
});";
|
||||
}
|
||||
?>
|
||||
|
||||
// --- 2. Client-side Color Selection Validation ---
|
||||
const form = document.querySelector('form[action="cart_handler.php"]');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(event) {
|
||||
const availableColors = <?php echo json_encode($available_colors); ?>;
|
||||
const hasMultipleColors = Array.isArray(availableColors) && availableColors.length > 1;
|
||||
|
||||
if (hasMultipleColors) {
|
||||
const selectedColor = document.querySelector('input[name="product_color"]:checked');
|
||||
if (!selectedColor) {
|
||||
event.preventDefault(); // Stop form submission
|
||||
Swal.fire({
|
||||
title: 'لطفاً یک رنگ انتخاب کنید',
|
||||
text: 'برای افزودن این محصول به سبد خرید، انتخاب رنگ الزامی است.',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'متوجه شدم',
|
||||
customClass: {
|
||||
popup: 'dark-theme-popup',
|
||||
title: 'dark-theme-title',
|
||||
htmlContainer: 'dark-theme-content',
|
||||
confirmButton: 'dark-theme-button'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
body.swal2-shown > [aria-hidden="true"] {
|
||||
filter: blur(5px);
|
||||
transition: filter 0.3s ease-out;
|
||||
}
|
||||
.swal2-popup.dark-theme-popup {
|
||||
background-color: #2a2a2e !important;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.swal2-title.dark-theme-title {
|
||||
color: #e8e6e3 !important;
|
||||
}
|
||||
.swal2-html-container.dark-theme-content {
|
||||
color: #b0b0b0 !important;
|
||||
}
|
||||
.swal2-confirm.dark-theme-button {
|
||||
background-color: var(--primary-color) !important;
|
||||
border-radius: 10px;
|
||||
padding: .6em 2em;
|
||||
box-shadow: none !important;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.swal2-confirm.dark-theme-button:hover {
|
||||
background-color: #c89c6c !important; /* A slightly lighter shade of primary for hover */
|
||||
}
|
||||
|
||||
/* Toast Styles */
|
||||
.swal2-toast.dark-theme-toast {
|
||||
background-color: #2a2a2e !important;
|
||||
color: #e8e6e3 !important;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
.swal2-toast.dark-theme-toast .swal2-title {
|
||||
color: #e8e6e3 !important;
|
||||
font-size: 1em;
|
||||
}
|
||||
.swal2-toast.dark-theme-toast .swal2-timer-progress-bar {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
336
profile.php
336
profile.php
@ -1,114 +1,299 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
require_once 'includes/jdf.php'; // For Jalali date conversion
|
||||
|
||||
// Require user to be logged in
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$pdo = db();
|
||||
|
||||
// Fetch user data
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Fetch user addresses
|
||||
$stmt = $pdo->prepare("SELECT * FROM user_addresses WHERE user_id = ? ORDER BY is_default DESC, id DESC");
|
||||
$stmt->execute([$user_id]);
|
||||
$addresses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// Fetch user addresses
|
||||
$stmt_addresses = $pdo->prepare("SELECT * FROM user_addresses WHERE user_id = ? ORDER BY is_default DESC, id DESC");
|
||||
$stmt_addresses->execute([$user_id]);
|
||||
$addresses = $stmt_addresses->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("Error fetching user data: " . $e->getMessage());
|
||||
}
|
||||
// Fetch user orders with items
|
||||
$stmt_orders = $pdo->prepare("SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC");
|
||||
$stmt_orders->execute([$user_id]);
|
||||
$orders = $stmt_orders->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$page_title = 'حساب کاربری من';
|
||||
$page_title = 'حساب کاربری';
|
||||
require_once 'includes/header.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #f4f7f6;
|
||||
}
|
||||
.profile-container {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
.profile-sidebar {
|
||||
flex: 0 0 280px;
|
||||
background-color: #fff;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 25px rgba(0,0,0,0.05);
|
||||
padding: 20px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.profile-content {
|
||||
flex: 1;
|
||||
}
|
||||
.user-card {
|
||||
text-align: center;
|
||||
padding: 20px 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.user-card .user-avatar {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bs-primary);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
margin: 0 auto 15px auto;
|
||||
font-weight: bold;
|
||||
}
|
||||
.user-card h5 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.user-card p {
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.profile-nav .nav-link {
|
||||
color: #6c757d;
|
||||
border-bottom: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 15px;
|
||||
border-radius: 10px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.profile-nav .nav-link.active {
|
||||
.profile-nav .nav-link i {
|
||||
font-size: 1.3rem;
|
||||
color: #888;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.profile-nav .nav-link.active,
|
||||
.profile-nav .nav-link:hover {
|
||||
background-color: var(--bs-primary-light);
|
||||
color: var(--bs-primary);
|
||||
border-bottom-color: var(--bs-primary);
|
||||
}
|
||||
.profile-nav .nav-link.active i,
|
||||
.profile-nav .nav-link:hover i {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.tab-pane h3 {
|
||||
font-weight: 700;
|
||||
margin-bottom: 25px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Order Accordion Styles */
|
||||
.order-accordion .accordion-item {
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 25px rgba(0,0,0,0.05);
|
||||
background-color: #fff;
|
||||
}
|
||||
.order-accordion .accordion-button {
|
||||
border-radius: 15px !important;
|
||||
background-color: #fff;
|
||||
box-shadow: none;
|
||||
padding: 20px;
|
||||
}
|
||||
.order-accordion .accordion-button:not(.collapsed) {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
.order-header-item {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.order-header-item:first-child { text-align: right; }
|
||||
.order-header-item span {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
}
|
||||
.order-header-item strong {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.order-status {
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.order-details-table {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.order-details-table img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.product-color-swatch {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #eee;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="container my-5">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center p-3">
|
||||
<i class="ri-user-smile-line fs-1 text-primary mb-3"></i>
|
||||
<h4 class="card-title"><?php echo htmlspecialchars($user['first_name'] . ' ' . $user['last_name']); ?></h4>
|
||||
<p class="text-muted"><?php echo htmlspecialchars($user['email']); ?></p>
|
||||
</div>
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">افزودن آدرس جدید</h5>
|
||||
<form action="#" method="POST"> <!-- Will be handled by a future handler -->
|
||||
<div class="mb-3">
|
||||
<label for="province" class="form-label">استان</label>
|
||||
<input type="text" class="form-control" id="province" name="province" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="city" class="form-label">شهر</label>
|
||||
<input type="text" class="form-control" id="city" name="city" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="address_line" class="form-label">آدرس دقیق</label>
|
||||
<textarea class="form-control" id="address_line" name="address_line" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="postal_code" class="form-label">کد پستی</label>
|
||||
<input type="text" class="form-control" id="postal_code" name="postal_code" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-secondary">ثبت آدرس</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="profile-container">
|
||||
<!-- Profile Sidebar -->
|
||||
<div class="profile-sidebar">
|
||||
<div class="user-card">
|
||||
<div class="user-avatar">
|
||||
<?php echo strtoupper(substr($user['first_name'], 0, 1)); ?>
|
||||
</div>
|
||||
<h5><?php echo htmlspecialchars($user['first_name'] . ' ' . $user['last_name']); ?></h5>
|
||||
<p><?php echo htmlspecialchars($user['email']); ?></p>
|
||||
</div>
|
||||
<ul class="nav flex-column profile-nav" id="profileTab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="orders-tab" data-bs-toggle="tab" href="#orders" role="tab" aria-controls="orders" aria-selected="true">
|
||||
<i class="ri-shopping-bag-3-line"></i>
|
||||
سفارشات من
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="addresses-tab" data-bs-toggle="tab" href="#addresses" role="tab" aria-controls="addresses" aria-selected="false">
|
||||
<i class="ri-map-pin-line"></i>
|
||||
آدرسهای من
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="logout.php">
|
||||
<i class="ri-logout-box-r-line"></i>
|
||||
خروج از حساب
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">آدرسهای من</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($addresses)): ?>
|
||||
<p class="text-center text-muted">شما هنوز هیچ آدرسی ثبت نکردهاید.</p>
|
||||
|
||||
<!-- Profile Content -->
|
||||
<div class="profile-content">
|
||||
<div class="tab-content" id="profileTabContent">
|
||||
<!-- Orders Tab -->
|
||||
<div class="tab-pane fade show active" id="orders" role="tabpanel" aria-labelledby="orders-tab">
|
||||
<h3>تاریخچه سفارشات</h3>
|
||||
<?php if (empty($orders)): ?>
|
||||
<div class="alert alert-light text-center">شما هنوز هیچ سفارشی ثبت نکردهاید.</div>
|
||||
<?php else: ?>
|
||||
<div class="list-group">
|
||||
<?php foreach ($addresses as $address): ?>
|
||||
<div class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1">
|
||||
<?php echo htmlspecialchars($address['province'] . '، ' . $address['city']); ?>
|
||||
<?php if($address['is_default']): ?>
|
||||
<span class="badge bg-primary">پیشفرض</span>
|
||||
<div class="accordion order-accordion" id="ordersAccordion">
|
||||
<?php foreach ($orders as $index => $order): ?>
|
||||
<?php
|
||||
$items = json_decode($order['items_json'], true);
|
||||
$status_map = [
|
||||
'pending' => ['label' => 'در انتظار پرداخت', 'color' => '#ffc107'],
|
||||
'processing' => ['label' => 'در حال پردازش', 'color' => '#0dcaf0'],
|
||||
'shipped' => ['label' => 'ارسال شده', 'color' => '#0d6efd'],
|
||||
'completed' => ['label' => 'تکمیل شده', 'color' => '#198754'],
|
||||
'cancelled' => ['label' => 'لغو شده', 'color' => '#dc3545'],
|
||||
];
|
||||
$status_info = $status_map[$order['status']] ?? ['label' => htmlspecialchars($order['status']), 'color' => '#6c757d'];
|
||||
?>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading<?php echo $order['id']; ?>">
|
||||
<button class="accordion-button <?php echo $index > 0 ? 'collapsed' : ''; ?>" type="button" data-bs-toggle="collapse" data-bs-target="#collapse<?php echo $order['id']; ?>" aria-expanded="<?php echo $index === 0 ? 'true' : 'false'; ?>" aria-controls="collapse<?php echo $order['id']; ?>">
|
||||
<div class="order-header">
|
||||
<div class="order-header-item">
|
||||
<span>شماره سفارش</span>
|
||||
<strong>#<?php echo $order['id']; ?></strong>
|
||||
</div>
|
||||
<div class="order-header-item">
|
||||
<span>تاریخ ثبت</span>
|
||||
<strong><?php echo jdate('d F Y', strtotime($order['created_at'])); ?></strong>
|
||||
</div>
|
||||
<div class="order-header-item">
|
||||
<span>مبلغ کل</span>
|
||||
<strong><?php echo number_format($order['total_amount']); ?> تومان</strong>
|
||||
</div>
|
||||
<div class="order-header-item text-start">
|
||||
<span class="order-status" style="background-color: <?php echo $status_info['color']; ?>;">
|
||||
<?php echo $status_info['label']; ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse<?php echo $order['id']; ?>" class="accordion-collapse collapse <?php echo $index === 0 ? 'show' : ''; ?>" aria-labelledby="heading<?php echo $order['id']; ?>" data-bs-parent="#ordersAccordion">
|
||||
<div class="accordion-body">
|
||||
<h6>جزئیات سفارش</h6>
|
||||
<?php if (!empty($order['tracking_id'])): ?>
|
||||
<p><strong>کد رهگیری:</strong> <?php echo htmlspecialchars($order['tracking_id']); ?></p>
|
||||
<?php endif; ?>
|
||||
</h6>
|
||||
<small class="text-muted">#<?php echo $address['id']; ?></small>
|
||||
</div>
|
||||
<p class="mb-1"><?php echo htmlspecialchars($address['address_line']); ?></p>
|
||||
<small class="text-muted">کدپستی: <?php echo htmlspecialchars($address['postal_code']); ?></small>
|
||||
<div class="mt-2">
|
||||
<a href="#" class="btn btn-sm btn-outline-danger">حذف</a>
|
||||
<a href="#" class="btn btn-sm btn-outline-primary">ویرایش</a>
|
||||
|
||||
<table class="table order-details-table">
|
||||
<tbody>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<tr>
|
||||
<td><img src="<?php echo htmlspecialchars($item['image_url']); ?>" alt="<?php echo htmlspecialchars($item['name']); ?>"></td>
|
||||
<td>
|
||||
<?php echo htmlspecialchars($item['name']); ?>
|
||||
<?php if (!empty($item['color'])): ?>
|
||||
<br>
|
||||
<small>
|
||||
رنگ:
|
||||
<span class="product-color-swatch" style="background-color: <?php echo htmlspecialchars($item['color']); ?>" title="<?php echo htmlspecialchars($item['color']); ?>"></span>
|
||||
</small>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo $item['quantity']; ?> عدد</td>
|
||||
<td class="text-start"><?php echo number_format($item['price']); ?> تومان</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Addresses Tab -->
|
||||
<div class="tab-pane fade" id="addresses" role="tabpanel" aria-labelledby="addresses-tab">
|
||||
<h3>آدرسهای من</h3>
|
||||
<!-- Address management will be implemented here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -117,3 +302,4 @@ require_once 'includes/header.php';
|
||||
<?php
|
||||
require_once 'includes/footer.php';
|
||||
?>
|
||||
|
||||
|
||||
156
track_order.php
Normal file
156
track_order.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
$page_title = "پیگیری سفارش";
|
||||
include 'includes/header.php';
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<div class="track-container">
|
||||
<h1>پیگیری سفارش</h1>
|
||||
<p>کد رهگیری و شماره تلفن خود را برای مشاهده جزئیات سفارش وارد کنید.</p>
|
||||
<form id="track-order-form">
|
||||
<div class="form-group">
|
||||
<label for="tracking_id">کد رهگیری</label>
|
||||
<input type="text" id="tracking_id" name="tracking_id" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone">شماره تلفن</label>
|
||||
<input type="text" id="phone" name="phone" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-track">جستجو</button>
|
||||
</form>
|
||||
<div id="result-message" style="margin-top: 20px; font-weight: bold;"></div>
|
||||
</div>
|
||||
|
||||
<!-- The Modal -->
|
||||
<div id="order-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn">×</span>
|
||||
<div id="modal-body">
|
||||
<!-- Order details will be injected here by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.getElementById('track-order-form');
|
||||
const modal = document.getElementById('order-modal');
|
||||
const modalBody = document.getElementById('modal-body');
|
||||
const closeBtn = document.querySelector('.close-btn');
|
||||
const resultMessage = document.getElementById('result-message');
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const trackingId = document.getElementById('tracking_id').value;
|
||||
const phone = document.getElementById('phone').value;
|
||||
|
||||
resultMessage.textContent = 'در حال جستجو...';
|
||||
resultMessage.style.color = 'var(--luxury-text-muted)';
|
||||
|
||||
fetch('api/get_order_details.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tracking_id: trackingId,
|
||||
phone: phone
|
||||
}),
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
resultMessage.textContent = '';
|
||||
displayOrderDetails(data.order, data.products);
|
||||
modal.style.display = 'block';
|
||||
} else {
|
||||
resultMessage.textContent = data.message;
|
||||
resultMessage.style.color = '#ff6b6b'; // A clearer error color
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
resultMessage.textContent = 'خطا در برقراری ارتباط با سرور.';
|
||||
resultMessage.style.color = '#ff6b6b';
|
||||
});
|
||||
});
|
||||
|
||||
function displayOrderDetails(order, products) {
|
||||
let productsHtml = `
|
||||
<div class="detail-box" style="grid-column: 1 / -1;">
|
||||
<h3>محصولات سفارش</h3>
|
||||
<table class="products-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>محصول</th>
|
||||
<th>تعداد</th>
|
||||
<th>رنگ</th>
|
||||
<th>قیمت واحد</th>
|
||||
<th>قیمت کل</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
products.forEach(p => {
|
||||
productsHtml += `
|
||||
<tr>
|
||||
<td><img src="assets/images/products/${p.image}" alt="${p.name}">${p.name}</td>
|
||||
<td>${p.quantity}</td>
|
||||
<td>${p.color || '-'}</td>
|
||||
<td>${parseInt(p.price).toLocaleString()} تومان</td>
|
||||
<td>${(p.quantity * p.price).toLocaleString()} تومان</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
productsHtml += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<div class="modal-header">
|
||||
<h2>جزئیات سفارش</h2>
|
||||
<p>کد رهگیری: ${order.tracking_id}</p>
|
||||
</div>
|
||||
<div class="order-details-grid">
|
||||
<div class="detail-box">
|
||||
<h3>اطلاعات خریدار</h3>
|
||||
<p><strong>نام و نام خانوادگی:</strong> ${order.full_name}</p>
|
||||
<p><strong>ایمیل:</strong> ${order.email}</p>
|
||||
<p><strong>تلفن:</strong> ${order.billing_phone}</p>
|
||||
</div>
|
||||
<div class="detail-box">
|
||||
<h3>اطلاعات سفارش</h3>
|
||||
<p><strong>وضعیت:</strong> <span style="font-weight: bold; color: #81c784;">${order.status}</span></p>
|
||||
<p><strong>تاریخ ثبت:</strong> ${order.created_at_jalali}</p>
|
||||
<p><strong>آدرس کامل:</strong> ${order.address}</p>
|
||||
</div>
|
||||
${productsHtml}
|
||||
</div>
|
||||
<div class="total-price-container">
|
||||
<p>جمع کل: ${parseInt(order.total_price).toLocaleString()} تومان</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
closeBtn.onclick = function () {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
window.onclick = function (event) {
|
||||
if (event.target == modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include 'includes/footer.php'; ?>
|
||||
Loading…
x
Reference in New Issue
Block a user