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 0000000..5d37eb9 Binary files /dev/null and b/public/images/glitzpark_logo.jpg differ 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 @@
- +
CONTROL. ACCOUNTABILITY. CLARITY.

Every uniform,
accounted for.

diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 14e1382..2e3bf53 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -4,7 +4,7 @@ @section('content')
- {{ now()->format('l, d F Y') }} +
{{ now()->timezone('Asia/Colombo')->format('l, d F Y') }}--:--:--SLST

Operations at a glance.

Stock health, employee accountability, and handover performance in one place.

@@ -29,8 +29,8 @@
Healthy sizes At or below reorder level
Uniform types{{ $uniformTypes }}
Low stock{{ $lowStock->count() }}
Out of stock{{ $outOfStock->count() }}
-
-
This month

Movement summary

+
+
This month

Movement summary

View all →
Uniforms issued{{ $issuedThisMonth }}
Exit handovers completed{{ $handoversThisMonth }}
Reusable returns{{ $handoverOutcomes['reusable'] ?? 0 }}
@@ -90,5 +90,32 @@
- + +
{{ now()->timezone('Asia/Colombo')->format('F Y') }}

Detailed monthly movement summary

Every inventory movement recorded during the current month.

+
+
Movement records{{ $monthlyMovements->count() }}
+
Units received / returned+{{ $monthlyMovements->where('quantity_change', '>', 0)->sum('quantity_change') }}
+
Units issued{{ $monthlyMovements->where('quantity_change', '<', 0)->sum('quantity_change') }}
+
Net stock movement{{ $monthlyMovements->sum('quantity_change') > 0 ? '+' : '' }}{{ $monthlyMovements->sum('quantity_change') }}
+
+
+ @forelse($monthlyMovements as $movement) + @empty@endforelse +
Date & timeMovementUniformSizeCategoryQuantityUnit valueEmployee / reference
{{ $movement->created_at->timezone('Asia/Colombo')->format('d M Y') }}{{ $movement->created_at->timezone('Asia/Colombo')->format('h:i:s A') }}{{ ucfirst(str_replace('_', ' ', $movement->type)) }}{{ $movement->variant->item->name }}{{ $movement->variant->size }}{{ $movement->stock_category ? ucfirst($movement->stock_category) : '—' }}{{ $movement->quantity_change > 0 ? '+' : '' }}{{ $movement->quantity_change }}{{ $movement->unit_value !== null ? number_format($movement->unit_value, 2) : '—' }}{{ $movement->employee?->name ?: 'Stock operation' }}{{ $movement->notes ?: 'No reference' }}
No stock movements have been recorded this month.
+
+
+ @endsection diff --git a/resources/views/handovers/create.blade.php b/resources/views/handovers/create.blade.php index 357fbd7..ac938b5 100644 --- a/resources/views/handovers/create.blade.php +++ b/resources/views/handovers/create.blade.php @@ -1,12 +1,12 @@ @extends('layouts.app') -@section('title', 'Handover '.$employee->name.' · UniformFlow') +@section('title', 'Handover '.$employee->name.' - UniformFlow') @section('page-heading', 'Complete handover') @section('content') -
← Exit handovers{{ $employee->employee_no }} · {{ $employee->department }}

Clear {{ $employee->name }}.

Choose the final outcome for every outstanding issue.

+
← Exit handovers{{ $employee->employee_no }} / {{ $employee->department }}

Clear {{ $employee->name }}.

{{ $employee->issues->count() ? 'Choose the final outcome for every outstanding issue.' : 'No uniforms are outstanding. Complete a clear exit handover below.' }}

@csrf -
Outstanding property

{{ $employee->issues->sum(fn($i) => $i->outstanding_quantity) }} units to account for

-@forelse($employee->issues as $index => $issue)
{{ strtoupper(substr($issue->variant->item->name,0,2)) }}
{{ $issue->variant->item->name }}Size {{ $issue->variant->size }} · {{ $issue->outstanding_quantity }} unit(s) · Issued {{ $issue->issued_at->format('d M Y') }}
@empty
There are no outstanding uniforms for this employee.
@endforelse +
Outstanding property

{{ $employee->issues->sum(fn($issue) => $issue->outstanding_quantity) }} units to account for

+@forelse($employee->issues as $index => $issue)
{{ strtoupper(substr($issue->variant->item->name,0,2)) }}
{{ $issue->variant->item->name }}Size {{ $issue->variant->size }} / {{ $issue->outstanding_quantity }} unit(s) / Issued {{ $issue->issued_at->format('d M Y') }}
@empty
No outstanding uniformsThis employee has no uniform property requiring an outcome.
@endforelse
-@if($employee->issues->count())
This completes employee clearance.The employee will be marked as left. Reusable items will return to stock.
@endif +
This completes employee clearance.The employee will be marked as left. Reusable items will enter returned stock.
@endsection diff --git a/resources/views/handovers/index.blade.php b/resources/views/handovers/index.blade.php index b2f9114..94d5bb2 100644 --- a/resources/views/handovers/index.blade.php +++ b/resources/views/handovers/index.blade.php @@ -1,9 +1,9 @@ @extends('layouts.app') -@section('title', 'Exit handovers · UniformFlow') +@section('title', 'Exit handovers - UniformFlow') @section('page-heading', 'Exit handovers') @section('content') -
Employee clearance

Uniform handover.

Account for issued uniforms and close an employee’s record safely.

-
Start clearance

Select a departing employee

Only active employees with outstanding uniform issues are shown.

+
Employee clearance

Uniform handover.

Account for issued uniforms and close an employee record safely.

+
Start clearance

Select a departing employee

All active employees are available. Employees with no issued uniforms can complete a clear handover.

Clearance log

Completed handovers

-@forelse($handovers as $handover)@empty@endforelse
DateEmployeeItems accountedOutcomeStatus
{{ $handover->handover_date->format('d M Y') }}{{ $handover->employee->name }}{{ $handover->employee->employee_no }}{{ $handover->items->sum('quantity') }} units@foreach($handover->items->groupBy('condition') as $condition => $items){{ $items->sum('quantity') }} {{ str_replace('_',' ',$condition) }} @endforeachCompleted
Completed employee handovers will appear here.
+@forelse($handovers as $handover){{ $handover->handover_date->format('d M Y') }}{{ $handover->employee->name }}{{ $handover->employee->employee_no }}{{ $handover->items->sum('quantity') }} units@forelse($handover->items->groupBy('condition') as $condition => $items){{ $items->sum('quantity') }} {{ str_replace('_',' ',$condition) }} @emptyNothing outstanding@endforelseCompleted@emptyCompleted employee handovers will appear here.@endforelse
@endsection diff --git a/resources/views/inventory/index.blade.php b/resources/views/inventory/index.blade.php index 7a93734..6439cc1 100644 --- a/resources/views/inventory/index.blade.php +++ b/resources/views/inventory/index.blade.php @@ -41,7 +41,7 @@
Live balances

Stock by size

{{ $items->sum(fn($i) => $i->variants->count()) }} size records
-@forelse($items as $item)@foreach($item->variants as $variant)@endforeach @empty@endforelse +@forelse($items as $item)@foreach($item->variants as $variant)@endforeach @empty@endforelse
UniformCodeSizeNewReturnedAvailableUnit valueTotal valueStatus
{{ $item->name }}{{ $item->code }}{{ $variant->size }}{{ $variant->new_quantity }}{{ $variant->returned_quantity }}{{ $variant->quantity }}{{ number_format($variant->unit_value, 2) }}{{ number_format($variant->quantity * $variant->unit_value, 2) }}{{ $variant->quantity === 0 ? 'Out of stock' : ($variant->quantity <= $variant->reorder_level ? 'Low stock' : 'Healthy') }}
Add your first uniform type above.
{{ $item->name }}{{ $item->code }}{{ $variant->size }}{{ $variant->new_quantity }}new units{{ $variant->returned_quantity }}returned{{ $variant->quantity }}{{ number_format($variant->unit_value, 2) }}{{ number_format($variant->quantity * $variant->unit_value, 2) }}{{ $variant->quantity === 0 ? 'Out of stock' : ($variant->quantity <= $variant->reorder_level ? 'Low stock' : 'Healthy') }}
Add your first uniform type above.
@yield('title', 'UniformFlow') + + @@ -16,12 +18,13 @@ +