@fayz-ai/plugin-shop 0.9.0 → 0.10.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.
Files changed (165) hide show
  1. package/README.md +4 -4
  2. package/dist/ShopPage-ZBJRNMKQ.js +2942 -0
  3. package/dist/ShopPage-ZBJRNMKQ.js.map +1 -0
  4. package/dist/analytics/cards.d.ts +3 -0
  5. package/dist/analytics/cards.d.ts.map +1 -0
  6. package/dist/chunk-EW4XJRA2.js +835 -0
  7. package/dist/chunk-EW4XJRA2.js.map +1 -0
  8. package/dist/data/categoryDataProvider.d.ts +5 -0
  9. package/dist/data/categoryDataProvider.d.ts.map +1 -0
  10. package/dist/data/collectionDataProvider.d.ts +5 -0
  11. package/dist/data/collectionDataProvider.d.ts.map +1 -0
  12. package/dist/data/customerGroupDataProvider.d.ts +5 -0
  13. package/dist/data/customerGroupDataProvider.d.ts.map +1 -0
  14. package/dist/data/customerSegmentDataProvider.d.ts +16 -0
  15. package/dist/data/customerSegmentDataProvider.d.ts.map +1 -0
  16. package/dist/data/giftCardDataProvider.d.ts +5 -0
  17. package/dist/data/giftCardDataProvider.d.ts.map +1 -0
  18. package/dist/data/registries.d.ts +53 -0
  19. package/dist/data/registries.d.ts.map +1 -0
  20. package/dist/data/registry-routes.d.ts +21 -0
  21. package/dist/data/registry-routes.d.ts.map +1 -0
  22. package/dist/data/shippingProfileDataProvider.d.ts +19 -0
  23. package/dist/data/shippingProfileDataProvider.d.ts.map +1 -0
  24. package/dist/data/stockLocationDataProvider.d.ts +13 -0
  25. package/dist/data/stockLocationDataProvider.d.ts.map +1 -0
  26. package/dist/index.d.ts +18 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5362 -418
  29. package/dist/index.js.map +1 -1
  30. package/dist/integrations/abacatepay/connectorDef.d.ts +41 -0
  31. package/dist/integrations/abacatepay/connectorDef.d.ts.map +1 -0
  32. package/dist/integrations/appmax/connectorDef.d.ts +28 -0
  33. package/dist/integrations/appmax/connectorDef.d.ts.map +1 -0
  34. package/dist/integrations/bling/connectorDef.d.ts +10 -0
  35. package/dist/integrations/bling/connectorDef.d.ts.map +1 -0
  36. package/dist/integrations/bling/data.d.ts +61 -0
  37. package/dist/integrations/bling/data.d.ts.map +1 -0
  38. package/dist/integrations/bling/inbound.d.ts +97 -0
  39. package/dist/integrations/bling/inbound.d.ts.map +1 -0
  40. package/dist/integrations/bling/index.d.ts +9 -0
  41. package/dist/integrations/bling/index.d.ts.map +1 -0
  42. package/dist/integrations/bling/limits.d.ts +33 -0
  43. package/dist/integrations/bling/limits.d.ts.map +1 -0
  44. package/dist/integrations/bling/mapping.d.ts +10 -0
  45. package/dist/integrations/bling/mapping.d.ts.map +1 -0
  46. package/dist/integrations/bling/sync.d.ts +70 -0
  47. package/dist/integrations/bling/sync.d.ts.map +1 -0
  48. package/dist/integrations/bling/types.d.ts +59 -0
  49. package/dist/integrations/bling/types.d.ts.map +1 -0
  50. package/dist/integrations/melhor-envio/connectorDef.d.ts +38 -0
  51. package/dist/integrations/melhor-envio/connectorDef.d.ts.map +1 -0
  52. package/dist/integrations/melhor-envio/labelProvider.d.ts +4 -0
  53. package/dist/integrations/melhor-envio/labelProvider.d.ts.map +1 -0
  54. package/dist/integrations/stripe-connect/connectorDef.d.ts +17 -0
  55. package/dist/integrations/stripe-connect/connectorDef.d.ts.map +1 -0
  56. package/dist/settings/CheckoutSettings.d.ts +17 -0
  57. package/dist/settings/CheckoutSettings.d.ts.map +1 -0
  58. package/dist/settings/ChoiceCards.d.ts +23 -0
  59. package/dist/settings/ChoiceCards.d.ts.map +1 -0
  60. package/dist/settings/FulfillmentSettings.d.ts +23 -0
  61. package/dist/settings/FulfillmentSettings.d.ts.map +1 -0
  62. package/dist/settings/HandlingSettings.d.ts +17 -0
  63. package/dist/settings/HandlingSettings.d.ts.map +1 -0
  64. package/dist/settings/PackagesSettings.d.ts +22 -0
  65. package/dist/settings/PackagesSettings.d.ts.map +1 -0
  66. package/dist/settings/PaymentProviderRow.d.ts +28 -0
  67. package/dist/settings/PaymentProviderRow.d.ts.map +1 -0
  68. package/dist/settings/PaymentsSettings.d.ts +7 -0
  69. package/dist/settings/PaymentsSettings.d.ts.map +1 -0
  70. package/dist/settings/SettingsState.d.ts +27 -0
  71. package/dist/settings/SettingsState.d.ts.map +1 -0
  72. package/dist/settings/ShippingSettings.d.ts +27 -0
  73. package/dist/settings/ShippingSettings.d.ts.map +1 -0
  74. package/dist/settings/StoreGeneralSettings.d.ts +19 -0
  75. package/dist/settings/StoreGeneralSettings.d.ts.map +1 -0
  76. package/dist/settings/error-message.d.ts +2 -0
  77. package/dist/settings/error-message.d.ts.map +1 -0
  78. package/dist/settings/index.d.ts +12 -0
  79. package/dist/settings/index.d.ts.map +1 -0
  80. package/dist/settings/payment-provider.d.ts +13 -0
  81. package/dist/settings/payment-provider.d.ts.map +1 -0
  82. package/dist/settings/useStoreSettings.d.ts +30 -0
  83. package/dist/settings/useStoreSettings.d.ts.map +1 -0
  84. package/dist/views/AgencySelect.d.ts +17 -0
  85. package/dist/views/AgencySelect.d.ts.map +1 -0
  86. package/dist/views/ChannelsPage.d.ts +1 -1
  87. package/dist/views/ChannelsPage.d.ts.map +1 -1
  88. package/dist/views/ContentPage.d.ts +7 -0
  89. package/dist/views/ContentPage.d.ts.map +1 -0
  90. package/dist/views/EmailTemplatePage.d.ts +9 -0
  91. package/dist/views/EmailTemplatePage.d.ts.map +1 -0
  92. package/dist/views/EmailsPage.d.ts +7 -0
  93. package/dist/views/EmailsPage.d.ts.map +1 -0
  94. package/dist/views/FulfillmentProviderSelect.d.ts +14 -0
  95. package/dist/views/FulfillmentProviderSelect.d.ts.map +1 -0
  96. package/dist/views/NewOrderCharge.d.ts +17 -0
  97. package/dist/views/NewOrderCharge.d.ts.map +1 -0
  98. package/dist/views/NewOrderPage.d.ts +9 -0
  99. package/dist/views/NewOrderPage.d.ts.map +1 -0
  100. package/dist/views/OrderDetailView.d.ts +2 -0
  101. package/dist/views/OrderDetailView.d.ts.map +1 -1
  102. package/dist/views/OrderPaymentDetail.d.ts +5 -0
  103. package/dist/views/OrderPaymentDetail.d.ts.map +1 -0
  104. package/dist/views/OrderProviderLinks.d.ts +5 -0
  105. package/dist/views/OrderProviderLinks.d.ts.map +1 -0
  106. package/dist/views/PackageSelect.d.ts +20 -0
  107. package/dist/views/PackageSelect.d.ts.map +1 -0
  108. package/dist/views/ProductImagesTab.d.ts.map +1 -1
  109. package/dist/views/ProductVariantsTab.d.ts +29 -0
  110. package/dist/views/ProductVariantsTab.d.ts.map +1 -0
  111. package/dist/views/SenderLocationModal.d.ts +20 -0
  112. package/dist/views/SenderLocationModal.d.ts.map +1 -0
  113. package/dist/views/ShipOrderForm.d.ts +10 -0
  114. package/dist/views/ShipOrderForm.d.ts.map +1 -0
  115. package/dist/views/ShipWithCarrier.d.ts +18 -0
  116. package/dist/views/ShipWithCarrier.d.ts.map +1 -0
  117. package/dist/views/ShipmentActions.d.ts +11 -0
  118. package/dist/views/ShipmentActions.d.ts.map +1 -0
  119. package/dist/views/ShippingProfileSelect.d.ts +13 -0
  120. package/dist/views/ShippingProfileSelect.d.ts.map +1 -0
  121. package/dist/views/ShopPage.d.ts.map +1 -0
  122. package/dist/views/categoryEntity.d.ts +4 -0
  123. package/dist/views/categoryEntity.d.ts.map +1 -0
  124. package/dist/views/collectionEntity.d.ts +4 -0
  125. package/dist/views/collectionEntity.d.ts.map +1 -0
  126. package/dist/views/customerEntity.d.ts.map +1 -1
  127. package/dist/views/customerGroupEntity.d.ts +4 -0
  128. package/dist/views/customerGroupEntity.d.ts.map +1 -0
  129. package/dist/views/customerSegmentEntity.d.ts +13 -0
  130. package/dist/views/customerSegmentEntity.d.ts.map +1 -0
  131. package/dist/views/eta-label.d.ts +6 -0
  132. package/dist/views/eta-label.d.ts.map +1 -0
  133. package/dist/views/giftCardEntity.d.ts +4 -0
  134. package/dist/views/giftCardEntity.d.ts.map +1 -0
  135. package/dist/views/media-links.d.ts +12 -0
  136. package/dist/views/media-links.d.ts.map +1 -0
  137. package/dist/views/new-order-rules.d.ts +55 -0
  138. package/dist/views/new-order-rules.d.ts.map +1 -0
  139. package/dist/views/order-actions.d.ts +4 -0
  140. package/dist/views/order-actions.d.ts.map +1 -0
  141. package/dist/views/productEntity.d.ts.map +1 -1
  142. package/dist/views/shippingZoneEntity.d.ts.map +1 -1
  143. package/package.json +11 -10
  144. package/dist/ShopPage-JP27MZS2.js +0 -690
  145. package/dist/ShopPage-JP27MZS2.js.map +0 -1
  146. package/dist/ShopPage.d.ts.map +0 -1
  147. package/dist/components/ShopSettings.d.ts +0 -15
  148. package/dist/components/ShopSettings.d.ts.map +0 -1
  149. package/src/ShopPage.tsx +0 -425
  150. package/src/components/ShopSettings.tsx +0 -108
  151. package/src/data/customerDataProvider.ts +0 -31
  152. package/src/data/discountDataProvider.ts +0 -42
  153. package/src/data/productDataProvider.ts +0 -54
  154. package/src/data/shared.ts +0 -42
  155. package/src/data/shippingZoneDataProvider.ts +0 -49
  156. package/src/index.ts +0 -270
  157. package/src/views/ChannelsPage.tsx +0 -274
  158. package/src/views/OrderDetailView.tsx +0 -319
  159. package/src/views/ProductImagesTab.tsx +0 -183
  160. package/src/views/customerEntity.tsx +0 -110
  161. package/src/views/customerLookup.ts +0 -31
  162. package/src/views/discountEntity.tsx +0 -144
  163. package/src/views/productEntity.tsx +0 -229
  164. package/src/views/shippingZoneEntity.tsx +0 -123
  165. /package/dist/{ShopPage.d.ts → views/ShopPage.d.ts} +0 -0
@@ -1,274 +0,0 @@
1
- import React from 'react'
2
- import { Globe, MessageCircle, Camera, Music, ShoppingBag, MapPin, Link2, Plus, ExternalLink, Trash2, Check, X } from 'lucide-react'
3
- import { Button, Input } from '@fayz-ai/ui'
4
- import type { ShopProvider, SalesChannel, SalesChannelKind } from '@fayz-ai/shop'
5
- import { getShopProvider } from '@fayz-ai/shop'
6
- import type { ShopProviderResolver } from '../ShopPage'
7
-
8
- // ---------------------------------------------------------------------------
9
- // Canais de venda — the store's points of sale, Shopify-style. The online_store
10
- // channel points at the merchant's fayz storefront; the rest are external links
11
- // the merchant maintains (WhatsApp, Instagram, TikTok Shop…). Every enabled
12
- // channel with a URL also surfaces as an external link in the left menu (see
13
- // the "Canais de venda" nav group), so this page is where they are configured.
14
- // ---------------------------------------------------------------------------
15
-
16
- interface KindMeta {
17
- label: string
18
- icon: React.ComponentType<{ className?: string }>
19
- hint: string
20
- placeholder: string
21
- }
22
-
23
- const KIND_META: Record<SalesChannelKind, KindMeta> = {
24
- online_store: { label: 'Loja Online', icon: Globe, hint: 'Seu storefront fayz — a vitrine pública da loja.', placeholder: 'https://minhaloja.com.br/' },
25
- whatsapp: { label: 'WhatsApp', icon: MessageCircle, hint: 'Link direto para o WhatsApp Business.', placeholder: 'https://wa.me/5521999999999' },
26
- instagram: { label: 'Instagram', icon: Camera, hint: 'Perfil ou loja no Instagram.', placeholder: 'https://instagram.com/sualoja' },
27
- tiktok_shop: { label: 'TikTok Shop', icon: Music, hint: 'Sua vitrine no TikTok Shop.', placeholder: 'https://shop.tiktok.com/@sualoja' },
28
- marketplace: { label: 'Marketplace', icon: ShoppingBag, hint: 'Mercado Livre, Amazon, Shopee…', placeholder: 'https://…' },
29
- physical: { label: 'Loja Física', icon: MapPin, hint: 'Endereço ou ponto de venda físico.', placeholder: 'https://maps.google.com/…' },
30
- custom: { label: 'Outro canal', icon: Link2, hint: 'Qualquer outro ponto de venda com um link.', placeholder: 'https://…' },
31
- }
32
-
33
- const ADDABLE_KINDS: SalesChannelKind[] = ['whatsapp', 'instagram', 'tiktok_shop', 'marketplace', 'physical', 'custom']
34
-
35
- function resolve(resolver?: ShopProviderResolver): ShopProvider {
36
- return typeof resolver === 'function' ? resolver() : resolver ?? getShopProvider()
37
- }
38
-
39
- // Let any runtime nav contributor (e.g. the app's ChannelsNav) refresh the
40
- // sidebar the moment channels change here, without coupling the two.
41
- function notifyChannelsChanged() {
42
- if (typeof window !== 'undefined') window.dispatchEvent(new Event('fayz:channels-changed'))
43
- }
44
-
45
- export interface ChannelsPageProps {
46
- resolveProvider?: ShopProviderResolver
47
- }
48
-
49
- export function ChannelsPage({ resolveProvider }: ChannelsPageProps) {
50
- const provider = React.useMemo(() => resolve(resolveProvider), [resolveProvider])
51
- const supported = typeof provider.listSalesChannels === 'function'
52
-
53
- const [channels, setChannels] = React.useState<SalesChannel[]>([])
54
- const [loading, setLoading] = React.useState(true)
55
- const [adding, setAdding] = React.useState<SalesChannelKind | null>(null)
56
-
57
- const reload = React.useCallback(async () => {
58
- if (!provider.listSalesChannels) { setLoading(false); return }
59
- setLoading(true)
60
- try {
61
- const rows = await provider.listSalesChannels()
62
- // Guarantee a Loja Online row exists so the storefront link is always
63
- // configurable — seed one on first visit if the store has none.
64
- if (!rows.some((c) => c.kind === 'online_store') && provider.createSalesChannel) {
65
- await provider.createSalesChannel({ kind: 'online_store', label: 'Loja Online', enabled: true, position: 0 })
66
- setChannels(await provider.listSalesChannels())
67
- } else {
68
- setChannels(rows)
69
- }
70
- } finally {
71
- setLoading(false)
72
- }
73
- }, [provider])
74
-
75
- React.useEffect(() => { void reload() }, [reload])
76
-
77
- const patch = async (id: string, input: Parameters<NonNullable<ShopProvider['updateSalesChannel']>>[1]) => {
78
- if (!provider.updateSalesChannel) return
79
- const updated = await provider.updateSalesChannel(id, input)
80
- setChannels((prev) => prev.map((c) => (c.id === id ? updated : c)))
81
- notifyChannelsChanged()
82
- }
83
-
84
- const remove = async (id: string) => {
85
- if (!provider.deleteSalesChannel) return
86
- await provider.deleteSalesChannel(id)
87
- setChannels((prev) => prev.filter((c) => c.id !== id))
88
- notifyChannelsChanged()
89
- }
90
-
91
- const add = async (kind: SalesChannelKind, label: string, url: string) => {
92
- if (!provider.createSalesChannel) return
93
- const created = await provider.createSalesChannel({ kind, label: label || KIND_META[kind].label, url: url || null, enabled: true, position: channels.length })
94
- setChannels((prev) => [...prev, created])
95
- setAdding(null)
96
- notifyChannelsChanged()
97
- }
98
-
99
- if (!supported) {
100
- return (
101
- <div className="mx-auto max-w-4xl p-6">
102
- <h1 className="text-xl font-semibold text-foreground">Canais de venda</h1>
103
- <p className="mt-2 text-sm text-muted-foreground">
104
- Este provedor de loja ainda não expõe canais de venda.
105
- </p>
106
- </div>
107
- )
108
- }
109
-
110
- const online = channels.find((c) => c.kind === 'online_store')
111
- const others = channels.filter((c) => c.kind !== 'online_store')
112
-
113
- return (
114
- <div className="mx-auto max-w-4xl p-6">
115
- <div className="mb-6">
116
- <h1 className="text-xl font-semibold text-foreground">Canais de venda</h1>
117
- <p className="mt-1 text-sm text-muted-foreground">
118
- Onde sua loja vende. A Loja Online é o seu storefront fayz; os demais são links que aparecem no menu lateral.
119
- </p>
120
- </div>
121
-
122
- {loading ? (
123
- <p className="text-sm text-muted-foreground">Carregando…</p>
124
- ) : (
125
- <div className="space-y-6">
126
- {online && <OnlineStoreCard channel={online} onSave={(url) => patch(online.id, { url })} onToggle={(enabled) => patch(online.id, { enabled })} />}
127
-
128
- <div>
129
- <div className="mb-2 flex items-center justify-between">
130
- <h2 className="text-sm font-semibold text-foreground">Outros canais</h2>
131
- </div>
132
- <div className="space-y-2">
133
- {others.map((c) => (
134
- <ChannelRow key={c.id} channel={c} onSave={(url) => patch(c.id, { url })} onToggle={(enabled) => patch(c.id, { enabled })} onRemove={() => remove(c.id)} />
135
- ))}
136
- {others.length === 0 && (
137
- <p className="rounded-card border border-dashed border-border p-4 text-sm text-muted-foreground">
138
- Nenhum canal adicional ainda. Adicione WhatsApp, Instagram, TikTok Shop e outros pontos de venda.
139
- </p>
140
- )}
141
- </div>
142
-
143
- {adding ? (
144
- <AddChannelForm kind={adding} onCancel={() => setAdding(null)} onAdd={(label, url) => add(adding, label, url)} onKind={setAdding} />
145
- ) : (
146
- <div className="mt-3 flex flex-wrap gap-2">
147
- {ADDABLE_KINDS.map((k) => {
148
- const Icon = KIND_META[k].icon
149
- return (
150
- <Button key={k} variant="outline" size="sm" onClick={() => setAdding(k)}>
151
- <Icon className="mr-1.5 h-3.5 w-3.5" /> {KIND_META[k].label}
152
- </Button>
153
- )
154
- })}
155
- </div>
156
- )}
157
- </div>
158
- </div>
159
- )}
160
- </div>
161
- )
162
- }
163
-
164
- function OnlineStoreCard({ channel, onSave, onToggle }: { channel: SalesChannel; onSave: (url: string | null) => void; onToggle: (enabled: boolean) => void }) {
165
- const [url, setUrl] = React.useState(channel.url ?? '')
166
- const dirty = (channel.url ?? '') !== url
167
- const meta = KIND_META.online_store
168
- return (
169
- <div className="rounded-card border border-border bg-card p-5">
170
- <div className="flex items-start gap-3">
171
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary/10">
172
- <meta.icon className="h-5 w-5 text-primary" />
173
- </div>
174
- <div className="min-w-0 flex-1">
175
- <div className="flex items-center gap-2">
176
- <span className="font-semibold text-foreground">Loja Online</span>
177
- <span className={`rounded-full px-2 py-0.5 text-xs font-medium ${channel.enabled ? 'bg-emerald-100 text-emerald-800' : 'bg-muted text-muted-foreground'}`}>
178
- {channel.enabled ? 'Ativa' : 'Inativa'}
179
- </span>
180
- </div>
181
- <p className="mt-0.5 text-sm text-muted-foreground">{meta.hint}</p>
182
-
183
- <div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center">
184
- <Input value={url} placeholder={meta.placeholder} onChange={(e) => setUrl(e.target.value)} className="flex-1" />
185
- <div className="flex gap-2">
186
- <Button size="sm" disabled={!dirty} onClick={() => onSave(url.trim() || null)}>Salvar</Button>
187
- <Button size="sm" variant="outline" disabled={!channel.url} onClick={() => channel.url && window.open(channel.url, '_blank', 'noopener,noreferrer')}>
188
- <ExternalLink className="mr-1.5 h-3.5 w-3.5" /> Ver loja
189
- </Button>
190
- </div>
191
- </div>
192
- <div className="mt-3">
193
- <button type="button" onClick={() => onToggle(!channel.enabled)} className="text-xs font-medium text-muted-foreground hover:text-foreground">
194
- {channel.enabled ? 'Desativar canal' : 'Ativar canal'}
195
- </button>
196
- </div>
197
- </div>
198
- </div>
199
- </div>
200
- )
201
- }
202
-
203
- function ChannelRow({ channel, onSave, onToggle, onRemove }: { channel: SalesChannel; onSave: (url: string | null) => void; onToggle: (enabled: boolean) => void; onRemove: () => void }) {
204
- const [url, setUrl] = React.useState(channel.url ?? '')
205
- const [editing, setEditing] = React.useState(false)
206
- const meta = KIND_META[channel.kind] ?? KIND_META.custom
207
- const Icon = meta.icon
208
- return (
209
- <div className="rounded-card border border-border bg-card p-3">
210
- <div className="flex items-center gap-3">
211
- <div className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-full ${channel.enabled ? 'bg-primary/10' : 'bg-muted'}`}>
212
- <Icon className={`h-4 w-4 ${channel.enabled ? 'text-primary' : 'text-muted-foreground'}`} />
213
- </div>
214
- <div className="min-w-0 flex-1">
215
- <p className="truncate text-sm font-medium text-foreground">{channel.label}</p>
216
- {editing ? (
217
- <Input value={url} placeholder={meta.placeholder} onChange={(e) => setUrl(e.target.value)} className="mt-1 h-8" />
218
- ) : (
219
- <p className="truncate text-xs text-muted-foreground">{channel.url || 'Sem link configurado'}</p>
220
- )}
221
- </div>
222
- <div className="flex shrink-0 items-center gap-1">
223
- {editing ? (
224
- <>
225
- <Button size="sm" variant="ghost" onClick={() => { onSave(url.trim() || null); setEditing(false) }}><Check className="h-4 w-4" /></Button>
226
- <Button size="sm" variant="ghost" onClick={() => { setUrl(channel.url ?? ''); setEditing(false) }}><X className="h-4 w-4" /></Button>
227
- </>
228
- ) : (
229
- <>
230
- {channel.url && (
231
- <Button size="sm" variant="ghost" onClick={() => window.open(channel.url!, '_blank', 'noopener,noreferrer')}><ExternalLink className="h-4 w-4" /></Button>
232
- )}
233
- <button type="button" onClick={() => onToggle(!channel.enabled)} title={channel.enabled ? 'Desativar' : 'Ativar'} className={`rounded-full px-2 py-0.5 text-xs font-medium ${channel.enabled ? 'bg-emerald-100 text-emerald-800' : 'bg-muted text-muted-foreground'}`}>
234
- {channel.enabled ? 'Ativo' : 'Inativo'}
235
- </button>
236
- <Button size="sm" variant="ghost" onClick={() => setEditing(true)}>Editar</Button>
237
- <Button size="sm" variant="ghost" onClick={onRemove}><Trash2 className="h-4 w-4 text-red-500" /></Button>
238
- </>
239
- )}
240
- </div>
241
- </div>
242
- </div>
243
- )
244
- }
245
-
246
- function AddChannelForm({ kind, onKind, onCancel, onAdd }: { kind: SalesChannelKind; onKind: (k: SalesChannelKind) => void; onCancel: () => void; onAdd: (label: string, url: string) => void }) {
247
- const meta = KIND_META[kind]
248
- const [label, setLabel] = React.useState(meta.label)
249
- const [url, setUrl] = React.useState('')
250
- React.useEffect(() => { setLabel(KIND_META[kind].label) }, [kind])
251
- return (
252
- <div className="mt-3 rounded-card border border-border bg-card p-4">
253
- <div className="mb-3 flex flex-wrap gap-2">
254
- {ADDABLE_KINDS.map((k) => {
255
- const Icon = KIND_META[k].icon
256
- return (
257
- <button key={k} type="button" onClick={() => onKind(k)} className={`flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium ${k === kind ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:text-foreground'}`}>
258
- <Icon className="h-3.5 w-3.5" /> {KIND_META[k].label}
259
- </button>
260
- )
261
- })}
262
- </div>
263
- <p className="mb-2 text-xs text-muted-foreground">{meta.hint}</p>
264
- <div className="flex flex-col gap-2 sm:flex-row">
265
- <Input value={label} placeholder="Nome do canal" onChange={(e) => setLabel(e.target.value)} className="sm:w-48" />
266
- <Input value={url} placeholder={meta.placeholder} onChange={(e) => setUrl(e.target.value)} className="flex-1" />
267
- </div>
268
- <div className="mt-3 flex gap-2">
269
- <Button size="sm" onClick={() => onAdd(label, url)}><Plus className="mr-1.5 h-3.5 w-3.5" /> Adicionar</Button>
270
- <Button size="sm" variant="outline" onClick={onCancel}>Cancelar</Button>
271
- </div>
272
- </div>
273
- )
274
- }
@@ -1,319 +0,0 @@
1
- import React, { useEffect, useState } from 'react'
2
- import {
3
- adminNavigateTo, PersonLink,
4
- OrderDetailView as SharedOrderDetailView, OrderDetailSkeleton, OrderDetailBlock, OrderDetailRow,
5
- type OrderDetailData, type OrderDetailInstallment, type OrderDetailTone,
6
- } from '@fayz-ai/saas'
7
- import { shopCustomerLookup } from './customerLookup'
8
- import type { Order, ShopProvider } from '@fayz-ai/shop'
9
- import { getSupabaseClientOptional } from '@fayz-ai/core'
10
-
11
- /** A shipment as stored in public.fulfillments (0018). */
12
- type Fulfillment = {
13
- id: string
14
- status: string
15
- carrier: string | null
16
- service: string | null
17
- tracking_code: string | null
18
- tracking_url: string | null
19
- shipped_at: string | null
20
- estimated_delivery_at: string | null
21
- delivered_at: string | null
22
- }
23
-
24
- type Bill = {
25
- id: string
26
- amount: number
27
- paid_amount: number | null
28
- status: string
29
- due_date: string
30
- installment_number: number | null
31
- }
32
- type Receivable = {
33
- balance: { amount: number; paid: number; balance: number; status: string; reference_number: string | null }
34
- bills: Bill[]
35
- }
36
-
37
- const money = (v: number, currency = 'BRL') =>
38
- new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(Number(v) || 0)
39
- const dateTime = (iso: string) =>
40
- new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(iso))
41
- const dateOnly = (iso: string) => new Date(iso).toLocaleDateString('pt-BR')
42
-
43
- const PAYMENT_LABEL: Record<string, string> = {
44
- pix: 'Pix', credit_card: 'Cartão de crédito', debit_card: 'Cartão de débito',
45
- boleto: 'Boleto', cash: 'Dinheiro', other: 'Outro',
46
- }
47
- const LABEL: Record<string, string> = {
48
- paid: 'Pago', pending: 'Aguardando pagamento', refunded: 'Estornado',
49
- partially_refunded: 'Estorno parcial', voided: 'Cancelado',
50
- fulfilled: 'Enviado', unfulfilled: 'Não enviado', partially_fulfilled: 'Envio parcial',
51
- open: 'Aberto', cancelled: 'Cancelado', archived: 'Arquivado',
52
- }
53
- const RECEIVABLE_LABEL: Record<string, string> = {
54
- open: 'Em aberto', partial: 'Parcialmente recebido', paid: 'Recebido', overdue: 'Vencido', cancelled: 'Cancelado',
55
- }
56
- const SHIPMENT_LABEL: Record<string, string> = {
57
- pending: 'Aguardando coleta', in_transit: 'Em trânsito', delivered: 'Entregue',
58
- failed: 'Falhou', returned: 'Devolvido', cancelled: 'Cancelado',
59
- }
60
-
61
- /** Map a shop/receivable status string to the shared component's tone. */
62
- function toneOf(value: string): OrderDetailTone {
63
- switch (value) {
64
- case 'paid': case 'fulfilled': case 'delivered': return 'success'
65
- case 'pending': case 'partial': case 'partially_fulfilled': case 'open': case 'in_transit': return 'warning'
66
- case 'refunded': case 'partially_refunded': case 'voided': case 'cancelled': case 'overdue': case 'failed': case 'returned': return 'destructive'
67
- default: return 'muted'
68
- }
69
- }
70
-
71
- /** Small inline badge for the shop-specific blocks (Entrega/Histórico). */
72
- function InlineBadge({ value }: { value: string }) {
73
- const tone = toneOf(value)
74
- const cls: Record<OrderDetailTone, string> = {
75
- success: 'bg-emerald-100 text-emerald-800', warning: 'bg-amber-100 text-amber-800',
76
- destructive: 'bg-red-100 text-red-700', info: 'bg-sky-100 text-sky-800', muted: 'bg-muted text-muted-foreground',
77
- }
78
- return <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${cls[tone]}`}>{LABEL[value] ?? value}</span>
79
- }
80
-
81
- function deliveryFromNotes(notes: string | null): string | null {
82
- if (!notes) return null
83
- const match = notes.match(/entrega:\s*(.+)/i)
84
- return (match?.[1] ?? '').trim() || null
85
- }
86
-
87
- // Cache por id (fora do componente) — trocar de aba remonta a view; sem cache
88
- // isso refaz o getOrder (skeleton + request) toda vez. Cache-first evita o refetch;
89
- // "Registrar recebimento" atualiza o cache.
90
- const shopOrderCache = new Map<string, Order>()
91
- const shopShipmentsCache = new Map<string, Fulfillment[]>()
92
- const shopReceivableCache = new Map<string, Receivable | null>()
93
-
94
- export function OrderDetailView({ orderId, provider, onBack }: {
95
- orderId: string
96
- provider: () => ShopProvider
97
- onBack: () => void
98
- }) {
99
- const [order, setOrder] = useState<Order | null>(() => shopOrderCache.get(orderId) ?? null)
100
- const [shipments, setShipments] = useState<Fulfillment[]>(() => shopShipmentsCache.get(orderId) ?? [])
101
- const [loading, setLoading] = useState(() => !shopOrderCache.has(orderId))
102
- const [error, setError] = useState<string | null>(null)
103
- const [confirming, setConfirming] = useState(false)
104
- const [confirmError, setConfirmError] = useState<string | null>(null)
105
- const [receivable, setReceivable] = useState<Receivable | null>(() => shopReceivableCache.get(orderId) ?? null)
106
-
107
- useEffect(() => {
108
- let cancelled = false
109
- const cached = shopOrderCache.get(orderId)
110
- if (cached) { setOrder(cached); setLoading(false); return () => { cancelled = true } } // cache hit → sem request
111
- setLoading(true); setError(null)
112
- provider().getOrder(orderId)
113
- .then((o) => { if (!cancelled) { if (o) shopOrderCache.set(orderId, o); setOrder(o) } })
114
- .catch((err: unknown) => { if (!cancelled) setError(err instanceof Error ? err.message : String(err)) })
115
- .finally(() => { if (!cancelled) setLoading(false) })
116
- return () => { cancelled = true }
117
- }, [orderId, provider])
118
-
119
- useEffect(() => {
120
- let cancelled = false
121
- const db = getSupabaseClientOptional() as unknown as { from: (t: string) => any } | null
122
- if (!db) return
123
- if (shopShipmentsCache.has(orderId)) return () => { cancelled = true } // cache hit → sem request
124
- db.from('fulfillments').select('*').eq('order_id', orderId).order('created_at', { ascending: false })
125
- .then(({ data }: { data: Fulfillment[] | null }) => { if (!cancelled) { shopShipmentsCache.set(orderId, data ?? []); setShipments(data ?? []) } })
126
- return () => { cancelled = true }
127
- }, [orderId])
128
-
129
- // The order does not decide whether it was paid — the receivable does. Keep this
130
- // hook ABOVE the early returns (hook order must be stable).
131
- useEffect(() => {
132
- let cancelled = false
133
- const db = getSupabaseClientOptional() as unknown as { from: (t: string) => any } | null
134
- if (!db) return
135
- if (shopReceivableCache.has(orderId)) { setReceivable(shopReceivableCache.get(orderId) ?? null); return () => { cancelled = true } } // cache hit → sem request
136
- void (async () => {
137
- const [balance, bills] = await Promise.all([
138
- db.from('v_invoice_balances').select('*').eq('invoice_id', orderId).maybeSingle(),
139
- db.from('plg_financial_movements').select('*').eq('invoice_id', orderId).eq('movement_kind', 'bill').order('installment_number', { ascending: true }),
140
- ])
141
- if (cancelled || !balance?.data) return
142
- const rec = { balance: balance.data, bills: bills?.data ?? [] }
143
- shopReceivableCache.set(orderId, rec)
144
- setReceivable(rec)
145
- })()
146
- return () => { cancelled = true }
147
- }, [orderId, confirming])
148
-
149
- if (loading) return <OrderDetailSkeleton />
150
- if (error) {
151
- return (
152
- <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
153
- <p className="font-medium">Não foi possível carregar o pedido.</p>
154
- <p className="mt-1 text-red-700">{error}</p>
155
- </div>
156
- )
157
- }
158
- if (!order) return <div className="py-12 text-center text-sm text-muted-foreground">Pedido não encontrado.</div>
159
-
160
- const cur = order.currency
161
- const delivery = deliveryFromNotes(order.notes)
162
- const addr = order.shippingAddress
163
-
164
- async function registerReceipt() {
165
- if (!order) return
166
- shopReceivableCache.delete(order.id) // pagamento muda o recebível → força refetch
167
- setConfirming(true); setConfirmError(null)
168
- try {
169
- const fresh = (await provider().confirmPayment?.(order.id)) ?? (await provider().getOrder(order.id))
170
- if (fresh) { shopOrderCache.set(order.id, fresh); setOrder(fresh) } // atualiza o cache
171
- } catch (err: unknown) {
172
- setConfirmError(err instanceof Error ? err.message : String(err))
173
- } finally { setConfirming(false) }
174
- }
175
-
176
- const installments: OrderDetailInstallment[] = (receivable && receivable.bills.length > 1)
177
- ? receivable.bills.map((b) => ({
178
- number: b.installment_number ?? 0,
179
- amount: `${money(Number(b.paid_amount ?? 0), cur)} / ${money(Number(b.amount), cur)}`,
180
- status: RECEIVABLE_LABEL[b.status] ?? b.status,
181
- statusTone: toneOf(b.status),
182
- dueDate: `vence ${dateOnly(b.due_date)}`,
183
- }))
184
- : []
185
-
186
- const data: OrderDetailData = {
187
- title: `Pedido #${order.orderNumber}`,
188
- subtitle: dateTime(order.createdAt),
189
- badges: [
190
- { label: LABEL[order.financialStatus] ?? order.financialStatus, tone: toneOf(order.financialStatus) },
191
- { label: LABEL[order.fulfillmentStatus] ?? order.fulfillmentStatus, tone: toneOf(order.fulfillmentStatus) },
192
- ],
193
- customer: { name: order.customerName, email: order.customerEmail },
194
- customerNode: (
195
- <div className="space-y-1 text-sm">
196
- {order.customerId ? (
197
- <PersonLink
198
- personId={order.customerId}
199
- name={order.customerName ?? order.customerEmail ?? 'Cliente'}
200
- lookup={shopCustomerLookup(provider)}
201
- profileHref={`#/customers/${order.customerId}`}
202
- className="font-medium"
203
- />
204
- ) : (
205
- <p className="font-medium">{order.customerName ?? '—'}</p>
206
- )}
207
- {order.customerEmail && <p className="text-muted-foreground">{order.customerEmail}</p>}
208
- {!order.customerId && <p className="text-xs text-muted-foreground">Compra sem cadastro — não há ficha de cliente para abrir.</p>}
209
- </div>
210
- ),
211
- items: order.items.map((it) => ({
212
- id: it.id, name: it.name, description: it.sku, imageUrl: it.imageUrl,
213
- quantity: it.quantity, unitPrice: money(it.unitPrice, cur), total: money(it.total, cur),
214
- })),
215
- totals: [
216
- { label: 'Subtotal', value: money(order.subtotal, cur) },
217
- ...(order.discountTotal > 0 ? [{ label: `Desconto${order.discountCode ? ` (${order.discountCode})` : ''}`, value: `− ${money(order.discountTotal, cur)}`, muted: true }] : []),
218
- { label: 'Entrega', value: money(order.shippingTotal, cur) },
219
- { label: 'Impostos', value: money(order.taxTotal, cur) },
220
- { label: 'Total', value: money(order.total, cur), strong: true },
221
- ],
222
- paymentMethod: order.paymentMethodKind ? (PAYMENT_LABEL[order.paymentMethodKind] ?? order.paymentMethodKind) : undefined,
223
- paidAt: order.paidAt ? dateTime(order.paidAt) : undefined,
224
- receivable: receivable ? {
225
- referenceNumber: receivable.balance.reference_number ?? undefined,
226
- statusBadge: { label: RECEIVABLE_LABEL[receivable.balance.status] ?? receivable.balance.status, tone: toneOf(receivable.balance.status) },
227
- rows: [
228
- { label: 'Faturado', value: money(receivable.balance.amount, cur) },
229
- { label: 'Recebido', value: money(receivable.balance.paid, cur) },
230
- { label: 'Saldo', value: money(receivable.balance.balance, cur), strong: true },
231
- ],
232
- installments,
233
- actions: (
234
- <>
235
- {receivable.balance.balance > 0 && receivable.balance.status !== 'cancelled' && (
236
- <button type="button" onClick={registerReceipt} disabled={confirming} data-testid="order-register-receipt"
237
- className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition hover:opacity-90 disabled:opacity-60">
238
- {confirming ? 'Registrando…' : 'Registrar recebimento'}
239
- </button>
240
- )}
241
- <button type="button" onClick={() => adminNavigateTo('/financial')}
242
- className="rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted">
243
- Abrir no Financeiro
244
- </button>
245
- {confirmError && <p className="mt-2 w-full rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">{confirmError}</p>}
246
- </>
247
- ),
248
- emptyLabel: 'Sem conta a receber para este pedido.',
249
- } : { rows: [], installments: [], emptyLabel: 'Sem conta a receber para este pedido.' },
250
- extraSections: (
251
- <>
252
- <OrderDetailBlock title="Entrega" hint="O que a logística precisa">
253
- <div className="space-y-2 text-sm">
254
- {addr ? (
255
- <div className="space-y-0.5">
256
- <p>{[addr.street, addr.number].filter(Boolean).join(', ')}{addr.complement ? ` — ${addr.complement}` : ''}</p>
257
- {addr.district && <p className="text-muted-foreground">{addr.district}</p>}
258
- <p className="text-muted-foreground">
259
- {[addr.city, addr.state].filter((v) => v && v !== '—').join('/')}{addr.postal_code ? ` · ${addr.postal_code}` : ''}
260
- </p>
261
- {(addr.recipient || addr.phone) && <p className="text-xs text-muted-foreground">{[addr.recipient, addr.phone].filter(Boolean).join(' · ')}</p>}
262
- </div>
263
- ) : (
264
- <p className={delivery ? '' : 'text-muted-foreground'}>{delivery ?? 'Nenhum endereço registrado neste pedido.'}</p>
265
- )}
266
- <div className="border-t pt-2">
267
- <OrderDetailRow label="Status" value={<InlineBadge value={order.fulfillmentStatus} />} />
268
- {shipments.length === 0 ? (
269
- <p className="mt-1 text-xs text-muted-foreground">Nenhum envio registrado.</p>
270
- ) : (
271
- <div className="mt-2 space-y-2">
272
- {shipments.map((s) => (
273
- <div key={s.id} className="rounded-md border px-2 py-1.5">
274
- <div className="flex items-center justify-between gap-2">
275
- <span className="text-sm font-medium">{[s.carrier, s.service].filter(Boolean).join(' · ') || 'Envio'}</span>
276
- <span className="text-[10px] text-muted-foreground">{SHIPMENT_LABEL[s.status] ?? s.status}</span>
277
- </div>
278
- {s.tracking_code && (
279
- <p className="mt-0.5 font-mono text-xs">
280
- {s.tracking_url ? <a href={s.tracking_url} target="_blank" rel="noreferrer" className="text-primary hover:underline">{s.tracking_code}</a> : s.tracking_code}
281
- </p>
282
- )}
283
- <div className="mt-1 space-y-0.5 text-[11px] text-muted-foreground">
284
- {s.shipped_at && <p>Despachado em {dateTime(s.shipped_at)}</p>}
285
- {s.estimated_delivery_at && <p>Previsão: {dateTime(s.estimated_delivery_at)}</p>}
286
- {s.delivered_at && <p>Entregue em {dateTime(s.delivered_at)}</p>}
287
- </div>
288
- </div>
289
- ))}
290
- </div>
291
- )}
292
- </div>
293
- </div>
294
- </OrderDetailBlock>
295
-
296
- <OrderDetailBlock title="Histórico">
297
- <div className="space-y-1 text-sm">
298
- <OrderDetailRow label="Criado" value={dateTime(order.createdAt)} />
299
- <OrderDetailRow label="Atualizado" value={dateTime(order.updatedAt)} />
300
- <OrderDetailRow label="Situação" value={<InlineBadge value={order.status} />} />
301
- </div>
302
- </OrderDetailBlock>
303
-
304
- {order.notes && (
305
- <OrderDetailBlock title="Observações">
306
- <p className="whitespace-pre-wrap text-sm text-muted-foreground">{order.notes}</p>
307
- </OrderDetailBlock>
308
- )}
309
- </>
310
- ),
311
- onBack,
312
- backLabel: 'Pedidos',
313
- }
314
-
315
- return <SharedOrderDetailView {...data} />
316
- }
317
-
318
- export const orderDetailPath = (id: string) => `/shop/orders/${id}`
319
- export const goToOrder = (id: string) => adminNavigateTo(orderDetailPath(id))