Skip to content

Commit fc81e5f

Browse files
committed
Design updates for sales and checkout
1 parent a44673e commit fc81e5f

9 files changed

Lines changed: 267 additions & 85 deletions

File tree

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# next...
2+
3+
- Update libraries and dependencies
4+
- Refactor design for current order in the Sales page for more consistency: convert "blocks" list to an actual "table" for better proportions, rearrange buttons a bit, and make size more fixed in vertical mode.
5+
- Add custom numpad/keyboard to avoid using the internal numpad for cash orders: the native one is slow to load and hides other buttons because its display is inconsistent across devices, so a "virtual" one integrated in the app directly is easier to handle
6+
- Fix imported types from Bootstrap
7+
8+
# v1.0.3
9+
10+
- Update libraries and dependencies
11+
- Fix displayed application name and description
12+
13+
# v1.0.2
14+
15+
- Slight update on the "Sales" page to better distinguish grouped categories vs single products
16+
- Fix issue on Android where app was visible under the main buttons and notification bar
17+
- Fix an issue where the dark overlay didn't disappear when closing the menu or opening a page from the menu
18+
- Fix javascript and css integration
19+
- Disable the "internet access" permission: this app can be offline without issues 👍
20+
- Update libraries and dependencies
21+
- Update default data
22+
23+
# v1.0.1
24+
25+
- Fixes for the release process
26+
27+
# v1.0.0
28+
29+
First release 🚀

src-tauri/tauri.conf.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
{
1515
"title": "PoS Sum",
1616
"width": 900,
17-
"height": 700
17+
"height": 700,
18+
"minWidth": 600,
19+
"minHeight": 700
1820
}
1921
],
2022
"security": {

src/app.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Offcanvas } from 'bootstrap';
2+
3+
declare global {
4+
interface Window {
5+
bootstrap?: {
6+
Offcanvas: typeof Offcanvas;
7+
};
8+
}
9+
}
10+
11+
export {};
Lines changed: 128 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script lang="ts">
2-
import { tick } from 'svelte';
32
import type { CartItem } from '$lib/types';
43
import { formatPrice } from '$lib/utils/format';
54
import { t } from '$lib/i18n';
5+
import Numpad from './Numpad.svelte';
66
77
interface Props {
88
items: CartItem[];
@@ -16,8 +16,8 @@
1616
let paymentMethod = $state<'cash' | 'card' | null>(null);
1717
let cashReceived = $state('');
1818
let isSubmitting = $state(false);
19-
let cashInput = $state<HTMLInputElement | null>(null);
2019
20+
let numberOfProducts = $derived(items.reduce((acc, item) => acc + item.quantity, 0));
2121
let formattedTotal = $derived((total / 100).toFixed(2).replace('.', ','));
2222
2323
let cashReceivedCents = $derived.by(() => {
@@ -41,11 +41,21 @@
4141
cashReceived = '';
4242
}
4343
44-
$effect(() => {
45-
if (paymentMethod === 'cash') {
46-
tick().then(() => cashInput?.focus());
44+
function handleNumpadKey(key: string) {
45+
if (key === 'backspace') {
46+
cashReceived = cashReceived.slice(0, -1);
47+
} else if (key.startsWith(',')) {
48+
if (!cashReceived.includes(',')) {
49+
cashReceived += key;
50+
}
51+
if (key.length > 1) {
52+
// cases for ",00" or ",50"
53+
cashReceived = cashReceived.replace(/,.*$/gi, key);
54+
}
55+
} else {
56+
cashReceived += key;
4757
}
48-
});
58+
}
4959
5060
async function handleConfirm() {
5161
if (!paymentMethod || !canConfirm) {
@@ -56,63 +66,124 @@
5666
}
5767
</script>
5868

59-
<!-- svelte-ignore a11y_no_static_element_interactions -->
60-
<div class="modal-overlay position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center" style="z-index: 100; background: rgba(0,0,0,0.5);" onclick={onCancel} onkeydown={(e) => e.key === 'Escape' && onCancel()}>
61-
<!-- svelte-ignore a11y_no_static_element_interactions -->
62-
<div class="modal-box bg-body rounded-4 p-4 overflow-auto" style="width: 90%; max-width: 420px; max-height: 90vh;" onclick={(e) => e.stopPropagation()}>
63-
<h2 class="h5 mb-3">{$t('checkout.title')}</h2>
64-
65-
<div class="mb-3">
66-
{#each items as item (item.product.id)}
67-
<div class="d-flex justify-content-between py-1" style="font-size:0.95rem">
68-
<span>{item.quantity}x {item.product.name}</span>
69-
<span>{formatPrice(item.product.price * item.quantity)}</span>
70-
</div>
71-
{/each}
72-
<div class="d-flex justify-content-between pt-2 mt-2 border-top border-2 fs-5 fw-bold">
73-
<span>{$t('checkout.total')}</span>
74-
<span>{formatPrice(total)}</span>
75-
</div>
76-
</div>
69+
<div class="modal-overlay position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center">
70+
<div class="checkout-modal bg-body rounded-4 p-3">
71+
<div class="checkout-inner">
7772

78-
<div class="d-flex gap-2 mb-3">
79-
<button
80-
class="btn flex-fill fw-semibold {paymentMethod === 'cash' ? 'btn-primary' : 'btn-outline-secondary'}"
81-
style="min-height: 48px;"
82-
onclick={() => selectPayment('cash')}
83-
>
84-
{$t('checkout.cash')}
85-
</button>
86-
<button
87-
class="btn flex-fill fw-semibold {paymentMethod === 'card' ? 'btn-primary' : 'btn-outline-secondary'}"
88-
style="min-height: 48px;"
89-
onclick={() => selectPayment('card')}
90-
>
91-
{$t('checkout.card')}
92-
</button>
93-
</div>
73+
<div class="checkout-items">
74+
<h2 class="h5">{$t('checkout.title')}</h2>
75+
76+
<table class="table table-hover table-bordered" style="font-size:0.95rem">
77+
<tbody>
78+
{#each items as item (item.product.id)}
79+
<tr>
80+
<td>{item.quantity}x {item.product.name}</td>
81+
<td>{formatPrice(item.product.price * item.quantity)}</td>
82+
</tr>
83+
{/each}
84+
<tr class="table-info">
85+
<td><strong>{$t('checkout.numberOfProducts')}</strong></td>
86+
<td><strong>{numberOfProducts}</strong></td>
87+
</tr>
88+
<tr class="table-success">
89+
<td><strong>{$t('checkout.total')}</strong></td>
90+
<td><strong>{formatPrice(total)}</strong></td>
91+
</tr>
92+
</tbody>
93+
</table>
94+
</div>
9495

95-
{#if paymentMethod === 'cash'}
96-
<div class="cash-section mb-3">
97-
<label class="d-flex flex-column gap-1 fw-semibold" style="font-size:0.95rem">
98-
{$t('checkout.amountReceived')}
99-
<input bind:this={cashInput} class="form-control text-end fs-5" type="text" inputmode="decimal" placeholder={formattedTotal} bind:value={cashReceived} />
100-
</label>
101-
{#if cashReceivedCents >= total}
96+
<div class="checkout-payment">
97+
<div class:visible={paymentMethod === 'cash'} class:invisible={paymentMethod !== 'cash'}>
98+
<div class="mt-3">
99+
<Numpad onKeyPress={handleNumpadKey} />
100+
</div>
101+
<label class="d-flex flex-column gap-1 fw-semibold" style="font-size:0.95rem">
102+
{$t('checkout.amountReceived')}
103+
<input class="form-control text-end fs-5" type="text" inputmode="none" readonly placeholder={formattedTotal} value={cashReceived} />
104+
</label>
102105
<div class="mt-2 fs-5 text-success">
103106
{$t('checkout.change')} <strong>{formatPrice(change)}</strong>
104107
</div>
105-
{/if}
108+
</div>
109+
</div>
110+
111+
<div class="checkout-actions">
112+
<div class="d-flex gap-2 mb-2">
113+
<button
114+
class="btn flex-fill fw-semibold {paymentMethod === 'cash' ? 'btn-primary' : 'btn-outline-secondary'}"
115+
style="min-height: 48px;"
116+
onclick={() => selectPayment('cash')}
117+
>
118+
💵 {$t('checkout.cash')}
119+
</button>
120+
<button
121+
class="btn flex-fill fw-semibold {paymentMethod === 'card' ? 'btn-primary' : 'btn-outline-secondary'}"
122+
style="min-height: 48px;"
123+
onclick={() => selectPayment('card')}
124+
>
125+
💳 {$t('checkout.card')}
126+
</button>
127+
</div>
128+
<div class="d-flex gap-2">
129+
<button class="btn btn-success flex-fill fw-semibold" onclick={handleConfirm} disabled={!canConfirm}>
130+
{isSubmitting ? $t('checkout.submitting') : $t('checkout.confirm')}
131+
</button>
132+
<button class="btn btn-danger flex-fill fw-semibold" onclick={onCancel} disabled={isSubmitting}>
133+
{$t('checkout.cancel')}
134+
</button>
135+
</div>
106136
</div>
107-
{/if}
108-
109-
<div class="d-flex gap-2">
110-
<button class="btn btn-secondary flex-fill fw-semibold" onclick={onCancel} disabled={isSubmitting}
111-
>{$t('checkout.cancel')}</button
112-
>
113-
<button class="btn btn-success flex-fill fw-semibold" onclick={handleConfirm} disabled={!canConfirm}>
114-
{isSubmitting ? $t('checkout.submitting') : $t('checkout.confirm')}
115-
</button>
116137
</div>
117138
</div>
118139
</div>
140+
141+
<style>
142+
.modal-overlay {
143+
z-index: 1200;
144+
background: rgba(0, 0, 0, 0.5);
145+
}
146+
.checkout-modal {
147+
width: 100vw;
148+
height: 100vh;
149+
}
150+
.checkout-inner {
151+
display: flex;
152+
flex-direction: column;
153+
height: 100%;
154+
}
155+
.checkout-items {
156+
flex: 1 1 0;
157+
min-height: 0;
158+
overflow-y: auto;
159+
}
160+
.checkout-payment {
161+
flex: 0 0 auto;
162+
}
163+
.checkout-actions {
164+
flex: 0 0 auto;
165+
padding-top: 0.5rem;
166+
}
167+
168+
@media (min-width: 768px) {
169+
.checkout-inner {
170+
display: grid;
171+
grid-template-columns: 1fr 1fr;
172+
grid-template-rows: 1fr auto;
173+
grid-template-areas:
174+
"items payment"
175+
"actions actions";
176+
gap: 1rem;
177+
}
178+
.checkout-items {
179+
grid-area: items;
180+
overflow-y: auto;
181+
}
182+
.checkout-payment {
183+
grid-area: payment;
184+
}
185+
.checkout-actions {
186+
grid-area: actions;
187+
}
188+
}
189+
</style>

src/lib/components/NavMenu.svelte

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,20 @@
1010
{ href: '/dashboard', labelKey: 'nav.dashboard' },
1111
{ href: '/configuration', labelKey: 'nav.configuration' }
1212
];
13-
13+
1414
let offcanvas: HTMLElement;
1515
16+
let isDarkMode: boolean = $state(false);
17+
18+
$effect(() => {
19+
changeMode(isDarkMode);
20+
});
21+
22+
function changeMode(newMode: boolean) {
23+
isDarkMode = newMode;
24+
window?.document.body.setAttribute('data-bs-theme', isDarkMode ? 'dark' : 'light');
25+
}
26+
1627
function isActive(href: string): boolean {
1728
return href === '/' ? page.url.pathname === '/' : page.url.pathname.startsWith(href);
1829
}
@@ -70,5 +81,17 @@
7081
</ul>
7182
</div>
7283
</div>
84+
85+
<div>
86+
<button class="btn btn-outline-secondary" type="button" onclick="{() => changeMode(!isDarkMode)}">
87+
{isDarkMode ? '' : '🌙'}
88+
</button>
89+
</div>
7390
</div>
7491
</nav>
92+
93+
<style>
94+
.navbar-toggler {
95+
background: #bbb;
96+
}
97+
</style>

src/lib/components/Numpad.svelte

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script lang="ts">
2+
interface Props {
3+
onKeyPress: (key: string) => void;
4+
}
5+
6+
let { onKeyPress }: Props = $props();
7+
8+
const remap: {[key: string]: string} = {
9+
'': 'backspace',
10+
'': '-',
11+
};
12+
13+
const keys = [
14+
'1', '2', '3',
15+
'4', '5', '6',
16+
'7', '8', '9',
17+
',', '0', '',
18+
',50', ',00', '',
19+
];
20+
</script>
21+
22+
<div class="grid">
23+
{#each keys as key}
24+
<button
25+
class="btn col-4 {key === '' ? 'btn-outline-danger' : 'btn-outline-primary'} fw-semibold fs-4"
26+
type="button"
27+
onclick={() => onKeyPress(remap[key] ?? key)}
28+
>
29+
{key}
30+
</button>
31+
{/each}
32+
</div>

0 commit comments

Comments
 (0)