@open-mercato/core 0.6.8-develop.6950.1.912d574d60 → 0.6.8-develop.6958.1.6696e8db69

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.
@@ -2,11 +2,12 @@ import { NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
- import { UserConsent } from "@open-mercato/core/modules/auth/data/entities";
5
+ import { User, UserConsent } from "@open-mercato/core/modules/auth/data/entities";
6
6
  import { verifyConsentIntegrityHash } from "@open-mercato/core/modules/auth/lib/consentIntegrity";
7
7
  import { assertActorCanAccessUserTarget } from "@open-mercato/core/modules/auth/lib/grantChecks";
8
8
  import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
9
- import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
9
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
10
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
10
11
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
11
12
  const metadata = {
12
13
  path: "/auth/users/consents",
@@ -30,17 +31,28 @@ async function GET(req) {
30
31
  }
31
32
  const container = await createRequestContainer();
32
33
  const em = container.resolve("em");
34
+ const rbacService = container.resolve("rbacService");
33
35
  const tenantId = auth.tenantId ?? null;
34
36
  const organizationId = auth.orgId ?? null;
37
+ const actorAcl = auth.sub ? await rbacService.loadAcl(auth.sub, { tenantId, organizationId }) : null;
38
+ const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin;
39
+ if (!actorIsSuperAdmin && !tenantId) {
40
+ const { translate } = await resolveTranslations();
41
+ return NextResponse.json({
42
+ ok: false,
43
+ error: translate("auth.users.consents.errors.tenantContextRequired", "Tenant context is required")
44
+ }, { status: 403 });
45
+ }
35
46
  if (auth.sub) {
36
47
  try {
37
48
  await assertActorCanAccessUserTarget({
38
49
  em,
39
- rbacService: container.resolve("rbacService"),
50
+ rbacService,
40
51
  actorUserId: auth.sub,
41
52
  tenantId,
42
53
  organizationId,
43
54
  targetUserId: parsed.data.userId,
55
+ actorIsSuperAdmin,
44
56
  organizationScope: await resolveOrganizationScopeForRequest({
45
57
  container,
46
58
  auth,
@@ -53,17 +65,29 @@ async function GET(req) {
53
65
  throw err;
54
66
  }
55
67
  }
68
+ let scopeTenantId = tenantId;
69
+ if (!scopeTenantId) {
70
+ const target = await findOneWithDecryption(
71
+ em,
72
+ User,
73
+ { id: parsed.data.userId },
74
+ {},
75
+ { tenantId: null, organizationId: null }
76
+ );
77
+ if (!target) return NextResponse.json({ ok: true, items: [] });
78
+ scopeTenantId = target.tenantId ?? null;
79
+ }
56
80
  const consents = await findWithDecryption(
57
81
  em,
58
82
  UserConsent,
59
83
  {
60
84
  userId: parsed.data.userId,
61
85
  deletedAt: null,
62
- ...tenantId ? { tenantId } : {},
86
+ tenantId: scopeTenantId,
63
87
  ...organizationId ? { organizationId } : {}
64
88
  },
65
89
  { orderBy: { createdAt: "DESC" } },
66
- { tenantId, organizationId }
90
+ { tenantId: scopeTenantId, organizationId }
67
91
  );
68
92
  const items = consents.map((c) => ({
69
93
  id: c.id,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/api/users/consents/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { UserConsent } from '@open-mercato/core/modules/auth/data/entities'\nimport { verifyConsentIntegrityHash } from '@open-mercato/core/modules/auth/lib/consentIntegrity'\nimport { assertActorCanAccessUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { ConsentItem } from '@open-mercato/core/modules/auth/lib/consentTypes'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nexport const metadata = {\n path: '/auth/users/consents',\n GET: {\n requireAuth: true,\n requireFeatures: ['auth.users.edit'],\n },\n}\n\nconst querySchema = z.object({\n userId: z.string().uuid(),\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = querySchema.safeParse({ userId: url.searchParams.get('userId') })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid userId' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n if (auth.sub) {\n try {\n await assertActorCanAccessUserTarget({\n em,\n rbacService: container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId,\n organizationId,\n targetUserId: parsed.data.userId,\n organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId,\n }),\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n const consents = await findWithDecryption(\n em,\n UserConsent,\n {\n userId: parsed.data.userId,\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n },\n { orderBy: { createdAt: 'DESC' } },\n { tenantId, organizationId },\n )\n\n const items: ConsentItem[] = consents.map((c) => ({\n id: c.id,\n consentType: c.consentType,\n isGranted: c.isGranted,\n grantedAt: c.grantedAt?.toISOString() ?? null,\n withdrawnAt: c.withdrawnAt?.toISOString() ?? null,\n source: c.source ?? null,\n ipAddress: c.ipAddress ?? null,\n integrityValid: verifyConsentIntegrityHash({\n userId: c.userId,\n consentType: c.consentType,\n isGranted: c.isGranted,\n grantedAt: c.grantedAt,\n withdrawnAt: c.withdrawnAt,\n ipAddress: c.ipAddress,\n source: c.source,\n }, c.integrityHash),\n createdAt: c.createdAt.toISOString(),\n updatedAt: c.updatedAt?.toISOString() ?? null,\n }))\n\n return NextResponse.json({ ok: true, items })\n}\n\nexport default GET\n\nconst consentsGetDoc: OpenApiMethodDoc = {\n summary: 'List user consents',\n description: 'Returns all consent records for a given user, with integrity verification status.',\n tags: ['Auth'],\n query: querySchema,\n responses: [\n { status: 200, description: 'Consent list returned' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Auth',\n summary: 'User consents',\n methods: {\n GET: consentsGetDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { User, UserConsent } from '@open-mercato/core/modules/auth/data/entities'\nimport { verifyConsentIntegrityHash } from '@open-mercato/core/modules/auth/lib/consentIntegrity'\nimport { assertActorCanAccessUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { ConsentItem } from '@open-mercato/core/modules/auth/lib/consentTypes'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nexport const metadata = {\n path: '/auth/users/consents',\n GET: {\n requireAuth: true,\n requireFeatures: ['auth.users.edit'],\n },\n}\n\nconst querySchema = z.object({\n userId: z.string().uuid(),\n})\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = querySchema.safeParse({ userId: url.searchParams.get('userId') })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid userId' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const rbacService = container.resolve('rbacService') as RbacService\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n const actorAcl = auth.sub ? await rbacService.loadAcl(auth.sub, { tenantId, organizationId }) : null\n const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin\n\n if (!actorIsSuperAdmin && !tenantId) {\n const { translate } = await resolveTranslations()\n return NextResponse.json({\n ok: false,\n error: translate('auth.users.consents.errors.tenantContextRequired', 'Tenant context is required'),\n }, { status: 403 })\n }\n\n if (auth.sub) {\n try {\n await assertActorCanAccessUserTarget({\n em,\n rbacService,\n actorUserId: auth.sub,\n tenantId,\n organizationId,\n targetUserId: parsed.data.userId,\n actorIsSuperAdmin,\n organizationScope: await resolveOrganizationScopeForRequest({\n container,\n auth,\n request: req,\n tenantId,\n }),\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n }\n\n let scopeTenantId = tenantId\n if (!scopeTenantId) {\n const target = await findOneWithDecryption(\n em,\n User,\n { id: parsed.data.userId } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n if (!target) return NextResponse.json({ ok: true, items: [] })\n scopeTenantId = target.tenantId ?? null\n }\n\n const consents = await findWithDecryption(\n em,\n UserConsent,\n {\n userId: parsed.data.userId,\n deletedAt: null,\n tenantId: scopeTenantId,\n ...(organizationId ? { organizationId } : {}),\n },\n { orderBy: { createdAt: 'DESC' } },\n { tenantId: scopeTenantId, organizationId },\n )\n\n const items: ConsentItem[] = consents.map((c) => ({\n id: c.id,\n consentType: c.consentType,\n isGranted: c.isGranted,\n grantedAt: c.grantedAt?.toISOString() ?? null,\n withdrawnAt: c.withdrawnAt?.toISOString() ?? null,\n source: c.source ?? null,\n ipAddress: c.ipAddress ?? null,\n integrityValid: verifyConsentIntegrityHash({\n userId: c.userId,\n consentType: c.consentType,\n isGranted: c.isGranted,\n grantedAt: c.grantedAt,\n withdrawnAt: c.withdrawnAt,\n ipAddress: c.ipAddress,\n source: c.source,\n }, c.integrityHash),\n createdAt: c.createdAt.toISOString(),\n updatedAt: c.updatedAt?.toISOString() ?? null,\n }))\n\n return NextResponse.json({ ok: true, items })\n}\n\nexport default GET\n\nconst consentsGetDoc: OpenApiMethodDoc = {\n summary: 'List user consents',\n description: 'Returns all consent records for a given user, with integrity verification status.',\n tags: ['Auth'],\n query: querySchema,\n responses: [\n { status: 200, description: 'Consent list returned' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Auth',\n summary: 'User consents',\n methods: {\n GET: consentsGetDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -100,103 +100,110 @@ function ConfirmDealLostDialog({
100
100
  });
101
101
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (nextOpen) => {
102
102
  if (!nextOpen) onClose();
103
- }, children: /* @__PURE__ */ jsx(DialogContent, { className: "overflow-hidden p-0 sm:max-w-[560px]", onKeyDown: handleKeyDown, children: /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-lg bg-card", children: [
104
- /* @__PURE__ */ jsx(DialogHeader, { className: "border-b border-border/70 px-7 py-5", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-4", children: [
105
- /* @__PURE__ */ jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive", children: /* @__PURE__ */ jsx(AlertTriangle, { className: "size-5" }) }),
106
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
107
- /* @__PURE__ */ jsx(DialogTitle, { className: "text-lg font-bold leading-none tracking-tight text-foreground", children: t("customers.deals.detail.lost.title", "Mark deal as Lost?") }),
108
- /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [
109
- dealTitle,
110
- dealValue ? ` \xB7 ${dealValue}` : "",
111
- companyName ? ` \xB7 ${companyName}` : ""
112
- ] })
113
- ] })
114
- ] }) }),
115
- /* @__PURE__ */ jsxs("div", { className: "space-y-6 px-7 py-6", children: [
116
- /* @__PURE__ */ jsxs(Alert, { status: "warning", className: "rounded-md", children: [
117
- /* @__PURE__ */ jsx(AlertTitle, { children: t("customers.deals.detail.lost.warningTitle", "This action closes the deal") }),
118
- /* @__PURE__ */ jsx(AlertDescription, { children: t("customers.deals.detail.lost.warning", "This action sets the stage to 'Lost' and cannot be undone without 'sales.reopen' permission") })
119
- ] }),
120
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
121
- /* @__PURE__ */ jsxs("label", { className: "text-sm font-semibold text-foreground", children: [
122
- t("customers.deals.detail.lost.reasonLabel", "Loss reason"),
123
- /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
103
+ }, children: /* @__PURE__ */ jsx(
104
+ DialogContent,
105
+ {
106
+ className: "flex max-h-[min(90vh,720px)] flex-col overflow-hidden p-0 sm:max-w-[560px]",
107
+ onKeyDown: handleKeyDown,
108
+ children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg bg-card", children: [
109
+ /* @__PURE__ */ jsx(DialogHeader, { className: "border-b border-border/70 px-7 py-5", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-4", children: [
110
+ /* @__PURE__ */ jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive", children: /* @__PURE__ */ jsx(AlertTriangle, { className: "size-5" }) }),
111
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
112
+ /* @__PURE__ */ jsx(DialogTitle, { className: "text-lg font-bold leading-none tracking-tight text-foreground", children: t("customers.deals.detail.lost.title", "Mark deal as Lost?") }),
113
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [
114
+ dealTitle,
115
+ dealValue ? ` \xB7 ${dealValue}` : "",
116
+ companyName ? ` \xB7 ${companyName}` : ""
117
+ ] })
118
+ ] })
119
+ ] }) }),
120
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 space-y-6 overflow-y-auto px-7 py-6", children: [
121
+ /* @__PURE__ */ jsxs(Alert, { status: "warning", className: "rounded-md", children: [
122
+ /* @__PURE__ */ jsx(AlertTitle, { children: t("customers.deals.detail.lost.warningTitle", "This action closes the deal") }),
123
+ /* @__PURE__ */ jsx(AlertDescription, { children: t("customers.deals.detail.lost.warning", "This action sets the stage to 'Lost' and cannot be undone without 'sales.reopen' permission") })
124
+ ] }),
125
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
126
+ /* @__PURE__ */ jsxs("label", { className: "text-sm font-semibold text-foreground", children: [
127
+ t("customers.deals.detail.lost.reasonLabel", "Loss reason"),
128
+ /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
129
+ ] }),
130
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
131
+ /* @__PURE__ */ jsxs(
132
+ Button,
133
+ {
134
+ type: "button",
135
+ variant: "outline",
136
+ onClick: () => setReasonListOpen((current) => !current),
137
+ className: "h-auto flex w-full items-center justify-between rounded-md border-2 border-foreground bg-background px-4 py-3 text-left",
138
+ children: [
139
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
140
+ /* @__PURE__ */ jsx("div", { className: "truncate text-base font-semibold text-foreground", children: selectedLossReason?.label ?? (isLoadingReasons ? t("customers.deals.detail.lost.reasonLoadingShort", "Loading...") : t("customers.deals.detail.lost.reasonPlaceholder", "Select loss reason")) }),
141
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm text-muted-foreground", children: reasonHelpText })
142
+ ] }),
143
+ /* @__PURE__ */ jsx(ChevronDown, { className: "ml-3 size-4 shrink-0 text-muted-foreground" })
144
+ ]
145
+ }
146
+ ),
147
+ reasonListOpen ? /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-md border border-border/80 bg-background", children: hasLossReasons ? lossReasons.map((reason, index) => {
148
+ const isSelected = reason.id === lossReasonId;
149
+ return /* @__PURE__ */ jsxs(
150
+ Button,
151
+ {
152
+ type: "button",
153
+ variant: "ghost",
154
+ onClick: () => {
155
+ setLossReasonId(reason.id);
156
+ setReasonListOpen(false);
157
+ setError("");
158
+ },
159
+ className: `h-auto flex w-full items-center justify-between rounded-none px-4 py-3 text-left ${index < lossReasons.length - 1 ? "border-b border-border/60" : ""} ${isSelected ? "bg-muted/60" : "hover:bg-accent/50"}`,
160
+ children: [
161
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
162
+ /* @__PURE__ */ jsx("div", { className: "text-base font-semibold text-foreground", children: reason.label }),
163
+ /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: reason.description ?? t("customers.deals.detail.lost.reasonFallbackDescription", "No description available.") })
164
+ ] }),
165
+ isSelected ? /* @__PURE__ */ jsx("span", { className: "ml-3 flex size-6 shrink-0 items-center justify-center rounded-full bg-foreground text-background", children: /* @__PURE__ */ jsx(Check, { className: "size-3.5" }) }) : null
166
+ ]
167
+ },
168
+ reason.id
169
+ );
170
+ }) : /* @__PURE__ */ jsx("div", { className: "px-4 py-3 text-sm text-muted-foreground", children: unavailableReasonText }) }) : null
171
+ ] }),
172
+ reasonUnavailable ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: unavailableReasonText }) : null,
173
+ error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error }) : null
174
+ ] }),
175
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
176
+ /* @__PURE__ */ jsx("label", { className: "text-sm font-semibold text-foreground", children: t("customers.deals.detail.lost.notesLabel", "Loss notes (optional)") }),
177
+ /* @__PURE__ */ jsx(
178
+ Textarea,
179
+ {
180
+ value: lossNotes,
181
+ onChange: (event) => setLossNotes(event.target.value),
182
+ placeholder: t("customers.deals.detail.lost.notesPlaceholder", "Additional context about the loss..."),
183
+ rows: 4,
184
+ className: "min-h-[88px] rounded-md border-border/80 px-4 py-3 shadow-none"
185
+ }
186
+ )
187
+ ] })
124
188
  ] }),
125
- /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
126
- /* @__PURE__ */ jsxs(
189
+ /* @__PURE__ */ jsxs(DialogFooter, { className: "border-t border-border/70 px-7 py-4 sm:justify-end", children: [
190
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: onClose, children: t("customers.deals.detail.lost.cancel", "Cancel") }),
191
+ /* @__PURE__ */ jsx(
127
192
  Button,
128
193
  {
129
194
  type: "button",
130
- variant: "outline",
131
- onClick: () => setReasonListOpen((current) => !current),
132
- className: "h-auto flex w-full items-center justify-between rounded-md border-2 border-foreground bg-background px-4 py-3 text-left",
133
- children: [
134
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
135
- /* @__PURE__ */ jsx("div", { className: "truncate text-base font-semibold text-foreground", children: selectedLossReason?.label ?? (isLoadingReasons ? t("customers.deals.detail.lost.reasonLoadingShort", "Loading...") : t("customers.deals.detail.lost.reasonPlaceholder", "Select loss reason")) }),
136
- /* @__PURE__ */ jsx("div", { className: "truncate text-sm text-muted-foreground", children: reasonHelpText })
137
- ] }),
138
- /* @__PURE__ */ jsx(ChevronDown, { className: "ml-3 size-4 shrink-0 text-muted-foreground" })
139
- ]
140
- }
141
- ),
142
- reasonListOpen ? /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-md border border-border/80 bg-background", children: hasLossReasons ? lossReasons.map((reason, index) => {
143
- const isSelected = reason.id === lossReasonId;
144
- return /* @__PURE__ */ jsxs(
145
- Button,
146
- {
147
- type: "button",
148
- variant: "ghost",
149
- onClick: () => {
150
- setLossReasonId(reason.id);
151
- setReasonListOpen(false);
152
- setError("");
153
- },
154
- className: `h-auto flex w-full items-center justify-between rounded-none px-4 py-3 text-left ${index < lossReasons.length - 1 ? "border-b border-border/60" : ""} ${isSelected ? "bg-muted/60" : "hover:bg-accent/50"}`,
155
- children: [
156
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
157
- /* @__PURE__ */ jsx("div", { className: "text-base font-semibold text-foreground", children: reason.label }),
158
- /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: reason.description ?? t("customers.deals.detail.lost.reasonFallbackDescription", "No description available.") })
159
- ] }),
160
- isSelected ? /* @__PURE__ */ jsx("span", { className: "ml-3 flex size-6 shrink-0 items-center justify-center rounded-full bg-foreground text-background", children: /* @__PURE__ */ jsx(Check, { className: "size-3.5" }) }) : null
161
- ]
195
+ variant: "destructive-solid",
196
+ disabled: confirmDisabled,
197
+ onClick: () => {
198
+ void handleConfirm();
162
199
  },
163
- reason.id
164
- );
165
- }) : /* @__PURE__ */ jsx("div", { className: "px-4 py-3 text-sm text-muted-foreground", children: unavailableReasonText }) }) : null
166
- ] }),
167
- reasonUnavailable ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: unavailableReasonText }) : null,
168
- error ? /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error }) : null
169
- ] }),
170
- /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
171
- /* @__PURE__ */ jsx("label", { className: "text-sm font-semibold text-foreground", children: t("customers.deals.detail.lost.notesLabel", "Loss notes (optional)") }),
172
- /* @__PURE__ */ jsx(
173
- Textarea,
174
- {
175
- value: lossNotes,
176
- onChange: (event) => setLossNotes(event.target.value),
177
- placeholder: t("customers.deals.detail.lost.notesPlaceholder", "Additional context about the loss..."),
178
- rows: 4,
179
- className: "min-h-[88px] rounded-md border-border/80 px-4 py-3 shadow-none"
180
- }
181
- )
200
+ children: t("customers.deals.detail.lost.confirm", "Mark as Lost")
201
+ }
202
+ )
203
+ ] })
182
204
  ] })
183
- ] }),
184
- /* @__PURE__ */ jsxs(DialogFooter, { className: "border-t border-border/70 px-7 py-4 sm:justify-end", children: [
185
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: onClose, children: t("customers.deals.detail.lost.cancel", "Cancel") }),
186
- /* @__PURE__ */ jsx(
187
- Button,
188
- {
189
- type: "button",
190
- variant: "destructive-solid",
191
- disabled: confirmDisabled,
192
- onClick: () => {
193
- void handleConfirm();
194
- },
195
- children: t("customers.deals.detail.lost.confirm", "Mark as Lost")
196
- }
197
- )
198
- ] })
199
- ] }) }) });
205
+ }
206
+ ) });
200
207
  }
201
208
  export {
202
209
  ConfirmDealLostDialog
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/ConfirmDealLostDialog.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertTriangle, Check, ChevronDown } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { loadDictionaryEntriesByKey } from '@open-mercato/core/modules/dictionaries/lib/clientEntries'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ntype LossReasonOption = {\n id: string\n value: string\n label: string\n description?: string | null\n}\n\ntype ConfirmDealLostDialogProps = {\n open: boolean\n dealTitle: string\n dealValue?: string | null\n companyName?: string | null\n onClose: () => void\n onConfirm: (input: { lossReasonId: string; lossNotes?: string }) => void | Promise<void>\n}\n\nexport function ConfirmDealLostDialog({\n open,\n dealTitle,\n dealValue,\n companyName,\n onClose,\n onConfirm,\n}: ConfirmDealLostDialogProps) {\n const t = useT()\n const [lossReasonId, setLossReasonId] = React.useState('')\n const [lossNotes, setLossNotes] = React.useState('')\n const [lossReasons, setLossReasons] = React.useState<LossReasonOption[]>([])\n const [reasonListOpen, setReasonListOpen] = React.useState(false)\n const [error, setError] = React.useState('')\n const [dictionaryLoadFailed, setDictionaryLoadFailed] = React.useState(false)\n const [isLoadingReasons, setIsLoadingReasons] = React.useState(false)\n const [isConfirming, setIsConfirming] = React.useState(false)\n\n React.useEffect(() => {\n if (!open) return\n let cancelled = false\n setIsLoadingReasons(true)\n setDictionaryLoadFailed(false)\n loadDictionaryEntriesByKey('sales.deal_loss_reason')\n .then((items) => {\n if (!cancelled) setLossReasons(items)\n })\n .catch((loadError) => {\n logger.error('customers.deals.detail.lossReasons failed', { loadError })\n if (!cancelled) {\n setLossReasons([])\n setDictionaryLoadFailed(true)\n }\n })\n .finally(() => {\n if (!cancelled) setIsLoadingReasons(false)\n })\n return () => {\n cancelled = true\n }\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n setLossReasonId('')\n setLossNotes('')\n setReasonListOpen(false)\n setError('')\n setDictionaryLoadFailed(false)\n setIsConfirming(false)\n }, [open])\n\n const selectedLossReason = React.useMemo(\n () => lossReasons.find((reason) => reason.id === lossReasonId) ?? null,\n [lossReasonId, lossReasons],\n )\n const hasLossReasons = lossReasons.length > 0\n const reasonUnavailable = !isLoadingReasons && !hasLossReasons\n const unavailableReasonText = dictionaryLoadFailed\n ? t('customers.deals.detail.lost.reasonLoadError', 'Loss reasons could not be loaded.')\n : t('customers.deals.detail.lost.reasonUnavailable', 'No loss reasons are configured.')\n const reasonHelpText = React.useMemo(() => {\n if (selectedLossReason?.description) return selectedLossReason.description\n if (isLoadingReasons) {\n return t('customers.deals.detail.lost.reasonLoading', 'Loading loss reasons...')\n }\n if (reasonUnavailable) {\n return unavailableReasonText\n }\n return t('customers.deals.detail.lost.reasonHelp', 'Choose the closest reason from the dictionary.')\n }, [isLoadingReasons, reasonUnavailable, selectedLossReason?.description, t, unavailableReasonText])\n\n const handleConfirm = React.useCallback(async () => {\n if (isLoadingReasons || reasonUnavailable) {\n setError(unavailableReasonText)\n return\n }\n if (!lossReasonId) {\n setError(t('customers.deals.detail.lost.reasonRequired', 'Please select a loss reason'))\n return\n }\n setIsConfirming(true)\n try {\n await onConfirm({\n lossReasonId,\n lossNotes: lossNotes.trim() || undefined,\n })\n } finally {\n setIsConfirming(false)\n }\n }, [isLoadingReasons, lossNotes, lossReasonId, onConfirm, reasonUnavailable, t, unavailableReasonText])\n\n const confirmDisabled = isConfirming || isLoadingReasons || reasonUnavailable || !lossReasonId\n\n const handleKeyDown = useDialogKeyHandler({\n onConfirm: () => void handleConfirm(),\n disabled: confirmDisabled,\n })\n\n return (\n <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>\n <DialogContent className=\"overflow-hidden p-0 sm:max-w-[560px]\" onKeyDown={handleKeyDown}>\n <div className=\"overflow-hidden rounded-lg bg-card\">\n <DialogHeader className=\"border-b border-border/70 px-7 py-5\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive\">\n <AlertTriangle className=\"size-5\" />\n </div>\n <div className=\"min-w-0\">\n <DialogTitle className=\"text-lg font-bold leading-none tracking-tight text-foreground\">\n {t('customers.deals.detail.lost.title', 'Mark deal as Lost?')}\n </DialogTitle>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {dealTitle}\n {dealValue ? ` \u00B7 ${dealValue}` : ''}\n {companyName ? ` \u00B7 ${companyName}` : ''}\n </p>\n </div>\n </div>\n </DialogHeader>\n\n <div className=\"space-y-6 px-7 py-6\">\n <Alert status=\"warning\" className=\"rounded-md\">\n <AlertTitle>\n {t('customers.deals.detail.lost.warningTitle', 'This action closes the deal')}\n </AlertTitle>\n <AlertDescription>\n {t('customers.deals.detail.lost.warning', \"This action sets the stage to 'Lost' and cannot be undone without 'sales.reopen' permission\")}\n </AlertDescription>\n </Alert>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-semibold text-foreground\">\n {t('customers.deals.detail.lost.reasonLabel', 'Loss reason')}\n <span className=\"ml-1 text-destructive\">*</span>\n </label>\n <div className=\"space-y-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setReasonListOpen((current) => !current)}\n className=\"h-auto flex w-full items-center justify-between rounded-md border-2 border-foreground bg-background px-4 py-3 text-left\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate text-base font-semibold text-foreground\">\n {selectedLossReason?.label\n ?? (isLoadingReasons\n ? t('customers.deals.detail.lost.reasonLoadingShort', 'Loading...')\n : t('customers.deals.detail.lost.reasonPlaceholder', 'Select loss reason'))}\n </div>\n <div className=\"truncate text-sm text-muted-foreground\">\n {reasonHelpText}\n </div>\n </div>\n <ChevronDown className=\"ml-3 size-4 shrink-0 text-muted-foreground\" />\n </Button>\n\n {reasonListOpen ? (\n <div className=\"overflow-hidden rounded-md border border-border/80 bg-background\">\n {hasLossReasons ? lossReasons.map((reason, index) => {\n const isSelected = reason.id === lossReasonId\n return (\n <Button\n key={reason.id}\n type=\"button\"\n variant=\"ghost\"\n onClick={() => {\n setLossReasonId(reason.id)\n setReasonListOpen(false)\n setError('')\n }}\n className={`h-auto flex w-full items-center justify-between rounded-none px-4 py-3 text-left ${\n index < lossReasons.length - 1 ? 'border-b border-border/60' : ''\n } ${isSelected ? 'bg-muted/60' : 'hover:bg-accent/50'}`}\n >\n <div className=\"min-w-0\">\n <div className=\"text-base font-semibold text-foreground\">{reason.label}</div>\n <div className=\"text-sm text-muted-foreground\">\n {reason.description ?? t('customers.deals.detail.lost.reasonFallbackDescription', 'No description available.')}\n </div>\n </div>\n {isSelected ? (\n <span className=\"ml-3 flex size-6 shrink-0 items-center justify-center rounded-full bg-foreground text-background\">\n <Check className=\"size-3.5\" />\n </span>\n ) : null}\n </Button>\n )\n }) : (\n <div className=\"px-4 py-3 text-sm text-muted-foreground\">\n {unavailableReasonText}\n </div>\n )}\n </div>\n ) : null}\n </div>\n {reasonUnavailable ? (\n <p className=\"text-xs text-destructive\">\n {unavailableReasonText}\n </p>\n ) : null}\n {error ? <p className=\"text-xs text-destructive\">{error}</p> : null}\n </div>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-semibold text-foreground\">\n {t('customers.deals.detail.lost.notesLabel', 'Loss notes (optional)')}\n </label>\n <Textarea\n value={lossNotes}\n onChange={(event) => setLossNotes(event.target.value)}\n placeholder={t('customers.deals.detail.lost.notesPlaceholder', 'Additional context about the loss...')}\n rows={4}\n className=\"min-h-[88px] rounded-md border-border/80 px-4 py-3 shadow-none\"\n />\n </div>\n </div>\n\n <DialogFooter className=\"border-t border-border/70 px-7 py-4 sm:justify-end\">\n <Button type=\"button\" variant=\"outline\" onClick={onClose}>\n {t('customers.deals.detail.lost.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n variant=\"destructive-solid\"\n disabled={confirmDisabled}\n onClick={() => { void handleConfirm() }}\n >\n {t('customers.deals.detail.lost.confirm', 'Mark as Lost')}\n </Button>\n </DialogFooter>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { AlertTriangle, Check, ChevronDown } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { loadDictionaryEntriesByKey } from '@open-mercato/core/modules/dictionaries/lib/clientEntries'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ntype LossReasonOption = {\n id: string\n value: string\n label: string\n description?: string | null\n}\n\ntype ConfirmDealLostDialogProps = {\n open: boolean\n dealTitle: string\n dealValue?: string | null\n companyName?: string | null\n onClose: () => void\n onConfirm: (input: { lossReasonId: string; lossNotes?: string }) => void | Promise<void>\n}\n\nexport function ConfirmDealLostDialog({\n open,\n dealTitle,\n dealValue,\n companyName,\n onClose,\n onConfirm,\n}: ConfirmDealLostDialogProps) {\n const t = useT()\n const [lossReasonId, setLossReasonId] = React.useState('')\n const [lossNotes, setLossNotes] = React.useState('')\n const [lossReasons, setLossReasons] = React.useState<LossReasonOption[]>([])\n const [reasonListOpen, setReasonListOpen] = React.useState(false)\n const [error, setError] = React.useState('')\n const [dictionaryLoadFailed, setDictionaryLoadFailed] = React.useState(false)\n const [isLoadingReasons, setIsLoadingReasons] = React.useState(false)\n const [isConfirming, setIsConfirming] = React.useState(false)\n\n React.useEffect(() => {\n if (!open) return\n let cancelled = false\n setIsLoadingReasons(true)\n setDictionaryLoadFailed(false)\n loadDictionaryEntriesByKey('sales.deal_loss_reason')\n .then((items) => {\n if (!cancelled) setLossReasons(items)\n })\n .catch((loadError) => {\n logger.error('customers.deals.detail.lossReasons failed', { loadError })\n if (!cancelled) {\n setLossReasons([])\n setDictionaryLoadFailed(true)\n }\n })\n .finally(() => {\n if (!cancelled) setIsLoadingReasons(false)\n })\n return () => {\n cancelled = true\n }\n }, [open])\n\n React.useEffect(() => {\n if (!open) return\n setLossReasonId('')\n setLossNotes('')\n setReasonListOpen(false)\n setError('')\n setDictionaryLoadFailed(false)\n setIsConfirming(false)\n }, [open])\n\n const selectedLossReason = React.useMemo(\n () => lossReasons.find((reason) => reason.id === lossReasonId) ?? null,\n [lossReasonId, lossReasons],\n )\n const hasLossReasons = lossReasons.length > 0\n const reasonUnavailable = !isLoadingReasons && !hasLossReasons\n const unavailableReasonText = dictionaryLoadFailed\n ? t('customers.deals.detail.lost.reasonLoadError', 'Loss reasons could not be loaded.')\n : t('customers.deals.detail.lost.reasonUnavailable', 'No loss reasons are configured.')\n const reasonHelpText = React.useMemo(() => {\n if (selectedLossReason?.description) return selectedLossReason.description\n if (isLoadingReasons) {\n return t('customers.deals.detail.lost.reasonLoading', 'Loading loss reasons...')\n }\n if (reasonUnavailable) {\n return unavailableReasonText\n }\n return t('customers.deals.detail.lost.reasonHelp', 'Choose the closest reason from the dictionary.')\n }, [isLoadingReasons, reasonUnavailable, selectedLossReason?.description, t, unavailableReasonText])\n\n const handleConfirm = React.useCallback(async () => {\n if (isLoadingReasons || reasonUnavailable) {\n setError(unavailableReasonText)\n return\n }\n if (!lossReasonId) {\n setError(t('customers.deals.detail.lost.reasonRequired', 'Please select a loss reason'))\n return\n }\n setIsConfirming(true)\n try {\n await onConfirm({\n lossReasonId,\n lossNotes: lossNotes.trim() || undefined,\n })\n } finally {\n setIsConfirming(false)\n }\n }, [isLoadingReasons, lossNotes, lossReasonId, onConfirm, reasonUnavailable, t, unavailableReasonText])\n\n const confirmDisabled = isConfirming || isLoadingReasons || reasonUnavailable || !lossReasonId\n\n const handleKeyDown = useDialogKeyHandler({\n onConfirm: () => void handleConfirm(),\n disabled: confirmDisabled,\n })\n\n return (\n <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>\n <DialogContent\n className=\"flex max-h-[min(90vh,720px)] flex-col overflow-hidden p-0 sm:max-w-[560px]\"\n onKeyDown={handleKeyDown}\n >\n <div className=\"flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg bg-card\">\n <DialogHeader className=\"border-b border-border/70 px-7 py-5\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive\">\n <AlertTriangle className=\"size-5\" />\n </div>\n <div className=\"min-w-0\">\n <DialogTitle className=\"text-lg font-bold leading-none tracking-tight text-foreground\">\n {t('customers.deals.detail.lost.title', 'Mark deal as Lost?')}\n </DialogTitle>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {dealTitle}\n {dealValue ? ` \u00B7 ${dealValue}` : ''}\n {companyName ? ` \u00B7 ${companyName}` : ''}\n </p>\n </div>\n </div>\n </DialogHeader>\n\n <div className=\"min-h-0 flex-1 space-y-6 overflow-y-auto px-7 py-6\">\n <Alert status=\"warning\" className=\"rounded-md\">\n <AlertTitle>\n {t('customers.deals.detail.lost.warningTitle', 'This action closes the deal')}\n </AlertTitle>\n <AlertDescription>\n {t('customers.deals.detail.lost.warning', \"This action sets the stage to 'Lost' and cannot be undone without 'sales.reopen' permission\")}\n </AlertDescription>\n </Alert>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-semibold text-foreground\">\n {t('customers.deals.detail.lost.reasonLabel', 'Loss reason')}\n <span className=\"ml-1 text-destructive\">*</span>\n </label>\n <div className=\"space-y-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setReasonListOpen((current) => !current)}\n className=\"h-auto flex w-full items-center justify-between rounded-md border-2 border-foreground bg-background px-4 py-3 text-left\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate text-base font-semibold text-foreground\">\n {selectedLossReason?.label\n ?? (isLoadingReasons\n ? t('customers.deals.detail.lost.reasonLoadingShort', 'Loading...')\n : t('customers.deals.detail.lost.reasonPlaceholder', 'Select loss reason'))}\n </div>\n <div className=\"truncate text-sm text-muted-foreground\">\n {reasonHelpText}\n </div>\n </div>\n <ChevronDown className=\"ml-3 size-4 shrink-0 text-muted-foreground\" />\n </Button>\n\n {reasonListOpen ? (\n <div className=\"overflow-hidden rounded-md border border-border/80 bg-background\">\n {hasLossReasons ? lossReasons.map((reason, index) => {\n const isSelected = reason.id === lossReasonId\n return (\n <Button\n key={reason.id}\n type=\"button\"\n variant=\"ghost\"\n onClick={() => {\n setLossReasonId(reason.id)\n setReasonListOpen(false)\n setError('')\n }}\n className={`h-auto flex w-full items-center justify-between rounded-none px-4 py-3 text-left ${\n index < lossReasons.length - 1 ? 'border-b border-border/60' : ''\n } ${isSelected ? 'bg-muted/60' : 'hover:bg-accent/50'}`}\n >\n <div className=\"min-w-0\">\n <div className=\"text-base font-semibold text-foreground\">{reason.label}</div>\n <div className=\"text-sm text-muted-foreground\">\n {reason.description ?? t('customers.deals.detail.lost.reasonFallbackDescription', 'No description available.')}\n </div>\n </div>\n {isSelected ? (\n <span className=\"ml-3 flex size-6 shrink-0 items-center justify-center rounded-full bg-foreground text-background\">\n <Check className=\"size-3.5\" />\n </span>\n ) : null}\n </Button>\n )\n }) : (\n <div className=\"px-4 py-3 text-sm text-muted-foreground\">\n {unavailableReasonText}\n </div>\n )}\n </div>\n ) : null}\n </div>\n {reasonUnavailable ? (\n <p className=\"text-xs text-destructive\">\n {unavailableReasonText}\n </p>\n ) : null}\n {error ? <p className=\"text-xs text-destructive\">{error}</p> : null}\n </div>\n\n <div className=\"space-y-2\">\n <label className=\"text-sm font-semibold text-foreground\">\n {t('customers.deals.detail.lost.notesLabel', 'Loss notes (optional)')}\n </label>\n <Textarea\n value={lossNotes}\n onChange={(event) => setLossNotes(event.target.value)}\n placeholder={t('customers.deals.detail.lost.notesPlaceholder', 'Additional context about the loss...')}\n rows={4}\n className=\"min-h-[88px] rounded-md border-border/80 px-4 py-3 shadow-none\"\n />\n </div>\n </div>\n\n <DialogFooter className=\"border-t border-border/70 px-7 py-4 sm:justify-end\">\n <Button type=\"button\" variant=\"outline\" onClick={onClose}>\n {t('customers.deals.detail.lost.cancel', 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n variant=\"destructive-solid\"\n disabled={confirmDisabled}\n onClick={() => { void handleConfirm() }}\n >\n {t('customers.deals.detail.lost.confirm', 'Mark as Lost')}\n </Button>\n </DialogFooter>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.8-develop.6950.1.912d574d60",
3
+ "version": "0.6.8-develop.6958.1.6696e8db69",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -254,16 +254,16 @@
254
254
  "zod": "^4.4.3"
255
255
  },
256
256
  "peerDependencies": {
257
- "@open-mercato/ai-assistant": "0.6.8-develop.6950.1.912d574d60",
258
- "@open-mercato/shared": "0.6.8-develop.6950.1.912d574d60",
259
- "@open-mercato/ui": "0.6.8-develop.6950.1.912d574d60",
257
+ "@open-mercato/ai-assistant": "0.6.8-develop.6958.1.6696e8db69",
258
+ "@open-mercato/shared": "0.6.8-develop.6958.1.6696e8db69",
259
+ "@open-mercato/ui": "0.6.8-develop.6958.1.6696e8db69",
260
260
  "react": "^19.0.0",
261
261
  "react-dom": "^19.0.0"
262
262
  },
263
263
  "devDependencies": {
264
- "@open-mercato/ai-assistant": "0.6.8-develop.6950.1.912d574d60",
265
- "@open-mercato/shared": "0.6.8-develop.6950.1.912d574d60",
266
- "@open-mercato/ui": "0.6.8-develop.6950.1.912d574d60",
264
+ "@open-mercato/ai-assistant": "0.6.8-develop.6958.1.6696e8db69",
265
+ "@open-mercato/shared": "0.6.8-develop.6958.1.6696e8db69",
266
+ "@open-mercato/ui": "0.6.8-develop.6958.1.6696e8db69",
267
267
  "@testing-library/dom": "^10.4.1",
268
268
  "@testing-library/jest-dom": "^7.0.0",
269
269
  "@testing-library/react": "^16.3.1",
@@ -1,16 +1,17 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
- import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
4
4
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
6
- import { UserConsent } from '@open-mercato/core/modules/auth/data/entities'
6
+ import { User, UserConsent } from '@open-mercato/core/modules/auth/data/entities'
7
7
  import { verifyConsentIntegrityHash } from '@open-mercato/core/modules/auth/lib/consentIntegrity'
8
8
  import { assertActorCanAccessUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'
9
9
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
10
10
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
11
- import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
12
12
  import type { ConsentItem } from '@open-mercato/core/modules/auth/lib/consentTypes'
13
13
  import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
14
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
14
15
  import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
15
16
 
16
17
  export const metadata = {
@@ -39,18 +40,31 @@ export async function GET(req: Request) {
39
40
 
40
41
  const container = await createRequestContainer()
41
42
  const em = container.resolve('em') as EntityManager
43
+ const rbacService = container.resolve('rbacService') as RbacService
42
44
  const tenantId = auth.tenantId ?? null
43
45
  const organizationId = auth.orgId ?? null
44
46
 
47
+ const actorAcl = auth.sub ? await rbacService.loadAcl(auth.sub, { tenantId, organizationId }) : null
48
+ const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin
49
+
50
+ if (!actorIsSuperAdmin && !tenantId) {
51
+ const { translate } = await resolveTranslations()
52
+ return NextResponse.json({
53
+ ok: false,
54
+ error: translate('auth.users.consents.errors.tenantContextRequired', 'Tenant context is required'),
55
+ }, { status: 403 })
56
+ }
57
+
45
58
  if (auth.sub) {
46
59
  try {
47
60
  await assertActorCanAccessUserTarget({
48
61
  em,
49
- rbacService: container.resolve('rbacService') as RbacService,
62
+ rbacService,
50
63
  actorUserId: auth.sub,
51
64
  tenantId,
52
65
  organizationId,
53
66
  targetUserId: parsed.data.userId,
67
+ actorIsSuperAdmin,
54
68
  organizationScope: await resolveOrganizationScopeForRequest({
55
69
  container,
56
70
  auth,
@@ -64,17 +78,30 @@ export async function GET(req: Request) {
64
78
  }
65
79
  }
66
80
 
81
+ let scopeTenantId = tenantId
82
+ if (!scopeTenantId) {
83
+ const target = await findOneWithDecryption(
84
+ em,
85
+ User,
86
+ { id: parsed.data.userId } as FilterQuery<User>,
87
+ {},
88
+ { tenantId: null, organizationId: null },
89
+ )
90
+ if (!target) return NextResponse.json({ ok: true, items: [] })
91
+ scopeTenantId = target.tenantId ?? null
92
+ }
93
+
67
94
  const consents = await findWithDecryption(
68
95
  em,
69
96
  UserConsent,
70
97
  {
71
98
  userId: parsed.data.userId,
72
99
  deletedAt: null,
73
- ...(tenantId ? { tenantId } : {}),
100
+ tenantId: scopeTenantId,
74
101
  ...(organizationId ? { organizationId } : {}),
75
102
  },
76
103
  { orderBy: { createdAt: 'DESC' } },
77
- { tenantId, organizationId },
104
+ { tenantId: scopeTenantId, organizationId },
78
105
  )
79
106
 
80
107
  const items: ConsentItem[] = consents.map((c) => ({
@@ -149,6 +149,7 @@
149
149
  "auth.session.refresh.errors.invalidToken": "Ungültiges oder abgelaufenes Refresh-Token",
150
150
  "auth.signIn": "Anmelden",
151
151
  "auth.users.consents.empty": "Keine Einwilligungsdatensätze gefunden.",
152
+ "auth.users.consents.errors.tenantContextRequired": "Ein Mandantenkontext ist erforderlich",
152
153
  "auth.users.consents.field.createdAt": "Erstellt",
153
154
  "auth.users.consents.field.grantedAt": "Erteilt am",
154
155
  "auth.users.consents.field.integrity": "Integrität",
@@ -149,6 +149,7 @@
149
149
  "auth.session.refresh.errors.invalidToken": "Invalid or expired refresh token",
150
150
  "auth.signIn": "Sign in",
151
151
  "auth.users.consents.empty": "No consent records found.",
152
+ "auth.users.consents.errors.tenantContextRequired": "Tenant context is required",
152
153
  "auth.users.consents.field.createdAt": "Created",
153
154
  "auth.users.consents.field.grantedAt": "Granted at",
154
155
  "auth.users.consents.field.integrity": "Integrity",
@@ -149,6 +149,7 @@
149
149
  "auth.session.refresh.errors.invalidToken": "El token de actualización no es válido o ha expirado",
150
150
  "auth.signIn": "Iniciar sesión",
151
151
  "auth.users.consents.empty": "No se encontraron registros de consentimiento.",
152
+ "auth.users.consents.errors.tenantContextRequired": "Se requiere un contexto de inquilino",
152
153
  "auth.users.consents.field.createdAt": "Creado",
153
154
  "auth.users.consents.field.grantedAt": "Otorgado el",
154
155
  "auth.users.consents.field.integrity": "Integridad",
@@ -149,6 +149,7 @@
149
149
  "auth.session.refresh.errors.invalidToken": "잘못되었거나 만료된 갱신 토큰입니다",
150
150
  "auth.signIn": "로그인",
151
151
  "auth.users.consents.empty": "동의 기록을 찾을 수 없습니다.",
152
+ "auth.users.consents.errors.tenantContextRequired": "테넌트 컨텍스트가 필요합니다",
152
153
  "auth.users.consents.field.createdAt": "생성됨",
153
154
  "auth.users.consents.field.grantedAt": "동의 시각",
154
155
  "auth.users.consents.field.integrity": "무결성",
@@ -149,6 +149,7 @@
149
149
  "auth.session.refresh.errors.invalidToken": "Token odświeżania jest nieprawidłowy lub wygasł",
150
150
  "auth.signIn": "Zaloguj się",
151
151
  "auth.users.consents.empty": "Nie znaleziono zapisów zgód.",
152
+ "auth.users.consents.errors.tenantContextRequired": "Wymagany jest kontekst najemcy",
152
153
  "auth.users.consents.field.createdAt": "Utworzono",
153
154
  "auth.users.consents.field.grantedAt": "Udzielono",
154
155
  "auth.users.consents.field.integrity": "Integralność",
@@ -130,8 +130,11 @@ export function ConfirmDealLostDialog({
130
130
 
131
131
  return (
132
132
  <Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>
133
- <DialogContent className="overflow-hidden p-0 sm:max-w-[560px]" onKeyDown={handleKeyDown}>
134
- <div className="overflow-hidden rounded-lg bg-card">
133
+ <DialogContent
134
+ className="flex max-h-[min(90vh,720px)] flex-col overflow-hidden p-0 sm:max-w-[560px]"
135
+ onKeyDown={handleKeyDown}
136
+ >
137
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg bg-card">
135
138
  <DialogHeader className="border-b border-border/70 px-7 py-5">
136
139
  <div className="flex items-start gap-4">
137
140
  <div className="flex size-10 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive">
@@ -150,7 +153,7 @@ export function ConfirmDealLostDialog({
150
153
  </div>
151
154
  </DialogHeader>
152
155
 
153
- <div className="space-y-6 px-7 py-6">
156
+ <div className="min-h-0 flex-1 space-y-6 overflow-y-auto px-7 py-6">
154
157
  <Alert status="warning" className="rounded-md">
155
158
  <AlertTitle>
156
159
  {t('customers.deals.detail.lost.warningTitle', 'This action closes the deal')}