Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions src/lib/components/DeleteModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script lang="ts">
import '@material/web/button/filled-button.js';
import '@material/web/button/outlined-button.js';
import '@material/web/iconbutton/icon-button.js';

let {
open = $bindable(false),
target = null as { type: string; id: number; label: string } | null,
onDelete
}: {
open: boolean;
target: { type: string; id: number; label: string } | null;
onDelete: (type: string, id: number, passcode: string) => Promise<void>;
} = $props();

let passcode = $state('');
let error = $state('');
let deleting = $state(false);

function close() {
if (deleting) return;
open = false;
passcode = '';
error = '';
}

async function submit() {
if (!target || passcode.length !== 4) {
error = 'Enter 4-digit passcode';
return;
}
deleting = true;
error = '';
try {
await onDelete(target.type, target.id, passcode);
close();
} catch (e: any) {
error = e.message || 'Delete failed';
}
deleting = false;
}
</script>

{#if open && target}
<div class="modal-overlay" onclick={close}>
<div class="modal-content" onclick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="delete-modal-title">
<div class="modal-header">
<div class="modal-title">
<span class="material-symbols-rounded" style="color: var(--md-sys-color-error)">delete</span>
<h2 id="delete-modal-title" class="md-headline-small">Delete {target.label}?</h2>
</div>
<md-icon-button onclick={close} aria-label="Close">
<span class="material-symbols-rounded">close</span>
</md-icon-button>
</div>
<div class="modal-body">
<p class="md-body-medium">This action requires passcode verification. The item will be marked as deleted but kept in records for the shift report.</p>
<div class="form-section">
<label class="form-label">
<span class="material-symbols-rounded">lock</span>
<span class="md-title-small">Enter Passcode</span>
</label>
<input
type="password"
maxlength="4"
inputmode="numeric"
placeholder="4-digit passcode"
bind:value={passcode}
class="passcode-input"
onkeydown={(e) => { if (e.key === 'Enter') submit(); }}
/>
Comment on lines +59 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Material Web 3 text field component API Svelte binding

💡 Result:

@material/web Material 3 text fields (<md-filled-text-field>, <md-outlined-text-field>) expose a value property and fire native input / change events (bubbling; input is also composed), so in Svelte you typically bind by wiring value + on:input. [1]

Svelte (recommended): “controlled” binding

<script>
  import '@material/web/textfield/filled-text-field.js';
  // or: import '@material/web/textfield/outlined-text-field.js';

  let value = '';
</script>

<md-filled-text-field
  label="Name"
  {value}
  on:input={(e) => (value = e.currentTarget.value)}
/>

This uses Svelte’s normal event handling and updates your state from the web component’s value. [1]

Why not bind:value directly?

Svelte’s built-in bind:value behavior is specifically documented for native form elements like <input> (where Svelte knows exactly which event to listen to). For web components, the reliable cross-version approach is the explicit value + on:input pattern above. [2]

Sources: Material Web text field API/events [1], Svelte binding docs [2].

[1] (material-web.dev)
[2] (svelte.dev)

Citations:


🌐 Web query:

Material Design 3 md-filled-text-field maxlength password type attributes

💡 Result:

In Material Design 3 / Material Web, <md-filled-text-field> forwards common <input> constraints and supports:

  • Password field: set the type attribute to password

    <md-filled-text-field label="Password" type="password"></md-filled-text-field>

    (type is the underlying HTML input type.) [1]

  • Max length: set maxlength (attribute) / maxLength (JS property). When maxlength is present, the component shows a character counter automatically.

    <md-filled-text-field label="PIN" type="password" maxlength="12"></md-filled-text-field>

    Material Web docs: maxLengthmaxlength, where -1 means “no limit”. [1]

Notes:

  • maxlength is an HTML constraint attribute (integer ≥ 0; measured in UTF‑16 code units). [2]
  • For <input type="password">, maxlength is supported and participates in constraint validation. [3]

Sources: Material Web text-field docs [1], MDN maxlength [2], MDN password input attributes [3].

[1] (material-web.dev)
[2] (developer.mozilla.org)
[3] (developer.mozilla.org)

Citations:


🏁 Script executed:

# Check if the file exists and examine the current implementation
cat -n src/lib/components/DeleteModal.svelte | sed -n '59,71p'

Repository: TheOneWhoBurns/cpoint

Length of output: 559


Use a Material Web text field component instead of native <input> for UI consistency and accessibility.
The current native <input> doesn't follow the "Material Web 3 components" guideline, and the label isn't properly associated with the control. Additionally, bind:value is unreliable with web components—use explicit value property binding and on:input instead.

♻️ Suggested fix (align with Material Web 3 + accessibility)
 <script lang="ts">
 	import '@material/web/button/filled-button.js';
 	import '@material/web/button/outlined-button.js';
 	import '@material/web/iconbutton/icon-button.js';
+	import '@material/web/textfield/filled-text-field.js';
@@
-					<label class="form-label">
+					<label class="form-label" for="delete-passcode">
 						<span class="material-symbols-rounded">lock</span>
 						<span class="md-title-small">Enter Passcode</span>
 					</label>
-					<input
-						type="password"
-						maxlength="4"
-						inputmode="numeric"
-						placeholder="4-digit passcode"
-						bind:value={passcode}
-						class="passcode-input"
-						onkeydown={(e) => { if (e.key === 'Enter') submit(); }}
-					/>
+					<md-filled-text-field
+						id="delete-passcode"
+						type="password"
+						label="Enter Passcode"
+						maxlength="4"
+						inputmode="numeric"
+						placeholder="4-digit passcode"
+						value={passcode}
+						on:input={(e) => (passcode = e.currentTarget.value)}
+						class="passcode-input"
+						onkeydown={(e) => { if (e.key === 'Enter') submit(); }}
+					/>

Per coding guidelines: "Use Material Web 3 components for UI consistency across the tablet-optimized interface" and "Review Svelte components for Accessibility (ARIA labels, keyboard navigation)."

🤖 Prompt for AI Agents
In `@src/lib/components/DeleteModal.svelte` around lines 59 - 71, Replace the
native <input> in DeleteModal.svelte with the Material Web text field component
(e.g., the MW3 textfield used in the project) so the control is consistent and
accessible; keep the same attributes (type="password", maxlength="4",
inputmode="numeric", placeholder) but bind the component's .value explicitly to
the passcode variable via an on:input handler that sets passcode (do not use
Svelte bind:value), and preserve the existing Enter key behavior by handling
on:keydown to call submit(). Ensure the label is properly associated with the
text field (use the component's label prop or a for/id pairing) and remove the
standalone span label so accessibility semantics come from the material
component.

{#if error}
<div class="error-banner">
<span class="material-symbols-rounded">error</span>
<span class="md-body-medium">{error}</span>
</div>
{/if}
</div>
</div>
<div class="modal-footer">
<md-outlined-button onclick={close} disabled={deleting}>Cancel</md-outlined-button>
<md-filled-button class="danger-btn" onclick={submit} disabled={deleting || passcode.length !== 4}>
<span class="material-symbols-rounded" slot="icon">delete</span>
Delete
</md-filled-button>
</div>
</div>
</div>
{/if}

<style>
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 200; padding: var(--md-sys-spacing-md); animation: fade-in 0.2s var(--md-sys-motion-easing-standard); }
.modal-content { background: var(--md-sys-color-surface); border-radius: var(--md-sys-shape-corner-extra-large); max-width: 500px; width: 100%; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; animation: scale-in 0.3s var(--md-sys-motion-easing-emphasized); }
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: var(--md-sys-spacing-lg); border-bottom: 1px solid var(--md-sys-color-outline-variant); flex-shrink: 0; }
.modal-title { display: flex; align-items: center; gap: var(--md-sys-spacing-md); }
.modal-title .material-symbols-rounded { font-size: 32px; }
.modal-title h2 { margin: 0; color: var(--md-sys-color-on-surface); }
.modal-body { padding: var(--md-sys-spacing-lg); display: flex; flex-direction: column; gap: var(--md-sys-spacing-xl); overflow-y: auto; flex: 1; min-height: 0; }
.modal-body p { margin: 0; color: var(--md-sys-color-on-surface-variant); }
.modal-footer { display: flex; justify-content: flex-end; gap: var(--md-sys-spacing-lg); padding: var(--md-sys-spacing-md) var(--md-sys-spacing-lg) var(--md-sys-spacing-lg); border-top: 1px solid var(--md-sys-color-outline-variant); flex-shrink: 0; }
.form-section { display: flex; flex-direction: column; gap: var(--md-sys-spacing-md); }
.form-label { display: flex; align-items: center; gap: var(--md-sys-spacing-sm); color: var(--md-sys-color-on-surface); font: var(--md-sys-typescale-title-small); }
.form-label .material-symbols-rounded { font-size: 24px; color: var(--md-sys-color-primary); flex-shrink: 0; }
.passcode-input { width: 100%; padding: var(--md-sys-spacing-sm) var(--md-sys-spacing-md); border: 1px solid var(--md-sys-color-outline); border-radius: var(--md-sys-shape-corner-small); font: var(--md-sys-typescale-title-medium); text-align: center; letter-spacing: 0.5em; background: var(--md-sys-color-surface); color: var(--md-sys-color-on-surface); }
.passcode-input:focus { outline: none; border-color: var(--md-sys-color-primary); border-width: 2px; }
.error-banner { display: flex; align-items: center; gap: var(--md-sys-spacing-sm); padding: var(--md-sys-spacing-md); background: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); border-radius: var(--md-sys-shape-corner-small); }
.error-banner .material-symbols-rounded { font-size: 20px; }
.danger-btn { --md-filled-button-container-color: var(--md-sys-color-error); --md-filled-button-label-text-color: var(--md-sys-color-on-error); }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes scale-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
@media (max-width: 768px) {
.modal-content { max-width: 100%; max-height: 95vh; border-radius: var(--md-sys-shape-corner-large) var(--md-sys-shape-corner-large) 0 0; margin-top: auto; }
.modal-overlay { align-items: flex-end; padding: 0; }
}
</style>
177 changes: 177 additions & 0 deletions src/lib/components/EditReservationModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<script lang="ts">
import '@material/web/button/filled-button.js';
import '@material/web/button/outlined-button.js';
import '@material/web/textfield/outlined-text-field.js';
import '@material/web/iconbutton/icon-button.js';
import flatpickr from 'flatpickr';
import 'flatpickr/dist/flatpickr.min.css';

let {
open = $bindable(false),
reservation = null as any,
loading = false,
onSave
}: {
open: boolean;
reservation: any;
loading?: boolean;
onSave: (id: number, data: { customer: object; reason: string; reservedFrom?: string; reservedUntil?: string }) => Promise<void>;
} = $props();

let customerName = $state('');
let customerHotel = $state('');
let reason = $state('');
let fromDate = $state('');
let untilDate = $state('');
let error = $state('');
let saving = $state(false);
let fromInput = $state<HTMLInputElement | null>(null);
let untilInput = $state<HTMLInputElement | null>(null);
let fromPicker: flatpickr.Instance | null = null;
let untilPicker: flatpickr.Instance | null = null;

$effect(() => {
if (open && reservation) {
const customer = reservation.customer as { name?: string; hotel?: string } | null;
customerName = customer?.name || '';
customerHotel = customer?.hotel || '';
reason = reservation.reason || '';
fromDate = reservation.reservedFrom ? new Date(reservation.reservedFrom).toISOString().slice(0, 16) : '';
untilDate = reservation.reservedUntil ? new Date(reservation.reservedUntil).toISOString().slice(0, 16) : '';
error = '';
}
});

$effect(() => {
if (open && fromInput) {
fromPicker = flatpickr(fromInput, {
enableTime: true, dateFormat: 'Y-m-d H:i', defaultDate: fromDate || undefined,
onChange: ([date]) => { if (date) fromDate = date.toISOString(); }
});
}
return () => { fromPicker?.destroy(); };
});

$effect(() => {
if (open && untilInput) {
untilPicker = flatpickr(untilInput, {
enableTime: true, dateFormat: 'Y-m-d H:i', defaultDate: untilDate || undefined,
onChange: ([date]) => { if (date) untilDate = date.toISOString(); }
});
}
return () => { untilPicker?.destroy(); };
});

function close() {
if (saving) return;
open = false;
}

async function submit() {
if (!reservation) return;
saving = true;
error = '';
try {
await onSave(reservation.id, {
customer: { name: customerName, hotel: customerHotel },
reason,
reservedFrom: fromDate || undefined,
reservedUntil: untilDate || undefined
});
close();
} catch (e: any) {
error = e.message || 'Failed to update';
}
saving = false;
}
</script>

{#if open && reservation}
<div class="modal-overlay" onclick={close}>
<div class="modal-content" onclick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="edit-res-title">
<div class="modal-header">
<div class="modal-title">
<span class="material-symbols-rounded">event</span>
<h2 id="edit-res-title" class="md-headline-small">Edit Reservation</h2>
</div>
<md-icon-button onclick={close} aria-label="Close">
<span class="material-symbols-rounded">close</span>
</md-icon-button>
</div>
<div class="modal-body">
<div class="form-section">
<label class="form-label">
<span class="material-symbols-rounded">person</span>
<span class="md-title-small">Customer</span>
</label>
<div class="customer-form-grid">
<md-outlined-text-field label="Name" value={customerName} oninput={(e: Event) => customerName = (e.target as HTMLInputElement).value} disabled={loading || saving}>
<span class="material-symbols-rounded" slot="leading-icon">badge</span>
</md-outlined-text-field>
<md-outlined-text-field label="Hotel" value={customerHotel} oninput={(e: Event) => customerHotel = (e.target as HTMLInputElement).value} disabled={loading || saving}>
<span class="material-symbols-rounded" slot="leading-icon">hotel</span>
</md-outlined-text-field>
</div>
</div>
<div class="form-section">
<label class="form-label">
<span class="material-symbols-rounded">info</span>
<span class="md-title-small">Reason</span>
</label>
<md-outlined-text-field label="Reason" value={reason} oninput={(e: Event) => reason = (e.target as HTMLInputElement).value} disabled={loading || saving}></md-outlined-text-field>
</div>
<div class="form-section">
<label class="form-label">
<span class="material-symbols-rounded">calendar_today</span>
<span class="md-title-small">Reservation Period</span>
</label>
<div class="dates-grid">
<input type="text" class="flatpickr-input form-date-input" placeholder="From..." bind:this={fromInput} disabled={loading || saving} readonly />
<input type="text" class="flatpickr-input form-date-input" placeholder="Until..." bind:this={untilInput} disabled={loading || saving} readonly />
</div>
</div>
{#if error}
<div class="error-banner">
<span class="material-symbols-rounded">error</span>
<span class="md-body-medium">{error}</span>
</div>
{/if}
</div>
<div class="modal-footer">
<md-outlined-button onclick={close} disabled={saving}>Cancel</md-outlined-button>
<md-filled-button onclick={submit} disabled={saving}>
<span class="material-symbols-rounded" slot="icon">check</span>
Save Changes
</md-filled-button>
</div>
</div>
</div>
{/if}

<style>
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 100; padding: var(--md-sys-spacing-md); animation: fade-in 0.2s var(--md-sys-motion-easing-standard); }
.modal-content { background: var(--md-sys-color-surface); border-radius: var(--md-sys-shape-corner-extra-large); max-width: 500px; width: 100%; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; animation: scale-in 0.3s var(--md-sys-motion-easing-emphasized); }
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: var(--md-sys-spacing-lg); border-bottom: 1px solid var(--md-sys-color-outline-variant); flex-shrink: 0; }
.modal-title { display: flex; align-items: center; gap: var(--md-sys-spacing-md); }
.modal-title .material-symbols-rounded { font-size: 32px; color: var(--md-sys-color-primary); }
.modal-title h2 { margin: 0; color: var(--md-sys-color-on-surface); }
.modal-body { padding: var(--md-sys-spacing-lg); display: flex; flex-direction: column; gap: var(--md-sys-spacing-xl); overflow-y: auto; flex: 1; min-height: 0; }
.modal-footer { display: flex; justify-content: flex-end; gap: var(--md-sys-spacing-lg); padding: var(--md-sys-spacing-md) var(--md-sys-spacing-lg) var(--md-sys-spacing-lg); border-top: 1px solid var(--md-sys-color-outline-variant); flex-shrink: 0; }
.form-section { display: flex; flex-direction: column; gap: var(--md-sys-spacing-md); }
.form-label { display: flex; align-items: center; gap: var(--md-sys-spacing-sm); color: var(--md-sys-color-on-surface); font: var(--md-sys-typescale-title-small); }
.form-label .material-symbols-rounded { font-size: 24px; color: var(--md-sys-color-primary); flex-shrink: 0; }
.customer-form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--md-sys-spacing-md); }
.customer-form-grid md-outlined-text-field { width: 100%; }
.dates-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--md-sys-spacing-md); }
.form-date-input { width: 100%; padding: var(--md-sys-spacing-md); border: 1px solid var(--md-sys-color-outline); border-radius: var(--md-sys-shape-corner-small); background: var(--md-sys-color-surface); color: var(--md-sys-color-on-surface); font: var(--md-sys-typescale-body-large); cursor: pointer; }
.form-date-input:focus { outline: none; border-color: var(--md-sys-color-primary); border-width: 2px; }
.error-banner { display: flex; align-items: center; gap: var(--md-sys-spacing-sm); padding: var(--md-sys-spacing-md); background: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); border-radius: var(--md-sys-shape-corner-small); }
.error-banner .material-symbols-rounded { font-size: 20px; }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes scale-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
@media (max-width: 768px) {
.modal-content { max-width: 100%; max-height: 95vh; border-radius: var(--md-sys-shape-corner-large) var(--md-sys-shape-corner-large) 0 0; margin-top: auto; }
.modal-overlay { align-items: flex-end; padding: 0; }
.customer-form-grid, .dates-grid { grid-template-columns: 1fr; }
}
</style>
Loading
Loading