@ecomiq/storefront 2.1.4 → 3.0.0
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/LICENSE +21 -0
- package/README.md +150 -103
- package/dist/account-Dx4e3syi.js +258 -0
- package/dist/account-forms-AVk71ko7.js +39 -0
- package/dist/account-forms.d.ts +13 -0
- package/dist/account.d.ts +240 -0
- package/dist/address-defaults-BqjRhS82.js +14 -0
- package/dist/address-defaults.d.ts +11 -0
- package/dist/analytics/analytics-CG1Qagwl.js +148 -0
- package/dist/analytics/analytics.d.ts +1 -0
- package/dist/analytics/config-DJr-GRhs.js +62 -0
- package/dist/analytics/config.d.ts +22 -0
- package/dist/analytics/consent-B13rBpBs.js +57 -0
- package/dist/analytics/consent-banner-9QvRRy8v.js +239 -0
- package/dist/analytics/consent-banner.d.ts +20 -0
- package/dist/analytics/cookies-pLozllyB.js +100 -0
- package/dist/analytics/cookies.d.ts +6 -0
- package/dist/analytics/destinations-iECrlLIr.js +144 -0
- package/dist/analytics/events-Dt81tIp1.js +102 -0
- package/dist/analytics/events.d.ts +51 -0
- package/dist/analytics/tracker-p4yaoOyk.js +51 -0
- package/dist/api-cache-DsuIn5hX.js +159 -0
- package/dist/bank-accounts-DlEczQQ5.js +81 -0
- package/dist/bank-accounts.d.ts +4 -0
- package/dist/captcha-DAqaIx8m.js +117 -0
- package/dist/captcha.d.ts +34 -0
- package/dist/cart-D7DxZ2lD.js +30 -0
- package/dist/cart-actions-Bsub_FFG.js +116 -0
- package/dist/cart-actions.d.ts +15 -0
- package/dist/cart-drawer-B7PPsR69.js +22 -0
- package/dist/cart-drawer.d.ts +9 -0
- package/dist/cart-form-y9P1r_EX.js +81 -0
- package/dist/cart-form.d.ts +24 -0
- package/dist/cart-summary-ur2VWKr-.js +89 -0
- package/dist/cart-summary.d.ts +31 -0
- package/dist/cart.d.ts +4 -0
- package/dist/catalog-7gJ-l5hW.js +67 -0
- package/dist/catalog-hooks-B7ZP4wYY.js +312 -0
- package/dist/catalog-hooks.d.ts +111 -0
- package/dist/catalog.d.ts +5 -0
- package/dist/checkout-DVWSXANi.js +474 -0
- package/dist/checkout-form-CbOIdENC.js +131 -0
- package/dist/checkout-form.d.ts +20 -0
- package/dist/checkout.d.ts +86 -0
- package/dist/complaint-book-BvwLvfx0.js +117 -0
- package/dist/complaint-book.d.ts +62 -0
- package/dist/culqi-authentication-BGr20vAJ.js +204 -0
- package/dist/culqi-authentication.d.ts +15 -0
- package/dist/culqi-checkout-CTXpKgp_.js +55 -0
- package/dist/culqi-three-ds-HP3mfrvV.js +33 -0
- package/dist/culqi-three-ds.d.ts +29 -0
- package/dist/customer-avatar-CJXSDroK.js +12 -0
- package/dist/customer-avatar.d.ts +5 -0
- package/dist/customer-cookies-DHOgFAMV.js +45 -0
- package/dist/customer-csrf-C1oDX-Py.js +17 -0
- package/dist/customer-csrf.d.ts +3 -0
- package/dist/customer-document-fields-C1K-_DAD.js +70 -0
- package/dist/customer-document-fields.d.ts +9 -0
- package/dist/customer-login-form-C7AC6vXd.js +144 -0
- package/dist/customer-login-form.d.ts +17 -0
- package/dist/customer-registration-form-BEm_NEfP.js +93 -0
- package/dist/customer-registration-form.d.ts +8 -0
- package/dist/document-url-CGsAl5Tf.js +9 -0
- package/dist/documents-T1mzc_j8.js +38 -0
- package/dist/documents.d.ts +16 -0
- package/dist/entries/account.d.ts +11 -0
- package/dist/entries/account.js +10 -0
- package/dist/entries/analytics.d.ts +6 -0
- package/dist/entries/analytics.js +5 -0
- package/dist/entries/cart.d.ts +8 -0
- package/dist/entries/cart.js +7 -0
- package/dist/entries/checkout.d.ts +12 -0
- package/dist/entries/checkout.js +11 -0
- package/dist/entries/order.d.ts +11 -0
- package/dist/entries/order.js +11 -0
- package/dist/favicon-YpyhVJ9H.js +28 -0
- package/dist/favicon.d.ts +4 -0
- package/dist/form-BtQcbXlJ.js +18 -0
- package/dist/form-contracts-BNpHaTGJ.js +166 -0
- package/dist/form-contracts.d.ts +42 -0
- package/dist/form.d.ts +3 -0
- package/dist/hidden-CCWtOWKR.js +3 -0
- package/dist/hidden.d.ts +4 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +50 -0
- package/dist/invoice-fields-BG0HjL6T.js +103 -0
- package/dist/invoice-fields.d.ts +39 -0
- package/dist/links-kFxBZfdb.js +96 -0
- package/dist/links.d.ts +21 -0
- package/dist/mercadopago-checkout-DC7RXa8k.js +27 -0
- package/dist/mercadopago-dialog-UTQev0MG.js +180 -0
- package/dist/mercadopago-dialog.d.ts +21 -0
- package/dist/money-DtGOtDri.js +32 -0
- package/dist/money.d.ts +1 -0
- package/dist/optimistic-cart-BveVOHuh.js +53 -0
- package/dist/optimistic-cart.d.ts +16 -0
- package/dist/order-display-DTt5WBbc.js +112 -0
- package/dist/order-display.d.ts +20 -0
- package/dist/order-draft-DxV1Uhm3.js +38 -0
- package/dist/order-draft.d.ts +16 -0
- package/dist/order-pages-Dm6l5m0m.js +38 -0
- package/dist/order-pages.d.ts +12 -0
- package/dist/order-payment-continuation-D_d5PGA4.js +212 -0
- package/dist/order-payment-continuation.d.ts +10 -0
- package/dist/order-payment-in-review-x4gdLFHZ.js +65 -0
- package/dist/order-payment-in-review.d.ts +5 -0
- package/dist/order-status-BJXLJwxq.js +162 -0
- package/dist/order-status.d.ts +89 -0
- package/dist/order-view-9IlBg5AF.js +62 -0
- package/dist/order-view.d.ts +12 -0
- package/dist/page-action-Ctkf4xC6.js +7 -0
- package/dist/page-seo-a5FJl8x5.js +262 -0
- package/dist/page-seo.d.ts +34 -0
- package/dist/pages-Ca_Gt6mq.js +150 -0
- package/dist/pages.d.ts +45 -0
- package/dist/payment-dialog-frame-B-Q_xQ94.js +57 -0
- package/dist/payment-logo-BoPaGJc_.js +46 -0
- package/dist/payment-logo-svgs-By9lDTLK.js +20 -0
- package/dist/payment-logo.d.ts +11 -0
- package/dist/payment-logos-BYvdwYyJ.js +23 -0
- package/dist/payment-methods-B8ocTgFZ.js +73 -0
- package/dist/payment-methods.d.ts +58 -0
- package/dist/payment-return-5cjypwIw.js +7 -0
- package/dist/place-CmW4hBWY.js +13 -0
- package/dist/place.d.ts +4 -0
- package/dist/preview-BiIBif3C.js +427 -0
- package/dist/preview-address-Cf0sJ-9p.js +21 -0
- package/dist/preview-cart-Dg2CVktS.js +376 -0
- package/dist/product-preview-BspMAecJ.js +51 -0
- package/dist/product-preview.d.ts +5 -0
- package/dist/product-variants-BNDHvs1y.js +46 -0
- package/dist/product-variants.d.ts +12 -0
- package/dist/public-pages-XtwlaLwt.js +194 -0
- package/dist/public-pages.d.ts +67 -0
- package/dist/registration-CpDcv8ql.js +91 -0
- package/dist/registration.d.ts +12 -0
- package/dist/request-security-CRsRMgwj.js +76 -0
- package/dist/rich-text-M-Ju8RL4.js +9 -0
- package/dist/rich-text.d.ts +5 -0
- package/dist/route-text-DcJw2SsK.js +9 -0
- package/dist/route-text.d.ts +4 -0
- package/dist/section-preview-CmsMl3yu.js +83 -0
- package/dist/section-preview.d.ts +12 -0
- package/dist/sections/account-addresses.d.ts +18 -0
- package/dist/sections/account-addresses.js +375 -0
- package/dist/sections/account-order.d.ts +12 -0
- package/dist/sections/account-order.js +592 -0
- package/dist/sections/account-orders.d.ts +8 -0
- package/dist/sections/account-orders.js +359 -0
- package/dist/sections/account-profile.d.ts +16 -0
- package/dist/sections/account-profile.js +123 -0
- package/dist/sections/checkout.d.ts +24 -0
- package/dist/sections/checkout.js +963 -0
- package/dist/sections/complaint-book.d.ts +8 -0
- package/dist/sections/complaint-book.js +538 -0
- package/dist/sections/design-system.d.ts +1 -0
- package/dist/sections/design-system.js +310 -0
- package/dist/sections/draft-invoice.d.ts +20 -0
- package/dist/sections/draft-invoice.js +158 -0
- package/dist/sections/order-tracking.d.ts +19 -0
- package/dist/sections/order-tracking.js +141 -0
- package/dist/sections-CNUMWTF_.js +77 -0
- package/dist/sections.d.ts +19 -0
- package/dist/seo-GpDRhAAA.js +78 -0
- package/dist/session-CKvmFJyg.js +379 -0
- package/dist/session.d.ts +24 -0
- package/dist/settings-B0NO2A6Y.js +78 -0
- package/dist/settings.d.ts +51 -0
- package/dist/source-marker-DhqSkW7n.js +4 -0
- package/dist/sources-B-3JFJfb.js +46 -0
- package/dist/sources.d.ts +7 -0
- package/dist/store-8Q-_LFrQ.js +152 -0
- package/dist/store-contact--hY6CoFb.js +19 -0
- package/dist/store-contact.d.ts +6 -0
- package/dist/store.d.ts +17 -0
- package/dist/storefront-language-DrMjVbmk.js +29 -0
- package/dist/storefront-language.d.ts +14 -0
- package/dist/storefront-text-D25ae1qf.js +11 -0
- package/dist/storefront-text-en-ljjjSS4q.js +1030 -0
- package/dist/storefront-text-pt-BtHaJLcp.js +1030 -0
- package/dist/storefront-text.d.ts +4 -0
- package/dist/stripe-dialog-Ev9s3Z62.js +396 -0
- package/dist/stripe-dialog.d.ts +30 -0
- package/dist/telemetry-HuZXWBPi.js +114 -0
- package/dist/telemetry.d.ts +10 -0
- package/dist/templates-CAqoNyTW.js +40 -0
- package/dist/templates.d.ts +15 -0
- package/dist/urls-CpGQPxvf.js +71 -0
- package/dist/urls.d.ts +18 -0
- package/dist/use-checkout-CZcxXJXi.js +42 -0
- package/dist/use-checkout-payment-9R-zacrJ.js +80 -0
- package/dist/use-checkout-payment.d.ts +18 -0
- package/dist/use-checkout.d.ts +36 -0
- package/dist/use-culqi-checkout-CrjLAipU.js +109 -0
- package/dist/use-order-payment-BKIz_IO0.js +45 -0
- package/dist/use-order-payment.d.ts +18 -0
- package/dist/use-payment-token-CErx-tN-.js +105 -0
- package/dist/vite-payment-logos-kziu7wKb.js +27 -0
- package/dist/vite-payment-logos.d.ts +24 -0
- package/dist/vite-public-assets-DW2fgtVb.js +209 -0
- package/dist/vite-public-assets.d.ts +29 -0
- package/dist/vite-sections-DTBr0-Kg.js +464 -0
- package/dist/vite-sections.d.ts +21 -0
- package/dist/vite-theme-ui-BMO-CDi3.js +41 -0
- package/dist/vite-theme-ui.d.ts +10 -0
- package/dist/vite.d.ts +36 -0
- package/dist/vite.js +127 -0
- package/dist/voucher-payment-CVFDZ1L8.js +66 -0
- package/dist/worker-CNi-gzCu.js +86 -0
- package/dist/worker.d.ts +27 -0
- package/package.json +100 -76
- package/src/sections/account-addresses.tsx +215 -0
- package/src/sections/account-order.tsx +424 -0
- package/src/sections/account-orders.tsx +259 -0
- package/src/sections/account-profile.tsx +70 -0
- package/src/sections/checkout.tsx +586 -0
- package/src/sections/complaint-book.tsx +325 -0
- package/src/sections/design-system.tsx +146 -0
- package/src/sections/draft-invoice.tsx +105 -0
- package/src/sections/order-tracking.tsx +81 -0
- package/src/vite-client.d.ts +4 -0
- package/bin/ecomiq.mjs +0 -179
- package/public/design-bridge.js +0 -318
- package/src/api-proxy.ts +0 -194
- package/src/auth/create-storefront-auth.ts +0 -94
- package/src/auth/ecomiq-oauth-password-client.ts +0 -26
- package/src/auth/ecomiq-oauth-password-constants.ts +0 -24
- package/src/auth/ecomiq-oauth-password-plugin.ts +0 -294
- package/src/auth/ecomiq-oauth-password-types.ts +0 -24
- package/src/auth/ecomiq-storefront-sign-in.ts +0 -44
- package/src/auth/env.ts +0 -7
- package/src/auth/secure-cookies.ts +0 -23
- package/src/auth/storefront-auth-base-url.ts +0 -117
- package/src/auth/storefront-auth.ts +0 -7
- package/src/cart-session.ts +0 -239
- package/src/client.ts +0 -48
- package/src/codegen.mjs +0 -801
- package/src/components/account/customer-addresses-panel.tsx +0 -678
- package/src/components/account/customer-order-detail-panel.tsx +0 -285
- package/src/components/account/customer-orders-panel.tsx +0 -192
- package/src/components/account/customer-profile-panel.tsx +0 -379
- package/src/components/checkout/cart-sync.ts +0 -143
- package/src/components/checkout/checkout-contact-section.tsx +0 -76
- package/src/components/checkout/checkout-external-update-banner.tsx +0 -15
- package/src/components/checkout/checkout-form-controls.tsx +0 -139
- package/src/components/checkout/checkout-line-items.tsx +0 -43
- package/src/components/checkout/checkout-order-summary.tsx +0 -165
- package/src/components/checkout/checkout-payment-section.tsx +0 -212
- package/src/components/checkout/checkout-session-view.tsx +0 -564
- package/src/components/checkout/checkout-shipping-address-section.tsx +0 -400
- package/src/components/checkout/checkout-shipping-section.tsx +0 -98
- package/src/components/checkout/checkout-states.tsx +0 -73
- package/src/components/checkout/checkout-step-card.tsx +0 -101
- package/src/components/checkout/checkout-steps.ts +0 -111
- package/src/components/checkout/form-schema.ts +0 -281
- package/src/components/checkout/format.ts +0 -15
- package/src/components/checkout/index.ts +0 -5
- package/src/components/checkout/map-customer-to-checkout.ts +0 -188
- package/src/components/checkout/types.ts +0 -41
- package/src/components/checkout/use-checkout-customer-prefill.ts +0 -409
- package/src/components/checkout/use-checkout-geo.ts +0 -50
- package/src/components/checkout/use-checkout-session.ts +0 -640
- package/src/components/hero.tsx +0 -291
- package/src/config.d.ts +0 -94
- package/src/config.mjs +0 -146
- package/src/format-money.ts +0 -31
- package/src/health.ts +0 -90
- package/src/hooks/index.ts +0 -9
- package/src/hooks/recently-viewed.ts +0 -125
- package/src/index.ts +0 -349
- package/src/link.tsx +0 -2
- package/src/pages/account/account-page-skeleton.tsx +0 -69
- package/src/pages/account/account-page-view.tsx +0 -270
- package/src/pages/account/account-shell.tsx +0 -101
- package/src/pages/account/parse-account-route.ts +0 -36
- package/src/pages/account.tsx +0 -16
- package/src/pages/api.$.ts +0 -20
- package/src/pages/api.auth.$.ts +0 -16
- package/src/pages/checkout.$checkoutId.tsx +0 -60
- package/src/pages/checkout.tsx +0 -31
- package/src/pages/pages.$pageSlug.tsx +0 -74
- package/src/pages/thank-you.tsx +0 -53
- package/src/providers/auth.tsx +0 -96
- package/src/providers/cart.tsx +0 -275
- package/src/providers/ecomiq.ts +0 -137
- package/src/providers/index.ts +0 -26
- package/src/proxy-api.ts +0 -20
- package/src/public-config.ts +0 -24
- package/src/review-submission.ts +0 -89
- package/src/runtime/context.ts +0 -2
- package/src/runtime/default-layout.tsx +0 -64
- package/src/runtime/head.ts +0 -149
- package/src/runtime/page.tsx +0 -143
- package/src/runtime/postcss-browser-stub.mjs +0 -5
- package/src/server-config-store.ts +0 -35
- package/src/server.ts +0 -286
- package/src/styles.css +0 -76
- package/src/ui/badge.tsx +0 -32
- package/src/ui/button.tsx +0 -31
- package/src/ui/card.tsx +0 -31
- package/src/ui/carousel.tsx +0 -218
- package/src/ui/cn.ts +0 -6
- package/src/ui/index.ts +0 -8
- package/src/ui/separator.tsx +0 -6
- package/src/ui/skeleton.tsx +0 -6
- package/src/ui/storefront-error.tsx +0 -49
- package/src/utils.ts +0 -36
- package/src/vite-inline.mjs +0 -263
- package/templates/__root.tsx +0 -83
- package/templates/router.tsx +0 -19
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { CustomerAddress } from "@ecomiq/core";
|
|
2
|
+
import { useState, type ReactNode } from "react";
|
|
3
|
+
import { useNavigation } from "react-router";
|
|
4
|
+
import { Badge } from "@theme/ui/badge";
|
|
5
|
+
import { Button } from "@theme/ui/button";
|
|
6
|
+
import { Input } from "@theme/ui/input";
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@theme/ui/select";
|
|
8
|
+
import { useStorefrontText, type SectionCatalog } from "../index";
|
|
9
|
+
import { AddressForm, addressDefaults, placeValue, useGeoCascade, type AddressActionData, type AddressDefaults } from "../entries/account";
|
|
10
|
+
|
|
11
|
+
export const catalog: SectionCatalog = {
|
|
12
|
+
name: "Mis direcciones",
|
|
13
|
+
description: "Lista, agrega y edita las direcciones del cliente.",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Country = { code: string; name: string };
|
|
17
|
+
type Place = { code: string; name: string };
|
|
18
|
+
|
|
19
|
+
function Labeled({ label, className = "", children }: { label: string; className?: string; children: ReactNode }) {
|
|
20
|
+
return (
|
|
21
|
+
<div className={`flex flex-col gap-1.5 text-sm ${className}`}>
|
|
22
|
+
<span className="font-medium">{label}</span>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Choice({ name, label, value, items, disabled, required, onChange }: { name: string; label: string; value: string; items: { value: string; name: string }[]; disabled?: boolean; required?: boolean; onChange: (value: string) => void }) {
|
|
29
|
+
const storefrontTranslate = useStorefrontText();
|
|
30
|
+
return (
|
|
31
|
+
<Labeled label={label}>
|
|
32
|
+
<Select name={name} required={required} disabled={disabled} value={value} onValueChange={onChange}>
|
|
33
|
+
<SelectTrigger aria-label={label} className="h-11 w-full data-[size=default]:h-11">
|
|
34
|
+
<SelectValue placeholder={storefrontTranslate("Elige…")}>{items.find((item) => item.value === value)?.name}</SelectValue>
|
|
35
|
+
</SelectTrigger>
|
|
36
|
+
<SelectContent className="max-h-72">
|
|
37
|
+
{items.map((item) => (
|
|
38
|
+
<SelectItem key={item.value} value={item.value}>
|
|
39
|
+
{item.name}
|
|
40
|
+
</SelectItem>
|
|
41
|
+
))}
|
|
42
|
+
</SelectContent>
|
|
43
|
+
</Select>
|
|
44
|
+
</Labeled>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function PlaceSelect({ name, label, items, value, disabled, onChange }: { name: string; label: string; items: Place[]; value: Place | null; disabled?: boolean; onChange: (value: string) => void }) {
|
|
49
|
+
return (
|
|
50
|
+
<Choice
|
|
51
|
+
name={name}
|
|
52
|
+
label={label}
|
|
53
|
+
required
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
value={placeValue(value)}
|
|
56
|
+
items={items.map((item) => ({ value: placeValue(item), name: item.name }))}
|
|
57
|
+
onChange={onChange}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function AddressFields({ countries, defaults, label }: { countries: Country[]; defaults: AddressDefaults; label: string }) {
|
|
63
|
+
const storefrontTranslate = useStorefrontText();
|
|
64
|
+
const geo = useGeoCascade({ ...defaults, country: defaults.country ?? countries[0]?.code });
|
|
65
|
+
return (
|
|
66
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
67
|
+
<Labeled label={storefrontTranslate("Nombre de la dirección")} className="sm:col-span-2">
|
|
68
|
+
<Input name="label" defaultValue={label} placeholder={storefrontTranslate("Casa, oficina…")} maxLength={50} />
|
|
69
|
+
</Labeled>
|
|
70
|
+
{countries.length > 1 ? (
|
|
71
|
+
<div className="sm:col-span-2">
|
|
72
|
+
<Choice name="country" label={storefrontTranslate("País")} value={geo.country} items={countries.map((item) => ({ value: item.code, name: item.name }))} onChange={geo.setCountry} />
|
|
73
|
+
</div>
|
|
74
|
+
) : (
|
|
75
|
+
<input type="hidden" name="country" value={geo.country} />
|
|
76
|
+
)}
|
|
77
|
+
{geo.structured ? (
|
|
78
|
+
<>
|
|
79
|
+
<PlaceSelect name="department" label={storefrontTranslate("Departamento")} items={geo.places.departments} value={geo.department} onChange={geo.setDepartmentValue} />
|
|
80
|
+
<PlaceSelect name="province" label={storefrontTranslate("Provincia")} items={geo.places.provinces} value={geo.province} disabled={geo.department === null} onChange={geo.setProvinceValue} />
|
|
81
|
+
<PlaceSelect name="district" label={storefrontTranslate("Distrito")} items={geo.places.districts} value={geo.district} disabled={geo.province === null} onChange={geo.setDistrictValue} />
|
|
82
|
+
</>
|
|
83
|
+
) : (
|
|
84
|
+
<>
|
|
85
|
+
<Labeled label={storefrontTranslate("Región")}>
|
|
86
|
+
<Input name="department" defaultValue={defaults.department ?? ""} required />
|
|
87
|
+
</Labeled>
|
|
88
|
+
<Labeled label={storefrontTranslate("Ciudad")}>
|
|
89
|
+
<Input name="district" defaultValue={defaults.district ?? ""} required />
|
|
90
|
+
</Labeled>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
<Labeled label={storefrontTranslate("Dirección")} className="sm:col-span-2">
|
|
94
|
+
<Input name="line" autoComplete="street-address" defaultValue={defaults.line ?? ""} required />
|
|
95
|
+
</Labeled>
|
|
96
|
+
<Labeled label={storefrontTranslate("Referencia (opcional)")}>
|
|
97
|
+
<Input name="reference" defaultValue={defaults.reference ?? ""} />
|
|
98
|
+
</Labeled>
|
|
99
|
+
<Labeled label={storefrontTranslate("Celular")}>
|
|
100
|
+
<Input name="phone" type="tel" autoComplete="tel" defaultValue={defaults.phone ?? ""} />
|
|
101
|
+
</Labeled>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function AddressEditor({ address, countries, primary, onDone }: { address: CustomerAddress | null; countries: Country[]; primary: boolean; onDone: () => void }) {
|
|
107
|
+
const storefrontTranslate = useStorefrontText();
|
|
108
|
+
const busy = useNavigation().state !== "idle";
|
|
109
|
+
return (
|
|
110
|
+
<AddressForm addressId={address?.id} className="flex flex-col gap-5 rounded-lg border bg-card p-5 text-card-foreground md:col-span-2 md:p-6" onSubmit={onDone}>
|
|
111
|
+
<h3 className="font-heading text-lg">{address ? storefrontTranslate("Editar dirección") : storefrontTranslate("Nueva dirección")}</h3>
|
|
112
|
+
<AddressFields countries={countries} defaults={addressDefaults(address)} label={address?.addressKey ?? ""} />
|
|
113
|
+
<label className="flex items-center gap-3 text-sm">
|
|
114
|
+
<input type="checkbox" name="default" defaultChecked={address?.isDefault ?? primary} className="size-4 accent-primary" />
|
|
115
|
+
{storefrontTranslate("Usar como dirección principal")}
|
|
116
|
+
</label>
|
|
117
|
+
<div className="flex flex-wrap gap-2">
|
|
118
|
+
<Button type="submit" disabled={busy}>{busy ? storefrontTranslate("Guardando…") : storefrontTranslate("Guardar dirección")}</Button>
|
|
119
|
+
<Button type="button" variant="ghost" onClick={onDone}>{storefrontTranslate("Cancelar")}</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</AddressForm>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function AddressCard({ address, onEdit }: { address: CustomerAddress; onEdit: () => void }) {
|
|
126
|
+
const storefrontTranslate = useStorefrontText();
|
|
127
|
+
const [confirming, setConfirming] = useState(false);
|
|
128
|
+
const locality = [address.district, address.province, address.department].filter(Boolean).join(", ");
|
|
129
|
+
const phone = address.cellPhone ?? address.phone;
|
|
130
|
+
return (
|
|
131
|
+
<div className="flex h-full flex-col gap-4 rounded-lg border bg-card p-5 text-sm text-card-foreground md:p-6">
|
|
132
|
+
<div className="flex items-center justify-between gap-3">
|
|
133
|
+
<p className="font-heading text-lg capitalize">{address.addressKey}</p>
|
|
134
|
+
{address.isDefault ? <Badge variant="secondary">{storefrontTranslate("Principal")}</Badge> : null}
|
|
135
|
+
</div>
|
|
136
|
+
<div className="flex flex-col gap-1">
|
|
137
|
+
<p>{address.addressLine}</p>
|
|
138
|
+
{address.reference ? <p className="text-muted-foreground">{address.reference}</p> : null}
|
|
139
|
+
{locality !== "" ? <p className="text-muted-foreground">{locality}</p> : null}
|
|
140
|
+
{phone ? <p className="text-muted-foreground">{phone}</p> : null}
|
|
141
|
+
</div>
|
|
142
|
+
{confirming ? (
|
|
143
|
+
<div className="mt-auto flex flex-wrap items-center gap-3 border-t pt-4">
|
|
144
|
+
<span>{storefrontTranslate("¿Eliminar esta dirección?")}</span>
|
|
145
|
+
<AddressForm intent="delete" addressId={address.id}>
|
|
146
|
+
<button type="submit" className="font-medium text-destructive underline underline-offset-4">{storefrontTranslate("Eliminar")}</button>
|
|
147
|
+
</AddressForm>
|
|
148
|
+
<button type="button" className="text-muted-foreground underline underline-offset-4" onClick={() => setConfirming(false)}>{storefrontTranslate("Cancelar")}</button>
|
|
149
|
+
</div>
|
|
150
|
+
) : (
|
|
151
|
+
<div className="mt-auto flex flex-wrap gap-x-4 gap-y-2 border-t pt-4">
|
|
152
|
+
<button type="button" className="font-medium underline underline-offset-4" onClick={onEdit}>{storefrontTranslate("Editar")}</button>
|
|
153
|
+
{!address.isDefault ? (
|
|
154
|
+
<AddressForm intent="default" addressId={address.id}>
|
|
155
|
+
<button type="submit" className="underline underline-offset-4">{storefrontTranslate("Hacer principal")}</button>
|
|
156
|
+
</AddressForm>
|
|
157
|
+
) : null}
|
|
158
|
+
<button type="button" className="text-muted-foreground underline underline-offset-4 hover:text-destructive" onClick={() => setConfirming(true)}>{storefrontTranslate("Eliminar")}</button>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function AccountAddresses({ loaderData, actionData }: { loaderData: { addresses: CustomerAddress[]; countries: Country[] }; actionData?: AddressActionData }) {
|
|
166
|
+
const storefrontTranslate = useStorefrontText();
|
|
167
|
+
const { addresses, countries } = loaderData;
|
|
168
|
+
const [editing, setEditing] = useState<string | null>(null);
|
|
169
|
+
const navigation = useNavigation();
|
|
170
|
+
const error = actionData?.error ?? null;
|
|
171
|
+
const sorted = [...addresses].sort((a, b) => Number(b.isDefault) - Number(a.isDefault));
|
|
172
|
+
const adding = editing === "new";
|
|
173
|
+
return (
|
|
174
|
+
<div className="flex flex-col gap-6">
|
|
175
|
+
<div className="flex flex-wrap items-baseline justify-between gap-4">
|
|
176
|
+
<h2 className="text-3xl">{storefrontTranslate("Direcciones")}</h2>
|
|
177
|
+
{adding ? null : (
|
|
178
|
+
<Button type="button" variant="outline" onClick={() => setEditing("new")} disabled={navigation.state !== "idle"}>
|
|
179
|
+
{storefrontTranslate("Agregar dirección")}
|
|
180
|
+
</Button>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
{error ? (
|
|
184
|
+
<p role="alert" className="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
185
|
+
{storefrontTranslate(error)}
|
|
186
|
+
</p>
|
|
187
|
+
) : null}
|
|
188
|
+
{addresses.length === 0 && !adding ? (
|
|
189
|
+
<div className="flex flex-col items-start gap-3 rounded-lg border bg-card p-8 text-card-foreground">
|
|
190
|
+
<p className="font-heading text-xl">{storefrontTranslate("Aún no guardas direcciones")}</p>
|
|
191
|
+
<p className="text-sm text-muted-foreground">{storefrontTranslate("Guarda una dirección para comprar más rápido.")}</p>
|
|
192
|
+
<Button type="button" onClick={() => setEditing("new")}>{storefrontTranslate("Agregar dirección")}</Button>
|
|
193
|
+
</div>
|
|
194
|
+
) : null}
|
|
195
|
+
<ul className="grid gap-4 md:grid-cols-2">
|
|
196
|
+
{adding ? (
|
|
197
|
+
<li className="md:col-span-2">
|
|
198
|
+
<AddressEditor address={null} countries={countries} primary={addresses.length === 0} onDone={() => setEditing(null)} />
|
|
199
|
+
</li>
|
|
200
|
+
) : null}
|
|
201
|
+
{sorted.map((address) =>
|
|
202
|
+
editing === address.id ? (
|
|
203
|
+
<li key={address.id} className="md:col-span-2">
|
|
204
|
+
<AddressEditor address={address} countries={countries} primary={false} onDone={() => setEditing(null)} />
|
|
205
|
+
</li>
|
|
206
|
+
) : (
|
|
207
|
+
<li key={address.id}>
|
|
208
|
+
<AddressCard address={address} onEdit={() => setEditing(address.id)} />
|
|
209
|
+
</li>
|
|
210
|
+
),
|
|
211
|
+
)}
|
|
212
|
+
</ul>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { formatOrderDate, type CustomerOrderDetail, type CustomerOrderLineItem, type OrderStep } from "@ecomiq/core";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Form, Link, useNavigation } from "react-router";
|
|
4
|
+
import { Button } from "@theme/ui/button";
|
|
5
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@theme/ui/select";
|
|
6
|
+
import { Textarea } from "@theme/ui/textarea";
|
|
7
|
+
import { CustomerCsrfField, money, orderProgress, useStorefrontLocale, useStorefrontText, type SectionCatalog } from "../index";
|
|
8
|
+
import {
|
|
9
|
+
PayButton,
|
|
10
|
+
PaymentPreview,
|
|
11
|
+
ReorderButton,
|
|
12
|
+
orderStepLabel,
|
|
13
|
+
orderStepOf,
|
|
14
|
+
orderSummaryLines,
|
|
15
|
+
paymentStatusLabel,
|
|
16
|
+
useOrderStepDetail,
|
|
17
|
+
type OrderRequestActionData,
|
|
18
|
+
} from "../entries/account";
|
|
19
|
+
|
|
20
|
+
export const catalog: SectionCatalog = {
|
|
21
|
+
name: "Detalle del pedido",
|
|
22
|
+
description: "Estado de cada producto, seguimiento, pago y totales de un pedido del cliente.",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const STEP_ORDER: Record<OrderStep, number> = {
|
|
26
|
+
outForDelivery: 0,
|
|
27
|
+
shipped: 1,
|
|
28
|
+
confirmed: 2,
|
|
29
|
+
pending: 3,
|
|
30
|
+
delivered: 4,
|
|
31
|
+
cancelled: 5,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type Shipment = {
|
|
35
|
+
key: string;
|
|
36
|
+
step: OrderStep;
|
|
37
|
+
carrierName: string | null;
|
|
38
|
+
trackingNumber: string | null;
|
|
39
|
+
trackingLink: string | null;
|
|
40
|
+
items: CustomerOrderLineItem[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function shipments(items: CustomerOrderLineItem[]): Shipment[] {
|
|
44
|
+
const groups = new Map<string, Shipment>();
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
const step = orderStepOf(item);
|
|
47
|
+
const key = `${step}|${item.trackingNumber ?? ""}|${item.trackingLink ?? ""}`;
|
|
48
|
+
const group = groups.get(key);
|
|
49
|
+
if (group) {
|
|
50
|
+
group.items.push(item);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
groups.set(key, {
|
|
54
|
+
key,
|
|
55
|
+
step,
|
|
56
|
+
carrierName: item.carrierName ?? null,
|
|
57
|
+
trackingNumber: item.trackingNumber ?? null,
|
|
58
|
+
trackingLink: item.trackingLink ?? null,
|
|
59
|
+
items: [item],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return [...groups.values()].sort((a, b) => STEP_ORDER[a.step] - STEP_ORDER[b.step]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function Progress({ step }: { step: OrderStep }) {
|
|
66
|
+
const storefrontTranslate = useStorefrontText();
|
|
67
|
+
return (
|
|
68
|
+
<ol className="grid grid-cols-4 gap-1.5">
|
|
69
|
+
{orderProgress(step).map((item) => (
|
|
70
|
+
<li key={item.key} className="flex flex-col gap-1.5">
|
|
71
|
+
<span className={`h-1 rounded-full ${item.done ? "bg-primary" : "bg-muted"}`} />
|
|
72
|
+
<span className={`text-xs ${item.current ? "font-medium text-foreground" : "text-muted-foreground"}`}>{storefrontTranslate(item.label)}</span>
|
|
73
|
+
</li>
|
|
74
|
+
))}
|
|
75
|
+
</ol>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ShipmentCard({ shipment, currency }: { shipment: Shipment; currency: string }) {
|
|
80
|
+
const storefrontTranslate = useStorefrontText();
|
|
81
|
+
const { locale } = useStorefrontLocale();
|
|
82
|
+
const stepDetail = useOrderStepDetail();
|
|
83
|
+
const tracking = [shipment.carrierName, shipment.trackingNumber].filter(Boolean).join(" · ");
|
|
84
|
+
const cancelled = shipment.step === "cancelled";
|
|
85
|
+
const detail = stepDetail(shipment.step, shipment.items);
|
|
86
|
+
return (
|
|
87
|
+
<section className="flex flex-col gap-5 rounded-lg border bg-card p-5 text-card-foreground md:p-6">
|
|
88
|
+
<div className="flex flex-col gap-1">
|
|
89
|
+
<h3 className={`font-heading text-2xl ${cancelled ? "text-muted-foreground" : ""}`}>{storefrontTranslate(orderStepLabel(shipment.step))}</h3>
|
|
90
|
+
{detail === null ? null : <p className="text-sm text-muted-foreground">{detail}</p>}
|
|
91
|
+
</div>
|
|
92
|
+
{cancelled ? null : <Progress step={shipment.step} />}
|
|
93
|
+
{tracking !== "" || shipment.trackingLink ? (
|
|
94
|
+
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 rounded-md bg-muted/60 px-4 py-3 text-sm">
|
|
95
|
+
<span>
|
|
96
|
+
<span className="text-muted-foreground">{storefrontTranslate("Seguimiento")}</span> {tracking}
|
|
97
|
+
</span>
|
|
98
|
+
{shipment.trackingLink ? (
|
|
99
|
+
<a href={shipment.trackingLink} target="_blank" rel="noopener noreferrer" className="font-medium underline underline-offset-4">
|
|
100
|
+
{storefrontTranslate("Seguir envío")}
|
|
101
|
+
</a>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
) : null}
|
|
105
|
+
<ul className="flex flex-col divide-y">
|
|
106
|
+
{shipment.items.map((item) => (
|
|
107
|
+
<li key={item.id} className={`flex gap-4 py-4 first:pt-0 last:pb-0 ${cancelled ? "opacity-60" : ""}`}>
|
|
108
|
+
<span className="w-16 shrink-0 overflow-hidden rounded-sm bg-muted">
|
|
109
|
+
{item.thumbnail ? (
|
|
110
|
+
<img src={item.thumbnail} alt="" width={64} height={80} loading="lazy" decoding="async" className="aspect-[4/5] size-full object-cover" />
|
|
111
|
+
) : (
|
|
112
|
+
<span className="block aspect-[4/5]" />
|
|
113
|
+
)}
|
|
114
|
+
</span>
|
|
115
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
116
|
+
{item.productUrl ? (
|
|
117
|
+
<Link to={item.productUrl} className="font-medium hover:underline">{item.title}</Link>
|
|
118
|
+
) : (
|
|
119
|
+
<p className="font-medium">{item.title}</p>
|
|
120
|
+
)}
|
|
121
|
+
{item.variantTitle ? <p className="text-sm text-muted-foreground">{item.variantTitle}</p> : null}
|
|
122
|
+
<p className="text-sm text-muted-foreground">
|
|
123
|
+
{money(item.unitPrice, currency, locale)} × {item.quantity}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
<p className="text-sm font-medium">{money(item.total, currency, locale)}</p>
|
|
127
|
+
</li>
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
</section>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function Payments({ order }: { order: CustomerOrderDetail }) {
|
|
135
|
+
const storefrontTranslate = useStorefrontText();
|
|
136
|
+
const { locale, timeZone } = useStorefrontLocale();
|
|
137
|
+
const format = (value: number) => money(value, order.currencyCode, locale);
|
|
138
|
+
const lines = orderSummaryLines(order, {
|
|
139
|
+
subtotal: storefrontTranslate("Subtotal"),
|
|
140
|
+
discount: storefrontTranslate("Descuentos"),
|
|
141
|
+
shipping: storefrontTranslate("Envío"),
|
|
142
|
+
tax: storefrontTranslate("Impuestos"),
|
|
143
|
+
total: storefrontTranslate("Total"),
|
|
144
|
+
free: storefrontTranslate("Gratis"),
|
|
145
|
+
pending: storefrontTranslate("Por calcular"),
|
|
146
|
+
});
|
|
147
|
+
const due = Math.max(0, order.total - order.totalPaid);
|
|
148
|
+
return (
|
|
149
|
+
<section className="flex flex-col gap-4 rounded-lg border bg-card p-5 text-card-foreground">
|
|
150
|
+
<h3 className="font-heading text-lg">{storefrontTranslate("Pago")}</h3>
|
|
151
|
+
{order.payments.length === 0 ? <p className="text-sm text-muted-foreground">{storefrontTranslate("Sin pagos registrados")}</p> : null}
|
|
152
|
+
{order.payments.map((payment) => {
|
|
153
|
+
const status = paymentStatusLabel(payment);
|
|
154
|
+
const installments = payment.installments ?? 0;
|
|
155
|
+
return (
|
|
156
|
+
<div key={payment.id} className="flex flex-col gap-1">
|
|
157
|
+
<div className="flex items-start justify-between gap-3 text-sm">
|
|
158
|
+
<PaymentPreview payment={payment} className="font-medium" />
|
|
159
|
+
<span className="font-medium">{format(payment.amount)}</span>
|
|
160
|
+
</div>
|
|
161
|
+
<p className="text-xs text-muted-foreground">
|
|
162
|
+
{[
|
|
163
|
+
formatOrderDate(payment.createdOn, locale, timeZone),
|
|
164
|
+
installments > 1 ? storefrontTranslate("{value0} cuotas", { value0: installments }) : null,
|
|
165
|
+
status ? storefrontTranslate(status) : null,
|
|
166
|
+
]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(" · ")}
|
|
169
|
+
</p>
|
|
170
|
+
{payment.receiptUrl ? (
|
|
171
|
+
<a href={payment.receiptUrl} target="_blank" rel="noopener noreferrer" className="w-fit text-xs font-medium underline underline-offset-4">
|
|
172
|
+
{storefrontTranslate("Ver comprobante")}
|
|
173
|
+
</a>
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
<dl className="flex flex-col gap-2 border-t pt-4 text-sm">
|
|
179
|
+
{lines.map((line) => (
|
|
180
|
+
<div key={line.key} className={`flex justify-between gap-4 ${line.key === "total" ? "font-semibold" : "text-muted-foreground"}`}>
|
|
181
|
+
<dt>{line.label}</dt>
|
|
182
|
+
<dd>{line.amount === null ? line.text : format(line.amount)}</dd>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
<div className="flex justify-between gap-4 text-muted-foreground">
|
|
186
|
+
<dt>{storefrontTranslate("Pagado")}</dt>
|
|
187
|
+
<dd>{format(order.totalPaid)}</dd>
|
|
188
|
+
</div>
|
|
189
|
+
{due > 0 ? (
|
|
190
|
+
<div className="flex justify-between gap-4 font-medium text-destructive">
|
|
191
|
+
<dt>{storefrontTranslate("Por pagar")}</dt>
|
|
192
|
+
<dd>{format(due)}</dd>
|
|
193
|
+
</div>
|
|
194
|
+
) : null}
|
|
195
|
+
</dl>
|
|
196
|
+
</section>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function History({ order }: { order: CustomerOrderDetail }) {
|
|
201
|
+
const storefrontTranslate = useStorefrontText();
|
|
202
|
+
const { locale, timeZone } = useStorefrontLocale();
|
|
203
|
+
const events = [...order.statusTimeline].reverse();
|
|
204
|
+
return (
|
|
205
|
+
<section className="flex flex-col gap-4 rounded-lg border bg-card p-5 text-card-foreground">
|
|
206
|
+
<h3 className="font-heading text-lg">{storefrontTranslate("Historial")}</h3>
|
|
207
|
+
<ol className="flex flex-col">
|
|
208
|
+
{events.map((event, index) => (
|
|
209
|
+
<li key={`${event.key}-${event.occurredAt}`} className="relative flex gap-3 pb-4 text-sm last:pb-0">
|
|
210
|
+
{index < events.length - 1 ? <span aria-hidden="true" className="absolute left-[3px] top-3 h-full w-px bg-border" /> : null}
|
|
211
|
+
<span aria-hidden="true" className={`relative mt-1.5 size-[7px] shrink-0 rounded-full ${index === 0 ? "bg-foreground" : "bg-foreground/25"}`} />
|
|
212
|
+
<div className="flex flex-1 flex-wrap justify-between gap-x-4 gap-y-0.5">
|
|
213
|
+
<span className={index === 0 ? "font-medium" : ""}>
|
|
214
|
+
{storefrontTranslate(event.label)}
|
|
215
|
+
{event.description ? <span className="block text-xs font-normal text-muted-foreground">{event.description}</span> : null}
|
|
216
|
+
</span>
|
|
217
|
+
<span className="text-muted-foreground">{formatOrderDate(event.occurredAt, locale, timeZone)}</span>
|
|
218
|
+
</div>
|
|
219
|
+
</li>
|
|
220
|
+
))}
|
|
221
|
+
</ol>
|
|
222
|
+
</section>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const RETURN_REASONS = ["La talla o medida no me quedó", "Llegó dañado o con fallas", "No es lo que pedí", "No es como en las fotos", "Ya no lo quiero", "Otro motivo"];
|
|
227
|
+
const CANCEL_REASONS = ["Me equivoqué al hacer el pedido", "Encontré un mejor precio", "Demora mucho en llegar", "Ya no lo necesito", "Otro motivo"];
|
|
228
|
+
|
|
229
|
+
const RETURN_STATES: Record<string, { title: string; detail: string }> = {
|
|
230
|
+
requested: { title: "Devolución solicitada", detail: "La tienda está revisando tu solicitud. Te escribiremos por correo." },
|
|
231
|
+
inProgress: { title: "Devolución aprobada", detail: "Te enviamos por correo los pasos para devolver los productos." },
|
|
232
|
+
inspected: { title: "Devolución en revisión", detail: "La tienda recibió los productos y los está revisando." },
|
|
233
|
+
returned: { title: "Devolución completada", detail: "La tienda recibió los productos." },
|
|
234
|
+
rejected: { title: "Devolución no aprobada", detail: "Te escribimos por correo con el motivo." },
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
type Request = "return" | "cancel";
|
|
238
|
+
|
|
239
|
+
function RequestStatus({ order }: { order: CustomerOrderDetail }) {
|
|
240
|
+
const storefrontTranslate = useStorefrontText();
|
|
241
|
+
const state = order.cancellationRequestedAt
|
|
242
|
+
? { title: "Cancelación solicitada", detail: "La tienda está revisando tu solicitud. Te escribiremos por correo." }
|
|
243
|
+
: RETURN_STATES[order.returnStatus ?? ""];
|
|
244
|
+
if (!state) return null;
|
|
245
|
+
return (
|
|
246
|
+
<section role="status" className="flex flex-col gap-1 rounded-lg border bg-card p-5 text-card-foreground">
|
|
247
|
+
<h3 className="font-heading text-lg">{storefrontTranslate(state.title)}</h3>
|
|
248
|
+
<p className="text-sm text-muted-foreground">{storefrontTranslate(state.detail)}</p>
|
|
249
|
+
</section>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function ReasonField({ reasons }: { reasons: string[] }) {
|
|
254
|
+
const storefrontTranslate = useStorefrontText();
|
|
255
|
+
return (
|
|
256
|
+
<div className="flex flex-col gap-1.5 text-sm">
|
|
257
|
+
<span className="font-medium">{storefrontTranslate("Motivo")}</span>
|
|
258
|
+
<Select name="reason" required>
|
|
259
|
+
<SelectTrigger aria-label={storefrontTranslate("Motivo")} className="h-11 w-full data-[size=default]:h-11">
|
|
260
|
+
<SelectValue placeholder={storefrontTranslate("Elige un motivo")} />
|
|
261
|
+
</SelectTrigger>
|
|
262
|
+
<SelectContent>
|
|
263
|
+
{reasons.map((reason) => (
|
|
264
|
+
<SelectItem key={reason} value={reason}>
|
|
265
|
+
{storefrontTranslate(reason)}
|
|
266
|
+
</SelectItem>
|
|
267
|
+
))}
|
|
268
|
+
</SelectContent>
|
|
269
|
+
</Select>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function RequestForm({ request, order, error, onClose }: { request: Request; order: CustomerOrderDetail; error: string | null; onClose: () => void }) {
|
|
275
|
+
const storefrontTranslate = useStorefrontText();
|
|
276
|
+
const { locale, timeZone } = useStorefrontLocale();
|
|
277
|
+
const busy = useNavigation().state === "submitting";
|
|
278
|
+
const returnable = order.items.filter((item) => item.returnableQuantity > 0);
|
|
279
|
+
const [quantities, setQuantities] = useState<Record<string, string>>(() =>
|
|
280
|
+
Object.fromEntries(returnable.map((item) => [item.id, returnable.length === 1 ? String(item.returnableQuantity) : "0"])),
|
|
281
|
+
);
|
|
282
|
+
return (
|
|
283
|
+
<Form method="post" encType="multipart/form-data" className="flex flex-col gap-5 rounded-lg border bg-card p-5 text-card-foreground md:p-6">
|
|
284
|
+
<CustomerCsrfField />
|
|
285
|
+
<input type="hidden" name="intent" value={request} />
|
|
286
|
+
<div className="flex flex-col gap-1">
|
|
287
|
+
<h3 className="font-heading text-2xl">{storefrontTranslate(request === "return" ? "Solicitar devolución" : "Solicitar cancelación")}</h3>
|
|
288
|
+
<p className="text-sm text-muted-foreground">
|
|
289
|
+
{request === "return" && order.returnDeadline
|
|
290
|
+
? storefrontTranslate("Puedes pedirla hasta el {value0}. La tienda la revisa y te escribe por correo.", { value0: formatOrderDate(order.returnDeadline, locale, timeZone) })
|
|
291
|
+
: storefrontTranslate("La tienda la revisa antes de enviar tu pedido y te escribe por correo.")}
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
{error ? (
|
|
295
|
+
<p role="alert" className="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
296
|
+
{storefrontTranslate(error)}
|
|
297
|
+
</p>
|
|
298
|
+
) : null}
|
|
299
|
+
{request === "return" ? (
|
|
300
|
+
<ul className="flex flex-col divide-y">
|
|
301
|
+
<li className="flex justify-end pb-2 text-xs text-muted-foreground">{storefrontTranslate("Cantidad")}</li>
|
|
302
|
+
{returnable.map((item) => (
|
|
303
|
+
<li key={item.id} className="flex items-center gap-4 py-3 first:pt-0">
|
|
304
|
+
<span className="w-12 shrink-0 overflow-hidden rounded-sm bg-muted">
|
|
305
|
+
{item.thumbnail ? <img src={item.thumbnail} alt="" width={48} height={60} loading="lazy" decoding="async" className="aspect-[4/5] size-full object-cover" /> : <span className="block aspect-[4/5]" />}
|
|
306
|
+
</span>
|
|
307
|
+
<div className="flex min-w-0 flex-1 flex-col text-sm">
|
|
308
|
+
<span className="font-medium">{item.title}</span>
|
|
309
|
+
{item.variantTitle ? <span className="text-muted-foreground">{item.variantTitle}</span> : null}
|
|
310
|
+
</div>
|
|
311
|
+
<input type="hidden" name="itemId" value={item.id} />
|
|
312
|
+
<Select name="quantity" value={quantities[item.id] ?? "0"} onValueChange={(value) => setQuantities((current) => ({ ...current, [item.id]: value }))}>
|
|
313
|
+
<SelectTrigger aria-label={storefrontTranslate("Cantidad a devolver de {value0}", { value0: item.title })} className="w-20 shrink-0">
|
|
314
|
+
<SelectValue>{quantities[item.id] ?? "0"}</SelectValue>
|
|
315
|
+
</SelectTrigger>
|
|
316
|
+
<SelectContent>
|
|
317
|
+
{Array.from({ length: item.returnableQuantity + 1 }, (_, quantity) => (
|
|
318
|
+
<SelectItem key={quantity} value={String(quantity)}>
|
|
319
|
+
{quantity}
|
|
320
|
+
</SelectItem>
|
|
321
|
+
))}
|
|
322
|
+
</SelectContent>
|
|
323
|
+
</Select>
|
|
324
|
+
</li>
|
|
325
|
+
))}
|
|
326
|
+
</ul>
|
|
327
|
+
) : null}
|
|
328
|
+
<ReasonField reasons={request === "return" ? RETURN_REASONS : CANCEL_REASONS} />
|
|
329
|
+
<label className="flex flex-col gap-1.5 text-sm">
|
|
330
|
+
<span className="font-medium">{storefrontTranslate("Comentario (opcional)")}</span>
|
|
331
|
+
<Textarea name="comment" rows={3} maxLength={1000} />
|
|
332
|
+
</label>
|
|
333
|
+
{request === "return" ? (
|
|
334
|
+
<label className="flex flex-col gap-1.5 text-sm">
|
|
335
|
+
<span className="font-medium">{storefrontTranslate("Fotos (opcional)")}</span>
|
|
336
|
+
<input type="file" name="photos" accept="image/jpeg,image/png,image/webp" multiple className="text-sm file:mr-3 file:rounded-md file:border file:bg-background file:px-3 file:py-1.5 file:text-sm" />
|
|
337
|
+
<span className="text-xs text-muted-foreground">{storefrontTranslate("Hasta 3 fotos JPG, PNG o WebP de hasta 5 MB.")}</span>
|
|
338
|
+
</label>
|
|
339
|
+
) : null}
|
|
340
|
+
<div className="flex flex-wrap gap-2">
|
|
341
|
+
<Button type="submit" disabled={busy}>
|
|
342
|
+
{busy ? storefrontTranslate("Enviando…") : storefrontTranslate("Enviar solicitud")}
|
|
343
|
+
</Button>
|
|
344
|
+
<Button type="button" variant="ghost" onClick={onClose}>
|
|
345
|
+
{storefrontTranslate("Cancelar")}
|
|
346
|
+
</Button>
|
|
347
|
+
</div>
|
|
348
|
+
</Form>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function AccountOrder({ loaderData, actionData }: { loaderData: { order: CustomerOrderDetail }; actionData?: OrderRequestActionData }) {
|
|
353
|
+
const storefrontTranslate = useStorefrontText();
|
|
354
|
+
const { locale, timeZone } = useStorefrontLocale();
|
|
355
|
+
const { order } = loaderData;
|
|
356
|
+
const [request, setRequest] = useState<Request | null>(null);
|
|
357
|
+
const sent = actionData?.sent ?? null;
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (sent) setRequest(null);
|
|
360
|
+
}, [sent]);
|
|
361
|
+
const units = order.items.reduce((sum, item) => sum + item.quantity, 0);
|
|
362
|
+
const address = order.shippingAddress;
|
|
363
|
+
const contact = [order.email, order.phone].filter(Boolean);
|
|
364
|
+
return (
|
|
365
|
+
<div className="flex flex-col gap-8">
|
|
366
|
+
<div className="flex flex-col gap-4">
|
|
367
|
+
<Link to="/account" className="w-fit text-sm text-muted-foreground hover:text-foreground">
|
|
368
|
+
← {storefrontTranslate("Mis pedidos")}
|
|
369
|
+
</Link>
|
|
370
|
+
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
371
|
+
<div className="flex flex-col gap-1">
|
|
372
|
+
<h2 className="text-3xl md:text-4xl">{storefrontTranslate("Pedido {value0}", { value0: order.orderNumber })}</h2>
|
|
373
|
+
<p className="text-sm text-muted-foreground">
|
|
374
|
+
{storefrontTranslate("Realizado el {value0}", { value0: formatOrderDate(order.orderDate, locale, timeZone) })} ·{" "}
|
|
375
|
+
{units === 1 ? storefrontTranslate("1 producto") : storefrontTranslate("{value0} productos", { value0: units })}
|
|
376
|
+
</p>
|
|
377
|
+
</div>
|
|
378
|
+
<div className="flex flex-wrap gap-2">
|
|
379
|
+
{order.paymentPath ? (
|
|
380
|
+
<PayButton path={order.paymentPath} label={storefrontTranslate("Pagar {value0}", { value0: money(order.total - order.totalPaid, order.currencyCode, locale) })} />
|
|
381
|
+
) : null}
|
|
382
|
+
<ReorderButton items={order.items} variant={order.paymentPath ? "outline" : "default"} />
|
|
383
|
+
{order.canRequestReturn && request !== "return" ? (
|
|
384
|
+
<Button type="button" variant="outline" onClick={() => setRequest("return")}>
|
|
385
|
+
{storefrontTranslate("Solicitar devolución")}
|
|
386
|
+
</Button>
|
|
387
|
+
) : null}
|
|
388
|
+
{order.canRequestCancellation && request !== "cancel" ? (
|
|
389
|
+
<Button type="button" variant="outline" onClick={() => setRequest("cancel")}>
|
|
390
|
+
{storefrontTranslate("Solicitar cancelación")}
|
|
391
|
+
</Button>
|
|
392
|
+
) : null}
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
|
|
397
|
+
<div className="flex flex-col gap-4">
|
|
398
|
+
<RequestStatus order={order} />
|
|
399
|
+
{request ? <RequestForm request={request} order={order} error={actionData?.error ?? null} onClose={() => setRequest(null)} /> : null}
|
|
400
|
+
{shipments(order.items).map((shipment) => (
|
|
401
|
+
<ShipmentCard key={shipment.key} shipment={shipment} currency={order.currencyCode} />
|
|
402
|
+
))}
|
|
403
|
+
{order.statusTimeline.length > 0 ? <History order={order} /> : null}
|
|
404
|
+
</div>
|
|
405
|
+
<aside className="flex flex-col gap-4">
|
|
406
|
+
<Payments order={order} />
|
|
407
|
+
<section className="flex flex-col gap-3 rounded-lg border bg-card p-5 text-sm text-card-foreground">
|
|
408
|
+
<h3 className="font-heading text-lg">{storefrontTranslate("Entrega")}</h3>
|
|
409
|
+
{address.name ? <p className="font-medium">{address.name}</p> : null}
|
|
410
|
+
<p className="text-muted-foreground">{order.shippingAddressSummary ?? address.address1}</p>
|
|
411
|
+
{contact.length > 0 ? (
|
|
412
|
+
<>
|
|
413
|
+
<h3 className="pt-2 font-heading text-lg">{storefrontTranslate("Contacto")}</h3>
|
|
414
|
+
{contact.map((value) => (
|
|
415
|
+
<p key={value} className="text-muted-foreground">{value}</p>
|
|
416
|
+
))}
|
|
417
|
+
</>
|
|
418
|
+
) : null}
|
|
419
|
+
</section>
|
|
420
|
+
</aside>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|