|
1 | 1 | <script lang="ts"> |
2 | | - import { tick } from 'svelte'; |
3 | 2 | import type { CartItem } from '$lib/types'; |
4 | 3 | import { formatPrice } from '$lib/utils/format'; |
5 | 4 | import { t } from '$lib/i18n'; |
| 5 | + import Numpad from './Numpad.svelte'; |
6 | 6 |
|
7 | 7 | interface Props { |
8 | 8 | items: CartItem[]; |
|
16 | 16 | let paymentMethod = $state<'cash' | 'card' | null>(null); |
17 | 17 | let cashReceived = $state(''); |
18 | 18 | let isSubmitting = $state(false); |
19 | | - let cashInput = $state<HTMLInputElement | null>(null); |
20 | 19 |
|
| 20 | + let numberOfProducts = $derived(items.reduce((acc, item) => acc + item.quantity, 0)); |
21 | 21 | let formattedTotal = $derived((total / 100).toFixed(2).replace('.', ',')); |
22 | 22 |
|
23 | 23 | let cashReceivedCents = $derived.by(() => { |
|
41 | 41 | cashReceived = ''; |
42 | 42 | } |
43 | 43 |
|
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; |
47 | 57 | } |
48 | | - }); |
| 58 | + } |
49 | 59 |
|
50 | 60 | async function handleConfirm() { |
51 | 61 | if (!paymentMethod || !canConfirm) { |
|
56 | 66 | } |
57 | 67 | </script> |
58 | 68 |
|
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"> |
77 | 72 |
|
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> |
94 | 95 |
|
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> |
102 | 105 | <div class="mt-2 fs-5 text-success"> |
103 | 106 | {$t('checkout.change')} <strong>{formatPrice(change)}</strong> |
104 | 107 | </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> |
106 | 136 | </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> |
116 | 137 | </div> |
117 | 138 | </div> |
118 | 139 | </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> |
0 commit comments