@fayz-ai/plugin-shop 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ShopPage-TVNY74FF.js → ShopPage-JP27MZS2.js} +222 -275
- package/dist/ShopPage-JP27MZS2.js.map +1 -0
- package/dist/{ShopPage-ALBPZAEW.cjs → ShopPage-NK6FSPJ5.cjs} +221 -274
- package/dist/ShopPage-NK6FSPJ5.cjs.map +1 -0
- package/dist/index.cjs +253 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +248 -20
- package/dist/index.js.map +1 -1
- package/dist/views/ChannelsPage.d.ts +7 -0
- package/dist/views/ChannelsPage.d.ts.map +1 -0
- package/dist/views/OrderDetailView.d.ts +1 -1
- package/dist/views/OrderDetailView.d.ts.map +1 -1
- package/dist/views/discountEntity.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/ShopPage.tsx +1 -1
- package/src/index.ts +47 -2
- package/src/views/ChannelsPage.tsx +274 -0
- package/src/views/OrderDetailView.tsx +192 -452
- package/src/views/discountEntity.tsx +7 -0
- package/dist/ShopPage-ALBPZAEW.cjs.map +0 -1
- package/dist/ShopPage-TVNY74FF.js.map +0 -1
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fayz": {
|
|
4
4
|
"status": "preview"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.8.
|
|
6
|
+
"version": "0.8.4",
|
|
7
7
|
"description": "[experimental] Fayz SDK — plugin-shop plugin",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.cjs",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@tanstack/react-table": "^8.20.0",
|
|
30
30
|
"lucide-react": "^0.400.0",
|
|
31
31
|
"zustand": "^4.5.0",
|
|
32
|
-
"@fayz-ai/core": "^0.8.
|
|
33
|
-
"@fayz-ai/saas": "^0.8.
|
|
34
|
-
"@fayz-ai/
|
|
35
|
-
"@fayz-ai/
|
|
32
|
+
"@fayz-ai/core": "^0.8.4",
|
|
33
|
+
"@fayz-ai/saas": "^0.8.4",
|
|
34
|
+
"@fayz-ai/ui": "^0.8.4",
|
|
35
|
+
"@fayz-ai/shop": "^0.8.4"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "^18.3.0",
|
package/src/ShopPage.tsx
CHANGED
|
@@ -191,7 +191,7 @@ function OrdersTab({ provider }: ShopPageProps) {
|
|
|
191
191
|
personId={id}
|
|
192
192
|
name={label}
|
|
193
193
|
lookup={shopCustomerLookup(() => resolveShopProvider(provider))}
|
|
194
|
-
profileHref={`#/
|
|
194
|
+
profileHref={`#/customers/${id}`}
|
|
195
195
|
/>
|
|
196
196
|
)
|
|
197
197
|
},
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createShopDiscountDataProvider } from './data/discountDataProvider'
|
|
|
12
12
|
import { createShopShippingZoneDataProvider } from './data/shippingZoneDataProvider'
|
|
13
13
|
import type { ShopProviderResolver } from './ShopPage'
|
|
14
14
|
import { ShopSettings } from './components/ShopSettings'
|
|
15
|
+
import { ChannelsPage } from './views/ChannelsPage'
|
|
15
16
|
|
|
16
17
|
const ShopPage = React.lazy(() => import('./ShopPage').then((m) => ({ default: m.ShopPage })))
|
|
17
18
|
|
|
@@ -45,7 +46,8 @@ export function createShopPlugin(options?: ShopPluginOptions): PluginManifest {
|
|
|
45
46
|
// Teach the shared person-path registry where a SHOP customer lives. The
|
|
46
47
|
// built-in map ships veterinary defaults (customer → /registry/tutors), so
|
|
47
48
|
// without this PersonLink would resolve a shop customer to the wrong module.
|
|
48
|
-
|
|
49
|
+
// Clientes now lives at the top level (/customers), out of the Loja accordion.
|
|
50
|
+
registerEntityPath('shop_customer', '/customers')
|
|
49
51
|
|
|
50
52
|
type ShopSection = 'products' | 'orders' | 'customers' | 'discounts' | 'shipping'
|
|
51
53
|
|
|
@@ -102,14 +104,25 @@ export function createShopPlugin(options?: ShopPluginOptions): PluginManifest {
|
|
|
102
104
|
shipping: ShippingZonesCrudPage as React.ComponentType<unknown>,
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
// "Clientes" is intentionally NOT here: it moved out of the Loja accordion to
|
|
108
|
+
// a top-level nav item (/customers), registered separately below. The shell
|
|
109
|
+
// nests by route prefix, so a route under /shop is what put it inside "Loja".
|
|
105
110
|
const SECTIONS: Array<{ id: ShopSection; label: string; icon: string }> = [
|
|
106
111
|
{ id: 'products', label: options?.sectionLabels?.products ?? 'Produtos', icon: 'Package' },
|
|
107
112
|
{ id: 'orders', label: options?.sectionLabels?.orders ?? 'Pedidos', icon: 'ShoppingCart' },
|
|
108
|
-
{ id: 'customers', label: options?.sectionLabels?.customers ?? 'Clientes', icon: 'Users' },
|
|
109
113
|
{ id: 'discounts', label: options?.sectionLabels?.discounts ?? 'Descontos', icon: 'Tag' },
|
|
110
114
|
{ id: 'shipping', label: options?.sectionLabels?.shipping ?? 'Entrega', icon: 'Truck' },
|
|
111
115
|
]
|
|
112
116
|
|
|
117
|
+
// Standalone Customers page — same CRUD component, now at /customers so it
|
|
118
|
+
// reads as its own top-level destination instead of a Loja sub-tab.
|
|
119
|
+
const CustomersNavLabel = options?.sectionLabels?.customers ?? 'Clientes'
|
|
120
|
+
|
|
121
|
+
// Canais de venda — wraps the ChannelsPage with the app's shop provider.
|
|
122
|
+
const ChannelsPageWrapped: React.ComponentType<unknown> = () =>
|
|
123
|
+
React.createElement(ChannelsPage, { resolveProvider: options?.provider })
|
|
124
|
+
ChannelsPageWrapped.displayName = 'ChannelsPage'
|
|
125
|
+
|
|
113
126
|
const navSection = options?.navSection ?? 'main'
|
|
114
127
|
const navPosition = options?.navPosition ?? 6
|
|
115
128
|
|
|
@@ -144,6 +157,26 @@ export function createShopPlugin(options?: ShopPluginOptions): PluginManifest {
|
|
|
144
157
|
icon: s.icon,
|
|
145
158
|
permission: { feature: 'shop', action: 'read' as const },
|
|
146
159
|
})),
|
|
160
|
+
// Clientes — top-level (route not under /shop, so it leaves the accordion).
|
|
161
|
+
// Fractional position keeps it right below "Loja", above other plugins.
|
|
162
|
+
{
|
|
163
|
+
section: navSection,
|
|
164
|
+
position: navPosition + 0.5,
|
|
165
|
+
label: CustomersNavLabel,
|
|
166
|
+
route: '/customers',
|
|
167
|
+
icon: 'Users',
|
|
168
|
+
permission: { feature: 'shop', action: 'read' as const },
|
|
169
|
+
},
|
|
170
|
+
// Canais de venda — inside the "Loja" group (route under /shop nests it in
|
|
171
|
+
// the accordion). The store's points of sale (Loja Online, WhatsApp, …).
|
|
172
|
+
{
|
|
173
|
+
section: navSection,
|
|
174
|
+
position: navPosition + SECTIONS.length + 1,
|
|
175
|
+
label: 'Canais de venda',
|
|
176
|
+
route: '/shop/channels',
|
|
177
|
+
icon: 'Radio',
|
|
178
|
+
permission: { feature: 'shop', action: 'read' as const },
|
|
179
|
+
},
|
|
147
180
|
],
|
|
148
181
|
routes: [
|
|
149
182
|
{
|
|
@@ -157,6 +190,18 @@ export function createShopPlugin(options?: ShopPluginOptions): PluginManifest {
|
|
|
157
190
|
component: CRUD_PAGES[s.id] ?? pageFor(s.id),
|
|
158
191
|
permission: { feature: 'shop', action: 'read' as const },
|
|
159
192
|
})),
|
|
193
|
+
// Clientes at the top level, same CRUD page as before.
|
|
194
|
+
{
|
|
195
|
+
path: '/customers',
|
|
196
|
+
component: CustomersCrudPage as React.ComponentType<unknown>,
|
|
197
|
+
permission: { feature: 'shop', action: 'read' as const },
|
|
198
|
+
},
|
|
199
|
+
// Canais de venda management page (under the Loja group).
|
|
200
|
+
{
|
|
201
|
+
path: '/shop/channels',
|
|
202
|
+
component: ChannelsPageWrapped,
|
|
203
|
+
permission: { feature: 'shop', action: 'read' as const },
|
|
204
|
+
},
|
|
160
205
|
],
|
|
161
206
|
widgets: [],
|
|
162
207
|
aiTools: [
|
|
@@ -0,0 +1,274 @@
|
|
|
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
|
+
}
|