Commit 4c209476 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add full branding settings system with live color theming

- BrandingSettings Livewire component with file uploads (logo, dark logo,
  favicon, signature, login background, invoice header)
- Color picker for primary/secondary/accent/sidebar/status colors with
  live preview panel and preset palettes
- Typography settings (Arabic/English font, base font size)
- Invoice/receipt footer text and terms & conditions
- Display toggles (logo in sidebar, logo in invoice, signature, compact mode)
- CSS variables injected into app layout that drive sidebar, buttons, etc.
- Sidebar now uses --brand-sidebar-bg/text/active variables
- Logo displays in sidebar when uploaded
- SystemSettingsSeeder populates all 6 groups with 50+ settings
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 5fcdb075
<?php
namespace App\Livewire\Settings;
use App\Domain\Shared\Models\Academy;
use App\Domain\Shared\Services\SettingsService;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Livewire\WithFileUploads;
#[Layout('layouts.app')]
#[Title('إعدادات الهوية البصرية')]
class BrandingSettings extends Component
{
use WithFileUploads;
// Logo uploads
public $logo;
public $logo_dark;
public $favicon;
public $signature;
public $login_background;
public $invoice_header;
// Current paths
public ?string $current_logo = null;
public ?string $current_logo_dark = null;
public ?string $current_favicon = null;
public ?string $current_signature = null;
public ?string $current_login_background = null;
public ?string $current_invoice_header = null;
// Colors
public string $primary_color = '#2563eb';
public string $secondary_color = '#7c3aed';
public string $accent_color = '#059669';
public string $sidebar_bg = '#0f172a';
public string $sidebar_text = '#e2e8f0';
public string $sidebar_active = '#2563eb';
public string $header_bg = '#ffffff';
public string $success_color = '#10b981';
public string $warning_color = '#f59e0b';
public string $danger_color = '#ef4444';
// Typography
public string $font_family_ar = 'Cairo';
public string $font_family_en = 'Inter';
public string $font_size_base = '14';
// Invoice/Receipt branding
public string $invoice_footer_text = '';
public string $receipt_footer_text = '';
public string $terms_and_conditions = '';
// Display options
public bool $show_logo_in_sidebar = true;
public bool $show_logo_in_invoice = true;
public bool $show_signature_in_invoice = false;
public bool $compact_sidebar = false;
public function mount(): void
{
$this->authorize('settings.manage');
$settings = app(SettingsService::class);
$this->current_logo = $settings->get('branding.logo');
$this->current_logo_dark = $settings->get('branding.logo_dark');
$this->current_favicon = $settings->get('branding.favicon');
$this->current_signature = $settings->get('branding.signature');
$this->current_login_background = $settings->get('branding.login_background');
$this->current_invoice_header = $settings->get('branding.invoice_header');
$this->primary_color = $settings->get('branding.primary_color', '#2563eb');
$this->secondary_color = $settings->get('branding.secondary_color', '#7c3aed');
$this->accent_color = $settings->get('branding.accent_color', '#059669');
$this->sidebar_bg = $settings->get('branding.sidebar_bg', '#0f172a');
$this->sidebar_text = $settings->get('branding.sidebar_text', '#e2e8f0');
$this->sidebar_active = $settings->get('branding.sidebar_active', '#2563eb');
$this->header_bg = $settings->get('branding.header_bg', '#ffffff');
$this->success_color = $settings->get('branding.success_color', '#10b981');
$this->warning_color = $settings->get('branding.warning_color', '#f59e0b');
$this->danger_color = $settings->get('branding.danger_color', '#ef4444');
$this->font_family_ar = $settings->get('branding.font_family_ar', 'Cairo');
$this->font_family_en = $settings->get('branding.font_family_en', 'Inter');
$this->font_size_base = $settings->get('branding.font_size_base', '14');
$this->invoice_footer_text = $settings->get('branding.invoice_footer_text', '');
$this->receipt_footer_text = $settings->get('branding.receipt_footer_text', '');
$this->terms_and_conditions = $settings->get('branding.terms_and_conditions', '');
$this->show_logo_in_sidebar = (bool) $settings->get('branding.show_logo_in_sidebar', true);
$this->show_logo_in_invoice = (bool) $settings->get('branding.show_logo_in_invoice', true);
$this->show_signature_in_invoice = (bool) $settings->get('branding.show_signature_in_invoice', false);
$this->compact_sidebar = (bool) $settings->get('branding.compact_sidebar', false);
}
public function updatedLogo(): void
{
$this->validate(['logo' => 'image|max:2048|mimes:png,jpg,jpeg,svg,webp']);
}
public function updatedLogoDark(): void
{
$this->validate(['logo_dark' => 'image|max:2048|mimes:png,jpg,jpeg,svg,webp']);
}
public function updatedFavicon(): void
{
$this->validate(['favicon' => 'image|max:512|mimes:png,ico,svg']);
}
public function updatedSignature(): void
{
$this->validate(['signature' => 'image|max:1024|mimes:png,jpg,jpeg,svg']);
}
public function updatedLoginBackground(): void
{
$this->validate(['login_background' => 'image|max:5120|mimes:png,jpg,jpeg,webp']);
}
public function updatedInvoiceHeader(): void
{
$this->validate(['invoice_header' => 'image|max:2048|mimes:png,jpg,jpeg,svg']);
}
public function removeImage(string $field): void
{
$settings = app(SettingsService::class);
$key = "branding.{$field}";
$currentPath = $settings->get($key);
if ($currentPath && Storage::disk('public')->exists($currentPath)) {
Storage::disk('public')->delete($currentPath);
}
$settings->set($key, null, 'branding', 'string');
$this->{"current_{$field}"} = null;
}
public function save(): void
{
$this->validate([
'logo' => 'nullable|image|max:2048|mimes:png,jpg,jpeg,svg,webp',
'logo_dark' => 'nullable|image|max:2048|mimes:png,jpg,jpeg,svg,webp',
'favicon' => 'nullable|image|max:512|mimes:png,ico,svg',
'signature' => 'nullable|image|max:1024|mimes:png,jpg,jpeg,svg',
'login_background' => 'nullable|image|max:5120|mimes:png,jpg,jpeg,webp',
'invoice_header' => 'nullable|image|max:2048|mimes:png,jpg,jpeg,svg',
'primary_color' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'secondary_color' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'accent_color' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'sidebar_bg' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'sidebar_text' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'sidebar_active' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'header_bg' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'font_size_base' => 'required|integer|min:12|max:18',
]);
$settings = app(SettingsService::class);
$academy = app('current_academy');
$basePath = "branding/{$academy->id}";
$uploads = [
'logo' => $this->logo,
'logo_dark' => $this->logo_dark,
'favicon' => $this->favicon,
'signature' => $this->signature,
'login_background' => $this->login_background,
'invoice_header' => $this->invoice_header,
];
foreach ($uploads as $field => $file) {
if ($file) {
$oldPath = $settings->get("branding.{$field}");
if ($oldPath && Storage::disk('public')->exists($oldPath)) {
Storage::disk('public')->delete($oldPath);
}
$path = $file->store("{$basePath}/{$field}", 'public');
$settings->set("branding.{$field}", $path, 'branding', 'string');
$this->{"current_{$field}"} = $path;
$this->{$field} = null;
}
}
$colorSettings = [
'primary_color', 'secondary_color', 'accent_color',
'sidebar_bg', 'sidebar_text', 'sidebar_active', 'header_bg',
'success_color', 'warning_color', 'danger_color',
];
foreach ($colorSettings as $key) {
$settings->set("branding.{$key}", $this->{$key}, 'branding', 'string');
}
$settings->set('branding.font_family_ar', $this->font_family_ar, 'branding', 'string');
$settings->set('branding.font_family_en', $this->font_family_en, 'branding', 'string');
$settings->set('branding.font_size_base', $this->font_size_base, 'branding', 'integer');
$settings->set('branding.invoice_footer_text', $this->invoice_footer_text, 'branding', 'string');
$settings->set('branding.receipt_footer_text', $this->receipt_footer_text, 'branding', 'string');
$settings->set('branding.terms_and_conditions', $this->terms_and_conditions, 'branding', 'string');
$settings->set('branding.show_logo_in_sidebar', $this->show_logo_in_sidebar ? '1' : '0', 'branding', 'boolean');
$settings->set('branding.show_logo_in_invoice', $this->show_logo_in_invoice ? '1' : '0', 'branding', 'boolean');
$settings->set('branding.show_signature_in_invoice', $this->show_signature_in_invoice ? '1' : '0', 'branding', 'boolean');
$settings->set('branding.compact_sidebar', $this->compact_sidebar ? '1' : '0', 'branding', 'boolean');
if ($this->current_logo) {
$academy->update(['logo_path' => $this->current_logo]);
}
session()->flash('success', __('تم حفظ إعدادات الهوية البصرية بنجاح'));
}
public function render()
{
return view('livewire.settings.branding-settings');
}
}
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class SystemSettingsSeeder extends Seeder
{
public function run(): void
{
$academyId = DB::table('academies')->value('id');
if (!$academyId) {
$this->command->warn('No academy found. Skipping system settings seed.');
return;
}
$now = now();
$settings = [
// General
['group' => 'general', 'key' => 'academy_name_ar', 'value' => 'أكاديمية الكابتن', 'type' => 'string', 'description_ar' => 'اسم الأكاديمية بالعربية (يظهر في الفواتير والتقارير)'],
['group' => 'general', 'key' => 'academy_name_en', 'value' => 'El Captain Academy', 'type' => 'string', 'description_ar' => 'اسم الأكاديمية بالإنجليزية'],
['group' => 'general', 'key' => 'default_language', 'value' => 'ar', 'type' => 'string', 'description_ar' => 'اللغة الافتراضية للنظام (ar أو en)'],
['group' => 'general', 'key' => 'date_format', 'value' => 'Y-m-d', 'type' => 'string', 'description_ar' => 'صيغة التاريخ (Y-m-d أو d/m/Y)'],
['group' => 'general', 'key' => 'time_format', 'value' => 'H:i', 'type' => 'string', 'description_ar' => 'صيغة الوقت (H:i أو h:i A)'],
['group' => 'general', 'key' => 'week_starts_on', 'value' => '6', 'type' => 'integer', 'description_ar' => 'أول يوم في الأسبوع (6=السبت، 0=الأحد، 1=الإثنين)'],
['group' => 'general', 'key' => 'pagination_size', 'value' => '25', 'type' => 'integer', 'description_ar' => 'عدد العناصر في كل صفحة'],
['group' => 'general', 'key' => 'session_timeout_minutes', 'value' => '120', 'type' => 'integer', 'description_ar' => 'مهلة انتهاء الجلسة بالدقائق'],
// Financial
['group' => 'financial', 'key' => 'currency_code', 'value' => 'EGP', 'type' => 'string', 'description_ar' => 'رمز العملة (EGP, SAR, AED)'],
['group' => 'financial', 'key' => 'currency_symbol', 'value' => 'ج.م', 'type' => 'string', 'description_ar' => 'رمز العملة للعرض'],
['group' => 'financial', 'key' => 'tax_enabled', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل الضريبة على الفواتير'],
['group' => 'financial', 'key' => 'tax_percentage', 'value' => '14', 'type' => 'float', 'description_ar' => 'نسبة الضريبة المضافة (%)'],
['group' => 'financial', 'key' => 'tax_registration_number', 'value' => '', 'type' => 'string', 'description_ar' => 'رقم التسجيل الضريبي'],
['group' => 'financial', 'key' => 'invoice_prefix', 'value' => 'INV', 'type' => 'string', 'description_ar' => 'بادئة رقم الفاتورة'],
['group' => 'financial', 'key' => 'receipt_prefix', 'value' => 'RCP', 'type' => 'string', 'description_ar' => 'بادئة رقم الإيصال'],
['group' => 'financial', 'key' => 'payment_due_days', 'value' => '7', 'type' => 'integer', 'description_ar' => 'عدد أيام استحقاق الدفع بعد إصدار الفاتورة'],
['group' => 'financial', 'key' => 'max_discount_percent', 'value' => '50', 'type' => 'integer', 'description_ar' => 'الحد الأقصى لنسبة الخصم (%)'],
['group' => 'financial', 'key' => 'refund_requires_approval', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'هل يتطلب الاسترداد موافقة مسبقة؟'],
['group' => 'financial', 'key' => 'refund_approval_threshold', 'value' => '50000', 'type' => 'integer', 'description_ar' => 'حد مبلغ الاسترداد الذي يتطلب موافقة (بالقروش)'],
['group' => 'financial', 'key' => 'wallet_enabled', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'تفعيل نظام المحفظة الإلكترونية'],
['group' => 'financial', 'key' => 'installment_late_fee', 'value' => '500', 'type' => 'integer', 'description_ar' => 'غرامة التأخير على الأقساط (بالقروش)'],
['group' => 'financial', 'key' => 'auto_overdue_after_days', 'value' => '3', 'type' => 'integer', 'description_ar' => 'عدد الأيام بعد تاريخ الاستحقاق لتحويل الفاتورة لمتأخرة'],
// Attendance
['group' => 'attendance', 'key' => 'participant_grace_period_minutes', 'value' => '15', 'type' => 'integer', 'description_ar' => 'فترة السماح بالتأخير للمشاركين (بالدقائق)'],
['group' => 'attendance', 'key' => 'trainer_grace_period_minutes', 'value' => '10', 'type' => 'integer', 'description_ar' => 'فترة السماح بالتأخير للمدربين (بالدقائق)'],
['group' => 'attendance', 'key' => 'auto_absent_after_hours', 'value' => '2', 'type' => 'integer', 'description_ar' => 'ساعات بعد نهاية الحصة لتسجيل غياب تلقائي'],
['group' => 'attendance', 'key' => 'consecutive_absences_for_suspend', 'value' => '5', 'type' => 'integer', 'description_ar' => 'عدد الغيابات المتتالية لإيقاف المشترك'],
['group' => 'attendance', 'key' => 'min_attendance_percent', 'value' => '75', 'type' => 'integer', 'description_ar' => 'الحد الأدنى لنسبة الحضور (%)'],
['group' => 'attendance', 'key' => 'notify_guardian_on_absence', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إشعار ولي الأمر عند الغياب'],
['group' => 'attendance', 'key' => 'allow_excuse_after_hours', 'value' => '48', 'type' => 'integer', 'description_ar' => 'مدة صلاحية تقديم عذر بعد الحصة (بالساعات)'],
['group' => 'attendance', 'key' => 'qr_check_in_enabled', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل تسجيل الحضور بـ QR Code'],
// Pricing
['group' => 'pricing', 'key' => 'global_max_discount', 'value' => '50', 'type' => 'integer', 'description_ar' => 'الحد الأقصى للخصم الكلي (%)'],
['group' => 'pricing', 'key' => 'allow_price_override', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'السماح بتجاوز السعر يدوياً في نقطة البيع'],
['group' => 'pricing', 'key' => 'auto_apply_family_discount', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'تطبيق خصم العائلة تلقائياً'],
['group' => 'pricing', 'key' => 'family_discount_start_child', 'value' => '2', 'type' => 'integer', 'description_ar' => 'رقم الطفل الذي يبدأ عنده خصم العائلة'],
['group' => 'pricing', 'key' => 'trial_period_days', 'value' => '7', 'type' => 'integer', 'description_ar' => 'مدة الفترة التجريبية (بالأيام)'],
['group' => 'pricing', 'key' => 'early_enrollment_days', 'value' => '14', 'type' => 'integer', 'description_ar' => 'أيام التسجيل المبكر (للخصم)'],
['group' => 'pricing', 'key' => 'coupon_max_uses_default', 'value' => '100', 'type' => 'integer', 'description_ar' => 'الحد الافتراضي لاستخدام الكوبون'],
// Notifications
['group' => 'notifications', 'key' => 'email_enabled', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'تفعيل إرسال البريد الإلكتروني'],
['group' => 'notifications', 'key' => 'sms_enabled', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل إرسال الرسائل القصيرة'],
['group' => 'notifications', 'key' => 'sms_provider', 'value' => 'log', 'type' => 'string', 'description_ar' => 'مزود خدمة SMS (log, twilio, vonage)'],
['group' => 'notifications', 'key' => 'sms_rate_limit_per_hour', 'value' => '100', 'type' => 'integer', 'description_ar' => 'الحد الأقصى للرسائل في الساعة'],
['group' => 'notifications', 'key' => 'digest_mode_enabled', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'تفعيل وضع الملخص اليومي'],
['group' => 'notifications', 'key' => 'digest_send_time', 'value' => '08:00', 'type' => 'string', 'description_ar' => 'وقت إرسال الملخص اليومي'],
['group' => 'notifications', 'key' => 'notify_admin_on_payment', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إشعار المدير عند تسجيل دفعة'],
['group' => 'notifications', 'key' => 'notify_guardian_on_enrollment', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إشعار ولي الأمر عند التسجيل'],
['group' => 'notifications', 'key' => 'payment_reminder_days_before', 'value' => '3', 'type' => 'integer', 'description_ar' => 'عدد أيام قبل الاستحقاق لإرسال تذكير الدفع'],
// Enrollment
['group' => 'enrollment', 'key' => 'allow_waitlist', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'تفعيل قائمة الانتظار عند امتلاء المجموعة'],
['group' => 'enrollment', 'key' => 'auto_promote_waitlist', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'ترقية المسجلين في قائمة الانتظار تلقائياً'],
['group' => 'enrollment', 'key' => 'require_medical_report', 'value' => '0', 'type' => 'boolean', 'description_ar' => 'إلزام رفع تقرير طبي عند التسجيل'],
['group' => 'enrollment', 'key' => 'require_guardian_for_minors', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'إلزام ربط ولي أمر للقاصرين'],
['group' => 'enrollment', 'key' => 'minor_age_threshold', 'value' => '18', 'type' => 'integer', 'description_ar' => 'السن الأدنى للتسجيل بدون ولي أمر'],
['group' => 'enrollment', 'key' => 'max_groups_per_participant', 'value' => '5', 'type' => 'integer', 'description_ar' => 'الحد الأقصى لعدد المجموعات لكل مشترك'],
['group' => 'enrollment', 'key' => 'allow_transfer_between_groups', 'value' => '1', 'type' => 'boolean', 'description_ar' => 'السماح بنقل المشاركين بين المجموعات'],
['group' => 'enrollment', 'key' => 'freeze_max_days', 'value' => '30', 'type' => 'integer', 'description_ar' => 'أقصى مدة تجميد الاشتراك (بالأيام)'],
['group' => 'enrollment', 'key' => 'freeze_max_times', 'value' => '2', 'type' => 'integer', 'description_ar' => 'أقصى عدد مرات التجميد في الاشتراك الواحد'],
['group' => 'enrollment', 'key' => 'enrollment_expiry_days', 'value' => '30', 'type' => 'integer', 'description_ar' => 'مدة صلاحية الاشتراك الافتراضية (بالأيام)'],
];
foreach ($settings as $setting) {
DB::table('system_settings')->updateOrInsert(
['academy_id' => $academyId, 'key' => $setting['key']],
[
'academy_id' => $academyId,
'group' => $setting['group'],
'key' => $setting['key'],
'value' => $setting['value'],
'type' => $setting['type'],
'description_ar' => $setting['description_ar'],
'is_public' => false,
'created_at' => $now,
'updated_at' => $now,
]
);
}
$this->command->info('Seeded ' . count($settings) . ' system settings across 6 groups.');
}
}
......@@ -62,6 +62,7 @@
['label' => 'الفروع', 'route' => 'branches.list', 'icon' => 'building-office', 'permission' => 'settings.manage'],
['label' => 'سجل المراجعة', 'route' => 'audit.list', 'icon' => 'eye', 'permission' => 'audit.list'],
['label' => 'إعدادات الأكاديمية', 'route' => 'settings.academy', 'icon' => 'cog-6-tooth', 'permission' => 'settings.manage'],
['label' => 'الهوية البصرية', 'route' => 'settings.branding', 'icon' => 'swatch', 'permission' => 'settings.manage'],
['label' => 'إعدادات النظام', 'route' => 'settings.system', 'icon' => 'cog-6-tooth', 'permission' => 'settings.manage'],
['label' => 'معالج الإعداد', 'route' => 'setup.wizard', 'icon' => 'bolt', 'permission' => 'settings.manage'],
['label' => 'لوحة المشرف', 'route' => 'admin.panel', 'icon' => 'shield-check', 'permission' => 'settings.manage'],
......@@ -100,6 +101,7 @@
'eye' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>',
'cog-6-tooth' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>',
'reception' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>',
'swatch' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008z"/>',
];
$permissionService = app(\App\Domain\Identity\Services\PermissionService::class);
......@@ -110,10 +112,16 @@
};
@endphp
<aside dir="rtl" class="fixed top-0 start-0 h-screen w-64 bg-slate-900 text-white flex flex-col z-40 overflow-hidden">
<aside dir="rtl" class="fixed top-0 start-0 h-screen w-64 flex flex-col z-40 overflow-hidden" style="background-color: var(--brand-sidebar-bg, #0f172a); color: var(--brand-sidebar-text, #e2e8f0);">
{{-- Logo --}}
<div class="flex items-center justify-center h-16 border-b border-slate-700 px-4 shrink-0">
<h1 class="text-xl font-bold text-white">الكابتن</h1>
<div class="flex items-center justify-center h-16 border-b border-white/10 px-4 shrink-0">
@if(isset($brandLogoDark) && $brandLogoDark && isset($showLogoSidebar) && $showLogoSidebar)
<img src="{{ Storage::disk('public')->url($brandLogoDark) }}" alt="" class="h-10 max-w-[140px] object-contain">
@elseif(isset($brandLogo) && $brandLogo && isset($showLogoSidebar) && $showLogoSidebar)
<img src="{{ Storage::disk('public')->url($brandLogo) }}" alt="" class="h-10 max-w-[140px] object-contain">
@else
<h1 class="text-xl font-bold">الكابتن</h1>
@endif
</div>
{{-- Navigation --}}
......@@ -123,8 +131,8 @@
@if(isset($item['route']))
@if(Route::has($item['route']) && $userCan($item['permission']))
<a href="{{ route($item['route']) }}"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors duration-150
{{ request()->routeIs($item['route'] . '*') ? 'bg-blue-600 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white' }}">
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors duration-150"
style="{{ request()->routeIs($item['route'] . '*') ? 'background-color: var(--brand-sidebar-active, #2563eb); color: #fff;' : 'color: var(--brand-sidebar-text, #e2e8f0);' }}">
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">{!! $icons[$item['icon']] ?? '' !!}</svg>
<span>{{ $item['label'] }}</span>
</a>
......@@ -140,13 +148,13 @@ class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transi
@if($visibleItems->isNotEmpty())
<div class="pt-5 pb-1">
<p class="px-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">{{ $item['section'] }}</p>
<p class="px-3 text-xs font-semibold uppercase tracking-wider" style="color: var(--brand-sidebar-text, #e2e8f0); opacity: 0.5;">{{ $item['section'] }}</p>
</div>
@foreach($visibleItems as $child)
<a href="{{ route($child['route']) }}"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors duration-150
{{ request()->routeIs(Str::before($child['route'], '.index') . '.*') ? 'bg-blue-600 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white' }}">
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors duration-150"
style="{{ request()->routeIs(Str::before($child['route'], '.index') . '.*') ? 'background-color: var(--brand-sidebar-active, #2563eb); color: #fff;' : 'color: var(--brand-sidebar-text, #e2e8f0);' }}">
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">{!! $icons[$child['icon']] ?? '' !!}</svg>
<span>{{ $child['label'] }}</span>
</a>
......@@ -157,9 +165,9 @@ class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transiti
</nav>
{{-- User info at bottom --}}
<div class="border-t border-slate-700 p-4 shrink-0">
<div class="border-t border-white/10 p-4 shrink-0">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center shrink-0">
<div class="w-8 h-8 rounded-full flex items-center justify-center shrink-0" style="background-color: var(--brand-sidebar-active, #2563eb);">
<span class="text-sm font-medium text-white">{{ mb_substr(auth()->user()->name_ar ?? auth()->user()->name, 0, 1) }}</span>
</div>
<div class="flex-1 min-w-0">
......
@php
$branding = app(\App\Domain\Shared\Services\SettingsService::class);
$brandPrimary = $branding->get('branding.primary_color', '#2563eb');
$brandSecondary = $branding->get('branding.secondary_color', '#7c3aed');
$brandAccent = $branding->get('branding.accent_color', '#059669');
$brandSidebarBg = $branding->get('branding.sidebar_bg', '#0f172a');
$brandSidebarText = $branding->get('branding.sidebar_text', '#e2e8f0');
$brandSidebarActive = $branding->get('branding.sidebar_active', '#2563eb');
$brandSuccess = $branding->get('branding.success_color', '#10b981');
$brandWarning = $branding->get('branding.warning_color', '#f59e0b');
$brandDanger = $branding->get('branding.danger_color', '#ef4444');
$brandFontAr = $branding->get('branding.font_family_ar', 'Cairo');
$brandFontEn = $branding->get('branding.font_family_en', 'Inter');
$brandFontSize = $branding->get('branding.font_size_base', '14');
$brandLogo = $branding->get('branding.logo');
$brandLogoDark = $branding->get('branding.logo_dark');
$showLogoSidebar = $branding->get('branding.show_logo_in_sidebar', true);
@endphp
<!DOCTYPE html>
<html dir="rtl" lang="ar" class="h-full">
<head>
......@@ -5,12 +23,30 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'El Captain' }} - الكابتن</title>
@if($favicon = $branding->get('branding.favicon'))
<link rel="icon" href="{{ Storage::disk('public')->url($favicon) }}" type="image/png">
@endif
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family={{ urlencode($brandFontAr) }}:wght@300;400;500;600;700;800&family={{ urlencode($brandFontEn) }}:wght@300;400;500;600;700&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
<style>
body { font-family: 'Cairo', sans-serif; }
:root {
--brand-primary: {{ $brandPrimary }};
--brand-secondary: {{ $brandSecondary }};
--brand-accent: {{ $brandAccent }};
--brand-sidebar-bg: {{ $brandSidebarBg }};
--brand-sidebar-text: {{ $brandSidebarText }};
--brand-sidebar-active: {{ $brandSidebarActive }};
--brand-success: {{ $brandSuccess }};
--brand-warning: {{ $brandWarning }};
--brand-danger: {{ $brandDanger }};
--brand-font-size: {{ $brandFontSize }}px;
}
body {
font-family: '{{ $brandFontAr }}', '{{ $brandFontEn }}', sans-serif;
font-size: var(--brand-font-size);
}
</style>
</head>
<body class="h-full bg-gray-50">
......
<div x-data="{
activeTab: 'logo',
preview: {
primary: @entangle('primary_color'),
secondary: @entangle('secondary_color'),
accent: @entangle('accent_color'),
sidebarBg: @entangle('sidebar_bg'),
sidebarText: @entangle('sidebar_text'),
sidebarActive: @entangle('sidebar_active'),
}
}">
<!-- Header -->
<div class="mb-8">
<h1 class="text-2xl font-bold text-gray-900">{{ __('الهوية البصرية') }}</h1>
<p class="mt-1 text-sm text-gray-500">{{ __('تخصيص مظهر النظام والشعارات والألوان والخطوط') }}</p>
</div>
@if(session('success'))
<div class="mb-4 p-4 bg-emerald-50 border border-emerald-200 text-emerald-700 rounded-xl text-sm flex items-center gap-2">
<svg class="w-5 h-5 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
{{ session('success') }}
</div>
@endif
<!-- Tabs -->
<div class="mb-6 border-b border-gray-200">
<nav class="flex gap-1 -mb-px">
@foreach([
'logo' => 'الشعارات والصور',
'colors' => 'الألوان',
'typography' => 'الخطوط',
'invoice' => 'الفواتير والمطبوعات',
'display' => 'خيارات العرض',
] as $key => $label)
<button type="button" @click="activeTab = '{{ $key }}'"
:class="activeTab === '{{ $key }}'
? 'border-blue-600 text-blue-700 bg-blue-50/50'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="px-5 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-all rounded-t-lg">
{{ __($label) }}
</button>
@endforeach
</nav>
</div>
<form wire:submit="save">
<!-- Logo & Images Tab -->
<div x-show="activeTab === 'logo'" x-transition>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Main Logo -->
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<div>
<h3 class="font-semibold text-gray-900">{{ __('الشعار الرئيسي') }}</h3>
<p class="text-xs text-gray-500 mt-1">{{ __('يظهر في الشريط الجانبي وصفحة تسجيل الدخول. PNG أو SVG، أقصى 2 ميجا') }}</p>
</div>
<div class="border-2 border-dashed border-gray-300 rounded-xl p-6 text-center hover:border-blue-400 transition-colors relative">
@if($logo)
<img src="{{ $logo->temporaryUrl() }}" class="mx-auto h-20 object-contain rounded">
@elseif($current_logo)
<img src="{{ Storage::disk('public')->url($current_logo) }}" class="mx-auto h-20 object-contain rounded">
<button type="button" wire:click="removeImage('logo')" class="absolute top-2 start-2 p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
@else
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<p class="mt-2 text-sm text-gray-500">{{ __('اسحب الملف هنا أو انقر للاختيار') }}</p>
@endif
<input type="file" wire:model="logo" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
</div>
@error('logo') <p class="text-sm text-red-600">{{ $message }}</p> @enderror
<div wire:loading wire:target="logo" class="text-sm text-blue-600 flex items-center gap-2">
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg>
{{ __('جارٍ رفع الشعار...') }}
</div>
</div>
<!-- Dark Logo -->
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<div>
<h3 class="font-semibold text-gray-900">{{ __('الشعار (الوضع الداكن)') }}</h3>
<p class="text-xs text-gray-500 mt-1">{{ __('نسخة فاتحة للخلفيات الداكنة. اختياري') }}</p>
</div>
<div class="border-2 border-dashed border-gray-300 rounded-xl p-6 text-center bg-slate-800 hover:border-blue-400 transition-colors relative">
@if($logo_dark)
<img src="{{ $logo_dark->temporaryUrl() }}" class="mx-auto h-20 object-contain rounded">
@elseif($current_logo_dark)
<img src="{{ Storage::disk('public')->url($current_logo_dark) }}" class="mx-auto h-20 object-contain rounded">
<button type="button" wire:click="removeImage('logo_dark')" class="absolute top-2 start-2 p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
@else
<svg class="mx-auto h-12 w-12 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<p class="mt-2 text-sm text-gray-400">{{ __('شعار للخلفية الداكنة') }}</p>
@endif
<input type="file" wire:model="logo_dark" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
</div>
@error('logo_dark') <p class="text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<!-- Favicon -->
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<div>
<h3 class="font-semibold text-gray-900">{{ __('الأيقونة المصغرة (Favicon)') }}</h3>
<p class="text-xs text-gray-500 mt-1">{{ __('تظهر في تبويب المتصفح. 32x32 أو 64x64 بكسل. أقصى 512 كيلو') }}</p>
</div>
<div class="border-2 border-dashed border-gray-300 rounded-xl p-6 text-center hover:border-blue-400 transition-colors relative">
@if($favicon)
<img src="{{ $favicon->temporaryUrl() }}" class="mx-auto h-12 w-12 object-contain">
@elseif($current_favicon)
<img src="{{ Storage::disk('public')->url($current_favicon) }}" class="mx-auto h-12 w-12 object-contain">
<button type="button" wire:click="removeImage('favicon')" class="absolute top-2 start-2 p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
@else
<svg class="mx-auto h-10 w-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
<p class="mt-2 text-sm text-gray-500">{{ __('أيقونة المتصفح') }}</p>
@endif
<input type="file" wire:model="favicon" accept=".png,.ico,.svg" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
</div>
@error('favicon') <p class="text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<!-- Signature -->
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<div>
<h3 class="font-semibold text-gray-900">{{ __('التوقيع الرسمي') }}</h3>
<p class="text-xs text-gray-500 mt-1">{{ __('يظهر في الفواتير والشهادات. صورة شفافة PNG مفضلة') }}</p>
</div>
<div class="border-2 border-dashed border-gray-300 rounded-xl p-6 text-center hover:border-blue-400 transition-colors relative">
@if($signature)
<img src="{{ $signature->temporaryUrl() }}" class="mx-auto h-16 object-contain">
@elseif($current_signature)
<img src="{{ Storage::disk('public')->url($current_signature) }}" class="mx-auto h-16 object-contain">
<button type="button" wire:click="removeImage('signature')" class="absolute top-2 start-2 p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
@else
<svg class="mx-auto h-10 w-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"/></svg>
<p class="mt-2 text-sm text-gray-500">{{ __('صورة التوقيع') }}</p>
@endif
<input type="file" wire:model="signature" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
</div>
@error('signature') <p class="text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<!-- Login Background -->
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<div>
<h3 class="font-semibold text-gray-900">{{ __('خلفية صفحة الدخول') }}</h3>
<p class="text-xs text-gray-500 mt-1">{{ __('صورة خلفية لشاشة تسجيل الدخول. أقصى 5 ميجا') }}</p>
</div>
<div class="border-2 border-dashed border-gray-300 rounded-xl p-6 text-center hover:border-blue-400 transition-colors relative">
@if($login_background)
<img src="{{ $login_background->temporaryUrl() }}" class="mx-auto h-24 object-cover rounded-lg w-full">
@elseif($current_login_background)
<img src="{{ Storage::disk('public')->url($current_login_background) }}" class="mx-auto h-24 object-cover rounded-lg w-full">
<button type="button" wire:click="removeImage('login_background')" class="absolute top-2 start-2 p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
@else
<svg class="mx-auto h-10 w-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<p class="mt-2 text-sm text-gray-500">{{ __('خلفية صفحة الدخول') }}</p>
@endif
<input type="file" wire:model="login_background" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
</div>
@error('login_background') <p class="text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<!-- Invoice Header -->
<div class="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<div>
<h3 class="font-semibold text-gray-900">{{ __('رأس الفاتورة') }}</h3>
<p class="text-xs text-gray-500 mt-1">{{ __('صورة تظهر أعلى الفاتورة. يفضل بعرض 800 بكسل') }}</p>
</div>
<div class="border-2 border-dashed border-gray-300 rounded-xl p-6 text-center hover:border-blue-400 transition-colors relative">
@if($invoice_header)
<img src="{{ $invoice_header->temporaryUrl() }}" class="mx-auto h-16 object-contain w-full">
@elseif($current_invoice_header)
<img src="{{ Storage::disk('public')->url($current_invoice_header) }}" class="mx-auto h-16 object-contain w-full">
<button type="button" wire:click="removeImage('invoice_header')" class="absolute top-2 start-2 p-1.5 bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
@else
<svg class="mx-auto h-10 w-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
<p class="mt-2 text-sm text-gray-500">{{ __('ترويسة الفاتورة') }}</p>
@endif
<input type="file" wire:model="invoice_header" accept="image/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
</div>
@error('invoice_header') <p class="text-sm text-red-600">{{ $message }}</p> @enderror
</div>
</div>
</div>
<!-- Colors Tab -->
<div x-show="activeTab === 'colors'" x-transition>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Color Pickers -->
<div class="lg:col-span-2 space-y-6">
<!-- Main Colors -->
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h3 class="font-semibold text-gray-900 mb-4">{{ __('الألوان الرئيسية') }}</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
@foreach([
'primary_color' => 'اللون الأساسي',
'secondary_color' => 'اللون الثانوي',
'accent_color' => 'لون التمييز',
] as $field => $label)
<div>
<label class="block text-xs font-medium text-gray-600 mb-2">{{ __($label) }}</label>
<div class="flex items-center gap-2">
<input type="color" wire:model.live="{{ $field }}" class="h-10 w-14 rounded-lg border border-gray-300 cursor-pointer p-0.5">
<input type="text" wire:model.live="{{ $field }}" dir="ltr" class="flex-1 text-xs font-mono border-gray-300 rounded-lg px-2 py-2">
</div>
</div>
@endforeach
</div>
</div>
<!-- Sidebar Colors -->
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h3 class="font-semibold text-gray-900 mb-4">{{ __('ألوان الشريط الجانبي') }}</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
@foreach([
'sidebar_bg' => 'خلفية الشريط',
'sidebar_text' => 'لون النص',
'sidebar_active' => 'العنصر النشط',
] as $field => $label)
<div>
<label class="block text-xs font-medium text-gray-600 mb-2">{{ __($label) }}</label>
<div class="flex items-center gap-2">
<input type="color" wire:model.live="{{ $field }}" class="h-10 w-14 rounded-lg border border-gray-300 cursor-pointer p-0.5">
<input type="text" wire:model.live="{{ $field }}" dir="ltr" class="flex-1 text-xs font-mono border-gray-300 rounded-lg px-2 py-2">
</div>
</div>
@endforeach
</div>
</div>
<!-- Status Colors -->
<div class="bg-white rounded-xl border border-gray-200 p-6">
<h3 class="font-semibold text-gray-900 mb-4">{{ __('ألوان الحالات') }}</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
@foreach([
'success_color' => 'نجاح',
'warning_color' => 'تحذير',
'danger_color' => 'خطأ',
'header_bg' => 'خلفية الرأس',
] as $field => $label)
<div>
<label class="block text-xs font-medium text-gray-600 mb-2">{{ __($label) }}</label>
<div class="flex items-center gap-2">
<input type="color" wire:model.live="{{ $field }}" class="h-10 w-14 rounded-lg border border-gray-300 cursor-pointer p-0.5">
<input type="text" wire:model.live="{{ $field }}" dir="ltr" class="flex-1 text-xs font-mono border-gray-300 rounded-lg px-2 py-2">
</div>
</div>
@endforeach
</div>
</div>
</div>
<!-- Live Preview -->
<div class="lg:col-span-1">
<div class="sticky top-4 space-y-4">
<div class="bg-white rounded-xl border border-gray-200 p-4">
<h4 class="text-xs font-semibold text-gray-500 uppercase mb-3">{{ __('معاينة حية') }}</h4>
<!-- Mini sidebar preview -->
<div class="rounded-xl overflow-hidden border border-gray-200 shadow-sm" style="height: 280px;">
<div class="flex h-full" dir="rtl">
<div class="w-16 flex flex-col items-center py-3 gap-2" :style="`background-color: ${preview.sidebarBg}`">
<div class="w-8 h-8 rounded-lg flex items-center justify-center" :style="`background-color: ${preview.sidebarActive}`">
<svg class="w-4 h-4" :style="`color: ${preview.sidebarText}`" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
</div>
<div class="w-8 h-8 rounded-lg flex items-center justify-center opacity-60" :style="`color: ${preview.sidebarText}`">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</div>
<div class="w-8 h-8 rounded-lg flex items-center justify-center opacity-60" :style="`color: ${preview.sidebarText}`">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
</div>
</div>
<div class="flex-1 bg-gray-50 p-3">
<div class="h-4 w-24 rounded" :style="`background-color: ${preview.primary}`"></div>
<div class="mt-3 bg-white rounded-lg p-2 shadow-sm space-y-2">
<div class="h-2.5 bg-gray-200 rounded w-full"></div>
<div class="h-2.5 bg-gray-200 rounded w-3/4"></div>
<div class="h-2.5 bg-gray-200 rounded w-1/2"></div>
</div>
<div class="mt-3 flex gap-2">
<div class="h-6 px-3 rounded-md flex items-center" :style="`background-color: ${preview.primary}`">
<span class="text-[8px] text-white">{{ __('حفظ') }}</span>
</div>
<div class="h-6 px-3 rounded-md flex items-center" :style="`background-color: ${preview.secondary}`">
<span class="text-[8px] text-white">{{ __('إلغاء') }}</span>
</div>
</div>
<div class="mt-3 flex gap-1.5">
<div class="h-4 w-4 rounded-full" :style="`background-color: ${preview.accent}`"></div>
<div class="h-2.5 bg-gray-200 rounded flex-1 mt-0.5"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Preset palettes -->
<div class="bg-white rounded-xl border border-gray-200 p-4">
<h4 class="text-xs font-semibold text-gray-500 uppercase mb-3">{{ __('لوحات جاهزة') }}</h4>
<div class="space-y-2">
<button type="button" @click="
$wire.set('primary_color', '#2563eb');
$wire.set('secondary_color', '#7c3aed');
$wire.set('accent_color', '#059669');
$wire.set('sidebar_bg', '#0f172a');
$wire.set('sidebar_text', '#e2e8f0');
$wire.set('sidebar_active', '#2563eb');
" class="w-full flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 border border-gray-100 transition-colors">
<div class="flex gap-0.5">
<div class="w-4 h-4 rounded-full bg-blue-600"></div>
<div class="w-4 h-4 rounded-full bg-violet-600"></div>
<div class="w-4 h-4 rounded-full bg-emerald-600"></div>
<div class="w-4 h-4 rounded-full bg-slate-900"></div>
</div>
<span class="text-xs text-gray-600">{{ __('الافتراضي') }}</span>
</button>
<button type="button" @click="
$wire.set('primary_color', '#dc2626');
$wire.set('secondary_color', '#ea580c');
$wire.set('accent_color', '#ca8a04');
$wire.set('sidebar_bg', '#1c1917');
$wire.set('sidebar_text', '#fafaf9');
$wire.set('sidebar_active', '#dc2626');
" class="w-full flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 border border-gray-100 transition-colors">
<div class="flex gap-0.5">
<div class="w-4 h-4 rounded-full bg-red-600"></div>
<div class="w-4 h-4 rounded-full bg-orange-600"></div>
<div class="w-4 h-4 rounded-full bg-yellow-600"></div>
<div class="w-4 h-4 rounded-full bg-stone-900"></div>
</div>
<span class="text-xs text-gray-600">{{ __('رياضي') }}</span>
</button>
<button type="button" @click="
$wire.set('primary_color', '#0d9488');
$wire.set('secondary_color', '#0891b2');
$wire.set('accent_color', '#4f46e5');
$wire.set('sidebar_bg', '#042f2e');
$wire.set('sidebar_text', '#ccfbf1');
$wire.set('sidebar_active', '#0d9488');
" class="w-full flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 border border-gray-100 transition-colors">
<div class="flex gap-0.5">
<div class="w-4 h-4 rounded-full bg-teal-600"></div>
<div class="w-4 h-4 rounded-full bg-cyan-600"></div>
<div class="w-4 h-4 rounded-full bg-indigo-600"></div>
<div class="w-4 h-4 rounded-full bg-teal-950"></div>
</div>
<span class="text-xs text-gray-600">{{ __('أكوا') }}</span>
</button>
<button type="button" @click="
$wire.set('primary_color', '#16a34a');
$wire.set('secondary_color', '#65a30d');
$wire.set('accent_color', '#0284c7');
$wire.set('sidebar_bg', '#14532d');
$wire.set('sidebar_text', '#dcfce7');
$wire.set('sidebar_active', '#16a34a');
" class="w-full flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50 border border-gray-100 transition-colors">
<div class="flex gap-0.5">
<div class="w-4 h-4 rounded-full bg-green-600"></div>
<div class="w-4 h-4 rounded-full bg-lime-600"></div>
<div class="w-4 h-4 rounded-full bg-sky-600"></div>
<div class="w-4 h-4 rounded-full bg-green-900"></div>
</div>
<span class="text-xs text-gray-600">{{ __('طبيعي') }}</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Typography Tab -->
<div x-show="activeTab === 'typography'" x-transition>
<div class="bg-white rounded-xl border border-gray-200 p-6 max-w-2xl">
<h3 class="font-semibold text-gray-900 mb-6">{{ __('إعدادات الخطوط') }}</h3>
<div class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('خط عربي') }}</label>
<select wire:model="font_family_ar" class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
<option value="Cairo">Cairo</option>
<option value="Tajawal">Tajawal</option>
<option value="Noto Sans Arabic">Noto Sans Arabic</option>
<option value="IBM Plex Sans Arabic">IBM Plex Sans Arabic</option>
<option value="Almarai">Almarai</option>
<option value="Changa">Changa</option>
<option value="El Messiri">El Messiri</option>
</select>
<p class="mt-1 text-xs text-gray-500">{{ __('الخط المستخدم للنصوص العربية في النظام') }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('خط إنجليزي') }}</label>
<select wire:model="font_family_en" class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
<option value="Inter">Inter</option>
<option value="Poppins">Poppins</option>
<option value="Roboto">Roboto</option>
<option value="Open Sans">Open Sans</option>
<option value="Nunito">Nunito</option>
<option value="DM Sans">DM Sans</option>
</select>
<p class="mt-1 text-xs text-gray-500">{{ __('الخط المستخدم للنصوص الإنجليزية والأرقام') }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('حجم الخط الأساسي') }}</label>
<div class="flex items-center gap-4">
<input type="range" wire:model.live="font_size_base" min="12" max="18" step="1" class="flex-1">
<span class="text-sm font-mono text-gray-600 w-12 text-center" dir="ltr">{{ $font_size_base }}px</span>
</div>
<p class="mt-1 text-xs text-gray-500">{{ __('الحجم الافتراضي للنصوص (12-18 بكسل)') }}</p>
</div>
<!-- Font preview -->
<div class="border border-gray-200 rounded-lg p-4 bg-gray-50 mt-4">
<p class="text-xs text-gray-400 mb-2">{{ __('معاينة') }}</p>
<p class="text-gray-900" style="font-size: {{ $font_size_base }}px">أكاديمية الكابتن الرياضية — El Captain Sports Academy</p>
<p class="text-gray-600 mt-1" style="font-size: {{ max(10, (int)$font_size_base - 2) }}px">هذا نص تجريبي لمعاينة حجم الخط المختار. The quick brown fox jumps over the lazy dog.</p>
</div>
</div>
</div>
</div>
<!-- Invoice/Print Tab -->
<div x-show="activeTab === 'invoice'" x-transition>
<div class="bg-white rounded-xl border border-gray-200 p-6 max-w-3xl space-y-6">
<h3 class="font-semibold text-gray-900">{{ __('نصوص الفواتير والإيصالات') }}</h3>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('تذييل الفاتورة') }}</label>
<textarea wire:model="invoice_footer_text" rows="3" class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-sm"
placeholder="{{ __('مثال: شكراً لثقتكم بنا. للاستفسار: 01234567890') }}"></textarea>
<p class="mt-1 text-xs text-gray-500">{{ __('يظهر أسفل كل فاتورة مطبوعة') }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('تذييل الإيصال') }}</label>
<textarea wire:model="receipt_footer_text" rows="2" class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-sm"
placeholder="{{ __('مثال: هذا الإيصال دليل على الدفع. يرجى الاحتفاظ به') }}"></textarea>
<p class="mt-1 text-xs text-gray-500">{{ __('يظهر أسفل كل إيصال') }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ __('الشروط والأحكام') }}</label>
<textarea wire:model="terms_and_conditions" rows="5" class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-sm"
placeholder="{{ __('الشروط والأحكام التي تظهر في الفواتير والعقود...') }}"></textarea>
<p class="mt-1 text-xs text-gray-500">{{ __('تظهر في الفواتير التفصيلية وعقود الاشتراك') }}</p>
</div>
</div>
</div>
<!-- Display Options Tab -->
<div x-show="activeTab === 'display'" x-transition>
<div class="bg-white rounded-xl border border-gray-200 p-6 max-w-2xl">
<h3 class="font-semibold text-gray-900 mb-6">{{ __('خيارات العرض') }}</h3>
<div class="space-y-5">
@foreach([
'show_logo_in_sidebar' => ['إظهار الشعار في الشريط الجانبي', 'يعرض شعار الأكاديمية أعلى القائمة الجانبية'],
'show_logo_in_invoice' => ['إظهار الشعار في الفواتير', 'يطبع شعار الأكاديمية في رأس الفاتورة'],
'show_signature_in_invoice' => ['إظهار التوقيع في الفواتير', 'يطبع التوقيع الرسمي أسفل الفاتورة'],
'compact_sidebar' => ['شريط جانبي مضغوط', 'يعرض الشريط الجانبي بحجم أصغر (أيقونات فقط)'],
] as $field => [$label, $desc])
<label class="flex items-start gap-4 cursor-pointer group">
<div class="relative mt-0.5">
<input type="checkbox" wire:model="{{ $field }}" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-100 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</div>
<div>
<span class="text-sm font-medium text-gray-900 group-hover:text-blue-700 transition-colors">{{ __($label) }}</span>
<p class="text-xs text-gray-500 mt-0.5">{{ __($desc) }}</p>
</div>
</label>
@endforeach
</div>
</div>
</div>
<!-- Save Button (always visible) -->
<div class="mt-8 flex items-center gap-4 border-t border-gray-200 pt-6">
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="px-8 py-3 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 disabled:bg-blue-400 transition-all shadow-sm hover:shadow-md">
<span wire:loading.remove wire:target="save">{{ __('حفظ التغييرات') }}</span>
<span wire:loading wire:target="save" class="flex items-center gap-2">
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg>
{{ __('جارٍ الحفظ...') }}
</span>
</button>
<p class="text-xs text-gray-500">{{ __('التغييرات تطبق فوراً على جميع صفحات النظام') }}</p>
</div>
</form>
</div>
......@@ -288,6 +288,8 @@
// System Settings
Route::get('/settings/system', SystemSettingsForm::class)->name('settings.system')
->middleware('permission:settings.manage');
Route::get('/settings/branding', \App\Livewire\Settings\BrandingSettings::class)->name('settings.branding')
->middleware('permission:settings.manage');
// SuperAdmin
Route::get('/admin', SuperAdminPanel::class)->name('admin.panel')
......
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