@ecomiq/storefront 2.0.12 → 2.0.14
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/bin/ecomiq.mjs +3 -1
- package/package.json +4 -2
- package/src/auth/create-storefront-auth.ts +65 -6
- package/src/auth/ecomiq-oauth-password-constants.ts +1 -3
- package/src/auth/ecomiq-oauth-password-plugin.ts +12 -17
- package/src/auth/ecomiq-storefront-sign-in.ts +0 -3
- package/src/auth/secure-cookies.ts +23 -0
- package/src/auth/storefront-auth-base-url.ts +117 -0
- package/src/codegen.mjs +27 -1
- package/src/components/account/customer-addresses-panel.tsx +678 -0
- package/src/components/account/customer-order-detail-panel.tsx +285 -0
- package/src/components/account/customer-orders-panel.tsx +192 -0
- package/src/components/account/customer-profile-panel.tsx +379 -0
- package/src/components/checkout/checkout-contact-section.tsx +17 -11
- package/src/components/checkout/checkout-session-view.tsx +83 -2
- package/src/components/checkout/checkout-shipping-address-section.tsx +107 -2
- package/src/components/checkout/checkout-steps.ts +15 -0
- package/src/components/checkout/map-customer-to-checkout.ts +188 -0
- package/src/components/checkout/use-checkout-customer-prefill.ts +409 -0
- package/src/components/checkout/use-checkout-session.ts +5 -4
- package/src/config.d.ts +4 -0
- package/src/config.mjs +14 -0
- package/src/index.ts +54 -0
- package/src/pages/account/account-page-skeleton.tsx +69 -0
- package/src/pages/account/account-page-view.tsx +270 -0
- package/src/pages/account/account-shell.tsx +101 -0
- package/src/pages/account/parse-account-route.ts +36 -0
- package/src/pages/account.tsx +16 -0
- package/src/pages/checkout.$checkoutId.tsx +1 -1
- package/src/providers/auth.tsx +11 -2
- package/src/proxy-api.ts +20 -0
- package/src/vite-inline.mjs +54 -1
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, useState, type FormEvent } from 'react'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type {
|
|
4
|
+
CustomerAddressUpsertInput,
|
|
5
|
+
CustomerSavedAddress,
|
|
6
|
+
} from '@ecomiq/core/models/customer-address'
|
|
7
|
+
import { Loader2, MapPin, Pencil, Plus, Star, Trash2 } from 'lucide-react'
|
|
8
|
+
import { Button, cn } from '../../ui'
|
|
9
|
+
import {
|
|
10
|
+
CheckoutSelectField,
|
|
11
|
+
CheckoutTextField,
|
|
12
|
+
FieldError,
|
|
13
|
+
fieldClass,
|
|
14
|
+
} from '../checkout/checkout-form-controls'
|
|
15
|
+
import { useCheckoutGeo } from '../checkout/use-checkout-geo'
|
|
16
|
+
|
|
17
|
+
const DEFAULT_COUNTRY_CODE = 'PE'
|
|
18
|
+
|
|
19
|
+
const addressFormSchema = z.object({
|
|
20
|
+
addressKey: z.string().trim().min(1, 'Nombre de la dirección requerido').max(48),
|
|
21
|
+
addressLine: z.string().trim().min(1, 'Calle o avenida requerida'),
|
|
22
|
+
addressNumber: z.string().trim().min(1, 'Número requerido'),
|
|
23
|
+
postalCode: z.string().trim().min(1, 'Código postal requerido'),
|
|
24
|
+
reference: z.string().trim(),
|
|
25
|
+
phone: z.string().trim(),
|
|
26
|
+
cellPhone: z.string().trim(),
|
|
27
|
+
departmentCode: z.string().min(1, 'Selecciona departamento'),
|
|
28
|
+
departmentName: z.string().min(1, 'Selecciona departamento'),
|
|
29
|
+
provinceCode: z.string().min(1, 'Selecciona provincia'),
|
|
30
|
+
provinceName: z.string().min(1, 'Selecciona provincia'),
|
|
31
|
+
districtCode: z.string().min(1, 'Selecciona distrito'),
|
|
32
|
+
districtName: z.string().min(1, 'Selecciona distrito'),
|
|
33
|
+
isDefault: z.boolean(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
type AddressFormValues = z.infer<typeof addressFormSchema>
|
|
37
|
+
type AddressFormErrors = Partial<Record<keyof AddressFormValues, string>>
|
|
38
|
+
|
|
39
|
+
const emptyDefaults: AddressFormValues = {
|
|
40
|
+
addressKey: 'Casa',
|
|
41
|
+
addressLine: '',
|
|
42
|
+
addressNumber: '',
|
|
43
|
+
postalCode: '',
|
|
44
|
+
reference: '',
|
|
45
|
+
phone: '',
|
|
46
|
+
cellPhone: '',
|
|
47
|
+
departmentCode: '',
|
|
48
|
+
departmentName: '',
|
|
49
|
+
provinceCode: '',
|
|
50
|
+
provinceName: '',
|
|
51
|
+
districtCode: '',
|
|
52
|
+
districtName: '',
|
|
53
|
+
isDefault: false,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toUpsertPayload(values: AddressFormValues): CustomerAddressUpsertInput {
|
|
57
|
+
return {
|
|
58
|
+
addressKey: values.addressKey.trim(),
|
|
59
|
+
countryIsoCode: DEFAULT_COUNTRY_CODE,
|
|
60
|
+
addressLine: values.addressLine.trim(),
|
|
61
|
+
addressNumber: values.addressNumber.trim(),
|
|
62
|
+
department: values.departmentName.trim(),
|
|
63
|
+
province: values.provinceName.trim(),
|
|
64
|
+
district: values.districtName.trim(),
|
|
65
|
+
phone: values.phone.trim(),
|
|
66
|
+
cellPhone: values.cellPhone.trim() || values.phone.trim(),
|
|
67
|
+
postalCode: values.postalCode.trim(),
|
|
68
|
+
reference: values.reference.trim(),
|
|
69
|
+
latitude: null,
|
|
70
|
+
longitude: null,
|
|
71
|
+
isDefault: values.isDefault,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findCodeByName(
|
|
76
|
+
items: { code: string; name: string }[],
|
|
77
|
+
name: string,
|
|
78
|
+
): string {
|
|
79
|
+
const target = name.trim().toLowerCase()
|
|
80
|
+
if (!target) return ''
|
|
81
|
+
return (
|
|
82
|
+
items.find((item) => item.name.trim().toLowerCase() === target)?.code ?? ''
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type CustomerAddressesPanelProps = {
|
|
87
|
+
addresses?: CustomerSavedAddress[]
|
|
88
|
+
isLoading: boolean
|
|
89
|
+
errorMessage?: string | null
|
|
90
|
+
onCreateAddress: (payload: CustomerAddressUpsertInput) => Promise<void>
|
|
91
|
+
onUpdateAddress: (
|
|
92
|
+
addressId: string,
|
|
93
|
+
payload: CustomerAddressUpsertInput,
|
|
94
|
+
) => Promise<void>
|
|
95
|
+
onDeleteAddress: (addressId: string) => Promise<void>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function CustomerAddressesPanel({
|
|
99
|
+
addresses,
|
|
100
|
+
isLoading,
|
|
101
|
+
errorMessage,
|
|
102
|
+
onCreateAddress,
|
|
103
|
+
onUpdateAddress,
|
|
104
|
+
onDeleteAddress,
|
|
105
|
+
}: CustomerAddressesPanelProps) {
|
|
106
|
+
const formId = useId()
|
|
107
|
+
const dialogRef = useRef<HTMLDialogElement>(null)
|
|
108
|
+
const deleteDialogRef = useRef<HTMLDialogElement>(null)
|
|
109
|
+
|
|
110
|
+
const [editing, setEditing] = useState<CustomerSavedAddress | null>(null)
|
|
111
|
+
const [form, setForm] = useState<AddressFormValues>(emptyDefaults)
|
|
112
|
+
const [fieldErrors, setFieldErrors] = useState<AddressFormErrors>({})
|
|
113
|
+
const [formError, setFormError] = useState<string | null>(null)
|
|
114
|
+
const [saving, setSaving] = useState(false)
|
|
115
|
+
const [deleteTarget, setDeleteTarget] = useState<CustomerSavedAddress | null>(
|
|
116
|
+
null,
|
|
117
|
+
)
|
|
118
|
+
const [deleting, setDeleting] = useState(false)
|
|
119
|
+
const [actionError, setActionError] = useState<string | null>(null)
|
|
120
|
+
|
|
121
|
+
const geo = useCheckoutGeo(
|
|
122
|
+
DEFAULT_COUNTRY_CODE,
|
|
123
|
+
form.departmentCode,
|
|
124
|
+
form.provinceCode,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const patchForm = (patch: Partial<AddressFormValues>) => {
|
|
128
|
+
setForm((prev) => ({ ...prev, ...patch }))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const clearFieldError = (key: keyof AddressFormValues) => {
|
|
132
|
+
setFieldErrors((prev) => {
|
|
133
|
+
if (!prev[key]) return prev
|
|
134
|
+
const next = { ...prev }
|
|
135
|
+
delete next[key]
|
|
136
|
+
return next
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const openCreate = () => {
|
|
141
|
+
setEditing(null)
|
|
142
|
+
setForm(emptyDefaults)
|
|
143
|
+
setFieldErrors({})
|
|
144
|
+
setFormError(null)
|
|
145
|
+
dialogRef.current?.showModal()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const openEdit = (addr: CustomerSavedAddress) => {
|
|
149
|
+
setEditing(addr)
|
|
150
|
+
setForm({
|
|
151
|
+
addressKey: addr.addressKey,
|
|
152
|
+
addressLine: addr.addressLine,
|
|
153
|
+
addressNumber: addr.addressNumber,
|
|
154
|
+
postalCode: addr.postalCode,
|
|
155
|
+
reference: addr.reference ?? '',
|
|
156
|
+
phone: addr.phone ?? '',
|
|
157
|
+
cellPhone: addr.cellPhone ?? '',
|
|
158
|
+
departmentCode: '',
|
|
159
|
+
departmentName: addr.department,
|
|
160
|
+
provinceCode: '',
|
|
161
|
+
provinceName: addr.province,
|
|
162
|
+
districtCode: '',
|
|
163
|
+
districtName: addr.district,
|
|
164
|
+
isDefault: addr.isDefault,
|
|
165
|
+
})
|
|
166
|
+
setFieldErrors({})
|
|
167
|
+
setFormError(null)
|
|
168
|
+
dialogRef.current?.showModal()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const closeDialog = () => {
|
|
172
|
+
dialogRef.current?.close()
|
|
173
|
+
setEditing(null)
|
|
174
|
+
setForm(emptyDefaults)
|
|
175
|
+
setFieldErrors({})
|
|
176
|
+
setFormError(null)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!editing || !form.departmentName || form.departmentCode) return
|
|
181
|
+
const code = findCodeByName(geo.departments, form.departmentName)
|
|
182
|
+
if (code) patchForm({ departmentCode: code })
|
|
183
|
+
}, [editing, form.departmentName, form.departmentCode, geo.departments])
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!editing || !form.provinceName || !form.departmentCode || form.provinceCode)
|
|
187
|
+
return
|
|
188
|
+
const code = findCodeByName(geo.provinces, form.provinceName)
|
|
189
|
+
if (code) patchForm({ provinceCode: code })
|
|
190
|
+
}, [
|
|
191
|
+
editing,
|
|
192
|
+
form.provinceName,
|
|
193
|
+
form.departmentCode,
|
|
194
|
+
form.provinceCode,
|
|
195
|
+
geo.provinces,
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!editing || !form.districtName || !form.provinceCode || form.districtCode)
|
|
200
|
+
return
|
|
201
|
+
const code = findCodeByName(geo.districts, form.districtName)
|
|
202
|
+
if (code) patchForm({ districtCode: code })
|
|
203
|
+
}, [
|
|
204
|
+
editing,
|
|
205
|
+
form.districtName,
|
|
206
|
+
form.provinceCode,
|
|
207
|
+
form.districtCode,
|
|
208
|
+
geo.districts,
|
|
209
|
+
])
|
|
210
|
+
|
|
211
|
+
const handleSubmit = async (event: FormEvent) => {
|
|
212
|
+
event.preventDefault()
|
|
213
|
+
const parsed = addressFormSchema.safeParse(form)
|
|
214
|
+
if (!parsed.success) {
|
|
215
|
+
const next: AddressFormErrors = {}
|
|
216
|
+
for (const issue of parsed.error.issues) {
|
|
217
|
+
const key = issue.path[0]
|
|
218
|
+
if (typeof key === 'string' && !(key in next)) {
|
|
219
|
+
next[key as keyof AddressFormValues] = issue.message
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
setFieldErrors(next)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
setSaving(true)
|
|
228
|
+
setFormError(null)
|
|
229
|
+
const payload = toUpsertPayload(parsed.data)
|
|
230
|
+
if (editing?.id) {
|
|
231
|
+
await onUpdateAddress(editing.id, payload)
|
|
232
|
+
} else {
|
|
233
|
+
await onCreateAddress(payload)
|
|
234
|
+
}
|
|
235
|
+
closeDialog()
|
|
236
|
+
} catch (error) {
|
|
237
|
+
setFormError(
|
|
238
|
+
error instanceof Error ? error.message : 'No se pudo guardar la dirección',
|
|
239
|
+
)
|
|
240
|
+
} finally {
|
|
241
|
+
setSaving(false)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const handleSetDefault = async (addr: CustomerSavedAddress) => {
|
|
246
|
+
if (!addr.id || addr.isDefault) return
|
|
247
|
+
const payload: CustomerAddressUpsertInput = {
|
|
248
|
+
addressKey: addr.addressKey,
|
|
249
|
+
countryIsoCode: addr.countryIsoCode,
|
|
250
|
+
addressLine: addr.addressLine,
|
|
251
|
+
addressNumber: addr.addressNumber,
|
|
252
|
+
department: addr.department,
|
|
253
|
+
province: addr.province,
|
|
254
|
+
district: addr.district,
|
|
255
|
+
phone: addr.phone,
|
|
256
|
+
cellPhone: addr.cellPhone || addr.phone,
|
|
257
|
+
postalCode: addr.postalCode,
|
|
258
|
+
reference: addr.reference,
|
|
259
|
+
latitude: addr.latitude,
|
|
260
|
+
longitude: addr.longitude,
|
|
261
|
+
isDefault: true,
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
setActionError(null)
|
|
265
|
+
await onUpdateAddress(addr.id, payload)
|
|
266
|
+
} catch (error) {
|
|
267
|
+
setActionError(
|
|
268
|
+
error instanceof Error ? error.message : 'No se pudo actualizar',
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const openDelete = (addr: CustomerSavedAddress) => {
|
|
274
|
+
setDeleteTarget(addr)
|
|
275
|
+
setActionError(null)
|
|
276
|
+
deleteDialogRef.current?.showModal()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const closeDelete = () => {
|
|
280
|
+
deleteDialogRef.current?.close()
|
|
281
|
+
setDeleteTarget(null)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const confirmDelete = async () => {
|
|
285
|
+
if (!deleteTarget?.id) return
|
|
286
|
+
try {
|
|
287
|
+
setDeleting(true)
|
|
288
|
+
setActionError(null)
|
|
289
|
+
await onDeleteAddress(deleteTarget.id)
|
|
290
|
+
closeDelete()
|
|
291
|
+
} catch (error) {
|
|
292
|
+
setActionError(
|
|
293
|
+
error instanceof Error ? error.message : 'No se pudo eliminar',
|
|
294
|
+
)
|
|
295
|
+
} finally {
|
|
296
|
+
setDeleting(false)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const list = addresses ?? []
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div className="space-y-6">
|
|
304
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
305
|
+
<div>
|
|
306
|
+
<h2 className="text-lg font-medium text-foreground">Tus direcciones</h2>
|
|
307
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
308
|
+
Envío a domicilio y datos de contacto para tus pedidos.
|
|
309
|
+
</p>
|
|
310
|
+
</div>
|
|
311
|
+
<Button type="button" onClick={openCreate} className="shrink-0 gap-2">
|
|
312
|
+
<Plus className="size-4" />
|
|
313
|
+
Añadir dirección
|
|
314
|
+
</Button>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{actionError ? (
|
|
318
|
+
<p className="rounded-md border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive">
|
|
319
|
+
{actionError}
|
|
320
|
+
</p>
|
|
321
|
+
) : null}
|
|
322
|
+
|
|
323
|
+
{isLoading ? (
|
|
324
|
+
<div className="flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-8 text-sm text-muted-foreground">
|
|
325
|
+
<Loader2 className="size-4 animate-spin" />
|
|
326
|
+
Cargando direcciones…
|
|
327
|
+
</div>
|
|
328
|
+
) : null}
|
|
329
|
+
|
|
330
|
+
{errorMessage && !isLoading ? (
|
|
331
|
+
<p className="rounded-md border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive">
|
|
332
|
+
{errorMessage}
|
|
333
|
+
</p>
|
|
334
|
+
) : null}
|
|
335
|
+
|
|
336
|
+
{!isLoading && !errorMessage && list.length === 0 ? (
|
|
337
|
+
<div className="rounded-lg border border-dashed border-border bg-muted/30 px-6 py-10 text-center text-sm text-muted-foreground">
|
|
338
|
+
<MapPin className="mx-auto mb-3 size-10 text-muted-foreground/70" />
|
|
339
|
+
Todavía no tienes direcciones guardadas.
|
|
340
|
+
</div>
|
|
341
|
+
) : null}
|
|
342
|
+
|
|
343
|
+
{!isLoading && list.length > 0 ? (
|
|
344
|
+
<ul className="grid gap-4 lg:grid-cols-2">
|
|
345
|
+
{list.map((addr) => (
|
|
346
|
+
<li
|
|
347
|
+
key={addr.id || `${addr.addressKey}-${addr.postalCode}`}
|
|
348
|
+
className={cn(
|
|
349
|
+
'relative overflow-hidden rounded-lg border border-border bg-background p-5',
|
|
350
|
+
addr.isDefault && 'ring-1 ring-primary/40',
|
|
351
|
+
)}
|
|
352
|
+
>
|
|
353
|
+
{addr.isDefault ? (
|
|
354
|
+
<span className="absolute right-0 top-0 rounded-bl-md bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
|
|
355
|
+
Predeterminada
|
|
356
|
+
</span>
|
|
357
|
+
) : null}
|
|
358
|
+
<div className="flex items-start gap-3 pr-24">
|
|
359
|
+
<span className="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
360
|
+
<MapPin className="size-5" />
|
|
361
|
+
</span>
|
|
362
|
+
<div className="min-w-0 flex-1">
|
|
363
|
+
<p className="font-medium text-foreground">{addr.addressKey}</p>
|
|
364
|
+
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">
|
|
365
|
+
{[addr.addressLine, addr.addressNumber].filter(Boolean).join(' ')}
|
|
366
|
+
<br />
|
|
367
|
+
{addr.district}, {addr.province}, {addr.department}
|
|
368
|
+
</p>
|
|
369
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
370
|
+
{addr.countryIsoCode} · CP {addr.postalCode}
|
|
371
|
+
</p>
|
|
372
|
+
{addr.reference ? (
|
|
373
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
374
|
+
Ref: {addr.reference}
|
|
375
|
+
</p>
|
|
376
|
+
) : null}
|
|
377
|
+
{(addr.phone || addr.cellPhone) && (
|
|
378
|
+
<p className="mt-2 text-xs text-muted-foreground">
|
|
379
|
+
{addr.phone}
|
|
380
|
+
{addr.cellPhone && addr.cellPhone !== addr.phone
|
|
381
|
+
? ` · ${addr.cellPhone}`
|
|
382
|
+
: ''}
|
|
383
|
+
</p>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div className="mt-4 flex flex-wrap gap-2 border-t border-border pt-4">
|
|
388
|
+
<Button
|
|
389
|
+
type="button"
|
|
390
|
+
variant="outline"
|
|
391
|
+
size="sm"
|
|
392
|
+
className="gap-1"
|
|
393
|
+
onClick={() => openEdit(addr)}
|
|
394
|
+
disabled={!addr.id}
|
|
395
|
+
>
|
|
396
|
+
<Pencil className="size-3.5" />
|
|
397
|
+
Editar
|
|
398
|
+
</Button>
|
|
399
|
+
<Button
|
|
400
|
+
type="button"
|
|
401
|
+
variant="outline"
|
|
402
|
+
size="sm"
|
|
403
|
+
className="gap-1"
|
|
404
|
+
disabled={!addr.id || addr.isDefault}
|
|
405
|
+
onClick={() => void handleSetDefault(addr)}
|
|
406
|
+
>
|
|
407
|
+
<Star className="size-3.5" />
|
|
408
|
+
Predeterminada
|
|
409
|
+
</Button>
|
|
410
|
+
<Button
|
|
411
|
+
type="button"
|
|
412
|
+
variant="ghost"
|
|
413
|
+
size="sm"
|
|
414
|
+
className="ml-auto gap-1 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
415
|
+
disabled={!addr.id}
|
|
416
|
+
onClick={() => openDelete(addr)}
|
|
417
|
+
>
|
|
418
|
+
<Trash2 className="size-3.5" />
|
|
419
|
+
Eliminar
|
|
420
|
+
</Button>
|
|
421
|
+
</div>
|
|
422
|
+
</li>
|
|
423
|
+
))}
|
|
424
|
+
</ul>
|
|
425
|
+
) : null}
|
|
426
|
+
|
|
427
|
+
<dialog
|
|
428
|
+
ref={dialogRef}
|
|
429
|
+
className="fixed left-1/2 top-1/2 z-50 m-0 max-h-[90vh] w-[min(100%-2rem,36rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-lg border border-border bg-background p-0 shadow-lg open:flex open:flex-col backdrop:bg-black/40"
|
|
430
|
+
onClose={() => {
|
|
431
|
+
setEditing(null)
|
|
432
|
+
setForm(emptyDefaults)
|
|
433
|
+
setFieldErrors({})
|
|
434
|
+
setFormError(null)
|
|
435
|
+
}}
|
|
436
|
+
>
|
|
437
|
+
<form className="space-y-4 p-6" onSubmit={(e) => void handleSubmit(e)}>
|
|
438
|
+
<h3 className="text-base font-medium text-foreground">
|
|
439
|
+
{editing ? 'Editar dirección' : 'Nueva dirección'}
|
|
440
|
+
</h3>
|
|
441
|
+
|
|
442
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
443
|
+
<CheckoutTextField
|
|
444
|
+
id={`${formId}-addressKey`}
|
|
445
|
+
label="Identificador"
|
|
446
|
+
value={form.addressKey}
|
|
447
|
+
error={fieldErrors.addressKey}
|
|
448
|
+
placeholder="Casa, Oficina…"
|
|
449
|
+
onChange={(v) => {
|
|
450
|
+
clearFieldError('addressKey')
|
|
451
|
+
patchForm({ addressKey: v })
|
|
452
|
+
}}
|
|
453
|
+
/>
|
|
454
|
+
<CheckoutTextField
|
|
455
|
+
id={`${formId}-postalCode`}
|
|
456
|
+
label="Código postal"
|
|
457
|
+
value={form.postalCode}
|
|
458
|
+
error={fieldErrors.postalCode}
|
|
459
|
+
placeholder="15074"
|
|
460
|
+
onChange={(v) => {
|
|
461
|
+
clearFieldError('postalCode')
|
|
462
|
+
patchForm({ postalCode: v })
|
|
463
|
+
}}
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<CheckoutTextField
|
|
468
|
+
id={`${formId}-addressLine`}
|
|
469
|
+
label="Dirección (calle / av.)"
|
|
470
|
+
value={form.addressLine}
|
|
471
|
+
error={fieldErrors.addressLine}
|
|
472
|
+
placeholder="Av. Principal"
|
|
473
|
+
onChange={(v) => {
|
|
474
|
+
clearFieldError('addressLine')
|
|
475
|
+
patchForm({ addressLine: v })
|
|
476
|
+
}}
|
|
477
|
+
/>
|
|
478
|
+
|
|
479
|
+
<CheckoutTextField
|
|
480
|
+
id={`${formId}-addressNumber`}
|
|
481
|
+
label="Número / interior"
|
|
482
|
+
value={form.addressNumber}
|
|
483
|
+
error={fieldErrors.addressNumber}
|
|
484
|
+
placeholder="123, Dpto. 4"
|
|
485
|
+
onChange={(v) => {
|
|
486
|
+
clearFieldError('addressNumber')
|
|
487
|
+
patchForm({ addressNumber: v })
|
|
488
|
+
}}
|
|
489
|
+
/>
|
|
490
|
+
|
|
491
|
+
<div>
|
|
492
|
+
<label
|
|
493
|
+
htmlFor={`${formId}-country`}
|
|
494
|
+
className="text-sm font-medium text-foreground"
|
|
495
|
+
>
|
|
496
|
+
País
|
|
497
|
+
</label>
|
|
498
|
+
<select
|
|
499
|
+
id={`${formId}-country`}
|
|
500
|
+
className={fieldClass()}
|
|
501
|
+
value={DEFAULT_COUNTRY_CODE}
|
|
502
|
+
disabled
|
|
503
|
+
>
|
|
504
|
+
<option value={DEFAULT_COUNTRY_CODE}>Perú</option>
|
|
505
|
+
</select>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
509
|
+
<CheckoutSelectField
|
|
510
|
+
id={`${formId}-department`}
|
|
511
|
+
label="Departamento"
|
|
512
|
+
value={form.departmentCode}
|
|
513
|
+
error={fieldErrors.departmentCode || fieldErrors.departmentName}
|
|
514
|
+
options={geo.departments.map((d) => ({
|
|
515
|
+
value: d.code,
|
|
516
|
+
label: d.name,
|
|
517
|
+
}))}
|
|
518
|
+
onChange={(code) => {
|
|
519
|
+
clearFieldError('departmentCode')
|
|
520
|
+
clearFieldError('departmentName')
|
|
521
|
+
const name =
|
|
522
|
+
geo.departments.find((d) => d.code === code)?.name ?? ''
|
|
523
|
+
patchForm({
|
|
524
|
+
departmentCode: code,
|
|
525
|
+
departmentName: name,
|
|
526
|
+
provinceCode: '',
|
|
527
|
+
provinceName: '',
|
|
528
|
+
districtCode: '',
|
|
529
|
+
districtName: '',
|
|
530
|
+
})
|
|
531
|
+
}}
|
|
532
|
+
/>
|
|
533
|
+
<CheckoutSelectField
|
|
534
|
+
id={`${formId}-province`}
|
|
535
|
+
label="Provincia"
|
|
536
|
+
value={form.provinceCode}
|
|
537
|
+
error={fieldErrors.provinceCode || fieldErrors.provinceName}
|
|
538
|
+
options={geo.provinces.map((p) => ({
|
|
539
|
+
value: p.code,
|
|
540
|
+
label: p.name,
|
|
541
|
+
}))}
|
|
542
|
+
onChange={(code) => {
|
|
543
|
+
clearFieldError('provinceCode')
|
|
544
|
+
clearFieldError('provinceName')
|
|
545
|
+
const name =
|
|
546
|
+
geo.provinces.find((p) => p.code === code)?.name ?? ''
|
|
547
|
+
patchForm({
|
|
548
|
+
provinceCode: code,
|
|
549
|
+
provinceName: name,
|
|
550
|
+
districtCode: '',
|
|
551
|
+
districtName: '',
|
|
552
|
+
})
|
|
553
|
+
}}
|
|
554
|
+
/>
|
|
555
|
+
<CheckoutSelectField
|
|
556
|
+
id={`${formId}-district`}
|
|
557
|
+
label="Distrito"
|
|
558
|
+
value={form.districtCode}
|
|
559
|
+
error={fieldErrors.districtCode || fieldErrors.districtName}
|
|
560
|
+
options={geo.districts.map((d) => ({
|
|
561
|
+
value: d.code,
|
|
562
|
+
label: d.name,
|
|
563
|
+
}))}
|
|
564
|
+
onChange={(code) => {
|
|
565
|
+
clearFieldError('districtCode')
|
|
566
|
+
clearFieldError('districtName')
|
|
567
|
+
const name =
|
|
568
|
+
geo.districts.find((d) => d.code === code)?.name ?? ''
|
|
569
|
+
patchForm({
|
|
570
|
+
districtCode: code,
|
|
571
|
+
districtName: name,
|
|
572
|
+
})
|
|
573
|
+
}}
|
|
574
|
+
/>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<div>
|
|
578
|
+
<label
|
|
579
|
+
htmlFor={`${formId}-reference`}
|
|
580
|
+
className="text-sm font-medium text-foreground"
|
|
581
|
+
>
|
|
582
|
+
Referencia (opcional)
|
|
583
|
+
</label>
|
|
584
|
+
<textarea
|
|
585
|
+
id={`${formId}-reference`}
|
|
586
|
+
rows={2}
|
|
587
|
+
className={cn(fieldClass(), 'h-auto py-2')}
|
|
588
|
+
value={form.reference}
|
|
589
|
+
placeholder="Puerta azul, portón negro…"
|
|
590
|
+
onChange={(e) => patchForm({ reference: e.target.value })}
|
|
591
|
+
/>
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
595
|
+
<CheckoutTextField
|
|
596
|
+
id={`${formId}-phone`}
|
|
597
|
+
label="Teléfono"
|
|
598
|
+
value={form.phone}
|
|
599
|
+
error={fieldErrors.phone}
|
|
600
|
+
placeholder="+51999999999"
|
|
601
|
+
onChange={(v) => {
|
|
602
|
+
clearFieldError('phone')
|
|
603
|
+
patchForm({ phone: v })
|
|
604
|
+
}}
|
|
605
|
+
/>
|
|
606
|
+
<CheckoutTextField
|
|
607
|
+
id={`${formId}-cellPhone`}
|
|
608
|
+
label="Celular (opcional)"
|
|
609
|
+
value={form.cellPhone}
|
|
610
|
+
error={fieldErrors.cellPhone}
|
|
611
|
+
placeholder="+51999999999"
|
|
612
|
+
onChange={(v) => {
|
|
613
|
+
clearFieldError('cellPhone')
|
|
614
|
+
patchForm({ cellPhone: v })
|
|
615
|
+
}}
|
|
616
|
+
/>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
<label className="flex items-center gap-2 text-sm text-foreground">
|
|
620
|
+
<input
|
|
621
|
+
type="checkbox"
|
|
622
|
+
className="size-4 rounded border-border"
|
|
623
|
+
checked={form.isDefault}
|
|
624
|
+
onChange={(e) => patchForm({ isDefault: e.target.checked })}
|
|
625
|
+
/>
|
|
626
|
+
Usar como dirección predeterminada
|
|
627
|
+
</label>
|
|
628
|
+
|
|
629
|
+
{formError ? <FieldError message={formError} /> : null}
|
|
630
|
+
|
|
631
|
+
<div className="flex flex-wrap justify-end gap-2 pt-2">
|
|
632
|
+
<Button type="button" variant="outline" onClick={closeDialog}>
|
|
633
|
+
Cancelar
|
|
634
|
+
</Button>
|
|
635
|
+
<Button type="submit" disabled={saving}>
|
|
636
|
+
{saving ? <Loader2 className="size-4 animate-spin" /> : 'Guardar'}
|
|
637
|
+
</Button>
|
|
638
|
+
</div>
|
|
639
|
+
</form>
|
|
640
|
+
</dialog>
|
|
641
|
+
|
|
642
|
+
<dialog
|
|
643
|
+
ref={deleteDialogRef}
|
|
644
|
+
className="fixed left-1/2 top-1/2 z-50 m-0 w-[min(100%-2rem,24rem)] -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-background p-6 shadow-lg backdrop:bg-black/40"
|
|
645
|
+
onClose={() => setDeleteTarget(null)}
|
|
646
|
+
>
|
|
647
|
+
<h3 className="text-base font-medium text-foreground">
|
|
648
|
+
¿Eliminar esta dirección?
|
|
649
|
+
</h3>
|
|
650
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
651
|
+
Esta acción no se puede deshacer. La dirección dejará de aparecer en tu
|
|
652
|
+
cuenta.
|
|
653
|
+
</p>
|
|
654
|
+
{actionError ? (
|
|
655
|
+
<p className="mt-3 text-sm text-destructive">{actionError}</p>
|
|
656
|
+
) : null}
|
|
657
|
+
<div className="mt-6 flex flex-wrap justify-end gap-2">
|
|
658
|
+
<Button
|
|
659
|
+
type="button"
|
|
660
|
+
variant="outline"
|
|
661
|
+
disabled={deleting}
|
|
662
|
+
onClick={closeDelete}
|
|
663
|
+
>
|
|
664
|
+
Cancelar
|
|
665
|
+
</Button>
|
|
666
|
+
<Button
|
|
667
|
+
type="button"
|
|
668
|
+
variant="destructive"
|
|
669
|
+
disabled={deleting}
|
|
670
|
+
onClick={() => void confirmDelete()}
|
|
671
|
+
>
|
|
672
|
+
{deleting ? <Loader2 className="size-4 animate-spin" /> : 'Eliminar'}
|
|
673
|
+
</Button>
|
|
674
|
+
</div>
|
|
675
|
+
</dialog>
|
|
676
|
+
</div>
|
|
677
|
+
)
|
|
678
|
+
}
|