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 @@
☾ ☀
- UF UniformFlowUniform asset management
+ UniformFlowUniform asset management
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') }}
+
+ Date & time Movement Uniform Size Category Quantity Unit value Employee / reference
+ @forelse($monthlyMovements as $movement){{ $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' }}
+ @emptyNo stock movements have been recorded this month. @endforelse
+
+
+
+
@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.' }}
@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 Date Employee Items accounted Outcome Status
-@forelse($handovers as $handover){{ $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) }} @endforeach Completed @emptyCompleted employee handovers will appear here. @endforelse
+@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 @endforelse Completed @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
Uniform Code Size New Returned Available Unit value Total value Status
-@forelse($items as $item)@foreach($item->variants as $variant){{ $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') }} @endforeach @emptyAdd your first uniform type above. @endforelse
+@forelse($items as $item)@foreach($item->variants as $variant){{ $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') }} @endforeach @emptyAdd your first uniform type above. @endforelse
@yield('title', 'UniformFlow')
+
+
@@ -16,12 +18,13 @@
+