From dbc40a133f93570ac26c55d0ef1868c2bf5bdbd2 Mon Sep 17 00:00:00 2001 From: ChanukaST <167134360+ChanukaST@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:31:47 +0530 Subject: [PATCH] Stock Movement Backfill for Non-Reusable Handover Audits --- app/Http/Controllers/DashboardController.php | 4 ++ app/Http/Controllers/HandoverController.php | 40 +++++++++------ app/Models/StockMovement.php | 5 ++ ..._non_reusable_handover_audit_movements.php | 47 ++++++++++++++++++ database/seeders/DatabaseSeeder.php | 42 +++++++++++----- public/css/dashboard.css | 7 +++ public/css/handover.css | 1 + public/css/inventory.css | 11 ++++ public/css/theme.css | 34 ++++++++++++- public/images/glitzpark_logo.jpg | Bin 0 -> 8519 bytes public/js/theme.js | 18 ++++++- resources/views/auth/login.blade.php | 4 +- resources/views/dashboard.blade.php | 35 +++++++++++-- resources/views/handovers/create.blade.php | 10 ++-- resources/views/handovers/index.blade.php | 8 +-- resources/views/inventory/index.blade.php | 2 +- resources/views/layouts/app.blade.php | 5 +- tests/Feature/AdminAuthenticationTest.php | 2 + tests/Feature/ExampleTest.php | 6 +++ tests/Feature/UniformWorkflowTest.php | 23 +++++++++ 20 files changed, 258 insertions(+), 46 deletions(-) create mode 100644 database/migrations/2026_07_14_000004_backfill_non_reusable_handover_audit_movements.php create mode 100644 public/css/handover.css create mode 100644 public/images/glitzpark_logo.jpg diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 6a4e785..3377ba9 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -17,6 +17,8 @@ class DashboardController extends Controller { $monthStart = now()->startOfMonth()->toDateString(); $monthEnd = now()->endOfMonth()->toDateString(); + $monthStartAt = now()->startOfMonth(); + $monthEndAt = now()->endOfMonth(); $variants = StockVariant::count(); $healthyVariants = StockVariant::whereColumn('quantity', '>', 'reorder_level')->count(); @@ -54,6 +56,8 @@ class DashboardController extends Controller ->select('handover_items.condition', DB::raw('SUM(handover_items.quantity) as units')) ->groupBy('condition')->pluck('units', 'condition'), 'recentMovements' => StockMovement::with(['variant.item'])->latest()->take(8)->get(), + 'monthlyMovements' => StockMovement::with(['variant.item', 'employee']) + ->whereBetween('created_at', [$monthStartAt, $monthEndAt])->latest()->get(), ]); } } diff --git a/app/Http/Controllers/HandoverController.php b/app/Http/Controllers/HandoverController.php index 8bd788f..01c745f 100644 --- a/app/Http/Controllers/HandoverController.php +++ b/app/Http/Controllers/HandoverController.php @@ -16,7 +16,7 @@ class HandoverController extends Controller public function index() { return view('handovers.index', [ - 'employees' => Employee::where('status', 'active')->whereHas('issues', fn ($q) => $q->where('status', 'issued'))->orderBy('name')->get(), + 'employees' => Employee::where('status', 'active')->withCount(['issues as outstanding_issues_count' => fn ($query) => $query->where('status', 'issued')])->orderBy('name')->get(), 'handovers' => Handover::with(['employee', 'items.issue.variant.item'])->latest('handover_date')->get(), ]); } @@ -34,7 +34,7 @@ class HandoverController extends Controller $data = $request->validate([ 'handover_date' => ['required', 'date'], 'notes' => ['nullable', 'string', 'max:1000'], - 'items' => ['required', 'array'], + 'items' => ['nullable', 'array'], 'items.*.issue_id' => ['required', 'integer', 'distinct', 'exists:uniform_issues,id'], 'items.*.condition' => ['required', Rule::in(['reusable', 'damaged', 'not_returned'])], ]); @@ -46,13 +46,14 @@ class HandoverController extends Controller } $openIssues = UniformIssue::where('employee_id', $employee->id)->where('status', 'issued')->lockForUpdate()->get()->keyBy('id'); - if ($openIssues->count() !== count($data['items'])) { + $submittedItems = $data['items'] ?? []; + if ($openIssues->count() !== count($submittedItems)) { throw ValidationException::withMessages(['items' => 'Every outstanding uniform must be accounted for before handover is completed.']); } $handover = Handover::create(['employee_id' => $employee->id, 'handover_date' => $data['handover_date'], 'notes' => $data['notes'] ?? null]); - foreach ($data['items'] as $itemData) { + foreach ($submittedItems as $itemData) { $issue = $openIssues->get($itemData['issue_id']); if (! $issue) { throw ValidationException::withMessages(['items' => 'An invalid issue was submitted.']); @@ -60,22 +61,31 @@ class HandoverController extends Controller $quantity = $issue->outstanding_quantity; $handover->items()->create(['uniform_issue_id' => $issue->id, 'quantity' => $quantity, 'condition' => $itemData['condition']]); + $variant = $issue->variant()->lockForUpdate()->first(); if ($itemData['condition'] === 'reusable') { - $variant = $issue->variant()->lockForUpdate()->first(); $variant->increment('quantity', $quantity); $variant->increment('returned_quantity', $quantity); - StockMovement::create([ - 'stock_variant_id' => $issue->stock_variant_id, - 'employee_id' => $employee->id, - 'uniform_issue_id' => $issue->id, - 'type' => 'return', - 'quantity_change' => $quantity, - 'stock_category' => 'returned', - 'unit_value' => $variant->unit_value, - 'notes' => 'Reusable return during employee exit handover', - ]); } + StockMovement::create([ + 'stock_variant_id' => $issue->stock_variant_id, + 'employee_id' => $employee->id, + 'uniform_issue_id' => $issue->id, + 'type' => match ($itemData['condition']) { + 'reusable' => 'handover_reusable', + 'damaged' => 'handover_damaged', + default => 'handover_not_returned', + }, + 'quantity_change' => $itemData['condition'] === 'reusable' ? $quantity : 0, + 'stock_category' => $itemData['condition'] === 'reusable' ? 'returned' : null, + 'unit_value' => $variant->unit_value, + 'notes' => match ($itemData['condition']) { + 'reusable' => 'Reusable return during employee exit handover', + 'damaged' => "{$quantity} damaged unit(s) recorded during employee exit handover; stock unchanged", + default => "{$quantity} unit(s) not returned during employee exit handover; stock unchanged", + }, + ]); + $issue->update(['returned_quantity' => $issue->quantity, 'status' => $itemData['condition'] === 'not_returned' ? 'not_returned' : 'returned']); } diff --git a/app/Models/StockMovement.php b/app/Models/StockMovement.php index 9a3329b..16a1e65 100644 --- a/app/Models/StockMovement.php +++ b/app/Models/StockMovement.php @@ -14,4 +14,9 @@ class StockMovement extends Model { return $this->belongsTo(StockVariant::class, 'stock_variant_id'); } + + public function employee() + { + return $this->belongsTo(Employee::class); + } } diff --git a/database/migrations/2026_07_14_000004_backfill_non_reusable_handover_audit_movements.php b/database/migrations/2026_07_14_000004_backfill_non_reusable_handover_audit_movements.php new file mode 100644 index 0000000..23a695f --- /dev/null +++ b/database/migrations/2026_07_14_000004_backfill_non_reusable_handover_audit_movements.php @@ -0,0 +1,47 @@ +join('handovers as h', 'h.id', '=', 'hi.handover_id') + ->join('uniform_issues as ui', 'ui.id', '=', 'hi.uniform_issue_id') + ->join('stock_variants as sv', 'sv.id', '=', 'ui.stock_variant_id') + ->whereIn('hi.condition', ['damaged', 'not_returned']) + ->select('hi.*', 'h.employee_id', 'h.created_at as handover_created_at', 'ui.stock_variant_id', 'sv.unit_value') + ->get(); + + foreach ($items as $item) { + $exists = DB::table('stock_movements') + ->where('uniform_issue_id', $item->uniform_issue_id) + ->whereIn('type', ['handover_damaged', 'handover_not_returned']) + ->exists(); + + if (! $exists) { + DB::table('stock_movements')->insert([ + 'stock_variant_id' => $item->stock_variant_id, + 'employee_id' => $item->employee_id, + 'uniform_issue_id' => $item->uniform_issue_id, + 'type' => $item->condition === 'damaged' ? 'handover_damaged' : 'handover_not_returned', + 'quantity_change' => 0, + 'stock_category' => null, + 'unit_value' => $item->unit_value, + 'notes' => $item->condition === 'damaged' + ? "{$item->quantity} damaged unit(s) recorded during employee exit handover; stock unchanged" + : "{$item->quantity} unit(s) not returned during employee exit handover; stock unchanged", + 'created_at' => $item->handover_created_at, + 'updated_at' => $item->handover_created_at, + ]); + } + } + } + + public function down(): void + { + DB::table('stock_movements')->whereIn('type', ['handover_damaged', 'handover_not_returned'])->delete(); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 9d4bab1..ba9741e 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -41,22 +41,38 @@ class DatabaseSeeder extends Seeder } $catalogue = [ - ['code' => 'SHIRT-NVY', 'name' => 'Navy Work Shirt', 'sizes' => ['S' => 8, 'M' => 14, 'L' => 9, 'XL' => 3]], - ['code' => 'TROUSER-GRY', 'name' => 'Grey Work Trouser', 'sizes' => ['28' => 4, '30' => 9, '32' => 12, '34' => 6, '36' => 2]], - ['code' => 'POLO-WHT', 'name' => 'White Polo Shirt', 'sizes' => ['S' => 7, 'M' => 10, 'L' => 5, 'XL' => 0]], - ['code' => 'SEC-SHIRT', 'name' => 'Security Officer Shirt', 'sizes' => ['S' => 6, 'M' => 12, 'L' => 10, 'XL' => 5, '2XL' => 2]], - ['code' => 'HK-TUNIC', 'name' => 'Housekeeping Tunic', 'sizes' => ['XS' => 4, 'S' => 9, 'M' => 11, 'L' => 7, 'XL' => 3]], - ['code' => 'CHEF-COAT', 'name' => 'White Chef Coat', 'sizes' => ['S' => 5, 'M' => 8, 'L' => 8, 'XL' => 4, '2XL' => 2]], - ['code' => 'VEST-HV', 'name' => 'High Visibility Safety Vest', 'sizes' => ['S' => 8, 'M' => 15, 'L' => 14, 'XL' => 7, '2XL' => 4]], - ['code' => 'BLAZER-BLK', 'name' => 'Black Front Office Blazer', 'sizes' => ['XS' => 2, 'S' => 5, 'M' => 7, 'L' => 5, 'XL' => 2]], + ['code' => 'SHIRT-NVY', 'name' => 'Navy Work Shirt', 'material' => '65% polyester, 35% cotton twill', 'color' => 'Navy blue', 'description' => 'Short-sleeve regular-fit operations shirt with chest pocket.', 'care_instructions' => 'Machine wash at 30 C. Warm iron. Do not bleach.', 'unit_value' => 2850, 'sizes' => ['S' => 8, 'M' => 14, 'L' => 9, 'XL' => 3], 'returned' => ['M' => 2, 'L' => 1]], + ['code' => 'TROUSER-GRY', 'name' => 'Grey Work Trouser', 'material' => 'Poly-viscose suiting fabric', 'color' => 'Charcoal grey', 'description' => 'Straight-leg work trouser with reinforced side pockets.', 'care_instructions' => 'Machine wash cold. Line dry. Medium iron.', 'unit_value' => 3650, 'sizes' => ['28' => 4, '30' => 9, '32' => 12, '34' => 6, '36' => 2], 'returned' => ['30' => 1, '32' => 2]], + ['code' => 'POLO-WHT', 'name' => 'White Polo Shirt', 'material' => '220 GSM cotton pique', 'color' => 'White', 'description' => 'Unisex polo shirt with ribbed collar and embroidered logo area.', 'care_instructions' => 'Wash whites separately at 30 C. Do not tumble dry.', 'unit_value' => 2250, 'sizes' => ['S' => 7, 'M' => 10, 'L' => 5, 'XL' => 2], 'returned' => ['S' => 1, 'M' => 1]], + ['code' => 'SEC-SHIRT', 'name' => 'Security Officer Shirt', 'material' => 'Durable polyester-cotton ripstop', 'color' => 'Light blue', 'description' => 'Epaulette security shirt with two buttoned chest pockets.', 'care_instructions' => 'Machine wash at 30 C. Close buttons before washing.', 'unit_value' => 3300, 'sizes' => ['S' => 6, 'M' => 12, 'L' => 10, 'XL' => 5, '2XL' => 2], 'returned' => ['M' => 2, 'L' => 1]], + ['code' => 'HK-TUNIC', 'name' => 'Housekeeping Tunic', 'material' => 'Easy-care polyester stretch blend', 'color' => 'Teal and charcoal', 'description' => 'Side-vent housekeeping tunic with concealed front fastening.', 'care_instructions' => 'Machine wash at 40 C. Low tumble dry. Cool iron.', 'unit_value' => 2750, 'sizes' => ['XS' => 4, 'S' => 9, 'M' => 11, 'L' => 7, 'XL' => 3], 'returned' => ['S' => 1, 'M' => 2]], + ['code' => 'CHEF-COAT', 'name' => 'White Chef Coat', 'material' => 'Heavyweight cotton drill', 'color' => 'White', 'description' => 'Double-breasted long-sleeve chef coat with thermometer pocket.', 'care_instructions' => 'Hot wash up to 60 C. Wash separately. Hot iron.', 'unit_value' => 3150, 'sizes' => ['S' => 5, 'M' => 8, 'L' => 8, 'XL' => 4, '2XL' => 2], 'returned' => ['L' => 1]], + ['code' => 'VEST-HV', 'name' => 'High Visibility Safety Vest', 'material' => 'Fluorescent polyester mesh with reflective tape', 'color' => 'Fluorescent yellow', 'description' => 'Lightweight high-visibility vest with front hook-and-loop closure.', 'care_instructions' => 'Gentle wash at 30 C. Do not iron reflective tape.', 'unit_value' => 1450, 'sizes' => ['S' => 8, 'M' => 15, 'L' => 14, 'XL' => 7, '2XL' => 4], 'returned' => ['M' => 2, 'L' => 2, 'XL' => 1]], + ['code' => 'BLAZER-BLK', 'name' => 'Black Front Office Blazer', 'material' => 'Premium poly-wool suiting blend', 'color' => 'Black', 'description' => 'Single-breasted tailored blazer with two-button closure.', 'care_instructions' => 'Dry clean only. Store on a shaped hanger.', 'unit_value' => 8950, 'sizes' => ['XS' => 2, 'S' => 5, 'M' => 7, 'L' => 5, 'XL' => 2], 'returned' => ['M' => 1]], ]; foreach ($catalogue as $data) { - $item = UniformItem::updateOrCreate(['code' => $data['code']], ['name' => $data['name']]); - foreach ($data['sizes'] as $size => $quantity) { - $variant = $item->variants()->firstOrCreate(['size' => $size], ['quantity' => $quantity, 'new_quantity' => $quantity, 'reorder_level' => 4]); - if ($variant->wasRecentlyCreated && $quantity) { - StockMovement::create(['stock_variant_id' => $variant->id, 'type' => 'opening_stock', 'quantity_change' => $quantity, 'stock_category' => 'new', 'unit_value' => 0, 'notes' => 'Opening stock balance']); + $item = UniformItem::updateOrCreate(['code' => $data['code']], [ + 'name' => $data['name'], + 'material' => $data['material'], + 'color' => $data['color'], + 'description' => $data['description'], + 'care_instructions' => $data['care_instructions'], + ]); + foreach ($data['sizes'] as $size => $newQuantity) { + $returnedQuantity = $data['returned'][$size] ?? 0; + $variant = $item->variants()->firstOrCreate(['size' => $size], [ + 'quantity' => $newQuantity + $returnedQuantity, + 'new_quantity' => $newQuantity, + 'returned_quantity' => $returnedQuantity, + 'unit_value' => $data['unit_value'], + 'reorder_level' => 4, + ]); + if ($variant->wasRecentlyCreated && $newQuantity) { + StockMovement::create(['stock_variant_id' => $variant->id, 'type' => 'opening_stock', 'quantity_change' => $newQuantity, 'stock_category' => 'new', 'unit_value' => $data['unit_value'], 'notes' => 'Opening new-stock balance']); + } + if ($variant->wasRecentlyCreated && $returnedQuantity) { + StockMovement::create(['stock_variant_id' => $variant->id, 'type' => 'opening_stock', 'quantity_change' => $returnedQuantity, 'stock_category' => 'returned', 'unit_value' => $data['unit_value'], 'notes' => 'Opening returned-stock balance']); } } } diff --git a/public/css/dashboard.css b/public/css/dashboard.css index c6b2f91..4aa8be6 100644 --- a/public/css/dashboard.css +++ b/public/css/dashboard.css @@ -1,4 +1,8 @@ .quick-actions { display: flex; gap: 9px; flex-wrap: wrap; justify-content: flex-end; } +.dashboard-datetime { display:flex; align-items:center; gap:8px; margin-bottom:6px; } +.dashboard-datetime .eyebrow { margin:0; } +.dashboard-datetime #dashboard-live-time { font:700 10px "DM Sans"; letter-spacing:.08em; color:var(--green); padding-left:8px; border-left:1px solid var(--line); } +.dashboard-datetime small { font-size:8px; color:var(--muted); font-weight:700; } .sidebar-foot.admin-account { display: block; } .admin-identity { display: flex; gap: 9px; align-items: center; min-width: 0; } .admin-identity > div { min-width: 0; } @@ -36,6 +40,9 @@ html[data-theme="dark"] .chart-bar-area{background:#293730} .monthly-metric { display: flex; justify-content: space-between; align-items: center; padding: 11px 0; border-top: 1px solid #edf0ee; font-size: 12px; color: var(--muted); } .monthly-metric strong { font: 800 17px Manrope; color: var(--ink); } .monthly-metric.danger-text strong { color: var(--rose); } +.monthly-panel-clickable{cursor:pointer;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}.monthly-panel-clickable:hover,.monthly-panel-clickable:focus{transform:translateY(-2px);border-color:#a7c8ba;box-shadow:0 16px 38px rgba(24,48,39,.12);outline:none}.view-detail-hint{font-size:10px;font-weight:700;color:var(--green)} +.monthly-movement-dialog{width:min(1220px,calc(100% - 30px))}.monthly-dialog-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;margin:18px 0;background:var(--line);border:1px solid var(--line);border-radius:10px;overflow:hidden}.monthly-dialog-metrics>div{background:var(--card);padding:14px 16px}.monthly-dialog-metrics small{display:block;color:var(--muted);font-size:9px;text-transform:uppercase;letter-spacing:.06em}.monthly-dialog-metrics strong{display:block;margin-top:5px;font:800 20px Manrope}.monthly-dialog-metrics .positive,.movement-in{color:#25825f}.monthly-dialog-metrics .negative,.movement-out{color:#b95151}.monthly-movement-table{max-height:48vh;border:1px solid var(--line);border-radius:10px}.monthly-movement-table th{position:sticky;top:0;background:var(--card);z-index:1}.monthly-movement-table td{white-space:nowrap}.monthly-movement-table td:last-child{white-space:normal;min-width:180px} +@media(max-width:760px){.monthly-dialog-metrics{grid-template-columns:1fr 1fr}} .dashboard-main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; } .person-avatar { width: 38px; height: 38px; border-radius: 50%; display: grid; place-items: center; background: #e1ede7; color: var(--green); font-weight: 800; font-size: 12px; flex: none; } .dashboard-insights-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } diff --git a/public/css/handover.css b/public/css/handover.css new file mode 100644 index 0000000..13e8275 --- /dev/null +++ b/public/css/handover.css @@ -0,0 +1 @@ +.clear-handover-state{display:flex;align-items:center;gap:13px;padding:22px;border:1px solid #cfe3da;border-radius:10px;background:#f1f8f5}.clear-handover-state>span{width:35px;height:35px;border-radius:50%;display:grid;place-items:center;background:#d8eee4;color:#17634e;font-weight:800}.clear-handover-state strong,.clear-handover-state small{display:block}.clear-handover-state small{margin-top:3px;color:var(--muted);font-size:10px}html[data-theme="dark"] .clear-handover-state{background:#172820;border-color:#315044} diff --git a/public/css/inventory.css b/public/css/inventory.css index be37698..4e9cc19 100644 --- a/public/css/inventory.css +++ b/public/css/inventory.css @@ -31,6 +31,17 @@ .bulk-value { text-align: right; font-size: 9px; } .bulk-value input { margin-top: 3px; padding: 8px 9px; text-align: right; } .bulk-value input:disabled { background: #eef1ef; color: #adb5b1; } +.stock-quantity { display: inline-flex; align-items: center; gap: 9px; min-width: 78px; } +.stock-quantity > i { width: 3px; height: 30px; border-radius: 99px; flex: none; } +.stock-quantity span { display: block; } +.stock-quantity b { display: block; color: #283a34; font: 800 16px Manrope; line-height: 1; font-variant-numeric: tabular-nums; } +.stock-quantity small { display: block; margin-top: 4px; color: #8a9691; font-size: 8px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; } +.new-stock > i { background: #4a9c79; } +.returned-stock > i { background: #d19c46; } +.returned-stock b { color: #8b6427; } +.available-total { display: inline-block; min-width: 32px; color: var(--ink); font: 800 18px Manrope; font-variant-numeric: tabular-nums; } +html[data-theme="dark"] .stock-quantity b { color: #dce9e3; } +html[data-theme="dark"] .returned-stock b { color: #e1b96f; } @media (max-width: 760px) { .check-options { grid-template-columns: repeat(2, 1fr); } diff --git a/public/css/theme.css b/public/css/theme.css index 59789aa..8c98230 100644 --- a/public/css/theme.css +++ b/public/css/theme.css @@ -1,9 +1,40 @@ -.theme-toggle { width: 38px; height: 38px; margin-left: auto; margin-right: 10px; border: 1px solid #dce3df; border-radius: 10px; background: #fff; color: #3e554d; cursor: pointer; display: grid; place-items: center; font-size: 16px; transition: background .18s, color .18s, border-color .18s, transform .18s; } +.theme-toggle { width: 38px; height: 38px; margin-left: auto; margin-right: 10px; border: 1px solid #dce3df; border-radius: 10px; background: #fff; color: #3e554d; cursor: pointer; display: grid; place-items: center; font-size: 16px; transition: background .28s ease, color .28s ease, border-color .28s ease, transform .28s ease; } +.brand-logo { width: 46px; height: 46px; flex: none; object-fit: contain; border-radius: 9px; background: #fff; padding: 2px; } +.login-brand-logo { width: 52px; height: 52px; border-radius: 10px; } .theme-toggle:hover { transform: translateY(-1px); background: #edf4f0; } .topbar .avatar { margin-left: 0; } +.theme-toggle span { transition: opacity .25s ease, transform .35s cubic-bezier(.2,.8,.2,1); } .theme-icon-dark { display: none; } [data-theme="dark"] .theme-icon-light { display: none; } [data-theme="dark"] .theme-icon-dark { display: inline; } +.theme-changing .theme-toggle { transform: rotate(12deg) scale(.94); } + +html.theme-changing body, +html.theme-changing .topbar, +html.theme-changing .panel, +html.theme-changing .stat-card, +html.theme-changing input, +html.theme-changing select, +html.theme-changing textarea, +html.theme-changing .check-dropdown summary, +html.theme-changing .check-options, +html.theme-changing .search-results, +html.theme-changing .search-result, +html.theme-changing td, +html.theme-changing th, +html.theme-changing .pill, +html.theme-changing .item-symbol, +html.theme-changing .size-badge, +html.theme-changing .bulk-stock-picker, +html.theme-changing .login-panel, +html.theme-changing .login-card h2, +html.theme-changing .security-note, +html.theme-changing .button.subtle { transition: background-color .36s ease, color .36s ease, border-color .36s ease, box-shadow .36s ease; } + +::view-transition-old(root) { animation: theme-fade-out .22s ease both; } +::view-transition-new(root) { animation: theme-fade-in .34s ease both; } +@keyframes theme-fade-out { to { opacity: .72; } } +@keyframes theme-fade-in { from { opacity: .72; } to { opacity: 1; } } html[data-theme="dark"] { color-scheme: dark; --ink:#e7efeb; --muted:#97a7a0; --line:#293832; --paper:#0d1512; --card:#16211d; --green:#67bd98; --green2:#1d382e; --amber:#d8a457; --rose:#dd7d7d; --shadow:0 14px 38px rgba(0,0,0,.24); } html[data-theme="dark"] body { background: var(--paper); color: var(--ink); } @@ -69,3 +100,4 @@ html[data-theme="dark"] .security-note { border-color:#2c3a34; color:#83928b; } html[data-theme="dark"] .security-note strong { color:#aebdb6; } @media(max-width:760px){.topbar .theme-toggle{margin-left:auto}.login-theme-toggle{top:14px;right:14px}} +@media(prefers-reduced-motion:reduce){.theme-toggle,.theme-toggle span,html.theme-changing *{transition:none!important;animation:none!important}::view-transition-old(root),::view-transition-new(root){animation:none!important}} diff --git a/public/images/glitzpark_logo.jpg b/public/images/glitzpark_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5d37eb90d01181cab843025dcfdd1189b9df5a68 GIT binary patch literal 8519 zcmb_h1yodDx4$z(=MY18NeI&24I&NFNJxpmPzoX`DcvOvN=t`GcXtU0(%m8m@&^5X z-~apGdT+h;t@X~ibI-ne?{n_nXaCMUF}Jg~%K%haUP&GRfdBx6$iVG4FolwgjH$Y& zn!J*V+;0IG%iiL-3m6Ij&mCNyH5H_3^z;pAP!|yTqyPk90ffvfT%4rT)m4E1HGQAH zYaIY4*zel@{-#UNj`rkU*nlcETK?E&n{V%lnztDfZ7oh?`EV%EA|KH^L^d-W60Fajk zfV)2Z)B4|i^*0YfgfM{z0Kj>F^Bi*kK=2&FkN%ryo&o@MfdD`<|2K~z3jlE606^`C zg`2bcpLYOch;jhb2eCkoMSF(%9V!9SAm^n$W{Kt|5s8&Y)_7sO|IEn~IkY0xtHieD z^+d-u=k4w77eEGpfWdbcA|W9zWHe-CBqU@ER8$l+EDS6xObkp+Y#e+jHVz&RCMJ{! zibp^QgTb(HiAjhEN$?3_gm;aAAc#6h$mqz(=!Dpq*o6Oax@`yW(12c`7XqRKz<3}C z9_Y3cpg@2O3<2Gt`5y_43PM3ch9DuN53zc?SwG=>l}96&@{;uJpj#*xfehk*j2N8 zr|tTc_IsxIX10siro6%dHKw7O$;TZr`l&RHg)NiFRmW1Xy3~#Ct#QZ!CmtB#Lfgg( zUxF4~%y>FI$;Ruj*2AJ^SHDJ#V`=jsxJV2eJLb3!zYO=@)H*3md2F}U@!eaS z&DzZK`grvF16=Iu4+xtf)Dq>J?GD~&bDuJeW#>&$dc9&xF&Q(wg+Y==sLi9hRaxRS z$jz-!p^T&ev&cz4zd^v<^Rg`if-KyD;qNfO zz>vT5#uyJEkv#O=xc#pC%sp!MjYh_k}|7GRqKLh!?V z$xwcYN;pwHE@FEWXARh7CUyblMK!^J2flbH42a17PDoM+=-fx(0vQQ}z&!F_@Bt&B zj{<=3xM)zJ2!wOu)AAD$^YBSZA+Usoz!(S_jNLq2qRR#g>oTv<`|m`#<43n&ueOAL ztUt&YT~B8CVO9wzDmus*O=L8 z2|eH%ag%(9fA~gnCs~Dz!5zVrkCzRgDm?#GmiB%hf(Qd92CqgG~iW);6bftKkgutrT`9 zpR4KrA@y8(>2qI8#2F3z0Jj*X>gW`82CUDt{$l}vWp@;hzr&{L< z$(!@Q#W`{N6;8e4(*40%u7$Q25s#W^7)~aM=h~z-@w*vVW=LtCvh1U;VRKDJXoP6j z2oh}sd_JRm(VSmS?C#vPOI%`7;n}rKc5Z{Dy|VC$lJ5n7zjnsQ&K|Pp3)}v{P%ck| z&Ih)kv64eX$B0UmL!Wh$9iue;8`1qQ9Mr%wN+b&xut~dDsh3kSGg`y$#1Fjxh z_$Wmr&AxKhvdX$74@e|ljy*mkV~r96OHQ#T6A<7Y(y>OlQHlo6AnbG6+4XzFSIJZG zmg{R#1QTnx;@da@H|yibRBvj9TL4#4;?rV5h!zaCO{b~NpQw(G-WMv~7)qm>nx($0 zpmEV0P)4Z~nR+!qx}ym(Tt5lwVL000QkeCu`q9blKHf)Zo-CjkPkk*$^XuR}b*SsB ztNx7sLo$aq4Lmi)_L8iZ4IO! z7uZ|L4r92QIeN5g>0&70SvzViA>tvCXx_&vMKN~opkdLv0p%9Z`J}XG9vnS8Eky0! zAT=#H8@7f20%0X?%*W4SwG~;`b=GP+`+ZMrl%*&XOCgVKv3$N#sLM-tUoO@Xs#R8b zF6SUDGTm$r@Tq;qmpWusrC&1r--+U{`-suaj|YIj5M&4j66#-r9t=VPAjl|qv|Lhz z+#2`!d|1v!n-OHdeF^s0I zBU^P~OLN~6rs@3D_ec59L;SWyXXhOwSH-PRXCFgqW=;=Tmp1Q*FkACAlzc``u|x0y zDX&0Px8yUTbiC>m?EHiLV=G29&h-MzVq8|ud?#5m5bQlJm6@eYxb@BUdW@~ z+2r_X9^+WGf4RbHes3@OT-1DGqM^^`#K2tW@|D|%J^l+t2CI9!ma@_z=dg+}5W&p~g}B zd+^*1ux^P#A4FuB5s`sFz{p6DzXJ1nf=9x`C!i(dfs($Hy0J~8_p3-DbmuDP^4 zN>@{KIIuh2#N)?zc)WAt6~XI*jW1%sD^IU9lC)Y}1=a>tBB3?-{qAKV-jkTDWLb=P zore^mRP+vV8tLhqFx_vKm6r0VQxHnR*#zxo7S|gZaor1l=`TAS^aQLE+nsLSJy{H zspCJ6sF7xsmnzx0F3Q(WgdA`ZXWtGLuDK5$z!{EtDHLX9UthZVD4*ArMomV3*4ec< zU(LD6bR@a>HgUtsmK@n+O-;2E_KiMFk?Rs#7eGoEvL@Ml3jc6Of_@mixu<$StkYX$YndD9!ZhR(UGWQMXpPpVl=s`Dk16=* zdag2xldsrM^3o22!mpL5Z7ppL)6$=bYVtlB!Xd2vOf$PNT6GH?7IDM|s8_W0E-zp! z2n=KEqcVNp+jA}oS1RNXcXa1lt9gcYVL|d^Om7%p^%bWFjsyxFV!d^4@5Oo|E|y~U zjdv8ssV-CJX9KmveY)o!u6+wlEmZj(33{0?czhgkDryx(u&tV%b3`FB6(dos#EBY6 zc;O789xlaktpmCJWlA>_J`QEwGT}#d8ON=sSisVD0P`j^0`a{LSIaOIF7WQGjQZTk?WQy9Xzj7PAji$wUGLO&*JrR5z4&L1(&sP(5Z zO-0*AT$07Ipgfj|kNmJ1vch1^`Jv2W5ndIuz89kMs7bi+>)>>UA+|l=$LS9J(v4W^ zDm`K)@kuw|1dE?|(;jQFRvVjSN;!-SG9kPlztGlMnd)78A;U|9YRUMWGj+rdM5#}| z8|L=cDI_>Vu`W{SG>R6xdgyMQl#Z?ckL-YfInm1cNM6#Ev?qk-TV+wNK8juRsQr{< zOgK-Q8{TA1=n4{Bwb>z#l|5yNR!G2k#|JxuS3hJDF0Yg7_>du8P!~3>hh>{dOmp0x z(t!2s+vP3r?$6zhZEMvgR>e*4n=1wD>=0z+OCp3G6htM?cMJGQoR%tBP^OPo1qr1D zk)c*rRm=Dy;6|dC`LeUl-2gMiwCe->>_Ay=zsdUkwyF@ zbEBlm^p|zvufF2LPq)CA;(Su}Z@SQQG^xioc}h$K5$mXnB_FbhW5b!m6QrhJQ2OrSaN}w48CEP?#FTx zb9}Eae15vQmS3SX%G|(j0jjKz;f@?!c+Muz)yBoxcwKDPPfDATHnz?ZO6v4MSXgAcymZrV?d1Wrh_pNjg$K2tgo+%`uds@A zLs1!bxjFhGS*CUiEYWJY!00VnrhpId|%6{|(i#I8ca_ zd!#9t9L<`TlFJRw-See6WJctffG-&;nPRGkgOTGyuCC-;-~#vQ*r}0hoPGyI(N0fj z2k!o(E%%Bqn@g;`nLTBzqm@@0()7blB;$snGY$>h^~9>MhOHiKK3M50>PxN&BMO(S zMRDWTgij+FtNrqL*eLkRqITt9p_vLU$HPGGW4b0ZE{11 zgvvZYSf)yVrN}a5D{lA2OYfPx=@g2XPQgehe{X_803H{u291;%9O@U7eRRxO(KaUe zk1dP@Ev5~D{eP07i;~&5fNVc?Y@O|qyHI7vkH}uYEP;OD+J)Jhvl`UL1QsqAdnQKs z-mLcojQTA_=6F;cN7`#>CYOW`(h1R8`695}hlDKbY=)P+q6*puCHDf~+c@9<%9DZ?z2v%@>gDJ?5@hnJ}I#mW)L8oFr$uBGM9nvi9z1FuPMJrPrh*)%zqP z5n?7s8;!M$%{r2I@dVXrAlWGr2;v4;9a274oNdfdr>_yNlN3CWqSyElg-Xxf*ALYg z(cHVHPM5zNy;%#i(Yp*Fu~J`QOz=@YT8qMe=|6F-{fl!k8YkrwnDsTzhlrc$9XqmT zyAc>yZhF^&t4+gS~9F6dDs${pbMB82WZi~h|h-FgjYelKght^$8 z>BeWF4{0DQ_}PeJq3t9^GN&+279{W}d$n1C_wfj1HGtlG*pepd%Zu9HaI<#3@VfTVDL_uw5sUb99cgjA99~( z;0|6aM5v>0Dms#f*$KMhsd<*a{ct=f|ADee{AU)#V;R|B9HxhUPY~wHOfj@7v57fI ze(}Zsl1-l0CWwXe4fcpfF5sQPUI^nG5~vH)0uOe32JrG2s^z*fNuCXn@;V5~6S)Ok zecEZB2*p{F)=Gla4>K1rY$vj{?h`OOrIM@nf-&WB8CX5SQ8?(lOG2;jk*R~XU|QG2 z+|`RO)8P5XiN^1DLXc3ha^qD5BYT5h0*1_u9 zYHzr=aR;M=S)Z%~mf%$i1U-z-9VHS#1}EqwzIIAm!^1$aU%8-9KxO=9UjS^-W@+1~ z*C5fb+Rubn?n{_(9ZeruYBC3-#aUxJU|~L za}41Fz!7gbeptIuD-s2lQ-%Kr9cooo|28TAr#Sdrg-_O(9fr`VkgPOH5p0*DX{`83uWWH5nrdg8WB%!FzQ=`8GdOEIU2R&b$%66E`a zhiiC`fZ1n^8)8Sm@t-hVy5OMSk08q{jEY4Oe#GPkE^mM@11#!Q-2qbQS zKNP=ALhNsr;O=naA8`tM#hV2$1aoz#+PC*PPe+(x z6tdC!>IiYR0`~DEw5k!&lOEBkcF|llQQv>;hfWcnbrw9tdDGp2ECl&4HV6+uY+W_X z;C?YQoY|6XE9w9>G&-Zpj)e@#o60yeoN0`iRvsiAWhOzw4!KV!6a1a_ z$mC8ZM}jFt2d~|;?RwxG)GqLbQ}u;vY^?9j-mk-li$9Tl4ihmS-3Ey+o+{uheHB z$T&%V2r-*Hm9nM7PN?X!6G3LBoUhkIIMHU7;`;6L^9KH`LUNS)$*2xzlbv`B^&9UQ zHtO__gX6|o!ro2w871^do0TcUN?A9@HHJtF!g;k6w*Rp~MFN45|Jn114JzW)K!a0? z#tfdlf|yOAlIk&k4h!x!sL-E6U8}=+4M(r(-rgIInUWGCpJg_khT0O1$VyV^hK4kV z`oqyk7UG8$OfmT=+gY_#EhNir@qcNct6M#i9=X;ym!&rt)j^H)jM5}{BgL(I5bp0u zQQmJGXC`(=5R)}yaxaG+q*TLFSk>qswc8K6KoXaK!Hlm8ZENu+a_vA5BEVuIQQ=HB z<7~IOZuz|^K%9Jo{&iLG+l6cde2$?saKEu5&X~4}m47Y@pvq2>NPC_CN#3$uc1x2v zVV9~p-fOlRy?!S9L^|-ve5rz%Haux-$S`E_!96Lg%(qWZ`GZ#-hs!~BmC?}E z!^+1ilgShPmfeU6iPy#6LbA0RVbSbP^+&!R5*Gso{~BlHTYi^Fvt3T&h78>R%mm@K zgP_e9lFZx2)Z4J6-%Sly-!HzOKe|sQOp#54^)^StZAh<)8KSHH%1OywFbf0Uw?=n~ zKkT&@_2_KWA~dio$4IM-9aPe=$$Ube##BkW3YredIi;B8FLPSFXjc_GbQEPm}#WqWbVaCO*H zptZ0Cm)$a+%`slBuF+Znn(X+fItTrNwMHd+mk}}YPx@u7d&;5R>BLvbNjALaGd6XG zaNn`@Ij^DYjgR$s`CDQ-s3qF|X&(`P!-&!8RV&9kMY#thy=Y?HiixT7Ch`o0)?sBc zhqmDt=}welC`dihotG`?er(%1_lb18I}%CE2&tVZG{%5VSQWFHW?tATz!w$Gp#Zur z>)rK(y;f$5L}-~(*28}&sM<5bIB9U6%0Z_%H%LP971cdFS7jRGW#CLG%UD$@ZLiKF z%UXu=WjL$55adno3>PBWDjL}V@l-0sc>BfX* zE>z-vm*>HS@8$YMcruypu*($9Ytag^TVr?K34JnDogiH7EhWb`M}n&@;Fio#Fgzn?Rw#LP}+$G=O=Pa23;oA~nah@|rlei7yq7O2WSQyjS|7)4pA zxw9VaL#A1-X|RmA2x9z*CJi5pN361bUXkhD6zP5uen`5+T^>i8r1zEYKshE~tFCBy ze%(99k2?yMkrXhWa}KG+SJ~jngeVY+@U$qvSQAz*JXNde9!X#oAMC@# zm1#S)B0COJJf{CxPi-ZO)?G6n^BT=2N!mV}e-xCA$-iJd{b6cVaq}ydpYm*yzN8>1 z&~A&5&ZRZc*XBXb-AGGfnsg{?vcr6<#ezAlP?6CQNL9975ZIMHPXc|1E!|&JF(}1H zNZu**auYcw*^)MIfu11X0^mWzsCMJu7N1B(YC{^wq|T3KYc!%JyKVA1EQU7fXU-u?%J`1^auo(3Qrh=HQ7$)$*SMx(UB2AVY_qRVi!oo>9RSQ&*1!8If#JT% z(5QOeaO ZWTjd literal 0 HcmV?d00001 diff --git a/public/js/theme.js b/public/js/theme.js index ca473d5..9c4fb15 100644 --- a/public/js/theme.js +++ b/public/js/theme.js @@ -18,7 +18,23 @@ document.addEventListener('DOMContentLoaded', () => { applyTheme(root.dataset.theme || preferredTheme()); document.querySelectorAll('[data-theme-toggle]').forEach(button => button.addEventListener('click', () => { - applyTheme(root.dataset.theme === 'dark' ? 'light' : 'dark'); + const nextTheme = root.dataset.theme === 'dark' ? 'light' : 'dark'; + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + if (reducedMotion) { + applyTheme(nextTheme); + return; + } + + root.classList.add('theme-changing'); + if (document.startViewTransition) { + document.startViewTransition(() => applyTheme(nextTheme)).finished.finally(() => { + window.setTimeout(() => root.classList.remove('theme-changing'), 120); + }); + } else { + requestAnimationFrame(() => applyTheme(nextTheme)); + window.setTimeout(() => root.classList.remove('theme-changing'), 420); + } })); }); })(); diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index fe91cd2..bb10f7a 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -5,6 +5,8 @@ Admin Login - UniformFlow + + @@ -16,7 +18,7 @@