sashank1989 commited on
Commit
2bed403
·
1 Parent(s): d1b20be

feat: make all pages mobile-responsive

Browse files

- Global CSS: touch targets (44px buttons), responsive table padding, .table-scroll utility, .mobile-overlay class
- Sidebar: mobile drawer with overlay backdrop, mobileOpen/onclose props, tap-to-close nav items
- Layout: mobileOpen state, toggleSidebar() with viewport detection, dual sidebar rendering (desktop wrapper + mobile drawer)
- Header: larger hamburger touch target (p-2.5), responsive padding
- Modal: larger close button touch target (p-2)
- POS: mobile cart FAB with item count + total, full-screen cart overlay, responsive product grid (2/3 cols)
- Captain: mobile cart FAB + overlay, responsive table/product grids, bigger cart buttons (w-8/w-9), responsive modals (max-w-[480px]/[400px])
- Tables: responsive stat grid (2/4 cols), responsive table grid (2-8 cols across breakpoints)
- KDS: stacked columns on mobile (grid-cols-1 md:grid-cols-3), flex-wrap filter bar, independent scroll per column on desktop
- Reports: all 10 tables in table-scroll wrappers, flex-wrap date range, responsive stat grids
- Admin: dual sidebar (mobile horizontal tabs + desktop sidebar), table-scroll on users table, responsive form grids
- Dashboard: overflow-x-auto on hourly chart

frontend/src/app.css CHANGED
@@ -40,7 +40,7 @@
40
 
41
  @layer components {
42
  .btn {
43
- @apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white disabled:opacity-50 disabled:cursor-not-allowed;
44
  }
45
 
46
  .btn-primary {
@@ -68,7 +68,7 @@
68
  }
69
 
70
  .btn-sm {
71
- @apply px-3 py-1.5 text-xs;
72
  }
73
 
74
  .btn-lg {
@@ -116,11 +116,11 @@
116
  }
117
 
118
  .table-pos th {
119
- @apply px-4 py-3 font-medium;
120
  }
121
 
122
  .table-pos td {
123
- @apply px-4 py-3 border-b border-gray-100;
124
  }
125
 
126
  .table-pos tbody tr {
@@ -132,7 +132,7 @@
132
  }
133
 
134
  .stat-value {
135
- @apply text-2xl font-bold;
136
  }
137
 
138
  .stat-label {
@@ -178,4 +178,15 @@
178
  .table-cell.cleaning {
179
  @apply bg-gray-50 border-2 border-gray-300 hover:border-gray-500 text-gray-500;
180
  }
 
 
 
 
 
 
 
 
 
 
 
181
  }
 
40
 
41
  @layer components {
42
  .btn {
43
+ @apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px];
44
  }
45
 
46
  .btn-primary {
 
68
  }
69
 
70
  .btn-sm {
71
+ @apply px-3 py-2 text-xs min-h-[36px];
72
  }
73
 
74
  .btn-lg {
 
116
  }
117
 
118
  .table-pos th {
119
+ @apply px-3 py-3 font-medium lg:px-4;
120
  }
121
 
122
  .table-pos td {
123
+ @apply px-3 py-3 border-b border-gray-100 lg:px-4;
124
  }
125
 
126
  .table-pos tbody tr {
 
132
  }
133
 
134
  .stat-value {
135
+ @apply text-xl font-bold lg:text-2xl;
136
  }
137
 
138
  .stat-label {
 
178
  .table-cell.cleaning {
179
  @apply bg-gray-50 border-2 border-gray-300 hover:border-gray-500 text-gray-500;
180
  }
181
+
182
+ /* Mobile-friendly scrollable table wrapper */
183
+ .table-scroll {
184
+ @apply overflow-x-auto -mx-4 px-4;
185
+ -webkit-overflow-scrolling: touch;
186
+ }
187
+
188
+ /* Mobile overlay backdrop */
189
+ .mobile-overlay {
190
+ @apply fixed inset-0 bg-black/40 z-40 lg:hidden;
191
+ }
192
  }
frontend/src/lib/components/Header.svelte CHANGED
@@ -21,18 +21,18 @@
21
  });
22
  </script>
23
 
24
- <header class="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-4 sticky top-0 z-30">
25
- <div class="flex items-center gap-3">
26
  <button
27
  onclick={oncollapse}
28
- class="text-gray-500 hover:text-gray-800 transition-colors p-1.5 rounded-lg hover:bg-gray-100"
29
  >
30
  <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
31
  <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
32
  </svg>
33
  </button>
34
  {#if title}
35
- <h1 class="text-lg font-semibold text-gray-800">{title}</h1>
36
  {/if}
37
  </div>
38
 
 
21
  });
22
  </script>
23
 
24
+ <header class="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-3 lg:px-4 sticky top-0 z-30">
25
+ <div class="flex items-center gap-2">
26
  <button
27
  onclick={oncollapse}
28
+ class="text-gray-500 hover:text-gray-800 transition-colors p-2.5 rounded-lg hover:bg-gray-100 -ml-1"
29
  >
30
  <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
31
  <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
32
  </svg>
33
  </button>
34
  {#if title}
35
+ <h1 class="text-lg font-semibold text-gray-800 truncate">{title}</h1>
36
  {/if}
37
  </div>
38
 
frontend/src/lib/components/Modal.svelte CHANGED
@@ -39,10 +39,10 @@
39
  {#if title}
40
  <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">
41
  <h2 class="text-lg font-semibold text-gray-800">{title}</h2>
42
- <button
43
- onclick={onclose}
44
- class="text-gray-400 hover:text-gray-700 transition-colors p-1 rounded-lg hover:bg-gray-100"
45
- >
46
  <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
47
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
48
  </svg>
 
39
  {#if title}
40
  <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">
41
  <h2 class="text-lg font-semibold text-gray-800">{title}</h2>
42
+ <button
43
+ onclick={onclose}
44
+ class="text-gray-400 hover:text-gray-700 transition-colors p-2 rounded-lg hover:bg-gray-100"
45
+ >
46
  <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
47
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
48
  </svg>
frontend/src/lib/components/Sidebar.svelte CHANGED
@@ -3,7 +3,7 @@
3
  import { authStore, currentUser, userRole } from '$stores/auth.js';
4
  import { hasRole } from '$utils/helpers.js';
5
 
6
- let { collapsed = false } = $props();
7
  let user = null;
8
  let role = '';
9
 
@@ -30,19 +30,42 @@
30
  return item.roles.some((r) => r === role);
31
  });
32
  }
 
 
 
 
 
33
  </script>
34
 
 
 
 
 
 
 
 
35
  <aside
36
- class="h-screen bg-white border-r border-gray-200 flex flex-col transition-all duration-300 {collapsed ? 'w-16' : 'w-56'}"
 
 
 
 
37
  >
38
  <!-- Logo -->
39
  <div class="px-4 py-5 border-b border-gray-200 flex items-center gap-3">
40
  <div class="w-8 h-8 rounded-lg bg-pos-highlight flex items-center justify-center flex-shrink-0">
41
  <span class="text-white font-bold text-sm">H</span>
42
  </div>
43
- {#if !collapsed}
44
- <span class="font-bold text-base text-gray-800 truncate">HSS POS</span>
45
- {/if}
 
 
 
 
 
 
 
46
  </div>
47
 
48
  <!-- Navigation -->
@@ -53,16 +76,15 @@
53
  {:else}
54
  <a
55
  href={item.href}
56
- class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors mb-0.5
57
  {currentPath.startsWith(item.href) ? 'bg-red-50 text-pos-highlight font-medium' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}"
58
  title={collapsed ? item.label : ''}
 
59
  >
60
  <svg class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
61
  <path stroke-linecap="round" stroke-linejoin="round" d={item.icon} />
62
  </svg>
63
- {#if !collapsed}
64
- <span class="truncate">{item.label}</span>
65
- {/if}
66
  </a>
67
  {/if}
68
  {/each}
@@ -70,35 +92,23 @@
70
 
71
  <!-- User info -->
72
  <div class="border-t border-gray-200 p-3">
73
- {#if !collapsed}
74
- <div class="flex items-center gap-3">
75
- <div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
76
- <span class="text-xs font-medium text-gray-600">{user?.full_name?.[0] || '?'}</span>
77
- </div>
78
- <div class="flex-1 min-w-0">
79
- <p class="text-sm font-medium text-gray-800 truncate">{user?.full_name || 'User'}</p>
80
- <p class="text-xs text-gray-500 capitalize">{role}</p>
81
- </div>
82
- <button
83
- onclick={() => authStore.logout()}
84
- class="text-gray-400 hover:text-pos-danger transition-colors"
85
- title="Logout"
86
- >
87
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
88
- <path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
89
- </svg>
90
- </button>
91
  </div>
92
- {:else}
93
  <button
94
  onclick={() => authStore.logout()}
95
- class="w-full flex justify-center text-gray-400 hover:text-pos-danger transition-colors"
96
  title="Logout"
97
  >
98
  <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
99
  <path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
100
  </svg>
101
  </button>
102
- {/if}
103
  </div>
104
  </aside>
 
3
  import { authStore, currentUser, userRole } from '$stores/auth.js';
4
  import { hasRole } from '$utils/helpers.js';
5
 
6
+ let { collapsed = false, mobileOpen = false, onclose = () => {} } = $props();
7
  let user = null;
8
  let role = '';
9
 
 
30
  return item.roles.some((r) => r === role);
31
  });
32
  }
33
+
34
+ function handleNavClick() {
35
+ // Close drawer on mobile after navigation
36
+ if (mobileOpen) onclose();
37
+ }
38
  </script>
39
 
40
+ <!-- Mobile backdrop overlay -->
41
+ {#if mobileOpen}
42
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
43
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
44
+ <div class="mobile-overlay" onclick={onclose}></div>
45
+ {/if}
46
+
47
  <aside
48
+ class="h-screen bg-white border-r border-gray-200 flex flex-col transition-all duration-300 flex-shrink-0
49
+ {collapsed ? 'w-16' : 'w-56'}
50
+ max-lg:fixed max-lg:inset-y-0 max-lg:left-0 max-lg:z-50 max-lg:w-64 max-lg:shadow-xl
51
+ {mobileOpen ? 'max-lg:translate-x-0' : 'max-lg:-translate-x-full'}
52
+ "
53
  >
54
  <!-- Logo -->
55
  <div class="px-4 py-5 border-b border-gray-200 flex items-center gap-3">
56
  <div class="w-8 h-8 rounded-lg bg-pos-highlight flex items-center justify-center flex-shrink-0">
57
  <span class="text-white font-bold text-sm">H</span>
58
  </div>
59
+ <span class="font-bold text-base text-gray-800 truncate lg:{collapsed ? 'hidden' : 'block'}">HSS POS</span>
60
+ <!-- Mobile close button -->
61
+ <button
62
+ class="ml-auto p-2 rounded-lg hover:bg-gray-100 text-gray-400 lg:hidden"
63
+ onclick={onclose}
64
+ >
65
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
66
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
67
+ </svg>
68
+ </button>
69
  </div>
70
 
71
  <!-- Navigation -->
 
76
  {:else}
77
  <a
78
  href={item.href}
79
+ class="flex items-center gap-3 px-3 py-3 rounded-lg text-sm transition-colors mb-0.5
80
  {currentPath.startsWith(item.href) ? 'bg-red-50 text-pos-highlight font-medium' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'}"
81
  title={collapsed ? item.label : ''}
82
+ onclick={handleNavClick}
83
  >
84
  <svg class="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
85
  <path stroke-linecap="round" stroke-linejoin="round" d={item.icon} />
86
  </svg>
87
+ <span class="truncate max-lg:block {collapsed ? 'lg:hidden' : ''}">{item.label}</span>
 
 
88
  </a>
89
  {/if}
90
  {/each}
 
92
 
93
  <!-- User info -->
94
  <div class="border-t border-gray-200 p-3">
95
+ <div class="flex items-center gap-3">
96
+ <div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center flex-shrink-0">
97
+ <span class="text-xs font-medium text-gray-600">{user?.full_name?.[0] || '?'}</span>
98
+ </div>
99
+ <div class="flex-1 min-w-0 max-lg:block {collapsed ? 'lg:hidden' : ''}">
100
+ <p class="text-sm font-medium text-gray-800 truncate">{user?.full_name || 'User'}</p>
101
+ <p class="text-xs text-gray-500 capitalize">{role}</p>
 
 
 
 
 
 
 
 
 
 
 
102
  </div>
 
103
  <button
104
  onclick={() => authStore.logout()}
105
+ class="p-2 text-gray-400 hover:text-pos-danger transition-colors rounded-lg hover:bg-gray-100 flex-shrink-0"
106
  title="Logout"
107
  >
108
  <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
109
  <path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
110
  </svg>
111
  </button>
112
+ </div>
113
  </div>
114
  </aside>
frontend/src/routes/(app)/+layout.svelte CHANGED
@@ -8,6 +8,7 @@
8
 
9
  let { children } = $props();
10
  let collapsed = $state(false);
 
11
  let ready = $state(false);
12
 
13
  onMount(() => {
@@ -39,15 +40,33 @@
39
  wsStore.disconnect();
40
  };
41
  });
 
 
 
 
 
 
 
 
 
 
42
  </script>
43
 
44
  {#if ready}
45
  <div class="flex h-screen overflow-hidden">
46
- <Sidebar {collapsed} />
47
- <div class="flex-1 flex flex-col overflow-hidden">
48
- <Header
49
- oncollapse={() => (collapsed = !collapsed)}
50
- />
 
 
 
 
 
 
 
 
51
  <main class="flex-1 overflow-auto bg-pos-bg">
52
  {@render children?.()}
53
  </main>
 
8
 
9
  let { children } = $props();
10
  let collapsed = $state(false);
11
+ let mobileOpen = $state(false);
12
  let ready = $state(false);
13
 
14
  onMount(() => {
 
40
  wsStore.disconnect();
41
  };
42
  });
43
+
44
+ function toggleSidebar() {
45
+ // On mobile (< lg), toggle the mobile drawer
46
+ // On desktop (>= lg), toggle collapsed state
47
+ if (window.innerWidth < 1024) {
48
+ mobileOpen = !mobileOpen;
49
+ } else {
50
+ collapsed = !collapsed;
51
+ }
52
+ }
53
  </script>
54
 
55
  {#if ready}
56
  <div class="flex h-screen overflow-hidden">
57
+ <!-- Desktop: inline sidebar. Mobile: drawer overlay (handled inside Sidebar) -->
58
+ <div class="hidden lg:block flex-shrink-0">
59
+ <Sidebar {collapsed} />
60
+ </div>
61
+ <!-- Mobile sidebar drawer -->
62
+ <Sidebar
63
+ {collapsed}
64
+ {mobileOpen}
65
+ onclose={() => (mobileOpen = false)}
66
+ />
67
+
68
+ <div class="flex-1 flex flex-col overflow-hidden min-w-0">
69
+ <Header oncollapse={toggleSidebar} />
70
  <main class="flex-1 overflow-auto bg-pos-bg">
71
  {@render children?.()}
72
  </main>
frontend/src/routes/(app)/admin/+page.svelte CHANGED
@@ -132,9 +132,23 @@
132
 
133
  <svelte:head><title>Settings - HSS POS</title></svelte:head>
134
 
135
- <div class="flex h-full">
136
- <!-- Settings sidebar -->
137
- <div class="w-48 bg-white border-r border-gray-200 py-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  <nav class="space-y-1 px-2">
139
  {#each [
140
  { id: 'users', label: 'Users & Roles' },
@@ -160,6 +174,7 @@
160
  {#if loading}
161
  <Spinner loading={true} />
162
  {:else}
 
163
  <table class="table-pos">
164
  <thead>
165
  <tr><th>Name</th><th>Username</th><th>Role</th><th>PIN</th><th>Phone</th><th>Actions</th></tr>
@@ -184,6 +199,7 @@
184
  {/each}
185
  </tbody>
186
  </table>
 
187
  {/if}
188
 
189
  {:else if activeTab === 'profile'}
@@ -216,7 +232,7 @@
216
  <!-- User Modal -->
217
  <Modal show={showUserModal} title={editingUser ? 'Edit User' : 'Add User'} onclose={() => (showUserModal = false)} size="md">
218
  <form onsubmit={(e) => { e.preventDefault(); saveUser(); }} class="space-y-4">
219
- <div class="grid grid-cols-2 gap-4">
220
  <div>
221
  <label class="label">Full Name</label>
222
  <input class="input" bind:value={userForm.full_name} placeholder="John Doe" required />
@@ -226,7 +242,7 @@
226
  <input class="input" bind:value={userForm.username} placeholder="johndoe" required />
227
  </div>
228
  </div>
229
- <div class="grid grid-cols-2 gap-4">
230
  <div>
231
  <label class="label">{editingUser ? 'New Password (leave blank to keep)' : 'Password'}</label>
232
  <input type="password" class="input" bind:value={userForm.password} placeholder="Min 6 chars" />
@@ -245,7 +261,7 @@
245
  {/each}
246
  </select>
247
  </div>
248
- <div class="grid grid-cols-2 gap-4">
249
  <div>
250
  <label class="label">Phone</label>
251
  <input class="input" bind:value={userForm.phone} placeholder="9876543210" />
 
132
 
133
  <svelte:head><title>Settings - HSS POS</title></svelte:head>
134
 
135
+ <div class="flex flex-col lg:flex-row h-full">
136
+ <!-- Mobile tab bar -->
137
+ <div class="lg:hidden flex gap-2 px-3 py-2.5 border-b border-gray-200 overflow-x-auto flex-shrink-0 scrollbar-hide">
138
+ {#each [
139
+ { id: 'users', label: 'Users & Roles' },
140
+ { id: 'profile', label: 'My Profile' },
141
+ { id: 'restaurant', label: 'Restaurant Info' },
142
+ ] as tab}
143
+ <button
144
+ class="px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap transition-colors {activeTab === tab.id ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
145
+ onclick={() => (activeTab = tab.id)}
146
+ >{tab.label}</button>
147
+ {/each}
148
+ </div>
149
+
150
+ <!-- Desktop sidebar -->
151
+ <div class="hidden lg:block w-48 bg-white border-r border-gray-200 py-4 flex-shrink-0">
152
  <nav class="space-y-1 px-2">
153
  {#each [
154
  { id: 'users', label: 'Users & Roles' },
 
174
  {#if loading}
175
  <Spinner loading={true} />
176
  {:else}
177
+ <div class="table-scroll">
178
  <table class="table-pos">
179
  <thead>
180
  <tr><th>Name</th><th>Username</th><th>Role</th><th>PIN</th><th>Phone</th><th>Actions</th></tr>
 
199
  {/each}
200
  </tbody>
201
  </table>
202
+ </div>
203
  {/if}
204
 
205
  {:else if activeTab === 'profile'}
 
232
  <!-- User Modal -->
233
  <Modal show={showUserModal} title={editingUser ? 'Edit User' : 'Add User'} onclose={() => (showUserModal = false)} size="md">
234
  <form onsubmit={(e) => { e.preventDefault(); saveUser(); }} class="space-y-4">
235
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
236
  <div>
237
  <label class="label">Full Name</label>
238
  <input class="input" bind:value={userForm.full_name} placeholder="John Doe" required />
 
242
  <input class="input" bind:value={userForm.username} placeholder="johndoe" required />
243
  </div>
244
  </div>
245
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
246
  <div>
247
  <label class="label">{editingUser ? 'New Password (leave blank to keep)' : 'Password'}</label>
248
  <input type="password" class="input" bind:value={userForm.password} placeholder="Min 6 chars" />
 
261
  {/each}
262
  </select>
263
  </div>
264
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
265
  <div>
266
  <label class="label">Phone</label>
267
  <input class="input" bind:value={userForm.phone} placeholder="9876543210" />
frontend/src/routes/(app)/captain/+page.svelte CHANGED
@@ -33,6 +33,7 @@
33
  cartItems.subscribe((v) => (items = v));
34
  cartTotal.subscribe((v) => (total = v));
35
  let submitting = $state(false);
 
36
 
37
  // ── Bill / Payment ──
38
  let showPaymentModal = $state(false);
@@ -330,7 +331,7 @@
330
  <div class="px-4 py-3 border-b border-gray-200 flex items-center gap-3">
331
  {#if view !== 'tables'}
332
  <button
333
- class="p-1.5 rounded-lg hover:bg-gray-100 text-gray-500 hover:text-gray-800 transition-colors"
334
  onclick={() => {
335
  if (view === 'menu' && selectedTable?.status === 'occupied') backToTableDetail();
336
  else if (view === 'menu') backToTables();
@@ -373,15 +374,15 @@
373
  <!-- ═══════════════════════════════════════════════════════ -->
374
  {:else if view === 'tables'}
375
  <div class="flex-1 overflow-y-auto p-4">
376
- <div class="flex items-center justify-between mb-4">
377
  <h2 class="text-lg font-semibold text-gray-800">Select a Table</h2>
378
- <div class="flex gap-3 text-xs text-gray-600">
379
  <span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm bg-green-100 border border-green-400"></span> Available</span>
380
  <span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm bg-orange-100 border border-orange-400"></span> Occupied</span>
381
  <span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm bg-amber-100 border border-amber-400"></span> Cleaning</span>
382
  </div>
383
  </div>
384
- <div class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3">
385
  {#each tableList as table}
386
  {@const info = activeOrdersMap[table.id]}
387
  <button
@@ -470,7 +471,7 @@
470
  </span>
471
  <span class="text-sm font-medium w-20 text-right text-gray-700">{formatCurrency(item.total_price)}</span>
472
  <button
473
- class="p-1 rounded-lg hover:bg-red-50 text-gray-400 hover:text-red-500 transition-colors"
474
  title="Remove item"
475
  onclick={() => promptCancelItem(item._order, item)}
476
  >
@@ -529,18 +530,18 @@
529
  </div>
530
  <div class="flex gap-2 px-3 py-2 overflow-x-auto border-b border-gray-200 scrollbar-hide">
531
  <button
532
- class="px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap {selectedCategory === 'all' ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
533
  onclick={() => (selectedCategory = 'all')}
534
  >All</button>
535
  {#each menuData as cat}
536
  <button
537
- class="px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap {selectedCategory === cat.name ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
538
  onclick={() => (selectedCategory = cat.name)}
539
  >{cat.name}</button>
540
  {/each}
541
  </div>
542
  <div class="flex-1 overflow-y-auto p-3">
543
- <div class="grid grid-cols-3 xl:grid-cols-4 gap-2">
544
  {#each filteredProducts() as p}
545
  <button class="menu-item-card text-left" onclick={() => cartStore.addItem(p)}>
546
  <div class="flex items-center gap-1.5 mb-1">
@@ -556,8 +557,8 @@
556
  </div>
557
  </div>
558
 
559
- <!-- Right: mini cart -->
560
- <div class="w-72 bg-white border-l border-gray-200 flex flex-col">
561
  <div class="px-4 py-3 border-b border-gray-200 font-semibold text-sm text-gray-800">
562
  {#if tableOrders.length > 0}
563
  Adding to {selectedTable?.name}
@@ -571,9 +572,9 @@
571
  <div class="flex-1 min-w-0">
572
  <p class="truncate text-gray-800">{item.name}</p>
573
  <div class="flex items-center gap-1 mt-1">
574
- <button class="w-6 h-6 rounded bg-gray-50 border border-gray-200 text-xs text-gray-600 hover:border-gray-300" onclick={() => cartStore.updateQuantity(item.cartId, item.quantity - 1)}>-</button>
575
- <span class="w-6 text-center text-xs text-gray-700">{item.quantity}</span>
576
- <button class="w-6 h-6 rounded bg-gray-50 border border-gray-200 text-xs text-gray-600 hover:border-gray-300" onclick={() => cartStore.updateQuantity(item.cartId, item.quantity + 1)}>+</button>
577
  </div>
578
  </div>
579
  <span class="font-medium text-gray-700">{formatCurrency(item.price * item.quantity)}</span>
@@ -607,6 +608,75 @@
607
  {/if}
608
  </div>
609
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
  <!-- ═══════════════════════════════════════════════════════ -->
611
  <!-- PAYMENT MODAL (combined table bill) -->
612
  <!-- ═══════════════════════════════════════════════════════ -->
@@ -615,14 +685,14 @@
615
  <!-- svelte-ignore a11y_no_static_element_interactions -->
616
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
617
  onclick={(e) => { if (e.target === e.currentTarget) showPaymentModal = false; }}>
618
- <div class="bg-white rounded-2xl w-[480px] max-h-[90vh] overflow-y-auto border border-gray-200 shadow-xl animate-in">
619
  <!-- Header -->
620
  <div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
621
  <div>
622
  <h3 class="text-lg font-bold text-gray-800">Generate Bill</h3>
623
  <p class="text-sm text-gray-500">{selectedTable?.name} - {allActiveItems().length} item{allActiveItems().length !== 1 ? 's' : ''}</p>
624
  </div>
625
- <button class="p-1.5 rounded-lg hover:bg-gray-100" onclick={() => { showPaymentModal = false; }}>
626
  <svg class="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
627
  </button>
628
  </div>
@@ -738,7 +808,7 @@
738
  <!-- svelte-ignore a11y_no_static_element_interactions -->
739
  <div class="fixed inset-0 z-[60] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
740
  onclick={(e) => { if (e.target === e.currentTarget) { showCancelDialog = false; cancellingItem = null; } }}>
741
- <div class="bg-white rounded-2xl w-[400px] border border-gray-200 shadow-xl animate-in">
742
  <div class="px-6 py-4 border-b border-gray-200">
743
  <h3 class="text-lg font-bold text-red-500">Remove Item</h3>
744
  <p class="text-sm text-gray-500 mt-1">
 
33
  cartItems.subscribe((v) => (items = v));
34
  cartTotal.subscribe((v) => (total = v));
35
  let submitting = $state(false);
36
+ let mobileCartOpen = $state(false);
37
 
38
  // ── Bill / Payment ──
39
  let showPaymentModal = $state(false);
 
331
  <div class="px-4 py-3 border-b border-gray-200 flex items-center gap-3">
332
  {#if view !== 'tables'}
333
  <button
334
+ class="p-2.5 rounded-lg hover:bg-gray-100 text-gray-500 hover:text-gray-800 transition-colors"
335
  onclick={() => {
336
  if (view === 'menu' && selectedTable?.status === 'occupied') backToTableDetail();
337
  else if (view === 'menu') backToTables();
 
374
  <!-- ═══════════════════════════════════════════════════════ -->
375
  {:else if view === 'tables'}
376
  <div class="flex-1 overflow-y-auto p-4">
377
+ <div class="flex flex-wrap items-center justify-between gap-2 mb-4">
378
  <h2 class="text-lg font-semibold text-gray-800">Select a Table</h2>
379
+ <div class="flex flex-wrap gap-2 sm:gap-3 text-xs text-gray-600">
380
  <span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm bg-green-100 border border-green-400"></span> Available</span>
381
  <span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm bg-orange-100 border border-orange-400"></span> Occupied</span>
382
  <span class="flex items-center gap-1.5"><span class="w-3 h-3 rounded-sm bg-amber-100 border border-amber-400"></span> Cleaning</span>
383
  </div>
384
  </div>
385
+ <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3">
386
  {#each tableList as table}
387
  {@const info = activeOrdersMap[table.id]}
388
  <button
 
471
  </span>
472
  <span class="text-sm font-medium w-20 text-right text-gray-700">{formatCurrency(item.total_price)}</span>
473
  <button
474
+ class="p-2 rounded-lg hover:bg-red-50 text-gray-400 hover:text-red-500 transition-colors"
475
  title="Remove item"
476
  onclick={() => promptCancelItem(item._order, item)}
477
  >
 
530
  </div>
531
  <div class="flex gap-2 px-3 py-2 overflow-x-auto border-b border-gray-200 scrollbar-hide">
532
  <button
533
+ class="px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap {selectedCategory === 'all' ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
534
  onclick={() => (selectedCategory = 'all')}
535
  >All</button>
536
  {#each menuData as cat}
537
  <button
538
+ class="px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap {selectedCategory === cat.name ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
539
  onclick={() => (selectedCategory = cat.name)}
540
  >{cat.name}</button>
541
  {/each}
542
  </div>
543
  <div class="flex-1 overflow-y-auto p-3">
544
+ <div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-2">
545
  {#each filteredProducts() as p}
546
  <button class="menu-item-card text-left" onclick={() => cartStore.addItem(p)}>
547
  <div class="flex items-center gap-1.5 mb-1">
 
557
  </div>
558
  </div>
559
 
560
+ <!-- Right: mini cart (desktop only) -->
561
+ <div class="hidden lg:flex w-72 bg-white border-l border-gray-200 flex-col">
562
  <div class="px-4 py-3 border-b border-gray-200 font-semibold text-sm text-gray-800">
563
  {#if tableOrders.length > 0}
564
  Adding to {selectedTable?.name}
 
572
  <div class="flex-1 min-w-0">
573
  <p class="truncate text-gray-800">{item.name}</p>
574
  <div class="flex items-center gap-1 mt-1">
575
+ <button class="w-8 h-8 rounded bg-gray-50 border border-gray-200 text-sm text-gray-600 hover:border-gray-300 flex items-center justify-center" onclick={() => cartStore.updateQuantity(item.cartId, item.quantity - 1)}>-</button>
576
+ <span class="w-8 text-center text-xs text-gray-700">{item.quantity}</span>
577
+ <button class="w-8 h-8 rounded bg-gray-50 border border-gray-200 text-sm text-gray-600 hover:border-gray-300 flex items-center justify-center" onclick={() => cartStore.updateQuantity(item.cartId, item.quantity + 1)}>+</button>
578
  </div>
579
  </div>
580
  <span class="font-medium text-gray-700">{formatCurrency(item.price * item.quantity)}</span>
 
608
  {/if}
609
  </div>
610
 
611
+ <!-- Mobile cart FAB (captain menu view) -->
612
+ {#if view === 'menu' && items.length > 0 && !mobileCartOpen}
613
+ <button
614
+ class="fixed bottom-6 right-6 z-30 lg:hidden bg-green-600 text-white rounded-2xl shadow-lg px-5 py-3.5 flex items-center gap-3 active:scale-95 transition-transform"
615
+ onclick={() => (mobileCartOpen = true)}
616
+ >
617
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z" /></svg>
618
+ <span class="font-bold">{items.length} item{items.length !== 1 ? 's' : ''}</span>
619
+ <span class="text-white/80">|</span>
620
+ <span class="font-bold">{formatCurrency(total)}</span>
621
+ </button>
622
+ {/if}
623
+
624
+ <!-- Mobile cart overlay (captain) -->
625
+ {#if mobileCartOpen}
626
+ <div class="fixed inset-0 z-40 lg:hidden flex flex-col bg-white">
627
+ <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
628
+ <h3 class="font-bold text-gray-800">
629
+ {#if tableOrders.length > 0}
630
+ Adding to {selectedTable?.name}
631
+ {:else}
632
+ New order for {selectedTable?.name}
633
+ {/if}
634
+ </h3>
635
+ <button class="p-2 rounded-lg hover:bg-gray-100" onclick={() => (mobileCartOpen = false)}>
636
+ <svg class="w-5 h-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
637
+ </button>
638
+ </div>
639
+ <div class="flex-1 overflow-y-auto">
640
+ {#each items as item (item.cartId)}
641
+ <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
642
+ <div class="flex-1 min-w-0">
643
+ <p class="font-medium text-gray-800">{item.name}</p>
644
+ <p class="text-sm text-gray-500">{formatCurrency(item.price)} each</p>
645
+ <div class="flex items-center gap-2 mt-2">
646
+ <button class="w-9 h-9 rounded-lg bg-gray-100 border border-gray-200 text-sm font-medium text-gray-600 flex items-center justify-center" onclick={() => cartStore.updateQuantity(item.cartId, item.quantity - 1)}>-</button>
647
+ <span class="w-8 text-center font-medium text-gray-700">{item.quantity}</span>
648
+ <button class="w-9 h-9 rounded-lg bg-gray-100 border border-gray-200 text-sm font-medium text-gray-600 flex items-center justify-center" onclick={() => cartStore.updateQuantity(item.cartId, item.quantity + 1)}>+</button>
649
+ </div>
650
+ </div>
651
+ <span class="font-bold text-gray-800 ml-3">{formatCurrency(item.price * item.quantity)}</span>
652
+ </div>
653
+ {/each}
654
+ {#if items.length === 0}
655
+ <div class="text-center py-12 text-gray-400">No items added yet</div>
656
+ {/if}
657
+ </div>
658
+ {#if items.length > 0}
659
+ <div class="px-4 py-4 border-t border-gray-200 bg-white">
660
+ <div class="flex justify-between font-bold text-lg mb-3">
661
+ <span class="text-gray-800">Total</span>
662
+ <span class="text-pos-highlight">{formatCurrency(total)}</span>
663
+ </div>
664
+ <button
665
+ class="btn-success w-full py-3.5 flex items-center justify-center gap-2 text-base"
666
+ onclick={() => { mobileCartOpen = false; sendToKitchen(); }}
667
+ disabled={submitting}
668
+ >
669
+ {#if submitting}
670
+ <div class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
671
+ {/if}
672
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
673
+ Send to Kitchen
674
+ </button>
675
+ </div>
676
+ {/if}
677
+ </div>
678
+ {/if}
679
+
680
  <!-- ═══════════════════════════════════════════════════════ -->
681
  <!-- PAYMENT MODAL (combined table bill) -->
682
  <!-- ═══════════════════════════════════════════════════════ -->
 
685
  <!-- svelte-ignore a11y_no_static_element_interactions -->
686
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
687
  onclick={(e) => { if (e.target === e.currentTarget) showPaymentModal = false; }}>
688
+ <div class="bg-white rounded-2xl w-full max-w-[480px] max-h-[90vh] overflow-y-auto border border-gray-200 shadow-xl animate-in">
689
  <!-- Header -->
690
  <div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
691
  <div>
692
  <h3 class="text-lg font-bold text-gray-800">Generate Bill</h3>
693
  <p class="text-sm text-gray-500">{selectedTable?.name} - {allActiveItems().length} item{allActiveItems().length !== 1 ? 's' : ''}</p>
694
  </div>
695
+ <button class="p-2 rounded-lg hover:bg-gray-100" onclick={() => { showPaymentModal = false; }}>
696
  <svg class="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
697
  </button>
698
  </div>
 
808
  <!-- svelte-ignore a11y_no_static_element_interactions -->
809
  <div class="fixed inset-0 z-[60] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
810
  onclick={(e) => { if (e.target === e.currentTarget) { showCancelDialog = false; cancellingItem = null; } }}>
811
+ <div class="bg-white rounded-2xl w-full max-w-[400px] border border-gray-200 shadow-xl animate-in">
812
  <div class="px-6 py-4 border-b border-gray-200">
813
  <h3 class="text-lg font-bold text-red-500">Remove Item</h3>
814
  <p class="text-sm text-gray-500 mt-1">
frontend/src/routes/(app)/dashboard/+page.svelte CHANGED
@@ -109,7 +109,7 @@
109
  {#if dashboard.hourly_sales?.length}
110
  <div class="card p-4">
111
  <h3 class="font-semibold text-sm text-gray-800 mb-3">Hourly Sales (Today)</h3>
112
- <div class="flex items-end gap-1 h-32">
113
  {#each dashboard.hourly_sales as hour}
114
  {@const maxVal = Math.max(...dashboard.hourly_sales.map(h => h.sales || 0), 1)}
115
  <div class="flex-1 flex flex-col items-center gap-1">
 
109
  {#if dashboard.hourly_sales?.length}
110
  <div class="card p-4">
111
  <h3 class="font-semibold text-sm text-gray-800 mb-3">Hourly Sales (Today)</h3>
112
+ <div class="flex items-end gap-1 h-32 overflow-x-auto min-w-0">
113
  {#each dashboard.hourly_sales as hour}
114
  {@const maxVal = Math.max(...dashboard.hourly_sales.map(h => h.sales || 0), 1)}
115
  <div class="flex-1 flex flex-col items-center gap-1">
frontend/src/routes/(app)/kds/+page.svelte CHANGED
@@ -103,8 +103,8 @@
103
 
104
  <div class="flex flex-col h-full">
105
  <!-- Station filter + Stats -->
106
- <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
107
- <div class="flex gap-2 overflow-x-auto">
108
  {#each stations as station}
109
  <button
110
  class="px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors {selectedStation === station ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
@@ -116,7 +116,7 @@
116
  </div>
117
 
118
  {#if stats}
119
- <div class="flex gap-4 text-xs">
120
  <span class="text-pos-warning font-medium">Pending: {stats.pending || 0}</span>
121
  <span class="text-pos-info font-medium">Preparing: {stats.preparing || 0}</span>
122
  <span class="text-pos-success font-medium">Ready: {stats.ready || 0}</span>
@@ -128,9 +128,9 @@
128
  {#if loading}
129
  <Spinner loading={true} text="Loading kitchen orders..." />
130
  {:else}
131
- <div class="flex-1 grid grid-cols-3 gap-0 overflow-hidden">
132
  {#each statusOrder as status}
133
- <div class="flex flex-col border-r border-gray-200 last:border-r-0 overflow-hidden">
134
  <!-- Column header -->
135
  <div class="px-4 py-2 border-b border-gray-200 flex items-center justify-between {
136
  status === 'pending' ? 'bg-amber-50' :
@@ -151,7 +151,7 @@
151
  </div>
152
 
153
  <!-- KOT Cards -->
154
- <div class="flex-1 overflow-y-auto p-3 space-y-3">
155
  {#each kotsByStatus(status) as kot}
156
  <div class="kot-card {status}">
157
  <!-- KOT Header -->
 
103
 
104
  <div class="flex flex-col h-full">
105
  <!-- Station filter + Stats -->
106
+ <div class="px-4 py-3 border-b border-gray-200 flex flex-wrap items-center justify-between gap-2">
107
+ <div class="flex gap-2 overflow-x-auto flex-shrink-0">
108
  {#each stations as station}
109
  <button
110
  class="px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors {selectedStation === station ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
 
116
  </div>
117
 
118
  {#if stats}
119
+ <div class="flex gap-3 sm:gap-4 text-xs flex-shrink-0">
120
  <span class="text-pos-warning font-medium">Pending: {stats.pending || 0}</span>
121
  <span class="text-pos-info font-medium">Preparing: {stats.preparing || 0}</span>
122
  <span class="text-pos-success font-medium">Ready: {stats.ready || 0}</span>
 
128
  {#if loading}
129
  <Spinner loading={true} text="Loading kitchen orders..." />
130
  {:else}
131
+ <div class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-0 overflow-y-auto md:overflow-hidden">
132
  {#each statusOrder as status}
133
+ <div class="flex flex-col border-b md:border-b-0 md:border-r border-gray-200 last:border-b-0 md:last:border-r-0 md:overflow-hidden">
134
  <!-- Column header -->
135
  <div class="px-4 py-2 border-b border-gray-200 flex items-center justify-between {
136
  status === 'pending' ? 'bg-amber-50' :
 
151
  </div>
152
 
153
  <!-- KOT Cards -->
154
+ <div class="p-3 space-y-3 md:flex-1 md:overflow-y-auto">
155
  {#each kotsByStatus(status) as kot}
156
  <div class="kot-card {status}">
157
  <!-- KOT Header -->
frontend/src/routes/(app)/pos/+page.svelte CHANGED
@@ -59,6 +59,9 @@
59
  // Confirm clear
60
  let showClearConfirm = $state(false);
61
 
 
 
 
62
  // ── Load menu ──
63
  onMount(async () => {
64
  try {
@@ -99,6 +102,7 @@
99
  return;
100
  }
101
  showPayment = true;
 
102
  }
103
 
104
  // ── Process payment ──
@@ -179,6 +183,7 @@
179
  if (items.length === 0) return;
180
  heldOrders = [...heldOrders, { ...cart, items: [...items], heldAt: new Date() }];
181
  cartStore.clear();
 
182
  toasts.info('Order held');
183
  }
184
 
@@ -204,7 +209,7 @@
204
  if (e.key === 'F2') { showPayment = true; }
205
  if (e.key === 'F3') { holdOrder(); }
206
  if (e.key === 'F4') { showDiscount = true; }
207
- if (e.key === 'Escape') { showPayment = false; showDiscount = false; showCustomer = false; }
208
  }
209
 
210
  const orderTypes = [
@@ -220,42 +225,44 @@
220
 
221
  <svelte:window onkeydown={handleKeydown} />
222
 
223
- <div class="flex h-full">
224
  <!-- LEFT: Menu Grid -->
225
  <div class="flex-1 flex flex-col overflow-hidden">
226
  <!-- Top bar: search + category filter -->
227
- <div class="p-3 border-b border-gray-200 flex gap-3 items-center">
228
  <!-- Order Type Badge -->
229
- <button
230
- class="btn-ghost btn-sm capitalize"
231
- onclick={() => (showOrderType = !showOrderType)}
232
- >
233
- {cart.orderType?.replace('_', ' ') || 'Dine In'}
234
- <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>
235
- </button>
 
236
 
237
- {#if showOrderType}
238
- <div class="absolute top-24 left-16 z-20 card p-2 shadow-xl">
239
- {#each orderTypes as ot}
240
- <button
241
- class="w-full text-left px-3 py-2 rounded-lg text-sm hover:bg-gray-100 transition-colors {cart.orderType === ot.value ? 'text-pos-highlight font-medium' : 'text-gray-600'}"
242
- onclick={() => { cartStore.setOrderType(ot.value); showOrderType = false; }}
243
- >
244
- {ot.label}
245
- </button>
246
- {/each}
247
- </div>
248
- {/if}
 
249
 
250
  <!-- Search -->
251
- <div class="flex-1 relative">
252
  <svg class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
253
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
254
  </svg>
255
  <input
256
  type="text"
257
  class="input pl-10"
258
- placeholder="Search items... (type to filter)"
259
  bind:value={searchQuery}
260
  />
261
  </div>
@@ -265,21 +272,21 @@
265
  <span class="badge-info">{cart.tableName}</span>
266
  {/if}
267
  {#if cart.customerName}
268
- <span class="badge-success">{cart.customerName}</span>
269
  {/if}
270
  </div>
271
 
272
  <!-- Category tabs -->
273
  <div class="flex gap-2 px-3 py-2 overflow-x-auto border-b border-gray-200 flex-shrink-0">
274
  <button
275
- class="px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors {selectedCategory === 'all' ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
276
  onclick={() => (selectedCategory = 'all')}
277
  >
278
  All Items
279
  </button>
280
  {#each menuData as cat}
281
  <button
282
- class="px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors {selectedCategory === cat.name ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
283
  onclick={() => (selectedCategory = cat.name)}
284
  >
285
  {cat.name}
@@ -293,7 +300,7 @@
293
  {#if loading}
294
  <Spinner loading={true} text="Loading menu..." />
295
  {:else}
296
- <div class="grid grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-2">
297
  {#each filteredProducts() as product}
298
  <button
299
  class="menu-item-card text-left"
@@ -324,8 +331,8 @@
324
  {/if}
325
  </div>
326
 
327
- <!-- Bottom shortcuts -->
328
- <div class="flex gap-2 px-3 py-2 border-t border-gray-200 text-xs text-gray-400">
329
  <span>F2: Pay</span>
330
  <span>F3: Hold</span>
331
  <span>F4: Discount</span>
@@ -335,10 +342,19 @@
335
  </button>
336
  {/if}
337
  </div>
 
 
 
 
 
 
 
 
 
338
  </div>
339
 
340
- <!-- RIGHT: Cart Panel -->
341
- <div class="w-80 xl:w-96 bg-white border-l border-gray-200 flex flex-col">
342
  <!-- Cart header -->
343
  <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
344
  <h2 class="font-semibold text-sm text-gray-800">
@@ -392,17 +408,17 @@
392
  <div class="flex items-center justify-between mt-2">
393
  <div class="flex items-center gap-1">
394
  <button
395
- class="w-7 h-7 rounded-md bg-gray-50 border border-gray-300 flex items-center justify-center hover:border-pos-highlight text-sm transition-colors text-gray-600"
396
  onclick={() => cartStore.updateQuantity(item.cartId, item.quantity - 1)}
397
  >-</button>
398
  <span class="w-8 text-center text-sm font-medium text-gray-800">{item.quantity}</span>
399
  <button
400
- class="w-7 h-7 rounded-md bg-gray-50 border border-gray-300 flex items-center justify-center hover:border-pos-highlight text-sm transition-colors text-gray-600"
401
  onclick={() => cartStore.updateQuantity(item.cartId, item.quantity + 1)}
402
  >+</button>
403
  </div>
404
  <button
405
- class="text-gray-400 hover:text-pos-danger transition-colors"
406
  onclick={() => cartStore.removeItem(item.cartId)}
407
  >
408
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@@ -418,7 +434,6 @@
418
  <!-- Cart totals + actions -->
419
  {#if items.length > 0}
420
  <div class="border-t border-gray-200">
421
- <!-- Totals -->
422
  <div class="px-4 py-3 space-y-1.5 text-sm">
423
  <div class="flex justify-between text-gray-500">
424
  <span>Subtotal</span>
@@ -439,18 +454,132 @@
439
  <span class="text-pos-highlight">{formatCurrency(total)}</span>
440
  </div>
441
  </div>
442
-
443
- <!-- Action buttons -->
444
  <div class="px-4 pb-4 grid grid-cols-2 gap-2">
445
- <button class="btn-ghost btn-sm" onclick={holdOrder}>Hold (F3)</button>
446
- <button class="btn-ghost btn-sm" onclick={() => (showDiscount = true)}>Discount (F4)</button>
447
  <button class="btn-primary w-full col-span-2 py-3 text-base" onclick={placeOrder}>
448
- Pay {formatCurrency(total)} (F2)
449
  </button>
450
  </div>
451
  </div>
452
  {/if}
453
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  </div>
455
 
456
  <!-- Payment Modal -->
@@ -493,17 +622,17 @@
493
  </div>
494
 
495
  <!-- Quick cash buttons -->
496
- <div class="grid grid-cols-4 gap-2">
497
  {#each [100, 200, 500, 1000, 2000] as amount}
498
  <button
499
- class="py-2 rounded-lg bg-gray-50 border border-gray-300 text-sm text-gray-700 hover:border-pos-highlight/50 transition-colors"
500
  onclick={() => (cashReceived = String(amount))}
501
  >
502
  {formatCurrency(amount)}
503
  </button>
504
  {/each}
505
  <button
506
- class="py-2 rounded-lg bg-red-50 border border-pos-highlight text-sm text-pos-highlight font-medium"
507
  onclick={() => (cashReceived = String(Math.ceil(total)))}
508
  >
509
  Exact
@@ -531,13 +660,13 @@
531
  <label class="label">Discount Type</label>
532
  <div class="grid grid-cols-2 gap-2">
533
  <button
534
- class="py-2 rounded-lg border text-sm transition-colors {discountType === 'percentage' ? 'bg-red-50 border-pos-highlight text-pos-highlight' : 'border-gray-300 text-gray-500'}"
535
  onclick={() => (discountType = 'percentage')}
536
  >
537
  Percentage (%)
538
  </button>
539
  <button
540
- class="py-2 rounded-lg border text-sm transition-colors {discountType === 'flat' ? 'bg-red-50 border-pos-highlight text-pos-highlight' : 'border-gray-300 text-gray-500'}"
541
  onclick={() => (discountType = 'flat')}
542
  >
543
  Flat Amount
@@ -605,6 +734,6 @@
605
  show={showClearConfirm}
606
  title="Clear Cart"
607
  message="Remove all items from the cart?"
608
- onconfirm={() => { cartStore.clear(); showClearConfirm = false; }}
609
  oncancel={() => (showClearConfirm = false)}
610
  />
 
59
  // Confirm clear
60
  let showClearConfirm = $state(false);
61
 
62
+ // Mobile cart visibility
63
+ let mobileCartOpen = $state(false);
64
+
65
  // ── Load menu ──
66
  onMount(async () => {
67
  try {
 
102
  return;
103
  }
104
  showPayment = true;
105
+ mobileCartOpen = false;
106
  }
107
 
108
  // ── Process payment ──
 
183
  if (items.length === 0) return;
184
  heldOrders = [...heldOrders, { ...cart, items: [...items], heldAt: new Date() }];
185
  cartStore.clear();
186
+ mobileCartOpen = false;
187
  toasts.info('Order held');
188
  }
189
 
 
209
  if (e.key === 'F2') { showPayment = true; }
210
  if (e.key === 'F3') { holdOrder(); }
211
  if (e.key === 'F4') { showDiscount = true; }
212
+ if (e.key === 'Escape') { showPayment = false; showDiscount = false; showCustomer = false; mobileCartOpen = false; }
213
  }
214
 
215
  const orderTypes = [
 
225
 
226
  <svelte:window onkeydown={handleKeydown} />
227
 
228
+ <div class="flex h-full relative">
229
  <!-- LEFT: Menu Grid -->
230
  <div class="flex-1 flex flex-col overflow-hidden">
231
  <!-- Top bar: search + category filter -->
232
+ <div class="p-3 border-b border-gray-200 flex gap-2 lg:gap-3 items-center flex-wrap">
233
  <!-- Order Type Badge -->
234
+ <div class="relative">
235
+ <button
236
+ class="btn-ghost btn-sm capitalize"
237
+ onclick={() => (showOrderType = !showOrderType)}
238
+ >
239
+ {cart.orderType?.replace('_', ' ') || 'Dine In'}
240
+ <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>
241
+ </button>
242
 
243
+ {#if showOrderType}
244
+ <div class="absolute top-full left-0 mt-1 z-20 card p-2 shadow-xl min-w-[140px]">
245
+ {#each orderTypes as ot}
246
+ <button
247
+ class="w-full text-left px-3 py-2 rounded-lg text-sm hover:bg-gray-100 transition-colors {cart.orderType === ot.value ? 'text-pos-highlight font-medium' : 'text-gray-600'}"
248
+ onclick={() => { cartStore.setOrderType(ot.value); showOrderType = false; }}
249
+ >
250
+ {ot.label}
251
+ </button>
252
+ {/each}
253
+ </div>
254
+ {/if}
255
+ </div>
256
 
257
  <!-- Search -->
258
+ <div class="flex-1 relative min-w-[120px]">
259
  <svg class="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
260
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
261
  </svg>
262
  <input
263
  type="text"
264
  class="input pl-10"
265
+ placeholder="Search items..."
266
  bind:value={searchQuery}
267
  />
268
  </div>
 
272
  <span class="badge-info">{cart.tableName}</span>
273
  {/if}
274
  {#if cart.customerName}
275
+ <span class="badge-success hidden sm:inline-flex">{cart.customerName}</span>
276
  {/if}
277
  </div>
278
 
279
  <!-- Category tabs -->
280
  <div class="flex gap-2 px-3 py-2 overflow-x-auto border-b border-gray-200 flex-shrink-0">
281
  <button
282
+ class="px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap transition-colors {selectedCategory === 'all' ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
283
  onclick={() => (selectedCategory = 'all')}
284
  >
285
  All Items
286
  </button>
287
  {#each menuData as cat}
288
  <button
289
+ class="px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap transition-colors {selectedCategory === cat.name ? 'bg-pos-highlight text-white' : 'bg-white text-gray-500 hover:text-gray-800 border border-gray-200'}"
290
  onclick={() => (selectedCategory = cat.name)}
291
  >
292
  {cat.name}
 
300
  {#if loading}
301
  <Spinner loading={true} text="Loading menu..." />
302
  {:else}
303
+ <div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-2">
304
  {#each filteredProducts() as product}
305
  <button
306
  class="menu-item-card text-left"
 
331
  {/if}
332
  </div>
333
 
334
+ <!-- Bottom shortcuts (desktop only) -->
335
+ <div class="hidden lg:flex gap-2 px-3 py-2 border-t border-gray-200 text-xs text-gray-400">
336
  <span>F2: Pay</span>
337
  <span>F3: Hold</span>
338
  <span>F4: Discount</span>
 
342
  </button>
343
  {/if}
344
  </div>
345
+
346
+ <!-- Mobile held orders bar -->
347
+ {#if heldOrders.length > 0}
348
+ <div class="lg:hidden px-3 py-2 border-t border-gray-200">
349
+ <button class="badge-warning w-full text-center py-2" onclick={() => recallOrder(0)}>
350
+ {heldOrders.length} held order(s) - Tap to recall
351
+ </button>
352
+ </div>
353
+ {/if}
354
  </div>
355
 
356
+ <!-- RIGHT: Cart Panel (desktop) -->
357
+ <div class="hidden lg:flex w-80 xl:w-96 bg-white border-l border-gray-200 flex-col">
358
  <!-- Cart header -->
359
  <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
360
  <h2 class="font-semibold text-sm text-gray-800">
 
408
  <div class="flex items-center justify-between mt-2">
409
  <div class="flex items-center gap-1">
410
  <button
411
+ class="w-8 h-8 rounded-md bg-gray-50 border border-gray-300 flex items-center justify-center hover:border-pos-highlight text-sm transition-colors text-gray-600"
412
  onclick={() => cartStore.updateQuantity(item.cartId, item.quantity - 1)}
413
  >-</button>
414
  <span class="w-8 text-center text-sm font-medium text-gray-800">{item.quantity}</span>
415
  <button
416
+ class="w-8 h-8 rounded-md bg-gray-50 border border-gray-300 flex items-center justify-center hover:border-pos-highlight text-sm transition-colors text-gray-600"
417
  onclick={() => cartStore.updateQuantity(item.cartId, item.quantity + 1)}
418
  >+</button>
419
  </div>
420
  <button
421
+ class="p-2 text-gray-400 hover:text-pos-danger transition-colors"
422
  onclick={() => cartStore.removeItem(item.cartId)}
423
  >
424
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
 
434
  <!-- Cart totals + actions -->
435
  {#if items.length > 0}
436
  <div class="border-t border-gray-200">
 
437
  <div class="px-4 py-3 space-y-1.5 text-sm">
438
  <div class="flex justify-between text-gray-500">
439
  <span>Subtotal</span>
 
454
  <span class="text-pos-highlight">{formatCurrency(total)}</span>
455
  </div>
456
  </div>
 
 
457
  <div class="px-4 pb-4 grid grid-cols-2 gap-2">
458
+ <button class="btn-ghost btn-sm" onclick={holdOrder}>Hold</button>
459
+ <button class="btn-ghost btn-sm" onclick={() => (showDiscount = true)}>Discount</button>
460
  <button class="btn-primary w-full col-span-2 py-3 text-base" onclick={placeOrder}>
461
+ Pay {formatCurrency(total)}
462
  </button>
463
  </div>
464
  </div>
465
  {/if}
466
  </div>
467
+
468
+ <!-- MOBILE: Floating cart button -->
469
+ {#if itemCount > 0}
470
+ <button
471
+ class="lg:hidden fixed bottom-4 right-4 z-30 bg-pos-highlight text-white rounded-full shadow-lg px-5 py-3 flex items-center gap-2 active:scale-95 transition-transform"
472
+ onclick={() => (mobileCartOpen = true)}
473
+ >
474
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
475
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z" />
476
+ </svg>
477
+ <span class="font-bold">{itemCount}</span>
478
+ <span class="text-sm">| {formatCurrency(total)}</span>
479
+ </button>
480
+ {/if}
481
+
482
+ <!-- MOBILE: Cart overlay -->
483
+ {#if mobileCartOpen}
484
+ <div class="lg:hidden fixed inset-0 z-40 flex flex-col bg-white">
485
+ <!-- Mobile cart header -->
486
+ <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between flex-shrink-0">
487
+ <h2 class="font-semibold text-base text-gray-800">
488
+ Cart
489
+ {#if itemCount > 0}
490
+ <span class="badge-info ml-2">{itemCount} items</span>
491
+ {/if}
492
+ </h2>
493
+ <div class="flex gap-2">
494
+ <button class="btn-ghost btn-sm" onclick={() => (showCustomer = true)} title="Customer">
495
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
496
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
497
+ </svg>
498
+ </button>
499
+ <button class="btn-ghost btn-sm" onclick={() => { if (items.length > 0) showClearConfirm = true; }} title="Clear">
500
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
501
+ <path stroke-linecap="round" stroke-linejoin="round" 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" />
502
+ </svg>
503
+ </button>
504
+ <button class="p-2 rounded-lg hover:bg-gray-100 text-gray-500" onclick={() => (mobileCartOpen = false)}>
505
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
506
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
507
+ </svg>
508
+ </button>
509
+ </div>
510
+ </div>
511
+
512
+ <!-- Mobile cart items -->
513
+ <div class="flex-1 overflow-y-auto">
514
+ {#each items as item (item.cartId)}
515
+ <div class="px-4 py-3 border-b border-gray-100">
516
+ <div class="flex items-start justify-between gap-2">
517
+ <div class="flex-1 min-w-0">
518
+ <p class="text-sm font-medium text-gray-800 truncate">{item.name}</p>
519
+ {#if item.modifiers.length > 0}
520
+ <p class="text-xs text-gray-400 mt-0.5">+ {item.modifiers.map((m) => m.name).join(', ')}</p>
521
+ {/if}
522
+ <p class="text-xs text-gray-400">{formatCurrency(item.price)} each</p>
523
+ </div>
524
+ <p class="text-sm font-semibold text-gray-800">{formatCurrency((item.price + item.modifiers.reduce((s, m) => s + (m.price || 0), 0)) * item.quantity)}</p>
525
+ </div>
526
+ <div class="flex items-center justify-between mt-2">
527
+ <div class="flex items-center gap-2">
528
+ <button
529
+ class="w-10 h-10 rounded-lg bg-gray-50 border border-gray-300 flex items-center justify-center text-base text-gray-600 active:bg-gray-100"
530
+ onclick={() => cartStore.updateQuantity(item.cartId, item.quantity - 1)}
531
+ >-</button>
532
+ <span class="w-8 text-center text-base font-medium text-gray-800">{item.quantity}</span>
533
+ <button
534
+ class="w-10 h-10 rounded-lg bg-gray-50 border border-gray-300 flex items-center justify-center text-base text-gray-600 active:bg-gray-100"
535
+ onclick={() => cartStore.updateQuantity(item.cartId, item.quantity + 1)}
536
+ >+</button>
537
+ </div>
538
+ <button
539
+ class="p-2 text-gray-400 hover:text-pos-danger"
540
+ onclick={() => cartStore.removeItem(item.cartId)}
541
+ >
542
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
543
+ <path stroke-linecap="round" stroke-linejoin="round" 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" />
544
+ </svg>
545
+ </button>
546
+ </div>
547
+ </div>
548
+ {/each}
549
+ </div>
550
+
551
+ <!-- Mobile cart totals + actions -->
552
+ <div class="border-t border-gray-200 flex-shrink-0">
553
+ <div class="px-4 py-3 space-y-1.5 text-sm">
554
+ <div class="flex justify-between text-gray-500">
555
+ <span>Subtotal</span>
556
+ <span>{formatCurrency(subtotal)}</span>
557
+ </div>
558
+ {#if discount > 0}
559
+ <div class="flex justify-between text-pos-success">
560
+ <span>Discount</span>
561
+ <span>-{formatCurrency(discount)}</span>
562
+ </div>
563
+ {/if}
564
+ <div class="flex justify-between text-gray-500">
565
+ <span>GST ({cart.gstRate}%)</span>
566
+ <span>{formatCurrency(tax)}</span>
567
+ </div>
568
+ <div class="flex justify-between text-lg font-bold pt-2 border-t border-gray-200">
569
+ <span class="text-gray-800">Total</span>
570
+ <span class="text-pos-highlight">{formatCurrency(total)}</span>
571
+ </div>
572
+ </div>
573
+ <div class="px-4 pb-4 grid grid-cols-2 gap-2">
574
+ <button class="btn-ghost btn-sm" onclick={holdOrder}>Hold</button>
575
+ <button class="btn-ghost btn-sm" onclick={() => { mobileCartOpen = false; showDiscount = true; }}>Discount</button>
576
+ <button class="btn-primary w-full col-span-2 py-3 text-base" onclick={placeOrder}>
577
+ Pay {formatCurrency(total)}
578
+ </button>
579
+ </div>
580
+ </div>
581
+ </div>
582
+ {/if}
583
  </div>
584
 
585
  <!-- Payment Modal -->
 
622
  </div>
623
 
624
  <!-- Quick cash buttons -->
625
+ <div class="grid grid-cols-3 sm:grid-cols-4 gap-2">
626
  {#each [100, 200, 500, 1000, 2000] as amount}
627
  <button
628
+ class="py-2.5 rounded-lg bg-gray-50 border border-gray-300 text-sm text-gray-700 hover:border-pos-highlight/50 transition-colors"
629
  onclick={() => (cashReceived = String(amount))}
630
  >
631
  {formatCurrency(amount)}
632
  </button>
633
  {/each}
634
  <button
635
+ class="py-2.5 rounded-lg bg-red-50 border border-pos-highlight text-sm text-pos-highlight font-medium"
636
  onclick={() => (cashReceived = String(Math.ceil(total)))}
637
  >
638
  Exact
 
660
  <label class="label">Discount Type</label>
661
  <div class="grid grid-cols-2 gap-2">
662
  <button
663
+ class="py-2.5 rounded-lg border text-sm transition-colors {discountType === 'percentage' ? 'bg-red-50 border-pos-highlight text-pos-highlight' : 'border-gray-300 text-gray-500'}"
664
  onclick={() => (discountType = 'percentage')}
665
  >
666
  Percentage (%)
667
  </button>
668
  <button
669
+ class="py-2.5 rounded-lg border text-sm transition-colors {discountType === 'flat' ? 'bg-red-50 border-pos-highlight text-pos-highlight' : 'border-gray-300 text-gray-500'}"
670
  onclick={() => (discountType = 'flat')}
671
  >
672
  Flat Amount
 
734
  show={showClearConfirm}
735
  title="Clear Cart"
736
  message="Remove all items from the cart?"
737
+ onconfirm={() => { cartStore.clear(); showClearConfirm = false; mobileCartOpen = false; }}
738
  oncancel={() => (showClearConfirm = false)}
739
  />
frontend/src/routes/(app)/reports/+page.svelte CHANGED
@@ -58,11 +58,11 @@
58
  </div>
59
 
60
  <!-- Date range + generate -->
61
- <div class="px-4 py-3 border-b border-gray-200 flex items-center gap-3">
62
  <label class="text-xs text-gray-500">From</label>
63
- <input type="date" class="input w-40" bind:value={dateFrom} />
64
  <label class="text-xs text-gray-500">To</label>
65
- <input type="date" class="input w-40" bind:value={dateTo} />
66
  <button class="btn-primary btn-sm" onclick={loadReport}>Generate</button>
67
  </div>
68
 
@@ -74,34 +74,34 @@
74
  <!-- Sales Report -->
75
  {#if activeTab === 'sales'}
76
  <div class="space-y-4">
77
- <div class="grid grid-cols-3 gap-3">
78
  <div class="stat-card"><span class="stat-value text-pos-highlight">{formatCurrency(reportData.totals?.sales || 0)}</span><span class="stat-label">Revenue</span></div>
79
  <div class="stat-card"><span class="stat-value text-pos-success">{reportData.totals?.count || 0}</span><span class="stat-label">Orders</span></div>
80
  <div class="stat-card"><span class="stat-value text-pos-info">{formatCurrency(reportData.totals?.count ? (reportData.totals.sales / reportData.totals.count) : 0)}</span><span class="stat-label">Avg Order</span></div>
81
  </div>
82
  {#if reportData.data?.length}
83
- <table class="table-pos">
84
  <thead><tr><th>Date</th><th>Orders</th><th>Revenue</th><th>Tax</th></tr></thead>
85
  <tbody>
86
  {#each reportData.data as row}
87
  <tr><td>{row.date}</td><td>{row.count}</td><td>{formatCurrency(row.sales)}</td><td>{formatCurrency(row.tax)}</td></tr>
88
  {/each}
89
  </tbody>
90
- </table>
91
  {/if}
92
  </div>
93
 
94
  <!-- GST Report -->
95
  {:else if activeTab === 'gst'}
96
  <div class="space-y-4">
97
- <div class="grid grid-cols-4 gap-3">
98
  <div class="stat-card"><span class="stat-value text-gray-800">{formatCurrency(reportData.summary?.sub_total || 0)}</span><span class="stat-label">Taxable</span></div>
99
  <div class="stat-card"><span class="stat-value text-pos-info">{formatCurrency(reportData.summary?.cgst_amount || 0)}</span><span class="stat-label">CGST</span></div>
100
  <div class="stat-card"><span class="stat-value text-pos-info">{formatCurrency(reportData.summary?.sgst_amount || 0)}</span><span class="stat-label">SGST</span></div>
101
  <div class="stat-card"><span class="stat-value text-pos-warning">{formatCurrency(reportData.summary?.total_tax || 0)}</span><span class="stat-label">Total GST</span></div>
102
  </div>
103
  {#if reportData.data?.length}
104
- <table class="table-pos">
105
  <thead><tr><th>Date</th><th>Taxable</th><th>CGST</th><th>SGST</th><th>Total Tax</th><th>Grand Total</th><th>Orders</th></tr></thead>
106
  <tbody>
107
  {#each reportData.data as row}
@@ -116,7 +116,7 @@
116
  </tr>
117
  {/each}
118
  </tbody>
119
- </table>
120
  {/if}
121
  </div>
122
 
@@ -124,7 +124,7 @@
124
  {:else if activeTab === 'items'}
125
  {@const rows = reportData.data || reportData}
126
  {#if Array.isArray(rows) && rows.length}
127
- <table class="table-pos">
128
  <thead><tr><th>Item</th><th>Qty Sold</th><th>Revenue</th></tr></thead>
129
  <tbody>
130
  {#each rows as row}
@@ -135,7 +135,7 @@
135
  </tr>
136
  {/each}
137
  </tbody>
138
- </table>
139
  {:else}
140
  <p class="text-gray-400 text-sm text-center py-6">No data for this period</p>
141
  {/if}
@@ -144,7 +144,7 @@
144
  {:else if activeTab === 'categories'}
145
  {@const rows = reportData.data || reportData}
146
  {#if Array.isArray(rows) && rows.length}
147
- <table class="table-pos">
148
  <thead><tr><th>Category</th><th>Items Sold</th><th>Revenue</th></tr></thead>
149
  <tbody>
150
  {#each rows as row}
@@ -155,7 +155,7 @@
155
  </tr>
156
  {/each}
157
  </tbody>
158
- </table>
159
  {:else}
160
  <p class="text-gray-400 text-sm text-center py-6">No data for this period</p>
161
  {/if}
@@ -164,7 +164,7 @@
164
  {:else if activeTab === 'captains'}
165
  {@const rows = reportData.data || reportData}
166
  {#if Array.isArray(rows) && rows.length}
167
- <table class="table-pos">
168
  <thead><tr><th>Captain</th><th>Orders</th><th>Revenue</th></tr></thead>
169
  <tbody>
170
  {#each rows as row}
@@ -175,7 +175,7 @@
175
  </tr>
176
  {/each}
177
  </tbody>
178
- </table>
179
  {:else}
180
  <p class="text-gray-400 text-sm text-center py-6">No data for this period</p>
181
  {/if}
@@ -184,7 +184,7 @@
184
  {:else if activeTab === 'kitchen'}
185
  {@const rows = Array.isArray(reportData) ? reportData : (reportData.data || [])}
186
  {#if rows.length}
187
- <table class="table-pos">
188
  <thead><tr><th>Station</th><th>Total KOTs</th><th>Pending</th><th>Completed</th><th>Cancelled</th></tr></thead>
189
  <tbody>
190
  {#each rows as row}
@@ -197,7 +197,7 @@
197
  </tr>
198
  {/each}
199
  </tbody>
200
- </table>
201
  {:else}
202
  <p class="text-gray-400 text-sm text-center py-6">No data for this period</p>
203
  {/if}
@@ -207,14 +207,14 @@
207
  <div class="space-y-4">
208
  <div class="stat-card w-fit"><span class="stat-value text-pos-danger">{formatCurrency(reportData.grand_total || reportData.total || 0)}</span><span class="stat-label">Total Expenses</span></div>
209
  {#if (reportData.data || reportData.by_category || []).length}
210
- <table class="table-pos">
211
  <thead><tr><th>Category</th><th>Amount</th><th>Count</th></tr></thead>
212
  <tbody>
213
  {#each (reportData.data || reportData.by_category || []) as row}
214
  <tr><td>{row.category}</td><td>{formatCurrency(row.total || row.amount)}</td><td>{row.count}</td></tr>
215
  {/each}
216
  </tbody>
217
- </table>
218
  {/if}
219
  </div>
220
 
@@ -222,13 +222,13 @@
222
  {:else if activeTab === 'inventory'}
223
  {#if (reportData.items || []).length}
224
  {#if reportData.total_items}
225
- <div class="grid grid-cols-3 gap-3 mb-4">
226
  <div class="stat-card"><span class="stat-value text-pos-info">{reportData.total_items}</span><span class="stat-label">Total Items</span></div>
227
  <div class="stat-card"><span class="stat-value text-pos-warning">{reportData.low_stock_count || 0}</span><span class="stat-label">Low Stock</span></div>
228
  <div class="stat-card"><span class="stat-value text-pos-highlight">{formatCurrency(reportData.total_value || 0)}</span><span class="stat-label">Total Value</span></div>
229
  </div>
230
  {/if}
231
- <table class="table-pos">
232
  <thead><tr><th>Item</th><th>Current Stock</th><th>Unit</th><th>Status</th></tr></thead>
233
  <tbody>
234
  {#each (reportData.items || []) as row}
@@ -240,7 +240,7 @@
240
  </tr>
241
  {/each}
242
  </tbody>
243
- </table>
244
  {:else}
245
  <p class="text-gray-400 text-sm text-center py-6">No inventory data</p>
246
  {/if}
@@ -281,7 +281,7 @@
281
  </div>
282
  {/if}
283
  {#if reportData.kots?.length}
284
- <table class="table-pos">
285
  <thead><tr><th>KOT #</th><th>Order</th><th>Station</th><th>Items</th><th>Status</th><th>Table</th><th>Captain</th><th>Time</th></tr></thead>
286
  <tbody>
287
  {#each reportData.kots as kot}
@@ -310,7 +310,7 @@
310
  </tr>
311
  {/each}
312
  </tbody>
313
- </table>
314
  {:else}
315
  <p class="text-gray-400 text-sm text-center py-6">No KOTs found for this period</p>
316
  {/if}
@@ -319,7 +319,7 @@
319
  <!-- Cancelled Items Report -->
320
  {:else if activeTab === 'cancelledItems'}
321
  <div class="space-y-4">
322
- <div class="grid grid-cols-2 md:grid-cols-3 gap-3">
323
  <div class="stat-card"><span class="stat-value text-red-500">{reportData.summary?.total_items || 0}</span><span class="stat-label">Items Cancelled</span></div>
324
  <div class="stat-card"><span class="stat-value text-red-500">{formatCurrency(reportData.summary?.total_lost_revenue || 0)}</span><span class="stat-label">Lost Revenue</span></div>
325
  <div class="stat-card"><span class="stat-value text-orange-500">{reportData.summary?.unique_orders || 0}</span><span class="stat-label">Affected Orders</span></div>
@@ -353,7 +353,7 @@
353
  {/if}
354
 
355
  {#if reportData.items?.length}
356
- <table class="table-pos">
357
  <thead><tr><th>Order</th><th>Table</th><th>Item</th><th>Qty</th><th>Price</th><th>Reason</th><th>Cancelled By</th><th>Time</th></tr></thead>
358
  <tbody>
359
  {#each reportData.items as item}
@@ -369,7 +369,7 @@
369
  </tr>
370
  {/each}
371
  </tbody>
372
- </table>
373
  {:else}
374
  <p class="text-gray-400 text-sm text-center py-6">No cancelled items found for this period</p>
375
  {/if}
 
58
  </div>
59
 
60
  <!-- Date range + generate -->
61
+ <div class="px-4 py-3 border-b border-gray-200 flex flex-wrap items-center gap-3">
62
  <label class="text-xs text-gray-500">From</label>
63
+ <input type="date" class="input w-full sm:w-40" bind:value={dateFrom} />
64
  <label class="text-xs text-gray-500">To</label>
65
+ <input type="date" class="input w-full sm:w-40" bind:value={dateTo} />
66
  <button class="btn-primary btn-sm" onclick={loadReport}>Generate</button>
67
  </div>
68
 
 
74
  <!-- Sales Report -->
75
  {#if activeTab === 'sales'}
76
  <div class="space-y-4">
77
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
78
  <div class="stat-card"><span class="stat-value text-pos-highlight">{formatCurrency(reportData.totals?.sales || 0)}</span><span class="stat-label">Revenue</span></div>
79
  <div class="stat-card"><span class="stat-value text-pos-success">{reportData.totals?.count || 0}</span><span class="stat-label">Orders</span></div>
80
  <div class="stat-card"><span class="stat-value text-pos-info">{formatCurrency(reportData.totals?.count ? (reportData.totals.sales / reportData.totals.count) : 0)}</span><span class="stat-label">Avg Order</span></div>
81
  </div>
82
  {#if reportData.data?.length}
83
+ <div class="table-scroll"><table class="table-pos">
84
  <thead><tr><th>Date</th><th>Orders</th><th>Revenue</th><th>Tax</th></tr></thead>
85
  <tbody>
86
  {#each reportData.data as row}
87
  <tr><td>{row.date}</td><td>{row.count}</td><td>{formatCurrency(row.sales)}</td><td>{formatCurrency(row.tax)}</td></tr>
88
  {/each}
89
  </tbody>
90
+ </table></div>
91
  {/if}
92
  </div>
93
 
94
  <!-- GST Report -->
95
  {:else if activeTab === 'gst'}
96
  <div class="space-y-4">
97
+ <div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
98
  <div class="stat-card"><span class="stat-value text-gray-800">{formatCurrency(reportData.summary?.sub_total || 0)}</span><span class="stat-label">Taxable</span></div>
99
  <div class="stat-card"><span class="stat-value text-pos-info">{formatCurrency(reportData.summary?.cgst_amount || 0)}</span><span class="stat-label">CGST</span></div>
100
  <div class="stat-card"><span class="stat-value text-pos-info">{formatCurrency(reportData.summary?.sgst_amount || 0)}</span><span class="stat-label">SGST</span></div>
101
  <div class="stat-card"><span class="stat-value text-pos-warning">{formatCurrency(reportData.summary?.total_tax || 0)}</span><span class="stat-label">Total GST</span></div>
102
  </div>
103
  {#if reportData.data?.length}
104
+ <div class="table-scroll"><table class="table-pos">
105
  <thead><tr><th>Date</th><th>Taxable</th><th>CGST</th><th>SGST</th><th>Total Tax</th><th>Grand Total</th><th>Orders</th></tr></thead>
106
  <tbody>
107
  {#each reportData.data as row}
 
116
  </tr>
117
  {/each}
118
  </tbody>
119
+ </table></div>
120
  {/if}
121
  </div>
122
 
 
124
  {:else if activeTab === 'items'}
125
  {@const rows = reportData.data || reportData}
126
  {#if Array.isArray(rows) && rows.length}
127
+ <div class="table-scroll"><table class="table-pos">
128
  <thead><tr><th>Item</th><th>Qty Sold</th><th>Revenue</th></tr></thead>
129
  <tbody>
130
  {#each rows as row}
 
135
  </tr>
136
  {/each}
137
  </tbody>
138
+ </table></div>
139
  {:else}
140
  <p class="text-gray-400 text-sm text-center py-6">No data for this period</p>
141
  {/if}
 
144
  {:else if activeTab === 'categories'}
145
  {@const rows = reportData.data || reportData}
146
  {#if Array.isArray(rows) && rows.length}
147
+ <div class="table-scroll"><table class="table-pos">
148
  <thead><tr><th>Category</th><th>Items Sold</th><th>Revenue</th></tr></thead>
149
  <tbody>
150
  {#each rows as row}
 
155
  </tr>
156
  {/each}
157
  </tbody>
158
+ </table></div>
159
  {:else}
160
  <p class="text-gray-400 text-sm text-center py-6">No data for this period</p>
161
  {/if}
 
164
  {:else if activeTab === 'captains'}
165
  {@const rows = reportData.data || reportData}
166
  {#if Array.isArray(rows) && rows.length}
167
+ <div class="table-scroll"><table class="table-pos">
168
  <thead><tr><th>Captain</th><th>Orders</th><th>Revenue</th></tr></thead>
169
  <tbody>
170
  {#each rows as row}
 
175
  </tr>
176
  {/each}
177
  </tbody>
178
+ </table></div>
179
  {:else}
180
  <p class="text-gray-400 text-sm text-center py-6">No data for this period</p>
181
  {/if}
 
184
  {:else if activeTab === 'kitchen'}
185
  {@const rows = Array.isArray(reportData) ? reportData : (reportData.data || [])}
186
  {#if rows.length}
187
+ <div class="table-scroll"><table class="table-pos">
188
  <thead><tr><th>Station</th><th>Total KOTs</th><th>Pending</th><th>Completed</th><th>Cancelled</th></tr></thead>
189
  <tbody>
190
  {#each rows as row}
 
197
  </tr>
198
  {/each}
199
  </tbody>
200
+ </table></div>
201
  {:else}
202
  <p class="text-gray-400 text-sm text-center py-6">No data for this period</p>
203
  {/if}
 
207
  <div class="space-y-4">
208
  <div class="stat-card w-fit"><span class="stat-value text-pos-danger">{formatCurrency(reportData.grand_total || reportData.total || 0)}</span><span class="stat-label">Total Expenses</span></div>
209
  {#if (reportData.data || reportData.by_category || []).length}
210
+ <div class="table-scroll"><table class="table-pos">
211
  <thead><tr><th>Category</th><th>Amount</th><th>Count</th></tr></thead>
212
  <tbody>
213
  {#each (reportData.data || reportData.by_category || []) as row}
214
  <tr><td>{row.category}</td><td>{formatCurrency(row.total || row.amount)}</td><td>{row.count}</td></tr>
215
  {/each}
216
  </tbody>
217
+ </table></div>
218
  {/if}
219
  </div>
220
 
 
222
  {:else if activeTab === 'inventory'}
223
  {#if (reportData.items || []).length}
224
  {#if reportData.total_items}
225
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-4">
226
  <div class="stat-card"><span class="stat-value text-pos-info">{reportData.total_items}</span><span class="stat-label">Total Items</span></div>
227
  <div class="stat-card"><span class="stat-value text-pos-warning">{reportData.low_stock_count || 0}</span><span class="stat-label">Low Stock</span></div>
228
  <div class="stat-card"><span class="stat-value text-pos-highlight">{formatCurrency(reportData.total_value || 0)}</span><span class="stat-label">Total Value</span></div>
229
  </div>
230
  {/if}
231
+ <div class="table-scroll"><table class="table-pos">
232
  <thead><tr><th>Item</th><th>Current Stock</th><th>Unit</th><th>Status</th></tr></thead>
233
  <tbody>
234
  {#each (reportData.items || []) as row}
 
240
  </tr>
241
  {/each}
242
  </tbody>
243
+ </table></div>
244
  {:else}
245
  <p class="text-gray-400 text-sm text-center py-6">No inventory data</p>
246
  {/if}
 
281
  </div>
282
  {/if}
283
  {#if reportData.kots?.length}
284
+ <div class="table-scroll"><table class="table-pos">
285
  <thead><tr><th>KOT #</th><th>Order</th><th>Station</th><th>Items</th><th>Status</th><th>Table</th><th>Captain</th><th>Time</th></tr></thead>
286
  <tbody>
287
  {#each reportData.kots as kot}
 
310
  </tr>
311
  {/each}
312
  </tbody>
313
+ </table></div>
314
  {:else}
315
  <p class="text-gray-400 text-sm text-center py-6">No KOTs found for this period</p>
316
  {/if}
 
319
  <!-- Cancelled Items Report -->
320
  {:else if activeTab === 'cancelledItems'}
321
  <div class="space-y-4">
322
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
323
  <div class="stat-card"><span class="stat-value text-red-500">{reportData.summary?.total_items || 0}</span><span class="stat-label">Items Cancelled</span></div>
324
  <div class="stat-card"><span class="stat-value text-red-500">{formatCurrency(reportData.summary?.total_lost_revenue || 0)}</span><span class="stat-label">Lost Revenue</span></div>
325
  <div class="stat-card"><span class="stat-value text-orange-500">{reportData.summary?.unique_orders || 0}</span><span class="stat-label">Affected Orders</span></div>
 
353
  {/if}
354
 
355
  {#if reportData.items?.length}
356
+ <div class="table-scroll"><table class="table-pos">
357
  <thead><tr><th>Order</th><th>Table</th><th>Item</th><th>Qty</th><th>Price</th><th>Reason</th><th>Cancelled By</th><th>Time</th></tr></thead>
358
  <tbody>
359
  {#each reportData.items as item}
 
369
  </tr>
370
  {/each}
371
  </tbody>
372
+ </table></div>
373
  {:else}
374
  <p class="text-gray-400 text-sm text-center py-6">No cancelled items found for this period</p>
375
  {/if}
frontend/src/routes/(app)/tables/+page.svelte CHANGED
@@ -111,7 +111,7 @@
111
  <div class="p-4 space-y-4">
112
  <!-- Overview Stats -->
113
  {#if overview}
114
- <div class="grid grid-cols-4 gap-3">
115
  <div class="stat-card">
116
  <span class="stat-value text-pos-success">{overview.available || 0}</span>
117
  <span class="stat-label">Available</span>
@@ -156,7 +156,7 @@
156
  {#if loading}
157
  <Spinner loading={true} text="Loading tables..." />
158
  {:else}
159
- <div class="grid grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 gap-3">
160
  {#each filteredTables() as table}
161
  <div class="table-cell {table.status || 'available'}" onclick={() => openEditTable(table)}>
162
  <span class="text-lg font-bold">{table.name}</span>
 
111
  <div class="p-4 space-y-4">
112
  <!-- Overview Stats -->
113
  {#if overview}
114
+ <div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
115
  <div class="stat-card">
116
  <span class="stat-value text-pos-success">{overview.available || 0}</span>
117
  <span class="stat-label">Available</span>
 
156
  {#if loading}
157
  <Spinner loading={true} text="Loading tables..." />
158
  {:else}
159
+ <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 gap-3">
160
  {#each filteredTables() as table}
161
  <div class="table-cell {table.status || 'available'}" onclick={() => openEditTable(table)}>
162
  <span class="text-lg font-bold">{table.name}</span>