11import { useState } from 'react'
2+ import { assignDeviceTable , type OrderingContext } from '../lib/api'
3+ import TabletLayout from './TabletLayout'
24
35type Step = 'pin' | 'table'
46
57const MIN_PIN_LENGTH = 4
68const MAX_PIN_LENGTH = 6
79
10+ // Same storage keys useDeviceBootstrap.ts uses for a provisioned device's
11+ // credentials — a portable/shared tablet is still a provisioned
12+ // URY Ordering Device (device_type "Portable Tablet", table_mode
13+ // "Selectable"), it just can't bootstrap straight into an ordering context
14+ // the way a fixed-table/kiosk device does, because it has no table until
15+ // staff assign one here.
16+ const DEVICE_ID_KEY = 'ury_device_id'
17+ const DEVICE_CREDENTIAL_KEY = 'ury_device_credential'
18+
819/**
920 * Standalone screen for the "portable/shared tablet" scenario: staff enters
10- * a PIN and picks a table before handing the tablet to a customer.
21+ * a PIN and picks a table before handing the tablet to a customer. Calls
22+ * the real `assign_device_table` backend endpoint (device credential +
23+ * staff PIN + table -> a bound ordering session), then hands off straight
24+ * into the normal ordering view via `TabletLayout`'s `initialContext` prop
25+ * — the same pattern `App.tsx` already uses for device-bootstrapped
26+ * sessions, so this screen never re-bootstraps once assigned.
1127 *
12- * There is currently NO backend endpoint for "staff PIN + table selection
13- * -> customer session" (only QR-token and device-credential bootstrap
14- * exist — see `get_ordering_context` in
15- * ury/ury/ury/api/self_ordering.py). This is therefore a UI shell only:
16- * any PIN of MIN_PIN_LENGTH+ digits is treated as "accepted" (no real
17- * verification), and there is no customer-safe "list all tables" endpoint
18- * either, so the table picker is a free-text field rather than a real
19- * picker. Submitting does not create a session — it surfaces the gap
20- * instead of silently pretending to succeed.
28+ * There is no customer-safe "list tables for this branch" endpoint yet, so
29+ * the table picker stays a free-text field (table name/number) rather than
30+ * a real picker — building a new tables-listing endpoint was out of scope
31+ * for this MVP pass. The backend still validates the table exists and
32+ * belongs to the device's branch, so a bad value is rejected, not silently
33+ * accepted.
2134 */
2235function PortableTabletAssignment ( ) {
2336 const [ step , setStep ] = useState < Step > ( 'pin' )
2437 const [ pin , setPin ] = useState ( '' )
2538 const [ table , setTable ] = useState ( '' )
39+ const [ submitting , setSubmitting ] = useState ( false )
40+ const [ error , setError ] = useState < string | null > ( null )
41+ const [ context , setContext ] = useState < OrderingContext | null > ( null )
2642
2743 function handlePinDigit ( digit : string ) {
2844 if ( pin . length >= MAX_PIN_LENGTH ) return
@@ -35,23 +51,50 @@ function PortableTabletAssignment() {
3551
3652 function handlePinSubmit ( ) {
3753 if ( pin . length < MIN_PIN_LENGTH ) return
38- // UI shell only — any PIN of sufficient length is treated as accepted.
39- // There is no real staff-PIN verification endpoint yet.
54+ // The PIN itself is only verified server-side, at assignment time —
55+ // this just advances to table entry once enough digits are entered.
56+ setError ( null )
4057 setStep ( 'table' )
4158 }
4259
43- function handleAssign ( ) {
44- if ( ! table . trim ( ) ) return
45- // TODO(backend): needs a staff-PIN + table-selection endpoint before
46- // this can create a real session. Until then, this screen cannot hand
47- // off a working session to a customer.
48- window . alert ( 'Backend endpoint not yet available — see TODO in this file' )
60+ async function handleAssign ( ) {
61+ if ( ! table . trim ( ) || submitting ) return
62+
63+ const deviceId = localStorage . getItem ( DEVICE_ID_KEY )
64+ const deviceCredential = localStorage . getItem ( DEVICE_CREDENTIAL_KEY )
65+ if ( ! deviceId || ! deviceCredential ) {
66+ setError ( 'This tablet is not provisioned. Please contact staff.' )
67+ return
68+ }
69+
70+ setSubmitting ( true )
71+ setError ( null )
72+ try {
73+ const assigned = await assignDeviceTable ( deviceId , deviceCredential , pin , table . trim ( ) )
74+ setContext ( assigned )
75+ } catch ( err ) {
76+ setError ( err instanceof Error ? err . message : 'Could not assign this table. Please check the PIN and try again.' )
77+ setPin ( '' )
78+ setStep ( 'pin' )
79+ } finally {
80+ setSubmitting ( false )
81+ }
82+ }
83+
84+ if ( context ) {
85+ return < TabletLayout initialContext = { context } />
4986 }
5087
5188 return (
5289 < div className = "flex min-h-screen flex-col items-center justify-center gap-6 p-6" >
5390 < h1 className = "text-xl font-semibold" > Assign This Tablet</ h1 >
5491
92+ { error && (
93+ < div className = "w-full max-w-xs rounded-md bg-destructive/10 p-3 text-center text-sm text-destructive" >
94+ { error }
95+ </ div >
96+ ) }
97+
5598 { step === 'pin' && (
5699 < div className = "flex w-full max-w-xs flex-col items-center gap-4" >
57100 < p className = "text-sm text-muted-foreground" > Enter staff PIN</ p >
@@ -103,13 +146,14 @@ function PortableTabletAssignment() {
103146 />
104147 < button
105148 onClick = { handleAssign }
106- disabled = { ! table . trim ( ) }
149+ disabled = { ! table . trim ( ) || submitting }
107150 className = "w-full rounded-md bg-primary py-3 font-medium text-primary-foreground disabled:opacity-50"
108151 >
109- Assign Table
152+ { submitting ? 'Assigning…' : ' Assign Table' }
110153 </ button >
111154 < button
112155 onClick = { ( ) => setStep ( 'pin' ) }
156+ disabled = { submitting }
113157 className = "w-full rounded-md border py-3 text-sm font-medium"
114158 >
115159 Back
0 commit comments