Commit ffa74159 authored by Mahmoud Aglan's avatar Mahmoud Aglan

go

parent 4372c4e4
......@@ -14,7 +14,8 @@ MenuRegistry::register('branches_settings', [
'children' => [
['label_ar' => 'الفروع', 'label_en' => 'Branches', 'route' => '/branches', 'permission' => 'settings.view', 'order' => 1],
['label_ar' => 'إعدادات النظام', 'label_en' => 'Settings', 'route' => '/settings', 'permission' => 'settings.view', 'order' => 2],
['label_ar' => 'سجل المراجعة', 'label_en' => 'Audit Log', 'route' => '/audit', 'permission' => 'report.view_audit', 'order' => 3],
['label_ar' => 'العلامة التجارية', 'label_en' => 'Branding', 'route' => '/settings/branding', 'permission' => 'settings.edit', 'order' => 3],
['label_ar' => 'سجل المراجعة', 'label_en' => 'Audit Log', 'route' => '/audit', 'permission' => 'report.view_audit', 'order' => 4],
],
]);
......
<?php
use App\Modules\Settings\Services\BrandingService;
$design = BrandingService::carnetDesign();
$clubNameAr = BrandingService::clubNameAr();
$clubNameEn = BrandingService::clubNameEn();
$clubSubtitle = BrandingService::subtitle();
$logoUrl = BrandingService::logo();
$bgColor = $design['front_bg_color'];
$textColor = $design['front_text_color'];
$showSub = $design['front_show_subtitle'];
$showEn = $design['front_show_english_name'];
$showType = $design['front_show_member_type'];
$showQr = $design['front_show_qr'];
$qrPos = $design['front_qr_position'];
$logoPos = $design['front_logo_position'];
$showStrip = $design['back_show_branch_strip'];
$stripColor = $design['back_strip_color'];
$showInstr = $design['back_show_instructions'];
$instrText = $design['back_instructions_text'];
$backFields = $design['back_fields'] ?? [];
// QR position CSS
$qrStyle = 'position:absolute;';
if (str_contains($qrPos, 'bottom')) $qrStyle .= 'bottom:15px;'; else $qrStyle .= 'top:15px;';
if (str_contains($qrPos, 'left')) $qrStyle .= 'left:15px;'; else $qrStyle .= 'right:15px;';
// Logo position CSS
$logoAlign = 'text-align:center;';
if ($logoPos === 'top-right') $logoAlign = 'text-align:right;';
elseif ($logoPos === 'top-left') $logoAlign = 'text-align:left;';
// Back field data map
$fieldLabels = [
'full_name_ar' => 'الاسم',
'full_name_en' => 'Name',
'membership_number' => 'رقم العضوية',
'branch_name' => 'الفرع',
'issued_at' => 'تاريخ الإصدار',
'national_id' => 'الرقم القومي',
'phone' => 'الهاتف',
];
$fieldValues = [
'full_name_ar' => $carnet['full_name_ar'] ?? '—',
'full_name_en' => $carnet['full_name_en'] ?? '—',
'membership_number' => $carnet['membership_number'] ?? '—',
'branch_name' => $carnet['branch_name'] ?? '—',
'issued_at' => isset($carnet['issued_at']) ? substr($carnet['issued_at'], 0, 10) : '—',
'national_id' => $carnet['national_id'] ?? '—',
'phone' => $carnet['phone'] ?? '—',
];
?>
<?php $__template->layout('Layout.print'); ?>
<?php $__template->section('title'); ?>كارنيه العضوية — <?= e($carnet['membership_number'] ?? $carnet['carnet_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<style>
@media print { body { margin: 0; } }
@media print { body { margin: 0; } .print-header { display: none; } }
.carnet-container { width: 340px; margin: 20px auto; font-family: 'Cairo', Arial, sans-serif; }
.carnet-front {
width: 340px; height: 215px; background: #0D7377; border-radius: 12px;
color: #fff; padding: 20px; position: relative; overflow: hidden; margin-bottom: 15px;
width: 340px; height: 215px; background: <?= e($bgColor) ?>; border-radius: 12px;
color: <?= e($textColor) ?>; padding: 20px; position: relative; overflow: hidden; margin-bottom: 15px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.carnet-back {
width: 340px; height: 215px; background: #fff; border-radius: 12px;
border: 2px solid #0D7377; padding: 20px; position: relative;
border: 2px solid <?= e($stripColor) ?>; padding: 20px; position: relative;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.carnet-front .club-name { text-align: center; font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.carnet-front .club-subtitle { text-align: center; font-size: 11px; opacity: 0.8; margin-bottom: 15px; }
.carnet-front .club-name { <?= $logoAlign ?> font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.carnet-front .club-subtitle { <?= $logoAlign ?> font-size: 11px; opacity: 0.8; margin-bottom: 15px; }
.carnet-front .front-logo { <?= $logoAlign ?> margin-bottom: 8px; }
.carnet-front .front-logo img { max-height: 30px; }
.carnet-front .member-name { font-size: 14px; font-weight: 700; margin-bottom: 5px; }
.carnet-front .member-name-en { font-size: 11px; opacity: 0.8; margin-bottom: 8px; }
.carnet-front .member-number { font-size: 20px; font-weight: 700; letter-spacing: 2px; direction: ltr; text-align: right; }
.carnet-front .qr-area { position: absolute; bottom: 15px; left: 15px; width: 70px; height: 70px; background: #fff; border-radius: 6px; padding: 5px; }
.carnet-front .member-type { font-size: 11px; margin-top: 5px; opacity: 0.8; }
.carnet-front .qr-area { <?= $qrStyle ?> width: 70px; height: 70px; background: #fff; border-radius: 6px; padding: 5px; }
.carnet-front .qr-area svg { width: 60px; height: 60px; }
.carnet-front .carnet-num { position: absolute; bottom: 15px; right: 15px; font-size: 10px; opacity: 0.7; }
.carnet-back .back-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 2px solid #0D7377; padding-bottom: 10px; }
.carnet-back .back-header .logo-text { font-size: 12px; font-weight: 700; color: #0D7377; }
.carnet-back .branch-strip { position: absolute; left: 0; top: 0; bottom: 0; width: 30px; background: #0D7377; border-radius: 12px 0 0 12px; writing-mode: vertical-rl; text-orientation: mixed; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 11px; font-weight: 600; }
.carnet-back .back-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 2px solid <?= e($stripColor) ?>; padding-bottom: 10px; }
.carnet-back .back-header .logo-text { font-size: 12px; font-weight: 700; color: <?= e($stripColor) ?>; }
.carnet-back .back-header .back-logo img { max-height: 25px; }
.carnet-back .branch-strip { position: absolute; left: 0; top: 0; bottom: 0; width: 30px; background: <?= e($stripColor) ?>; border-radius: 12px 0 0 12px; writing-mode: vertical-rl; text-orientation: mixed; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 11px; font-weight: 600; }
.carnet-back .instructions { font-size: 10px; color: #6B7280; text-align: center; position: absolute; bottom: 10px; left: 40px; right: 10px; }
.carnet-back .member-info { margin-right: 35px; font-size: 12px; color: #1A1A2E; }
.carnet-back .member-info div { margin-bottom: 4px; }
.carnet-back .member-info strong { color: #0D7377; }
.carnet-back .member-info strong { color: <?= e($stripColor) ?>; }
</style>
<div class="carnet-container">
<!-- Front -->
<div class="carnet-front">
<div class="club-name">THE CLUB — نادي النادي شيراتون</div>
<div class="club-subtitle">SPORTS CITY</div>
<?php if ($logoUrl): ?>
<div class="front-logo"><img src="<?= e($logoUrl) ?>" alt="Logo"></div>
<?php endif; ?>
<div class="club-name"><?= e($clubNameEn) ?><?= e($clubNameAr) ?></div>
<?php if ($showSub): ?>
<div class="club-subtitle"><?= e($clubSubtitle) ?></div>
<?php endif; ?>
<div class="member-name"><?= e($carnet['full_name_ar']) ?></div>
<?php if ($carnet['full_name_en']): ?>
<div style="font-size:11px;opacity:0.8;margin-bottom:8px;"><?= e($carnet['full_name_en']) ?></div>
<?php if ($showEn && !empty($carnet['full_name_en'])): ?>
<div class="member-name-en"><?= e($carnet['full_name_en']) ?></div>
<?php endif; ?>
<div class="member-number"><?= e($carnet['membership_number'] ?? '—') ?></div>
<div style="font-size:11px;margin-top:5px;opacity:0.8;">
<?= $carnet['carnet_type'] === 'seasonal' ? 'عضوية موسمية' : 'عضو عامل' ?>
</div>
<?php if ($showType): ?>
<div class="member-type"><?= $carnet['carnet_type'] === 'seasonal' ? 'عضوية موسمية' : 'عضو عامل' ?></div>
<?php endif; ?>
<?php if ($showQr): ?>
<div class="qr-area"><?= $qrSvg ?></div>
<?php endif; ?>
<div class="carnet-num"><?= e($carnet['carnet_number']) ?></div>
</div>
<!-- Back -->
<div class="carnet-back">
<?php if ($showStrip): ?>
<div class="branch-strip"><?= e($carnet['branch_name'] ?? 'فرع شيراتون') ?></div>
<?php endif; ?>
<div class="back-header">
<div class="logo-text">THE CLUB<br>نادي النادي</div>
<div style="font-size:20px;">🇪🇬</div>
<?php if ($logoUrl): ?>
<div class="back-logo"><img src="<?= e($logoUrl) ?>" alt="Logo"></div>
<?php else: ?>
<div class="logo-text"><?= e($clubNameEn) ?><br><?= e($clubNameAr) ?></div>
<?php endif; ?>
</div>
<div class="member-info">
<div><strong>الاسم:</strong> <?= e($carnet['full_name_ar']) ?></div>
<div><strong>رقم العضوية:</strong> <?= e($carnet['membership_number'] ?? '—') ?></div>
<div><strong>الفرع:</strong> <?= e($carnet['branch_name'] ?? '—') ?></div>
<div><strong>تاريخ الإصدار:</strong> <?= e(substr($carnet['issued_at'], 0, 10)) ?></div>
<?php foreach ($backFields as $field): ?>
<?php if (isset($fieldLabels[$field])): ?>
<div><strong><?= e($fieldLabels[$field]) ?>:</strong> <?= e($fieldValues[$field] ?? '—') ?></div>
<?php endif; ?>
<?php endforeach; ?>
</div>
<div class="instructions">برجاء حمل هذه البطاقة أثناء التواجد بالنادي وتقديمها عند الطلب</div>
<?php if ($showInstr): ?>
<div class="instructions"><?= e($instrText) ?></div>
<?php endif; ?>
</div>
</div>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Settings\Services\BrandingService;
$rd = BrandingService::receiptDesign();
$clubNameAr = BrandingService::clubNameAr();
$clubNameEn = BrandingService::clubNameEn();
$logoUrl = BrandingService::logo();
$showLogo = $rd['show_logo'];
$showWatermark = $rd['show_watermark'];
$watermarkText = $rd['watermark_text'];
$watermarkOp = $rd['watermark_opacity'];
$headerColor = $rd['header_color'];
$showFooter = $rd['show_footer_print_info'];
?>
<?php $__template->layout('Layout.print'); ?>
<?php $__template->section('title'); ?>إيصال رقم <?= e($receipt['receipt_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="max-width:700px;margin:0 auto;font-family:'Cairo',sans-serif;direction:rtl;">
<div style="max-width:700px;margin:0 auto;font-family:'Cairo',sans-serif;direction:rtl;position:relative;overflow:hidden;">
<?php if ($showWatermark && $watermarkText): ?>
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) rotate(-35deg);font-size:52px;font-weight:900;color:<?= e($headerColor) ?>;opacity:<?= e($watermarkOp) ?>;white-space:nowrap;pointer-events:none;z-index:0;">
<?= e($watermarkText) ?>
</div>
<?php endif; ?>
<div style="position:relative;z-index:1;">
<?php if ($receipt['is_voided']): ?>
<div style="text-align:center;color:#DC2626;font-size:24px;font-weight:700;border:3px solid #DC2626;padding:10px;margin-bottom:20px;">ملغى</div>
<?php endif; ?>
<div style="text-align:center;margin-bottom:30px;">
<h2 style="margin:0;color:#0D7377;">نادي النادي شيراتون</h2>
<p style="margin:5px 0;color:#6B7280;">THE CLUB Sheraton</p>
<div style="text-align:center;margin-bottom:30px;padding-bottom:16px;border-bottom:2px solid <?= e($headerColor) ?>;">
<?php if ($showLogo && $logoUrl): ?>
<div style="margin-bottom:10px;"><img src="<?= e($logoUrl) ?>" alt="Logo" style="max-height:45px;"></div>
<?php endif; ?>
<h2 style="margin:0;color:<?= e($headerColor) ?>;"><?= e($clubNameAr) ?></h2>
<p style="margin:5px 0;color:#6B7280;"><?= e($clubNameEn) ?></p>
<h3 style="margin:15px 0 5px;color:#1A1A2E;">إيصال تحصيل</h3>
</div>
......@@ -36,7 +62,7 @@
</tr>
<tr>
<td style="padding:8px;border:1px solid #E5E7EB;background:#F9FAFB;font-weight:600;">المبلغ (رقماً)</td>
<td style="padding:8px;border:1px solid #E5E7EB;font-size:22px;font-weight:700;color:#0D7377;direction:ltr;text-align:right;"><?= money($receipt['amount']) ?></td>
<td style="padding:8px;border:1px solid #E5E7EB;font-size:22px;font-weight:700;color:<?= e($headerColor) ?>;direction:ltr;text-align:right;"><?= money($receipt['amount']) ?></td>
</tr>
<tr>
<td style="padding:8px;border:1px solid #E5E7EB;background:#F9FAFB;font-weight:600;">المبلغ (كتابةً)</td>
......@@ -62,8 +88,11 @@
<div style="border-top:1px solid #000;padding-top:10px;">المدير المالي</div>
</div>
<?php if ($showFooter): ?>
<div style="margin-top:30px;text-align:center;font-size:11px;color:#9CA3AF;">
طبع بتاريخ: <?= date('Y-m-d H:i:s') ?> — عدد مرات الطباعة: <?= (int) ($receipt['print_count'] ?? 0) + 1 ?>
</div>
<?php endif; ?>
</div>
</div>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
namespace App\Modules\Settings\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Settings\Models\SystemConfig;
use App\Modules\Settings\Services\BrandingService;
class BrandingController extends Controller
{
public function index(Request $request): Response
{
return $this->view('Settings.Views.branding', [
'logo' => BrandingService::logo(),
'clubNameAr' => BrandingService::clubNameAr(),
'clubNameEn' => BrandingService::clubNameEn(),
'subtitle' => BrandingService::subtitle(),
'carnetDesign' => BrandingService::carnetDesign(),
'receiptDesign' => BrandingService::receiptDesign(),
]);
}
public function updateLogo(Request $request): Response
{
// Handle club name / subtitle fields
$clubNameAr = trim($request->post('club_name_ar', ''));
$clubNameEn = trim($request->post('club_name_en', ''));
$subtitle = trim($request->post('club_subtitle', ''));
if ($clubNameAr !== '') {
SystemConfig::set('branding.club_name_ar', $clubNameAr);
}
if ($clubNameEn !== '') {
SystemConfig::set('branding.club_name_en', $clubNameEn);
}
SystemConfig::set('branding.club_subtitle', $subtitle);
// Handle logo upload
if ($request->hasFile('logo_file')) {
$file = $request->file('logo_file');
if ($file && $file['error'] === UPLOAD_ERR_OK) {
// Validate size (max 2MB)
if ($file['size'] > 2 * 1024 * 1024) {
return $this->redirect('/settings/branding')->withError('حجم الشعار يجب أن لا يتجاوز 2 ميجا');
}
// Validate MIME type
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
$allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml', 'image/webp'];
if (!in_array($mimeType, $allowedTypes)) {
return $this->redirect('/settings/branding')->withError('نوع الملف غير مسموح (PNG, JPG, SVG, WebP فقط)');
}
// Generate filename
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($ext === 'svg') $ext = 'svg';
$storedFilename = 'logo_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
// Ensure upload directory exists
$uploadDir = App::getInstance()->basePath() . '/storage/uploads/branding/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$filePath = $uploadDir . $storedFilename;
if (!move_uploaded_file($file['tmp_name'], $filePath)) {
return $this->redirect('/settings/branding')->withError('فشل في حفظ الشعار');
}
// Delete old logo file if exists
$oldPath = BrandingService::logoFilePath();
if ($oldPath && file_exists($oldPath)) {
@unlink($oldPath);
}
// Save path to config
SystemConfig::set('branding.logo_path', 'storage/uploads/branding/' . $storedFilename);
BrandingService::clearCache();
}
}
// Handle logo removal
if ($request->post('remove_logo') === '1') {
$oldPath = BrandingService::logoFilePath();
if ($oldPath && file_exists($oldPath)) {
@unlink($oldPath);
}
SystemConfig::set('branding.logo_path', '');
}
BrandingService::clearCache();
return $this->redirect('/settings/branding')->withSuccess('تم تحديث الهوية البصرية بنجاح');
}
public function updateCarnetDesign(Request $request): Response
{
$design = [
'front_bg_color' => $request->post('front_bg_color', '#0D7377'),
'front_text_color' => $request->post('front_text_color', '#ffffff'),
'front_show_subtitle' => $request->post('front_show_subtitle') === '1',
'front_show_english_name'=> $request->post('front_show_english_name') === '1',
'front_show_member_type' => $request->post('front_show_member_type') === '1',
'front_show_qr' => $request->post('front_show_qr') === '1',
'front_qr_position' => $request->post('front_qr_position', 'bottom-left'),
'front_logo_position' => $request->post('front_logo_position', 'top-center'),
'back_show_branch_strip' => $request->post('back_show_branch_strip') === '1',
'back_strip_color' => $request->post('back_strip_color', '#0D7377'),
'back_show_instructions' => $request->post('back_show_instructions') === '1',
'back_instructions_text' => trim($request->post('back_instructions_text', '')),
'back_fields' => $request->post('back_fields') ? (array) $request->post('back_fields') : ['full_name_ar', 'membership_number'],
];
$json = json_encode($design, JSON_UNESCAPED_UNICODE);
SystemConfig::set('branding.carnet_design', $json);
BrandingService::clearCache();
return $this->redirect('/settings/branding')->withSuccess('تم تحديث تصميم الكارنيه بنجاح');
}
public function updateReceiptDesign(Request $request): Response
{
$design = [
'show_logo' => $request->post('show_logo') === '1',
'show_watermark' => $request->post('show_watermark') === '1',
'watermark_text' => trim($request->post('watermark_text', '')),
'watermark_opacity' => max(0.01, min(0.2, (float) $request->post('watermark_opacity', '0.06'))),
'header_color' => $request->post('header_color', '#0D7377'),
'show_footer_print_info' => $request->post('show_footer_print_info') === '1',
];
$json = json_encode($design, JSON_UNESCAPED_UNICODE);
SystemConfig::set('branding.receipt_design', $json);
BrandingService::clearCache();
return $this->redirect('/settings/branding')->withSuccess('تم تحديث تصميم الإيصال بنجاح');
}
}
......@@ -5,4 +5,10 @@ return [
['GET', '/settings', 'Settings\Controllers\SettingsController@index', ['auth'], 'settings.view'],
['GET', '/settings/group/{group}', 'Settings\Controllers\SettingsController@editGroup', ['auth'], 'settings.edit'],
['POST', '/settings/group/{group}', 'Settings\Controllers\SettingsController@updateGroup', ['auth', 'csrf'], 'settings.edit'],
// Branding
['GET', '/settings/branding', 'Settings\Controllers\BrandingController@index', ['auth'], 'settings.edit'],
['POST', '/settings/branding/logo', 'Settings\Controllers\BrandingController@updateLogo', ['auth', 'csrf'], 'settings.edit'],
['POST', '/settings/branding/carnet', 'Settings\Controllers\BrandingController@updateCarnetDesign', ['auth', 'csrf'], 'settings.edit'],
['POST', '/settings/branding/receipt', 'Settings\Controllers\BrandingController@updateReceiptDesign',['auth', 'csrf'], 'settings.edit'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Settings\Services;
use App\Core\App;
/**
* BrandingService — Centralized access to branding configuration.
* Reads from system_config (group: branding) and caches per-request.
*/
class BrandingService
{
private static ?array $cache = null;
private static function load(): array
{
if (self::$cache !== null) {
return self::$cache;
}
try {
$db = App::getInstance()->db();
$rows = $db->select(
"SELECT config_key, config_value, config_type FROM system_config WHERE group_name = 'branding'"
);
} catch (\Throwable $e) {
$rows = [];
}
$data = [];
foreach ($rows as $row) {
$key = str_replace('branding.', '', $row['config_key']);
$value = $row['config_value'];
if ($row['config_type'] === 'json' && $value) {
$decoded = json_decode($value, true);
$data[$key] = is_array($decoded) ? $decoded : [];
} else {
$data[$key] = $value;
}
}
self::$cache = $data;
return $data;
}
/**
* Clear the cache (after updating branding settings).
*/
public static function clearCache(): void
{
self::$cache = null;
}
/**
* Get URL path to the logo image, or null if not set.
*/
public static function logo(): ?string
{
$data = self::load();
$path = $data['logo_path'] ?? null;
if ($path && $path !== '') {
// Return as web-accessible URL relative to public/
// Logo is stored in storage/uploads/branding/ — we need to serve it
// via a relative path from the app root
$basePath = rtrim(parse_url(config('app.url', ''), PHP_URL_PATH) ?: '', '/');
return $basePath . '/' . ltrim($path, '/');
}
return null;
}
/**
* Get the absolute file path to the logo, or null.
*/
public static function logoFilePath(): ?string
{
$data = self::load();
$path = $data['logo_path'] ?? null;
if ($path && $path !== '') {
$full = App::getInstance()->basePath() . '/' . ltrim($path, '/');
return file_exists($full) ? $full : null;
}
return null;
}
public static function clubNameAr(): string
{
$data = self::load();
return $data['club_name_ar'] ?? 'نادي النادي شيراتون';
}
public static function clubNameEn(): string
{
$data = self::load();
return $data['club_name_en'] ?? 'THE CLUB Sheraton';
}
public static function subtitle(): string
{
$data = self::load();
return $data['club_subtitle'] ?? 'SPORTS CITY';
}
/**
* Get carnet design configuration as an associative array.
*/
public static function carnetDesign(): array
{
$data = self::load();
$design = $data['carnet_design'] ?? [];
// Merge with defaults so views always have every key
return array_merge([
'front_bg_color' => '#0D7377',
'front_text_color' => '#ffffff',
'front_show_subtitle' => true,
'front_show_english_name' => true,
'front_show_member_type'=> true,
'front_show_qr' => true,
'front_qr_position' => 'bottom-left',
'front_logo_position' => 'top-center',
'back_show_branch_strip'=> true,
'back_strip_color' => '#0D7377',
'back_show_instructions'=> true,
'back_instructions_text'=> 'برجاء حمل هذه البطاقة أثناء التواجد بالنادي وتقديمها عند الطلب',
'back_fields' => ['full_name_ar', 'membership_number', 'branch_name', 'issued_at'],
], $design);
}
/**
* Get receipt design configuration as an associative array.
*/
public static function receiptDesign(): array
{
$data = self::load();
$design = $data['receipt_design'] ?? [];
return array_merge([
'show_logo' => true,
'show_watermark' => true,
'watermark_text' => 'نادي النادي شيراتون',
'watermark_opacity' => 0.06,
'header_color' => '#0D7377',
'show_footer_print_info'=> true,
], $design);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>العلامة التجارية<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<link rel="stylesheet" href="<?= url('assets/css/branding.css') ?>">
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
use App\Core\CSRF;
$cd = $carnetDesign;
$rd = $receiptDesign;
?>
<!-- Tabs -->
<div class="branding-tabs">
<button class="branding-tab active" data-tab="identity" onclick="switchTab(this)">
<i data-lucide="image"></i>
<span>الشعار والهوية</span>
</button>
<button class="branding-tab" data-tab="carnet" onclick="switchTab(this)">
<i data-lucide="id-card"></i>
<span>تصميم الكارنيه</span>
</button>
<button class="branding-tab" data-tab="receipt" onclick="switchTab(this)">
<i data-lucide="receipt"></i>
<span>تصميم الإيصال</span>
</button>
</div>
<!-- ============================================ -->
<!-- TAB 1: Logo & Identity -->
<!-- ============================================ -->
<div class="tab-panel active" id="tab-identity">
<form method="POST" action="/settings/branding/logo" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= e(CSRF::token()) ?>">
<div class="branding-grid">
<!-- Logo Upload Card -->
<div class="card branding-card">
<div class="card-header">
<h3><i data-lucide="upload"></i> شعار النادي</h3>
</div>
<div class="card-body">
<div class="logo-upload-area" id="logo-upload-area">
<?php if ($logo): ?>
<div class="logo-preview" id="logo-preview">
<img src="<?= e($logo) ?>" alt="شعار النادي" id="logo-preview-img">
<div class="logo-preview-overlay">
<button type="button" class="btn btn-sm btn-danger" onclick="removeLogo()">
<i data-lucide="trash-2"></i> حذف
</button>
</div>
</div>
<?php else: ?>
<div class="logo-placeholder" id="logo-placeholder" onclick="document.getElementById('logo_file').click()">
<i data-lucide="image-plus"></i>
<p>اضغط لرفع الشعار</p>
<small>PNG, JPG, SVG, WebP — أقصى حجم 2MB</small>
</div>
<?php endif; ?>
<input type="file" name="logo_file" id="logo_file" accept="image/png,image/jpeg,image/svg+xml,image/webp" style="display:none;" onchange="previewLogo(this)">
<input type="hidden" name="remove_logo" id="remove_logo" value="0">
</div>
</div>
</div>
<!-- Club Name Card -->
<div class="card branding-card">
<div class="card-header">
<h3><i data-lucide="type"></i> بيانات النادي</h3>
</div>
<div class="card-body">
<div class="form-group">
<label class="form-label">اسم النادي بالعربية</label>
<input type="text" name="club_name_ar" class="form-input" value="<?= e($clubNameAr) ?>" placeholder="نادي النادي شيراتون" dir="rtl">
</div>
<div class="form-group">
<label class="form-label">اسم النادي بالإنجليزية</label>
<input type="text" name="club_name_en" class="form-input" value="<?= e($clubNameEn) ?>" placeholder="THE CLUB Sheraton" dir="ltr" style="text-align:left;">
</div>
<div class="form-group">
<label class="form-label">العنوان الفرعي</label>
<input type="text" name="club_subtitle" class="form-input" value="<?= e($subtitle) ?>" placeholder="SPORTS CITY" dir="ltr" style="text-align:left;">
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i data-lucide="save"></i>
حفظ الهوية البصرية
</button>
</div>
</form>
</div>
<!-- ============================================ -->
<!-- TAB 2: Carnet Designer -->
<!-- ============================================ -->
<div class="tab-panel" id="tab-carnet">
<form method="POST" action="/settings/branding/carnet">
<input type="hidden" name="csrf_token" value="<?= e(CSRF::token()) ?>">
<div class="designer-layout">
<!-- Settings Panel -->
<div class="designer-settings">
<!-- Front Card Settings -->
<div class="card branding-card">
<div class="card-header">
<h3><i data-lucide="credit-card"></i> الوجه الأمامي</h3>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<label class="form-label">لون الخلفية</label>
<div class="color-input-wrap">
<input type="color" name="front_bg_color" value="<?= e($cd['front_bg_color']) ?>" id="front_bg_color" onchange="updateCarnetPreview()">
<input type="text" class="form-input form-input-sm color-hex" value="<?= e($cd['front_bg_color']) ?>" onchange="this.previousElementSibling.value=this.value;updateCarnetPreview()" dir="ltr">
</div>
</div>
<div class="form-group">
<label class="form-label">لون النص</label>
<div class="color-input-wrap">
<input type="color" name="front_text_color" value="<?= e($cd['front_text_color']) ?>" id="front_text_color" onchange="updateCarnetPreview()">
<input type="text" class="form-input form-input-sm color-hex" value="<?= e($cd['front_text_color']) ?>" onchange="this.previousElementSibling.value=this.value;updateCarnetPreview()" dir="ltr">
</div>
</div>
</div>
<div class="toggle-group">
<label class="toggle-label">
<input type="hidden" name="front_show_subtitle" value="0">
<input type="checkbox" name="front_show_subtitle" value="1" <?= $cd['front_show_subtitle'] ? 'checked' : '' ?> onchange="updateCarnetPreview()">
<span class="toggle-switch"></span>
<span>إظهار العنوان الفرعي</span>
</label>
<label class="toggle-label">
<input type="hidden" name="front_show_english_name" value="0">
<input type="checkbox" name="front_show_english_name" value="1" <?= $cd['front_show_english_name'] ? 'checked' : '' ?> onchange="updateCarnetPreview()">
<span class="toggle-switch"></span>
<span>إظهار الاسم الإنجليزي</span>
</label>
<label class="toggle-label">
<input type="hidden" name="front_show_member_type" value="0">
<input type="checkbox" name="front_show_member_type" value="1" <?= $cd['front_show_member_type'] ? 'checked' : '' ?> onchange="updateCarnetPreview()">
<span class="toggle-switch"></span>
<span>إظهار نوع العضوية</span>
</label>
<label class="toggle-label">
<input type="hidden" name="front_show_qr" value="0">
<input type="checkbox" name="front_show_qr" value="1" <?= $cd['front_show_qr'] ? 'checked' : '' ?> onchange="updateCarnetPreview()">
<span class="toggle-switch"></span>
<span>إظهار QR Code</span>
</label>
</div>
<div class="form-group">
<label class="form-label">موضع QR Code</label>
<select name="front_qr_position" class="form-input" id="front_qr_position" onchange="updateCarnetPreview()">
<option value="bottom-left" <?= $cd['front_qr_position'] === 'bottom-left' ? 'selected' : '' ?>>أسفل يسار</option>
<option value="bottom-right" <?= $cd['front_qr_position'] === 'bottom-right' ? 'selected' : '' ?>>أسفل يمين</option>
<option value="top-left" <?= $cd['front_qr_position'] === 'top-left' ? 'selected' : '' ?>>أعلى يسار</option>
<option value="top-right" <?= $cd['front_qr_position'] === 'top-right' ? 'selected' : '' ?>>أعلى يمين</option>
</select>
</div>
<div class="form-group">
<label class="form-label">موضع الشعار</label>
<select name="front_logo_position" class="form-input" id="front_logo_position" onchange="updateCarnetPreview()">
<option value="top-center" <?= $cd['front_logo_position'] === 'top-center' ? 'selected' : '' ?>>أعلى الوسط</option>
<option value="top-right" <?= $cd['front_logo_position'] === 'top-right' ? 'selected' : '' ?>>أعلى يمين</option>
<option value="top-left" <?= $cd['front_logo_position'] === 'top-left' ? 'selected' : '' ?>>أعلى يسار</option>
</select>
</div>
</div>
</div>
<!-- Back Card Settings -->
<div class="card branding-card">
<div class="card-header">
<h3><i data-lucide="flip-horizontal"></i> الوجه الخلفي</h3>
</div>
<div class="card-body">
<div class="toggle-group">
<label class="toggle-label">
<input type="hidden" name="back_show_branch_strip" value="0">
<input type="checkbox" name="back_show_branch_strip" value="1" <?= $cd['back_show_branch_strip'] ? 'checked' : '' ?> onchange="updateCarnetPreview()">
<span class="toggle-switch"></span>
<span>إظهار شريط الفرع</span>
</label>
<label class="toggle-label">
<input type="hidden" name="back_show_instructions" value="0">
<input type="checkbox" name="back_show_instructions" value="1" <?= $cd['back_show_instructions'] ? 'checked' : '' ?> onchange="updateCarnetPreview()">
<span class="toggle-switch"></span>
<span>إظهار التعليمات</span>
</label>
</div>
<div class="form-group">
<label class="form-label">لون شريط الفرع</label>
<div class="color-input-wrap">
<input type="color" name="back_strip_color" value="<?= e($cd['back_strip_color']) ?>" id="back_strip_color" onchange="updateCarnetPreview()">
<input type="text" class="form-input form-input-sm color-hex" value="<?= e($cd['back_strip_color']) ?>" onchange="this.previousElementSibling.value=this.value;updateCarnetPreview()" dir="ltr">
</div>
</div>
<div class="form-group">
<label class="form-label">نص التعليمات</label>
<textarea name="back_instructions_text" class="form-input" rows="2" onchange="updateCarnetPreview()" id="back_instructions_text"><?= e($cd['back_instructions_text']) ?></textarea>
</div>
<div class="form-group">
<label class="form-label">حقول الوجه الخلفي</label>
<?php
$allFields = [
'full_name_ar' => 'الاسم بالعربية',
'full_name_en' => 'الاسم بالإنجليزية',
'membership_number' => 'رقم العضوية',
'branch_name' => 'الفرع',
'issued_at' => 'تاريخ الإصدار',
'national_id' => 'الرقم القومي',
'phone' => 'الهاتف',
];
$selectedFields = $cd['back_fields'] ?? [];
?>
<div class="checkbox-grid">
<?php foreach ($allFields as $fieldKey => $fieldLabel): ?>
<label class="checkbox-label">
<input type="checkbox" name="back_fields[]" value="<?= e($fieldKey) ?>" <?= in_array($fieldKey, $selectedFields) ? 'checked' : '' ?> onchange="updateCarnetPreview()">
<span class="checkbox-box"></span>
<span><?= e($fieldLabel) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Live Preview -->
<div class="designer-preview">
<div class="preview-sticky">
<h3 class="preview-title"><i data-lucide="eye"></i> معاينة حية</h3>
<!-- Front Preview -->
<div class="preview-label">الوجه الأمامي</div>
<div class="carnet-preview-front" id="carnet-front-preview">
<div class="cpf-club-name"><?= e($clubNameAr) ?><?= e($clubNameEn) ?></div>
<div class="cpf-subtitle" id="cpf-subtitle"><?= e($subtitle) ?></div>
<div class="cpf-member-name">أحمد محمد عبدالله</div>
<div class="cpf-member-name-en" id="cpf-name-en">Ahmed Mohamed Abdullah</div>
<div class="cpf-member-number">2024-001234</div>
<div class="cpf-member-type" id="cpf-member-type">عضو عامل</div>
<div class="cpf-qr" id="cpf-qr">
<svg viewBox="0 0 40 40" fill="#000"><rect x="2" y="2" width="8" height="8"/><rect x="12" y="2" width="4" height="4"/><rect x="18" y="2" width="4" height="8"/><rect x="30" y="2" width="8" height="8"/><rect x="2" y="12" width="4" height="4"/><rect x="10" y="14" width="4" height="4"/><rect x="18" y="12" width="8" height="4"/><rect x="30" y="12" width="4" height="4"/><rect x="2" y="18" width="8" height="4"/><rect x="14" y="18" width="4" height="4"/><rect x="22" y="18" width="4" height="4"/><rect x="30" y="18" width="8" height="4"/><rect x="2" y="30" width="8" height="8"/><rect x="14" y="30" width="4" height="4"/><rect x="22" y="28" width="4" height="6"/><rect x="30" y="30" width="8" height="8"/></svg>
</div>
<div class="cpf-carnet-num">CRN-2024-0001</div>
</div>
<!-- Back Preview -->
<div class="preview-label" style="margin-top:16px;">الوجه الخلفي</div>
<div class="carnet-preview-back" id="carnet-back-preview">
<div class="cpb-branch-strip" id="cpb-branch-strip">فرع شيراتون</div>
<div class="cpb-header">
<div class="cpb-logo-text"><?= e($clubNameEn) ?><br><?= e($clubNameAr) ?></div>
</div>
<div class="cpb-info" id="cpb-info">
<div><strong>الاسم:</strong> أحمد محمد عبدالله</div>
<div><strong>رقم العضوية:</strong> 2024-001234</div>
<div><strong>الفرع:</strong> فرع شيراتون</div>
<div><strong>تاريخ الإصدار:</strong> 2024-01-15</div>
</div>
<div class="cpb-instructions" id="cpb-instructions"><?= e($cd['back_instructions_text']) ?></div>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i data-lucide="save"></i>
حفظ تصميم الكارنيه
</button>
</div>
</form>
</div>
<!-- ============================================ -->
<!-- TAB 3: Receipt Designer -->
<!-- ============================================ -->
<div class="tab-panel" id="tab-receipt">
<form method="POST" action="/settings/branding/receipt">
<input type="hidden" name="csrf_token" value="<?= e(CSRF::token()) ?>">
<div class="designer-layout">
<!-- Settings Panel -->
<div class="designer-settings">
<div class="card branding-card">
<div class="card-header">
<h3><i data-lucide="receipt"></i> إعدادات الإيصال</h3>
</div>
<div class="card-body">
<div class="toggle-group">
<label class="toggle-label">
<input type="hidden" name="show_logo" value="0">
<input type="checkbox" name="show_logo" value="1" <?= $rd['show_logo'] ? 'checked' : '' ?> onchange="updateReceiptPreview()">
<span class="toggle-switch"></span>
<span>إظهار الشعار في الرأس</span>
</label>
<label class="toggle-label">
<input type="hidden" name="show_watermark" value="0">
<input type="checkbox" name="show_watermark" value="1" <?= $rd['show_watermark'] ? 'checked' : '' ?> onchange="updateReceiptPreview()">
<span class="toggle-switch"></span>
<span>إظهار العلامة المائية</span>
</label>
<label class="toggle-label">
<input type="hidden" name="show_footer_print_info" value="0">
<input type="checkbox" name="show_footer_print_info" value="1" <?= $rd['show_footer_print_info'] ? 'checked' : '' ?> onchange="updateReceiptPreview()">
<span class="toggle-switch"></span>
<span>إظهار معلومات الطباعة</span>
</label>
</div>
<div class="form-group">
<label class="form-label">نص العلامة المائية</label>
<input type="text" name="watermark_text" class="form-input" value="<?= e($rd['watermark_text']) ?>" id="watermark_text" onchange="updateReceiptPreview()">
</div>
<div class="form-group">
<label class="form-label">شفافية العلامة المائية: <span id="opacity-value"><?= number_format($rd['watermark_opacity'], 2) ?></span></label>
<input type="range" name="watermark_opacity" class="form-range" min="0.01" max="0.20" step="0.01" value="<?= e($rd['watermark_opacity']) ?>" id="watermark_opacity" oninput="document.getElementById('opacity-value').textContent=parseFloat(this.value).toFixed(2);updateReceiptPreview()">
</div>
<div class="form-group">
<label class="form-label">لون الرأس</label>
<div class="color-input-wrap">
<input type="color" name="header_color" value="<?= e($rd['header_color']) ?>" id="receipt_header_color" onchange="updateReceiptPreview()">
<input type="text" class="form-input form-input-sm color-hex" value="<?= e($rd['header_color']) ?>" onchange="this.previousElementSibling.value=this.value;updateReceiptPreview()" dir="ltr">
</div>
</div>
</div>
</div>
</div>
<!-- Receipt Preview -->
<div class="designer-preview">
<div class="preview-sticky">
<h3 class="preview-title"><i data-lucide="eye"></i> معاينة حية</h3>
<div class="receipt-preview" id="receipt-preview">
<div class="rp-watermark" id="rp-watermark"><?= e($rd['watermark_text']) ?></div>
<?php if ($logo): ?>
<div class="rp-logo" id="rp-logo"><img src="<?= e($logo) ?>" alt="Logo"></div>
<?php else: ?>
<div class="rp-logo" id="rp-logo" style="display:none;"></div>
<?php endif; ?>
<div class="rp-header" id="rp-header">
<h2><?= e($clubNameAr) ?></h2>
<p><?= e($clubNameEn) ?></p>
<h3>إيصال تحصيل</h3>
</div>
<div class="rp-body">
<table>
<tr><td class="rp-label">رقم الإيصال</td><td class="rp-value">RCP-2024-0001</td></tr>
<tr><td class="rp-label">التاريخ</td><td class="rp-value">2024-01-15</td></tr>
<tr><td class="rp-label">اسم العضو</td><td class="rp-value">أحمد محمد عبدالله</td></tr>
<tr><td class="rp-label">المبلغ</td><td class="rp-value rp-amount">1,500.00 ج.م</td></tr>
</table>
</div>
<div class="rp-signatures">
<div>توقيع المستلم</div>
<div>أمين الخزينة</div>
<div>المدير المالي</div>
</div>
<div class="rp-footer" id="rp-footer">طبع بتاريخ: <?= date('Y-m-d H:i') ?></div>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i data-lucide="save"></i>
حفظ تصميم الإيصال
</button>
</div>
</form>
</div>
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?>
<script>
// Tab switching
function switchTab(btn) {
document.querySelectorAll('.branding-tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
lucide.createIcons();
}
// Logo preview
function previewLogo(input) {
if (!input.files || !input.files[0]) return;
var file = input.files[0];
if (file.size > 2 * 1024 * 1024) {
alert('حجم الملف يتجاوز 2 ميجا');
input.value = '';
return;
}
var reader = new FileReader();
reader.onload = function(e) {
var area = document.getElementById('logo-upload-area');
area.innerHTML = '<div class="logo-preview" id="logo-preview">' +
'<img src="' + e.target.result + '" alt="معاينة" id="logo-preview-img">' +
'<div class="logo-preview-overlay">' +
'<button type="button" class="btn btn-sm btn-danger" onclick="removeLogo()">' +
'<i data-lucide="trash-2"></i> حذف</button></div></div>';
// Re-append the hidden inputs
var fileInput = document.getElementById('logo_file');
var removeInput = document.getElementById('remove_logo');
if (!area.contains(fileInput)) area.appendChild(fileInput);
if (!area.contains(removeInput)) area.appendChild(removeInput);
removeInput.value = '0';
lucide.createIcons();
};
reader.readAsDataURL(file);
}
function removeLogo() {
var area = document.getElementById('logo-upload-area');
area.innerHTML = '<div class="logo-placeholder" id="logo-placeholder" onclick="document.getElementById(\'logo_file\').click()">' +
'<i data-lucide="image-plus"></i>' +
'<p>اضغط لرفع الشعار</p>' +
'<small>PNG, JPG, SVG, WebP — أقصى حجم 2MB</small></div>' +
'<input type="file" name="logo_file" id="logo_file" accept="image/png,image/jpeg,image/svg+xml,image/webp" style="display:none;" onchange="previewLogo(this)">' +
'<input type="hidden" name="remove_logo" id="remove_logo" value="1">';
lucide.createIcons();
}
// Carnet live preview
function updateCarnetPreview() {
var front = document.getElementById('carnet-front-preview');
var bgColor = document.getElementById('front_bg_color').value;
var textColor = document.getElementById('front_text_color').value;
front.style.background = bgColor;
front.style.color = textColor;
var showSub = document.querySelector('[name="front_show_subtitle"][type="checkbox"]').checked;
var showEn = document.querySelector('[name="front_show_english_name"][type="checkbox"]').checked;
var showType = document.querySelector('[name="front_show_member_type"][type="checkbox"]').checked;
var showQr = document.querySelector('[name="front_show_qr"][type="checkbox"]').checked;
document.getElementById('cpf-subtitle').style.display = showSub ? '' : 'none';
document.getElementById('cpf-name-en').style.display = showEn ? '' : 'none';
document.getElementById('cpf-member-type').style.display = showType ? '' : 'none';
document.getElementById('cpf-qr').style.display = showQr ? '' : 'none';
// QR position
var qrPos = document.getElementById('front_qr_position').value;
var qr = document.getElementById('cpf-qr');
qr.style.top = ''; qr.style.bottom = ''; qr.style.left = ''; qr.style.right = '';
if (qrPos === 'bottom-left') { qr.style.bottom = '10px'; qr.style.left = '10px'; }
else if (qrPos === 'bottom-right') { qr.style.bottom = '10px'; qr.style.right = '10px'; }
else if (qrPos === 'top-left') { qr.style.top = '10px'; qr.style.left = '10px'; }
else if (qrPos === 'top-right') { qr.style.top = '10px'; qr.style.right = '10px'; }
// Back
var stripColor = document.getElementById('back_strip_color').value;
var showStrip = document.querySelector('[name="back_show_branch_strip"][type="checkbox"]').checked;
var showInstr = document.querySelector('[name="back_show_instructions"][type="checkbox"]').checked;
var instrText = document.getElementById('back_instructions_text').value;
var strip = document.getElementById('cpb-branch-strip');
strip.style.display = showStrip ? '' : 'none';
strip.style.background = stripColor;
document.getElementById('cpb-instructions').style.display = showInstr ? '' : 'none';
document.getElementById('cpb-instructions').textContent = instrText;
// Back fields
var checked = [];
document.querySelectorAll('[name="back_fields[]"]:checked').forEach(function(cb) { checked.push(cb.value); });
var fieldLabels = {
'full_name_ar': 'الاسم',
'full_name_en': 'Name',
'membership_number': 'رقم العضوية',
'branch_name': 'الفرع',
'issued_at': 'تاريخ الإصدار',
'national_id': 'الرقم القومي',
'phone': 'الهاتف'
};
var fieldValues = {
'full_name_ar': 'أحمد محمد عبدالله',
'full_name_en': 'Ahmed M. Abdullah',
'membership_number': '2024-001234',
'branch_name': 'فرع شيراتون',
'issued_at': '2024-01-15',
'national_id': '29001010123456',
'phone': '01012345678'
};
var infoHtml = '';
for (var i = 0; i < checked.length; i++) {
var k = checked[i];
infoHtml += '<div><strong>' + (fieldLabels[k] || k) + ':</strong> ' + (fieldValues[k] || '—') + '</div>';
}
document.getElementById('cpb-info').innerHTML = infoHtml;
}
// Receipt live preview
function updateReceiptPreview() {
var showWatermark = document.querySelector('[name="show_watermark"][type="checkbox"]').checked;
var showLogo = document.querySelector('[name="show_logo"][type="checkbox"]').checked;
var showFooter = document.querySelector('[name="show_footer_print_info"][type="checkbox"]').checked;
var watermarkText = document.getElementById('watermark_text').value;
var opacity = parseFloat(document.getElementById('watermark_opacity').value);
var headerColor = document.getElementById('receipt_header_color').value;
var wm = document.getElementById('rp-watermark');
wm.style.display = showWatermark ? '' : 'none';
wm.textContent = watermarkText;
wm.style.opacity = opacity;
var logo = document.getElementById('rp-logo');
logo.style.display = showLogo ? '' : 'none';
var footer = document.getElementById('rp-footer');
footer.style.display = showFooter ? '' : 'none';
var header = document.getElementById('rp-header');
header.style.borderBottomColor = headerColor;
header.querySelector('h2').style.color = headerColor;
}
// Sync color hex inputs
document.querySelectorAll('.color-input-wrap input[type="color"]').forEach(function(colorInput) {
colorInput.addEventListener('input', function() {
var hex = this.nextElementSibling;
if (hex) hex.value = this.value;
});
});
// Initialize previews on page load
document.addEventListener('DOMContentLoaded', function() {
updateCarnetPreview();
updateReceiptPreview();
});
</script>
<?php $__template->endSection(); ?>
......@@ -6,6 +6,7 @@
use App\Core\App;
use App\Core\Registries\MenuRegistry;
use App\Modules\Settings\Services\BrandingService;
$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$employee = App::getInstance()->currentEmployee();
......@@ -150,7 +151,12 @@ if (empty($menuItems)) {
?>
<div class="sidebar-header">
<div class="sidebar-brand">THE CLUB</div>
<?php $brandLogo = BrandingService::logo(); ?>
<?php if ($brandLogo): ?>
<img src="<?= e($brandLogo) ?>" alt="<?= e(BrandingService::clubNameEn()) ?>" class="sidebar-logo">
<?php else: ?>
<div class="sidebar-brand"><?= e(BrandingService::clubNameEn()) ?></div>
<?php endif; ?>
</div>
<nav class="sidebar-nav">
......
<?php
use App\Core\CSRF;
use App\Modules\Settings\Services\BrandingService;
$brandLogo = BrandingService::logo();
$brandNameAr = BrandingService::clubNameAr();
$brandNameEn = BrandingService::clubNameEn();
?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
......@@ -269,11 +274,17 @@ use App\Core\CSRF;
<div class="auth-card">
<div class="auth-header">
<?php if ($brandLogo): ?>
<div class="auth-logo" style="background:transparent;box-shadow:none;">
<img src="<?= e($brandLogo) ?>" alt="<?= e($brandNameEn) ?>" style="max-width:48px;max-height:48px;object-fit:contain;">
</div>
<?php else: ?>
<div class="auth-logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h1>نادي النادي شيراتون</h1>
<p>THE CLUB Sheraton</p>
<?php endif; ?>
<h1><?= e($brandNameAr) ?></h1>
<p><?= e($brandNameEn) ?></p>
</div>
<?php
......
......@@ -6,6 +6,7 @@
use App\Core\App;
use App\Core\CSRF;
use App\Modules\Settings\Services\BrandingService;
$app = App::getInstance();
$employee = $app->currentEmployee();
......@@ -90,7 +91,7 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
<!-- Footer -->
<footer class="page-footer">
<span>THE CLUB Sheraton &copy; <?= date('Y') ?></span>
<span><?= e(BrandingService::clubNameEn()) ?> &copy; <?= date('Y') ?></span>
<span>v1.0.0</span>
<span><?= arabic_date(date('Y-m-d')) ?></span>
</footer>
......
<?php use App\Modules\Settings\Services\BrandingService; ?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
......@@ -44,7 +45,11 @@
</head>
<body>
<div class="print-header">
<h2>نادي النادي شيراتون</h2>
<?php $printLogo = BrandingService::logo(); ?>
<?php if ($printLogo): ?>
<img src="<?= e($printLogo) ?>" alt="Logo" style="max-height:40px;margin-bottom:8px;">
<?php endif; ?>
<h2><?= e(BrandingService::clubNameAr()) ?></h2>
<p><?= arabic_date(today()) ?></p>
</div>
......
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$configs = [
[
'config_key' => 'branding.logo_path',
'config_value' => '',
'config_type' => 'string',
'group_name' => 'branding',
'description_ar' => 'مسار شعار النادي',
'description_en' => 'Club logo file path',
'is_editable' => 1,
],
[
'config_key' => 'branding.club_name_ar',
'config_value' => 'نادي النادي شيراتون',
'config_type' => 'string',
'group_name' => 'branding',
'description_ar' => 'اسم النادي بالعربية',
'description_en' => 'Club name in Arabic',
'is_editable' => 1,
],
[
'config_key' => 'branding.club_name_en',
'config_value' => 'THE CLUB Sheraton',
'config_type' => 'string',
'group_name' => 'branding',
'description_ar' => 'اسم النادي بالإنجليزية',
'description_en' => 'Club name in English',
'is_editable' => 1,
],
[
'config_key' => 'branding.club_subtitle',
'config_value' => 'SPORTS CITY',
'config_type' => 'string',
'group_name' => 'branding',
'description_ar' => 'العنوان الفرعي للنادي',
'description_en' => 'Club subtitle',
'is_editable' => 1,
],
[
'config_key' => 'branding.carnet_design',
'config_value' => json_encode([
'front_bg_color' => '#0D7377',
'front_text_color' => '#ffffff',
'front_show_subtitle' => true,
'front_show_english_name'=> true,
'front_show_member_type' => true,
'front_show_qr' => true,
'front_qr_position' => 'bottom-left',
'front_logo_position' => 'top-center',
'back_show_branch_strip' => true,
'back_strip_color' => '#0D7377',
'back_show_instructions' => true,
'back_instructions_text' => 'برجاء حمل هذه البطاقة أثناء التواجد بالنادي وتقديمها عند الطلب',
'back_fields' => ['full_name_ar', 'membership_number', 'branch_name', 'issued_at'],
], JSON_UNESCAPED_UNICODE),
'config_type' => 'json',
'group_name' => 'branding',
'description_ar' => 'تصميم الكارنيه (وجه وخلفية)',
'description_en' => 'Carnet card design (front & back)',
'is_editable' => 1,
],
[
'config_key' => 'branding.receipt_design',
'config_value' => json_encode([
'show_logo' => true,
'show_watermark' => true,
'watermark_text' => 'نادي النادي شيراتون',
'watermark_opacity' => 0.06,
'header_color' => '#0D7377',
'show_footer_print_info' => true,
], JSON_UNESCAPED_UNICODE),
'config_type' => 'json',
'group_name' => 'branding',
'description_ar' => 'تصميم الإيصال (شعار وعلامة مائية)',
'description_en' => 'Receipt design (logo & watermark)',
'is_editable' => 1,
],
];
foreach ($configs as $config) {
$existing = $db->selectOne(
"SELECT id FROM system_config WHERE config_key = ?",
[$config['config_key']]
);
if ($existing) {
continue;
}
$db->insert('system_config', array_merge($config, [
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]));
}
};
/* ================================================
Branding Settings — Tabs, Designer, Previews
================================================ */
/* Tabs */
.branding-tabs {
display: flex;
gap: 4px;
background: rgba(255,255,255,0.6);
border-radius: 14px;
padding: 5px;
margin-bottom: 24px;
backdrop-filter: blur(8px);
border: 1px solid #e2e8f0;
}
.branding-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
background: transparent;
border: none;
border-radius: 10px;
font-family: inherit;
font-size: 14px;
font-weight: 600;
color: #64748b;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.branding-tab:hover {
color: #0D7377;
background: rgba(13, 115, 119, 0.05);
}
.branding-tab.active {
background: #fff;
color: #0D7377;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.branding-tab i, .branding-tab svg {
width: 18px;
height: 18px;
}
/* Tab panels */
.tab-panel {
display: none;
animation: tabFadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.tab-panel.active {
display: block;
}
@keyframes tabFadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Branding grid (identity tab) */
.branding-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.branding-grid {
grid-template-columns: 1fr;
}
}
/* Branding card */
.branding-card {
border-radius: 16px;
overflow: hidden;
}
.branding-card .card-header {
padding: 16px 20px;
border-bottom: 1px solid #e2e8f0;
background: #f8fafc;
}
.branding-card .card-header h3 {
margin: 0;
font-size: 15px;
font-weight: 700;
color: #0f172a;
display: flex;
align-items: center;
gap: 8px;
}
.branding-card .card-header h3 i,
.branding-card .card-header h3 svg {
width: 18px;
height: 18px;
color: #0D7377;
}
.branding-card .card-body {
padding: 20px;
}
/* Logo upload area */
.logo-upload-area {
min-height: 200px;
}
.logo-placeholder {
border: 2px dashed #cbd5e1;
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
color: #94a3b8;
}
.logo-placeholder:hover {
border-color: #0D7377;
background: rgba(13, 115, 119, 0.03);
color: #0D7377;
}
.logo-placeholder i, .logo-placeholder svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.logo-placeholder p {
margin: 0 0 4px;
font-size: 14px;
font-weight: 600;
}
.logo-placeholder small {
font-size: 12px;
opacity: 0.7;
}
.logo-preview {
position: relative;
text-align: center;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 24px;
background: #f8fafc;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.logo-preview img {
max-width: 200px;
max-height: 150px;
object-fit: contain;
}
.logo-preview-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.5);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.25s ease;
}
.logo-preview:hover .logo-preview-overlay {
opacity: 1;
}
/* Form actions */
.form-actions {
margin-top: 24px;
display: flex;
justify-content: flex-start;
}
/* Color input */
.color-input-wrap {
display: flex;
align-items: center;
gap: 10px;
}
.color-input-wrap input[type="color"] {
width: 42px;
height: 42px;
border: 2px solid #e2e8f0;
border-radius: 10px;
padding: 3px;
cursor: pointer;
background: #fff;
transition: border-color 0.2s;
}
.color-input-wrap input[type="color"]:hover {
border-color: #0D7377;
}
.color-hex {
width: 100px;
font-family: 'Courier New', monospace;
font-size: 13px;
text-align: center;
}
/* Toggle switches */
.toggle-group {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.toggle-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: #334155;
}
.toggle-label input[type="checkbox"] {
display: none;
}
.toggle-switch {
position: relative;
width: 40px;
height: 22px;
background: #cbd5e1;
border-radius: 11px;
flex-shrink: 0;
transition: background 0.3s ease;
}
.toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
background: #fff;
border-radius: 50%;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}
.toggle-label input[type="checkbox"]:checked + .toggle-switch {
background: #0D7377;
}
.toggle-label input[type="checkbox"]:checked + .toggle-switch::after {
transform: translateX(18px);
}
/* Checkbox grid */
.checkbox-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
color: #334155;
padding: 6px 8px;
border-radius: 8px;
transition: background 0.2s;
}
.checkbox-label:hover {
background: #f1f5f9;
}
.checkbox-label input[type="checkbox"] {
display: none;
}
.checkbox-box {
width: 18px;
height: 18px;
border: 2px solid #cbd5e1;
border-radius: 5px;
flex-shrink: 0;
position: relative;
transition: all 0.2s;
}
.checkbox-label input[type="checkbox"]:checked + .checkbox-box {
background: #0D7377;
border-color: #0D7377;
}
.checkbox-label input[type="checkbox"]:checked + .checkbox-box::after {
content: '';
position: absolute;
top: 1px;
left: 5px;
width: 5px;
height: 9px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* Form row */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
/* Form range */
.form-range {
width: 100%;
height: 6px;
border-radius: 3px;
background: #e2e8f0;
outline: none;
-webkit-appearance: none;
appearance: none;
margin-top: 8px;
}
.form-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #0D7377;
cursor: pointer;
box-shadow: 0 2px 6px rgba(13, 115, 119, 0.3);
transition: transform 0.2s;
}
.form-range::-webkit-slider-thumb:hover {
transform: scale(1.15);
}
.form-range::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #0D7377;
border: none;
cursor: pointer;
}
/* ================================================
Designer Layout (side-by-side)
================================================ */
.designer-layout {
display: grid;
grid-template-columns: 1fr 380px;
gap: 24px;
align-items: start;
}
@media (max-width: 1024px) {
.designer-layout {
grid-template-columns: 1fr;
}
}
.designer-settings {
display: flex;
flex-direction: column;
gap: 20px;
}
.designer-preview {
position: relative;
}
.preview-sticky {
position: sticky;
top: 80px;
}
.preview-title {
font-size: 14px;
font-weight: 700;
color: #475569;
margin: 0 0 12px;
display: flex;
align-items: center;
gap: 6px;
}
.preview-title i, .preview-title svg {
width: 16px;
height: 16px;
color: #0D7377;
}
.preview-label {
font-size: 12px;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
/* ================================================
Carnet Preview (front & back)
================================================ */
.carnet-preview-front {
width: 340px;
height: 215px;
background: <?= e($cd['front_bg_color']) ?>;
color: <?= e($cd['front_text_color']) ?>;
border-radius: 12px;
padding: 16px 18px;
position: relative;
overflow: hidden;
font-family: 'Cairo', sans-serif;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
transition: background 0.3s, color 0.3s;
}
.cpf-club-name { text-align: center; font-size: 13px; font-weight: 700; margin-bottom: 2px; }
.cpf-subtitle { text-align: center; font-size: 10px; opacity: 0.75; margin-bottom: 12px; }
.cpf-member-name { font-size: 13px; font-weight: 700; margin-bottom: 3px; }
.cpf-member-name-en { font-size: 10px; opacity: 0.8; margin-bottom: 6px; }
.cpf-member-number { font-size: 18px; font-weight: 700; letter-spacing: 2px; direction: ltr; text-align: right; }
.cpf-member-type { font-size: 10px; margin-top: 4px; opacity: 0.8; }
.cpf-qr {
position: absolute;
bottom: 10px;
left: 10px;
width: 55px;
height: 55px;
background: #fff;
border-radius: 6px;
padding: 5px;
transition: all 0.3s;
}
.cpf-qr svg { width: 100%; height: 100%; }
.cpf-carnet-num { position: absolute; bottom: 10px; right: 14px; font-size: 9px; opacity: 0.6; }
/* Carnet back */
.carnet-preview-back {
width: 340px;
height: 215px;
background: #fff;
border-radius: 12px;
border: 2px solid #e2e8f0;
padding: 14px 14px 14px 14px;
position: relative;
font-family: 'Cairo', sans-serif;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
overflow: hidden;
}
.cpb-branch-strip {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 28px;
background: <?= e($cd['back_strip_color']) ?>;
border-radius: 12px 0 0 12px;
writing-mode: vertical-rl;
text-orientation: mixed;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 10px;
font-weight: 600;
transition: background 0.3s;
}
.cpb-header {
margin-right: 20px;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1.5px solid #e2e8f0;
}
.cpb-logo-text {
font-size: 10px;
font-weight: 700;
color: #0D7377;
line-height: 1.4;
}
.cpb-info {
margin-right: 20px;
font-size: 11px;
color: #1a1a2e;
}
.cpb-info div {
margin-bottom: 2px;
}
.cpb-info strong {
color: #0D7377;
}
.cpb-instructions {
position: absolute;
bottom: 8px;
left: 35px;
right: 10px;
font-size: 9px;
color: #94a3b8;
text-align: center;
}
/* ================================================
Receipt Preview
================================================ */
.receipt-preview {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 24px 20px;
position: relative;
overflow: hidden;
font-family: 'Cairo', sans-serif;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
direction: rtl;
min-height: 400px;
}
.rp-watermark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-35deg);
font-size: 48px;
font-weight: 900;
color: #0D7377;
opacity: <?= e($rd['watermark_opacity']) ?>;
white-space: nowrap;
pointer-events: none;
z-index: 0;
transition: opacity 0.3s;
}
.rp-logo {
text-align: center;
margin-bottom: 12px;
position: relative;
z-index: 1;
}
.rp-logo img {
max-height: 40px;
object-fit: contain;
}
.rp-header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid <?= e($rd['header_color']) ?>;
position: relative;
z-index: 1;
transition: border-color 0.3s;
}
.rp-header h2 {
margin: 0;
font-size: 16px;
font-weight: 800;
color: <?= e($rd['header_color']) ?>;
transition: color 0.3s;
}
.rp-header p {
margin: 2px 0 0;
font-size: 11px;
color: #94a3b8;
}
.rp-header h3 {
margin: 10px 0 0;
font-size: 14px;
color: #0f172a;
}
.rp-body {
position: relative;
z-index: 1;
}
.rp-body table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.rp-body td {
padding: 8px 10px;
border: 1px solid #e2e8f0;
}
.rp-label {
background: #f8fafc;
font-weight: 600;
width: 35%;
color: #475569;
}
.rp-value {
color: #0f172a;
}
.rp-amount {
font-size: 16px;
font-weight: 700;
color: #0D7377;
direction: ltr;
text-align: right;
}
.rp-signatures {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin-top: 30px;
text-align: center;
font-size: 11px;
color: #64748b;
position: relative;
z-index: 1;
}
.rp-signatures > div {
border-top: 1px solid #000;
padding-top: 8px;
}
.rp-footer {
margin-top: 20px;
text-align: center;
font-size: 10px;
color: #94a3b8;
position: relative;
z-index: 1;
}
/* ================================================
Responsive
================================================ */
@media (max-width: 640px) {
.branding-tab span {
display: none;
}
.branding-tabs {
justify-content: center;
}
.branding-tab {
flex: 0;
padding: 12px 20px;
}
.form-row {
grid-template-columns: 1fr;
}
.checkbox-grid {
grid-template-columns: 1fr;
}
.carnet-preview-front,
.carnet-preview-back {
width: 100%;
max-width: 340px;
}
}
......@@ -202,6 +202,17 @@ code {
background-clip: text;
}
.sidebar-logo {
max-height: 36px;
max-width: 180px;
object-fit: contain;
transition: opacity var(--duration-normal) ease;
}
.sidebar-logo:hover {
opacity: 0.85;
}
.sidebar-toggle {
background: none;
border: none;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment