@@ -3,6 +3,7 @@ use uuid::Uuid;
33
44use crate :: i18n:: I18n ;
55use crate :: models:: * ;
6+ use crate :: pages:: keyboard:: OnScreenKeyboard ;
67use crate :: server_fns:: * ;
78
89#[ cfg( not( target_arch = "wasm32" ) ) ]
@@ -90,6 +91,41 @@ pub fn ItemsPage() -> impl IntoView {
9091 let ( stock_quantity, set_stock_quantity) = signal ( String :: new ( ) ) ;
9192 let ( kitchen_item, set_kitchen_item) = signal ( false ) ;
9293
94+ // On-screen keyboard target: "name", "description" or "sku" (hidden on mobile via CSS)
95+ let ( kb_target, set_kb_target) = signal ( Option :: < String > :: None ) ;
96+ let ( kb_shift, set_kb_shift) = signal ( false ) ;
97+
98+ let on_kb_key = move |key : String | {
99+ let Some ( target) = kb_target. get ( ) else { return } ;
100+ let numeric = target == "price" ;
101+ let setter = match target. as_str ( ) {
102+ "name" => set_name,
103+ "description" => set_description,
104+ "sku" => set_sku,
105+ "price" => set_price,
106+ _ => return ,
107+ } ;
108+ match key. as_str ( ) {
109+ "Backspace" => { setter. update ( |s| { s. pop ( ) ; } ) ; }
110+ "Enter" => { set_kb_target. set ( None ) ; }
111+ "Shift" => { set_kb_shift. update ( |s| * s = !* s) ; }
112+ "Space" => { if !numeric { setter. update ( |s| s. push ( ' ' ) ) ; } }
113+ ch => {
114+ if numeric {
115+ // Price accepts digits and a single decimal point only
116+ if ch. chars ( ) . all ( |c| c. is_ascii_digit ( ) ) {
117+ setter. update ( |s| s. push_str ( ch) ) ;
118+ } else if ch == "." || ch == "," {
119+ setter. update ( |s| if !s. contains ( '.' ) { s. push ( '.' ) ; } ) ;
120+ }
121+ } else {
122+ let ch = if kb_shift. get ( ) { ch. to_uppercase ( ) } else { ch. to_lowercase ( ) } ;
123+ setter. update ( |s| s. push_str ( & ch) ) ;
124+ }
125+ }
126+ }
127+ } ;
128+
93129 let ( reload, set_reload) = signal ( 0u32 ) ;
94130
95131 Effect :: new ( move || {
@@ -101,6 +137,7 @@ pub fn ItemsPage() -> impl IntoView {
101137 } ) ;
102138
103139 let start_edit = move |item : Item | {
140+ set_kb_target. set ( None ) ;
104141 set_name. set ( item. name . clone ( ) ) ;
105142 set_description. set ( item. description . clone ( ) . unwrap_or_default ( ) ) ;
106143 set_price. set ( item. price . to_string ( ) ) ;
@@ -180,6 +217,7 @@ pub fn ItemsPage() -> impl IntoView {
180217 } ;
181218 let cancel_delete = move |_| { set_deleting_item. set ( None ) ; } ;
182219 let cancel_edit = move |_| {
220+ set_kb_target. set ( None ) ;
183221 set_editing_item. set ( None ) ; set_creating_item. set ( false ) ;
184222 set_name. set ( String :: new ( ) ) ; set_description. set ( String :: new ( ) ) ;
185223 set_price. set ( String :: new ( ) ) ; set_category_id. set ( String :: new ( ) ) ;
@@ -188,6 +226,7 @@ pub fn ItemsPage() -> impl IntoView {
188226 set_stock_quantity. set ( String :: new ( ) ) ; set_kitchen_item. set ( false ) ;
189227 } ;
190228 let start_create = move |_| {
229+ set_kb_target. set ( None ) ;
191230 set_name. set ( String :: new ( ) ) ; set_description. set ( String :: new ( ) ) ;
192231 set_price. set ( String :: new ( ) ) ;
193232 set_category_id. set ( if let Some ( cat) = categories. get ( ) . first ( ) { cat. id . to_string ( ) } else { String :: new ( ) } ) ;
@@ -239,11 +278,23 @@ pub fn ItemsPage() -> impl IntoView {
239278 <div class="form-grid" >
240279 <div class="form-group" >
241280 <label>{ move || i18n. get( ) . t( "general.name" ) } </label>
242- <input type ="text" value=move || name. get( ) on: input=move |ev| set_name. set( event_target_value( & ev) ) />
281+ <div class="admin-input-row" >
282+ <input type ="text" value=move || name. get( ) on: input=move |ev| set_name. set( event_target_value( & ev) ) />
283+ <button class="btn-secondary-small" type ="button" on: click=move |_| {
284+ if kb_target. get( ) == Some ( "name" . into( ) ) { set_kb_target. set( None ) ; }
285+ else { set_kb_target. set( Some ( "name" . into( ) ) ) ; set_kb_shift. set( false ) ; }
286+ } >{ move || if kb_target. get( ) == Some ( "name" . into( ) ) { i18n. get( ) . t( "admin.hide_kb" ) } else { i18n. get( ) . t( "admin.keyboard" ) } } </button>
287+ </div>
243288 </div>
244289 <div class="form-group" >
245290 <label>{ move || i18n. get( ) . t( "items.price" ) } </label>
246- <input type ="number" step="0.01" value=move || price. get( ) on: input=move |ev| set_price. set( event_target_value( & ev) ) />
291+ <div class="admin-input-row" >
292+ <input type ="text" inputmode="decimal" value=move || price. get( ) on: input=move |ev| set_price. set( event_target_value( & ev) ) />
293+ <button class="btn-secondary-small" type ="button" on: click=move |_| {
294+ if kb_target. get( ) == Some ( "price" . into( ) ) { set_kb_target. set( None ) ; }
295+ else { set_kb_target. set( Some ( "price" . into( ) ) ) ; set_kb_shift. set( false ) ; }
296+ } >{ move || if kb_target. get( ) == Some ( "price" . into( ) ) { i18n. get( ) . t( "admin.hide_kb" ) } else { i18n. get( ) . t( "admin.keyboard" ) } } </button>
297+ </div>
247298 </div>
248299 <div class="form-group" >
249300 <label>{ move || i18n. get( ) . t( "items.category" ) } </label>
@@ -255,11 +306,23 @@ pub fn ItemsPage() -> impl IntoView {
255306 </div>
256307 <div class="form-group" >
257308 <label>{ move || i18n. get( ) . t( "items.sku" ) } </label>
258- <input type ="text" value=move || sku. get( ) on: input=move |ev| set_sku. set( event_target_value( & ev) ) />
309+ <div class="admin-input-row" >
310+ <input type ="text" value=move || sku. get( ) on: input=move |ev| set_sku. set( event_target_value( & ev) ) />
311+ <button class="btn-secondary-small" type ="button" on: click=move |_| {
312+ if kb_target. get( ) == Some ( "sku" . into( ) ) { set_kb_target. set( None ) ; }
313+ else { set_kb_target. set( Some ( "sku" . into( ) ) ) ; set_kb_shift. set( false ) ; }
314+ } >{ move || if kb_target. get( ) == Some ( "sku" . into( ) ) { i18n. get( ) . t( "admin.hide_kb" ) } else { i18n. get( ) . t( "admin.keyboard" ) } } </button>
315+ </div>
259316 </div>
260317 <div class="form-group" >
261318 <label>{ move || i18n. get( ) . t( "general.description" ) } </label>
262- <input type ="text" value=move || description. get( ) on: input=move |ev| set_description. set( event_target_value( & ev) ) />
319+ <div class="admin-input-row" >
320+ <input type ="text" value=move || description. get( ) on: input=move |ev| set_description. set( event_target_value( & ev) ) />
321+ <button class="btn-secondary-small" type ="button" on: click=move |_| {
322+ if kb_target. get( ) == Some ( "description" . into( ) ) { set_kb_target. set( None ) ; }
323+ else { set_kb_target. set( Some ( "description" . into( ) ) ) ; set_kb_shift. set( false ) ; }
324+ } >{ move || if kb_target. get( ) == Some ( "description" . into( ) ) { i18n. get( ) . t( "admin.hide_kb" ) } else { i18n. get( ) . t( "admin.keyboard" ) } } </button>
325+ </div>
263326 </div>
264327 <div class="form-group" >
265328 <label>
@@ -296,6 +359,9 @@ pub fn ItemsPage() -> impl IntoView {
296359 </Show >
297360 </div>
298361 </div>
362+ <Show when=move || kb_target. get( ) . is_some( ) fallback=|| ( ) >
363+ <OnScreenKeyboard on_key=on_kb_key shift=kb_shift i18n=i18n />
364+ </Show >
299365 <div class="form-actions" >
300366 <button class="btn-success" on: click=save_item>{ move || i18n. get( ) . t( "general.save" ) } </button>
301367 <button class="btn-secondary" on: click=cancel_edit>{ move || i18n. get( ) . t( "general.cancel" ) } </button>
0 commit comments