@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.
- package/dist/modules/auth/api/users/consents/route.js +29 -5
- package/dist/modules/auth/api/users/consents/route.js.map +2 -2
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +98 -91
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/users/consents/route.ts +33 -6
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/ko.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +6 -3
|
@@ -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
|
|
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
|
-
|
|
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
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,mBAAmB;
|
|
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(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
126
|
-
/* @__PURE__ */
|
|
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: "
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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": ";
|
|
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.
|
|
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.
|
|
258
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
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.
|
|
265
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
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
|
|
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
|
-
|
|
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
|
|
134
|
-
|
|
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')}
|