@licklist/design 0.78.5-dev.55 → 0.78.5-dev.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/v2/components/Button/Button.d.ts +2 -1
- package/dist/v2/components/Button/Button.d.ts.map +1 -1
- package/dist/v2/components/Button/Button.js +12 -6
- package/dist/v2/components/Button/Button.scss.js +1 -1
- package/dist/v2/styles/components/Button.scss +10 -0
- package/package.json +1 -1
- package/src/v2/components/Button/Button.tsx +5 -2
- package/src/v2/components/Customer/CustomerDetail.scss +7 -3
- package/src/v2/styles/components/Button.scss +10 -0
- package/dist/v2/components/Customer/CustomerCreate/CustomerCreate.d.ts +0 -11
- package/dist/v2/components/Customer/CustomerCreate/CustomerCreate.d.ts.map +0 -1
- package/dist/v2/components/Customer/CustomerCreate/index.d.ts +0 -2
- package/dist/v2/components/Customer/CustomerCreate/index.d.ts.map +0 -1
- package/dist/v2/components/Customer/CustomerDetail/CustomerDetail.d.ts +0 -35
- package/dist/v2/components/Customer/CustomerDetail/CustomerDetail.d.ts.map +0 -1
- package/dist/v2/components/Customer/CustomerDetail/index.d.ts +0 -2
- package/dist/v2/components/Customer/CustomerDetail/index.d.ts.map +0 -1
- package/dist/v2/components/Customer/CustomerEdit/CustomerEdit.d.ts +0 -11
- package/dist/v2/components/Customer/CustomerEdit/CustomerEdit.d.ts.map +0 -1
- package/dist/v2/components/Customer/CustomerEdit/index.d.ts +0 -2
- package/dist/v2/components/Customer/CustomerEdit/index.d.ts.map +0 -1
- package/dist/v2/components/Customer/CustomerForm/CustomerForm.d.ts +0 -22
- package/dist/v2/components/Customer/CustomerForm/CustomerForm.d.ts.map +0 -1
- package/dist/v2/components/Customer/CustomerForm/index.d.ts +0 -2
- package/dist/v2/components/Customer/CustomerForm/index.d.ts.map +0 -1
- package/dist/v2/components/Customer/CustomersList.d.ts +0 -37
- package/dist/v2/components/Customer/CustomersList.d.ts.map +0 -1
- package/dist/v2/components/Customer/index.d.ts +0 -6
- package/dist/v2/components/Customer/index.d.ts.map +0 -1
- package/src/v2/components/Customer/CustomerCreate/CustomerCreate.tsx +0 -36
- package/src/v2/components/Customer/CustomerCreate/index.ts +0 -1
- package/src/v2/components/Customer/CustomerDetail/CustomerDetail.scss +0 -315
- package/src/v2/components/Customer/CustomerDetail/CustomerDetail.tsx +0 -161
- package/src/v2/components/Customer/CustomerDetail/index.ts +0 -1
- package/src/v2/components/Customer/CustomerEdit/CustomerEdit.tsx +0 -37
- package/src/v2/components/Customer/CustomerEdit/index.ts +0 -1
- package/src/v2/components/Customer/CustomerForm/CustomerForm.tsx +0 -434
- package/src/v2/components/Customer/CustomerForm/index.ts +0 -1
- package/src/v2/components/Customer/CustomersList.tsx +0 -193
- package/src/v2/components/Customer/index.ts +0 -5
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
2
|
-
import { showAlert } from '@licklist/plugins/dist/context/app/AlertContext'
|
|
3
|
-
import { NewInput } from '../../NewInput'
|
|
4
|
-
import { Select } from '../../Select'
|
|
5
|
-
import { Checkbox } from '../../Checkbox'
|
|
6
|
-
import { Button } from '../../Button'
|
|
7
|
-
import {useTranslation} from "react-i18next";
|
|
8
|
-
|
|
9
|
-
export interface CustomerData {
|
|
10
|
-
firstName: string
|
|
11
|
-
lastName: string
|
|
12
|
-
email: string
|
|
13
|
-
dobDay: string
|
|
14
|
-
dobMonth: string
|
|
15
|
-
dobYear: string
|
|
16
|
-
phone: string
|
|
17
|
-
optIn: boolean
|
|
18
|
-
postcode: string
|
|
19
|
-
gender: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface CustomerFormProps {
|
|
23
|
-
onSave: (data: CustomerData) => void
|
|
24
|
-
initialData?: Partial<CustomerData>
|
|
25
|
-
isLoading?: boolean
|
|
26
|
-
submitButtonLabel?: string
|
|
27
|
-
isEditing?: boolean
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const CustomerForm: React.FC<CustomerFormProps> = ({
|
|
31
|
-
onSave,
|
|
32
|
-
initialData,
|
|
33
|
-
isLoading,
|
|
34
|
-
submitButtonLabel = 'Save Changes',
|
|
35
|
-
isEditing = false,
|
|
36
|
-
}) => {
|
|
37
|
-
const STORAGE_KEY = 'customer_form_draft'
|
|
38
|
-
|
|
39
|
-
const [formData, setFormData] = useState<CustomerData>(() => {
|
|
40
|
-
// Try to restore from sessionStorage first
|
|
41
|
-
try {
|
|
42
|
-
const savedData = sessionStorage.getItem(STORAGE_KEY)
|
|
43
|
-
if (savedData) {
|
|
44
|
-
const parsed = JSON.parse(savedData)
|
|
45
|
-
// If we have initialData (editing), prefer that over saved draft
|
|
46
|
-
if (initialData?.email) {
|
|
47
|
-
sessionStorage.removeItem(STORAGE_KEY) // Clear draft when editing
|
|
48
|
-
return {
|
|
49
|
-
firstName: '',
|
|
50
|
-
lastName: '',
|
|
51
|
-
email: '',
|
|
52
|
-
dobDay: '',
|
|
53
|
-
dobMonth: '',
|
|
54
|
-
dobYear: '',
|
|
55
|
-
phone: '',
|
|
56
|
-
optIn: false,
|
|
57
|
-
postcode: '',
|
|
58
|
-
gender: '',
|
|
59
|
-
...initialData,
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return parsed
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error('Failed to restore form data:', error)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
firstName: '',
|
|
70
|
-
lastName: '',
|
|
71
|
-
email: '',
|
|
72
|
-
dobDay: '',
|
|
73
|
-
dobMonth: '',
|
|
74
|
-
dobYear: '',
|
|
75
|
-
phone: '',
|
|
76
|
-
optIn: false,
|
|
77
|
-
postcode: '',
|
|
78
|
-
gender: '',
|
|
79
|
-
...initialData,
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const [errors, setErrors] = useState<Partial<Record<keyof CustomerData, string>>>({})
|
|
84
|
-
const { t } = useTranslation(['App'])
|
|
85
|
-
|
|
86
|
-
// Save to sessionStorage whenever form data changes (but not when editing)
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
if (!initialData?.email) { // Only save drafts for new customers
|
|
89
|
-
try {
|
|
90
|
-
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(formData))
|
|
91
|
-
} catch (error) {
|
|
92
|
-
console.error('Failed to save form data:', error)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}, [formData, initialData])
|
|
96
|
-
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (initialData) {
|
|
99
|
-
setFormData((prev) => ({ ...prev, ...initialData }))
|
|
100
|
-
}
|
|
101
|
-
}, [initialData])
|
|
102
|
-
|
|
103
|
-
// Clear sessionStorage when component unmounts successfully (form submitted)
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
return () => {
|
|
106
|
-
// Only clear if we're not in the middle of validation error
|
|
107
|
-
if (Object.keys(errors).length === 0) {
|
|
108
|
-
sessionStorage.removeItem(STORAGE_KEY)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}, [errors])
|
|
112
|
-
|
|
113
|
-
const handleChange = (
|
|
114
|
-
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
|
|
115
|
-
) => {
|
|
116
|
-
const { name, value, type } = e.target
|
|
117
|
-
const checked = (e.target as HTMLInputElement).checked
|
|
118
|
-
|
|
119
|
-
setFormData((prev) => ({
|
|
120
|
-
...prev,
|
|
121
|
-
[name]: type === 'checkbox' ? checked : value,
|
|
122
|
-
}))
|
|
123
|
-
|
|
124
|
-
// Clear error when user types
|
|
125
|
-
if (errors[name as keyof CustomerData]) {
|
|
126
|
-
setErrors((prev) => {
|
|
127
|
-
const newErrors = { ...prev }
|
|
128
|
-
delete newErrors[name as keyof CustomerData]
|
|
129
|
-
return newErrors
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const handleSelectChange = (e: React.FormEvent<HTMLSelectElement>) => {
|
|
135
|
-
const { name, value } = e.currentTarget
|
|
136
|
-
setFormData((prev) => ({
|
|
137
|
-
...prev,
|
|
138
|
-
[name]: value,
|
|
139
|
-
}))
|
|
140
|
-
|
|
141
|
-
// Clear error when user selects
|
|
142
|
-
if (errors[name as keyof CustomerData]) {
|
|
143
|
-
setErrors((prev) => {
|
|
144
|
-
const newErrors = { ...prev }
|
|
145
|
-
delete newErrors[name as keyof CustomerData]
|
|
146
|
-
return newErrors
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const validate = () => {
|
|
152
|
-
const fieldErrors: Partial<Record<keyof CustomerData, string>> = {}
|
|
153
|
-
const messages: string[] = []
|
|
154
|
-
|
|
155
|
-
if (!formData.firstName.trim()) {
|
|
156
|
-
fieldErrors.firstName = ' '
|
|
157
|
-
//messages.push('First Name is required')
|
|
158
|
-
messages.push(t('Validation:fieldRequired', { attribute: t('App:firstName') }))
|
|
159
|
-
}
|
|
160
|
-
if (!formData.lastName.trim()) {
|
|
161
|
-
fieldErrors.lastName = ' '
|
|
162
|
-
messages.push(t('Validation:fieldRequired', { attribute: t('App:lastName') }))
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Skip email validation when editing (field is readonly)
|
|
166
|
-
if (!isEditing) {
|
|
167
|
-
if (!formData.email.trim()) {
|
|
168
|
-
fieldErrors.email = ' '
|
|
169
|
-
messages.push(t('Validation:fieldRequired', { attribute: t('App:emailAddress') }))
|
|
170
|
-
} else {
|
|
171
|
-
// Basic email format check
|
|
172
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
173
|
-
if (!emailRegex.test(formData.email)) {
|
|
174
|
-
fieldErrors.email = ' '
|
|
175
|
-
messages.push(t('App:emailAddressInvalid'))
|
|
176
|
-
} else {
|
|
177
|
-
// Additional validation for domain quality
|
|
178
|
-
const parts = formData.email.split('@')
|
|
179
|
-
if (parts.length === 2) {
|
|
180
|
-
const domain = parts[1]
|
|
181
|
-
|
|
182
|
-
// Check for valid TLD (at least 2 characters after last dot - allows .it, .co, etc.)
|
|
183
|
-
const domainParts = domain.split('.')
|
|
184
|
-
const tld = domainParts[domainParts.length - 1]
|
|
185
|
-
|
|
186
|
-
// TLD must be at least 2 characters (allows .it, .co) but rejects single char like .c
|
|
187
|
-
if (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld)) {
|
|
188
|
-
fieldErrors.email = ' '
|
|
189
|
-
messages.push(t('App:emailAddressInvalid'))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Check for consecutive dots or dots at wrong positions
|
|
193
|
-
if (domain.includes('..') || domain.startsWith('.') || domain.endsWith('.')) {
|
|
194
|
-
fieldErrors.email = ' '
|
|
195
|
-
messages.push(t('App:emailAddressInvalid'))
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Optional Date of Birth validation
|
|
203
|
-
const day = formData.dobDay?.trim()
|
|
204
|
-
const month = formData.dobMonth?.trim()
|
|
205
|
-
const year = formData.dobYear?.trim()
|
|
206
|
-
const anyDobPart = !!(day || month || year)
|
|
207
|
-
|
|
208
|
-
if (anyDobPart) {
|
|
209
|
-
t('App:dateOfBirth', 'Date of Birth');
|
|
210
|
-
const dayLabel = t('App:day', 'Day')
|
|
211
|
-
const monthLabel = t('App:month', 'Month')
|
|
212
|
-
const yearLabel = t('App:year', 'Year')
|
|
213
|
-
|
|
214
|
-
// Require all three parts if any is provided
|
|
215
|
-
if (!day) {
|
|
216
|
-
fieldErrors.dobDay = ' '
|
|
217
|
-
}
|
|
218
|
-
if (!month) {
|
|
219
|
-
fieldErrors.dobMonth = ' '
|
|
220
|
-
}
|
|
221
|
-
if (!year) {
|
|
222
|
-
fieldErrors.dobYear = ' '
|
|
223
|
-
}
|
|
224
|
-
if (!day || !month || !year) {
|
|
225
|
-
messages.push(t('Validation:fieldValidDate'))
|
|
226
|
-
} else {
|
|
227
|
-
// Numeric only checks
|
|
228
|
-
const numOnly = /^\d+$/
|
|
229
|
-
if (!numOnly.test(day)) {
|
|
230
|
-
fieldErrors.dobDay = ' '
|
|
231
|
-
messages.push(t('Validation:fieldOnlyNumbers', { attribute: dayLabel }))
|
|
232
|
-
}
|
|
233
|
-
if (!numOnly.test(month)) {
|
|
234
|
-
fieldErrors.dobMonth = ' '
|
|
235
|
-
messages.push(t('Validation:fieldOnlyNumbers', { attribute: monthLabel }))
|
|
236
|
-
}
|
|
237
|
-
if (!/^\d{4}$/.test(year)) {
|
|
238
|
-
fieldErrors.dobYear = ' '
|
|
239
|
-
// Prefer explicit invalid year formatting over generic numbers message to enforce 4 digits
|
|
240
|
-
messages.push(t('Validation:fieldInvalid', { attribute: yearLabel }))
|
|
241
|
-
} else if (!numOnly.test(year)) {
|
|
242
|
-
fieldErrors.dobYear = ' '
|
|
243
|
-
messages.push(t('Validation:fieldOnlyNumbers', { attribute: yearLabel }))
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Range checks if numeric
|
|
247
|
-
const d = parseInt(day, 10)
|
|
248
|
-
const m = parseInt(month, 10)
|
|
249
|
-
const y = parseInt(year, 10)
|
|
250
|
-
|
|
251
|
-
if (numOnly.test(day) && (d < 1 || d > 31)) {
|
|
252
|
-
fieldErrors.dobDay = ' '
|
|
253
|
-
messages.push(t('Validation:fieldValidDay', { attribute: dayLabel }))
|
|
254
|
-
}
|
|
255
|
-
if (numOnly.test(month) && (m < 1 || m > 12)) {
|
|
256
|
-
fieldErrors.dobMonth = ' '
|
|
257
|
-
messages.push(t('Validation:fieldValidMonth', { attribute: monthLabel }))
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Validate actual calendar date and future constraint
|
|
261
|
-
if (numOnly.test(day) && numOnly.test(month) && /^\d{4}$/.test(year)) {
|
|
262
|
-
const date = new Date(y, m - 1, d)
|
|
263
|
-
const isValidDate =
|
|
264
|
-
date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d
|
|
265
|
-
if (!isValidDate) {
|
|
266
|
-
fieldErrors.dobDay = ' '
|
|
267
|
-
fieldErrors.dobMonth = ' '
|
|
268
|
-
fieldErrors.dobYear = ' '
|
|
269
|
-
messages.push(t('Validation:fieldValidDate'))
|
|
270
|
-
} else {
|
|
271
|
-
const today = new Date()
|
|
272
|
-
const todayOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
|
273
|
-
if (date > todayOnly) {
|
|
274
|
-
fieldErrors.dobDay = ' '
|
|
275
|
-
fieldErrors.dobMonth = ' '
|
|
276
|
-
fieldErrors.dobYear = ' '
|
|
277
|
-
messages.push(t('Validation:birthdayInFuture'))
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Phone validation (Optional field, but must match format if provided)
|
|
285
|
-
if (formData.phone?.trim()) {
|
|
286
|
-
const phoneRegex = /^[+\-()\d\s]*$/
|
|
287
|
-
if (!phoneRegex.test(formData.phone)) {
|
|
288
|
-
fieldErrors.phone = ' '
|
|
289
|
-
messages.push(t('Validation:fieldValidPhone', { attribute: t('App:phone') }))
|
|
290
|
-
} else {
|
|
291
|
-
// Enforce max of 15 numeric digits in total (ignoring spaces and symbols)
|
|
292
|
-
const digitsCount = formData.phone.replace(/\D/g, '').length
|
|
293
|
-
if (digitsCount > 15) {
|
|
294
|
-
fieldErrors.phone = ' '
|
|
295
|
-
messages.push(t('Validation:fieldMaxLength', { attribute: t('App:phone'), max: 15 }))
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
setErrors(fieldErrors)
|
|
301
|
-
return { isValid: messages.length === 0, messages }
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
305
|
-
e.preventDefault()
|
|
306
|
-
const { isValid, messages } = validate()
|
|
307
|
-
if (isValid) {
|
|
308
|
-
// Clear draft from storage on successful validation
|
|
309
|
-
try {
|
|
310
|
-
sessionStorage.removeItem(STORAGE_KEY)
|
|
311
|
-
} catch (error) {
|
|
312
|
-
console.error('Failed to clear form data:', error)
|
|
313
|
-
}
|
|
314
|
-
onSave(formData)
|
|
315
|
-
} else {
|
|
316
|
-
showAlert({
|
|
317
|
-
type: 'error',
|
|
318
|
-
title: 'Form Error',
|
|
319
|
-
message: messages,
|
|
320
|
-
})
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return (
|
|
325
|
-
<form onSubmit={handleSubmit} noValidate className="tw-flex tw-flex-col tw-gap-6">
|
|
326
|
-
<NewInput
|
|
327
|
-
label={t('App:firstName', "First")}
|
|
328
|
-
name="firstName"
|
|
329
|
-
value={formData.firstName}
|
|
330
|
-
onChange={handleChange}
|
|
331
|
-
error={errors.firstName}
|
|
332
|
-
required
|
|
333
|
-
/>
|
|
334
|
-
|
|
335
|
-
<NewInput
|
|
336
|
-
label={t('App:lastName', "Last Name")}
|
|
337
|
-
name="lastName"
|
|
338
|
-
value={formData.lastName}
|
|
339
|
-
onChange={handleChange}
|
|
340
|
-
error={errors.lastName}
|
|
341
|
-
required
|
|
342
|
-
/>
|
|
343
|
-
|
|
344
|
-
<NewInput
|
|
345
|
-
label={t('App:emailAddress', "Email Address")}
|
|
346
|
-
name="email"
|
|
347
|
-
type="text"
|
|
348
|
-
value={formData.email}
|
|
349
|
-
onChange={handleChange}
|
|
350
|
-
error={errors.email}
|
|
351
|
-
required
|
|
352
|
-
readOnly={isEditing}
|
|
353
|
-
disabled={isEditing}
|
|
354
|
-
helperText={isEditing ? t('App:emailCannotBeEdited') : undefined}
|
|
355
|
-
/>
|
|
356
|
-
|
|
357
|
-
<div className="tw-flex tw-flex-col tw-gap-2">
|
|
358
|
-
<div className="tw-flex tw-items-center tw-gap-1">
|
|
359
|
-
<label className="tw-text-[15px] tw-font-semibold tw-text-[#121E52] tw-leading-5">Date of Birth (Optional)</label>
|
|
360
|
-
</div>
|
|
361
|
-
<div className="tw-grid tw-grid-cols-3 tw-gap-4">
|
|
362
|
-
<NewInput
|
|
363
|
-
label={t("App:day", "Day")}
|
|
364
|
-
name="dobDay"
|
|
365
|
-
value={formData.dobDay}
|
|
366
|
-
onChange={handleChange}
|
|
367
|
-
error={errors.dobDay}
|
|
368
|
-
className="dob-input"
|
|
369
|
-
/>
|
|
370
|
-
<NewInput
|
|
371
|
-
label={t("App:month", "Month")}
|
|
372
|
-
name="dobMonth"
|
|
373
|
-
value={formData.dobMonth}
|
|
374
|
-
onChange={handleChange}
|
|
375
|
-
error={errors.dobMonth}
|
|
376
|
-
className="dob-input"
|
|
377
|
-
/>
|
|
378
|
-
<NewInput
|
|
379
|
-
label={t("App:year", "Year")}
|
|
380
|
-
name="dobYear"
|
|
381
|
-
value={formData.dobYear}
|
|
382
|
-
onChange={handleChange}
|
|
383
|
-
error={errors.dobYear}
|
|
384
|
-
className="dob-input"
|
|
385
|
-
/>
|
|
386
|
-
</div>
|
|
387
|
-
</div>
|
|
388
|
-
|
|
389
|
-
<NewInput
|
|
390
|
-
label={t('App:phone', "Phone Number")}
|
|
391
|
-
name="phone"
|
|
392
|
-
value={formData.phone}
|
|
393
|
-
onChange={handleChange}
|
|
394
|
-
error={errors.phone}
|
|
395
|
-
optional
|
|
396
|
-
/>
|
|
397
|
-
|
|
398
|
-
<Checkbox
|
|
399
|
-
label="Opt-in to marketing communications"
|
|
400
|
-
name="optIn"
|
|
401
|
-
checked={formData.optIn}
|
|
402
|
-
onChange={handleChange}
|
|
403
|
-
/>
|
|
404
|
-
|
|
405
|
-
<NewInput
|
|
406
|
-
label={t('App:postcode', "Postcode")}
|
|
407
|
-
name="postcode"
|
|
408
|
-
value={formData.postcode}
|
|
409
|
-
onChange={handleChange}
|
|
410
|
-
optional
|
|
411
|
-
/>
|
|
412
|
-
|
|
413
|
-
<Select
|
|
414
|
-
label={t('App:gender', "Gender")}
|
|
415
|
-
name="gender"
|
|
416
|
-
value={formData.gender}
|
|
417
|
-
onChange={handleSelectChange}
|
|
418
|
-
optional
|
|
419
|
-
>
|
|
420
|
-
<option value="">{t('App:selectGender', "Select gender")}</option>
|
|
421
|
-
<option value="female">{t('App:female', "Female")}</option>
|
|
422
|
-
<option value="male">{t('App:male', "Male")}</option>
|
|
423
|
-
<option value="prefer_not_to_say">{t('App:preferNotToSay', "Prefer not to say")}</option>
|
|
424
|
-
</Select>
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
<div className="tw-pt-4">
|
|
428
|
-
<Button type="submit" disabled={isLoading} className="tw-px-8">
|
|
429
|
-
{isLoading ? 'Saving...' : submitButtonLabel}
|
|
430
|
-
</Button>
|
|
431
|
-
</div>
|
|
432
|
-
</form>
|
|
433
|
-
)
|
|
434
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './CustomerForm'
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { QuickFilter, QuickFilterOption } from '../QuickFilter'
|
|
3
|
-
import { NewTable, NewTableColumn } from '../NewTable'
|
|
4
|
-
import { NewInput } from '../NewInput'
|
|
5
|
-
import { Pagination } from '../Pagination'
|
|
6
|
-
import { SearchIcon, RefreshIcon, SendIcon, ExternalLinkIcon, ExportIcon, ClearIcon } from '../../icons'
|
|
7
|
-
import './CustomersList.scss'
|
|
8
|
-
|
|
9
|
-
export interface CustomersListProps {
|
|
10
|
-
t: (key: string, options?: any) => string
|
|
11
|
-
customers: any[]
|
|
12
|
-
isLoading: boolean
|
|
13
|
-
kioskLink?: string
|
|
14
|
-
waiversEnabled?: boolean
|
|
15
|
-
search: string
|
|
16
|
-
onSearchChange: (value: string) => void
|
|
17
|
-
onSearchIconClick?: () => void
|
|
18
|
-
onClearSearch?: () => void
|
|
19
|
-
quickFilters: string[]
|
|
20
|
-
onQuickFiltersChange: (values: string[]) => void
|
|
21
|
-
onRefresh: () => void
|
|
22
|
-
onExport: () => void
|
|
23
|
-
onSendWaiverRequest: () => void
|
|
24
|
-
onAddCustomer?: () => void
|
|
25
|
-
onTabChange?: (tabId: string) => void
|
|
26
|
-
activeTab?: string
|
|
27
|
-
tabs?: { id: string; label: string; path: string }[]
|
|
28
|
-
columns: NewTableColumn<any>[]
|
|
29
|
-
pagination?: {
|
|
30
|
-
currentPage: number
|
|
31
|
-
totalPages: number
|
|
32
|
-
onPageChange: (page: number) => void
|
|
33
|
-
totalItems?: number
|
|
34
|
-
itemsPerPage?: number
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export const CustomersList: React.FC<CustomersListProps> = ({
|
|
40
|
-
t,
|
|
41
|
-
customers,
|
|
42
|
-
isLoading,
|
|
43
|
-
kioskLink,
|
|
44
|
-
search,
|
|
45
|
-
onSearchChange,
|
|
46
|
-
quickFilters,
|
|
47
|
-
onSearchIconClick,
|
|
48
|
-
onClearSearch,
|
|
49
|
-
onQuickFiltersChange,
|
|
50
|
-
onRefresh,
|
|
51
|
-
onExport,
|
|
52
|
-
onSendWaiverRequest,
|
|
53
|
-
onTabChange,
|
|
54
|
-
activeTab,
|
|
55
|
-
tabs,
|
|
56
|
-
columns,
|
|
57
|
-
pagination,
|
|
58
|
-
waiversEnabled,
|
|
59
|
-
onAddCustomer,
|
|
60
|
-
}) => {
|
|
61
|
-
const quickFilterOptions: QuickFilterOption[] = waiversEnabled ? [
|
|
62
|
-
{ label: t('App:todayBookings'), value: 'today' },
|
|
63
|
-
{ label: t('App:signed', 'Signed'), value: 'signed' },
|
|
64
|
-
{ label: t('App:unsigned', 'Unsigned'), value: 'unsigned' },
|
|
65
|
-
] : [
|
|
66
|
-
{ label: t('App:todayBookings', 'Bookings for: Today'), value: 'today' },
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
// Handle mutually exclusive signed/unsigned filters
|
|
70
|
-
const handleQuickFiltersChange = (newFilters: string[]) => {
|
|
71
|
-
const hasSigned = newFilters.includes('signed')
|
|
72
|
-
const hasUnsigned = newFilters.includes('unsigned')
|
|
73
|
-
const hadSigned = quickFilters.includes('signed')
|
|
74
|
-
const hadUnsigned = quickFilters.includes('unsigned')
|
|
75
|
-
|
|
76
|
-
// If both signed and unsigned are being selected, keep only the newly selected one
|
|
77
|
-
if (hasSigned && hasUnsigned) {
|
|
78
|
-
if (!hadSigned && hasUnsigned) {
|
|
79
|
-
// signed was just added, remove unsigned
|
|
80
|
-
onQuickFiltersChange(newFilters.filter(f => f !== 'unsigned'))
|
|
81
|
-
} else if (hasSigned && !hadUnsigned) {
|
|
82
|
-
// unsigned was just added, remove signed
|
|
83
|
-
onQuickFiltersChange(newFilters.filter(f => f !== 'signed'))
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
onQuickFiltersChange(newFilters)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<div className="waivers-page">
|
|
92
|
-
<header className="waivers-page__header">
|
|
93
|
-
{tabs && tabs.length > 0 ? (
|
|
94
|
-
<div className="waivers-page__tabs-container">
|
|
95
|
-
<nav className="waivers-page__tabs">
|
|
96
|
-
{tabs.map((tab) => (
|
|
97
|
-
<button
|
|
98
|
-
key={tab.id}
|
|
99
|
-
className={`waivers-page__tab ${tab.id === activeTab ? 'waivers-page__tab--active' : ''}`}
|
|
100
|
-
onClick={() => onTabChange?.(tab.id)}
|
|
101
|
-
>
|
|
102
|
-
{tab.label}
|
|
103
|
-
{tab.id === activeTab && <div className="waivers-page__tab-indicator" />}
|
|
104
|
-
</button>
|
|
105
|
-
))}
|
|
106
|
-
</nav>
|
|
107
|
-
{kioskLink && (
|
|
108
|
-
<a href={kioskLink} target="_blank" rel="noopener noreferrer" className="kiosk-link">
|
|
109
|
-
<ExternalLinkIcon />
|
|
110
|
-
{t('App:launchKiosk', 'Launch Kiosk')}
|
|
111
|
-
</a>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
) : (
|
|
115
|
-
<div className="waivers-page__simple-header">
|
|
116
|
-
|
|
117
|
-
{onAddCustomer && (
|
|
118
|
-
<button className="waivers-page__add-customer-btn" onClick={onAddCustomer}>
|
|
119
|
-
+ {t('App:addCustomer', 'Add Customer')}
|
|
120
|
-
</button>
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
)}
|
|
124
|
-
</header>
|
|
125
|
-
|
|
126
|
-
<main className="waivers-page__content">
|
|
127
|
-
<section className="filter-section">
|
|
128
|
-
<div className="search-row">
|
|
129
|
-
<div className="search-input-wrapper">
|
|
130
|
-
<NewInput
|
|
131
|
-
label={t('App:search', 'Search')}
|
|
132
|
-
value={search}
|
|
133
|
-
onChange={(e) => onSearchChange(e.target.value)}
|
|
134
|
-
onIconClick={search ? onClearSearch : onSearchIconClick}
|
|
135
|
-
icon={search ? <ClearIcon /> : <SearchIcon />}
|
|
136
|
-
iconPosition={search ? 'right' : 'left'}
|
|
137
|
-
/>
|
|
138
|
-
<p className="search-helper-text">{t('App:searchPlaceholder')}</p>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<div className="filters-actions-row">
|
|
143
|
-
<div className="quick-filters-wrapper">
|
|
144
|
-
<QuickFilter
|
|
145
|
-
label={t('App:quickFilters', 'Quick Filters:')}
|
|
146
|
-
options={quickFilterOptions}
|
|
147
|
-
selectedValues={quickFilters}
|
|
148
|
-
onChange={handleQuickFiltersChange}
|
|
149
|
-
/>
|
|
150
|
-
</div>
|
|
151
|
-
<div className="action-buttons">
|
|
152
|
-
<button className="action-btn" onClick={onExport}>
|
|
153
|
-
<ExportIcon />
|
|
154
|
-
{t('App:exportData', 'Export')}
|
|
155
|
-
</button>
|
|
156
|
-
{waiversEnabled && (
|
|
157
|
-
<button className="action-btn" onClick={onSendWaiverRequest}>
|
|
158
|
-
<SendIcon />
|
|
159
|
-
{t('App:requestWaiver', 'Request Waiver')}
|
|
160
|
-
</button>
|
|
161
|
-
)}
|
|
162
|
-
<button className="action-btn" onClick={onRefresh}>
|
|
163
|
-
<RefreshIcon />
|
|
164
|
-
{t('App:refresh', 'Refresh')}
|
|
165
|
-
</button>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
</section>
|
|
169
|
-
|
|
170
|
-
<section className="table-section">
|
|
171
|
-
<NewTable
|
|
172
|
-
columns={columns}
|
|
173
|
-
data={customers}
|
|
174
|
-
noDataText={!isLoading && customers.length === 0 ? t('App:noResultsFound') : undefined}
|
|
175
|
-
/>
|
|
176
|
-
{pagination && customers.length > 0 && (
|
|
177
|
-
<div className="waivers-page__pagination">
|
|
178
|
-
<Pagination
|
|
179
|
-
currentPage={pagination.currentPage}
|
|
180
|
-
totalPages={pagination.totalPages}
|
|
181
|
-
totalItems={pagination.totalItems ?? 0}
|
|
182
|
-
itemsPerPage={pagination.itemsPerPage ?? 24}
|
|
183
|
-
onPageChange={pagination.onPageChange}
|
|
184
|
-
t={t}
|
|
185
|
-
entityName={t('App:customersLowerCase', 'customers')}
|
|
186
|
-
/>
|
|
187
|
-
</div>
|
|
188
|
-
)}
|
|
189
|
-
</section>
|
|
190
|
-
</main>
|
|
191
|
-
</div>
|
|
192
|
-
)
|
|
193
|
-
}
|