@fayz-ai/saas 0.8.0 → 0.8.2

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 (236) hide show
  1. package/dist/access/context.d.ts +47 -0
  2. package/dist/access/context.d.ts.map +1 -0
  3. package/dist/access/headless.d.ts +21 -0
  4. package/dist/access/headless.d.ts.map +1 -0
  5. package/dist/access/index.d.ts +12 -0
  6. package/dist/access/index.d.ts.map +1 -0
  7. package/dist/access/limits-registry.d.ts +33 -0
  8. package/dist/access/limits-registry.d.ts.map +1 -0
  9. package/dist/access/module-nav.d.ts +15 -0
  10. package/dist/access/module-nav.d.ts.map +1 -0
  11. package/dist/access/resolver.d.ts +2 -0
  12. package/dist/access/resolver.d.ts.map +1 -0
  13. package/dist/access/types.d.ts +43 -0
  14. package/dist/access/types.d.ts.map +1 -0
  15. package/dist/access/upgrade-modal-store.d.ts +23 -0
  16. package/dist/access/upgrade-modal-store.d.ts.map +1 -0
  17. package/dist/app/AdminShell.d.ts +3 -0
  18. package/dist/app/AdminShell.d.ts.map +1 -1
  19. package/dist/app/admin-app.d.ts +3 -1
  20. package/dist/app/admin-app.d.ts.map +1 -1
  21. package/dist/app/config.d.ts +47 -2
  22. package/dist/app/config.d.ts.map +1 -1
  23. package/dist/app/core-entities.d.ts +25 -0
  24. package/dist/app/core-entities.d.ts.map +1 -0
  25. package/dist/app/derive-contract.d.ts +8 -0
  26. package/dist/app/derive-contract.d.ts.map +1 -0
  27. package/dist/app/scaffold.d.ts.map +1 -1
  28. package/dist/archetype-lookup.d.ts.map +1 -1
  29. package/dist/billing/index.cjs +6 -2
  30. package/dist/billing/index.d.ts +2 -0
  31. package/dist/billing/index.d.ts.map +1 -1
  32. package/dist/billing/index.js +1 -1
  33. package/dist/billing/plan-badge.d.ts +17 -0
  34. package/dist/billing/plan-badge.d.ts.map +1 -0
  35. package/dist/billing/store.d.ts +6 -1
  36. package/dist/billing/store.d.ts.map +1 -1
  37. package/dist/{chunk-3M25GRRU.js → chunk-I3DY47N5.js} +3 -4
  38. package/dist/chunk-I3DY47N5.js.map +1 -0
  39. package/dist/chunk-NNFGMUF2.cjs +39 -0
  40. package/dist/chunk-NNFGMUF2.cjs.map +1 -0
  41. package/dist/{chunk-4XTQ4V5L.cjs → chunk-SAZLW54Y.cjs} +16 -12
  42. package/dist/chunk-SAZLW54Y.cjs.map +1 -0
  43. package/dist/{chunk-IPWCHJPE.cjs → chunk-VKYZACG5.cjs} +5 -6
  44. package/dist/chunk-VKYZACG5.cjs.map +1 -0
  45. package/dist/chunk-VN5NTK26.js +36 -0
  46. package/dist/chunk-VN5NTK26.js.map +1 -0
  47. package/dist/{chunk-XNWCGJYH.js → chunk-VTEP2AQX.js} +15 -11
  48. package/dist/chunk-VTEP2AQX.js.map +1 -0
  49. package/dist/components/shared/ContactPicker.d.ts +92 -0
  50. package/dist/components/shared/ContactPicker.d.ts.map +1 -0
  51. package/dist/components/shared/PersonLink.d.ts +12 -1
  52. package/dist/components/shared/PersonLink.d.ts.map +1 -1
  53. package/dist/components/shared/create-person.d.ts +20 -0
  54. package/dist/components/shared/create-person.d.ts.map +1 -0
  55. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  56. package/dist/crud/CrudFormPage.d.ts.map +1 -1
  57. package/dist/crud/CrudListView.d.ts.map +1 -1
  58. package/dist/crud/CrudPage.d.ts.map +1 -1
  59. package/dist/crud/archetypes/AddressesTab.d.ts +21 -0
  60. package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -0
  61. package/dist/crud/archetypes/PersonDetailTabs.d.ts +2 -1
  62. package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
  63. package/dist/crud/createCrudPage.d.ts.map +1 -1
  64. package/dist/crud/index.d.ts +1 -0
  65. package/dist/crud/index.d.ts.map +1 -1
  66. package/dist/index.cjs +4300 -722
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/index.d.ts +17 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4080 -542
  71. package/dist/index.js.map +1 -1
  72. package/dist/org/index.cjs +11 -11
  73. package/dist/org/index.js +2 -2
  74. package/dist/org/store.d.ts.map +1 -1
  75. package/dist/permissions/context.d.ts +2 -0
  76. package/dist/permissions/context.d.ts.map +1 -1
  77. package/dist/permissions/index.cjs +7 -7
  78. package/dist/permissions/index.js +1 -1
  79. package/dist/shell/components/auth/ProtectedRoute.d.ts.map +1 -1
  80. package/dist/shell/components/billing/SoftLimitBanner.d.ts +5 -0
  81. package/dist/shell/components/billing/SoftLimitBanner.d.ts.map +1 -0
  82. package/dist/shell/components/billing/SubscriptionPage.d.ts +5 -0
  83. package/dist/shell/components/billing/SubscriptionPage.d.ts.map +1 -0
  84. package/dist/shell/components/billing/UpgradeModal.d.ts +3 -0
  85. package/dist/shell/components/billing/UpgradeModal.d.ts.map +1 -0
  86. package/dist/shell/components/billing/UpgradePrompt.d.ts +10 -0
  87. package/dist/shell/components/billing/UpgradePrompt.d.ts.map +1 -0
  88. package/dist/shell/components/billing/access-contract.d.ts +18 -0
  89. package/dist/shell/components/billing/access-contract.d.ts.map +1 -0
  90. package/dist/shell/components/billing/gates.d.ts +14 -0
  91. package/dist/shell/components/billing/gates.d.ts.map +1 -0
  92. package/dist/shell/components/billing/index.d.ts +9 -2
  93. package/dist/shell/components/billing/index.d.ts.map +1 -1
  94. package/dist/shell/components/chat/ChatFab.d.ts +3 -1
  95. package/dist/shell/components/chat/ChatFab.d.ts.map +1 -1
  96. package/dist/shell/components/chat/ChatPanel.d.ts +3 -1
  97. package/dist/shell/components/chat/ChatPanel.d.ts.map +1 -1
  98. package/dist/shell/components/chat/ConfirmActionCard.d.ts +13 -0
  99. package/dist/shell/components/chat/ConfirmActionCard.d.ts.map +1 -0
  100. package/dist/shell/components/chat/markdown.d.ts +5 -0
  101. package/dist/shell/components/chat/markdown.d.ts.map +1 -0
  102. package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
  103. package/dist/shell/components/organization/TenantOnboarding.d.ts.map +1 -1
  104. package/dist/shell/components/organization/index.d.ts +0 -1
  105. package/dist/shell/components/organization/index.d.ts.map +1 -1
  106. package/dist/shell/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
  107. package/dist/shell/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
  108. package/dist/shell/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
  109. package/dist/shell/components/settings/LocationsCrudPage.d.ts +1 -1
  110. package/dist/shell/components/settings/LocationsCrudPage.d.ts.map +1 -1
  111. package/dist/shell/components/settings/SettingsPage.d.ts +23 -2
  112. package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
  113. package/dist/shell/hooks/index.d.ts +3 -2
  114. package/dist/shell/hooks/index.d.ts.map +1 -1
  115. package/dist/shell/hooks/useAITools.d.ts +2 -0
  116. package/dist/shell/hooks/useAITools.d.ts.map +1 -1
  117. package/dist/shell/hooks/useChat.d.ts +22 -0
  118. package/dist/shell/hooks/useChat.d.ts.map +1 -1
  119. package/dist/shell/hooks/useSystemPermission.d.ts +3 -0
  120. package/dist/shell/hooks/useSystemPermission.d.ts.map +1 -0
  121. package/dist/shell/hooks/useTenantPluginSettings.d.ts +12 -0
  122. package/dist/shell/hooks/useTenantPluginSettings.d.ts.map +1 -0
  123. package/dist/shell/lib/ai-tool-handlers.d.ts +119 -0
  124. package/dist/shell/lib/ai-tool-handlers.d.ts.map +1 -0
  125. package/dist/shell/lib/core-ai-tools.d.ts +14 -0
  126. package/dist/shell/lib/core-ai-tools.d.ts.map +1 -1
  127. package/dist/shell/lib/create-client-orders-provider.d.ts.map +1 -1
  128. package/dist/shell/lib/fayz-agent.d.ts +23 -0
  129. package/dist/shell/lib/fayz-agent.d.ts.map +1 -0
  130. package/dist/shell/lib/i18n.d.ts.map +1 -1
  131. package/dist/shell/stores/chat.store.d.ts +69 -0
  132. package/dist/shell/stores/chat.store.d.ts.map +1 -1
  133. package/dist/shell/types/billing.d.ts +7 -0
  134. package/dist/shell/types/billing.d.ts.map +1 -1
  135. package/dist/shell/types/crud.d.ts +1 -1
  136. package/dist/shell/types/crud.d.ts.map +1 -1
  137. package/dist/shell/types/plugins.d.ts +2 -45
  138. package/dist/shell/types/plugins.d.ts.map +1 -1
  139. package/dist/stores/createCrudStore.d.ts.map +1 -1
  140. package/package.json +15 -12
  141. package/src/access/context.tsx +228 -0
  142. package/src/access/headless.ts +71 -0
  143. package/src/access/index.ts +27 -0
  144. package/src/access/limits-registry.ts +86 -0
  145. package/src/access/module-nav.test.ts +78 -0
  146. package/src/access/module-nav.ts +82 -0
  147. package/src/access/resolver.test.ts +143 -0
  148. package/src/access/resolver.ts +5 -0
  149. package/src/access/types.ts +47 -0
  150. package/src/access/upgrade-modal-store.ts +30 -0
  151. package/src/app/AdminShell.tsx +451 -68
  152. package/src/app/admin-app.tsx +46 -9
  153. package/src/app/config.ts +54 -1
  154. package/src/app/core-entities.ts +110 -0
  155. package/src/app/derive-contract.test.ts +155 -0
  156. package/src/app/derive-contract.ts +214 -0
  157. package/src/app/scaffold.tsx +4 -0
  158. package/src/archetype-lookup.ts +8 -2
  159. package/src/billing/index.ts +2 -0
  160. package/src/billing/plan-badge.ts +32 -0
  161. package/src/billing/store.ts +8 -1
  162. package/src/components/shared/ContactPicker.tsx +394 -0
  163. package/src/components/shared/PersonLink.tsx +11 -3
  164. package/src/components/shared/create-person.ts +74 -0
  165. package/src/crud/CrudDetailPage.tsx +8 -1
  166. package/src/crud/CrudFormPage.tsx +124 -6
  167. package/src/crud/CrudListView.tsx +16 -1
  168. package/src/crud/CrudPage.tsx +27 -5
  169. package/src/crud/DeleteConfirmDialog.tsx +1 -1
  170. package/src/crud/archetypes/AddressesTab.tsx +387 -0
  171. package/src/crud/archetypes/PersonDetailTabs.tsx +5 -0
  172. package/src/crud/createCrudPage.tsx +2 -0
  173. package/src/crud/index.ts +6 -0
  174. package/src/env.d.ts +7 -0
  175. package/src/index.ts +49 -1
  176. package/src/org/store.ts +9 -1
  177. package/src/permissions/context.tsx +29 -15
  178. package/src/shell/components/auth/ProtectedRoute.tsx +12 -2
  179. package/src/shell/components/billing/SoftLimitBanner.tsx +81 -0
  180. package/src/shell/components/billing/SubscriptionPage.tsx +372 -0
  181. package/src/shell/components/billing/UpgradeModal.tsx +102 -0
  182. package/src/shell/components/billing/UpgradePrompt.tsx +229 -0
  183. package/src/shell/components/billing/access-contract.ts +64 -0
  184. package/src/shell/components/billing/gates.tsx +62 -0
  185. package/src/shell/components/billing/index.ts +18 -2
  186. package/src/shell/components/chat/ChatFab.tsx +4 -2
  187. package/src/shell/components/chat/ChatPanel.tsx +174 -23
  188. package/src/shell/components/chat/ConfirmActionCard.tsx +57 -0
  189. package/src/shell/components/chat/markdown.tsx +102 -0
  190. package/src/shell/components/organization/TeamTab.tsx +21 -4
  191. package/src/shell/components/organization/TenantOnboarding.tsx +3 -1
  192. package/src/shell/components/organization/index.ts +0 -1
  193. package/src/shell/components/settings/ConnectedCompanySettings.tsx +4 -1
  194. package/src/shell/components/settings/ConnectedFieldRulesSettings.tsx +3 -1
  195. package/src/shell/components/settings/ConnectedSecuritySettings.tsx +3 -1
  196. package/src/shell/components/settings/LocationsCrudPage.ts +6 -1
  197. package/src/shell/components/settings/SettingsPage.tsx +63 -21
  198. package/src/shell/hooks/index.ts +3 -2
  199. package/src/shell/hooks/useAITools.ts +27 -23
  200. package/src/shell/hooks/useChat.ts +381 -16
  201. package/src/shell/hooks/useSystemPermission.ts +23 -0
  202. package/src/shell/hooks/useTenantPluginSettings.ts +127 -0
  203. package/src/shell/lib/ai-tool-handlers.ts +910 -0
  204. package/src/shell/lib/core-ai-tools.ts +234 -2
  205. package/src/shell/lib/create-client-orders-provider.ts +24 -2
  206. package/src/shell/lib/fayz-agent.ts +56 -0
  207. package/src/shell/lib/i18n.ts +71 -0
  208. package/src/shell/stores/chat.store.ts +118 -1
  209. package/src/shell/styles.css +30 -0
  210. package/src/shell/types/billing.ts +8 -0
  211. package/src/shell/types/crud.ts +1 -1
  212. package/src/shell/types/plugins.ts +13 -50
  213. package/src/stores/createCrudStore.ts +9 -0
  214. package/dist/chunk-3M25GRRU.js.map +0 -1
  215. package/dist/chunk-4XTQ4V5L.cjs.map +0 -1
  216. package/dist/chunk-5GYQK5IW.js +0 -21
  217. package/dist/chunk-5GYQK5IW.js.map +0 -1
  218. package/dist/chunk-IPWCHJPE.cjs.map +0 -1
  219. package/dist/chunk-KYID2MR6.cjs +0 -23
  220. package/dist/chunk-KYID2MR6.cjs.map +0 -1
  221. package/dist/chunk-XNWCGJYH.js.map +0 -1
  222. package/dist/shell/components/billing/BillingPage.d.ts +0 -7
  223. package/dist/shell/components/billing/BillingPage.d.ts.map +0 -1
  224. package/dist/shell/components/billing/PaywallGate.d.ts +0 -11
  225. package/dist/shell/components/billing/PaywallGate.d.ts.map +0 -1
  226. package/dist/shell/components/organization/PermissionGate.d.ts +0 -11
  227. package/dist/shell/components/organization/PermissionGate.d.ts.map +0 -1
  228. package/dist/shell/hooks/useBilling.d.ts +0 -14
  229. package/dist/shell/hooks/useBilling.d.ts.map +0 -1
  230. package/dist/shell/hooks/usePermission.d.ts +0 -16
  231. package/dist/shell/hooks/usePermission.d.ts.map +0 -1
  232. package/src/shell/components/billing/BillingPage.tsx +0 -56
  233. package/src/shell/components/billing/PaywallGate.tsx +0 -80
  234. package/src/shell/components/organization/PermissionGate.tsx +0 -16
  235. package/src/shell/hooks/useBilling.ts +0 -151
  236. package/src/shell/hooks/usePermission.ts +0 -101
@@ -0,0 +1,387 @@
1
+ import React, { useCallback, useEffect, useState } from 'react'
2
+ import { MapPin, Star, Pencil, Trash2, Plus } from 'lucide-react'
3
+ import {
4
+ Card, CardContent, Badge, Button, Input, ConfirmDialog,
5
+ Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalFooter,
6
+ Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
7
+ } from '@fayz-ai/ui'
8
+ import { getSupabaseClientOptional, getActiveTenantId, useTranslation } from '@fayz-ai/core'
9
+ import { toast } from '../../shell/components/notifications/ToastProvider'
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Addresses tab — generic on purpose. public.addresses is a CORE table (spine
13
+ // migration @fayz-ai/db 017_core_addresses), so any app with a person record
14
+ // gets this: a client's delivery addresses, a supplier's pickup point, a staff
15
+ // member's home address. Nothing here is shop-specific.
16
+ //
17
+ // Rows are read by owner_id alone, without an owner_type filter, because a
18
+ // person can be reached from two sides today: a core `people` row, or a
19
+ // module's own customer row that mirrors it (the shop's plg_shop_customers
20
+ // keeps the same id). Both resolve to the same address book. Rows WRITTEN here
21
+ // are stamped 'person' — the spine's canonical owner kind.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface AddressRow {
25
+ id: string
26
+ label: string | null
27
+ recipient: string | null
28
+ phone: string | null
29
+ postal_code: string
30
+ street: string
31
+ number: string | null
32
+ complement: string | null
33
+ district: string | null
34
+ city: string
35
+ state: string
36
+ country: string
37
+ is_default: boolean
38
+ kind: string
39
+ }
40
+
41
+ /** The editable shape. Separate from AddressRow: no id, every field a string. */
42
+ interface AddressForm {
43
+ label: string
44
+ kind: string
45
+ recipient: string
46
+ phone: string
47
+ postal_code: string
48
+ street: string
49
+ number: string
50
+ complement: string
51
+ district: string
52
+ city: string
53
+ state: string
54
+ is_default: boolean
55
+ }
56
+
57
+ const EMPTY_FORM: AddressForm = {
58
+ label: '', kind: 'both', recipient: '', phone: '', postal_code: '', street: '',
59
+ number: '', complement: '', district: '', city: '', state: '', is_default: false,
60
+ }
61
+
62
+ function toForm(a: AddressRow): AddressForm {
63
+ return {
64
+ label: a.label ?? '', kind: a.kind, recipient: a.recipient ?? '', phone: a.phone ?? '',
65
+ postal_code: a.postal_code, street: a.street, number: a.number ?? '',
66
+ complement: a.complement ?? '', district: a.district ?? '', city: a.city,
67
+ state: a.state, is_default: a.is_default,
68
+ }
69
+ }
70
+
71
+ const SPAN_CLASS: Record<number, string> = {
72
+ 1: 'sm:col-span-1', 2: 'sm:col-span-2', 3: 'sm:col-span-3',
73
+ 4: 'sm:col-span-4', 6: 'sm:col-span-6',
74
+ }
75
+
76
+ /** Label + control, wired by htmlFor/id so the label is actually clickable and
77
+ * the control is reachable by its accessible name. */
78
+ function Field({ id, label, required, span, children }: {
79
+ id: string
80
+ label: string
81
+ required?: boolean
82
+ span: number
83
+ children: React.ReactNode
84
+ }) {
85
+ return (
86
+ <div className={SPAN_CLASS[span]}>
87
+ <label htmlFor={`address-${id}`} className="text-xs text-muted-foreground">
88
+ {label}{required ? ' *' : ''}
89
+ </label>
90
+ {children}
91
+ </div>
92
+ )
93
+ }
94
+
95
+ function formatLine(a: AddressRow): string {
96
+ // Rua, número — complemento · bairro · cidade/UF · CEP, skipping whatever is
97
+ // missing so a partial address never renders stray commas.
98
+ const streetLine = [a.street, a.number].filter(Boolean).join(', ')
99
+ const withComplement = a.complement ? `${streetLine} — ${a.complement}` : streetLine
100
+ return [withComplement, a.district, `${a.city}/${a.state}`, a.postal_code]
101
+ .filter((part) => part && part !== '—' && part !== '/')
102
+ .join(' · ')
103
+ }
104
+
105
+ export function AddressesTab({ item }: { item?: unknown }) {
106
+ const t = useTranslation()
107
+ const record = item as { id?: string } | undefined
108
+ const [rows, setRows] = useState<AddressRow[]>([])
109
+ const [loading, setLoading] = useState(true)
110
+ const [error, setError] = useState<string | null>(null)
111
+
112
+ const [editing, setEditing] = useState<AddressRow | null>(null)
113
+ const [formOpen, setFormOpen] = useState(false)
114
+ const [form, setForm] = useState<AddressForm>(EMPTY_FORM)
115
+ const [saving, setSaving] = useState(false)
116
+ const [deleting, setDeleting] = useState<AddressRow | null>(null)
117
+
118
+ const ownerId = record?.id
119
+
120
+ // getSupabaseClientOptional is loosely typed at this layer; the query shape is
121
+ // asserted here rather than pulling supabase-js into the UI package.
122
+ const db = () => getSupabaseClientOptional() as unknown as { from: (t: string) => any } | null
123
+
124
+ const load = useCallback(async () => {
125
+ const client = db()
126
+ if (!client || !ownerId) { setLoading(false); return }
127
+ setLoading(true)
128
+ setError(null)
129
+ const { data, error: err } = await client
130
+ .from('addresses')
131
+ .select('*')
132
+ .eq('owner_id', ownerId)
133
+ .order('is_default', { ascending: false })
134
+ if (err) setError(err.message)
135
+ else setRows((data ?? []) as AddressRow[])
136
+ setLoading(false)
137
+ }, [ownerId])
138
+
139
+ useEffect(() => { void load() }, [load])
140
+
141
+ const openCreate = () => { setEditing(null); setForm(EMPTY_FORM); setFormOpen(true) }
142
+ const openEdit = (a: AddressRow) => { setEditing(a); setForm(toForm(a)); setFormOpen(true) }
143
+
144
+ const set = <K extends keyof AddressForm>(key: K, value: AddressForm[K]) =>
145
+ setForm((prev) => ({ ...prev, [key]: value }))
146
+
147
+ // CEP, street and city are what make an address findable; the rest is optional.
148
+ const canSave = form.postal_code.trim() !== '' && form.street.trim() !== '' && form.city.trim() !== ''
149
+
150
+ const handleSave = async () => {
151
+ const client = db()
152
+ if (!client || !ownerId || !canSave) return
153
+ const tenantId = getActiveTenantId()
154
+ if (!tenantId) { toast.error(t('crud.archetype.addresses.noTenant')); return }
155
+
156
+ setSaving(true)
157
+ try {
158
+ const values = {
159
+ label: form.label.trim() || null,
160
+ kind: form.kind,
161
+ recipient: form.recipient.trim() || null,
162
+ phone: form.phone.trim() || null,
163
+ postal_code: form.postal_code.trim(),
164
+ street: form.street.trim(),
165
+ number: form.number.trim() || null,
166
+ complement: form.complement.trim() || null,
167
+ district: form.district.trim() || null,
168
+ city: form.city.trim(),
169
+ state: form.state.trim().toUpperCase(),
170
+ is_default: form.is_default,
171
+ }
172
+
173
+ // addresses_one_default_idx is a UNIQUE partial index on
174
+ // (owner_type, owner_id, kind) WHERE is_default — the previous default
175
+ // must be cleared BEFORE this row claims the flag, or the write fails.
176
+ if (form.is_default) {
177
+ let q = client.from('addresses').update({ is_default: false })
178
+ .eq('owner_id', ownerId).eq('kind', form.kind).eq('is_default', true)
179
+ if (editing) q = q.neq('id', editing.id)
180
+ const { error: clearErr } = await q
181
+ if (clearErr) throw clearErr
182
+ }
183
+
184
+ const { error: err } = editing
185
+ ? await client.from('addresses').update(values).eq('id', editing.id)
186
+ : await client.from('addresses').insert({
187
+ ...values,
188
+ tenant_id: tenantId,
189
+ owner_type: 'person',
190
+ owner_id: ownerId,
191
+ })
192
+ if (err) throw err
193
+
194
+ setFormOpen(false)
195
+ await load()
196
+ toast.success(editing ? t('crud.archetype.addresses.updated') : t('crud.archetype.addresses.created'))
197
+ } catch (e: any) {
198
+ toast.error(t('crud.archetype.addresses.saveFailed'), { description: e?.message })
199
+ } finally {
200
+ setSaving(false)
201
+ }
202
+ }
203
+
204
+ const handleDelete = async () => {
205
+ const client = db()
206
+ if (!client || !deleting) return
207
+ const { error: err } = await client.from('addresses').delete().eq('id', deleting.id)
208
+ setDeleting(null)
209
+ if (err) { toast.error(t('crud.archetype.addresses.deleteFailed'), { description: err.message }); return }
210
+ await load()
211
+ toast.success(t('crud.archetype.addresses.deleted'))
212
+ }
213
+
214
+ if (loading) {
215
+ return (
216
+ <Card><CardContent className="space-y-2 py-6">
217
+ <div className="h-4 w-56 animate-pulse rounded bg-muted/40" />
218
+ <div className="h-4 w-40 animate-pulse rounded bg-muted/30" />
219
+ </CardContent></Card>
220
+ )
221
+ }
222
+
223
+ if (error) {
224
+ return (
225
+ <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
226
+ <p className="font-medium">{t('crud.archetype.addresses.loadFailed')}</p>
227
+ <p className="mt-1 text-red-700">{error}</p>
228
+ </div>
229
+ )
230
+ }
231
+
232
+ return (
233
+ <div className="space-y-3">
234
+ <div className="flex justify-end">
235
+ <Button size="sm" onClick={openCreate}>
236
+ <Plus className="mr-1.5 h-3.5 w-3.5" /> {t('crud.archetype.addresses.add')}
237
+ </Button>
238
+ </div>
239
+
240
+ {rows.length === 0 ? (
241
+ <Card><CardContent className="flex flex-col items-center justify-center py-12 text-center">
242
+ <div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
243
+ <MapPin className="h-5 w-5 text-muted-foreground" />
244
+ </div>
245
+ <p className="text-sm font-medium">{t('crud.archetype.addresses.emptyTitle')}</p>
246
+ <p className="mt-1 max-w-xs text-xs text-muted-foreground">
247
+ {t('crud.archetype.addresses.emptyDescription')}
248
+ </p>
249
+ </CardContent></Card>
250
+ ) : (
251
+ <div className="space-y-2">
252
+ {rows.map((a) => (
253
+ <Card key={a.id}>
254
+ <CardContent className="flex items-start gap-3 py-3">
255
+ <MapPin className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
256
+ <div className="min-w-0 flex-1">
257
+ <div className="flex flex-wrap items-center gap-2">
258
+ <span className="text-sm font-medium">
259
+ {a.label ?? t('crud.archetype.addresses.fallbackLabel')}
260
+ </span>
261
+ {a.is_default && (
262
+ <Badge variant="secondary" className="gap-1 text-[10px]">
263
+ <Star className="h-2.5 w-2.5" /> {t('crud.archetype.addresses.default')}
264
+ </Badge>
265
+ )}
266
+ {a.kind !== 'both' && (
267
+ <Badge variant="secondary" className="text-[10px]">
268
+ {t(`crud.archetype.addresses.kind.${a.kind}`)}
269
+ </Badge>
270
+ )}
271
+ </div>
272
+ <p className="mt-0.5 text-sm text-muted-foreground">{formatLine(a)}</p>
273
+ {(a.recipient || a.phone) && (
274
+ <p className="mt-0.5 text-xs text-muted-foreground">
275
+ {[a.recipient, a.phone].filter(Boolean).join(' · ')}
276
+ </p>
277
+ )}
278
+ </div>
279
+ <div className="flex shrink-0 gap-1">
280
+ {/* Named for the ADDRESS, not just "Editar": the person
281
+ detail header already owns a plain Editar/Excluir pair,
282
+ and two identical accessible names on one screen is a
283
+ coin flip for a screen reader user. */}
284
+ <Button variant="ghost" size="sm" onClick={() => openEdit(a)}
285
+ aria-label={t('crud.archetype.addresses.editTitle')}>
286
+ <Pencil className="h-3.5 w-3.5" />
287
+ </Button>
288
+ <Button variant="ghost" size="sm" onClick={() => setDeleting(a)}
289
+ aria-label={t('crud.archetype.addresses.deleteTitle')}>
290
+ <Trash2 className="h-3.5 w-3.5 text-destructive" />
291
+ </Button>
292
+ </div>
293
+ </CardContent>
294
+ </Card>
295
+ ))}
296
+ </div>
297
+ )}
298
+
299
+ <Modal open={formOpen} onOpenChange={setFormOpen}>
300
+ <ModalContent size="lg">
301
+ <ModalHeader>
302
+ <ModalTitle>
303
+ {editing ? t('crud.archetype.addresses.editTitle') : t('crud.archetype.addresses.addTitle')}
304
+ </ModalTitle>
305
+ <ModalDescription>{t('crud.archetype.addresses.formDescription')}</ModalDescription>
306
+ </ModalHeader>
307
+
308
+ <div className="grid grid-cols-1 gap-3 px-6 sm:grid-cols-6">
309
+ <Field id="label" span={3} label={t('crud.archetype.addresses.field.label')}>
310
+ <Input id="address-label" value={form.label} onChange={(e) => set('label', e.target.value)}
311
+ placeholder={t('crud.archetype.addresses.labelPlaceholder')} />
312
+ </Field>
313
+ <Field id="kind" span={3} label={t('crud.archetype.addresses.field.kind')}>
314
+ <Select value={form.kind} onValueChange={(v: string) => set('kind', v)}>
315
+ <SelectTrigger id="address-kind"><SelectValue /></SelectTrigger>
316
+ <SelectContent>
317
+ <SelectItem value="both">{t('crud.archetype.addresses.kind.both')}</SelectItem>
318
+ <SelectItem value="shipping">{t('crud.archetype.addresses.kind.shipping')}</SelectItem>
319
+ <SelectItem value="billing">{t('crud.archetype.addresses.kind.billing')}</SelectItem>
320
+ </SelectContent>
321
+ </Select>
322
+ </Field>
323
+
324
+ <Field id="postal_code" span={2} required label={t('crud.archetype.addresses.field.postalCode')}>
325
+ <Input id="address-postal_code" value={form.postal_code}
326
+ onChange={(e) => set('postal_code', e.target.value)} placeholder="00000-000" />
327
+ </Field>
328
+ <Field id="street" span={4} required label={t('crud.archetype.addresses.field.street')}>
329
+ <Input id="address-street" value={form.street} onChange={(e) => set('street', e.target.value)} />
330
+ </Field>
331
+
332
+ <Field id="number" span={2} label={t('crud.archetype.addresses.field.number')}>
333
+ <Input id="address-number" value={form.number} onChange={(e) => set('number', e.target.value)} />
334
+ </Field>
335
+ <Field id="complement" span={4} label={t('crud.archetype.addresses.field.complement')}>
336
+ <Input id="address-complement" value={form.complement} onChange={(e) => set('complement', e.target.value)} />
337
+ </Field>
338
+
339
+ <Field id="district" span={3} label={t('crud.archetype.addresses.field.district')}>
340
+ <Input id="address-district" value={form.district} onChange={(e) => set('district', e.target.value)} />
341
+ </Field>
342
+ <Field id="city" span={2} required label={t('crud.archetype.addresses.field.city')}>
343
+ <Input id="address-city" value={form.city} onChange={(e) => set('city', e.target.value)} />
344
+ </Field>
345
+ <Field id="state" span={1} label={t('crud.archetype.addresses.field.state')}>
346
+ <Input id="address-state" value={form.state} onChange={(e) => set('state', e.target.value)}
347
+ maxLength={2} placeholder="UF" />
348
+ </Field>
349
+
350
+ <Field id="recipient" span={3} label={t('crud.archetype.addresses.field.recipient')}>
351
+ <Input id="address-recipient" value={form.recipient} onChange={(e) => set('recipient', e.target.value)} />
352
+ </Field>
353
+ <Field id="phone" span={3} label={t('crud.archetype.addresses.field.phone')}>
354
+ <Input id="address-phone" value={form.phone} onChange={(e) => set('phone', e.target.value)} />
355
+ </Field>
356
+
357
+ <label className="flex items-center gap-2 text-sm sm:col-span-6">
358
+ <input type="checkbox" checked={form.is_default}
359
+ onChange={(e) => set('is_default', e.target.checked)} className="h-4 w-4" />
360
+ {t('crud.archetype.addresses.setDefault')}
361
+ </label>
362
+ </div>
363
+
364
+ <ModalFooter>
365
+ <Button variant="ghost" onClick={() => setFormOpen(false)} disabled={saving}>
366
+ {t('common.cancel')}
367
+ </Button>
368
+ <Button onClick={() => void handleSave()} disabled={!canSave || saving}>
369
+ {saving ? t('common.saving') : t('common.save')}
370
+ </Button>
371
+ </ModalFooter>
372
+ </ModalContent>
373
+ </Modal>
374
+
375
+ <ConfirmDialog
376
+ open={deleting !== null}
377
+ variant="destructive"
378
+ title={t('crud.archetype.addresses.deleteTitle')}
379
+ description={deleting ? formatLine(deleting) : ''}
380
+ confirmLabel={t('common.delete')}
381
+ cancelLabel={t('common.cancel')}
382
+ onConfirm={() => void handleDelete()}
383
+ onCancel={() => setDeleting(null)}
384
+ />
385
+ </div>
386
+ )
387
+ }
@@ -3,6 +3,7 @@ import { Clock, FileText, CalendarDays } from 'lucide-react'
3
3
  import { Card, CardContent } from '@fayz-ai/ui'
4
4
  import { Badge } from '@fayz-ai/ui'
5
5
  import { ScheduleEditor } from './ScheduleEditor'
6
+ import { AddressesTab } from './AddressesTab'
6
7
  import { useTranslation } from '@fayz-ai/core'
7
8
  import { usePluginRuntimeOptional as usePluginsOptional } from '@fayz-ai/core'
8
9
  import { getWidgetsForZone } from '@fayz-ai/core'
@@ -101,6 +102,10 @@ export function ScheduleTab({ item }: { item: any }) {
101
102
 
102
103
  export const PERSON_DETAIL_TABS = [
103
104
  { id: 'schedule', label: 'Schedule', icon: 'CalendarDays', component: ScheduleTab, visibleFor: ['staff'] },
105
+ // Core table (public.addresses), so every app with a person record gets this:
106
+ // delivery addresses for a client, pickup point for a supplier, home address
107
+ // for staff. Not shop-specific.
108
+ { id: 'addresses', label: 'Endereços', icon: 'MapPin', component: AddressesTab },
104
109
  { id: 'documents', label: 'Documents', icon: 'FileText', component: DocumentsTab },
105
110
  // Per-person financial statement — only shown when the financial plugin contributes
106
111
  // its widget (see requiresWidgetZone). Clients = paid invoices; staff = commissions.
@@ -59,6 +59,8 @@ export function createCrudPage<T extends { id: string }>(
59
59
  fields: entityDef.fields,
60
60
  source: 'app',
61
61
  archetype: entityDef.data?.archetype,
62
+ entityDef: entityDef as EntityDef,
63
+ mockData: options?.mockData as Array<{ id: string }> | undefined,
62
64
  })
63
65
 
64
66
  return GeneratedCrudPage
package/src/crud/index.ts CHANGED
@@ -10,3 +10,9 @@ export { ImportWizard } from './ImportWizard'
10
10
  export type { ImportRowError } from './ImportWizard'
11
11
  export { exportToCSV } from './csv-export'
12
12
  export { RelationSelect, RelationCell, useRelationOptions, loadRelationOptions, invalidateRelationOptions } from './relation-field'
13
+ // Link to another entity's detail page — used by detail tabs and by plugins
14
+ // that reference a record from a different module (e.g. the order list linking
15
+ // its customer).
16
+ // PersonLink is intentionally NOT re-exported: plugin-reports already exports
17
+ // a different component under that name and the collision breaks its build.
18
+ export { EntityLink, registerEntityPath, getPersonPath } from './archetypes/EntityLink'
package/src/env.d.ts CHANGED
@@ -8,6 +8,13 @@ interface ImportMetaEnv {
8
8
  /** Deployment-fixed canonical origin (e.g. https://beauty-saas.live.fayz.ai),
9
9
  * injected by the container. Used to build invite/magic-link redirects. */
10
10
  readonly VITE_APP_URL?: string
11
+ /** Fayz project this app belongs to, injected by the container. */
12
+ readonly VITE_FAYZ_PROJECT_ID?: string
13
+ /** Fayz API base (e.g. https://fayz.ai/api), injected by the container. */
14
+ readonly VITE_FAYZ_API_BASE_URL?: string
15
+ /** Publishable key for the project's web agent channel. Present only once an
16
+ * agent is enabled in Fayz — its absence is what leaves the assistant off. */
17
+ readonly VITE_FAYZ_AGENT_KEY?: string
11
18
  }
12
19
 
13
20
  interface ImportMeta {
package/src/index.ts CHANGED
@@ -2,11 +2,13 @@
2
2
  // App factory (main entry point)
3
3
  // ---------------------------------------------------------------------------
4
4
  export { createFayzApp, AdminProviders } from './app/admin-app'
5
- export type { FayzAppConfig, AuthConfig, OrgConfig, ChatConfig, CustomPage, PageSection } from './app/config'
5
+ export type { FayzAppConfig, AuthConfig, OrgConfig, ChatConfig, FayzBillingConfig, CustomPage, PageSection } from './app/config'
6
6
 
7
7
  // Native admin scaffold — manifest-first entry. Importing this registers the
8
8
  // 'admin' scaffold so renderApp(manifest) can resolve it.
9
9
  export { defineSaas, AdminScaffold } from './app/scaffold'
10
+ export { deriveAgentContract } from './app/derive-contract'
11
+ export type { DerivedAgentSections } from './app/derive-contract'
10
12
  export { AdminShell } from './app/AdminShell'
11
13
  export { LoginPage } from './app/LoginPage'
12
14
  export { navigateTo as adminNavigateTo, useAdminPath } from './app/routing'
@@ -40,8 +42,10 @@ export { WidgetSlot } from './plugins/WidgetSlot'
40
42
  export { useFieldRules } from './hooks/useFieldRules'
41
43
 
42
44
  export { createCrudPage } from './crud/createCrudPage'
45
+ export { EntityLink, registerEntityPath, getPersonPath } from './crud/archetypes/EntityLink'
43
46
  // Client-orders archetype (used by beauty-saas client detail tab)
44
47
  export { ClientOrdersTab } from './shell/components/crud/archetypes/ClientOrdersTab'
48
+ export type { ClientOrdersProvider, ClientOrdersQuery, ClientOrdersNavigator, ClientDocument, ClientDocumentStage, ClientOrdersStageFilter } from './shell/types/client-orders'
45
49
  export { createClientOrdersProvider } from './shell/lib/create-client-orders-provider'
46
50
 
47
51
  // ---------------------------------------------------------------------------
@@ -90,15 +94,53 @@ export {
90
94
  usePermissionsStore,
91
95
  PermissionsProvider,
92
96
  usePermission,
97
+ usePermissionOptional,
93
98
  useHasPermission,
94
99
  usePermissions,
95
100
  } from './permissions/index'
96
101
  export type { PermissionsStore, PermissionsProviderProps } from './permissions/index'
97
102
 
103
+ // ---------------------------------------------------------------------------
104
+ // Access engine — role × plan decision, quantity limits, upgrade modal store
105
+ // ---------------------------------------------------------------------------
106
+ export {
107
+ AccessProvider,
108
+ useAccess,
109
+ useAccessOptional,
110
+ useLimit,
111
+ useLimitGuard,
112
+ useModuleNavAccess,
113
+ resolveAccess,
114
+ isEntitledByPlan,
115
+ invalidateLimit,
116
+ useUpgradeModalStore,
117
+ getAccessSnapshot,
118
+ checkAccess,
119
+ guardLimit,
120
+ agentDenial,
121
+ UPGRADE_URL,
122
+ } from './access/index'
123
+ export type {
124
+ AccessProviderProps,
125
+ AccessApi,
126
+ AccessDecision,
127
+ AccessSession,
128
+ DenyReason,
129
+ LimitState,
130
+ UpgradeModalPayload,
131
+ UpgradeModalStore,
132
+ AgentDenial,
133
+ AgentDenialLimit,
134
+ AgentGuardResult,
135
+ } from './access/index'
136
+
98
137
  // ---------------------------------------------------------------------------
99
138
  // Billing
100
139
  // ---------------------------------------------------------------------------
101
140
  export { useBillingStore } from './billing/index'
141
+ export { EntitlementGate, LimitGate } from './shell/components/billing/gates'
142
+ export { UpgradePrompt } from './shell/components/billing/UpgradePrompt'
143
+ export type { UpgradePromptProps } from './shell/components/billing/UpgradePrompt'
102
144
  export type { BillingStore, BillingState, Subscription, Invoice } from './billing/index'
103
145
 
104
146
  // ---------------------------------------------------------------------------
@@ -168,8 +210,14 @@ export { formatCurrency, type CurrencyConfig } from './lib/currency'
168
210
  export { PluginRegistryManager } from './plugins/PluginRegistryManager'
169
211
  export { useModuleNavigation } from './hooks/useModuleNavigation'
170
212
  export { PersonLink } from './components/shared/PersonLink'
213
+ export { ContactPicker } from './components/shared/ContactPicker'
214
+ export type { ContactPickerProps, ContactPickerValue } from './components/shared/ContactPicker'
215
+ export { createPerson } from './components/shared/create-person'
216
+ export type { CreatePersonInput, CreatedPerson } from './components/shared/create-person'
171
217
  export { resolveEntityHref } from './lib/entity-routes'
172
218
  export { usePluginPrefs } from './hooks/usePluginPrefs'
219
+ export { useTenantPluginSettings } from './shell/hooks/useTenantPluginSettings'
220
+ export type { TenantPluginSettings } from './shell/hooks/useTenantPluginSettings'
173
221
  export { setScheduleBlockConfig, getScheduleBlockConfig, subscribeScheduleBlockConfig } from './lib/schedule-config'
174
222
 
175
223
  // Assistant chat store — lets apps drive the shell's chat panel imperatively
package/src/org/store.ts CHANGED
@@ -62,7 +62,15 @@ export const useOrganizationStore = create<OrgStore>((set) => ({
62
62
  setLoading: (loading) => set({ loading }),
63
63
  reset: () => {
64
64
  setActiveTenantId(undefined)
65
- persistOrgId(null)
65
+ // NOTE: do NOT clear the persisted org id here. The org providers call
66
+ // reset() whenever the auth store transiently reports `user === null` —
67
+ // which happens on every fresh load while the Supabase session hydrates
68
+ // asynchronously. Wiping 'fayz:current-org' at that moment drops a
69
+ // multi-org user's workspace selection BEFORE loadOrgs can read it back,
70
+ // so the app always falls to memberships[0] on reload (single-org users
71
+ // never noticed). The pin is a last-selected hint that's re-validated
72
+ // against the fresh membership list on load, so it's safe to keep across
73
+ // sign-out too (a mismatching pin just falls back to memberships[0]).
66
74
  set({ currentOrg: null, userOrgs: [], members: [], loading: false })
67
75
  },
68
76
  }))
@@ -1,24 +1,29 @@
1
1
  import * as React from 'react'
2
2
  import type { FeatureDeclaration, PermissionsConfig, PermissionProfile } from '@fayz-ai/core'
3
+ import { usePluginRuntimeOptional } from '@fayz-ai/core'
3
4
  import { usePermissionsStore } from './store'
4
5
 
5
6
  // ---------------------------------------------------------------------------
6
7
  // Permission check helpers
7
8
  // ---------------------------------------------------------------------------
8
9
 
9
- function profileHasPermission(
10
- profile: PermissionProfile | null,
11
- feature: string,
12
- action?: string,
13
- ): boolean {
14
- if (!profile) return true
10
+ // The role-side check moved to @fayz-ai/core/access (single implementation for
11
+ // browser, headless and the Fayz broker). Re-exported to keep call-sites stable.
12
+ import { profileHasPermission } from '@fayz-ai/core/access'
13
+ export { profileHasPermission }
15
14
 
16
- const actions = profile.grants[feature]
17
- if (!actions) return false
18
-
19
- if (!action) return actions.length > 0
20
-
21
- return actions.includes(action) || actions.includes('manage')
15
+ /**
16
+ * Merge plugin-declared features with the app's own, deduped by id. The app's
17
+ * declaration wins on collision (it can override a plugin's label/actions).
18
+ */
19
+ function mergeFeatures(
20
+ pluginFeatures: FeatureDeclaration[] | undefined,
21
+ appFeatures: FeatureDeclaration[] | undefined,
22
+ ): FeatureDeclaration[] {
23
+ const byId = new Map<string, FeatureDeclaration>()
24
+ for (const f of pluginFeatures ?? []) byId.set(f.id, f)
25
+ for (const f of appFeatures ?? []) byId.set(f.id, f) // app override wins
26
+ return Array.from(byId.values())
22
27
  }
23
28
 
24
29
  // ---------------------------------------------------------------------------
@@ -47,11 +52,20 @@ export function PermissionsProvider({ config, children }: PermissionsProviderPro
47
52
  const { setFeatures, startImpersonation, stopImpersonation, currentProfile, isImpersonating } =
48
53
  usePermissionsStore()
49
54
 
55
+ // Plugins declare their own permission features via `declaredFeatures`, which the
56
+ // plugin runtime aggregates into `pluginFeatures`. Merging them here is what makes
57
+ // a plugin "born compatible" — its features show up in the RBAC matrix and gate
58
+ // checks without the app re-declaring them. The app's own `config.features` wins
59
+ // on id collisions (app override), so an app can refine a plugin's declaration.
60
+ const runtime = usePluginRuntimeOptional()
61
+ const pluginFeatures = runtime?.pluginFeatures
62
+
50
63
  React.useEffect(() => {
51
- if (config?.features) {
52
- setFeatures(config.features)
64
+ const merged = mergeFeatures(pluginFeatures, config?.features)
65
+ if (merged.length > 0) {
66
+ setFeatures(merged)
53
67
  }
54
- }, [config, setFeatures])
68
+ }, [config, pluginFeatures, setFeatures])
55
69
 
56
70
  function hasPermission(feature: string, action?: string): boolean {
57
71
  return profileHasPermission(currentProfile, feature, action)
@@ -1,8 +1,16 @@
1
1
  import React from 'react'
2
2
  import { useAuth } from '../../hooks/useAuth'
3
- import { usePermission } from '../../hooks/usePermission'
3
+ import { useTenantStore } from '../../stores/tenant.store'
4
4
  import type { MemberRole } from '../../types'
5
5
 
6
+ const ROLE_HIERARCHY: Record<MemberRole, number> = {
7
+ owner: 5,
8
+ admin: 4,
9
+ manager: 3,
10
+ staff: 2,
11
+ viewer: 1,
12
+ }
13
+
6
14
  interface ProtectedRouteProps {
7
15
  children?: React.ReactNode
8
16
  requiredRole?: MemberRole
@@ -16,7 +24,9 @@ function LoadingSkeleton() {
16
24
 
17
25
  export function ProtectedRoute({ children, requiredRole, fallback, onUnauthenticated }: ProtectedRouteProps) {
18
26
  const { user, loading, initialized } = useAuth()
19
- const { requireRole } = usePermission()
27
+ const currentRole = useTenantStore((s) => s.currentRole)
28
+ const requireRole = (role: MemberRole): boolean =>
29
+ currentRole ? ROLE_HIERARCHY[currentRole] >= ROLE_HIERARCHY[role] : false
20
30
 
21
31
  React.useEffect(() => {
22
32
  if (initialized && !loading && !user && onUnauthenticated) {