@open-mercato/core 0.5.1-develop.2987.9f8c1e0f68 → 0.5.1-develop.3032.01699048cb
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/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/sidebar_variant/index.js +25 -0
- package/dist/generated/entities/sidebar_variant/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +13 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/authUi.js +1 -1
- package/dist/helpers/integration/authUi.js.map +2 -2
- package/dist/modules/audit_logs/services/actionLogService.js +4 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/auth/api/sidebar/preferences/route.js +224 -35
- package/dist/modules/auth/api/sidebar/preferences/route.js.map +3 -3
- package/dist/modules/auth/api/sidebar/variants/[id]/route.js +161 -0
- package/dist/modules/auth/api/sidebar/variants/[id]/route.js.map +7 -0
- package/dist/modules/auth/api/sidebar/variants/route.js +142 -0
- package/dist/modules/auth/api/sidebar/variants/route.js.map +7 -0
- package/dist/modules/auth/backend/sidebar-customization/page.js +16 -0
- package/dist/modules/auth/backend/sidebar-customization/page.js.map +7 -0
- package/dist/modules/auth/backend/sidebar-customization/page.meta.js +28 -0
- package/dist/modules/auth/backend/sidebar-customization/page.meta.js.map +7 -0
- package/dist/modules/auth/data/entities.js +45 -4
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/data/validators.js +63 -1
- package/dist/modules/auth/data/validators.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260427081815.js +15 -0
- package/dist/modules/auth/migrations/Migration20260427081815.js.map +7 -0
- package/dist/modules/auth/migrations/Migration20260427124900.js +15 -0
- package/dist/modules/auth/migrations/Migration20260427124900.js.map +7 -0
- package/dist/modules/auth/migrations/Migration20260427143311.js +72 -0
- package/dist/modules/auth/migrations/Migration20260427143311.js.map +7 -0
- package/dist/modules/auth/services/sidebarPreferencesService.js +176 -16
- package/dist/modules/auth/services/sidebarPreferencesService.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +30 -20
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +12 -7
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +12 -7
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +12 -7
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/[id]/page.js +3 -1
- package/dist/modules/customers/backend/customers/companies/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +25 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +35 -33
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js +56 -0
- package/dist/modules/customers/components/detail/ActivitiesAddNewMenu.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesCard.js +175 -0
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +324 -0
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +7 -0
- package/dist/modules/customers/components/detail/ActivitiesSection.js +62 -13
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +14 -23
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +13 -13
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +35 -22
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/AiActionChips.js +15 -22
- package/dist/modules/customers/components/detail/AiActionChips.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +3 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +196 -28
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/FooterFields.js +14 -2
- package/dist/modules/customers/components/detail/schedule/FooterFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +9 -2
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +9 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +25 -4
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +20 -3
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +3 -3
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/displayName.js +12 -0
- package/dist/modules/customers/lib/displayName.js.map +2 -2
- package/dist/modules/entities/cli.js +5 -6
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js +124 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.js.map +7 -0
- package/generated/entities/sidebar_variant/index.ts +11 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/generated/entity-fields-registry.ts +13 -0
- package/package.json +6 -6
- package/src/helpers/integration/authUi.ts +1 -1
- package/src/modules/audit_logs/services/actionLogService.ts +5 -6
- package/src/modules/auth/api/sidebar/preferences/route.ts +266 -34
- package/src/modules/auth/api/sidebar/variants/[id]/route.ts +183 -0
- package/src/modules/auth/api/sidebar/variants/route.ts +157 -0
- package/src/modules/auth/backend/sidebar-customization/page.meta.ts +34 -0
- package/src/modules/auth/backend/sidebar-customization/page.tsx +17 -0
- package/src/modules/auth/data/entities.ts +48 -2
- package/src/modules/auth/data/validators.ts +70 -0
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +790 -71
- package/src/modules/auth/migrations/Migration20260427081815.ts +16 -0
- package/src/modules/auth/migrations/Migration20260427124900.ts +19 -0
- package/src/modules/auth/migrations/Migration20260427143311.ts +83 -0
- package/src/modules/auth/services/sidebarPreferencesService.ts +243 -18
- package/src/modules/customers/api/companies/[id]/route.ts +30 -20
- package/src/modules/customers/api/companies/route.ts +12 -7
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +12 -7
- package/src/modules/customers/api/people/route.ts +12 -7
- package/src/modules/customers/backend/customers/companies/[id]/page.tsx +5 -4
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +28 -5
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +41 -30
- package/src/modules/customers/components/detail/ActivitiesAddNewMenu.tsx +67 -0
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +231 -0
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +390 -0
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +91 -40
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +25 -23
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +15 -19
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +36 -29
- package/src/modules/customers/components/detail/AiActionChips.tsx +17 -23
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +3 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +233 -41
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +6 -2
- package/src/modules/customers/components/detail/schedule/FooterFields.tsx +22 -2
- package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +10 -2
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +10 -2
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +26 -6
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +32 -3
- package/src/modules/customers/components/formConfig.tsx +3 -3
- package/src/modules/customers/i18n/de.json +69 -2
- package/src/modules/customers/i18n/en.json +69 -2
- package/src/modules/customers/i18n/es.json +69 -2
- package/src/modules/customers/i18n/pl.json +68 -1
- package/src/modules/customers/lib/displayName.ts +21 -0
- package/src/modules/entities/cli.ts +5 -6
- package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/reset-password/page.tsx +168 -0
- package/src/modules/portal/i18n/de.json +20 -0
- package/src/modules/portal/i18n/en.json +20 -0
- package/src/modules/portal/i18n/es.json +20 -0
- package/src/modules/portal/i18n/pl.json +20 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260427081815 extends Migration {
|
|
4
|
+
|
|
5
|
+
override up(): void | Promise<void> {
|
|
6
|
+
this.addSql(`create table "sidebar_variants" ("id" uuid not null default gen_random_uuid(), "user_id" uuid not null, "tenant_id" uuid null, "organization_id" uuid null, "locale" text not null, "name" text not null, "settings_json" jsonb null, "is_active" boolean not null default false, "created_at" timestamptz not null, "updated_at" timestamptz null, "deleted_at" timestamptz null, primary key ("id"));`);
|
|
7
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_tenant_id_locale_name_unique" unique ("user_id", "tenant_id", "locale", "name");`);
|
|
8
|
+
|
|
9
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_foreign" foreign key ("user_id") references "users" ("id");`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override down(): void | Promise<void> {
|
|
13
|
+
this.addSql(`drop table if exists "sidebar_variants" cascade;`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
// Replace the full unique constraint on sidebar_variants with a partial unique index
|
|
4
|
+
// scoped to live rows (deleted_at IS NULL). Without this, soft-deleting a variant and
|
|
5
|
+
// then trying to recreate one with the same name throws a duplicate-key error because
|
|
6
|
+
// the regular unique constraint considers tombstoned rows.
|
|
7
|
+
export class Migration20260427124900 extends Migration {
|
|
8
|
+
|
|
9
|
+
override up(): void | Promise<void> {
|
|
10
|
+
this.addSql(`alter table "sidebar_variants" drop constraint if exists "sidebar_variants_user_id_tenant_id_locale_name_unique";`);
|
|
11
|
+
this.addSql(`create unique index if not exists "sidebar_variants_active_name_unique_idx" on "sidebar_variants" ("user_id", "tenant_id", "locale", "name") where "deleted_at" is null;`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override down(): void | Promise<void> {
|
|
15
|
+
this.addSql(`drop index if exists "sidebar_variants_active_name_unique_idx";`);
|
|
16
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_tenant_id_locale_name_unique" unique ("user_id", "tenant_id", "locale", "name");`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
// Cross-locale sidebar customization. Strip `locale` from uniqueness scope so the same
|
|
4
|
+
// variant/preference applies regardless of the user's active language. Soft-delete any
|
|
5
|
+
// historical duplicates first (keep the most-recently-updated row per scope) so the new
|
|
6
|
+
// constraints can be created without violations.
|
|
7
|
+
export class Migration20260427143311 extends Migration {
|
|
8
|
+
|
|
9
|
+
override up(): void | Promise<void> {
|
|
10
|
+
// --- sidebar_variants: collapse (user_id, tenant_id, name) across locales ---------
|
|
11
|
+
this.addSql(`
|
|
12
|
+
with ranked as (
|
|
13
|
+
select id,
|
|
14
|
+
row_number() over (
|
|
15
|
+
partition by user_id, tenant_id, name
|
|
16
|
+
order by coalesce(updated_at, created_at) desc, created_at desc, id desc
|
|
17
|
+
) as rn
|
|
18
|
+
from sidebar_variants
|
|
19
|
+
where deleted_at is null
|
|
20
|
+
)
|
|
21
|
+
update sidebar_variants
|
|
22
|
+
set deleted_at = now()
|
|
23
|
+
from ranked
|
|
24
|
+
where sidebar_variants.id = ranked.id and ranked.rn > 1;
|
|
25
|
+
`);
|
|
26
|
+
this.addSql(`drop index if exists "sidebar_variants_active_name_unique_idx";`);
|
|
27
|
+
this.addSql(`alter table "sidebar_variants" drop constraint if exists "sidebar_variants_user_id_tenant_id_locale_name_unique";`);
|
|
28
|
+
this.addSql(`create unique index if not exists "sidebar_variants_active_name_unique_idx" on "sidebar_variants" ("user_id", "tenant_id", "name") where "deleted_at" is null;`);
|
|
29
|
+
|
|
30
|
+
// --- user_sidebar_preferences: collapse (user_id, tenant_id, organization_id) -----
|
|
31
|
+
this.addSql(`
|
|
32
|
+
with ranked as (
|
|
33
|
+
select id,
|
|
34
|
+
row_number() over (
|
|
35
|
+
partition by user_id, tenant_id, organization_id
|
|
36
|
+
order by coalesce(updated_at, created_at) desc, created_at desc, id desc
|
|
37
|
+
) as rn
|
|
38
|
+
from user_sidebar_preferences
|
|
39
|
+
where deleted_at is null
|
|
40
|
+
)
|
|
41
|
+
update user_sidebar_preferences
|
|
42
|
+
set deleted_at = now()
|
|
43
|
+
from ranked
|
|
44
|
+
where user_sidebar_preferences.id = ranked.id and ranked.rn > 1;
|
|
45
|
+
`);
|
|
46
|
+
this.addSql(`alter table "user_sidebar_preferences" drop constraint if exists "user_sidebar_preferences_user_id_tenant_id_organi_35248_unique";`);
|
|
47
|
+
this.addSql(`alter table "user_sidebar_preferences" drop constraint if exists "user_sidebar_preferences_user_id_tenant_id_organi_f3f2f_unique";`);
|
|
48
|
+
this.addSql(`drop index if exists "user_sidebar_preferences_active_unique_idx";`);
|
|
49
|
+
this.addSql(`create unique index if not exists "user_sidebar_preferences_active_unique_idx" on "user_sidebar_preferences" ("user_id", "tenant_id", "organization_id") where "deleted_at" is null;`);
|
|
50
|
+
|
|
51
|
+
// --- role_sidebar_preferences: collapse (role_id, tenant_id) ----------------------
|
|
52
|
+
this.addSql(`
|
|
53
|
+
with ranked as (
|
|
54
|
+
select id,
|
|
55
|
+
row_number() over (
|
|
56
|
+
partition by role_id, tenant_id
|
|
57
|
+
order by coalesce(updated_at, created_at) desc, created_at desc, id desc
|
|
58
|
+
) as rn
|
|
59
|
+
from role_sidebar_preferences
|
|
60
|
+
where deleted_at is null
|
|
61
|
+
)
|
|
62
|
+
update role_sidebar_preferences
|
|
63
|
+
set deleted_at = now()
|
|
64
|
+
from ranked
|
|
65
|
+
where role_sidebar_preferences.id = ranked.id and ranked.rn > 1;
|
|
66
|
+
`);
|
|
67
|
+
this.addSql(`alter table "role_sidebar_preferences" drop constraint if exists "role_sidebar_preferences_role_id_tenant_id_locale_unique";`);
|
|
68
|
+
this.addSql(`drop index if exists "role_sidebar_preferences_active_unique_idx";`);
|
|
69
|
+
this.addSql(`create unique index if not exists "role_sidebar_preferences_active_unique_idx" on "role_sidebar_preferences" ("role_id", "tenant_id") where "deleted_at" is null;`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override down(): void | Promise<void> {
|
|
73
|
+
this.addSql(`drop index if exists "sidebar_variants_active_name_unique_idx";`);
|
|
74
|
+
this.addSql(`alter table "sidebar_variants" add constraint "sidebar_variants_user_id_tenant_id_locale_name_unique" unique ("user_id", "tenant_id", "locale", "name");`);
|
|
75
|
+
|
|
76
|
+
this.addSql(`drop index if exists "user_sidebar_preferences_active_unique_idx";`);
|
|
77
|
+
this.addSql(`alter table "user_sidebar_preferences" add constraint "user_sidebar_preferences_user_id_tenant_id_organi_35248_unique" unique ("user_id", "tenant_id", "organization_id", "locale");`);
|
|
78
|
+
|
|
79
|
+
this.addSql(`drop index if exists "role_sidebar_preferences_active_unique_idx";`);
|
|
80
|
+
this.addSql(`alter table "role_sidebar_preferences" add constraint "role_sidebar_preferences_role_id_tenant_id_locale_unique" unique ("role_id", "tenant_id", "locale");`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import {
|
|
1
|
+
import { EntityManager, type FilterQuery } from '@mikro-orm/postgresql'
|
|
2
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import { Role, RoleSidebarPreference, SidebarVariant, User, UserSidebarPreference } from '../data/entities'
|
|
3
4
|
import {
|
|
4
5
|
SIDEBAR_PREFERENCES_VERSION,
|
|
5
6
|
SidebarPreferencesSettings,
|
|
@@ -39,8 +40,16 @@ export async function loadSidebarPreference(
|
|
|
39
40
|
em: EntityManager,
|
|
40
41
|
scope: SidebarPreferenceScope,
|
|
41
42
|
): Promise<SidebarPreferencesSettings> {
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
// Cross-locale: variants & preferences are scoped per (user, tenant, org) only.
|
|
44
|
+
// The `locale` field on the row is kept for audit / when the row was created.
|
|
45
|
+
const { userId, tenantId, organizationId } = normalizeScope(scope)
|
|
46
|
+
const existing = await findOneWithDecryption(
|
|
47
|
+
em,
|
|
48
|
+
UserSidebarPreference,
|
|
49
|
+
{ user: userId, tenantId, organizationId },
|
|
50
|
+
undefined,
|
|
51
|
+
{ tenantId, organizationId },
|
|
52
|
+
)
|
|
44
53
|
return normalizeSidebarSettings(existing?.settingsJson as SidebarPreferencesSettings | undefined)
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -54,7 +63,13 @@ export async function saveSidebarPreference(
|
|
|
54
63
|
version: input?.version ?? SIDEBAR_PREFERENCES_VERSION,
|
|
55
64
|
})
|
|
56
65
|
const { userId, tenantId, organizationId, locale } = normalizeScope(scope)
|
|
57
|
-
let pref = await
|
|
66
|
+
let pref = await findOneWithDecryption(
|
|
67
|
+
em,
|
|
68
|
+
UserSidebarPreference,
|
|
69
|
+
{ user: userId, tenantId, organizationId },
|
|
70
|
+
undefined,
|
|
71
|
+
{ tenantId, organizationId },
|
|
72
|
+
)
|
|
58
73
|
if (!pref) {
|
|
59
74
|
pref = em.create(UserSidebarPreference, {
|
|
60
75
|
user: em.getReference(User, userId),
|
|
@@ -73,16 +88,18 @@ export async function saveSidebarPreference(
|
|
|
73
88
|
|
|
74
89
|
export async function loadRoleSidebarPreferences(
|
|
75
90
|
em: EntityManager,
|
|
76
|
-
options: { roleIds: string[]; tenantId?: string | null; locale
|
|
91
|
+
options: { roleIds: string[]; tenantId?: string | null; locale?: string },
|
|
77
92
|
): Promise<Map<string, SidebarPreferencesSettings>> {
|
|
78
93
|
if (!options.roleIds.length) return new Map()
|
|
79
94
|
const tenantId = options.tenantId ?? null
|
|
80
95
|
const tenantFilter = tenantId === null ? null : { $in: [tenantId, null] }
|
|
81
|
-
const prefs = await
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
const prefs = await findWithDecryption(
|
|
97
|
+
em,
|
|
98
|
+
RoleSidebarPreference,
|
|
99
|
+
{ role: { $in: options.roleIds }, tenantId: tenantFilter } as FilterQuery<RoleSidebarPreference>,
|
|
100
|
+
undefined,
|
|
101
|
+
{ tenantId, organizationId: null },
|
|
102
|
+
)
|
|
86
103
|
const map = new Map<string, SidebarPreferencesSettings>()
|
|
87
104
|
for (const pref of prefs) {
|
|
88
105
|
const key = pref.role.id
|
|
@@ -101,16 +118,18 @@ export async function loadRoleSidebarPreferences(
|
|
|
101
118
|
|
|
102
119
|
export async function loadFirstRoleSidebarPreference(
|
|
103
120
|
em: EntityManager,
|
|
104
|
-
options: { roleIds: string[]; tenantId?: string | null; locale
|
|
121
|
+
options: { roleIds: string[]; tenantId?: string | null; locale?: string },
|
|
105
122
|
): Promise<SidebarPreferencesSettings | null> {
|
|
106
123
|
if (!options.roleIds.length) return null
|
|
107
124
|
const tenantId = options.tenantId ?? null
|
|
108
125
|
const tenantFilter = tenantId === null ? null : { $in: [tenantId, null] }
|
|
109
|
-
const prefs = await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
const prefs = await findWithDecryption(
|
|
127
|
+
em,
|
|
128
|
+
RoleSidebarPreference,
|
|
129
|
+
{ role: { $in: options.roleIds }, tenantId: tenantFilter } as FilterQuery<RoleSidebarPreference>,
|
|
130
|
+
undefined,
|
|
131
|
+
{ tenantId, organizationId: null },
|
|
132
|
+
)
|
|
114
133
|
if (!prefs.length) return null
|
|
115
134
|
const ordered = options.roleIds
|
|
116
135
|
.map((id) => {
|
|
@@ -135,7 +154,13 @@ export async function saveRoleSidebarPreference(
|
|
|
135
154
|
version: input?.version ?? SIDEBAR_PREFERENCES_VERSION,
|
|
136
155
|
})
|
|
137
156
|
const { roleId, tenantId, locale } = normalizeRoleScope(scope)
|
|
138
|
-
let pref = await
|
|
157
|
+
let pref = await findOneWithDecryption(
|
|
158
|
+
em,
|
|
159
|
+
RoleSidebarPreference,
|
|
160
|
+
{ role: roleId, tenantId },
|
|
161
|
+
undefined,
|
|
162
|
+
{ tenantId, organizationId: null },
|
|
163
|
+
)
|
|
139
164
|
if (!pref) {
|
|
140
165
|
pref = em.create(RoleSidebarPreference, {
|
|
141
166
|
role: em.getReference(Role, roleId),
|
|
@@ -217,3 +242,203 @@ function normalizeRoleScope(scope: RoleSidebarPreferenceScope) {
|
|
|
217
242
|
locale: scope.locale,
|
|
218
243
|
}
|
|
219
244
|
}
|
|
245
|
+
|
|
246
|
+
// --- Named variants (per-user library of saved sidebar layouts) ----------------
|
|
247
|
+
|
|
248
|
+
export type VariantScope = {
|
|
249
|
+
userId: string
|
|
250
|
+
tenantId?: string | null
|
|
251
|
+
organizationId?: string | null
|
|
252
|
+
locale: string
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export type SidebarVariantRecord = {
|
|
256
|
+
id: string
|
|
257
|
+
name: string
|
|
258
|
+
isActive: boolean
|
|
259
|
+
settings: SidebarPreferencesSettings
|
|
260
|
+
createdAt: Date
|
|
261
|
+
updatedAt?: Date | null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toVariantRecord(variant: SidebarVariant): SidebarVariantRecord {
|
|
265
|
+
return {
|
|
266
|
+
id: variant.id,
|
|
267
|
+
name: variant.name,
|
|
268
|
+
isActive: variant.isActive === true,
|
|
269
|
+
settings: normalizeSidebarSettings(variant.settingsJson as SidebarPreferencesSettings | undefined),
|
|
270
|
+
createdAt: variant.createdAt,
|
|
271
|
+
updatedAt: variant.updatedAt ?? null,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function listSidebarVariants(
|
|
276
|
+
em: EntityManager,
|
|
277
|
+
scope: VariantScope,
|
|
278
|
+
): Promise<SidebarVariantRecord[]> {
|
|
279
|
+
// Cross-locale: variants are scoped per (user, tenant) only.
|
|
280
|
+
const { userId, tenantId, organizationId } = normalizeVariantScope(scope)
|
|
281
|
+
const variants = await findWithDecryption(
|
|
282
|
+
em,
|
|
283
|
+
SidebarVariant,
|
|
284
|
+
{ user: userId, tenantId, deletedAt: null },
|
|
285
|
+
{ orderBy: { createdAt: 'asc' } },
|
|
286
|
+
{ tenantId, organizationId },
|
|
287
|
+
)
|
|
288
|
+
return variants.map(toVariantRecord)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function loadSidebarVariant(
|
|
292
|
+
em: EntityManager,
|
|
293
|
+
scope: VariantScope,
|
|
294
|
+
variantId: string,
|
|
295
|
+
): Promise<SidebarVariantRecord | null> {
|
|
296
|
+
const { userId, tenantId, organizationId } = normalizeVariantScope(scope)
|
|
297
|
+
const variant = await findOneWithDecryption(
|
|
298
|
+
em,
|
|
299
|
+
SidebarVariant,
|
|
300
|
+
{ id: variantId, user: userId, tenantId, deletedAt: null },
|
|
301
|
+
undefined,
|
|
302
|
+
{ tenantId, organizationId },
|
|
303
|
+
)
|
|
304
|
+
return variant ? toVariantRecord(variant) : null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function nextVariantAutoName(
|
|
308
|
+
em: EntityManager,
|
|
309
|
+
scope: VariantScope,
|
|
310
|
+
prefix = 'My preferences',
|
|
311
|
+
): Promise<string> {
|
|
312
|
+
const variants = await listSidebarVariants(em, scope)
|
|
313
|
+
// Match names like "My preferences", "My preferences 2", "My preferences 17"
|
|
314
|
+
const usedNumbers = new Set<number>()
|
|
315
|
+
for (const variant of variants) {
|
|
316
|
+
if (variant.name === prefix) {
|
|
317
|
+
usedNumbers.add(1)
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
321
|
+
const match = variant.name.match(new RegExp(`^${escaped}\\s+(\\d+)$`))
|
|
322
|
+
if (match) {
|
|
323
|
+
const n = Number.parseInt(match[1], 10)
|
|
324
|
+
if (!Number.isNaN(n)) usedNumbers.add(n)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (!usedNumbers.has(1)) return prefix
|
|
328
|
+
let next = 2
|
|
329
|
+
while (usedNumbers.has(next)) next += 1
|
|
330
|
+
return `${prefix} ${next}`
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function createSidebarVariant(
|
|
334
|
+
em: EntityManager,
|
|
335
|
+
scope: VariantScope,
|
|
336
|
+
input: {
|
|
337
|
+
name?: string | null
|
|
338
|
+
settings?: Partial<SidebarPreferencesSettings> | null
|
|
339
|
+
isActive?: boolean
|
|
340
|
+
},
|
|
341
|
+
): Promise<SidebarVariantRecord> {
|
|
342
|
+
const { userId, tenantId, organizationId, locale } = normalizeVariantScope(scope)
|
|
343
|
+
const finalName = (input.name ?? '').trim() || (await nextVariantAutoName(em, scope))
|
|
344
|
+
const settings = normalizeSidebarSettings({
|
|
345
|
+
...(input.settings ?? {}),
|
|
346
|
+
version: input.settings?.version ?? SIDEBAR_PREFERENCES_VERSION,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
if (input.isActive === true) {
|
|
350
|
+
await deactivateAllVariants(em, scope)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const variant = em.create(SidebarVariant, {
|
|
354
|
+
user: em.getReference(User, userId),
|
|
355
|
+
tenantId,
|
|
356
|
+
organizationId,
|
|
357
|
+
locale,
|
|
358
|
+
name: finalName,
|
|
359
|
+
settingsJson: settings,
|
|
360
|
+
isActive: input.isActive === true,
|
|
361
|
+
createdAt: new Date(),
|
|
362
|
+
})
|
|
363
|
+
await em.flush()
|
|
364
|
+
return toVariantRecord(variant)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function updateSidebarVariant(
|
|
368
|
+
em: EntityManager,
|
|
369
|
+
scope: VariantScope,
|
|
370
|
+
variantId: string,
|
|
371
|
+
input: {
|
|
372
|
+
name?: string
|
|
373
|
+
settings?: Partial<SidebarPreferencesSettings> | null
|
|
374
|
+
isActive?: boolean
|
|
375
|
+
},
|
|
376
|
+
): Promise<SidebarVariantRecord | null> {
|
|
377
|
+
const { userId, tenantId, organizationId } = normalizeVariantScope(scope)
|
|
378
|
+
const variant = await findOneWithDecryption(
|
|
379
|
+
em,
|
|
380
|
+
SidebarVariant,
|
|
381
|
+
{ id: variantId, user: userId, tenantId, deletedAt: null },
|
|
382
|
+
undefined,
|
|
383
|
+
{ tenantId, organizationId },
|
|
384
|
+
)
|
|
385
|
+
if (!variant) return null
|
|
386
|
+
if (typeof input.name === 'string' && input.name.trim().length > 0) {
|
|
387
|
+
variant.name = input.name.trim()
|
|
388
|
+
}
|
|
389
|
+
if (input.settings) {
|
|
390
|
+
variant.settingsJson = normalizeSidebarSettings({
|
|
391
|
+
...input.settings,
|
|
392
|
+
version: input.settings.version ?? SIDEBAR_PREFERENCES_VERSION,
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
if (typeof input.isActive === 'boolean') {
|
|
396
|
+
if (input.isActive) {
|
|
397
|
+
await deactivateAllVariants(em, scope, variantId)
|
|
398
|
+
}
|
|
399
|
+
variant.isActive = input.isActive
|
|
400
|
+
}
|
|
401
|
+
await em.flush()
|
|
402
|
+
return toVariantRecord(variant)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function deleteSidebarVariant(
|
|
406
|
+
em: EntityManager,
|
|
407
|
+
scope: VariantScope,
|
|
408
|
+
variantId: string,
|
|
409
|
+
): Promise<boolean> {
|
|
410
|
+
const { userId, tenantId, organizationId } = normalizeVariantScope(scope)
|
|
411
|
+
const variant = await findOneWithDecryption(
|
|
412
|
+
em,
|
|
413
|
+
SidebarVariant,
|
|
414
|
+
{ id: variantId, user: userId, tenantId, deletedAt: null },
|
|
415
|
+
undefined,
|
|
416
|
+
{ tenantId, organizationId },
|
|
417
|
+
)
|
|
418
|
+
if (!variant) return false
|
|
419
|
+
variant.deletedAt = new Date()
|
|
420
|
+
variant.isActive = false
|
|
421
|
+
await em.flush()
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function deactivateAllVariants(
|
|
426
|
+
em: EntityManager,
|
|
427
|
+
scope: VariantScope,
|
|
428
|
+
exceptId?: string,
|
|
429
|
+
): Promise<void> {
|
|
430
|
+
const { userId, tenantId } = normalizeVariantScope(scope)
|
|
431
|
+
const where: FilterQuery<SidebarVariant> = exceptId
|
|
432
|
+
? { user: userId, tenantId, isActive: true, deletedAt: null, id: { $ne: exceptId } }
|
|
433
|
+
: { user: userId, tenantId, isActive: true, deletedAt: null }
|
|
434
|
+
await em.nativeUpdate(SidebarVariant, where, { isActive: false })
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeVariantScope(scope: VariantScope) {
|
|
438
|
+
return {
|
|
439
|
+
userId: scope.userId,
|
|
440
|
+
tenantId: scope.tenantId ?? null,
|
|
441
|
+
organizationId: scope.organizationId ?? null,
|
|
442
|
+
locale: scope.locale,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
@@ -42,7 +42,10 @@ import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
|
42
42
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
43
43
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
44
44
|
import { parseBooleanFromUnknown } from '@open-mercato/shared/lib/boolean'
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
filterActivePersonCompanyLinks,
|
|
47
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
48
|
+
} from '../../../lib/personCompanyLinkTable'
|
|
46
49
|
import { normalizeCustomerDetailCustomFields } from '../../detailCustomFields'
|
|
47
50
|
|
|
48
51
|
export const metadata = {
|
|
@@ -703,15 +706,17 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
703
706
|
},
|
|
704
707
|
'customers.companies.GET',
|
|
705
708
|
)
|
|
706
|
-
const companyLinks =
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
709
|
+
const companyLinks = filterActivePersonCompanyLinks(
|
|
710
|
+
await findWithDecryption(
|
|
711
|
+
em,
|
|
712
|
+
CustomerPersonCompanyLink,
|
|
713
|
+
companyLinkWhere,
|
|
714
|
+
{
|
|
715
|
+
populate: ['person', 'person.personProfile'],
|
|
716
|
+
orderBy: { isPrimary: 'desc', createdAt: 'asc' },
|
|
717
|
+
},
|
|
718
|
+
peopleDecryptionScope,
|
|
719
|
+
),
|
|
715
720
|
)
|
|
716
721
|
companyLinks.forEach((link) => {
|
|
717
722
|
const entity = typeof link.person === 'string' ? null : link.person
|
|
@@ -829,18 +834,23 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
829
834
|
})
|
|
830
835
|
const peopleCount = includePeople
|
|
831
836
|
? relatedPeople.length
|
|
832
|
-
:
|
|
833
|
-
|
|
834
|
-
await withActiveCustomerPersonCompanyLinkFilter(
|
|
837
|
+
: filterActivePersonCompanyLinks(
|
|
838
|
+
await findWithDecryption(
|
|
835
839
|
em,
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
840
|
+
CustomerPersonCompanyLink,
|
|
841
|
+
await withActiveCustomerPersonCompanyLinkFilter(
|
|
842
|
+
em,
|
|
843
|
+
{
|
|
844
|
+
company: company.id,
|
|
845
|
+
organizationId: company.organizationId,
|
|
846
|
+
tenantId: company.tenantId,
|
|
847
|
+
},
|
|
848
|
+
'customers.companies.GET',
|
|
849
|
+
),
|
|
850
|
+
{},
|
|
851
|
+
{ tenantId: company.tenantId, organizationId: company.organizationId },
|
|
842
852
|
),
|
|
843
|
-
)
|
|
853
|
+
).length
|
|
844
854
|
const kpiInteractionRows = canonicalActiveInteractions.length
|
|
845
855
|
? canonicalActiveInteractions
|
|
846
856
|
: await findWithDecryption(
|
|
@@ -33,7 +33,10 @@ import {
|
|
|
33
33
|
createPagedListResponseSchema,
|
|
34
34
|
defaultOkResponseSchema,
|
|
35
35
|
} from '../openapi'
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
filterActivePersonCompanyLinks,
|
|
38
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
39
|
+
} from '../../lib/personCompanyLinkTable'
|
|
37
40
|
import { normalizeCompanyProfilePayload } from './payload'
|
|
38
41
|
|
|
39
42
|
const rawBodySchema = z.object({}).passthrough()
|
|
@@ -217,12 +220,14 @@ const crud = makeCrudRoute({
|
|
|
217
220
|
{ person: query.excludeLinkedPersonId },
|
|
218
221
|
'customers.companies.GET',
|
|
219
222
|
)
|
|
220
|
-
const links =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
const links = filterActivePersonCompanyLinks(
|
|
224
|
+
await findWithDecryption(
|
|
225
|
+
em,
|
|
226
|
+
CustomerPersonCompanyLink,
|
|
227
|
+
linkWhere,
|
|
228
|
+
{ populate: ['company'] },
|
|
229
|
+
decryptionScope,
|
|
230
|
+
),
|
|
226
231
|
)
|
|
227
232
|
links.forEach((link) => {
|
|
228
233
|
const companyId = link.company?.id
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
CustomerDeal,
|
|
21
21
|
CustomerInteraction,
|
|
22
22
|
} from '../../../../../data/entities'
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
filterActivePersonCompanyLinks,
|
|
25
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
26
|
+
} from '../../../../../lib/personCompanyLinkTable'
|
|
24
27
|
|
|
25
28
|
const paramsSchema = z.object({
|
|
26
29
|
id: z.string().uuid(),
|
|
@@ -187,12 +190,14 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
187
190
|
},
|
|
188
191
|
'customers.people.companiesEnriched.GET',
|
|
189
192
|
)
|
|
190
|
-
const links =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
const links = filterActivePersonCompanyLinks(
|
|
194
|
+
await findWithDecryption(
|
|
195
|
+
em,
|
|
196
|
+
CustomerPersonCompanyLink,
|
|
197
|
+
linkWhere,
|
|
198
|
+
{ populate: ['company'] },
|
|
199
|
+
entityScope,
|
|
200
|
+
),
|
|
196
201
|
)
|
|
197
202
|
|
|
198
203
|
const companyIds = links.map((link) => (link.company as CustomerEntity).id)
|
|
@@ -29,7 +29,10 @@ import {
|
|
|
29
29
|
createPagedListResponseSchema,
|
|
30
30
|
defaultOkResponseSchema,
|
|
31
31
|
} from '../openapi'
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
filterActivePersonCompanyLinks,
|
|
34
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
35
|
+
} from '../../lib/personCompanyLinkTable'
|
|
33
36
|
import { normalizeProfilePayload } from './payload'
|
|
34
37
|
|
|
35
38
|
const rawBodySchema = z.object({}).passthrough()
|
|
@@ -221,12 +224,14 @@ const crud = makeCrudRoute({
|
|
|
221
224
|
{ company: query.excludeLinkedCompanyId },
|
|
222
225
|
'customers.people.GET',
|
|
223
226
|
)
|
|
224
|
-
const links =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
const links = filterActivePersonCompanyLinks(
|
|
228
|
+
await findWithDecryption(
|
|
229
|
+
em,
|
|
230
|
+
CustomerPersonCompanyLink,
|
|
231
|
+
linkWhere,
|
|
232
|
+
{ populate: ['person'] },
|
|
233
|
+
decryptionScope,
|
|
234
|
+
),
|
|
230
235
|
)
|
|
231
236
|
links.forEach((link) => {
|
|
232
237
|
const personId = link.person?.id
|
|
@@ -35,6 +35,7 @@ import { CompanyHighlights } from '../../../../components/detail/CompanyHighligh
|
|
|
35
35
|
import { normalizeCustomFieldSubmitValue } from '../../../../components/detail/customFieldUtils'
|
|
36
36
|
import { InlineDictionaryEditor, renderMultilineMarkdownDisplay } from '../../../../components/detail/InlineEditors'
|
|
37
37
|
import { formatTemplate } from '../../../../components/detail/utils'
|
|
38
|
+
import { coerceDisplayName } from '../../../../lib/displayName'
|
|
38
39
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
39
40
|
import {
|
|
40
41
|
CompanyPeopleSection,
|
|
@@ -117,10 +118,10 @@ export default function CustomerCompanyDetailPage({ params }: { params?: { id?:
|
|
|
117
118
|
const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
|
|
118
119
|
const [isDeleting, setIsDeleting] = React.useState(false)
|
|
119
120
|
const currentCompanyId = data?.company?.id ?? null
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const companyDisplayName = coerceDisplayName(data?.company?.displayName)
|
|
122
|
+
const companyName = companyDisplayName.trim().length
|
|
123
|
+
? companyDisplayName
|
|
124
|
+
: t('customers.companies.list.deleteFallbackName', 'this company')
|
|
124
125
|
const translateCompanyDetail = React.useCallback(
|
|
125
126
|
(key: string, fallback?: string, params?: Record<string, string | number>) => {
|
|
126
127
|
const mappedKey = key.startsWith('customers.people.detail.')
|