@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
@@ -5,6 +5,7 @@ import { Button } from '@fayz-ai/ui'
5
5
  import { Input } from '@fayz-ai/ui'
6
6
  import { Checkbox } from '@fayz-ai/ui'
7
7
  import { CurrencyInput } from '@fayz-ai/ui'
8
+ import { MarkdownEditor } from '@fayz-ai/ui'
8
9
  import { toast } from '@fayz-ai/ui'
9
10
  import { useSaveBar, useBackHandler } from '@fayz-ai/ui'
10
11
  import { PersonFormLayout } from './archetypes/PersonFormLayout'
@@ -13,9 +14,11 @@ import { ServiceFormLayout } from './archetypes/ServiceFormLayout'
13
14
  import { LocationFormLayout } from './archetypes/LocationFormLayout'
14
15
  import { SubjectFormLayout } from './archetypes/SubjectFormLayout'
15
16
  import { useTranslation } from '@fayz-ai/core'
17
+ import type { EntityImageConfig } from '@fayz-ai/core'
16
18
  import type { FieldDef, FieldGroup, EntityDef } from '@fayz-ai/core'
17
19
  import type { FormLayout } from '@fayz-ai/core'
18
20
  import { RelationSelect } from './relation-field'
21
+ import { useLimitGuard, invalidateLimit } from '../access'
19
22
 
20
23
  interface CrudFormPageProps {
21
24
  entityDef: EntityDef
@@ -134,6 +137,16 @@ function renderField(field: FieldDef, value: any, onChange: (val: any) => void,
134
137
  className={`${baseClass} min-h-[60px] py-1.5`}
135
138
  />
136
139
  )
140
+ case 'markdown':
141
+ return (
142
+ <MarkdownEditor
143
+ value={value ?? ''}
144
+ onChange={onChange}
145
+ placeholder={field.placeholder}
146
+ minRows={12}
147
+ defaultMode="edit"
148
+ />
149
+ )
137
150
  case 'relation':
138
151
  return (
139
152
  <RelationSelect
@@ -237,16 +250,104 @@ function FormFieldItem({ field, value, onChange, allValues }: { field: FieldDef;
237
250
  )
238
251
  }
239
252
 
253
+ /**
254
+ * The image slot, wired. Renders the record's current image and, when the entity
255
+ * supplies an `upload`, lets it be replaced. Upload attaches the file to an
256
+ * existing record id, so in create mode the slot explains itself instead of
257
+ * offering an action that cannot work yet.
258
+ */
259
+ function ImageSlot({
260
+ image,
261
+ row,
262
+ canEdit,
263
+ }: {
264
+ image: EntityImageConfig
265
+ row: Record<string, any>
266
+ canEdit: boolean
267
+ }) {
268
+ const t = useTranslation()
269
+ const inputRef = React.useRef<HTMLInputElement>(null)
270
+ const [url, setUrl] = React.useState<string | undefined>(() => image.get(row))
271
+ const [busy, setBusy] = React.useState(false)
272
+ const [error, setError] = React.useState<string | null>(null)
273
+
274
+ // The form mounts before the record is loaded in edit mode, so the initial
275
+ // value can be undefined and arrive later.
276
+ React.useEffect(() => { setUrl(image.get(row)) }, [image.get(row)])
277
+
278
+ const pick = () => { if (canEdit && !busy) inputRef.current?.click() }
279
+
280
+ const onFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
281
+ const file = e.target.files?.[0]
282
+ e.target.value = ''
283
+ if (!file || !image.upload) return
284
+ setBusy(true)
285
+ setError(null)
286
+ try {
287
+ setUrl(await image.upload(file, row))
288
+ } catch (err: unknown) {
289
+ setError(err instanceof Error ? err.message : String(err))
290
+ } finally {
291
+ setBusy(false)
292
+ }
293
+ }
294
+
295
+ const editable = canEdit && Boolean(image.upload)
296
+
297
+ return (
298
+ <div className="shrink-0">
299
+ <div
300
+ onClick={pick}
301
+ role={editable ? 'button' : undefined}
302
+ tabIndex={editable ? 0 : undefined}
303
+ onKeyDown={(e) => { if (editable && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); pick() } }}
304
+ title={editable ? t('crud.form.changeImage') : undefined}
305
+ className={`relative flex h-20 w-20 items-center justify-center overflow-hidden rounded-lg border-2 text-muted-foreground transition-colors ${
306
+ url ? 'border-solid border-border' : 'border-dashed border-muted'
307
+ } ${editable ? 'cursor-pointer hover:border-primary/30 hover:text-primary/50' : ''}`}
308
+ >
309
+ {url
310
+ ? <img src={url} alt="" className="h-full w-full object-cover" />
311
+ : <ImagePlus className="h-5 w-5" />}
312
+ {busy && (
313
+ <span className="absolute inset-0 flex items-center justify-center bg-background/70 text-[10px]">
314
+ ...
315
+ </span>
316
+ )}
317
+ </div>
318
+ {editable && (
319
+ <input
320
+ ref={inputRef}
321
+ type="file"
322
+ accept={image.accept ?? 'image/*'}
323
+ className="hidden"
324
+ onChange={onFile}
325
+ />
326
+ )}
327
+ {!canEdit && (
328
+ <p className="mt-1 w-20 text-[10px] leading-tight text-muted-foreground">
329
+ {t('crud.form.imageAfterSave')}
330
+ </p>
331
+ )}
332
+ {error && <p className="mt-1 w-20 text-[10px] leading-tight text-destructive">{error}</p>}
333
+ </div>
334
+ )
335
+ }
336
+
240
337
  function FormGroup({
241
338
  group,
242
339
  fields,
243
340
  values,
244
341
  onChange,
342
+ image,
343
+ canEditImage,
245
344
  }: {
246
345
  group: FieldGroup
247
346
  fields: FieldDef[]
248
347
  values: Record<string, any>
249
348
  onChange: (key: string, val: any) => void
349
+ image?: EntityImageConfig
350
+ canEditImage?: boolean
250
351
  }) {
251
352
  const cols = group.columns ?? 2
252
353
 
@@ -274,12 +375,16 @@ function FormGroup({
274
375
  <CardContent className="pt-4">
275
376
  {group.imageSlot ? (
276
377
  <div className="flex gap-4">
277
- {/* Decorative image slot — non-functional placeholder for now. */}
278
- <div className="shrink-0">
279
- <div className="flex h-20 w-20 cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-muted text-muted-foreground transition-colors hover:border-primary/30 hover:text-primary/50">
280
- <ImagePlus className="h-5 w-5" />
281
- </div>
282
- </div>
378
+ {image
379
+ ? <ImageSlot image={image} row={values} canEdit={Boolean(canEditImage)} />
380
+ : (
381
+ // No entity image config: keep the old decorative square.
382
+ <div className="shrink-0">
383
+ <div className="flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-muted text-muted-foreground">
384
+ <ImagePlus className="h-5 w-5" />
385
+ </div>
386
+ </div>
387
+ )}
283
388
  <div className="flex-1">{grid}</div>
284
389
  </div>
285
390
  ) : (
@@ -309,7 +414,16 @@ export function CrudFormPage({ entityDef, mode, initialData, onSubmit, onCancel,
309
414
 
310
415
  const computedKeys = new Set(formFields.filter((f) => f.type === 'computed').map((f) => f.key))
311
416
 
417
+ // Plan quantity guard for CREATE. Hook is called unconditionally (rules of
418
+ // hooks) with a safe empty key when the entity declares none; the guard then
419
+ // resolves to 'ok'. Degrades to allow-all outside <AccessProvider>.
420
+ const limitKey = entityDef.limitKey
421
+ const guardLimit = useLimitGuard(limitKey ?? '')
422
+
312
423
  const submit = async () => {
424
+ // Client-side plan cap: block the save (the guard opens the UpgradeModal)
425
+ // before touching the store. Only CREATE consumes a new slot.
426
+ if (mode === 'create' && limitKey && (await guardLimit(1)) === 'blocked') return
313
427
  setSaving(true)
314
428
  try {
315
429
  // Sanitize: convert empty strings to null so the DB doesn't choke on e.g. empty date fields.
@@ -320,6 +434,8 @@ export function CrudFormPage({ entityDef, mode, initialData, onSubmit, onCancel,
320
434
  sanitized[key] = val === '' ? null : val
321
435
  }
322
436
  await onSubmit(sanitized)
437
+ // Refresh the live count so gates/banners reflect the new row immediately.
438
+ if (mode === 'create' && limitKey) invalidateLimit(limitKey)
323
439
  } catch (err: any) {
324
440
  const message = err?.message || 'Something went wrong'
325
441
  toast.error(t('crud.form.failedToSave', { entity: entityDef.name.toLowerCase() }), { description: message })
@@ -435,6 +551,8 @@ export function CrudFormPage({ entityDef, mode, initialData, onSubmit, onCancel,
435
551
  fields={fields}
436
552
  values={values}
437
553
  onChange={handleChange}
554
+ image={entityDef.image}
555
+ canEditImage={mode === 'edit'}
438
556
  />
439
557
  )
440
558
  })}
@@ -10,6 +10,7 @@ import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem } from '@fayz-
10
10
  import { fieldToColumns } from './fieldToColumn'
11
11
  import { CrudCardGrid } from './CrudCardGrid'
12
12
  import { PermissionGate } from '../permissions/PermissionGate'
13
+ import { LimitGate } from '../shell/components/billing/gates'
13
14
 
14
15
  /** A resolved facet: a field plus the option pills to render. */
15
16
  export interface CrudFacet {
@@ -112,6 +113,20 @@ export function CrudListView<T extends { id: string }>({
112
113
  ? <Button onClick={onNew}>{addLabel ?? t('crud.list.addEntity', { entity: entityDef.name })}</Button>
113
114
  : null
114
115
 
116
+ // Compose the create action's gates: RBAC (PermissionGate, role axis) then the
117
+ // plan quantity cap (LimitGate, plan axis). At the cap, LimitGate dims the
118
+ // button and intercepts the click to open the global UpgradeModal.
119
+ const gatedNewButton = newButton
120
+ ? (() => {
121
+ const roleGated = feature
122
+ ? <PermissionGate feature={feature} action="create">{newButton}</PermissionGate>
123
+ : newButton
124
+ return entityDef.limitKey
125
+ ? <LimitGate limitKey={entityDef.limitKey}>{roleGated}</LimitGate>
126
+ : roleGated
127
+ })()
128
+ : null
129
+
115
130
  return (
116
131
  <div className="space-y-6">
117
132
  <div className="flex items-center justify-between">
@@ -124,7 +139,7 @@ export function CrudListView<T extends { id: string }>({
124
139
  )}
125
140
  </div>
126
141
  <div className="flex items-center gap-2">
127
- {newButton && (feature ? <PermissionGate feature={feature} action="create">{newButton}</PermissionGate> : newButton)}
142
+ {gatedNewButton}
128
143
  {hasImportExport && (
129
144
  <Dropdown>
130
145
  <DropdownTrigger asChild>
@@ -13,6 +13,7 @@ import { ImportWizard } from './ImportWizard'
13
13
  import { exportToCSV } from './csv-export'
14
14
  import { fieldToColumns } from './fieldToColumn'
15
15
  import { PermissionGate } from '../permissions/PermissionGate'
16
+ import { useLimitGuard, invalidateLimit } from '../access'
16
17
  import { useFieldRules } from '../hooks/useFieldRules'
17
18
  import { deriveEntityKey } from '@fayz-ai/core'
18
19
  import { toast } from '@fayz-ai/ui'
@@ -196,6 +197,7 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
196
197
  }, [store])
197
198
  const tanColumns = useCrudColumns(entityDef, { basePath: normalizedBasePath, onDelete: readOnly ? undefined : (item) => setDeleteItem(item), onInlineUpdate: handleInlineUpdate, feature, readOnly })
198
199
  const currentOrgId = useOrganizationStore((s) => s.currentOrg?.id)
200
+ const guardImport = useLimitGuard(entityDef.limitKey ?? '')
199
201
 
200
202
  useEffect(() => {
201
203
  if (currentOrgId) store.fetch()
@@ -216,11 +218,25 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
216
218
  }
217
219
  }
218
220
 
221
+ // Plan quantity guard BEFORE the batch insert: adding `rows.length` rows must
222
+ // stay within the plan cap. When it would exceed, the guard opens the global
223
+ // UpgradeModal; we also surface a clear toast + a wizard error row and abort.
224
+ if (entityDef.limitKey && (await guardImport(rows.length)) === 'blocked') {
225
+ toast.error(t('crud.import.limitExceeded'))
226
+ return {
227
+ success: 0,
228
+ errors: [{ row: 0, message: t('crud.import.limitExceeded') }],
229
+ }
230
+ }
231
+
219
232
  const { success, results } = await store.createMany(rows, {
220
233
  batchSize: 10,
221
234
  onProgress,
222
235
  })
223
236
 
237
+ // Refresh the live count so gates/banners reflect the imported rows.
238
+ if (success > 0 && entityDef.limitKey) invalidateLimit(entityDef.limitKey)
239
+
224
240
  const errors: ImportRowError[] = []
225
241
  for (let i = 0; i < results.length; i++) {
226
242
  if (results[i].error) {
@@ -232,7 +248,7 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
232
248
  toast.success(t('crud.import.toastSuccess', { count: String(success) }))
233
249
  }
234
250
  return { success, errors }
235
- }, [store, t])
251
+ }, [store, t, guardImport, entityDef.limitKey])
236
252
 
237
253
  const handleExport = useCallback(() => {
238
254
  const items = store.items
@@ -460,10 +476,16 @@ export function CrudPage<T extends { id: string }>({ entityDef: rawEntityDef, us
460
476
  )
461
477
  }
462
478
 
463
- const handleDeleteConfirm = () => {
464
- if (deleteItem) {
465
- store.remove((deleteItem as any).id)
466
- setDeleteItem(null)
479
+ const handleDeleteConfirm = async () => {
480
+ if (!deleteItem) return
481
+ const id = (deleteItem as any).id
482
+ setDeleteItem(null)
483
+ // Await the removal so the (cache-invalidated) list refetch completes and the
484
+ // deleted row is gone before we settle on the list view — previously this was
485
+ // fire-and-forget and raced with navigation, leaving a ghost row behind.
486
+ try {
487
+ await store.remove(id)
488
+ } finally {
467
489
  navigateToList()
468
490
  }
469
491
  }
@@ -27,7 +27,7 @@ export function DeleteConfirmDialog({ open, onClose, onConfirm, entityName, disp
27
27
  <ModalTitle>{t('crud.delete.title', { entity: entityName })}</ModalTitle>
28
28
  <ModalDescription>
29
29
  {displayValue
30
- ? t('crud.delete.confirmNamed', { name: displayValue })
30
+ ? t('crud.delete.confirmNamed', { displayValue })
31
31
  : t('crud.delete.confirmGeneric', { entity: entityName.toLowerCase() })}
32
32
  </ModalDescription>
33
33
  </ModalHeader>