@fayz-ai/saas 0.6.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.
- package/dist/app/AdminShell.d.ts +14 -1
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/LoginPage.d.ts +2 -16
- package/dist/app/LoginPage.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts +21 -0
- package/dist/app/admin-app.d.ts.map +1 -0
- package/dist/app/config.d.ts +40 -3
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/scaffold.d.ts +1 -2
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/{chunk-DJW67TCY.js → chunk-DCRT47W4.js} +155 -28
- package/dist/chunk-DCRT47W4.js.map +1 -0
- package/dist/{chunk-QGMDVLLW.cjs → chunk-SRIUD35R.cjs} +155 -28
- package/dist/chunk-SRIUD35R.cjs.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/index.cjs +1127 -5792
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +839 -5509
- package/dist/index.js.map +1 -1
- package/dist/org/adapters/supabase.d.ts +18 -0
- package/dist/org/adapters/supabase.d.ts.map +1 -1
- package/dist/org/context.d.ts.map +1 -1
- package/dist/org/index.cjs +11 -10
- package/dist/org/index.js +2 -1
- package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts +4 -2
- package/dist/shell/components/crud/archetypes/ClientOrdersTab.d.ts.map +1 -1
- package/dist/shell/components/layout/BottomNav.d.ts.map +1 -1
- package/dist/shell/components/layout/CommandPalette.d.ts.map +1 -1
- package/dist/shell/components/organization/InviteMemberDialog.d.ts.map +1 -1
- package/dist/shell/components/organization/PermissionMatrixEditor.d.ts +6 -1
- package/dist/shell/components/organization/PermissionMatrixEditor.d.ts.map +1 -1
- package/dist/shell/components/organization/PermissionProfilesTab.d.ts.map +1 -1
- package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
- package/dist/shell/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedUserProfile.d.ts +2 -1
- package/dist/shell/components/settings/ConnectedUserProfile.d.ts.map +1 -1
- package/dist/shell/components/settings/SettingsPage.d.ts +8 -1
- package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/shell/config/theme/tokens.d.ts +12 -1
- package/dist/shell/config/theme/tokens.d.ts.map +1 -1
- package/dist/shell/config/theme/utils.d.ts +8 -0
- package/dist/shell/config/theme/utils.d.ts.map +1 -1
- package/dist/shell/lib/i18n.d.ts.map +1 -1
- package/dist/shell/types/client-orders.d.ts +4 -0
- package/dist/shell/types/client-orders.d.ts.map +1 -1
- package/dist/shell/types/crud.d.ts +4 -0
- package/dist/shell/types/crud.d.ts.map +1 -1
- package/package.json +6 -8
- package/src/app/AdminShell.tsx +103 -35
- package/src/app/LoginPage.tsx +2 -264
- package/src/app/{createFayzApp.tsx → admin-app.tsx} +98 -40
- package/src/app/config.ts +44 -2
- package/src/app/scaffold.tsx +13 -26
- package/src/crud/CrudDetailPage.tsx +43 -9
- package/src/crud/CrudPage.tsx +1 -1
- package/src/index.ts +12 -5
- package/src/org/adapters/supabase.ts +176 -28
- package/src/org/context.tsx +33 -2
- package/src/shell/components/crud/archetypes/ClientOrdersTab.tsx +12 -4
- package/src/shell/components/layout/BottomNav.tsx +39 -4
- package/src/shell/components/layout/CommandPalette.tsx +14 -10
- package/src/shell/components/organization/InviteMemberDialog.tsx +14 -3
- package/src/shell/components/organization/PermissionMatrixEditor.tsx +48 -10
- package/src/shell/components/organization/PermissionProfilesTab.tsx +65 -23
- package/src/shell/components/organization/TeamTab.tsx +36 -9
- package/src/shell/components/plugins/PluginSettingsPanel.tsx +5 -3
- package/src/shell/components/settings/ConnectedUserProfile.tsx +68 -25
- package/src/shell/components/settings/SettingsPage.tsx +12 -3
- package/src/shell/config/theme/tokens.ts +13 -1
- package/src/shell/config/theme/utils.ts +76 -15
- package/src/shell/lib/i18n.ts +4 -0
- package/src/shell/types/client-orders.ts +5 -0
- package/src/shell/types/crud.ts +4 -0
- package/dist/app/createFayzApp.d.ts +0 -11
- package/dist/app/createFayzApp.d.ts.map +0 -1
- package/dist/chunk-DJW67TCY.js.map +0 -1
- package/dist/chunk-QGMDVLLW.cjs.map +0 -1
- package/dist/shell/components/organization/ImpersonationBanner.d.ts +0 -3
- package/dist/shell/components/organization/ImpersonationBanner.d.ts.map +0 -1
- package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts +0 -3
- package/dist/shell/components/settings/ConnectedHolidaysSettings.d.ts.map +0 -1
- package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts +0 -3
- package/dist/shell/components/settings/ConnectedLocationsOverview.d.ts.map +0 -1
- package/dist/shell/createSaasApp.d.ts +0 -145
- package/dist/shell/createSaasApp.d.ts.map +0 -1
- package/src/shell/components/organization/ImpersonationBanner.tsx +0 -29
- package/src/shell/components/settings/ConnectedHolidaysSettings.tsx +0 -99
- package/src/shell/components/settings/ConnectedLocationsOverview.tsx +0 -57
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
//
|
|
126
|
-
|
|
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
|
|
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)
|
|
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: '
|
|
150
|
-
update: '
|
|
151
|
-
manage: '
|
|
194
|
+
create: 'create',
|
|
195
|
+
update: 'edit',
|
|
196
|
+
manage: 'edit',
|
|
152
197
|
delete: 'delete',
|
|
153
|
-
configure: '
|
|
154
|
-
invite: '
|
|
198
|
+
configure: 'edit',
|
|
199
|
+
invite: 'create',
|
|
155
200
|
}
|
|
156
201
|
|
|
157
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
418
|
+
orgId: string,
|
|
419
|
+
profile: Omit<PermissionProfile, 'id' | 'isSystem'>,
|
|
341
420
|
): Promise<PermissionProfile> {
|
|
342
|
-
|
|
343
|
-
|
|
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(
|
|
397
|
-
|
|
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
|
-
|
|
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[]> {
|
package/src/org/context.tsx
CHANGED
|
@@ -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)
|
|
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,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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(
|
|
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 ?? '')
|