1+ import { useId } from "react" ;
12import { useT } from "../../lib/useT" ;
23import { useLabelStore } from "../../store/labelStore" ;
34import { inputCls , labelCls } from "../ui/formStyles" ;
45import type { PrinterProfile } from "../../types/PrinterProfile" ;
56import {
6- HEAD_CLEANING_INTERVAL_RANGE ,
7+ HEAD_CLEANING_INTERVAL_METERS ,
78 MAINTENANCE_ALERT_DEFAULTS ,
89 MAINTENANCE_ALERT_PRINT_VALUES ,
910 MAINTENANCE_ALERT_TYPES ,
1011 MAINTENANCE_ALERT_UNITS ,
11- MAINTENANCE_DISTANCE_MAX ,
12+ MAINTENANCE_DISTANCE_MAX_BY_TYPE ,
1213 MAINTENANCE_MESSAGE_MAX_LEN ,
14+ isHeadCleaningIntervalMeters ,
1315 isMaintenanceAlertPrint ,
1416 isMaintenanceAlertType ,
1517 isMaintenanceAlertUnit ,
18+ type HeadCleaningIntervalMeters ,
1619 type MaintenanceAlertType ,
1720 type MaintenanceAlertUnit ,
1821} from "../../types/PrinterProfile" ;
1922import {
2023 BoundedIntControl ,
2124 SafeStringInput ,
22- ZplBoundedIntInput ,
2325 ZplCommandLabel ,
2426 ZplEnumSelect ,
2527 ZplField ,
@@ -30,14 +32,14 @@ type LocMaintenance = ReturnType<typeof useT>["printerSettings"]["maintenance"];
3032type MaintenanceAlert = NonNullable < PrinterProfile [ "maintenanceAlert" ] > ;
3133
3234const ALERT_TYPE_LABEL_KEYS = {
33- H : 'alertTypeH' ,
3435 R : 'alertTypeR' ,
36+ C : 'alertTypeC' ,
3537} as const satisfies Record < MaintenanceAlertType , keyof LocMaintenance > ;
3638
3739const ALERT_UNIT_LABEL_KEYS = {
38- M : 'alertUnitsM' ,
39- I : 'alertUnitsI' ,
4040 C : 'alertUnitsC' ,
41+ I : 'alertUnitsI' ,
42+ M : 'alertUnitsM' ,
4143} as const satisfies Record < MaintenanceAlertUnit , keyof LocMaintenance > ;
4244
4345/** Maintenance tab (^MA/^MI/^MW/^JH). */
@@ -49,9 +51,7 @@ export function MaintenanceTab() {
4951 const alert = profile . maintenanceAlert ;
5052 const message = profile . maintenanceMessage ;
5153
52- // Cascade between maintenanceAlert.type and maintenanceMessage.type
53- // lives in patchPrinterProfile so UI call sites don't carry the
54- // invariant; this helper only owns the per-field merge.
54+ // Type-cascade lives in patchPrinterProfile; this helper only owns the merge.
5555 const updateAlert = < K extends keyof MaintenanceAlert > ( key : K , value : MaintenanceAlert [ K ] ) => {
5656 if ( ! alert ) return ;
5757 patchPrinterProfile ( { maintenanceAlert : { ...alert , [ key ] : value } } ) ;
@@ -61,109 +61,116 @@ export function MaintenanceTab() {
6161 < div className = "flex flex-col gap-4" >
6262 < ZplField >
6363 < ZplCommandLabel text = { loc . maintenanceAlertHeading } command = "^MA" />
64- < div className = "flex flex-col gap-2 pl-2 border-l border-border" >
65- < div className = "grid grid-cols-2 gap-2" >
66- < div className = "flex flex-col gap-1" >
67- < label className = { labelCls } > { loc . alertType } </ label >
64+ < ZplSubField label = { loc . alertType } >
65+ { ( id ) => (
66+ < select
67+ id = { id }
68+ className = { inputCls }
69+ value = { alert ?. type ?? "" }
70+ onChange = { ( e ) => {
71+ const raw = e . target . value ;
72+ if ( raw === "" ) {
73+ patchPrinterProfile ( { maintenanceAlert : undefined } ) ;
74+ return ;
75+ }
76+ if ( ! isMaintenanceAlertType ( raw ) ) return ;
77+ // First creation seeds defaults; later edits re-spread.
78+ // Threshold/frequency clamp to the new type's cap so a switch
79+ // R→C with stale 50000 doesn't trip the schema and snap back.
80+ const cap = MAINTENANCE_DISTANCE_MAX_BY_TYPE [ raw ] ;
81+ patchPrinterProfile ( {
82+ maintenanceAlert : alert
83+ ? {
84+ ...alert ,
85+ type : raw ,
86+ threshold : Math . min ( alert . threshold , cap ) ,
87+ frequency : Math . min ( alert . frequency , cap ) ,
88+ }
89+ : { ...MAINTENANCE_ALERT_DEFAULTS , type : raw } ,
90+ } ) ;
91+ } }
92+ >
93+ < option value = "" > { t . printerSettings . defaultOption } </ option >
94+ { MAINTENANCE_ALERT_TYPES . map ( ( v ) => (
95+ < option key = { v } value = { v } > { loc [ ALERT_TYPE_LABEL_KEYS [ v ] ] } </ option >
96+ ) ) }
97+ </ select >
98+ ) }
99+ </ ZplSubField >
100+ < div
101+ className = { `grid grid-cols-4 items-end gap-2 pl-2 mt-1 border-l-2 ${
102+ alert ? "border-accent/60" : "border-border/40"
103+ } `}
104+ >
105+ < ZplSubField label = { loc . alertPrint } >
106+ { ( id ) => (
68107 < select
108+ id = { id }
69109 className = { inputCls }
70- value = { alert ?. type ?? "" }
110+ value = { alert ?. print ?? "" }
71111 onChange = { ( e ) => {
72112 const raw = e . target . value ;
73- if ( raw === "" ) {
74- patchPrinterProfile ( { maintenanceAlert : undefined } ) ;
75- return ;
76- }
77- if ( ! isMaintenanceAlertType ( raw ) ) return ;
78- // First-time creation seeds defaults; subsequent edits
79- // re-spread the alert. The cross-field type cascade
80- // is enforced by patchPrinterProfile.
81- patchPrinterProfile ( {
82- maintenanceAlert : alert
83- ? { ...alert , type : raw }
84- : { ...MAINTENANCE_ALERT_DEFAULTS , type : raw } ,
85- } ) ;
113+ if ( isMaintenanceAlertPrint ( raw ) ) updateAlert ( "print" , raw ) ;
86114 } }
115+ disabled = { ! alert }
87116 >
88- < option value = "" > { t . printerSettings . defaultOption } </ option >
89- { MAINTENANCE_ALERT_TYPES . map ( ( v ) => (
90- < option key = { v } value = { v } > { loc [ ALERT_TYPE_LABEL_KEYS [ v ] ] } </ option >
117+ { /* placeholder shown only while the alert is undefined */ }
118+ < option value = "" disabled hidden > </ option >
119+ { MAINTENANCE_ALERT_PRINT_VALUES . map ( ( v ) => (
120+ < option key = { v } value = { v } > { v === "Y" ? loc . alertPrintY : loc . alertPrintN } </ option >
91121 ) ) }
92122 </ select >
93- </ div >
94- < ZplSubField label = { loc . alertPrint } >
95- { ( id ) => (
96- < select
97- id = { id }
98- className = { inputCls }
99- value = { alert ?. print ?? MAINTENANCE_ALERT_DEFAULTS . print }
100- onChange = { ( e ) => {
101- const raw = e . target . value ;
102- if ( isMaintenanceAlertPrint ( raw ) ) updateAlert ( "print" , raw ) ;
103- } }
104- disabled = { ! alert }
105- >
106- { MAINTENANCE_ALERT_PRINT_VALUES . map ( ( v ) => (
107- < option key = { v } value = { v } > { v === "Y" ? loc . alertPrintY : loc . alertPrintN } </ option >
108- ) ) }
109- </ select >
110- ) }
111- </ ZplSubField >
112- </ div >
113- < div className = "grid grid-cols-3 gap-2" >
114- < ZplSubField label = { loc . alertThreshold } >
115- { ( id ) => (
116- < BoundedIntControl
117- id = { id }
118- min = { 0 }
119- max = { MAINTENANCE_DISTANCE_MAX }
120- value = { alert ?. threshold }
121- disabled = { ! alert }
122- onChange = { ( threshold ) => {
123- // Drop undefined (mid-typing empty); blur clamps stale state.
124- if ( threshold !== undefined ) updateAlert ( "threshold" , threshold ) ;
125- } }
126- />
127- ) }
128- </ ZplSubField >
129- < ZplSubField label = { loc . alertFrequency } >
130- { ( id ) => (
131- < BoundedIntControl
132- id = { id }
133- min = { 1 }
134- max = { MAINTENANCE_DISTANCE_MAX }
135- value = { alert ?. frequency }
136- disabled = { ! alert }
137- onChange = { ( frequency ) => {
138- // readBoundedInt lets sub-min positives through so a
139- // user typing "1" toward "12" survives a min=2 field;
140- // schema requires >= min, so gate the commit here.
141- if ( frequency !== undefined && frequency >= 1 ) {
142- updateAlert ( "frequency" , frequency ) ;
143- }
144- } }
145- />
146- ) }
147- </ ZplSubField >
148- < ZplSubField label = { loc . alertUnits } >
149- { ( id ) => (
150- < select
151- id = { id }
152- className = { inputCls }
153- value = { alert ?. units ?? MAINTENANCE_ALERT_DEFAULTS . units }
154- onChange = { ( e ) => {
155- const raw = e . target . value ;
156- if ( isMaintenanceAlertUnit ( raw ) ) updateAlert ( "units" , raw ) ;
157- } }
158- disabled = { ! alert }
159- >
160- { MAINTENANCE_ALERT_UNITS . map ( ( v ) => (
161- < option key = { v } value = { v } > { loc [ ALERT_UNIT_LABEL_KEYS [ v ] ] } </ option >
162- ) ) }
163- </ select >
164- ) }
165- </ ZplSubField >
166- </ div >
123+ ) }
124+ </ ZplSubField >
125+ < ZplSubField label = { loc . alertThreshold } >
126+ { ( id ) => (
127+ < BoundedIntControl
128+ id = { id }
129+ min = { 0 }
130+ max = { MAINTENANCE_DISTANCE_MAX_BY_TYPE [ alert ?. type ?? MAINTENANCE_ALERT_DEFAULTS . type ] }
131+ value = { alert ?. threshold }
132+ disabled = { ! alert }
133+ required = { ! ! alert }
134+ onChange = { ( threshold ) => {
135+ if ( threshold !== undefined ) updateAlert ( "threshold" , threshold ) ;
136+ } }
137+ />
138+ ) }
139+ </ ZplSubField >
140+ < ZplSubField label = { loc . alertFrequency } >
141+ { ( id ) => (
142+ < BoundedIntControl
143+ id = { id }
144+ min = { 0 }
145+ max = { MAINTENANCE_DISTANCE_MAX_BY_TYPE [ alert ?. type ?? MAINTENANCE_ALERT_DEFAULTS . type ] }
146+ value = { alert ?. frequency }
147+ disabled = { ! alert }
148+ required = { ! ! alert }
149+ onChange = { ( frequency ) => {
150+ if ( frequency !== undefined ) updateAlert ( "frequency" , frequency ) ;
151+ } }
152+ />
153+ ) }
154+ </ ZplSubField >
155+ < ZplSubField label = { loc . alertUnits } >
156+ { ( id ) => (
157+ < select
158+ id = { id }
159+ className = { inputCls }
160+ value = { alert ?. units ?? "" }
161+ onChange = { ( e ) => {
162+ const raw = e . target . value ;
163+ if ( isMaintenanceAlertUnit ( raw ) ) updateAlert ( "units" , raw ) ;
164+ } }
165+ disabled = { ! alert }
166+ >
167+ < option value = "" disabled hidden > </ option >
168+ { MAINTENANCE_ALERT_UNITS . map ( ( v ) => (
169+ < option key = { v } value = { v } > { loc [ ALERT_UNIT_LABEL_KEYS [ v ] ] } </ option >
170+ ) ) }
171+ </ select >
172+ ) }
173+ </ ZplSubField >
167174 </ div >
168175 </ ZplField >
169176
@@ -209,16 +216,14 @@ export function MaintenanceTab() {
209216 { loc . earlyWarningGateHint }
210217 </ span >
211218
212- < ZplBoundedIntInput
219+ < HeadCleaningIntervalSelect
213220 label = { loc . headCleaningIntervalMeters }
214- command = "^JH"
215- min = { HEAD_CLEANING_INTERVAL_RANGE . min }
216- max = { HEAD_CLEANING_INTERVAL_RANGE . max }
221+ defaultLabel = { t . printerSettings . defaultOption }
222+ unit = { loc . alertUnitsM }
217223 value = { profile . headCleaningIntervalMeters }
218224 onChange = { ( headCleaningIntervalMeters ) =>
219225 patchPrinterProfile ( { headCleaningIntervalMeters } )
220226 }
221- unit = { loc . alertUnitsM }
222227 />
223228 < span className = { `${ labelCls } normal-case tracking-normal text-muted/70 -mt-2` } >
224229 { loc . headCleaningIntervalHint }
@@ -238,3 +243,42 @@ export function MaintenanceTab() {
238243 ) ;
239244}
240245
246+ function HeadCleaningIntervalSelect ( {
247+ label,
248+ defaultLabel,
249+ unit,
250+ value,
251+ onChange,
252+ } : {
253+ label : string ;
254+ defaultLabel : string ;
255+ unit : string ;
256+ value : HeadCleaningIntervalMeters | undefined ;
257+ onChange : ( next : HeadCleaningIntervalMeters | undefined ) => void ;
258+ } ) {
259+ const id = useId ( ) ;
260+ return (
261+ < ZplField >
262+ < ZplCommandLabel text = { label } command = "^JH" htmlFor = { id } />
263+ < div className = "flex items-center gap-2" >
264+ < select
265+ id = { id }
266+ className = { inputCls }
267+ value = { value ?? "" }
268+ onChange = { ( e ) => {
269+ const raw = e . target . value ;
270+ if ( raw === "" ) { onChange ( undefined ) ; return ; }
271+ const n = Number ( raw ) ;
272+ if ( isHeadCleaningIntervalMeters ( n ) ) onChange ( n ) ;
273+ } }
274+ >
275+ < option value = "" > { defaultLabel } </ option >
276+ { HEAD_CLEANING_INTERVAL_METERS . map ( ( m ) => (
277+ < option key = { m } value = { m } > { m } </ option >
278+ ) ) }
279+ </ select >
280+ < span className = { labelCls } > { unit } </ span >
281+ </ div >
282+ </ ZplField >
283+ ) ;
284+ }
0 commit comments