@licklist/design 0.78.5-dev.49 → 0.78.5-dev.50
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/index.js +11 -1
- package/dist/v2/components/ActionMenu/ActionMenu.d.ts +13 -0
- package/dist/v2/components/ActionMenu/ActionMenu.d.ts.map +1 -0
- package/dist/v2/components/ActionMenu/ActionMenu.js +100 -0
- package/dist/v2/components/ActionMenu/ActionMenu.scss.js +6 -0
- package/dist/v2/components/ActionMenu/index.d.ts +2 -0
- package/dist/v2/components/ActionMenu/index.d.ts.map +1 -0
- package/dist/v2/components/Alert/Alert.scss.js +1 -1
- package/dist/v2/components/Badge/Badge.d.ts +10 -0
- package/dist/v2/components/Badge/Badge.d.ts.map +1 -0
- package/dist/v2/components/Badge/Badge.js +19 -0
- package/dist/v2/components/Badge/Badge.scss.js +6 -0
- package/dist/v2/components/Badge/index.d.ts +2 -0
- package/dist/v2/components/Badge/index.d.ts.map +1 -0
- package/dist/v2/components/Checkbox/Checkbox.scss.js +1 -1
- package/dist/v2/components/Customer/CustomerCreate/CustomerCreate.d.ts +11 -0
- package/dist/v2/components/Customer/CustomerCreate/CustomerCreate.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomerCreate/CustomerCreate.js +32 -0
- package/dist/v2/components/Customer/CustomerCreate/index.d.ts +2 -0
- package/dist/v2/components/Customer/CustomerCreate/index.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomerDetail/CustomerDetail.d.ts +35 -0
- package/dist/v2/components/Customer/CustomerDetail/CustomerDetail.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomerDetail/CustomerDetail.js +235 -0
- package/dist/v2/components/Customer/CustomerDetail/CustomerDetail.scss.js +6 -0
- package/dist/v2/components/Customer/CustomerDetail/index.d.ts +2 -0
- package/dist/v2/components/Customer/CustomerDetail/index.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomerEdit/CustomerEdit.d.ts +11 -0
- package/dist/v2/components/Customer/CustomerEdit/CustomerEdit.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomerEdit/CustomerEdit.js +32 -0
- package/dist/v2/components/Customer/CustomerEdit/index.d.ts +2 -0
- package/dist/v2/components/Customer/CustomerEdit/index.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomerForm/CustomerForm.d.ts +22 -0
- package/dist/v2/components/Customer/CustomerForm/CustomerForm.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomerForm/CustomerForm.js +535 -0
- package/dist/v2/components/Customer/CustomerForm/index.d.ts +2 -0
- package/dist/v2/components/Customer/CustomerForm/index.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomersList.d.ts +37 -0
- package/dist/v2/components/Customer/CustomersList.d.ts.map +1 -0
- package/dist/v2/components/Customer/CustomersList.js +204 -0
- package/dist/v2/components/Customer/CustomersList.scss.js +6 -0
- package/dist/v2/components/Customer/index.d.ts +6 -0
- package/dist/v2/components/Customer/index.d.ts.map +1 -0
- package/dist/v2/components/FormField/FormField.scss.js +1 -1
- package/dist/v2/components/Modal/DeleteModal.d.ts +15 -0
- package/dist/v2/components/Modal/DeleteModal.d.ts.map +1 -0
- package/dist/v2/components/Modal/DeleteModal.js +147 -0
- package/dist/v2/components/Modal/DeleteModal.scss.js +6 -0
- package/dist/v2/components/Modal/index.d.ts +3 -0
- package/dist/v2/components/Modal/index.d.ts.map +1 -0
- package/dist/v2/components/NewInput/NewInput.d.ts +2 -0
- package/dist/v2/components/NewInput/NewInput.d.ts.map +1 -1
- package/dist/v2/components/NewInput/NewInput.js +29 -12
- package/dist/v2/components/NewPageHeader/NewPageHeader.d.ts +1 -0
- package/dist/v2/components/NewPageHeader/NewPageHeader.d.ts.map +1 -1
- package/dist/v2/components/NewPageHeader/NewPageHeader.js +15 -9
- package/dist/v2/components/NewPageHeader/NewPageHeader.scss.js +1 -1
- package/dist/v2/components/NewTable/NewTable.d.ts +20 -0
- package/dist/v2/components/NewTable/NewTable.d.ts.map +1 -0
- package/dist/v2/components/NewTable/NewTable.js +57 -0
- package/dist/v2/components/NewTable/NewTable.scss.js +6 -0
- package/dist/v2/components/NewTable/index.d.ts +2 -0
- package/dist/v2/components/NewTable/index.d.ts.map +1 -0
- package/dist/v2/components/Pagination/Pagination.d.ts +13 -0
- package/dist/v2/components/Pagination/Pagination.d.ts.map +1 -0
- package/dist/v2/components/Pagination/Pagination.js +79 -0
- package/dist/v2/components/Pagination/Pagination.scss.js +6 -0
- package/dist/v2/components/Pagination/index.d.ts +2 -0
- package/dist/v2/components/Pagination/index.d.ts.map +1 -0
- package/dist/v2/components/QuickFilter/QuickFilter.d.ts +14 -0
- package/dist/v2/components/QuickFilter/QuickFilter.d.ts.map +1 -0
- package/dist/v2/components/QuickFilter/QuickFilter.js +67 -0
- package/dist/v2/components/QuickFilter/QuickFilter.scss.js +6 -0
- package/dist/v2/components/QuickFilter/index.d.ts +2 -0
- package/dist/v2/components/QuickFilter/index.d.ts.map +1 -0
- package/dist/v2/components/Select/Select.d.ts +7 -4
- package/dist/v2/components/Select/Select.d.ts.map +1 -1
- package/dist/v2/components/Select/Select.js +53 -24
- package/dist/v2/components/Select/Select.scss.js +1 -1
- package/dist/v2/components/WYSIWYGEditor/WYSIWYGEditor.scss.js +1 -1
- package/dist/v2/components/index.d.ts +18 -0
- package/dist/v2/components/index.d.ts.map +1 -1
- package/dist/v2/icons/index.d.ts +21 -0
- package/dist/v2/icons/index.d.ts.map +1 -1
- package/dist/v2/icons/index.js +155 -4
- package/dist/v2/navigation/DashboardLayout/ProviderSidebar.d.ts.map +1 -1
- package/dist/v2/navigation/DashboardLayout/ProviderSidebar.js +4 -8
- package/dist/v2/styles/common.scss +7 -0
- package/dist/v2/styles/form/NewInput.scss +45 -21
- package/dist/v2/styles/form/NewInput.scss.js +1 -1
- package/dist/v2/styles/index.scss +1 -0
- package/package.json +3 -3
- package/src/v2/components/ActionMenu/ActionMenu.scss +78 -0
- package/src/v2/components/ActionMenu/ActionMenu.tsx +64 -0
- package/src/v2/components/ActionMenu/index.ts +1 -0
- package/src/v2/components/Badge/Badge.scss +69 -0
- package/src/v2/components/Badge/Badge.tsx +23 -0
- package/src/v2/components/Badge/index.ts +1 -0
- package/src/v2/components/Customer/CustomerCreate/CustomerCreate.tsx +36 -0
- package/src/v2/components/Customer/CustomerCreate/index.ts +1 -0
- package/src/v2/components/Customer/CustomerDetail/CustomerDetail.scss +315 -0
- package/src/v2/components/Customer/CustomerDetail/CustomerDetail.tsx +161 -0
- package/src/v2/components/Customer/CustomerDetail/index.ts +1 -0
- package/src/v2/components/Customer/CustomerEdit/CustomerEdit.tsx +37 -0
- package/src/v2/components/Customer/CustomerEdit/index.ts +1 -0
- package/src/v2/components/Customer/CustomerForm/CustomerForm.tsx +434 -0
- package/src/v2/components/Customer/CustomerForm/index.ts +1 -0
- package/src/v2/components/Customer/CustomersList.scss +586 -0
- package/src/v2/components/Customer/CustomersList.tsx +193 -0
- package/src/v2/components/Customer/index.ts +5 -0
- package/src/v2/components/Modal/DeleteModal.scss +254 -0
- package/src/v2/components/Modal/DeleteModal.tsx +100 -0
- package/src/v2/components/Modal/index.ts +3 -0
- package/src/v2/components/NewInput/NewInput.stories.tsx +3 -18
- package/src/v2/components/NewInput/NewInput.tsx +23 -12
- package/src/v2/components/NewPageHeader/NewPageHeader.scss +13 -7
- package/src/v2/components/NewPageHeader/NewPageHeader.tsx +14 -9
- package/src/v2/components/NewTable/NewTable.scss +110 -0
- package/src/v2/components/NewTable/NewTable.tsx +85 -0
- package/src/v2/components/NewTable/index.ts +1 -0
- package/src/v2/components/Pagination/Pagination.scss +142 -0
- package/src/v2/components/Pagination/Pagination.tsx +80 -0
- package/src/v2/components/Pagination/index.ts +1 -0
- package/src/v2/components/QuickFilter/QuickFilter.scss +84 -0
- package/src/v2/components/QuickFilter/QuickFilter.tsx +49 -0
- package/src/v2/components/QuickFilter/index.ts +1 -0
- package/src/v2/components/Select/Select.scss +61 -24
- package/src/v2/components/Select/Select.stories.tsx +77 -1
- package/src/v2/components/Select/Select.tsx +63 -34
- package/src/v2/components/index.ts +28 -0
- package/src/v2/icons/index.tsx +79 -2
- package/src/v2/navigation/DashboardLayout/ProviderSidebar.tsx +3 -1
- package/src/v2/navigation/config.tsx +1 -1
- package/src/v2/styles/common.scss +7 -0
- package/src/v2/styles/form/NewInput.scss +45 -21
- package/src/v2/styles/index.scss +1 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Badge } from '../../Badge'
|
|
3
|
+
import { EditIcon, ArrowLeftIcon, InfoIcon } from '../../../icons'
|
|
4
|
+
import './CustomerDetail.scss'
|
|
5
|
+
|
|
6
|
+
export interface CustomerDetailProps {
|
|
7
|
+
onBack: () => void
|
|
8
|
+
onEdit?: () => void
|
|
9
|
+
name: string
|
|
10
|
+
customerId: string
|
|
11
|
+
waiverStatus: string
|
|
12
|
+
waiversEnabled?: boolean
|
|
13
|
+
waiverStatusVariant?: 'success' | 'danger' | 'warning' | 'neutral'
|
|
14
|
+
contactDetails: {
|
|
15
|
+
email: string
|
|
16
|
+
phone?: string
|
|
17
|
+
age?: string
|
|
18
|
+
}
|
|
19
|
+
bookingDetails: {
|
|
20
|
+
nextBooking: string
|
|
21
|
+
customerSince: string
|
|
22
|
+
}
|
|
23
|
+
waiverDetails: {
|
|
24
|
+
status: string
|
|
25
|
+
statusVariant?: 'success' | 'danger' | 'warning' | 'neutral'
|
|
26
|
+
expires: string
|
|
27
|
+
}
|
|
28
|
+
minors?: Array<{
|
|
29
|
+
name: string
|
|
30
|
+
age: string
|
|
31
|
+
expires: string
|
|
32
|
+
status: string
|
|
33
|
+
statusVariant?: 'success' | 'danger' | 'warning' | 'neutral'
|
|
34
|
+
}>
|
|
35
|
+
customerSinceFooter: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const CustomerDetail: React.FC<CustomerDetailProps> = ({
|
|
39
|
+
onBack,
|
|
40
|
+
onEdit,
|
|
41
|
+
name,
|
|
42
|
+
customerId,
|
|
43
|
+
waiverStatus,
|
|
44
|
+
waiverStatusVariant = 'success',
|
|
45
|
+
waiversEnabled = true,
|
|
46
|
+
contactDetails,
|
|
47
|
+
bookingDetails,
|
|
48
|
+
waiverDetails,
|
|
49
|
+
minors,
|
|
50
|
+
customerSinceFooter,
|
|
51
|
+
}) => {
|
|
52
|
+
return (
|
|
53
|
+
<div className="customer-detail">
|
|
54
|
+
<div className="customer-detail__back" onClick={onBack}>
|
|
55
|
+
<span className="customer-detail__back-icon"><ArrowLeftIcon width={24} height={24} /></span> Back to Customers
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="customer-detail__header">
|
|
59
|
+
<div className="customer-detail__title-row">
|
|
60
|
+
<h1 className="customer-detail__name">{name}</h1>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="customer-detail__badges-row">
|
|
63
|
+
<div className="customer-detail__badges">
|
|
64
|
+
<span className="customer-detail__id-badge">{customerId}</span>
|
|
65
|
+
{waiversEnabled && (
|
|
66
|
+
<Badge
|
|
67
|
+
variant={waiverStatusVariant}
|
|
68
|
+
icon={(waiverStatus === 'Expired' || waiverStatus === 'Not Signed') ? <InfoIcon /> : undefined}
|
|
69
|
+
>
|
|
70
|
+
{waiverStatus}
|
|
71
|
+
</Badge>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
{onEdit && (
|
|
75
|
+
<div className="customer-detail__edit" onClick={onEdit}>
|
|
76
|
+
<EditIcon /> Edit
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="customer-detail__grid">
|
|
83
|
+
<div className="customer-detail__card">
|
|
84
|
+
<h2 className="customer-detail__card-title">Contact Details</h2>
|
|
85
|
+
<div className="customer-detail__info-group">
|
|
86
|
+
<label>Email</label>
|
|
87
|
+
<span>{contactDetails.email}</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div className="customer-detail__info-group">
|
|
90
|
+
<label>Phone</label>
|
|
91
|
+
<span>{contactDetails.phone || '-'}</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="customer-detail__info-group">
|
|
94
|
+
<label>Age</label>
|
|
95
|
+
<span>{contactDetails.age || '-'}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="customer-detail__card">
|
|
100
|
+
<h2 className="customer-detail__card-title">Booking Details</h2>
|
|
101
|
+
<div className="customer-detail__info-group">
|
|
102
|
+
<label>Next Booking</label>
|
|
103
|
+
<span>{bookingDetails.nextBooking}</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="customer-detail__info-group">
|
|
106
|
+
<label>Customer Since</label>
|
|
107
|
+
<span>{bookingDetails.customerSince}</span>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{waiversEnabled && (
|
|
112
|
+
<div className="customer-detail__card">
|
|
113
|
+
<h2 className="customer-detail__card-title">Waiver Details</h2>
|
|
114
|
+
<div className="customer-detail__info-group">
|
|
115
|
+
<label>Status</label>
|
|
116
|
+
<span>
|
|
117
|
+
<Badge
|
|
118
|
+
variant={waiverDetails.statusVariant}
|
|
119
|
+
icon={(waiverDetails.status === 'Expired' || waiverDetails.status === 'Not Signed') ? <InfoIcon /> : undefined}
|
|
120
|
+
>
|
|
121
|
+
{waiverDetails.status}
|
|
122
|
+
</Badge>
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="customer-detail__info-group">
|
|
126
|
+
<label>Expires</label>
|
|
127
|
+
<span>{waiverDetails.expires}</span>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{waiversEnabled && minors && minors.length > 0 && (
|
|
134
|
+
<div className="customer-detail__card customer-detail__card--full">
|
|
135
|
+
<h2 className="customer-detail__card-title">Minors Signed For</h2>
|
|
136
|
+
{minors.map((minor, index) => (
|
|
137
|
+
<div key={index} className="customer-detail__minor-row">
|
|
138
|
+
<div className="customer-detail__minor-info">
|
|
139
|
+
<div className="customer-detail__minor-name">{minor.name}</div>
|
|
140
|
+
<div className="customer-detail__minor-age">{minor.age}</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div className="customer-detail__minor-status-group">
|
|
143
|
+
<span className="customer-detail__minor-expires">Expires: {minor.expires}</span>
|
|
144
|
+
<Badge
|
|
145
|
+
variant={minor.statusVariant}
|
|
146
|
+
icon={(minor.status === 'Expired' || minor.status === 'Not Signed') ? <InfoIcon /> : undefined}
|
|
147
|
+
>
|
|
148
|
+
{minor.status}
|
|
149
|
+
</Badge>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
<div className="customer-detail__footer">
|
|
157
|
+
Customer since: {customerSinceFooter}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CustomerDetail'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { NewPageHeader } from '../../NewPageHeader'
|
|
3
|
+
import { CustomerForm, CustomerData } from '../CustomerForm'
|
|
4
|
+
|
|
5
|
+
export interface CustomerEditProps {
|
|
6
|
+
onBack: () => void
|
|
7
|
+
onCancel: () => void
|
|
8
|
+
onSave: (data: CustomerData) => void
|
|
9
|
+
initialData?: Partial<CustomerData>
|
|
10
|
+
isLoading?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CustomerEdit: React.FC<CustomerEditProps> = ({
|
|
14
|
+
|
|
15
|
+
onCancel,
|
|
16
|
+
onSave,
|
|
17
|
+
initialData,
|
|
18
|
+
isLoading,
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<div className="tw-bg-white tw-min-h-screen tw-font-sans">
|
|
22
|
+
<div className='tw-max-w-4xl tw-mx-auto tw-w-full'>
|
|
23
|
+
<NewPageHeader title="Edit Customer" onCancel={onCancel} />
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div className="tw-max-w-4xl tw-mx-auto tw-w-full tw-bg-white tw-border-2 tw-border-[#e8e9ef] tw-rounded-lg tw-p-4 md:tw-p-8 tw-mt-8">
|
|
27
|
+
<CustomerForm
|
|
28
|
+
onSave={onSave}
|
|
29
|
+
initialData={initialData}
|
|
30
|
+
isLoading={isLoading}
|
|
31
|
+
submitButtonLabel="Save Changes"
|
|
32
|
+
isEditing={true}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CustomerEdit'
|
|
@@ -0,0 +1,434 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CustomerForm'
|