@open-mercato/core 0.6.6-develop.6290.1.4bb5a8ba3f → 0.6.6-develop.6299.1.29224e2d86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +8 -0
- package/dist/generated/entities/module_config/index.js +4 -0
- package/dist/generated/entities/module_config/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +1 -1
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductComplianceSection.js +235 -62
- package/dist/modules/catalog/components/products/ProductComplianceSection.js.map +2 -2
- package/dist/modules/configs/components/CachePanel.js +2 -1
- package/dist/modules/configs/components/CachePanel.js.map +2 -2
- package/dist/modules/configs/components/SystemStatusPanel.js +10 -12
- package/dist/modules/configs/components/SystemStatusPanel.js.map +2 -2
- package/dist/modules/configs/data/entities.js +15 -1
- package/dist/modules/configs/data/entities.js.map +2 -2
- package/dist/modules/configs/lib/module-config-service.js +52 -19
- package/dist/modules/configs/lib/module-config-service.js.map +3 -3
- package/dist/modules/configs/migrations/Migration20260617150000.js +21 -0
- package/dist/modules/configs/migrations/Migration20260617150000.js.map +7 -0
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +47 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
- package/generated/entities/module_config/index.ts +2 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/auth/lib/setup-app.ts +1 -1
- package/src/modules/catalog/components/products/ProductComplianceSection.tsx +138 -27
- package/src/modules/configs/components/CachePanel.tsx +2 -3
- package/src/modules/configs/components/SystemStatusPanel.tsx +12 -19
- package/src/modules/configs/data/entities.ts +17 -1
- package/src/modules/configs/lib/module-config-service.ts +83 -24
- package/src/modules/configs/migrations/.snapshot-open-mercato.json +41 -4
- package/src/modules/configs/migrations/.snapshot-openmercato.json +41 -4
- package/src/modules/configs/migrations/Migration20260617150000.ts +21 -0
- package/src/modules/customers/api/activities/route.ts +6 -0
- package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +54 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../../src/modules/inbox_ops/api/proposals/%5Bid%5D/replies/%5BreplyId%5D/send/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { Resend } from 'resend'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { resolveDefaultEmailFromAddress } from '@open-mercato/shared/lib/email/config'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposalAction, InboxEmail } from '../../../../../../data/entities'\nimport { draftReplyPayloadSchema } from '../../../../../../data/validators'\nimport { emitInboxOpsEvent } from '../../../../../../events'\nimport { createMessageRecordForReply } from '../../../../../../lib/messagesIntegration'\nimport {\n resolveRequestContext,\n resolveProposal,\n extractPathSegment,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.replies.send'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const url = new URL(req.url)\n const proposal = await resolveProposal(url, ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const replyId = extractPathSegment(url, 'replies')\n if (!replyId) {\n return NextResponse.json({ error: 'Missing reply ID' }, { status: 400 })\n }\n\n const action = await findOneWithDecryption(\n ctx.em,\n InboxProposalAction,\n {\n id: replyId,\n proposalId: proposal.id,\n actionType: 'draft_reply',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n undefined,\n ctx.scope,\n )\n\n if (!action) {\n return NextResponse.json({ error: 'Reply action not found' }, { status: 404 })\n }\n\n if (action.status !== 'executed') {\n return NextResponse.json(\n { error: `Cannot send reply \u2014 action must be accepted first (current status: \"${action.status}\")` },\n { status: 409 },\n )\n }\n\n const existingMetadata =\n action.metadata && typeof action.metadata === 'object' ? action.metadata : {}\n if ((existingMetadata as Record<string, unknown>).replySentAt) {\n return NextResponse.json(\n { error: 'Reply has already been sent for this action' },\n { status: 409 },\n )\n }\n\n const email = await findOneWithDecryption(\n ctx.em,\n InboxEmail,\n { id: proposal.inboxEmailId, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n const payloadResult = draftReplyPayloadSchema.safeParse(action.payload)\n if (!payloadResult.success) {\n return NextResponse.json({ error: 'Reply payload missing required fields (to, subject, body)' }, { status: 400 })\n }\n const { to: toAddress, toName, subject, body } = payloadResult.data\n\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) {\n return NextResponse.json({ error: 'Email service not configured' }, { status: 503 })\n }\n\n const emailDisabled =\n parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) ||\n parseBooleanWithDefault(process.env.OM_TEST_MODE, false)\n if (emailDisabled) {\n return NextResponse.json({ error: 'Email delivery is disabled' }, { status: 503 })\n }\n\n const { inReplyToMessageId, references: payloadReferences } = payloadResult.data\n const fromAddress = resolveDefaultEmailFromAddress()\n if (!fromAddress) {\n return NextResponse.json({\n error: 'Email sender is not configured. Set NOTIFICATIONS_EMAIL_FROM, EMAIL_FROM, or ADMIN_EMAIL.',\n }, { status: 503 })\n }\n\n const headers: Record<string, string> = {}\n const inReplyTo = inReplyToMessageId || email?.messageId\n if (inReplyTo) {\n headers['In-Reply-To'] = inReplyTo\n }\n const references = payloadReferences || (email?.emailReferences as string[])\n if (references && references.length > 0) {\n headers['References'] = references.join(' ')\n }\n\n const resend = new Resend(apiKey)\n const { data: sendData, error: sendError } = await resend.emails.send({\n to: toAddress,\n from: fromAddress,\n subject,\n text: body,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n })\n\n if (sendError) {\n const errorMessage = sendError.message || 'Unknown error'\n return NextResponse.json({ error: `Failed to send email: ${errorMessage}` }, { status: 502 })\n }\n\n const sentMessageId = sendData?.id || null\n const messagesResult = await createMessageRecordForReply(\n { to: toAddress, toName, subject, body },\n proposal.inboxEmailId,\n {\n container: ctx.container,\n scope: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n },\n },\n )\n\n action.metadata = {\n ...existingMetadata,\n replySentAt: new Date().toISOString(),\n sentMessageId,\n ...(messagesResult ? { messageRecordId: messagesResult.messageId } : {}),\n }\n await ctx.em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.reply.sent', {\n proposalId: proposal.id,\n actionId: replyId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n toAddress,\n sentMessageId,\n messageRecordId: messagesResult?.messageId ?? null,\n })\n } catch (eventError) {\n console.error('[inbox_ops:reply:send] Failed to emit event:', eventError)\n }\n\n return NextResponse.json({\n ok: true,\n sentMessageId,\n ...(messagesResult ? { messageRecordId: messagesResult.messageId } : {}),\n })\n } catch (err) {\n return handleRouteError(err, 'send reply')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Send draft reply',\n methods: {\n POST: {\n summary: 'Send a draft reply email and register it in messages when available',\n description: 'Sends the draft_reply action payload via the configured email provider. When the messages module is available, also records the sent reply as an internal message record. Sets In-Reply-To and References headers for threading.',\n responses: [\n { status: 200, description: 'Reply sent successfully' },\n { status: 400, description: 'Missing required payload fields' },\n { status: 404, description: 'Reply action not found' },\n { status: 409, description: 'Action in invalid state for sending' },\n { status: 502, description: 'Email delivery failed' },\n { status: 503, description: 'Email service not configured or disabled' },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { raw } from '@mikro-orm/core'\nimport { Resend } from 'resend'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { resolveDefaultEmailFromAddress } from '@open-mercato/shared/lib/email/config'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { InboxProposalAction, InboxEmail } from '../../../../../../data/entities'\nimport { draftReplyPayloadSchema } from '../../../../../../data/validators'\nimport { emitInboxOpsEvent } from '../../../../../../events'\nimport { createMessageRecordForReply } from '../../../../../../lib/messagesIntegration'\nimport {\n resolveRequestContext,\n resolveProposal,\n extractPathSegment,\n handleRouteError,\n isErrorResponse,\n} from '../../../../../routeHelpers'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['inbox_ops.replies.send'] },\n}\n\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const url = new URL(req.url)\n const proposal = await resolveProposal(url, ctx)\n if (isErrorResponse(proposal)) return proposal\n\n const replyId = extractPathSegment(url, 'replies')\n if (!replyId) {\n return NextResponse.json({ error: 'Missing reply ID' }, { status: 400 })\n }\n\n const action = await findOneWithDecryption(\n ctx.em,\n InboxProposalAction,\n {\n id: replyId,\n proposalId: proposal.id,\n actionType: 'draft_reply',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n },\n undefined,\n ctx.scope,\n )\n\n if (!action) {\n return NextResponse.json({ error: 'Reply action not found' }, { status: 404 })\n }\n\n if (action.status !== 'executed') {\n return NextResponse.json(\n { error: `Cannot send reply \u2014 action must be accepted first (current status: \"${action.status}\")` },\n { status: 409 },\n )\n }\n\n const existingMetadata =\n action.metadata && typeof action.metadata === 'object' ? action.metadata : {}\n if ((existingMetadata as Record<string, unknown>).replySentAt) {\n return NextResponse.json(\n { error: 'Reply has already been sent for this action' },\n { status: 409 },\n )\n }\n\n const email = await findOneWithDecryption(\n ctx.em,\n InboxEmail,\n { id: proposal.inboxEmailId, deletedAt: null },\n undefined,\n ctx.scope,\n )\n\n const payloadResult = draftReplyPayloadSchema.safeParse(action.payload)\n if (!payloadResult.success) {\n return NextResponse.json({ error: 'Reply payload missing required fields (to, subject, body)' }, { status: 400 })\n }\n const { to: toAddress, toName, subject, body } = payloadResult.data\n\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) {\n return NextResponse.json({ error: 'Email service not configured' }, { status: 503 })\n }\n\n const emailDisabled =\n parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) ||\n parseBooleanWithDefault(process.env.OM_TEST_MODE, false)\n if (emailDisabled) {\n return NextResponse.json({ error: 'Email delivery is disabled' }, { status: 503 })\n }\n\n const { inReplyToMessageId, references: payloadReferences } = payloadResult.data\n const fromAddress = resolveDefaultEmailFromAddress()\n if (!fromAddress) {\n return NextResponse.json({\n error: 'Email sender is not configured. Set NOTIFICATIONS_EMAIL_FROM, EMAIL_FROM, or ADMIN_EMAIL.',\n }, { status: 503 })\n }\n\n const headers: Record<string, string> = {}\n const inReplyTo = inReplyToMessageId || email?.messageId\n if (inReplyTo) {\n headers['In-Reply-To'] = inReplyTo\n }\n const references = payloadReferences || (email?.emailReferences as string[])\n if (references && references.length > 0) {\n headers['References'] = references.join(' ')\n }\n\n // Atomically claim the send right before calling the external email provider so\n // two concurrent requests cannot both deliver the same reply. The claim only\n // succeeds when neither replySentAt nor replySendClaimedAt is already set in the\n // metadata; a losing request gets 0 rows back and is rejected with 409.\n const sendClaimedAt = new Date().toISOString()\n const claimed = await ctx.em.nativeUpdate(\n InboxProposalAction,\n {\n id: action.id,\n proposalId: proposal.id,\n actionType: 'draft_reply',\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n deletedAt: null,\n status: 'executed',\n [raw(`coalesce(\"metadata\"->>'replySentAt', '')`)]: '',\n [raw(`coalesce(\"metadata\"->>'replySendClaimedAt', '')`)]: '',\n },\n {\n metadata: raw(\n `coalesce(\"metadata\", '{}'::jsonb) || jsonb_build_object('replySendClaimedAt', ?::text)`,\n [sendClaimedAt],\n ),\n },\n )\n\n if (claimed === 0) {\n return NextResponse.json(\n { error: 'Reply send is already in progress or has been sent for this action' },\n { status: 409 },\n )\n }\n\n const releaseSendClaim = async () => {\n try {\n await ctx.em.nativeUpdate(\n InboxProposalAction,\n {\n id: action.id,\n organizationId: ctx.organizationId,\n tenantId: ctx.tenantId,\n [raw(`coalesce(\"metadata\"->>'replySentAt', '')`)]: '',\n [raw(`(\"metadata\"->>'replySendClaimedAt')`)]: sendClaimedAt,\n },\n { metadata: raw(`\"metadata\" - 'replySendClaimedAt'`) },\n )\n } catch (releaseError) {\n console.error('[inbox_ops:reply:send] Failed to release send claim:', releaseError)\n }\n }\n\n const resend = new Resend(apiKey)\n const { data: sendData, error: sendError } = await resend.emails.send({\n to: toAddress,\n from: fromAddress,\n subject,\n text: body,\n ...(Object.keys(headers).length > 0 ? { headers } : {}),\n })\n\n if (sendError) {\n await releaseSendClaim()\n const errorMessage = sendError.message || 'Unknown error'\n return NextResponse.json({ error: `Failed to send email: ${errorMessage}` }, { status: 502 })\n }\n\n const sentMessageId = sendData?.id || null\n const messagesResult = await createMessageRecordForReply(\n { to: toAddress, toName, subject, body },\n proposal.inboxEmailId,\n {\n container: ctx.container,\n scope: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n userId: ctx.userId,\n },\n },\n )\n\n action.metadata = {\n ...existingMetadata,\n replySentAt: new Date().toISOString(),\n sentMessageId,\n ...(messagesResult ? { messageRecordId: messagesResult.messageId } : {}),\n }\n await ctx.em.flush()\n\n try {\n await emitInboxOpsEvent('inbox_ops.reply.sent', {\n proposalId: proposal.id,\n actionId: replyId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n toAddress,\n sentMessageId,\n messageRecordId: messagesResult?.messageId ?? null,\n })\n } catch (eventError) {\n console.error('[inbox_ops:reply:send] Failed to emit event:', eventError)\n }\n\n return NextResponse.json({\n ok: true,\n sentMessageId,\n ...(messagesResult ? { messageRecordId: messagesResult.messageId } : {}),\n })\n } catch (err) {\n return handleRouteError(err, 'send reply')\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'InboxOps',\n summary: 'Send draft reply',\n methods: {\n POST: {\n summary: 'Send a draft reply email and register it in messages when available',\n description: 'Sends the draft_reply action payload via the configured email provider. When the messages module is available, also records the sent reply as an internal message record. Sets In-Reply-To and References headers for threading.',\n responses: [\n { status: 200, description: 'Reply sent successfully' },\n { status: 400, description: 'Missing required payload fields' },\n { status: 404, description: 'Reply action not found' },\n { status: 409, description: 'Action in invalid state for sending, or a send is already in progress / completed for this reply' },\n { status: 502, description: 'Email delivery failed' },\n { status: 503, description: 'Email service not configured or disabled' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,5 +2,7 @@ export const id = "id";
|
|
|
2
2
|
export const module_id = "module_id";
|
|
3
3
|
export const name = "name";
|
|
4
4
|
export const value_json = "value_json";
|
|
5
|
+
export const organization_id = "organization_id";
|
|
6
|
+
export const tenant_id = "tenant_id";
|
|
5
7
|
export const created_at = "created_at";
|
|
6
8
|
export const updated_at = "updated_at";
|
|
@@ -1541,6 +1541,8 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
|
|
|
1541
1541
|
"module_id": "module_id",
|
|
1542
1542
|
"name": "name",
|
|
1543
1543
|
"value_json": "value_json",
|
|
1544
|
+
"organization_id": "organization_id",
|
|
1545
|
+
"tenant_id": "tenant_id",
|
|
1544
1546
|
"created_at": "created_at",
|
|
1545
1547
|
"updated_at": "updated_at"
|
|
1546
1548
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6299.1.29224e2d86",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6299.1.29224e2d86",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6299.1.29224e2d86",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6299.1.29224e2d86",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6299.1.29224e2d86",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6299.1.29224e2d86",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6299.1.29224e2d86",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -300,7 +300,7 @@ export async function setupInitialTenant(
|
|
|
300
300
|
|
|
301
301
|
await em.transactional(async (tem) => {
|
|
302
302
|
const tenant = tem.create(Tenant, {
|
|
303
|
-
name:
|
|
303
|
+
name: options.orgName,
|
|
304
304
|
isActive: true,
|
|
305
305
|
createdAt: new Date(),
|
|
306
306
|
updatedAt: new Date(),
|
|
@@ -29,9 +29,26 @@ type ProductComplianceSectionProps = {
|
|
|
29
29
|
embedded?: boolean;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
function
|
|
32
|
+
function fieldErrorId(inputId: string) {
|
|
33
|
+
return `${inputId}-error`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function describedByError(
|
|
37
|
+
inputId: string,
|
|
38
|
+
message?: string,
|
|
39
|
+
): { "aria-invalid"?: true; "aria-describedby"?: string } {
|
|
40
|
+
return message
|
|
41
|
+
? { "aria-invalid": true, "aria-describedby": fieldErrorId(inputId) }
|
|
42
|
+
: {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function FieldError({ id, message }: { id: string; message?: string }) {
|
|
33
46
|
if (!message) return null;
|
|
34
|
-
return
|
|
47
|
+
return (
|
|
48
|
+
<p id={id} className="text-xs text-destructive">
|
|
49
|
+
{message}
|
|
50
|
+
</p>
|
|
51
|
+
);
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
export function ProductComplianceSection({
|
|
@@ -85,8 +102,12 @@ export function ProductComplianceSection({
|
|
|
85
102
|
onChange={(event) =>
|
|
86
103
|
setValue("countryOfOriginCode", event.target.value.toUpperCase())
|
|
87
104
|
}
|
|
105
|
+
{...describedByError("catalog-product-compliance-country", errors.countryOfOriginCode)}
|
|
106
|
+
/>
|
|
107
|
+
<FieldError
|
|
108
|
+
id={fieldErrorId("catalog-product-compliance-country")}
|
|
109
|
+
message={errors.countryOfOriginCode}
|
|
88
110
|
/>
|
|
89
|
-
<FieldError message={errors.countryOfOriginCode} />
|
|
90
111
|
</div>
|
|
91
112
|
<div className="space-y-2">
|
|
92
113
|
<Label htmlFor="catalog-product-compliance-tax-classification">
|
|
@@ -96,8 +117,12 @@ export function ProductComplianceSection({
|
|
|
96
117
|
id="catalog-product-compliance-tax-classification"
|
|
97
118
|
value={values.taxClassificationCode ?? ""}
|
|
98
119
|
onChange={(event) => setValue("taxClassificationCode", event.target.value)}
|
|
120
|
+
{...describedByError("catalog-product-compliance-tax-classification", errors.taxClassificationCode)}
|
|
121
|
+
/>
|
|
122
|
+
<FieldError
|
|
123
|
+
id={fieldErrorId("catalog-product-compliance-tax-classification")}
|
|
124
|
+
message={errors.taxClassificationCode}
|
|
99
125
|
/>
|
|
100
|
-
<FieldError message={errors.taxClassificationCode} />
|
|
101
126
|
</div>
|
|
102
127
|
<div className="space-y-2">
|
|
103
128
|
<Label htmlFor="catalog-product-compliance-pkwiu">
|
|
@@ -107,8 +132,12 @@ export function ProductComplianceSection({
|
|
|
107
132
|
id="catalog-product-compliance-pkwiu"
|
|
108
133
|
value={values.pkwiuCode ?? ""}
|
|
109
134
|
onChange={(event) => setValue("pkwiuCode", event.target.value)}
|
|
135
|
+
{...describedByError("catalog-product-compliance-pkwiu", errors.pkwiuCode)}
|
|
136
|
+
/>
|
|
137
|
+
<FieldError
|
|
138
|
+
id={fieldErrorId("catalog-product-compliance-pkwiu")}
|
|
139
|
+
message={errors.pkwiuCode}
|
|
110
140
|
/>
|
|
111
|
-
<FieldError message={errors.pkwiuCode} />
|
|
112
141
|
</div>
|
|
113
142
|
<div className="space-y-2">
|
|
114
143
|
<Label htmlFor="catalog-product-compliance-cn">
|
|
@@ -118,8 +147,12 @@ export function ProductComplianceSection({
|
|
|
118
147
|
id="catalog-product-compliance-cn"
|
|
119
148
|
value={values.cnCode ?? ""}
|
|
120
149
|
onChange={(event) => setValue("cnCode", event.target.value)}
|
|
150
|
+
{...describedByError("catalog-product-compliance-cn", errors.cnCode)}
|
|
151
|
+
/>
|
|
152
|
+
<FieldError
|
|
153
|
+
id={fieldErrorId("catalog-product-compliance-cn")}
|
|
154
|
+
message={errors.cnCode}
|
|
121
155
|
/>
|
|
122
|
-
<FieldError message={errors.cnCode} />
|
|
123
156
|
</div>
|
|
124
157
|
<div className="space-y-2 md:col-span-2">
|
|
125
158
|
<Label htmlFor="catalog-product-compliance-hs">
|
|
@@ -129,13 +162,24 @@ export function ProductComplianceSection({
|
|
|
129
162
|
id="catalog-product-compliance-hs"
|
|
130
163
|
value={values.hsCode ?? ""}
|
|
131
164
|
onChange={(event) => setValue("hsCode", event.target.value)}
|
|
165
|
+
{...describedByError("catalog-product-compliance-hs", errors.hsCode)}
|
|
166
|
+
/>
|
|
167
|
+
<FieldError
|
|
168
|
+
id={fieldErrorId("catalog-product-compliance-hs")}
|
|
169
|
+
message={errors.hsCode}
|
|
132
170
|
/>
|
|
133
|
-
<FieldError message={errors.hsCode} />
|
|
134
171
|
</div>
|
|
135
172
|
</div>
|
|
136
173
|
<div className="space-y-2">
|
|
137
|
-
<Label
|
|
138
|
-
|
|
174
|
+
<Label id="catalog-product-compliance-gtu-label">
|
|
175
|
+
{t("catalog.products.compliance.fields.gtuCodes", "GTU codes (JPK_V7)")}
|
|
176
|
+
</Label>
|
|
177
|
+
<div
|
|
178
|
+
role="group"
|
|
179
|
+
aria-labelledby="catalog-product-compliance-gtu-label"
|
|
180
|
+
className="grid gap-2 sm:grid-cols-3 md:grid-cols-4"
|
|
181
|
+
{...describedByError("catalog-product-compliance-gtu", errors.gtuCodes)}
|
|
182
|
+
>
|
|
139
183
|
{CATALOG_GTU_CODES.map((code) => (
|
|
140
184
|
<label
|
|
141
185
|
key={code}
|
|
@@ -151,7 +195,10 @@ export function ProductComplianceSection({
|
|
|
151
195
|
</label>
|
|
152
196
|
))}
|
|
153
197
|
</div>
|
|
154
|
-
<FieldError
|
|
198
|
+
<FieldError
|
|
199
|
+
id={fieldErrorId("catalog-product-compliance-gtu")}
|
|
200
|
+
message={errors.gtuCodes}
|
|
201
|
+
/>
|
|
155
202
|
</div>
|
|
156
203
|
</div>
|
|
157
204
|
|
|
@@ -179,8 +226,12 @@ export function ProductComplianceSection({
|
|
|
179
226
|
max={120}
|
|
180
227
|
value={values.ageMin ?? ""}
|
|
181
228
|
onChange={(event) => setValue("ageMin", event.target.value)}
|
|
229
|
+
{...describedByError("catalog-product-compliance-age-min", errors.ageMin)}
|
|
230
|
+
/>
|
|
231
|
+
<FieldError
|
|
232
|
+
id={fieldErrorId("catalog-product-compliance-age-min")}
|
|
233
|
+
message={errors.ageMin}
|
|
182
234
|
/>
|
|
183
|
-
<FieldError message={errors.ageMin} />
|
|
184
235
|
</div>
|
|
185
236
|
<div className="space-y-2">
|
|
186
237
|
<Label htmlFor="catalog-product-compliance-excise-category">
|
|
@@ -192,7 +243,10 @@ export function ProductComplianceSection({
|
|
|
192
243
|
setValue("exciseCategory", value === NONE_OPTION ? null : value)
|
|
193
244
|
}
|
|
194
245
|
>
|
|
195
|
-
<SelectTrigger
|
|
246
|
+
<SelectTrigger
|
|
247
|
+
id="catalog-product-compliance-excise-category"
|
|
248
|
+
{...describedByError("catalog-product-compliance-excise-category", errors.exciseCategory)}
|
|
249
|
+
>
|
|
196
250
|
<SelectValue />
|
|
197
251
|
</SelectTrigger>
|
|
198
252
|
<SelectContent>
|
|
@@ -206,7 +260,10 @@ export function ProductComplianceSection({
|
|
|
206
260
|
))}
|
|
207
261
|
</SelectContent>
|
|
208
262
|
</Select>
|
|
209
|
-
<FieldError
|
|
263
|
+
<FieldError
|
|
264
|
+
id={fieldErrorId("catalog-product-compliance-excise-category")}
|
|
265
|
+
message={errors.exciseCategory}
|
|
266
|
+
/>
|
|
210
267
|
</div>
|
|
211
268
|
</div>
|
|
212
269
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
@@ -253,8 +310,12 @@ export function ProductComplianceSection({
|
|
|
253
310
|
id="catalog-product-compliance-hazmat-class"
|
|
254
311
|
value={values.hazmatClass ?? ""}
|
|
255
312
|
onChange={(event) => setValue("hazmatClass", event.target.value)}
|
|
313
|
+
{...describedByError("catalog-product-compliance-hazmat-class", errors.hazmatClass)}
|
|
314
|
+
/>
|
|
315
|
+
<FieldError
|
|
316
|
+
id={fieldErrorId("catalog-product-compliance-hazmat-class")}
|
|
317
|
+
message={errors.hazmatClass}
|
|
256
318
|
/>
|
|
257
|
-
<FieldError message={errors.hazmatClass} />
|
|
258
319
|
</div>
|
|
259
320
|
<div className="space-y-2">
|
|
260
321
|
<Label htmlFor="catalog-product-compliance-un-number">
|
|
@@ -265,8 +326,12 @@ export function ProductComplianceSection({
|
|
|
265
326
|
placeholder={t("catalog.products.compliance.placeholders.unNumber", "UN1234")}
|
|
266
327
|
value={values.unNumber ?? ""}
|
|
267
328
|
onChange={(event) => setValue("unNumber", event.target.value)}
|
|
329
|
+
{...describedByError("catalog-product-compliance-un-number", errors.unNumber)}
|
|
330
|
+
/>
|
|
331
|
+
<FieldError
|
|
332
|
+
id={fieldErrorId("catalog-product-compliance-un-number")}
|
|
333
|
+
message={errors.unNumber}
|
|
268
334
|
/>
|
|
269
|
-
<FieldError message={errors.unNumber} />
|
|
270
335
|
</div>
|
|
271
336
|
<div className="space-y-2">
|
|
272
337
|
<Label htmlFor="catalog-product-compliance-packing-group">
|
|
@@ -278,7 +343,10 @@ export function ProductComplianceSection({
|
|
|
278
343
|
setValue("hazmatPackingGroup", value === NONE_OPTION ? null : value)
|
|
279
344
|
}
|
|
280
345
|
>
|
|
281
|
-
<SelectTrigger
|
|
346
|
+
<SelectTrigger
|
|
347
|
+
id="catalog-product-compliance-packing-group"
|
|
348
|
+
{...describedByError("catalog-product-compliance-packing-group", errors.hazmatPackingGroup)}
|
|
349
|
+
>
|
|
282
350
|
<SelectValue />
|
|
283
351
|
</SelectTrigger>
|
|
284
352
|
<SelectContent>
|
|
@@ -292,7 +360,10 @@ export function ProductComplianceSection({
|
|
|
292
360
|
))}
|
|
293
361
|
</SelectContent>
|
|
294
362
|
</Select>
|
|
295
|
-
<FieldError
|
|
363
|
+
<FieldError
|
|
364
|
+
id={fieldErrorId("catalog-product-compliance-packing-group")}
|
|
365
|
+
message={errors.hazmatPackingGroup}
|
|
366
|
+
/>
|
|
296
367
|
</div>
|
|
297
368
|
</div>
|
|
298
369
|
</div>
|
|
@@ -319,8 +390,12 @@ export function ProductComplianceSection({
|
|
|
319
390
|
type="date"
|
|
320
391
|
value={values.launchAt ?? ""}
|
|
321
392
|
onChange={(event) => setValue("launchAt", event.target.value)}
|
|
393
|
+
{...describedByError("catalog-product-compliance-launch-at", errors.launchAt)}
|
|
394
|
+
/>
|
|
395
|
+
<FieldError
|
|
396
|
+
id={fieldErrorId("catalog-product-compliance-launch-at")}
|
|
397
|
+
message={errors.launchAt}
|
|
322
398
|
/>
|
|
323
|
-
<FieldError message={errors.launchAt} />
|
|
324
399
|
</div>
|
|
325
400
|
<div className="space-y-2">
|
|
326
401
|
<Label htmlFor="catalog-product-compliance-eol-at">
|
|
@@ -331,8 +406,12 @@ export function ProductComplianceSection({
|
|
|
331
406
|
type="date"
|
|
332
407
|
value={values.endOfLifeAt ?? ""}
|
|
333
408
|
onChange={(event) => setValue("endOfLifeAt", event.target.value)}
|
|
409
|
+
{...describedByError("catalog-product-compliance-eol-at", errors.endOfLifeAt)}
|
|
410
|
+
/>
|
|
411
|
+
<FieldError
|
|
412
|
+
id={fieldErrorId("catalog-product-compliance-eol-at")}
|
|
413
|
+
message={errors.endOfLifeAt}
|
|
334
414
|
/>
|
|
335
|
-
<FieldError message={errors.endOfLifeAt} />
|
|
336
415
|
</div>
|
|
337
416
|
<div className="space-y-2">
|
|
338
417
|
<Label htmlFor="catalog-product-compliance-available-from">
|
|
@@ -343,8 +422,12 @@ export function ProductComplianceSection({
|
|
|
343
422
|
type="date"
|
|
344
423
|
value={values.availableFrom ?? ""}
|
|
345
424
|
onChange={(event) => setValue("availableFrom", event.target.value)}
|
|
425
|
+
{...describedByError("catalog-product-compliance-available-from", errors.availableFrom)}
|
|
426
|
+
/>
|
|
427
|
+
<FieldError
|
|
428
|
+
id={fieldErrorId("catalog-product-compliance-available-from")}
|
|
429
|
+
message={errors.availableFrom}
|
|
346
430
|
/>
|
|
347
|
-
<FieldError message={errors.availableFrom} />
|
|
348
431
|
</div>
|
|
349
432
|
<div className="space-y-2">
|
|
350
433
|
<Label htmlFor="catalog-product-compliance-available-until">
|
|
@@ -355,8 +438,12 @@ export function ProductComplianceSection({
|
|
|
355
438
|
type="date"
|
|
356
439
|
value={values.availableUntil ?? ""}
|
|
357
440
|
onChange={(event) => setValue("availableUntil", event.target.value)}
|
|
441
|
+
{...describedByError("catalog-product-compliance-available-until", errors.availableUntil)}
|
|
442
|
+
/>
|
|
443
|
+
<FieldError
|
|
444
|
+
id={fieldErrorId("catalog-product-compliance-available-until")}
|
|
445
|
+
message={errors.availableUntil}
|
|
358
446
|
/>
|
|
359
|
-
<FieldError message={errors.availableUntil} />
|
|
360
447
|
</div>
|
|
361
448
|
</div>
|
|
362
449
|
<label
|
|
@@ -395,8 +482,12 @@ export function ProductComplianceSection({
|
|
|
395
482
|
min={1}
|
|
396
483
|
value={values.minOrderQty ?? ""}
|
|
397
484
|
onChange={(event) => setValue("minOrderQty", event.target.value)}
|
|
485
|
+
{...describedByError("catalog-product-compliance-min-qty", errors.minOrderQty)}
|
|
486
|
+
/>
|
|
487
|
+
<FieldError
|
|
488
|
+
id={fieldErrorId("catalog-product-compliance-min-qty")}
|
|
489
|
+
message={errors.minOrderQty}
|
|
398
490
|
/>
|
|
399
|
-
<FieldError message={errors.minOrderQty} />
|
|
400
491
|
</div>
|
|
401
492
|
<div className="space-y-2">
|
|
402
493
|
<Label htmlFor="catalog-product-compliance-max-qty">
|
|
@@ -408,8 +499,12 @@ export function ProductComplianceSection({
|
|
|
408
499
|
min={1}
|
|
409
500
|
value={values.maxOrderQty ?? ""}
|
|
410
501
|
onChange={(event) => setValue("maxOrderQty", event.target.value)}
|
|
502
|
+
{...describedByError("catalog-product-compliance-max-qty", errors.maxOrderQty)}
|
|
503
|
+
/>
|
|
504
|
+
<FieldError
|
|
505
|
+
id={fieldErrorId("catalog-product-compliance-max-qty")}
|
|
506
|
+
message={errors.maxOrderQty}
|
|
411
507
|
/>
|
|
412
|
-
<FieldError message={errors.maxOrderQty} />
|
|
413
508
|
</div>
|
|
414
509
|
<div className="space-y-2">
|
|
415
510
|
<Label htmlFor="catalog-product-compliance-qty-increment">
|
|
@@ -421,8 +516,12 @@ export function ProductComplianceSection({
|
|
|
421
516
|
min={1}
|
|
422
517
|
value={values.orderQtyIncrement ?? ""}
|
|
423
518
|
onChange={(event) => setValue("orderQtyIncrement", event.target.value)}
|
|
519
|
+
{...describedByError("catalog-product-compliance-qty-increment", errors.orderQtyIncrement)}
|
|
520
|
+
/>
|
|
521
|
+
<FieldError
|
|
522
|
+
id={fieldErrorId("catalog-product-compliance-qty-increment")}
|
|
523
|
+
message={errors.orderQtyIncrement}
|
|
424
524
|
/>
|
|
425
|
-
<FieldError message={errors.orderQtyIncrement} />
|
|
426
525
|
</div>
|
|
427
526
|
</div>
|
|
428
527
|
<label
|
|
@@ -460,8 +559,12 @@ export function ProductComplianceSection({
|
|
|
460
559
|
maxLength={255}
|
|
461
560
|
value={values.seoTitle ?? ""}
|
|
462
561
|
onChange={(event) => setValue("seoTitle", event.target.value)}
|
|
562
|
+
{...describedByError("catalog-product-compliance-seo-title", errors.seoTitle)}
|
|
563
|
+
/>
|
|
564
|
+
<FieldError
|
|
565
|
+
id={fieldErrorId("catalog-product-compliance-seo-title")}
|
|
566
|
+
message={errors.seoTitle}
|
|
463
567
|
/>
|
|
464
|
-
<FieldError message={errors.seoTitle} />
|
|
465
568
|
</div>
|
|
466
569
|
<div className="space-y-2">
|
|
467
570
|
<Label htmlFor="catalog-product-compliance-seo-description">
|
|
@@ -473,8 +576,12 @@ export function ProductComplianceSection({
|
|
|
473
576
|
maxLength={1000}
|
|
474
577
|
value={values.seoDescription ?? ""}
|
|
475
578
|
onChange={(event) => setValue("seoDescription", event.target.value)}
|
|
579
|
+
{...describedByError("catalog-product-compliance-seo-description", errors.seoDescription)}
|
|
580
|
+
/>
|
|
581
|
+
<FieldError
|
|
582
|
+
id={fieldErrorId("catalog-product-compliance-seo-description")}
|
|
583
|
+
message={errors.seoDescription}
|
|
476
584
|
/>
|
|
477
|
-
<FieldError message={errors.seoDescription} />
|
|
478
585
|
</div>
|
|
479
586
|
<div className="space-y-2">
|
|
480
587
|
<Label htmlFor="catalog-product-compliance-canonical-url">
|
|
@@ -487,8 +594,12 @@ export function ProductComplianceSection({
|
|
|
487
594
|
placeholder="https://"
|
|
488
595
|
value={values.canonicalUrl ?? ""}
|
|
489
596
|
onChange={(event) => setValue("canonicalUrl", event.target.value)}
|
|
597
|
+
{...describedByError("catalog-product-compliance-canonical-url", errors.canonicalUrl)}
|
|
598
|
+
/>
|
|
599
|
+
<FieldError
|
|
600
|
+
id={fieldErrorId("catalog-product-compliance-canonical-url")}
|
|
601
|
+
message={errors.canonicalUrl}
|
|
490
602
|
/>
|
|
491
|
-
<FieldError message={errors.canonicalUrl} />
|
|
492
603
|
</div>
|
|
493
604
|
</div>
|
|
494
605
|
</div>
|
|
@@ -4,6 +4,7 @@ import * as React from 'react'
|
|
|
4
4
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
5
5
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
6
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
7
|
+
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
7
8
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
8
9
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
10
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -248,9 +249,7 @@ export function CachePanel() {
|
|
|
248
249
|
{t('configs.cache.description', 'Inspect cached responses and clear segments when necessary.')}
|
|
249
250
|
</p>
|
|
250
251
|
</header>
|
|
251
|
-
<
|
|
252
|
-
{state.error}
|
|
253
|
-
</div>
|
|
252
|
+
<ErrorMessage label={state.error} />
|
|
254
253
|
<div className="flex flex-wrap gap-2">
|
|
255
254
|
<Button variant="outline" type="button" onClick={handleRefresh}>
|
|
256
255
|
{t('configs.cache.retry', 'Retry')}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
5
5
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
6
|
+
import { StatusBadge, type StatusMap } from '@open-mercato/ui/primitives/status-badge'
|
|
7
|
+
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
6
8
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
7
9
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
10
|
import type {
|
|
@@ -23,12 +25,12 @@ const STATUS_LABEL_KEYS: Record<SystemStatusState, string> = {
|
|
|
23
25
|
unknown: 'configs.systemStatus.state.unknown',
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
enabled: '
|
|
28
|
-
disabled: '
|
|
29
|
-
set: '
|
|
30
|
-
unset: '
|
|
31
|
-
unknown: '
|
|
28
|
+
const STATUS_BADGE_VARIANTS: StatusMap<SystemStatusState> = {
|
|
29
|
+
enabled: 'success',
|
|
30
|
+
disabled: 'neutral',
|
|
31
|
+
set: 'info',
|
|
32
|
+
unset: 'neutral',
|
|
33
|
+
unknown: 'warning',
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
const RUNTIME_MODE_LABEL_KEYS: Record<SystemStatusRuntimeMode, string> = {
|
|
@@ -84,15 +86,6 @@ function renderEnvAssignment(
|
|
|
84
86
|
)
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
function StatusBadge({ state }: { state: SystemStatusState }) {
|
|
88
|
-
const t = useT()
|
|
89
|
-
return (
|
|
90
|
-
<span className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium ${STATUS_BADGE_CLASSES[state]}`}>
|
|
91
|
-
{t(STATUS_LABEL_KEYS[state])}
|
|
92
|
-
</span>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
89
|
type FetchState = {
|
|
97
90
|
loading: boolean
|
|
98
91
|
error: string | null
|
|
@@ -163,9 +156,7 @@ export function SystemStatusPanel() {
|
|
|
163
156
|
)}
|
|
164
157
|
</p>
|
|
165
158
|
</header>
|
|
166
|
-
<
|
|
167
|
-
{state.error}
|
|
168
|
-
</div>
|
|
159
|
+
<ErrorMessage label={state.error} />
|
|
169
160
|
<Button type="button" variant="outline" onClick={() => loadSnapshot().catch(() => {})}>
|
|
170
161
|
{t('configs.systemStatus.retry', 'Retry')}
|
|
171
162
|
</Button>
|
|
@@ -215,7 +206,9 @@ export function SystemStatusPanel() {
|
|
|
215
206
|
<h4 className="text-sm font-semibold">{t(item.labelKey)}</h4>
|
|
216
207
|
<p className="text-xs text-muted-foreground">{t(item.descriptionKey)}</p>
|
|
217
208
|
</div>
|
|
218
|
-
<StatusBadge
|
|
209
|
+
<StatusBadge variant={STATUS_BADGE_VARIANTS[item.state]}>
|
|
210
|
+
{t(STATUS_LABEL_KEYS[item.state])}
|
|
211
|
+
</StatusBadge>
|
|
219
212
|
</div>
|
|
220
213
|
<dl className="grid gap-3 text-sm sm:grid-cols-2">
|
|
221
214
|
<div className="space-y-1">
|
|
@@ -2,7 +2,17 @@ import { OptionalProps } from '@mikro-orm/core'
|
|
|
2
2
|
import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
|
|
3
3
|
|
|
4
4
|
@Entity({ tableName: 'module_configs' })
|
|
5
|
-
@
|
|
5
|
+
@Index({
|
|
6
|
+
name: 'module_configs_global_unique',
|
|
7
|
+
expression:
|
|
8
|
+
'create unique index "module_configs_global_unique" on "module_configs" ("module_id", "name") where "tenant_id" is null',
|
|
9
|
+
})
|
|
10
|
+
@Index({
|
|
11
|
+
name: 'module_configs_scoped_unique',
|
|
12
|
+
expression:
|
|
13
|
+
'create unique index "module_configs_scoped_unique" on "module_configs" ("module_id", "name", "tenant_id") where "tenant_id" is not null',
|
|
14
|
+
})
|
|
15
|
+
@Index({ name: 'module_configs_module_name_tenant_idx', properties: ['moduleId', 'name', 'tenantId'] })
|
|
6
16
|
export class ModuleConfig {
|
|
7
17
|
[OptionalProps]?: 'createdAt' | 'updatedAt'
|
|
8
18
|
|
|
@@ -18,6 +28,12 @@ export class ModuleConfig {
|
|
|
18
28
|
@Property({ name: 'value_json', type: 'json', nullable: true })
|
|
19
29
|
valueJson!: unknown
|
|
20
30
|
|
|
31
|
+
@Property({ name: 'organization_id', type: 'uuid', nullable: true })
|
|
32
|
+
organizationId?: string | null
|
|
33
|
+
|
|
34
|
+
@Property({ name: 'tenant_id', type: 'uuid', nullable: true })
|
|
35
|
+
tenantId?: string | null
|
|
36
|
+
|
|
21
37
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
22
38
|
createdAt: Date = new Date()
|
|
23
39
|
|