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 +16 -5
- frontend/src/lib/components/Header.svelte +4 -4
- frontend/src/lib/components/Modal.svelte +4 -4
- frontend/src/lib/components/Sidebar.svelte +40 -30
- frontend/src/routes/(app)/+layout.svelte +24 -5
- frontend/src/routes/(app)/admin/+page.svelte +22 -6
- frontend/src/routes/(app)/captain/+page.svelte +86 -16
- frontend/src/routes/(app)/dashboard/+page.svelte +1 -1
- frontend/src/routes/(app)/kds/+page.svelte +6 -6
- frontend/src/routes/(app)/pos/+page.svelte +176 -47
- frontend/src/routes/(app)/reports/+page.svelte +27 -27
- frontend/src/routes/(app)/tables/+page.svelte +2 -2
|
@@ -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-
|
| 72 |
}
|
| 73 |
|
| 74 |
.btn-lg {
|
|
@@ -116,11 +116,11 @@
|
|
| 116 |
}
|
| 117 |
|
| 118 |
.table-pos th {
|
| 119 |
-
@apply px-
|
| 120 |
}
|
| 121 |
|
| 122 |
.table-pos td {
|
| 123 |
-
@apply px-
|
| 124 |
}
|
| 125 |
|
| 126 |
.table-pos tbody tr {
|
|
@@ -132,7 +132,7 @@
|
|
| 132 |
}
|
| 133 |
|
| 134 |
.stat-value {
|
| 135 |
-
@apply text-
|
| 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 |
}
|
|
@@ -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-
|
| 26 |
<button
|
| 27 |
onclick={oncollapse}
|
| 28 |
-
class="text-gray-500 hover:text-gray-800 transition-colors p-
|
| 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 |
|
|
@@ -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 |
-
|
| 43 |
-
|
| 44 |
-
|
| 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>
|
|
@@ -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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 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 |
-
|
| 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 |
-
|
| 74 |
-
<div class="flex items-center
|
| 75 |
-
<
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
<
|
| 79 |
-
|
| 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="
|
| 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 |
-
|
| 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>
|
|
@@ -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 |
-
<
|
| 47 |
-
<div class="
|
| 48 |
-
<
|
| 49 |
-
|
| 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>
|
|
@@ -132,9 +132,23 @@
|
|
| 132 |
|
| 133 |
<svelte:head><title>Settings - HSS POS</title></svelte:head>
|
| 134 |
|
| 135 |
-
<div class="flex h-full">
|
| 136 |
-
<!--
|
| 137 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
|
@@ -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-
|
| 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-
|
| 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-
|
| 533 |
onclick={() => (selectedCategory = 'all')}
|
| 534 |
>All</button>
|
| 535 |
{#each menuData as cat}
|
| 536 |
<button
|
| 537 |
-
class="px-3 py-
|
| 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
|
| 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-
|
| 575 |
-
<span class="w-
|
| 576 |
-
<button class="w-
|
| 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-
|
| 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">
|
|
@@ -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">
|
|
@@ -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="
|
| 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 -->
|
|
@@ -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 |
-
<
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
|
|
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
| 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...
|
| 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-
|
| 276 |
onclick={() => (selectedCategory = 'all')}
|
| 277 |
>
|
| 278 |
All Items
|
| 279 |
</button>
|
| 280 |
{#each menuData as cat}
|
| 281 |
<button
|
| 282 |
-
class="px-3 py-
|
| 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
|
| 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-
|
| 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-
|
| 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
|
| 446 |
-
<button class="btn-ghost btn-sm" onclick={() => (showDiscount = true)}>Discount
|
| 447 |
<button class="btn-primary w-full col-span-2 py-3 text-base" onclick={placeOrder}>
|
| 448 |
-
Pay {formatCurrency(total)}
|
| 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 |
/>
|
|
@@ -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
|
| 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}
|
|
@@ -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-
|
| 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>
|