@fayz-ai/saas 0.5.0 → 0.6.6

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 (91) hide show
  1. package/dist/app/AdminShell.d.ts +14 -1
  2. package/dist/app/AdminShell.d.ts.map +1 -1
  3. package/dist/app/LoginPage.d.ts +2 -16
  4. package/dist/app/LoginPage.d.ts.map +1 -1
  5. package/dist/app/admin-app.d.ts +21 -0
  6. package/dist/app/admin-app.d.ts.map +1 -0
  7. package/dist/app/config.d.ts +40 -3
  8. package/dist/app/config.d.ts.map +1 -1
  9. package/dist/app/scaffold.d.ts +1 -2
  10. package/dist/app/scaffold.d.ts.map +1 -1
  11. package/dist/{chunk-DJW67TCY.js → chunk-DCRT47W4.js} +155 -28
  12. package/dist/chunk-DCRT47W4.js.map +1 -0
  13. package/dist/{chunk-QGMDVLLW.cjs → chunk-SRIUD35R.cjs} +155 -28
  14. package/dist/chunk-SRIUD35R.cjs.map +1 -0
  15. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  16. package/dist/index.cjs +1127 -5792
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +4 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +839 -5509
  21. package/dist/index.js.map +1 -1
  22. package/dist/org/adapters/supabase.d.ts +18 -0
  23. package/dist/org/adapters/supabase.d.ts.map +1 -1
  24. package/dist/org/context.d.ts.map +1 -1
  25. package/dist/org/index.cjs +11 -10
  26. package/dist/org/index.js +2 -1
  27. package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts +4 -2
  28. package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts.map +1 -1
  29. package/dist/shell/components/layout/BottomNav.d.ts.map +1 -1
  30. package/dist/shell/components/layout/CommandPalette.d.ts.map +1 -1
  31. package/dist/shell/components/organization/InviteMemberDialog.d.ts.map +1 -1
  32. package/dist/shell/components/organization/PermissionMatrixEditor.d.ts +6 -1
  33. package/dist/shell/components/organization/PermissionMatrixEditor.d.ts.map +1 -1
  34. package/dist/shell/components/organization/PermissionProfilesTab.d.ts.map +1 -1
  35. package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
  36. package/dist/shell/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
  37. package/dist/shell/components/settings/ConnectedUserProfile.d.ts +2 -1
  38. package/dist/shell/components/settings/ConnectedUserProfile.d.ts.map +1 -1
  39. package/dist/shell/components/settings/SettingsPage.d.ts +8 -1
  40. package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
  41. package/dist/shell/config/theme/tokens.d.ts +12 -1
  42. package/dist/shell/config/theme/tokens.d.ts.map +1 -1
  43. package/dist/shell/config/theme/utils.d.ts +8 -0
  44. package/dist/shell/config/theme/utils.d.ts.map +1 -1
  45. package/dist/shell/lib/i18n.d.ts.map +1 -1
  46. package/dist/shell/types/client-orders.d.ts +4 -0
  47. package/dist/shell/types/client-orders.d.ts.map +1 -1
  48. package/dist/shell/types/crud.d.ts +4 -0
  49. package/dist/shell/types/crud.d.ts.map +1 -1
  50. package/package.json +7 -9
  51. package/src/app/AdminShell.tsx +103 -35
  52. package/src/app/LoginPage.tsx +2 -264
  53. package/src/app/{createFayzApp.tsx → admin-app.tsx} +98 -40
  54. package/src/app/config.ts +44 -2
  55. package/src/app/scaffold.tsx +13 -26
  56. package/src/crud/CrudDetailPage.tsx +43 -9
  57. package/src/crud/CrudPage.tsx +1 -1
  58. package/src/index.ts +12 -5
  59. package/src/org/adapters/supabase.ts +176 -28
  60. package/src/org/context.tsx +33 -2
  61. package/src/shell/components/crud/archetypes/ClientOrdersTab.tsx +12 -4
  62. package/src/shell/components/layout/BottomNav.tsx +39 -4
  63. package/src/shell/components/layout/CommandPalette.tsx +14 -10
  64. package/src/shell/components/organization/InviteMemberDialog.tsx +14 -3
  65. package/src/shell/components/organization/PermissionMatrixEditor.tsx +48 -10
  66. package/src/shell/components/organization/PermissionProfilesTab.tsx +65 -23
  67. package/src/shell/components/organization/TeamTab.tsx +36 -9
  68. package/src/shell/components/plugins/PluginSettingsPanel.tsx +5 -3
  69. package/src/shell/components/settings/ConnectedUserProfile.tsx +68 -25
  70. package/src/shell/components/settings/SettingsPage.tsx +12 -3
  71. package/src/shell/config/theme/tokens.ts +13 -1
  72. package/src/shell/config/theme/utils.ts +76 -15
  73. package/src/shell/lib/i18n.ts +4 -0
  74. package/src/shell/types/client-orders.ts +5 -0
  75. package/src/shell/types/crud.ts +4 -0
  76. package/dist/app/createFayzApp.d.ts +0 -11
  77. package/dist/app/createFayzApp.d.ts.map +0 -1
  78. package/dist/chunk-DJW67TCY.js.map +0 -1
  79. package/dist/chunk-QGMDVLLW.cjs.map +0 -1
  80. package/dist/shell/components/organization/ImpersonationBanner.d.ts +0 -3
  81. package/dist/shell/components/organization/ImpersonationBanner.d.ts.map +0 -1
  82. package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts +0 -3
  83. package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts.map +0 -1
  84. package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts +0 -3
  85. package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts.map +0 -1
  86. package/dist/shell/createSaasApp.d.ts +0 -145
  87. package/dist/shell/createSaasApp.d.ts.map +0 -1
  88. package/src/shell/components/organization/ImpersonationBanner.tsx +0 -29
  89. package/src/shell/components/settings/ConnectedHolidaysSettings.tsx +0 -99
  90. package/src/shell/components/settings/ConnectedLocationsOverview.tsx +0 -57
  91. package/src/shell/createSaasApp.tsx +0 -943
@@ -7,7 +7,7 @@ import type {
7
7
  Location,
8
8
  Invite,
9
9
  } from '@fayz-ai/core'
10
- import type { PermissionProfile } from '@fayz-ai/core'
10
+ import type { PermissionProfile, SystemPermission, AuthAdapter } from '@fayz-ai/core'
11
11
  import { getFayzSupabaseClient, CORE_SCHEMA } from '../../supabase/client'
12
12
 
13
13
  // ---------------------------------------------------------------------------
@@ -67,7 +67,8 @@ function mapMemberRow(row: Record<string, unknown>): OrgMember {
67
67
  fullName: (profile['full_name'] as string) ?? '',
68
68
  avatarUrl: (profile['avatar_url'] as string | null) ?? undefined,
69
69
  },
70
- joinedAt: row['joined_at'] as string,
70
+ // tenant_members has no `joined_at` column — it's `created_at`.
71
+ joinedAt: (row['joined_at'] ?? row['created_at']) as string,
71
72
  }
72
73
  }
73
74
 
@@ -103,12 +104,54 @@ function mapInviteRow(row: Record<string, unknown>): Invite {
103
104
  }
104
105
  }
105
106
 
106
- const SYSTEM_ROLES = ['owner', 'admin', 'manager', 'staff', 'viewer'] as const
107
+ /** Minimal role identity the adapter needs to build profiles. Grants come from
108
+ * the DB catalog; id/name/order/description come from what the app declares. */
109
+ export interface RoleIdentity {
110
+ id: string
111
+ name: string
112
+ description?: string
113
+ /** System roles are app-defined and static (not user-editable). Custom roles
114
+ * (duplicated by users, stored in tenant_roles) are false. Defaults true. */
115
+ isSystem?: boolean
116
+ }
117
+
118
+ // Matrix emits read/create/edit/delete; map back to DB action verbs.
119
+ const DB_ACTION: Record<string, string> = {
120
+ read: 'read', create: 'create', edit: 'update', write: 'create', manage: 'manage', delete: 'delete',
121
+ }
107
122
 
108
- type SystemRole = (typeof SYSTEM_ROLES)[number]
123
+ function slugifyRole(name: string): string {
124
+ return name.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
125
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'role'
126
+ }
127
+
128
+ // Fallback role set when the host app declares none. The SDK is domain-agnostic:
129
+ // each app defines its OWN business roles (a salon's roles ≠ an agency's) via
130
+ // config.permissions.defaultProfiles. These five are only a back-compat default.
131
+ const DEFAULT_ROLES: RoleIdentity[] = [
132
+ { id: 'owner', name: 'Owner' },
133
+ { id: 'admin', name: 'Admin' },
134
+ { id: 'manager', name: 'Manager' },
135
+ { id: 'staff', name: 'Staff' },
136
+ { id: 'viewer', name: 'Viewer' },
137
+ ]
138
+
139
+ // DB permission ids that confer an org-management system permission.
140
+ const SYSTEM_PERMISSION_MAP: Record<string, SystemPermission> = {
141
+ 'team.read': 'manage_team',
142
+ 'team.invite': 'manage_team',
143
+ 'team.manage': 'manage_team',
144
+ 'team.manage_roles': 'manage_permissions',
145
+ 'billing.read': 'manage_billing',
146
+ 'billing.manage': 'manage_billing',
147
+ 'settings.read': 'manage_settings',
148
+ 'settings.update': 'manage_settings',
149
+ }
150
+ const ALL_SYSTEM_PERMISSIONS: SystemPermission[] = ['manage_team', 'manage_billing', 'manage_settings', 'manage_permissions']
109
151
 
110
152
  // Map DB permission ids to feature action grants
111
153
  function buildPermissionProfiles(
154
+ roles: RoleIdentity[],
112
155
  permissions: Array<{ id: string; category: string }>,
113
156
  rolePerms: Array<{ role: string; permission_id: string }>,
114
157
  overrides: Array<{ role: string; permission_id: string; granted: boolean }>,
@@ -118,22 +161,23 @@ function buildPermissionProfiles(
118
161
  overrideMap.set(`${o.role}:${o.permission_id}`, o.granted)
119
162
  }
120
163
 
121
- const rolePermMap = new Map<SystemRole, Set<string>>()
122
- for (const role of SYSTEM_ROLES) {
123
- rolePermMap.set(role, new Set())
164
+ // The role SET comes from what the app declares — the SDK no longer bakes it in.
165
+ const rolePermMap = new Map<string, Set<string>>()
166
+ for (const r of roles) {
167
+ rolePermMap.set(r.id, new Set())
124
168
  }
125
- // Owner gets all permissions by default
126
- for (const p of permissions) {
127
- rolePermMap.get('owner')!.add(p.id)
169
+ // Convention: an 'owner' role (if declared) gets every permission implicitly.
170
+ if (rolePermMap.has('owner')) {
171
+ for (const p of permissions) rolePermMap.get('owner')!.add(p.id)
128
172
  }
129
173
  for (const rp of rolePerms) {
130
- rolePermMap.get(rp.role as SystemRole)?.add(rp.permission_id)
174
+ rolePermMap.get(rp.role)?.add(rp.permission_id)
131
175
  }
132
176
 
133
177
  // Apply overrides
134
178
  for (const [key, granted] of overrideMap) {
135
179
  const colonIdx = key.indexOf(':')
136
- const role = key.slice(0, colonIdx) as SystemRole
180
+ const role = key.slice(0, colonIdx)
137
181
  const permId = key.slice(colonIdx + 1)
138
182
  const perms = rolePermMap.get(role)
139
183
  if (!perms) continue
@@ -144,17 +188,18 @@ function buildPermissionProfiles(
144
188
  }
145
189
  }
146
190
 
191
+ // Align DB actions with the matrix's action vocabulary (read/create/edit/delete).
147
192
  const actionMap: Record<string, string> = {
148
193
  read: 'read',
149
- create: 'write',
150
- update: 'write',
151
- manage: 'manage',
194
+ create: 'create',
195
+ update: 'edit',
196
+ manage: 'edit',
152
197
  delete: 'delete',
153
- configure: 'manage',
154
- invite: 'write',
198
+ configure: 'edit',
199
+ invite: 'create',
155
200
  }
156
201
 
157
- return SYSTEM_ROLES.map((role) => {
202
+ return roles.map(({ id: role, name, description, isSystem }) => {
158
203
  const permIds = rolePermMap.get(role)!
159
204
  const features: Record<string, string[]> = {}
160
205
 
@@ -172,10 +217,20 @@ function buildPermissionProfiles(
172
217
  }
173
218
  }
174
219
 
220
+ // Org-management system permissions, derived from team/billing/settings
221
+ // grants (owner gets all four). These gate the Equipe/Permissões tabs.
222
+ const systemPermissions: SystemPermission[] = []
223
+ const source = role === 'owner' ? ALL_SYSTEM_PERMISSIONS : [...permIds].map((id) => SYSTEM_PERMISSION_MAP[id])
224
+ for (const sp of source) {
225
+ if (sp && !systemPermissions.includes(sp)) systemPermissions.push(sp)
226
+ }
227
+
175
228
  return {
176
229
  id: role,
177
- name: capitalize(role),
178
- isSystem: true,
230
+ name: name || capitalize(role),
231
+ description,
232
+ isSystem: isSystem ?? true,
233
+ systemPermissions,
179
234
  grants: features,
180
235
  }
181
236
  })
@@ -188,10 +243,20 @@ function buildPermissionProfiles(
188
243
  export interface SupabaseOrgAdapterConfig {
189
244
  /** Override the core schema name (default: 'saas_core') */
190
245
  coreSchema?: string
246
+ /** Business roles the host app declares (config.permissions.defaultProfiles).
247
+ * Falls back to the generic five when omitted. */
248
+ roles?: RoleIdentity[]
249
+ /** Auth adapter used to actually DELIVER invites (send the e-mail + create the
250
+ * auth user) — the org adapter only records the audit row. Injected by the app;
251
+ * when omitted, invites are recorded but not delivered. */
252
+ authAdapter?: AuthAdapter
191
253
  }
192
254
 
193
255
  export function createSupabaseOrgAdapter(config?: SupabaseOrgAdapterConfig): OrgAdapter {
194
256
  const schema = config?.coreSchema ?? CORE_SCHEMA
257
+ const roles: RoleIdentity[] = config?.roles && config.roles.length > 0 ? config.roles : DEFAULT_ROLES
258
+ const authAdapter = config?.authAdapter
259
+ const inviteRedirectTo = typeof window !== 'undefined' ? `${window.location.origin}/` : undefined
195
260
 
196
261
  function supabase() {
197
262
  return getFayzSupabaseClient()
@@ -318,17 +383,31 @@ export function createSupabaseOrgAdapter(config?: SupabaseOrgAdapterConfig): Org
318
383
  },
319
384
 
320
385
  async listProfiles(orgId: string): Promise<PermissionProfile[]> {
321
- const [permsResult, rolePermsResult, overridesResult] = await Promise.all([
386
+ const [permsResult, rolePermsResult, overridesResult, customResult] = await Promise.all([
322
387
  core().from('permissions').select('id, category'),
323
388
  core().from('role_permissions').select('role, permission_id'),
324
389
  core().from('tenant_role_overrides').select('role, permission_id, granted').eq('tenant_id', orgId),
390
+ // Custom roles the tenant created by duplicating a system role. Tolerate a
391
+ // missing table (pre-seed) — just show the system roles in that case.
392
+ core().from('tenant_roles').select('key, name, description').eq('tenant_id', orgId),
325
393
  ])
326
394
 
327
395
  if (permsResult.error) throw permsResult.error
328
396
  if (rolePermsResult.error) throw rolePermsResult.error
329
397
  if (overridesResult.error) throw overridesResult.error
330
398
 
399
+ const systemRoles: RoleIdentity[] = roles.map((r) => ({ ...r, isSystem: true }))
400
+ const customRoles: RoleIdentity[] = customResult.error
401
+ ? []
402
+ : ((customResult.data ?? []) as Array<{ key: string; name: string; description: string | null }>).map((r) => ({
403
+ id: r.key,
404
+ name: r.name,
405
+ description: r.description ?? undefined,
406
+ isSystem: false,
407
+ }))
408
+
331
409
  return buildPermissionProfiles(
410
+ [...systemRoles, ...customRoles],
332
411
  (permsResult.data ?? []) as Array<{ id: string; category: string }>,
333
412
  (rolePermsResult.data ?? []) as Array<{ role: string; permission_id: string }>,
334
413
  (overridesResult.data ?? []) as Array<{ role: string; permission_id: string; granted: boolean }>,
@@ -336,12 +415,45 @@ export function createSupabaseOrgAdapter(config?: SupabaseOrgAdapterConfig): Org
336
415
  },
337
416
 
338
417
  async createProfile(
339
- _orgId: string,
340
- _profile: Omit<PermissionProfile, 'id' | 'isSystem'>,
418
+ orgId: string,
419
+ profile: Omit<PermissionProfile, 'id' | 'isSystem'>,
341
420
  ): Promise<PermissionProfile> {
342
- throw new Error(
343
- 'Custom roles are not supported. Roles are system-defined (owner, admin, manager, staff, viewer).',
421
+ // Custom roles live in tenant_roles (identity/name) + tenant_role_overrides
422
+ // (their grants). System roles stay app-defined and untouched.
423
+ const existing = new Set(
424
+ ((await core().from('tenant_roles').select('key').eq('tenant_id', orgId)).data ?? [])
425
+ .map((r: { key: string }) => r.key),
344
426
  )
427
+ let key = slugifyRole(profile.name)
428
+ if (existing.has(key) || roles.some((r) => r.id === key)) {
429
+ let n = 2
430
+ while (existing.has(`${key}-${n}`) || roles.some((r) => r.id === `${key}-${n}`)) n++
431
+ key = `${key}-${n}`
432
+ }
433
+
434
+ const insertRole = await core()
435
+ .from('tenant_roles')
436
+ .insert({ tenant_id: orgId, key, name: profile.name, description: profile.description ?? null })
437
+ if (insertRole.error) throw insertRole.error
438
+
439
+ const overrides = Object.entries(profile.grants ?? {}).flatMap(([category, actions]) =>
440
+ actions.map((action) => ({
441
+ tenant_id: orgId,
442
+ role: key,
443
+ permission_id: `${category}.${DB_ACTION[action] ?? action}`,
444
+ granted: true,
445
+ })),
446
+ )
447
+ if (overrides.length > 0) {
448
+ const { error } = await core().from('tenant_role_overrides').insert(overrides)
449
+ if (error) throw error
450
+ }
451
+
452
+ const profiles = await this.listProfiles(orgId)
453
+ return profiles.find((p) => p.id === key) ?? {
454
+ id: key, name: profile.name, description: profile.description, isSystem: false,
455
+ systemPermissions: profile.systemPermissions ?? [], grants: profile.grants ?? {},
456
+ }
345
457
  },
346
458
 
347
459
  async updateProfile(
@@ -365,8 +477,11 @@ export function createSupabaseOrgAdapter(config?: SupabaseOrgAdapterConfig): Org
365
477
 
366
478
  for (const [category, actions] of Object.entries(data.grants)) {
367
479
  for (const action of actions) {
480
+ // Matrix emits read/create/edit/delete; map back to DB action verbs.
368
481
  const dbActionMap: Record<string, string> = {
369
482
  read: 'read',
483
+ create: 'create',
484
+ edit: 'update',
370
485
  write: 'create',
371
486
  manage: 'manage',
372
487
  delete: 'delete',
@@ -393,8 +508,21 @@ export function createSupabaseOrgAdapter(config?: SupabaseOrgAdapterConfig): Org
393
508
  return updated
394
509
  },
395
510
 
396
- async deleteProfile(_orgId: string, _profileId: string): Promise<void> {
397
- throw new Error('System roles cannot be deleted.')
511
+ async deleteProfile(orgId: string, profileId: string): Promise<void> {
512
+ // Only custom roles (present in tenant_roles) can be deleted; system roles
513
+ // are app-defined and static.
514
+ const found = await core()
515
+ .from('tenant_roles')
516
+ .select('key')
517
+ .eq('tenant_id', orgId)
518
+ .eq('key', profileId)
519
+ .maybeSingle()
520
+ if (found.error) throw found.error
521
+ if (!found.data) throw new Error('System roles cannot be deleted.')
522
+
523
+ await core().from('tenant_role_overrides').delete().eq('tenant_id', orgId).eq('role', profileId)
524
+ const { error } = await core().from('tenant_roles').delete().eq('tenant_id', orgId).eq('key', profileId)
525
+ if (error) throw error
398
526
  },
399
527
 
400
528
  async listInvites(orgId: string): Promise<Invite[]> {
@@ -421,6 +549,12 @@ export function createSupabaseOrgAdapter(config?: SupabaseOrgAdapterConfig): Org
421
549
  .single()
422
550
 
423
551
  if (error) throw error
552
+ // Deliver via the auth abstraction (sends the e-mail + creates the auth user,
553
+ // stamping tenant_id/role on its metadata for the accept-time membership trigger).
554
+ await authAdapter?.inviteUser?.(email, {
555
+ redirectTo: inviteRedirectTo,
556
+ data: { tenant_id: orgId, role: profileId, invited_by: invitedBy },
557
+ })
424
558
  return mapInviteRow(data as Record<string, unknown>)
425
559
  },
426
560
 
@@ -434,6 +568,14 @@ export function createSupabaseOrgAdapter(config?: SupabaseOrgAdapterConfig): Org
434
568
 
435
569
  const { data, error } = await core().from('invitations').insert(rows).select()
436
570
  if (error) throw error
571
+ await Promise.all(
572
+ emails.map((email) =>
573
+ authAdapter?.inviteUser?.(email, {
574
+ redirectTo: inviteRedirectTo,
575
+ data: { tenant_id: orgId, role: profileId, invited_by: invitedBy },
576
+ }),
577
+ ),
578
+ )
437
579
  return ((data ?? []) as Array<Record<string, unknown>>).map(mapInviteRow)
438
580
  },
439
581
 
@@ -460,7 +602,13 @@ export function createSupabaseOrgAdapter(config?: SupabaseOrgAdapterConfig): Org
460
602
  .single()
461
603
 
462
604
  if (error) throw error
463
- return mapInviteRow(data as Record<string, unknown>)
605
+ const row = data as Record<string, unknown>
606
+ // Re-send the invite e-mail through the auth abstraction.
607
+ await authAdapter?.inviteUser?.(row.email as string, {
608
+ redirectTo: inviteRedirectTo,
609
+ data: { tenant_id: orgId, role: row.role as string, invited_by: row.invited_by as string },
610
+ })
611
+ return mapInviteRow(row)
464
612
  },
465
613
 
466
614
  async listLocations(orgId: string): Promise<Location[]> {
@@ -2,6 +2,7 @@ import * as React from 'react'
2
2
  import type { OrgAdapter, Organization, OrgMembership } from '@fayz-ai/core'
3
3
  import { useAuthStore } from '@fayz-ai/auth'
4
4
  import { useOrganizationStore, getPersistedOrgId } from './store'
5
+ import { usePermissionsStore } from '../permissions'
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
8
  // Adapter context
@@ -68,12 +69,36 @@ export interface OrgProviderProps {
68
69
 
69
70
  export function OrgProvider({ adapter, autoCreate = false, children }: OrgProviderProps) {
70
71
  const { user } = useAuthStore()
71
- const { currentOrg, userOrgs, loading, setCurrentOrg, setUserOrgs, setLoading, reset } =
72
+ const { currentOrg, userOrgs, loading, setCurrentOrg, setUserOrgs, setMembers, setLoading, reset } =
72
73
  useOrganizationStore()
74
+ const setProfiles = usePermissionsStore((s) => s.setProfiles)
75
+ const setCurrentProfile = usePermissionsStore((s) => s.setCurrentProfile)
73
76
 
74
77
  const adapterRef = React.useRef(adapter)
75
78
  adapterRef.current = adapter
76
79
 
80
+ // Load the org's members + permission profiles into the shared stores whenever
81
+ // an org resolves. This is what makes the role list, the invite dialog's role
82
+ // dropdown, and permission checks work — previously nothing loaded them here,
83
+ // so those surfaces came up empty. Also resolves the signed-in user's own role.
84
+ const hydrateOrg = React.useCallback(
85
+ async (orgId: string, uid: string | undefined) => {
86
+ try {
87
+ const [members, profiles] = await Promise.all([
88
+ adapterRef.current.listMembers(orgId),
89
+ adapterRef.current.listProfiles(orgId),
90
+ ])
91
+ setMembers(members)
92
+ setProfiles(profiles)
93
+ const myMembership = uid ? members.find((m) => m.userId === uid) : undefined
94
+ setCurrentProfile(profiles.find((p) => p.id === myMembership?.profileId) ?? null)
95
+ } catch {
96
+ // Non-fatal — leave stores as-is if the load fails.
97
+ }
98
+ },
99
+ [setMembers, setProfiles, setCurrentProfile],
100
+ )
101
+
77
102
  // Load orgs when user is authenticated
78
103
  React.useEffect(() => {
79
104
  if (!user) {
@@ -109,6 +134,7 @@ export function OrgProvider({ adapter, autoCreate = false, children }: OrgProvid
109
134
  },
110
135
  ])
111
136
  setCurrentOrg(newOrg)
137
+ await hydrateOrg(newOrg.id, user!.id)
112
138
  }
113
139
  return
114
140
  }
@@ -120,7 +146,10 @@ export function OrgProvider({ adapter, autoCreate = false, children }: OrgProvid
120
146
 
121
147
  if (target && !cancelled) {
122
148
  const org = await adapterRef.current.getOrg(target.orgId)
123
- if (!cancelled) setCurrentOrg(org)
149
+ if (!cancelled) {
150
+ setCurrentOrg(org)
151
+ await hydrateOrg(org.id, user!.id)
152
+ }
124
153
  }
125
154
  } finally {
126
155
  if (!cancelled) setLoading(false)
@@ -140,6 +169,7 @@ export function OrgProvider({ adapter, autoCreate = false, children }: OrgProvid
140
169
  try {
141
170
  const org = await adapterRef.current.getOrg(orgId)
142
171
  setCurrentOrg(org)
172
+ await hydrateOrg(org.id, user?.id)
143
173
  } finally {
144
174
  setLoading(false)
145
175
  }
@@ -151,6 +181,7 @@ export function OrgProvider({ adapter, autoCreate = false, children }: OrgProvid
151
181
  try {
152
182
  const org = await adapterRef.current.getOrg(currentOrg.id)
153
183
  setCurrentOrg(org)
184
+ await hydrateOrg(org.id, user?.id)
154
185
  } finally {
155
186
  setLoading(false)
156
187
  }
@@ -7,7 +7,7 @@ import type { ColumnDef } from '@tanstack/react-table'
7
7
  import { ListView } from '@fayz-ai/ui'
8
8
  import { useTranslation } from '../../../hooks/useTranslation'
9
9
  import type { EntityDef } from '../../../types/crud'
10
- import type { ClientDocument, ClientDocumentStage, ClientOrdersProvider, ClientOrdersNavigator } from '../../../types/client-orders'
10
+ import type { ClientDocument, ClientDocumentStage, ClientOrdersProvider, ClientOrdersNavigator, ClientOrdersStageFilter } from '../../../types/client-orders'
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Stage badge config
@@ -146,6 +146,8 @@ export function ClientOrdersTab({
146
146
  entityDef,
147
147
  provider,
148
148
  navigator,
149
+ initialStage,
150
+ stageFilters,
149
151
  onBookingClick,
150
152
  onInvoiceClick,
151
153
  currency = { code: 'BRL', locale: 'pt-BR' },
@@ -154,6 +156,8 @@ export function ClientOrdersTab({
154
156
  entityDef: EntityDef
155
157
  provider: ClientOrdersProvider
156
158
  navigator?: ClientOrdersNavigator
159
+ initialStage?: string
160
+ stageFilters?: ClientOrdersStageFilter[]
157
161
  onBookingClick?: (orderId: string) => void
158
162
  onInvoiceClick?: (orderId: string) => void
159
163
  currency?: { code: string; locale: string }
@@ -162,7 +166,7 @@ export function ClientOrdersTab({
162
166
  const [docs, setDocs] = useState<ClientDocument[]>([])
163
167
  const [total, setTotal] = useState(0)
164
168
  const [loading, setLoading] = useState(true)
165
- const [stageFilter, setStageFilter] = useState<string | undefined>()
169
+ const [stageFilter, setStageFilter] = useState<string | undefined>(initialStage)
166
170
 
167
171
  const clientId = item.id as string
168
172
 
@@ -185,14 +189,18 @@ export function ClientOrdersTab({
185
189
 
186
190
  useEffect(() => { fetchDocs() }, [fetchDocs])
187
191
 
192
+ useEffect(() => {
193
+ setStageFilter(initialStage)
194
+ }, [initialStage])
195
+
188
196
  const columns = useColumns(currency, t, onBookingClick, onInvoiceClick)
189
197
 
190
- const tags = useMemo(() => [
198
+ const tags = useMemo(() => stageFilters ?? [
191
199
  { value: 'booked', label: t('crud.orders.stage.booked') },
192
200
  { value: 'invoiced', label: t('crud.orders.stage.invoiced') },
193
201
  { value: 'paid', label: t('crud.orders.stage.paid') },
194
202
  { value: 'quoted', label: t('crud.orders.stage.quoted') },
195
- ], [t])
203
+ ], [stageFilters, t])
196
204
 
197
205
  return (
198
206
  <ListView<ClientDocument>
@@ -6,26 +6,53 @@ import {
6
6
  CreditCard,
7
7
  Bell,
8
8
  Calendar,
9
+ CalendarClock,
10
+ CalendarCheck2,
11
+ CalendarX,
9
12
  Package,
13
+ Activity,
10
14
  BarChart3,
11
15
  FileText,
16
+ FileCheck2,
12
17
  Mail,
13
18
  MessageCircle,
14
19
  DollarSign,
20
+ BadgeDollarSign,
21
+ CircleDollarSign,
15
22
  Megaphone,
16
23
  ShoppingCart,
24
+ ShoppingBag,
17
25
  Target,
18
26
  Wrench,
19
27
  ClipboardList,
20
28
  Briefcase,
21
29
  UserCog,
22
30
  BookOpen,
31
+ BookOpenCheck,
23
32
  Globe,
24
33
  Percent,
25
34
  Tag,
35
+ Tags,
26
36
  Camera,
27
37
  UtensilsCrossed,
38
+ MapPin,
39
+ Handshake,
40
+ Contact,
41
+ Building2,
28
42
  Search,
43
+ Shield,
44
+ Plus,
45
+ List,
46
+ ListPlus,
47
+ ListChecks,
48
+ FolderOpen,
49
+ Boxes,
50
+ Landmark,
51
+ Receipt,
52
+ Map,
53
+ Ban,
54
+ Clock,
55
+ Clock3,
29
56
  PartyPopper,
30
57
  Radio,
31
58
  CalendarDays,
@@ -37,6 +64,13 @@ import {
37
64
  Disc3,
38
65
  UsersRound,
39
66
  Mic,
67
+ Inbox,
68
+ Star,
69
+ TrendingUp,
70
+ UserCheck,
71
+ UserPlus,
72
+ UserX,
73
+ Zap,
40
74
  type LucideIcon,
41
75
  } from 'lucide-react'
42
76
  import { cn } from '../../lib/cn'
@@ -62,11 +96,12 @@ interface BottomNavProps {
62
96
  }
63
97
 
64
98
  const ICON_MAP: Record<string, LucideIcon> = {
65
- Home, Users, Settings, CreditCard, Bell, Calendar, Package, BarChart3,
66
- FileText, Mail, MessageCircle, DollarSign, Megaphone, ShoppingCart,
99
+ Home, Users, Settings, CreditCard, Bell, Calendar, CalendarClock, Package, Activity, BarChart3,
100
+ CalendarCheck2, CalendarX, FileText, FileCheck2, Mail, MessageCircle, DollarSign, BadgeDollarSign, CircleDollarSign, Megaphone, ShoppingCart, ShoppingBag,
67
101
  Target, Wrench, ClipboardList, Briefcase, UserCog, BookOpen, Globe,
68
- Percent, Tag, Camera, UtensilsCrossed, Search,
69
- PartyPopper, Radio, CalendarDays, Banknote, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
102
+ BookOpenCheck, Percent, Tag, Tags, Camera, UtensilsCrossed, MapPin, Handshake, Contact, Building2, Search,
103
+ Shield, Plus, List, ListPlus, ListChecks, FolderOpen, Boxes, Landmark, Receipt, Map, Ban, Clock, Clock3, Inbox, PartyPopper, Radio, CalendarDays, Banknote, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
104
+ Star, TrendingUp, UserCheck, UserPlus, UserX, Zap,
70
105
  }
71
106
 
72
107
  export function BottomNav({ navigation, activeRoute, onNavigate, showChat = true, customItems }: BottomNavProps) {
@@ -3,11 +3,14 @@ import { useEffect, useRef, useState, useCallback } from 'react'
3
3
  import { Command } from 'cmdk'
4
4
  import * as Dialog from '@radix-ui/react-dialog'
5
5
  import {
6
- Home, Users, Settings, CreditCard, Bell, Calendar, Package, BarChart3,
7
- FileText, Mail, Search, DollarSign, Megaphone, ShoppingCart, Target,
8
- Wrench, ClipboardList, Briefcase, UserCog, BookOpen, MessageCircle,
9
- Globe, Percent, Tag, Camera, UtensilsCrossed, MapPin, Handshake,
10
- Contact, Building2, Filter, Plus, List, User, Box, Sparkles, Loader2,
6
+ Home, Users, Settings, CreditCard, Bell, Calendar, CalendarCheck2,
7
+ CalendarClock, CalendarX, Package, Activity, BarChart3, FileText, FileCheck2, Mail, Search,
8
+ DollarSign, BadgeDollarSign, CircleDollarSign, Megaphone, ShoppingCart, ShoppingBag, Target,
9
+ Wrench, ClipboardList, ListChecks, Briefcase, UserCog, BookOpen, BookOpenCheck, MessageCircle,
10
+ Globe, Percent, Tag, Tags, Camera, UtensilsCrossed, MapPin, Map, Handshake,
11
+ Contact, Building2, Filter, Plus, List, Shield, User, Box, Sparkles, Loader2,
12
+ ListPlus, FolderOpen, Boxes, Landmark, Receipt, Ban, Clock, Clock3, Inbox,
13
+ Star, TrendingUp, UserCheck, UserPlus, UserX, Zap,
11
14
  type LucideIcon,
12
15
  } from 'lucide-react'
13
16
  import { useTranslation } from '../../hooks/useTranslation'
@@ -42,11 +45,12 @@ interface CommandPaletteProps {
42
45
  }
43
46
 
44
47
  const ICON_MAP: Record<string, LucideIcon> = {
45
- Home, Users, Settings, CreditCard, Bell, Calendar, Package, BarChart3,
46
- FileText, Mail, Search, DollarSign, Megaphone, ShoppingCart, Target,
47
- Wrench, ClipboardList, Briefcase, UserCog, BookOpen, MessageCircle,
48
- Globe, Percent, Tag, Camera, UtensilsCrossed, MapPin, Handshake,
49
- Contact, Building2, Filter, Plus, List, User, Box, Sparkles,
48
+ Home, Users, Settings, CreditCard, Bell, Calendar, CalendarClock, Package, Activity, BarChart3,
49
+ CalendarCheck2, CalendarX, FileText, FileCheck2, Mail, Search, DollarSign, BadgeDollarSign, CircleDollarSign, Megaphone, ShoppingCart, ShoppingBag, Target,
50
+ Wrench, ClipboardList, ListChecks, Briefcase, UserCog, BookOpen, BookOpenCheck, MessageCircle,
51
+ Globe, Percent, Tag, Tags, Camera, UtensilsCrossed, MapPin, Map, Handshake,
52
+ Contact, Building2, Filter, Plus, List, Shield, ListPlus, FolderOpen, User, Box, Boxes, Sparkles, Landmark, Receipt, Ban, Clock, Clock3, Inbox,
53
+ Star, TrendingUp, UserCheck, UserPlus, UserX, Zap,
50
54
  }
51
55
 
52
56
  const ENTITY_ICON: Record<string, LucideIcon> = {
@@ -16,8 +16,19 @@ import { Badge } from '@fayz-ai/ui'
16
16
  import { usePermissionsStore } from '../../stores/permissions.store'
17
17
  import { useOrganizationStore } from '../../stores/organization.store'
18
18
  import { useInviteStore } from '../../stores/invite.store'
19
- import { useOrgAdapterOptional } from '../../lib/org-context'
20
- import { useAuthStore } from '../../stores/auth.store'
19
+ // Native org adapter (the one the app actually provides). The shell lib/org-context
20
+ // is a separate, un-provided context that returns null here — which made the submit
21
+ // button a no-op (handleSend bailed on `!adapter`).
22
+ import { useOrgAdapterOptional as useNativeOrgAdapter } from '../../../org'
23
+ import type { OrgAdapter } from '../../types/org-adapter'
24
+
25
+ // The native adapter is the same runtime object; its Invite/Location types differ
26
+ // from the shell's only in fields these components don't read, so a cast is safe.
27
+ const useOrgAdapterOptional = () => useNativeOrgAdapter() as unknown as OrgAdapter | null
28
+ // The signed-in user lives in @fayz-ai/auth's store (what OrgProvider reads). The
29
+ // shell stores/auth.store is a separate, unpopulated store — reading `user` from
30
+ // it left `user` null, so handleSend bailed on `!user` (silent no-op).
31
+ import { useAuthStore } from '@fayz-ai/auth'
21
32
  import { useTranslation } from '../../hooks/useTranslation'
22
33
 
23
34
  interface InviteMemberDialogProps {
@@ -33,7 +44,7 @@ export function InviteMemberDialog({ open, onOpenChange, defaultEmail }: InviteM
33
44
  const currentOrg = useOrganizationStore((s) => s.currentOrg)
34
45
  const profiles = usePermissionsStore((s) => s.profiles)
35
46
  const setInvites = useInviteStore((s) => s.setInvites)
36
- const user = useAuthStore((s) => s.user)
47
+ const { user } = useAuthStore()
37
48
 
38
49
  const [emails, setEmails] = React.useState<string[]>([defaultEmail ?? ''])
39
50
  const [profileId, setProfileId] = React.useState(profiles[1]?.id ?? profiles[0]?.id ?? '')