@open-mercato/core 0.6.3-develop.3811.1.be22750402 → 0.6.3-develop.3838.1.7f2657a47c

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 2603 entry points
1
+ [build:core] found 2607 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 172 entry points
4
4
  [build:core:generated] built successfully
@@ -1,12 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
- import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
- import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
5
- import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
6
- import { User } from "@open-mercato/core/modules/auth/data/entities";
7
- import { StaffTeam, StaffTeamMember } from "@open-mercato/core/modules/staff/data/entities";
8
3
  import { createPagedListResponseSchema } from "../openapi.js";
9
- import { resolveAuthActorId, resolveCustomersRequestContext } from "../../lib/interactionRequestContext.js";
10
4
  const querySchema = z.object({
11
5
  page: z.coerce.number().min(1).default(1),
12
6
  pageSize: z.coerce.number().min(1).max(100).default(24),
@@ -32,164 +26,31 @@ const errorSchema = z.object({ error: z.string() });
32
26
  const metadata = {
33
27
  GET: { requireAuth: true, requireFeatures: ["customers.roles.view"] }
34
28
  };
35
- async function canAccessAssignableStaff(rbac, userId, scope) {
36
- if (!rbac) return false;
37
- if (await rbac.userHasAllFeatures(userId, ["customers.roles.manage"], scope)) {
38
- return true;
39
- }
40
- return rbac.userHasAllFeatures(userId, ["customers.activities.manage"], scope);
41
- }
42
29
  async function GET(request) {
43
- const { translate } = await resolveTranslations();
44
- try {
45
- const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams));
46
- const { container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request);
47
- if (!selectedOrganizationId) {
48
- throw new CrudHttpError(
49
- 400,
50
- { error: translate("customers.errors.organization_required", "Organization context is required") }
51
- );
52
- }
53
- const actorId = resolveAuthActorId(auth);
54
- const rbacService = container.resolve("rbacService");
55
- const scope = { tenantId: auth.tenantId, organizationId: selectedOrganizationId };
56
- const hasAccess = await canAccessAssignableStaff(rbacService, actorId, scope);
57
- if (!hasAccess) {
58
- throw new CrudHttpError(
59
- 403,
60
- {
61
- error: translate(
62
- "customers.assignableStaff.forbidden",
63
- "Insufficient permissions to load assignable staff."
64
- )
65
- }
66
- );
67
- }
68
- const normalizedSearch = query.search?.trim().toLowerCase() ?? "";
69
- const members = await findWithDecryption(
70
- em,
71
- StaffTeamMember,
72
- {
73
- tenantId: auth.tenantId,
74
- organizationId: selectedOrganizationId,
75
- deletedAt: null,
76
- isActive: true
77
- },
78
- { orderBy: { displayName: "asc" } },
79
- scope
80
- );
81
- const userIds = Array.from(
82
- new Set(
83
- members.map((member) => typeof member.userId === "string" && member.userId.trim().length > 0 ? member.userId : null).filter((value) => typeof value === "string")
84
- )
85
- );
86
- const teamIds = Array.from(
87
- new Set(
88
- members.map((member) => typeof member.teamId === "string" && member.teamId.trim().length > 0 ? member.teamId : null).filter((value) => typeof value === "string")
89
- )
90
- );
91
- const [users, teams] = await Promise.all([
92
- userIds.length > 0 ? findWithDecryption(
93
- em,
94
- User,
95
- {
96
- id: { $in: userIds },
97
- deletedAt: null,
98
- tenantId: auth.tenantId,
99
- organizationId: selectedOrganizationId
100
- },
101
- void 0,
102
- scope
103
- ) : Promise.resolve([]),
104
- teamIds.length > 0 ? findWithDecryption(
105
- em,
106
- StaffTeam,
107
- {
108
- id: { $in: teamIds },
109
- deletedAt: null,
110
- tenantId: auth.tenantId,
111
- organizationId: selectedOrganizationId
112
- },
113
- void 0,
114
- scope
115
- ) : Promise.resolve([])
116
- ]);
117
- const userById = new Map(
118
- users.map((user) => [
119
- user.id,
120
- {
121
- id: user.id,
122
- email: user.email ?? null
123
- }
124
- ])
125
- );
126
- const teamById = new Map(
127
- teams.map((team) => [
128
- team.id,
129
- {
130
- id: team.id,
131
- name: team.name ?? null
132
- }
133
- ])
134
- );
135
- const items = members.filter((member) => typeof member.userId === "string" && member.userId.trim().length > 0).map((member) => {
136
- const userId = member.userId;
137
- const user = userById.get(userId) ?? { id: userId, email: null };
138
- const team = member.teamId ? teamById.get(member.teamId) ?? null : null;
139
- return {
140
- id: member.id,
141
- teamMemberId: member.id,
142
- userId,
143
- displayName: member.displayName?.trim() || user.email || userId,
144
- email: user.email,
145
- teamName: team?.name ?? null,
146
- user,
147
- team
148
- };
149
- }).filter((item) => {
150
- if (!normalizedSearch) return true;
151
- const haystack = [item.displayName, item.email, item.teamName].filter((value) => typeof value === "string" && value.length > 0).join(" ").toLowerCase();
152
- return haystack.includes(normalizedSearch);
153
- });
154
- const deduped = Array.from(
155
- items.reduce((acc, item) => {
156
- if (!acc.has(item.userId)) {
157
- acc.set(item.userId, item);
158
- }
159
- return acc;
160
- }, /* @__PURE__ */ new Map())
161
- ).map(([, item]) => item);
162
- const start = (query.page - 1) * query.pageSize;
163
- return NextResponse.json({
164
- items: deduped.slice(start, start + query.pageSize),
165
- total: deduped.length,
166
- page: query.page,
167
- pageSize: query.pageSize
168
- });
169
- } catch (error) {
170
- if (isCrudHttpError(error)) {
171
- return NextResponse.json(error.body, { status: error.status });
172
- }
173
- if (error instanceof z.ZodError) {
174
- return NextResponse.json({ error: translate("customers.errors.validationFailed", "Validation failed") }, { status: 400 });
175
- }
176
- console.error("customers.assignable-staff.get failed", error);
177
- return NextResponse.json({ error: translate("customers.errors.assignable_staff_load_failed", "Failed to load assignable staff") }, { status: 500 });
178
- }
30
+ const url = new URL(request.url);
31
+ const target = new URL("/api/staff/team-members/assignable", url.origin);
32
+ target.search = url.search;
33
+ return NextResponse.redirect(target, 308);
179
34
  }
180
35
  const openApi = {
181
36
  tag: "Customers",
182
- summary: "Assignable staff candidates",
37
+ summary: "Assignable staff candidates (DEPRECATED \u2014 redirects to /api/staff/team-members/assignable)",
183
38
  methods: {
184
39
  GET: {
185
- summary: "List staff members that can be assigned from customer flows",
40
+ deprecated: true,
41
+ summary: "DEPRECATED: use GET /api/staff/team-members/assignable instead.",
186
42
  query: querySchema,
187
- description: "Returns active staff members linked to auth users. Access requires either customers.roles.manage or customers.activities.manage.",
43
+ description: "Deprecated. Returns 308 Permanent Redirect to /api/staff/team-members/assignable preserving the query string. Will be removed no earlier than the next major release.",
188
44
  responses: [
189
45
  {
190
46
  status: 200,
191
- description: "Assignable staff members",
47
+ description: "Assignable staff members (only reachable by following the redirect).",
192
48
  schema: createPagedListResponseSchema(itemSchema)
49
+ },
50
+ {
51
+ status: 308,
52
+ description: "Permanent redirect to /api/staff/team-members/assignable.",
53
+ schema: errorSchema
193
54
  }
194
55
  ],
195
56
  errors: [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/api/assignable-staff/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { StaffTeam, StaffTeamMember } from '@open-mercato/core/modules/staff/data/entities'\nimport { createPagedListResponseSchema } from '../openapi'\nimport { resolveAuthActorId, resolveCustomersRequestContext } from '../../lib/interactionRequestContext'\n\nconst querySchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(24),\n search: z.string().optional(),\n })\n .passthrough()\n\nconst itemSchema = z.object({\n id: z.string().uuid(),\n teamMemberId: z.string().uuid(),\n userId: z.string().uuid(),\n displayName: z.string(),\n email: z.string().nullable().optional(),\n teamName: z.string().nullable().optional(),\n user: z\n .object({\n id: z.string().uuid(),\n email: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n team: z\n .object({\n id: z.string().uuid(),\n name: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.roles.view'] },\n}\n\nasync function canAccessAssignableStaff(\n rbac: RbacService | undefined,\n userId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<boolean> {\n if (!rbac) return false\n if (\n await rbac.userHasAllFeatures(userId, ['customers.roles.manage'], scope)\n ) {\n return true\n }\n return rbac.userHasAllFeatures(userId, ['customers.activities.manage'], scope)\n}\n\nexport async function GET(request: Request) {\n const { translate } = await resolveTranslations()\n try {\n const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams))\n const { container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request)\n\n if (!selectedOrganizationId) {\n throw new CrudHttpError(\n 400,\n { error: translate('customers.errors.organization_required', 'Organization context is required') },\n )\n }\n\n const actorId = resolveAuthActorId(auth)\n const rbacService = container.resolve('rbacService') as RbacService | undefined\n const scope = { tenantId: auth.tenantId, organizationId: selectedOrganizationId }\n const hasAccess = await canAccessAssignableStaff(rbacService, actorId, scope)\n if (!hasAccess) {\n throw new CrudHttpError(\n 403,\n {\n error: translate(\n 'customers.assignableStaff.forbidden',\n 'Insufficient permissions to load assignable staff.',\n ),\n },\n )\n }\n\n const normalizedSearch = query.search?.trim().toLowerCase() ?? ''\n\n const members = await findWithDecryption(\n em,\n StaffTeamMember,\n {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n deletedAt: null,\n isActive: true,\n },\n { orderBy: { displayName: 'asc' } },\n scope,\n )\n\n const userIds = Array.from(\n new Set(\n members\n .map((member) => (typeof member.userId === 'string' && member.userId.trim().length > 0 ? member.userId : null))\n .filter((value): value is string => typeof value === 'string'),\n ),\n )\n const teamIds = Array.from(\n new Set(\n members\n .map((member) => (typeof member.teamId === 'string' && member.teamId.trim().length > 0 ? member.teamId : null))\n .filter((value): value is string => typeof value === 'string'),\n ),\n )\n\n const [users, teams] = await Promise.all([\n userIds.length > 0\n ? findWithDecryption(\n em,\n User,\n {\n id: { $in: userIds },\n deletedAt: null,\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n },\n undefined,\n scope,\n )\n : Promise.resolve([]),\n teamIds.length > 0\n ? findWithDecryption(\n em,\n StaffTeam,\n {\n id: { $in: teamIds },\n deletedAt: null,\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n },\n undefined,\n scope,\n )\n : Promise.resolve([]),\n ])\n\n const userById = new Map(\n users.map((user) => [\n user.id,\n {\n id: user.id,\n email: user.email ?? null,\n },\n ]),\n )\n const teamById = new Map(\n teams.map((team) => [\n team.id,\n {\n id: team.id,\n name: team.name ?? null,\n },\n ]),\n )\n\n const items = members\n .filter((member) => typeof member.userId === 'string' && member.userId.trim().length > 0)\n .map((member) => {\n const userId = member.userId as string\n const user = userById.get(userId) ?? { id: userId, email: null }\n const team = member.teamId ? teamById.get(member.teamId) ?? null : null\n return {\n id: member.id,\n teamMemberId: member.id,\n userId,\n displayName: member.displayName?.trim() || user.email || userId,\n email: user.email,\n teamName: team?.name ?? null,\n user,\n team,\n }\n })\n .filter((item) => {\n if (!normalizedSearch) return true\n const haystack = [item.displayName, item.email, item.teamName]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .join(' ')\n .toLowerCase()\n return haystack.includes(normalizedSearch)\n })\n\n const deduped = Array.from(\n items.reduce((acc, item) => {\n if (!acc.has(item.userId)) {\n acc.set(item.userId, item)\n }\n return acc\n }, new Map<string, (typeof items)[number]>()),\n ).map(([, item]) => item)\n\n const start = (query.page - 1) * query.pageSize\n return NextResponse.json({\n items: deduped.slice(start, start + query.pageSize),\n total: deduped.length,\n page: query.page,\n pageSize: query.pageSize,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n if (error instanceof z.ZodError) {\n return NextResponse.json({ error: translate('customers.errors.validationFailed', 'Validation failed') }, { status: 400 })\n }\n console.error('customers.assignable-staff.get failed', error)\n return NextResponse.json({ error: translate('customers.errors.assignable_staff_load_failed', 'Failed to load assignable staff') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Assignable staff candidates',\n methods: {\n GET: {\n summary: 'List staff members that can be assigned from customer flows',\n query: querySchema,\n description:\n 'Returns active staff members linked to auth users. Access requires either customers.roles.manage or customers.activities.manage.',\n responses: [\n {\n status: 200,\n description: 'Assignable staff members',\n schema: createPagedListResponseSchema(itemSchema),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createPagedListResponseSchema } from '../openapi'\n\nconst querySchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(24),\n search: z.string().optional(),\n })\n .passthrough()\n\nconst itemSchema = z.object({\n id: z.string().uuid(),\n teamMemberId: z.string().uuid(),\n userId: z.string().uuid(),\n displayName: z.string(),\n email: z.string().nullable().optional(),\n teamName: z.string().nullable().optional(),\n user: z\n .object({\n id: z.string().uuid(),\n email: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n team: z\n .object({\n id: z.string().uuid(),\n name: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.roles.view'] },\n}\n\nexport async function GET(request: Request) {\n const url = new URL(request.url)\n const target = new URL('/api/staff/team-members/assignable', url.origin)\n target.search = url.search\n return NextResponse.redirect(target, 308)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Assignable staff candidates (DEPRECATED \u2014 redirects to /api/staff/team-members/assignable)',\n methods: {\n GET: {\n deprecated: true,\n summary: 'DEPRECATED: use GET /api/staff/team-members/assignable instead.',\n query: querySchema,\n description:\n 'Deprecated. Returns 308 Permanent Redirect to /api/staff/team-members/assignable preserving the query string. Will be removed no earlier than the next major release.',\n responses: [\n {\n status: 200,\n description: 'Assignable staff members (only reachable by following the redirect).',\n schema: createPagedListResponseSchema(itemSchema),\n },\n {\n status: 308,\n description: 'Permanent redirect to /api/staff/team-members/assignable.',\n schema: errorSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,qCAAqC;AAE9C,MAAM,cAAc,EACjB,OAAO;AAAA,EACN,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAC9B,CAAC,EACA,YAAY;AAEf,MAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,cAAc,EAAE,OAAO,EAAE,KAAK;AAAA,EAC9B,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA,EACxB,aAAa,EAAE,OAAO;AAAA,EACtB,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACtC,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACzC,MAAM,EACH,OAAO;AAAA,IACN,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,IACpB,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACxC,CAAC,EACA,SAAS,EACT,SAAS;AAAA,EACZ,MAAM,EACH,OAAO;AAAA,IACN,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,IACpB,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,EACvC,CAAC,EACA,SAAS,EACT,SAAS;AACd,CAAC;AAED,MAAM,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAE3C,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAEA,eAAsB,IAAI,SAAkB;AAC1C,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,QAAM,SAAS,IAAI,IAAI,sCAAsC,IAAI,MAAM;AACvE,SAAO,SAAS,IAAI;AACpB,SAAO,aAAa,SAAS,QAAQ,GAAG;AAC1C;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,OAAO;AAAA,MACP,aACE;AAAA,MACF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,8BAA8B,UAAU;AAAA,QAClD;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,YAAY;AAAA,QACnE,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,YAAY;AAAA,QAChE,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,YAAY;AAAA,MAC/D;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -8,7 +8,7 @@ async function fetchAssignableStaffMembersPage(query, options) {
8
8
  params.set("search", normalizedQuery);
9
9
  }
10
10
  const data = await readApiResultOrThrow(
11
- `/api/customers/assignable-staff?${params.toString()}`,
11
+ `/api/staff/team-members/assignable?${params.toString()}`,
12
12
  options?.signal ? { signal: options.signal } : void 0
13
13
  );
14
14
  const rawItems = Array.isArray(data?.items) ? data.items : [];
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/assignableStaff.ts"],
4
- "sourcesContent": ["import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { FilterOption } from '@open-mercato/shared/lib/query/advanced-filter'\n\nexport type AssignableStaffMember = {\n teamMemberId: string\n userId: string\n displayName: string\n email: string | null\n teamName: string | null\n}\n\ntype AssignableStaffResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n page?: number\n pageSize?: number\n}\n\nexport type AssignableStaffMembersPage = {\n items: AssignableStaffMember[]\n total: number\n page: number\n pageSize: number\n}\n\nexport async function fetchAssignableStaffMembersPage(\n query: string,\n options?: { page?: number; pageSize?: number; signal?: AbortSignal },\n): Promise<AssignableStaffMembersPage> {\n const params = new URLSearchParams()\n params.set('page', String(options?.page ?? 1))\n params.set('pageSize', String(options?.pageSize ?? 24))\n const normalizedQuery = query.trim()\n if (normalizedQuery.length > 0) {\n params.set('search', normalizedQuery)\n }\n\n const data = await readApiResultOrThrow<AssignableStaffResponse>(\n `/api/customers/assignable-staff?${params.toString()}`,\n options?.signal ? { signal: options.signal } : undefined,\n )\n\n const rawItems = Array.isArray(data?.items) ? data.items : []\n const deduped = new Map<string, AssignableStaffMember>()\n\n for (const item of rawItems) {\n const userId =\n typeof item?.userId === 'string'\n ? item.userId\n : typeof item?.user_id === 'string'\n ? item.user_id\n : null\n if (!userId || deduped.has(userId)) continue\n\n const user =\n item?.user && typeof item.user === 'object'\n ? (item.user as Record<string, unknown>)\n : null\n const team =\n item?.team && typeof item.team === 'object'\n ? (item.team as Record<string, unknown>)\n : null\n\n const displayName =\n typeof item?.displayName === 'string' && item.displayName.trim().length > 0\n ? item.displayName.trim()\n : typeof item?.display_name === 'string' && item.display_name.trim().length > 0\n ? item.display_name.trim()\n : null\n const email =\n user && typeof user.email === 'string' && user.email.trim().length > 0\n ? user.email.trim()\n : typeof item?.email === 'string' && item.email.trim().length > 0\n ? item.email.trim()\n : null\n const teamName =\n typeof item?.teamName === 'string' && item.teamName.trim().length > 0\n ? item.teamName.trim()\n : typeof item?.team_name === 'string' && item.team_name.trim().length > 0\n ? item.team_name.trim()\n : team && typeof team.name === 'string' && team.name.trim().length > 0\n ? team.name.trim()\n : null\n const teamMemberId =\n typeof item?.teamMemberId === 'string'\n ? item.teamMemberId\n : typeof item?.team_member_id === 'string'\n ? item.team_member_id\n : typeof item?.id === 'string'\n ? item.id\n : userId\n\n deduped.set(userId, {\n teamMemberId,\n userId,\n displayName: displayName ?? email ?? userId,\n email,\n teamName,\n })\n }\n\n return {\n items: Array.from(deduped.values()),\n total:\n typeof data?.total === 'number' && Number.isFinite(data.total)\n ? data.total\n : deduped.size,\n page:\n typeof data?.page === 'number' && Number.isFinite(data.page)\n ? data.page\n : options?.page ?? 1,\n pageSize:\n typeof data?.pageSize === 'number' && Number.isFinite(data.pageSize)\n ? data.pageSize\n : options?.pageSize ?? 24,\n }\n}\n\nexport async function fetchAssignableStaffMembers(\n query: string,\n options?: { pageSize?: number; signal?: AbortSignal },\n): Promise<AssignableStaffMember[]> {\n const result = await fetchAssignableStaffMembersPage(query, options)\n return result.items\n}\n\nexport function mapAssignableStaffToFilterOptions(items: AssignableStaffMember[]): FilterOption[] {\n return items.map((item) => ({\n value: item.userId,\n label: item.email && item.email !== item.displayName\n ? `${item.displayName} (${item.email})`\n : item.displayName,\n tone: 'neutral',\n }))\n}\n\nexport function ensureCurrentUserFilterOption(\n options: FilterOption[],\n currentUserId: string,\n fallbackLabel: string,\n): FilterOption[] {\n const trimmed = currentUserId.trim()\n if (!trimmed || options.some((option) => option.value === trimmed)) return options\n return [{ value: trimmed, label: fallbackLabel, tone: 'neutral' }, ...options]\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { FilterOption } from '@open-mercato/shared/lib/query/advanced-filter'\n\nexport type AssignableStaffMember = {\n teamMemberId: string\n userId: string\n displayName: string\n email: string | null\n teamName: string | null\n}\n\ntype AssignableStaffResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n page?: number\n pageSize?: number\n}\n\nexport type AssignableStaffMembersPage = {\n items: AssignableStaffMember[]\n total: number\n page: number\n pageSize: number\n}\n\nexport async function fetchAssignableStaffMembersPage(\n query: string,\n options?: { page?: number; pageSize?: number; signal?: AbortSignal },\n): Promise<AssignableStaffMembersPage> {\n const params = new URLSearchParams()\n params.set('page', String(options?.page ?? 1))\n params.set('pageSize', String(options?.pageSize ?? 24))\n const normalizedQuery = query.trim()\n if (normalizedQuery.length > 0) {\n params.set('search', normalizedQuery)\n }\n\n const data = await readApiResultOrThrow<AssignableStaffResponse>(\n `/api/staff/team-members/assignable?${params.toString()}`,\n options?.signal ? { signal: options.signal } : undefined,\n )\n\n const rawItems = Array.isArray(data?.items) ? data.items : []\n const deduped = new Map<string, AssignableStaffMember>()\n\n for (const item of rawItems) {\n const userId =\n typeof item?.userId === 'string'\n ? item.userId\n : typeof item?.user_id === 'string'\n ? item.user_id\n : null\n if (!userId || deduped.has(userId)) continue\n\n const user =\n item?.user && typeof item.user === 'object'\n ? (item.user as Record<string, unknown>)\n : null\n const team =\n item?.team && typeof item.team === 'object'\n ? (item.team as Record<string, unknown>)\n : null\n\n const displayName =\n typeof item?.displayName === 'string' && item.displayName.trim().length > 0\n ? item.displayName.trim()\n : typeof item?.display_name === 'string' && item.display_name.trim().length > 0\n ? item.display_name.trim()\n : null\n const email =\n user && typeof user.email === 'string' && user.email.trim().length > 0\n ? user.email.trim()\n : typeof item?.email === 'string' && item.email.trim().length > 0\n ? item.email.trim()\n : null\n const teamName =\n typeof item?.teamName === 'string' && item.teamName.trim().length > 0\n ? item.teamName.trim()\n : typeof item?.team_name === 'string' && item.team_name.trim().length > 0\n ? item.team_name.trim()\n : team && typeof team.name === 'string' && team.name.trim().length > 0\n ? team.name.trim()\n : null\n const teamMemberId =\n typeof item?.teamMemberId === 'string'\n ? item.teamMemberId\n : typeof item?.team_member_id === 'string'\n ? item.team_member_id\n : typeof item?.id === 'string'\n ? item.id\n : userId\n\n deduped.set(userId, {\n teamMemberId,\n userId,\n displayName: displayName ?? email ?? userId,\n email,\n teamName,\n })\n }\n\n return {\n items: Array.from(deduped.values()),\n total:\n typeof data?.total === 'number' && Number.isFinite(data.total)\n ? data.total\n : deduped.size,\n page:\n typeof data?.page === 'number' && Number.isFinite(data.page)\n ? data.page\n : options?.page ?? 1,\n pageSize:\n typeof data?.pageSize === 'number' && Number.isFinite(data.pageSize)\n ? data.pageSize\n : options?.pageSize ?? 24,\n }\n}\n\nexport async function fetchAssignableStaffMembers(\n query: string,\n options?: { pageSize?: number; signal?: AbortSignal },\n): Promise<AssignableStaffMember[]> {\n const result = await fetchAssignableStaffMembersPage(query, options)\n return result.items\n}\n\nexport function mapAssignableStaffToFilterOptions(items: AssignableStaffMember[]): FilterOption[] {\n return items.map((item) => ({\n value: item.userId,\n label: item.email && item.email !== item.displayName\n ? `${item.displayName} (${item.email})`\n : item.displayName,\n tone: 'neutral',\n }))\n}\n\nexport function ensureCurrentUserFilterOption(\n options: FilterOption[],\n currentUserId: string,\n fallbackLabel: string,\n): FilterOption[] {\n const trimmed = currentUserId.trim()\n if (!trimmed || options.some((option) => option.value === trimmed)) return options\n return [{ value: trimmed, label: fallbackLabel, tone: 'neutral' }, ...options]\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,71 +1,38 @@
1
1
  import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
2
- import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
- import { StaffTeamMember } from "@open-mercato/core/modules/staff/data/entities";
4
- const MANAGE_AVAILABILITY_FEATURE = "planner.manage_availability";
5
- const SELF_MANAGE_FEATURE = "staff.my_availability.manage";
6
- const SELF_UNAVAILABILITY_FEATURE = "staff.my_availability.unavailability";
7
2
  function buildForbiddenError(translate) {
8
- return new CrudHttpError(403, { error: translate("planner.availability.errors.unauthorized", "Unauthorized") });
3
+ return new CrudHttpError(403, {
4
+ error: translate("planner.availability.errors.unauthorized", "Unauthorized")
5
+ });
6
+ }
7
+ function buildStaffModuleNotLoadedError() {
8
+ return new CrudHttpError(403, { error: "staff_module_not_loaded" });
9
9
  }
10
10
  async function resolveAvailabilityWriteAccess(ctx) {
11
- const auth = ctx.auth;
12
- const tenantId = auth?.tenantId ?? null;
13
- const organizationId = ctx.selectedOrganizationId ?? auth?.orgId ?? null;
14
- if (!auth || !auth.sub || auth.isApiKey) {
15
- return {
16
- canManageAll: false,
17
- canManageSelf: false,
18
- canManageUnavailability: false,
19
- memberId: null,
20
- tenantId,
21
- organizationId
22
- };
23
- }
24
- const rbac = ctx.container.resolve("rbacService");
25
- const canManageAll = await rbac.userHasAllFeatures(auth.sub, [MANAGE_AVAILABILITY_FEATURE], { tenantId, organizationId });
26
- if (canManageAll) {
27
- return {
28
- canManageAll: true,
29
- canManageSelf: true,
30
- canManageUnavailability: true,
31
- memberId: null,
32
- tenantId,
33
- organizationId
34
- };
35
- }
36
- const [canManageSelf, canManageUnavailability] = await Promise.all([
37
- rbac.userHasAllFeatures(auth.sub, [SELF_MANAGE_FEATURE], { tenantId, organizationId }),
38
- rbac.userHasAllFeatures(auth.sub, [SELF_UNAVAILABILITY_FEATURE], { tenantId, organizationId })
39
- ]);
40
- if (!canManageSelf) {
11
+ const tenantId = ctx.auth?.tenantId ?? null;
12
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
13
+ const resolver = ctx.container.resolve(
14
+ "availabilityAccessResolver",
15
+ { allowUnregistered: true }
16
+ );
17
+ if (!resolver) {
18
+ console.warn(
19
+ "[planner] staff_module_not_loaded \u2014 availabilityAccessResolver unregistered; denying availability write access"
20
+ );
41
21
  return {
42
22
  canManageAll: false,
43
23
  canManageSelf: false,
44
24
  canManageUnavailability: false,
45
25
  memberId: null,
46
26
  tenantId,
47
- organizationId
27
+ organizationId,
28
+ unregistered: true
48
29
  };
49
30
  }
50
- const em = ctx.container.resolve("em");
51
- const member = await findOneWithDecryption(
52
- em,
53
- StaffTeamMember,
54
- { userId: auth.sub, deletedAt: null },
55
- void 0,
56
- { tenantId, organizationId }
57
- );
58
- return {
59
- canManageAll: false,
60
- canManageSelf,
61
- canManageUnavailability,
62
- memberId: member?.id ?? null,
63
- tenantId,
64
- organizationId
65
- };
31
+ return resolver.resolveAvailabilityWriteAccess(ctx);
66
32
  }
67
33
  async function assertAvailabilityWriteAccess(ctx, params, translate) {
68
34
  const access = await resolveAvailabilityWriteAccess(ctx);
35
+ if (access.unregistered) throw buildStaffModuleNotLoadedError();
69
36
  if (access.canManageAll) return access;
70
37
  if (!access.canManageSelf) throw buildForbiddenError(translate);
71
38
  if (!access.memberId || params.subjectType !== "member" || params.subjectId !== access.memberId) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/planner/api/access.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { StaffTeamMember } from '@open-mercato/core/modules/staff/data/entities'\n\ntype TranslateFn = (key: string, fallback?: string) => string\n\nexport type AvailabilityAccessContext = {\n container: AwilixContainer\n auth: AuthContext | null\n selectedOrganizationId?: string | null\n}\n\nexport type AvailabilityWriteAccess = {\n canManageAll: boolean\n canManageSelf: boolean\n canManageUnavailability: boolean\n memberId: string | null\n tenantId: string | null\n organizationId: string | null\n}\n\nconst MANAGE_AVAILABILITY_FEATURE = 'planner.manage_availability'\nconst SELF_MANAGE_FEATURE = 'staff.my_availability.manage'\nconst SELF_UNAVAILABILITY_FEATURE = 'staff.my_availability.unavailability'\n\nfunction buildForbiddenError(translate: TranslateFn) {\n return new CrudHttpError(403, { error: translate('planner.availability.errors.unauthorized', 'Unauthorized') })\n}\n\nexport async function resolveAvailabilityWriteAccess(ctx: AvailabilityAccessContext): Promise<AvailabilityWriteAccess> {\n const auth = ctx.auth\n const tenantId = auth?.tenantId ?? null\n const organizationId = ctx.selectedOrganizationId ?? auth?.orgId ?? null\n if (!auth || !auth.sub || auth.isApiKey) {\n return {\n canManageAll: false,\n canManageSelf: false,\n canManageUnavailability: false,\n memberId: null,\n tenantId,\n organizationId,\n }\n }\n const rbac = ctx.container.resolve('rbacService') as RbacService\n const canManageAll = await rbac.userHasAllFeatures(auth.sub, [MANAGE_AVAILABILITY_FEATURE], { tenantId, organizationId })\n if (canManageAll) {\n return {\n canManageAll: true,\n canManageSelf: true,\n canManageUnavailability: true,\n memberId: null,\n tenantId,\n organizationId,\n }\n }\n const [canManageSelf, canManageUnavailability] = await Promise.all([\n rbac.userHasAllFeatures(auth.sub, [SELF_MANAGE_FEATURE], { tenantId, organizationId }),\n rbac.userHasAllFeatures(auth.sub, [SELF_UNAVAILABILITY_FEATURE], { tenantId, organizationId }),\n ])\n if (!canManageSelf) {\n return {\n canManageAll: false,\n canManageSelf: false,\n canManageUnavailability: false,\n memberId: null,\n tenantId,\n organizationId,\n }\n }\n const em = ctx.container.resolve('em') as EntityManager\n const member = await findOneWithDecryption(\n em,\n StaffTeamMember,\n { userId: auth.sub, deletedAt: null },\n undefined,\n { tenantId, organizationId },\n )\n return {\n canManageAll: false,\n canManageSelf,\n canManageUnavailability,\n memberId: member?.id ?? null,\n tenantId,\n organizationId,\n }\n}\n\nexport async function assertAvailabilityWriteAccess(\n ctx: AvailabilityAccessContext,\n params: { subjectType: string; subjectId: string; requiresUnavailability?: boolean },\n translate: TranslateFn,\n): Promise<AvailabilityWriteAccess> {\n const access = await resolveAvailabilityWriteAccess(ctx)\n if (access.canManageAll) return access\n if (!access.canManageSelf) throw buildForbiddenError(translate)\n if (!access.memberId || params.subjectType !== 'member' || params.subjectId !== access.memberId) {\n throw buildForbiddenError(translate)\n }\n if (params.requiresUnavailability && !access.canManageUnavailability) {\n throw buildForbiddenError(translate)\n }\n return access\n}\n"],
5
- "mappings": "AAIA,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AACtC,SAAS,uBAAuB;AAmBhC,MAAM,8BAA8B;AACpC,MAAM,sBAAsB;AAC5B,MAAM,8BAA8B;AAEpC,SAAS,oBAAoB,WAAwB;AACnD,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,4CAA4C,cAAc,EAAE,CAAC;AAChH;AAEA,eAAsB,+BAA+B,KAAkE;AACrH,QAAM,OAAO,IAAI;AACjB,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,iBAAiB,IAAI,0BAA0B,MAAM,SAAS;AACpE,MAAI,CAAC,QAAQ,CAAC,KAAK,OAAO,KAAK,UAAU;AACvC,WAAO;AAAA,MACL,cAAc;AAAA,MACd,eAAe;AAAA,MACf,yBAAyB;AAAA,MACzB,UAAU;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,IAAI,UAAU,QAAQ,aAAa;AAChD,QAAM,eAAe,MAAM,KAAK,mBAAmB,KAAK,KAAK,CAAC,2BAA2B,GAAG,EAAE,UAAU,eAAe,CAAC;AACxH,MAAI,cAAc;AAChB,WAAO;AAAA,MACL,cAAc;AAAA,MACd,eAAe;AAAA,MACf,yBAAyB;AAAA,MACzB,UAAU;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,CAAC,eAAe,uBAAuB,IAAI,MAAM,QAAQ,IAAI;AAAA,IACjE,KAAK,mBAAmB,KAAK,KAAK,CAAC,mBAAmB,GAAG,EAAE,UAAU,eAAe,CAAC;AAAA,IACrF,KAAK,mBAAmB,KAAK,KAAK,CAAC,2BAA2B,GAAG,EAAE,UAAU,eAAe,CAAC;AAAA,EAC/F,CAAC;AACD,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,MACL,cAAc;AAAA,MACd,eAAe;AAAA,MACf,yBAAyB;AAAA,MACzB,UAAU;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AACrC,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA,EAAE,QAAQ,KAAK,KAAK,WAAW,KAAK;AAAA,IACpC;AAAA,IACA,EAAE,UAAU,eAAe;AAAA,EAC7B;AACA,SAAO;AAAA,IACL,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA,UAAU,QAAQ,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAsB,8BACpB,KACA,QACA,WACkC;AAClC,QAAM,SAAS,MAAM,+BAA+B,GAAG;AACvD,MAAI,OAAO,aAAc,QAAO;AAChC,MAAI,CAAC,OAAO,cAAe,OAAM,oBAAoB,SAAS;AAC9D,MAAI,CAAC,OAAO,YAAY,OAAO,gBAAgB,YAAY,OAAO,cAAc,OAAO,UAAU;AAC/F,UAAM,oBAAoB,SAAS;AAAA,EACrC;AACA,MAAI,OAAO,0BAA0B,CAAC,OAAO,yBAAyB;AACpE,UAAM,oBAAoB,SAAS;AAAA,EACrC;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\n\ntype TranslateFn = (key: string, fallback?: string) => string\n\nexport type AvailabilityAccessContext = {\n container: AwilixContainer\n auth: AuthContext | null\n selectedOrganizationId?: string | null\n}\n\nexport type AvailabilityWriteAccess = {\n canManageAll: boolean\n canManageSelf: boolean\n canManageUnavailability: boolean\n memberId: string | null\n tenantId: string | null\n organizationId: string | null\n unregistered?: boolean\n}\n\ntype AvailabilityAccessResolver = {\n resolveAvailabilityWriteAccess(\n ctx: AvailabilityAccessContext,\n ): Promise<AvailabilityWriteAccess>\n}\n\nfunction buildForbiddenError(translate: TranslateFn) {\n return new CrudHttpError(403, {\n error: translate('planner.availability.errors.unauthorized', 'Unauthorized'),\n })\n}\n\nfunction buildStaffModuleNotLoadedError() {\n return new CrudHttpError(403, { error: 'staff_module_not_loaded' })\n}\n\nexport async function resolveAvailabilityWriteAccess(\n ctx: AvailabilityAccessContext,\n): Promise<AvailabilityWriteAccess> {\n const tenantId = ctx.auth?.tenantId ?? null\n const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null\n const resolver = ctx.container.resolve<AvailabilityAccessResolver | undefined>(\n 'availabilityAccessResolver',\n { allowUnregistered: true },\n )\n if (!resolver) {\n console.warn(\n '[planner] staff_module_not_loaded \u2014 availabilityAccessResolver unregistered; denying availability write access',\n )\n return {\n canManageAll: false,\n canManageSelf: false,\n canManageUnavailability: false,\n memberId: null,\n tenantId,\n organizationId,\n unregistered: true,\n }\n }\n return resolver.resolveAvailabilityWriteAccess(ctx)\n}\n\nexport async function assertAvailabilityWriteAccess(\n ctx: AvailabilityAccessContext,\n params: { subjectType: string; subjectId: string; requiresUnavailability?: boolean },\n translate: TranslateFn,\n): Promise<AvailabilityWriteAccess> {\n const access = await resolveAvailabilityWriteAccess(ctx)\n if (access.unregistered) throw buildStaffModuleNotLoadedError()\n if (access.canManageAll) return access\n if (!access.canManageSelf) throw buildForbiddenError(translate)\n if (!access.memberId || params.subjectType !== 'member' || params.subjectId !== access.memberId) {\n throw buildForbiddenError(translate)\n }\n if (params.requiresUnavailability && !access.canManageUnavailability) {\n throw buildForbiddenError(translate)\n }\n return access\n}\n"],
5
+ "mappings": "AAEA,SAAS,qBAAqB;AA0B9B,SAAS,oBAAoB,WAAwB;AACnD,SAAO,IAAI,cAAc,KAAK;AAAA,IAC5B,OAAO,UAAU,4CAA4C,cAAc;AAAA,EAC7E,CAAC;AACH;AAEA,SAAS,iCAAiC;AACxC,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,0BAA0B,CAAC;AACpE;AAEA,eAAsB,+BACpB,KACkC;AAClC,QAAM,WAAW,IAAI,MAAM,YAAY;AACvC,QAAM,iBAAiB,IAAI,0BAA0B,IAAI,MAAM,SAAS;AACxE,QAAM,WAAW,IAAI,UAAU;AAAA,IAC7B;AAAA,IACA,EAAE,mBAAmB,KAAK;AAAA,EAC5B;AACA,MAAI,CAAC,UAAU;AACb,YAAQ;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,MACL,cAAc;AAAA,MACd,eAAe;AAAA,MACf,yBAAyB;AAAA,MACzB,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA,cAAc;AAAA,IAChB;AAAA,EACF;AACA,SAAO,SAAS,+BAA+B,GAAG;AACpD;AAEA,eAAsB,8BACpB,KACA,QACA,WACkC;AAClC,QAAM,SAAS,MAAM,+BAA+B,GAAG;AACvD,MAAI,OAAO,aAAc,OAAM,+BAA+B;AAC9D,MAAI,OAAO,aAAc,QAAO;AAChC,MAAI,CAAC,OAAO,cAAe,OAAM,oBAAoB,SAAS;AAC9D,MAAI,CAAC,OAAO,YAAY,OAAO,gBAAgB,YAAY,OAAO,cAAc,OAAO,UAAU;AAC/F,UAAM,oBAAoB,SAAS;AAAA,EACrC;AACA,MAAI,OAAO,0BAA0B,CAAC,OAAO,yBAAyB;AACpE,UAAM,oBAAoB,SAAS;AAAA,EACrC;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,214 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
4
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
5
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
6
+ import { createPagedListResponseSchema as createSharedPagedListResponseSchema } from "@open-mercato/shared/lib/openapi/crud";
7
+ import { User } from "@open-mercato/core/modules/auth/data/entities";
8
+ import {
9
+ resolveAuthActorId,
10
+ resolveCustomersRequestContext
11
+ } from "@open-mercato/core/modules/customers/lib/interactionRequestContext";
12
+ import { StaffTeam, StaffTeamMember } from "../../../data/entities.js";
13
+ const querySchema = z.object({
14
+ page: z.coerce.number().min(1).default(1),
15
+ pageSize: z.coerce.number().min(1).max(100).default(24),
16
+ search: z.string().optional()
17
+ }).passthrough();
18
+ const itemSchema = z.object({
19
+ id: z.string().uuid(),
20
+ teamMemberId: z.string().uuid(),
21
+ userId: z.string().uuid(),
22
+ displayName: z.string(),
23
+ email: z.string().nullable().optional(),
24
+ teamName: z.string().nullable().optional(),
25
+ user: z.object({
26
+ id: z.string().uuid(),
27
+ email: z.string().nullable().optional()
28
+ }).nullable().optional(),
29
+ team: z.object({
30
+ id: z.string().uuid(),
31
+ name: z.string().nullable().optional()
32
+ }).nullable().optional()
33
+ });
34
+ const errorSchema = z.object({ error: z.string() });
35
+ const pagedListSchema = createSharedPagedListResponseSchema(itemSchema, {
36
+ paginationMetaOptional: true
37
+ });
38
+ const metadata = {
39
+ GET: { requireAuth: true, requireFeatures: ["customers.roles.view"] }
40
+ };
41
+ async function canAccessAssignableStaff(rbac, userId, scope) {
42
+ if (!rbac) return false;
43
+ if (await rbac.userHasAllFeatures(userId, ["customers.roles.manage"], scope)) {
44
+ return true;
45
+ }
46
+ return rbac.userHasAllFeatures(userId, ["customers.activities.manage"], scope);
47
+ }
48
+ async function GET(request) {
49
+ const { translate } = await resolveTranslations();
50
+ try {
51
+ const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams));
52
+ const { container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request);
53
+ if (!selectedOrganizationId) {
54
+ throw new CrudHttpError(
55
+ 400,
56
+ { error: translate("customers.errors.organization_required", "Organization context is required") }
57
+ );
58
+ }
59
+ const actorId = resolveAuthActorId(auth);
60
+ const rbacService = container.resolve("rbacService");
61
+ const scope = { tenantId: auth.tenantId, organizationId: selectedOrganizationId };
62
+ const hasAccess = await canAccessAssignableStaff(rbacService, actorId, scope);
63
+ if (!hasAccess) {
64
+ throw new CrudHttpError(
65
+ 403,
66
+ {
67
+ error: translate(
68
+ "customers.assignableStaff.forbidden",
69
+ "Insufficient permissions to load assignable staff."
70
+ )
71
+ }
72
+ );
73
+ }
74
+ const normalizedSearch = query.search?.trim().toLowerCase() ?? "";
75
+ const members = await findWithDecryption(
76
+ em,
77
+ StaffTeamMember,
78
+ {
79
+ tenantId: auth.tenantId,
80
+ organizationId: selectedOrganizationId,
81
+ deletedAt: null,
82
+ isActive: true
83
+ },
84
+ { orderBy: { displayName: "asc" } },
85
+ scope
86
+ );
87
+ const userIds = Array.from(
88
+ new Set(
89
+ members.map((member) => typeof member.userId === "string" && member.userId.trim().length > 0 ? member.userId : null).filter((value) => typeof value === "string")
90
+ )
91
+ );
92
+ const teamIds = Array.from(
93
+ new Set(
94
+ members.map((member) => typeof member.teamId === "string" && member.teamId.trim().length > 0 ? member.teamId : null).filter((value) => typeof value === "string")
95
+ )
96
+ );
97
+ const [users, teams] = await Promise.all([
98
+ userIds.length > 0 ? findWithDecryption(
99
+ em,
100
+ User,
101
+ {
102
+ id: { $in: userIds },
103
+ deletedAt: null,
104
+ tenantId: auth.tenantId,
105
+ organizationId: selectedOrganizationId
106
+ },
107
+ void 0,
108
+ scope
109
+ ) : Promise.resolve([]),
110
+ teamIds.length > 0 ? findWithDecryption(
111
+ em,
112
+ StaffTeam,
113
+ {
114
+ id: { $in: teamIds },
115
+ deletedAt: null,
116
+ tenantId: auth.tenantId,
117
+ organizationId: selectedOrganizationId
118
+ },
119
+ void 0,
120
+ scope
121
+ ) : Promise.resolve([])
122
+ ]);
123
+ const userById = new Map(
124
+ users.map((user) => [
125
+ user.id,
126
+ {
127
+ id: user.id,
128
+ email: user.email ?? null
129
+ }
130
+ ])
131
+ );
132
+ const teamById = new Map(
133
+ teams.map((team) => [
134
+ team.id,
135
+ {
136
+ id: team.id,
137
+ name: team.name ?? null
138
+ }
139
+ ])
140
+ );
141
+ const items = members.filter((member) => typeof member.userId === "string" && member.userId.trim().length > 0).map((member) => {
142
+ const userId = member.userId;
143
+ const user = userById.get(userId) ?? { id: userId, email: null };
144
+ const team = member.teamId ? teamById.get(member.teamId) ?? null : null;
145
+ return {
146
+ id: member.id,
147
+ teamMemberId: member.id,
148
+ userId,
149
+ displayName: member.displayName?.trim() || user.email || userId,
150
+ email: user.email,
151
+ teamName: team?.name ?? null,
152
+ user,
153
+ team
154
+ };
155
+ }).filter((item) => {
156
+ if (!normalizedSearch) return true;
157
+ const haystack = [item.displayName, item.email, item.teamName].filter((value) => typeof value === "string" && value.length > 0).join(" ").toLowerCase();
158
+ return haystack.includes(normalizedSearch);
159
+ });
160
+ const deduped = Array.from(
161
+ items.reduce((acc, item) => {
162
+ if (!acc.has(item.userId)) {
163
+ acc.set(item.userId, item);
164
+ }
165
+ return acc;
166
+ }, /* @__PURE__ */ new Map())
167
+ ).map(([, item]) => item);
168
+ const start = (query.page - 1) * query.pageSize;
169
+ return NextResponse.json({
170
+ items: deduped.slice(start, start + query.pageSize),
171
+ total: deduped.length,
172
+ page: query.page,
173
+ pageSize: query.pageSize
174
+ });
175
+ } catch (error) {
176
+ if (isCrudHttpError(error)) {
177
+ return NextResponse.json(error.body, { status: error.status });
178
+ }
179
+ if (error instanceof z.ZodError) {
180
+ return NextResponse.json({ error: translate("customers.errors.validationFailed", "Validation failed") }, { status: 400 });
181
+ }
182
+ console.error("staff.assignable-team-members.get failed", error);
183
+ return NextResponse.json({ error: translate("customers.errors.assignable_staff_load_failed", "Failed to load assignable staff") }, { status: 500 });
184
+ }
185
+ }
186
+ const openApi = {
187
+ tag: "Staff",
188
+ summary: "Assignable staff candidates",
189
+ methods: {
190
+ GET: {
191
+ summary: "List staff members that can be assigned from customer flows",
192
+ query: querySchema,
193
+ description: "Returns active staff members linked to auth users. Access requires either customers.roles.manage or customers.activities.manage. Owned by the staff module; consumed from customer flows via this canonical URL. Replaces the deprecated /api/customers/assignable-staff route.",
194
+ responses: [
195
+ {
196
+ status: 200,
197
+ description: "Assignable staff members",
198
+ schema: pagedListSchema
199
+ }
200
+ ],
201
+ errors: [
202
+ { status: 400, description: "Invalid request", schema: errorSchema },
203
+ { status: 401, description: "Unauthorized", schema: errorSchema },
204
+ { status: 403, description: "Forbidden", schema: errorSchema }
205
+ ]
206
+ }
207
+ }
208
+ };
209
+ export {
210
+ GET,
211
+ metadata,
212
+ openApi
213
+ };
214
+ //# sourceMappingURL=route.js.map