@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6245.1.2be3b151f8
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/dist/modules/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarToolbar.js +4 -6
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
- package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
- package/dist/modules/customers/lib/calendar/format.js +27 -0
- package/dist/modules/customers/lib/calendar/format.js.map +7 -0
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +46 -13
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +43 -8
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/components/calendar/CalendarToolbar.tsx +4 -7
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
- package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/lib/calendar/format.ts +25 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +57 -13
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +57 -9
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -5,9 +5,14 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
|
5
5
|
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
6
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
7
7
|
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
|
+
import {
|
|
9
|
+
runCrudMutationGuardAfterSuccess,
|
|
10
|
+
validateCrudMutationGuard
|
|
11
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
8
12
|
import { customerSettingsUpsertSchema } from "../../../data/validators.js";
|
|
9
13
|
import { loadCustomerSettings } from "../../../commands/settings.js";
|
|
10
14
|
import { withScopedPayload } from "../../utils.js";
|
|
15
|
+
const SETTINGS_RESOURCE_KIND = "customers.settings";
|
|
11
16
|
const metadata = {
|
|
12
17
|
GET: { requireAuth: true, requireFeatures: ["customers.settings.manage"] },
|
|
13
18
|
PUT: { requireAuth: true, requireFeatures: ["customers.settings.manage"] }
|
|
@@ -59,15 +64,42 @@ async function GET(req) {
|
|
|
59
64
|
}
|
|
60
65
|
async function PUT(req) {
|
|
61
66
|
try {
|
|
62
|
-
const { ctx, translate } = await resolveSettingsContext(req);
|
|
67
|
+
const { ctx, tenantId, organizationId, translate } = await resolveSettingsContext(req);
|
|
63
68
|
const payload = await req.json().catch(() => ({}));
|
|
64
69
|
const scoped = withScopedPayload(payload, ctx, translate);
|
|
65
70
|
const input = customerSettingsUpsertSchema.parse(scoped);
|
|
71
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
72
|
+
tenantId,
|
|
73
|
+
organizationId,
|
|
74
|
+
userId: ctx.auth.sub,
|
|
75
|
+
resourceKind: SETTINGS_RESOURCE_KIND,
|
|
76
|
+
resourceId: organizationId,
|
|
77
|
+
operation: "update",
|
|
78
|
+
requestMethod: req.method,
|
|
79
|
+
requestHeaders: req.headers,
|
|
80
|
+
mutationPayload: input
|
|
81
|
+
});
|
|
82
|
+
if (guardResult && !guardResult.ok) {
|
|
83
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
84
|
+
}
|
|
66
85
|
const commandBus = ctx.container.resolve("commandBus");
|
|
67
86
|
const { result } = await commandBus.execute(
|
|
68
87
|
"customers.settings.save",
|
|
69
88
|
{ input, ctx }
|
|
70
89
|
);
|
|
90
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
91
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
92
|
+
tenantId,
|
|
93
|
+
organizationId,
|
|
94
|
+
userId: ctx.auth.sub,
|
|
95
|
+
resourceKind: SETTINGS_RESOURCE_KIND,
|
|
96
|
+
resourceId: organizationId,
|
|
97
|
+
operation: "update",
|
|
98
|
+
requestMethod: req.method,
|
|
99
|
+
requestHeaders: req.headers,
|
|
100
|
+
metadata: guardResult.metadata ?? null
|
|
101
|
+
});
|
|
102
|
+
}
|
|
71
103
|
return NextResponse.json({
|
|
72
104
|
addressFormat: result?.addressFormat ?? input.addressFormat
|
|
73
105
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/api/settings/address-format/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { customerSettingsUpsertSchema, type CustomerSettingsUpsertInput } from '../../../data/validators'\nimport { loadCustomerSettings } from '../../../commands/settings'\nimport type { CustomerAddressFormat } from '../../../data/entities'\nimport { withScopedPayload } from '../../utils'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\ntype SettingsRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n translate: (key: string, fallback?: string) => string\n em: EntityManager\n}\n\nasync function resolveSettingsContext(req: Request): Promise<SettingsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('customers.errors.organization_required', 'Organization context is required') })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n const em = (container.resolve('em') as EntityManager)\n return {\n ctx,\n tenantId: auth.tenantId,\n organizationId,\n translate,\n em,\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const { em, tenantId, organizationId } = await resolveSettingsContext(req)\n const record = await loadCustomerSettings(em, { tenantId, organizationId })\n return NextResponse.json({\n addressFormat: record?.addressFormat ?? 'line_first',\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.settings.address-format.get failed', err)\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to load settings') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n try {\n const { ctx, translate } = await resolveSettingsContext(req)\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload, ctx, translate)\n const input = customerSettingsUpsertSchema.parse(scoped)\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result } = await commandBus.execute<CustomerSettingsUpsertInput, { settingsId: string; addressFormat: CustomerAddressFormat }>(\n 'customers.settings.save',\n { input, ctx },\n )\n\n return NextResponse.json({\n addressFormat: result?.addressFormat ?? input.addressFormat,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.settings.address-format.put failed', err)\n return NextResponse.json({ error: translate('customers.errors.save_failed', 'Failed to save settings') }, { status: 400 })\n }\n}\n\nconst addressFormatResponseSchema = z.object({\n addressFormat: z.string(),\n})\n\nconst addressFormatErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer address format settings',\n methods: {\n GET: {\n summary: 'Retrieve address format',\n description: 'Returns the current address formatting preference for the selected organization.',\n responses: [\n { status: 200, description: 'Current address format', schema: addressFormatResponseSchema },\n { status: 401, description: 'Unauthorized', schema: addressFormatErrorSchema },\n { status: 400, description: 'Organization context missing', schema: addressFormatErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update address format',\n description: 'Updates the address format preference for the selected organization.',\n requestBody: {\n contentType: 'application/json',\n schema: customerSettingsUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Updated address format', schema: addressFormatResponseSchema },\n { status: 401, description: 'Unauthorized', schema: addressFormatErrorSchema },\n { status: 400, description: 'Invalid payload or organization context', schema: addressFormatErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AAGnD,SAAS,2BAA2B;AACpC,SAAS,eAAe,uBAAuB;AAC/C,SAAS,oCAAsE;AAC/E,SAAS,4BAA4B;AAErC,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { customerSettingsUpsertSchema, type CustomerSettingsUpsertInput } from '../../../data/validators'\nimport { loadCustomerSettings } from '../../../commands/settings'\nimport type { CustomerAddressFormat } from '../../../data/entities'\nimport { withScopedPayload } from '../../utils'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst SETTINGS_RESOURCE_KIND = 'customers.settings'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\ntype SettingsRouteContext = {\n ctx: CommandRuntimeContext\n tenantId: string\n organizationId: string\n translate: (key: string, fallback?: string) => string\n em: EntityManager\n}\n\nasync function resolveSettingsContext(req: Request): Promise<SettingsRouteContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, { error: translate('customers.errors.organization_required', 'Organization context is required') })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n const em = (container.resolve('em') as EntityManager)\n return {\n ctx,\n tenantId: auth.tenantId,\n organizationId,\n translate,\n em,\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const { em, tenantId, organizationId } = await resolveSettingsContext(req)\n const record = await loadCustomerSettings(em, { tenantId, organizationId })\n return NextResponse.json({\n addressFormat: record?.addressFormat ?? 'line_first',\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.settings.address-format.get failed', err)\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to load settings') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n try {\n const { ctx, tenantId, organizationId, translate } = await resolveSettingsContext(req)\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload, ctx, translate)\n const input = customerSettingsUpsertSchema.parse(scoped)\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId,\n organizationId,\n userId: ctx.auth!.sub,\n resourceKind: SETTINGS_RESOURCE_KIND,\n resourceId: organizationId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result } = await commandBus.execute<CustomerSettingsUpsertInput, { settingsId: string; addressFormat: CustomerAddressFormat }>(\n 'customers.settings.save',\n { input, ctx },\n )\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId,\n organizationId,\n userId: ctx.auth!.sub,\n resourceKind: SETTINGS_RESOURCE_KIND,\n resourceId: organizationId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({\n addressFormat: result?.addressFormat ?? input.addressFormat,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.settings.address-format.put failed', err)\n return NextResponse.json({ error: translate('customers.errors.save_failed', 'Failed to save settings') }, { status: 400 })\n }\n}\n\nconst addressFormatResponseSchema = z.object({\n addressFormat: z.string(),\n})\n\nconst addressFormatErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer address format settings',\n methods: {\n GET: {\n summary: 'Retrieve address format',\n description: 'Returns the current address formatting preference for the selected organization.',\n responses: [\n { status: 200, description: 'Current address format', schema: addressFormatResponseSchema },\n { status: 401, description: 'Unauthorized', schema: addressFormatErrorSchema },\n { status: 400, description: 'Organization context missing', schema: addressFormatErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update address format',\n description: 'Updates the address format preference for the selected organization.',\n requestBody: {\n contentType: 'application/json',\n schema: customerSettingsUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Updated address format', schema: addressFormatResponseSchema },\n { status: 401, description: 'Unauthorized', schema: addressFormatErrorSchema },\n { status: 400, description: 'Invalid payload or organization context', schema: addressFormatErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AAGnD,SAAS,2BAA2B;AACpC,SAAS,eAAe,uBAAuB;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oCAAsE;AAC/E,SAAS,4BAA4B;AAErC,SAAS,yBAAyB;AAGlC,MAAM,yBAAyB;AAExB,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,2BAA2B,EAAE;AAAA,EACzE,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,2BAA2B,EAAE;AAC3E;AAUA,eAAe,uBAAuB,KAA6C;AACjF,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,iCAAiC,cAAc,EAAE,CAAC;AAAA,EACpG;AAEA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,iBAAiB,OAAO,cAAc,KAAK,SAAS;AAC1D,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,0CAA0C,kCAAkC,EAAE,CAAC;AAAA,EACjI;AAEA,QAAM,MAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB;AAAA,IACxB,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAClE,SAAS;AAAA,EACX;AAEA,QAAM,KAAM,UAAU,QAAQ,IAAI;AAClC,SAAO;AAAA,IACL;AAAA,IACA,UAAU,KAAK;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,EAAE,IAAI,UAAU,eAAe,IAAI,MAAM,uBAAuB,GAAG;AACzE,UAAM,SAAS,MAAM,qBAAqB,IAAI,EAAE,UAAU,eAAe,CAAC;AAC1E,WAAO,aAAa,KAAK;AAAA,MACvB,eAAe,QAAQ,iBAAiB;AAAA,IAC1C,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,YAAQ,MAAM,gDAAgD,GAAG;AACjE,WAAO,aAAa,KAAK,EAAE,OAAO,UAAU,kCAAkC,yBAAyB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7H;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,MAAI;AACF,UAAM,EAAE,KAAK,UAAU,gBAAgB,UAAU,IAAI,MAAM,uBAAuB,GAAG;AACrF,UAAM,UAAU,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACjD,UAAM,SAAS,kBAAkB,SAAS,KAAK,SAAS;AACxD,UAAM,QAAQ,6BAA6B,MAAM,MAAM;AAEvD,UAAM,cAAc,MAAM,0BAA0B,IAAI,WAAW;AAAA,MACjE;AAAA,MACA;AAAA,MACA,QAAQ,IAAI,KAAM;AAAA,MAClB,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB;AAAA,IACnB,CAAC;AACD,QAAI,eAAe,CAAC,YAAY,IAAI;AAClC,aAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,IAC3E;AAEA,UAAM,aAAc,IAAI,UAAU,QAAQ,YAAY;AACtD,UAAM,EAAE,OAAO,IAAI,MAAM,WAAW;AAAA,MAClC;AAAA,MACA,EAAE,OAAO,IAAI;AAAA,IACf;AAEA,QAAI,aAAa,MAAM,YAAY,uBAAuB;AACxD,YAAM,iCAAiC,IAAI,WAAW;AAAA,QACpD;AAAA,QACA;AAAA,QACA,QAAQ,IAAI,KAAM;AAAA,QAClB,cAAc;AAAA,QACd,YAAY;AAAA,QACZ,WAAW;AAAA,QACX,eAAe,IAAI;AAAA,QACnB,gBAAgB,IAAI;AAAA,QACpB,UAAU,YAAY,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB,eAAe,QAAQ,iBAAiB,MAAM;AAAA,IAChD,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,YAAQ,MAAM,gDAAgD,GAAG;AACjE,WAAO,aAAa,KAAK,EAAE,OAAO,UAAU,gCAAgC,yBAAyB,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3H;AACF;AAEA,MAAM,8BAA8B,EAAE,OAAO;AAAA,EAC3C,eAAe,EAAE,OAAO;AAC1B,CAAC;AAED,MAAM,2BAA2B,EAAE,OAAO;AAAA,EACxC,OAAO,EAAE,OAAO;AAClB,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,4BAA4B;AAAA,QAC1F,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,yBAAyB;AAAA,QAC7E,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,yBAAyB;AAAA,MAC/F;AAAA,IACF;AAAA,IACA,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,4BAA4B;AAAA,QAC1F,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,yBAAyB;AAAA,QAC7E,EAAE,QAAQ,KAAK,aAAa,2CAA2C,QAAQ,yBAAyB;AAAA,MAC1G;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,11 @@ import { tagAssignmentSchema } from "../../../data/validators.js";
|
|
|
8
8
|
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
9
9
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
10
10
|
import { withScopedPayload } from "../../utils.js";
|
|
11
|
+
import {
|
|
12
|
+
runCrudMutationGuardAfterSuccess,
|
|
13
|
+
validateCrudMutationGuard
|
|
14
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
15
|
+
const TAG_ASSIGNMENT_RESOURCE_KIND = "customers.tagAssignment";
|
|
11
16
|
const metadata = {
|
|
12
17
|
POST: { requireAuth: true, requireFeatures: ["customers.activities.manage"] }
|
|
13
18
|
};
|
|
@@ -30,14 +35,46 @@ async function buildContext(req) {
|
|
|
30
35
|
async function POST(req) {
|
|
31
36
|
try {
|
|
32
37
|
const { ctx, auth, translate } = await buildContext(req);
|
|
38
|
+
const tenantId = auth.tenantId;
|
|
39
|
+
const organizationId = ctx.selectedOrganizationId;
|
|
40
|
+
if (!tenantId || !organizationId) {
|
|
41
|
+
return NextResponse.json({ error: translate("customers.errors.context_required", "Organization and tenant context required") }, { status: 400 });
|
|
42
|
+
}
|
|
33
43
|
const body = await req.json().catch(() => ({}));
|
|
34
44
|
const scoped = withScopedPayload(body, ctx, translate);
|
|
35
45
|
const input = tagAssignmentSchema.parse(scoped);
|
|
46
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
47
|
+
tenantId,
|
|
48
|
+
organizationId,
|
|
49
|
+
userId: auth.sub,
|
|
50
|
+
resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
|
|
51
|
+
resourceId: input.entityId,
|
|
52
|
+
operation: "custom",
|
|
53
|
+
requestMethod: req.method,
|
|
54
|
+
requestHeaders: req.headers,
|
|
55
|
+
mutationPayload: input
|
|
56
|
+
});
|
|
57
|
+
if (guardResult && !guardResult.ok) {
|
|
58
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
59
|
+
}
|
|
36
60
|
const commandBus = ctx.container.resolve("commandBus");
|
|
37
61
|
const { result, logEntry } = await commandBus.execute(
|
|
38
62
|
"customers.tags.assign",
|
|
39
63
|
{ input, ctx }
|
|
40
64
|
);
|
|
65
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
66
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
67
|
+
tenantId,
|
|
68
|
+
organizationId,
|
|
69
|
+
userId: auth.sub,
|
|
70
|
+
resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
|
|
71
|
+
resourceId: input.entityId,
|
|
72
|
+
operation: "custom",
|
|
73
|
+
requestMethod: req.method,
|
|
74
|
+
requestHeaders: req.headers,
|
|
75
|
+
metadata: guardResult.metadata ?? null
|
|
76
|
+
});
|
|
77
|
+
}
|
|
41
78
|
const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 });
|
|
42
79
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
43
80
|
response.headers.set(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/api/tags/assign/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../../utils'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },\n}\n\nasync function buildContext(\n req: Request\n): Promise<{ ctx: CommandRuntimeContext; auth: Awaited<ReturnType<typeof getAuthFromRequest>>; translate: (key: string, fallback?: string) => string }> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n return { ctx, auth, translate }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx, auth, translate } = await buildContext(req)\n const body = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(body, ctx, translate)\n const input = tagAssignmentSchema.parse(scoped)\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string }>(\n 'customers.tags.assign',\n { input, ctx },\n )\n const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.tagAssignment',\n resourceId: logEntry.resourceId ?? result?.assignmentId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.tags.assign failed', err)\n return NextResponse.json({ error: translate('customers.errors.assign_failed', 'Failed to assign tag') }, { status: 400 })\n }\n}\n\nconst tagAssignmentResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nconst tagAssignmentErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Assign customer tag',\n methods: {\n POST: {\n summary: 'Assign tag to customer entity',\n description: 'Links a tag to a customer entity within the validated tenant / organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: tagAssignmentSchema,\n },\n responses: [\n { status: 201, description: 'Tag assigned to customer', schema: tagAssignmentResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation or assignment failed', schema: tagAssignmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: tagAssignmentErrorSchema },\n { status: 403, description: 'Insufficient tenant/organization access', schema: tagAssignmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,kCAAkC;AAE3C,SAAS,2BAAoD;AAC7D,SAAS,eAAe,uBAAuB;AAC/C,SAAS,2BAA2B;AACpC,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../../utils'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst TAG_ASSIGNMENT_RESOURCE_KIND = 'customers.tagAssignment'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },\n}\n\nasync function buildContext(\n req: Request\n): Promise<{ ctx: CommandRuntimeContext; auth: Awaited<ReturnType<typeof getAuthFromRequest>>; translate: (key: string, fallback?: string) => string }> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n return { ctx, auth, translate }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx, auth, translate } = await buildContext(req)\n const tenantId = auth!.tenantId\n const organizationId = ctx.selectedOrganizationId\n if (!tenantId || !organizationId) {\n return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })\n }\n const body = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(body, ctx, translate)\n const input = tagAssignmentSchema.parse(scoped)\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId,\n organizationId,\n userId: auth!.sub,\n resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,\n resourceId: input.entityId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string }>(\n 'customers.tags.assign',\n { input, ctx },\n )\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId,\n organizationId,\n userId: auth!.sub,\n resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,\n resourceId: input.entityId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.tagAssignment',\n resourceId: logEntry.resourceId ?? result?.assignmentId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.tags.assign failed', err)\n return NextResponse.json({ error: translate('customers.errors.assign_failed', 'Failed to assign tag') }, { status: 400 })\n }\n}\n\nconst tagAssignmentResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nconst tagAssignmentErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Assign customer tag',\n methods: {\n POST: {\n summary: 'Assign tag to customer entity',\n description: 'Links a tag to a customer entity within the validated tenant / organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: tagAssignmentSchema,\n },\n responses: [\n { status: 201, description: 'Tag assigned to customer', schema: tagAssignmentResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation or assignment failed', schema: tagAssignmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: tagAssignmentErrorSchema },\n { status: 403, description: 'Insufficient tenant/organization access', schema: tagAssignmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,11 @@ import { tagAssignmentSchema } from "../../../data/validators.js";
|
|
|
8
8
|
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
9
9
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
10
10
|
import { withScopedPayload } from "../../utils.js";
|
|
11
|
+
import {
|
|
12
|
+
runCrudMutationGuardAfterSuccess,
|
|
13
|
+
validateCrudMutationGuard
|
|
14
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
15
|
+
const TAG_ASSIGNMENT_RESOURCE_KIND = "customers.tagAssignment";
|
|
11
16
|
const metadata = {
|
|
12
17
|
POST: { requireAuth: true, requireFeatures: ["customers.activities.manage"] }
|
|
13
18
|
};
|
|
@@ -30,14 +35,46 @@ async function buildContext(req) {
|
|
|
30
35
|
async function POST(req) {
|
|
31
36
|
try {
|
|
32
37
|
const { ctx, auth, translate } = await buildContext(req);
|
|
38
|
+
const tenantId = auth.tenantId;
|
|
39
|
+
const organizationId = ctx.selectedOrganizationId;
|
|
40
|
+
if (!tenantId || !organizationId) {
|
|
41
|
+
return NextResponse.json({ error: translate("customers.errors.context_required", "Organization and tenant context required") }, { status: 400 });
|
|
42
|
+
}
|
|
33
43
|
const body = await req.json().catch(() => ({}));
|
|
34
44
|
const scoped = withScopedPayload(body, ctx, translate);
|
|
35
45
|
const input = tagAssignmentSchema.parse(scoped);
|
|
46
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
47
|
+
tenantId,
|
|
48
|
+
organizationId,
|
|
49
|
+
userId: auth.sub,
|
|
50
|
+
resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
|
|
51
|
+
resourceId: input.entityId,
|
|
52
|
+
operation: "custom",
|
|
53
|
+
requestMethod: req.method,
|
|
54
|
+
requestHeaders: req.headers,
|
|
55
|
+
mutationPayload: input
|
|
56
|
+
});
|
|
57
|
+
if (guardResult && !guardResult.ok) {
|
|
58
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
59
|
+
}
|
|
36
60
|
const commandBus = ctx.container.resolve("commandBus");
|
|
37
61
|
const { result, logEntry } = await commandBus.execute(
|
|
38
62
|
"customers.tags.unassign",
|
|
39
63
|
{ input, ctx }
|
|
40
64
|
);
|
|
65
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
66
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
67
|
+
tenantId,
|
|
68
|
+
organizationId,
|
|
69
|
+
userId: auth.sub,
|
|
70
|
+
resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
|
|
71
|
+
resourceId: input.entityId,
|
|
72
|
+
operation: "custom",
|
|
73
|
+
requestMethod: req.method,
|
|
74
|
+
requestHeaders: req.headers,
|
|
75
|
+
metadata: guardResult.metadata ?? null
|
|
76
|
+
});
|
|
77
|
+
}
|
|
41
78
|
const response = NextResponse.json({ id: result?.assignmentId ?? null });
|
|
42
79
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
43
80
|
response.headers.set(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/api/tags/unassign/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../../utils'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },\n}\n\nasync function buildContext(\n req: Request\n): Promise<{ ctx: CommandRuntimeContext; auth: Awaited<ReturnType<typeof getAuthFromRequest>>; translate: (key: string, fallback?: string) => string }> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n return { ctx, auth, translate }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx, auth, translate } = await buildContext(req)\n const body = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(body, ctx, translate)\n const input = tagAssignmentSchema.parse(scoped)\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string | null }>(\n 'customers.tags.unassign',\n { input, ctx },\n )\n const response = NextResponse.json({ id: result?.assignmentId ?? null })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.tagAssignment',\n resourceId: logEntry.resourceId ?? result?.assignmentId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.tags.unassign failed', err)\n return NextResponse.json({ error: translate('customers.errors.unassign_failed', 'Failed to unassign tag') }, { status: 400 })\n }\n}\n\nconst tagUnassignResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nconst tagUnassignErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Unassign customer tag',\n methods: {\n POST: {\n summary: 'Remove tag from customer entity',\n description: 'Detaches a tag from a customer entity within the validated tenant / organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: tagAssignmentSchema,\n },\n responses: [\n { status: 200, description: 'Tag unassigned from customer', schema: tagUnassignResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation or unassignment failed', schema: tagUnassignErrorSchema },\n { status: 401, description: 'Unauthorized', schema: tagUnassignErrorSchema },\n { status: 403, description: 'Insufficient tenant/organization access', schema: tagUnassignErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,kCAAkC;AAE3C,SAAS,2BAAoD;AAC7D,SAAS,eAAe,uBAAuB;AAC/C,SAAS,2BAA2B;AACpC,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/validators'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../../utils'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst TAG_ASSIGNMENT_RESOURCE_KIND = 'customers.tagAssignment'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },\n}\n\nasync function buildContext(\n req: Request\n): Promise<{ ctx: CommandRuntimeContext; auth: Awaited<ReturnType<typeof getAuthFromRequest>>; translate: (key: string, fallback?: string) => string }> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n return { ctx, auth, translate }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx, auth, translate } = await buildContext(req)\n const tenantId = auth!.tenantId\n const organizationId = ctx.selectedOrganizationId\n if (!tenantId || !organizationId) {\n return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })\n }\n const body = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(body, ctx, translate)\n const input = tagAssignmentSchema.parse(scoped)\n\n const guardResult = await validateCrudMutationGuard(ctx.container, {\n tenantId,\n organizationId,\n userId: auth!.sub,\n resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,\n resourceId: input.entityId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const commandBus = (ctx.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string | null }>(\n 'customers.tags.unassign',\n { input, ctx },\n )\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(ctx.container, {\n tenantId,\n organizationId,\n userId: auth!.sub,\n resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,\n resourceId: input.entityId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n const response = NextResponse.json({ id: result?.assignmentId ?? null })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.tagAssignment',\n resourceId: logEntry.resourceId ?? result?.assignmentId ?? null,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n console.error('customers.tags.unassign failed', err)\n return NextResponse.json({ error: translate('customers.errors.unassign_failed', 'Failed to unassign tag') }, { status: 400 })\n }\n}\n\nconst tagUnassignResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nconst tagUnassignErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Unassign customer tag',\n methods: {\n POST: {\n summary: 'Remove tag from customer entity',\n description: 'Detaches a tag from a customer entity within the validated tenant / organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: tagAssignmentSchema,\n },\n responses: [\n { status: 200, description: 'Tag unassigned from customer', schema: tagUnassignResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation or unassignment failed', schema: tagUnassignErrorSchema },\n { status: 401, description: 'Unauthorized', schema: tagUnassignErrorSchema },\n { status: 403, description: 'Insufficient tenant/organization access', schema: tagUnassignErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { format } from "date-fns/format";
|
|
5
4
|
import { CalendarRange, ListFilter, Settings } from "lucide-react";
|
|
6
5
|
import { Badge } from "@open-mercato/ui/primitives/badge";
|
|
7
6
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
@@ -18,14 +17,12 @@ import {
|
|
|
18
17
|
SelectTrigger,
|
|
19
18
|
SelectValue
|
|
20
19
|
} from "@open-mercato/ui/primitives/select";
|
|
21
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
20
|
+
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
21
|
+
import { formatDateRangeLabel } from "../../lib/calendar/format.js";
|
|
22
22
|
const RANGE_PRESETS = ["thisWeek", "next7", "thisMonth", "next30"];
|
|
23
23
|
const STATUS_OPTIONS = ["planned", "done", "canceled"];
|
|
24
24
|
const ALL_OPTION = "all";
|
|
25
25
|
const EMPTY_FILTERS = { types: [], status: null, ownerUserId: null };
|
|
26
|
-
function formatRangeLabel(from, to) {
|
|
27
|
-
return `${format(from, "MMM dd")} \u2013 ${format(to, "MMM dd, yyyy")}`;
|
|
28
|
-
}
|
|
29
26
|
function CalendarToolbar(props) {
|
|
30
27
|
const {
|
|
31
28
|
anchor,
|
|
@@ -43,6 +40,7 @@ function CalendarToolbar(props) {
|
|
|
43
40
|
onOpenSettings
|
|
44
41
|
} = props;
|
|
45
42
|
const t = useT();
|
|
43
|
+
const locale = useLocale();
|
|
46
44
|
const [rangeOpen, setRangeOpen] = React.useState(false);
|
|
47
45
|
const [filtersOpen, setFiltersOpen] = React.useState(false);
|
|
48
46
|
const [pendingFilters, setPendingFilters] = React.useState(filters);
|
|
@@ -108,7 +106,7 @@ function CalendarToolbar(props) {
|
|
|
108
106
|
className: "min-w-0 text-muted-foreground sm:-ml-px sm:rounded-l-none",
|
|
109
107
|
children: [
|
|
110
108
|
/* @__PURE__ */ jsx(CalendarRange, { "aria-hidden": "true" }),
|
|
111
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children:
|
|
109
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: formatDateRangeLabel(locale, range.from, range.to) })
|
|
112
110
|
]
|
|
113
111
|
}
|
|
114
112
|
) }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/calendar/CalendarToolbar.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { format } from 'date-fns/format'\nimport { CalendarRange, ListFilter, Settings } from 'lucide-react'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Calendar } from '@open-mercato/ui/primitives/calendar'\nimport { CheckboxField } from '@open-mercato/ui/primitives/checkbox-field'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Kbd } from '@open-mercato/ui/primitives/kbd'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SearchInput } from '@open-mercato/ui/primitives/search-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type {\n CalendarFiltersValue,\n CalendarRangePreset,\n CalendarToolbarProps,\n} from './types'\n\nconst RANGE_PRESETS: CalendarRangePreset[] = ['thisWeek', 'next7', 'thisMonth', 'next30']\n\nconst STATUS_OPTIONS = ['planned', 'done', 'canceled'] as const\n\nconst ALL_OPTION = 'all'\n\nconst EMPTY_FILTERS: CalendarFiltersValue = { types: [], status: null, ownerUserId: null }\n\nfunction formatRangeLabel(from: Date, to: Date): string {\n return `${format(from, 'MMM dd')} \u2013 ${format(to, 'MMM dd, yyyy')}`\n}\n\nexport function CalendarToolbar(props: CalendarToolbarProps) {\n const {\n anchor,\n range,\n preset,\n search,\n filters,\n typeOptions,\n ownerOptions,\n onToday,\n onPresetChange,\n onAnchorChange,\n onSearchChange,\n onFiltersChange,\n onOpenSettings,\n } = props\n const t = useT()\n const [rangeOpen, setRangeOpen] = React.useState(false)\n const [filtersOpen, setFiltersOpen] = React.useState(false)\n const [pendingFilters, setPendingFilters] = React.useState<CalendarFiltersValue>(filters)\n\n const presetLabels: Record<CalendarRangePreset, string> = {\n thisWeek: t('customers.calendar.toolbar.presets.thisWeek', 'This week'),\n next7: t('customers.calendar.toolbar.presets.next7', 'Next 7 days'),\n thisMonth: t('customers.calendar.toolbar.presets.thisMonth', 'This month'),\n next30: t('customers.calendar.toolbar.presets.next30', 'Next 30 days'),\n }\n\n const activeFilterCount =\n filters.types.length + (filters.status ? 1 : 0) + (filters.ownerUserId ? 1 : 0)\n\n const handleFiltersOpenChange = (open: boolean) => {\n if (open) setPendingFilters(filters)\n setFiltersOpen(open)\n }\n\n const applyFilters = () => {\n onFiltersChange(pendingFilters)\n setFiltersOpen(false)\n }\n\n const clearFilters = () => {\n setPendingFilters(EMPTY_FILTERS)\n onFiltersChange(EMPTY_FILTERS)\n setFiltersOpen(false)\n }\n\n const togglePendingType = (value: string, checked: boolean) => {\n setPendingFilters((current) => ({\n ...current,\n types: checked\n ? [...current.types, value]\n : current.types.filter((entry) => entry !== value),\n }))\n }\n\n return (\n <div className=\"flex flex-wrap items-center gap-2 sm:gap-3\">\n <div className=\"flex min-w-0 flex-wrap items-center gap-2 sm:gap-3\">\n <Button type=\"button\" variant=\"outline\" onClick={onToday}>\n {t('customers.calendar.toolbar.today', 'Today')}\n </Button>\n <div className=\"flex min-w-0 items-center\">\n <Select\n value={preset ?? ''}\n onValueChange={(value) => onPresetChange(value as CalendarRangePreset)}\n >\n <SelectTrigger\n className=\"hidden w-auto min-w-32 rounded-r-none sm:flex\"\n aria-label={t('customers.calendar.toolbar.presetLabel', 'Date range preset')}\n >\n <SelectValue\n placeholder={t('customers.calendar.toolbar.presetPlaceholder', 'Custom range')}\n />\n </SelectTrigger>\n <SelectContent>\n {RANGE_PRESETS.map((value) => (\n <SelectItem key={value} value={value}>\n {presetLabels[value]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Popover open={rangeOpen} onOpenChange={setRangeOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"min-w-0 text-muted-foreground sm:-ml-px sm:rounded-l-none\"\n >\n <CalendarRange aria-hidden=\"true\" />\n <span className=\"truncate\">{formatRangeLabel(range.from, range.to)}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-auto min-w-0 p-2\">\n <Calendar\n mode=\"single\"\n selected={anchor}\n defaultMonth={anchor}\n onSelect={(date) => {\n if (!date) return\n onAnchorChange(date)\n setRangeOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n <div className=\"flex w-full min-w-0 items-center gap-2 sm:ml-auto sm:w-auto sm:flex-none sm:gap-3\">\n <div className=\"relative min-w-0 flex-1 sm:w-72 sm:flex-none\">\n <SearchInput\n value={search}\n onChange={onSearchChange}\n placeholder={t('customers.calendar.toolbar.searchPlaceholder', 'Search\u2026')}\n aria-label={t('customers.calendar.toolbar.searchPlaceholder', 'Search\u2026')}\n data-calendar-search=\"\"\n />\n {search.length === 0 ? (\n <Kbd className=\"pointer-events-none absolute right-2 top-1/2 hidden -translate-y-1/2 sm:inline-flex\">/</Kbd>\n ) : null}\n </div>\n <Popover open={filtersOpen} onOpenChange={handleFiltersOpenChange}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n aria-label={t('customers.calendar.toolbar.filters.label', 'Filter')}\n >\n <ListFilter aria-hidden=\"true\" />\n <span className=\"hidden sm:inline\">\n {t('customers.calendar.toolbar.filters.label', 'Filter')}\n </span>\n {activeFilterCount > 0 ? (\n <Badge variant=\"secondary\" size=\"sm\">\n {activeFilterCount}\n </Badge>\n ) : null}\n </Button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n className=\"w-72 p-3\"\n onKeyDown={(event) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n applyFilters()\n }\n }}\n >\n <div className=\"flex flex-col gap-4\">\n {typeOptions.length > 0 ? (\n <fieldset className=\"flex flex-col gap-2\">\n <legend className=\"pb-1 text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.types', 'Type')}\n </legend>\n {typeOptions.map((option) => (\n <CheckboxField\n key={option.value}\n label={option.label}\n size=\"sm\"\n checked={pendingFilters.types.includes(option.value)}\n onCheckedChange={(checked) =>\n togglePendingType(option.value, checked === true)\n }\n />\n ))}\n </fieldset>\n ) : null}\n <div className=\"flex flex-col gap-1.5\">\n <span className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.status', 'Status')}\n </span>\n <Select\n value={pendingFilters.status ?? ALL_OPTION}\n onValueChange={(value) =>\n setPendingFilters((current) => ({\n ...current,\n status: value === ALL_OPTION ? null : value,\n }))\n }\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.calendar.toolbar.filters.status', 'Status')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_OPTION}>\n {t('customers.calendar.toolbar.filters.allStatuses', 'All statuses')}\n </SelectItem>\n {STATUS_OPTIONS.map((status) => (\n <SelectItem key={status} value={status}>\n {t(`customers.calendar.toolbar.filters.statuses.${status}`, status)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n {ownerOptions.length > 0 ? (\n <div className=\"flex flex-col gap-1.5\">\n <span className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.owner', 'Owner')}\n </span>\n <Select\n value={pendingFilters.ownerUserId ?? ALL_OPTION}\n onValueChange={(value) =>\n setPendingFilters((current) => ({\n ...current,\n ownerUserId: value === ALL_OPTION ? null : value,\n }))\n }\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.calendar.toolbar.filters.owner', 'Owner')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_OPTION}>\n {t('customers.calendar.toolbar.filters.allOwners', 'All owners')}\n </SelectItem>\n {ownerOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : null}\n <div className=\"flex items-center justify-end gap-2 border-t pt-3\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={clearFilters}>\n {t('customers.calendar.toolbar.filters.clear', 'Clear')}\n </Button>\n <Button type=\"button\" size=\"sm\" onClick={applyFilters}>\n {t('customers.calendar.toolbar.filters.apply', 'Apply')}\n </Button>\n </div>\n </div>\n </PopoverContent>\n </Popover>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n aria-label={t('customers.calendar.toolbar.settings', 'Calendar settings')}\n onClick={onOpenSettings}\n >\n <Settings aria-hidden />\n </IconButton>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CalendarRange, ListFilter, Settings } from 'lucide-react'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Calendar } from '@open-mercato/ui/primitives/calendar'\nimport { CheckboxField } from '@open-mercato/ui/primitives/checkbox-field'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Kbd } from '@open-mercato/ui/primitives/kbd'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SearchInput } from '@open-mercato/ui/primitives/search-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { formatDateRangeLabel } from '../../lib/calendar/format'\nimport type {\n CalendarFiltersValue,\n CalendarRangePreset,\n CalendarToolbarProps,\n} from './types'\n\nconst RANGE_PRESETS: CalendarRangePreset[] = ['thisWeek', 'next7', 'thisMonth', 'next30']\n\nconst STATUS_OPTIONS = ['planned', 'done', 'canceled'] as const\n\nconst ALL_OPTION = 'all'\n\nconst EMPTY_FILTERS: CalendarFiltersValue = { types: [], status: null, ownerUserId: null }\n\nexport function CalendarToolbar(props: CalendarToolbarProps) {\n const {\n anchor,\n range,\n preset,\n search,\n filters,\n typeOptions,\n ownerOptions,\n onToday,\n onPresetChange,\n onAnchorChange,\n onSearchChange,\n onFiltersChange,\n onOpenSettings,\n } = props\n const t = useT()\n const locale = useLocale()\n const [rangeOpen, setRangeOpen] = React.useState(false)\n const [filtersOpen, setFiltersOpen] = React.useState(false)\n const [pendingFilters, setPendingFilters] = React.useState<CalendarFiltersValue>(filters)\n\n const presetLabels: Record<CalendarRangePreset, string> = {\n thisWeek: t('customers.calendar.toolbar.presets.thisWeek', 'This week'),\n next7: t('customers.calendar.toolbar.presets.next7', 'Next 7 days'),\n thisMonth: t('customers.calendar.toolbar.presets.thisMonth', 'This month'),\n next30: t('customers.calendar.toolbar.presets.next30', 'Next 30 days'),\n }\n\n const activeFilterCount =\n filters.types.length + (filters.status ? 1 : 0) + (filters.ownerUserId ? 1 : 0)\n\n const handleFiltersOpenChange = (open: boolean) => {\n if (open) setPendingFilters(filters)\n setFiltersOpen(open)\n }\n\n const applyFilters = () => {\n onFiltersChange(pendingFilters)\n setFiltersOpen(false)\n }\n\n const clearFilters = () => {\n setPendingFilters(EMPTY_FILTERS)\n onFiltersChange(EMPTY_FILTERS)\n setFiltersOpen(false)\n }\n\n const togglePendingType = (value: string, checked: boolean) => {\n setPendingFilters((current) => ({\n ...current,\n types: checked\n ? [...current.types, value]\n : current.types.filter((entry) => entry !== value),\n }))\n }\n\n return (\n <div className=\"flex flex-wrap items-center gap-2 sm:gap-3\">\n <div className=\"flex min-w-0 flex-wrap items-center gap-2 sm:gap-3\">\n <Button type=\"button\" variant=\"outline\" onClick={onToday}>\n {t('customers.calendar.toolbar.today', 'Today')}\n </Button>\n <div className=\"flex min-w-0 items-center\">\n <Select\n value={preset ?? ''}\n onValueChange={(value) => onPresetChange(value as CalendarRangePreset)}\n >\n <SelectTrigger\n className=\"hidden w-auto min-w-32 rounded-r-none sm:flex\"\n aria-label={t('customers.calendar.toolbar.presetLabel', 'Date range preset')}\n >\n <SelectValue\n placeholder={t('customers.calendar.toolbar.presetPlaceholder', 'Custom range')}\n />\n </SelectTrigger>\n <SelectContent>\n {RANGE_PRESETS.map((value) => (\n <SelectItem key={value} value={value}>\n {presetLabels[value]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Popover open={rangeOpen} onOpenChange={setRangeOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"min-w-0 text-muted-foreground sm:-ml-px sm:rounded-l-none\"\n >\n <CalendarRange aria-hidden=\"true\" />\n <span className=\"truncate\">{formatDateRangeLabel(locale, range.from, range.to)}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-auto min-w-0 p-2\">\n <Calendar\n mode=\"single\"\n selected={anchor}\n defaultMonth={anchor}\n onSelect={(date) => {\n if (!date) return\n onAnchorChange(date)\n setRangeOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n </div>\n <div className=\"flex w-full min-w-0 items-center gap-2 sm:ml-auto sm:w-auto sm:flex-none sm:gap-3\">\n <div className=\"relative min-w-0 flex-1 sm:w-72 sm:flex-none\">\n <SearchInput\n value={search}\n onChange={onSearchChange}\n placeholder={t('customers.calendar.toolbar.searchPlaceholder', 'Search\u2026')}\n aria-label={t('customers.calendar.toolbar.searchPlaceholder', 'Search\u2026')}\n data-calendar-search=\"\"\n />\n {search.length === 0 ? (\n <Kbd className=\"pointer-events-none absolute right-2 top-1/2 hidden -translate-y-1/2 sm:inline-flex\">/</Kbd>\n ) : null}\n </div>\n <Popover open={filtersOpen} onOpenChange={handleFiltersOpenChange}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n aria-label={t('customers.calendar.toolbar.filters.label', 'Filter')}\n >\n <ListFilter aria-hidden=\"true\" />\n <span className=\"hidden sm:inline\">\n {t('customers.calendar.toolbar.filters.label', 'Filter')}\n </span>\n {activeFilterCount > 0 ? (\n <Badge variant=\"secondary\" size=\"sm\">\n {activeFilterCount}\n </Badge>\n ) : null}\n </Button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n className=\"w-72 p-3\"\n onKeyDown={(event) => {\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n applyFilters()\n }\n }}\n >\n <div className=\"flex flex-col gap-4\">\n {typeOptions.length > 0 ? (\n <fieldset className=\"flex flex-col gap-2\">\n <legend className=\"pb-1 text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.types', 'Type')}\n </legend>\n {typeOptions.map((option) => (\n <CheckboxField\n key={option.value}\n label={option.label}\n size=\"sm\"\n checked={pendingFilters.types.includes(option.value)}\n onCheckedChange={(checked) =>\n togglePendingType(option.value, checked === true)\n }\n />\n ))}\n </fieldset>\n ) : null}\n <div className=\"flex flex-col gap-1.5\">\n <span className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.status', 'Status')}\n </span>\n <Select\n value={pendingFilters.status ?? ALL_OPTION}\n onValueChange={(value) =>\n setPendingFilters((current) => ({\n ...current,\n status: value === ALL_OPTION ? null : value,\n }))\n }\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.calendar.toolbar.filters.status', 'Status')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_OPTION}>\n {t('customers.calendar.toolbar.filters.allStatuses', 'All statuses')}\n </SelectItem>\n {STATUS_OPTIONS.map((status) => (\n <SelectItem key={status} value={status}>\n {t(`customers.calendar.toolbar.filters.statuses.${status}`, status)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n {ownerOptions.length > 0 ? (\n <div className=\"flex flex-col gap-1.5\">\n <span className=\"text-overline font-semibold uppercase tracking-widest text-muted-foreground\">\n {t('customers.calendar.toolbar.filters.owner', 'Owner')}\n </span>\n <Select\n value={pendingFilters.ownerUserId ?? ALL_OPTION}\n onValueChange={(value) =>\n setPendingFilters((current) => ({\n ...current,\n ownerUserId: value === ALL_OPTION ? null : value,\n }))\n }\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t('customers.calendar.toolbar.filters.owner', 'Owner')}\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_OPTION}>\n {t('customers.calendar.toolbar.filters.allOwners', 'All owners')}\n </SelectItem>\n {ownerOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : null}\n <div className=\"flex items-center justify-end gap-2 border-t pt-3\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={clearFilters}>\n {t('customers.calendar.toolbar.filters.clear', 'Clear')}\n </Button>\n <Button type=\"button\" size=\"sm\" onClick={applyFilters}>\n {t('customers.calendar.toolbar.filters.apply', 'Apply')}\n </Button>\n </div>\n </div>\n </PopoverContent>\n </Popover>\n <IconButton\n type=\"button\"\n variant=\"outline\"\n aria-label={t('customers.calendar.toolbar.settings', 'Calendar settings')}\n onClick={onOpenSettings}\n >\n <Settings aria-hidden />\n </IconButton>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,6 +6,7 @@ import { cn } from "@open-mercato/shared/lib/utils";
|
|
|
6
6
|
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
7
|
import { Avatar, AvatarStack } from "@open-mercato/ui/primitives/avatar";
|
|
8
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
9
|
+
import { formatTimeRangeLabel } from "../../lib/calendar/format.js";
|
|
9
10
|
const SHOW_TIME_MIN_HEIGHT_PX = 44;
|
|
10
11
|
const SHOW_META_MIN_HEIGHT_PX = 96;
|
|
11
12
|
const WRAP_TITLE_MIN_HEIGHT_PX = 128;
|
|
@@ -50,12 +51,7 @@ function resolveEventTone(item, nowMs) {
|
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
function formatTimeRange(locale, start, end) {
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
return formatter.formatRange(start, end);
|
|
56
|
-
} catch {
|
|
57
|
-
return `${formatter.format(start)} - ${formatter.format(end)}`;
|
|
58
|
-
}
|
|
54
|
+
return formatTimeRangeLabel(locale, start, end);
|
|
59
55
|
}
|
|
60
56
|
function participantLabel(participant) {
|
|
61
57
|
return participant.name ?? participant.email ?? "?";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/calendar/EventBlock.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Globe, MapPin } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport type { CalendarItem, CalendarPlatform } from './types'\n\nconst SHOW_TIME_MIN_HEIGHT_PX = 44\nconst SHOW_META_MIN_HEIGHT_PX = 96\nconst WRAP_TITLE_MIN_HEIGHT_PX = 128\nconst MAX_VISIBLE_AVATARS = 3\n\nconst PLATFORM_LABELS: Record<CalendarPlatform, { key: string; fallback: string }> = {\n zoom: { key: 'customers.calendar.platform.zoom', fallback: 'Zoom' },\n meet: { key: 'customers.calendar.platform.meet', fallback: 'Meet' },\n slack: { key: 'customers.calendar.platform.slack', fallback: 'Slack' },\n teams: { key: 'customers.calendar.platform.teams', fallback: 'Teams' },\n}\n\nexport type EventTone = {\n surfaceClassName: string\n titleClassName: string\n subClassName: string\n style?: React.CSSProperties\n}\n\nfunction softTintStyle(color: string): React.CSSProperties {\n if (/^#[0-9a-fA-F]{6}$/.test(color)) return { backgroundColor: `${color}1A` }\n return { backgroundColor: `color-mix(in srgb, ${color} 10%, transparent)` }\n}\n\nexport function resolveEventTone(item: CalendarItem, nowMs: number): EventTone {\n if (item.status === 'canceled') {\n return {\n surfaceClassName: 'bg-muted/60',\n titleClassName: 'text-muted-foreground line-through',\n subClassName: 'text-muted-foreground/70',\n }\n }\n if (item.status === 'done' || item.end.getTime() < nowMs) {\n return {\n surfaceClassName: 'bg-muted',\n titleClassName: 'text-muted-foreground',\n subClassName: 'text-muted-foreground',\n }\n }\n if (item.color) {\n return {\n surfaceClassName: '',\n titleClassName: 'text-foreground',\n subClassName: 'text-muted-foreground',\n style: softTintStyle(item.color),\n }\n }\n return {\n surfaceClassName: 'bg-muted/60',\n titleClassName: 'text-foreground',\n subClassName: 'text-muted-foreground',\n }\n}\n\nexport function formatTimeRange(locale: string, start: Date, end: Date): string {\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Globe, MapPin } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { formatTimeRangeLabel } from '../../lib/calendar/format'\nimport type { CalendarItem, CalendarPlatform } from './types'\n\nconst SHOW_TIME_MIN_HEIGHT_PX = 44\nconst SHOW_META_MIN_HEIGHT_PX = 96\nconst WRAP_TITLE_MIN_HEIGHT_PX = 128\nconst MAX_VISIBLE_AVATARS = 3\n\nconst PLATFORM_LABELS: Record<CalendarPlatform, { key: string; fallback: string }> = {\n zoom: { key: 'customers.calendar.platform.zoom', fallback: 'Zoom' },\n meet: { key: 'customers.calendar.platform.meet', fallback: 'Meet' },\n slack: { key: 'customers.calendar.platform.slack', fallback: 'Slack' },\n teams: { key: 'customers.calendar.platform.teams', fallback: 'Teams' },\n}\n\nexport type EventTone = {\n surfaceClassName: string\n titleClassName: string\n subClassName: string\n style?: React.CSSProperties\n}\n\nfunction softTintStyle(color: string): React.CSSProperties {\n if (/^#[0-9a-fA-F]{6}$/.test(color)) return { backgroundColor: `${color}1A` }\n return { backgroundColor: `color-mix(in srgb, ${color} 10%, transparent)` }\n}\n\nexport function resolveEventTone(item: CalendarItem, nowMs: number): EventTone {\n if (item.status === 'canceled') {\n return {\n surfaceClassName: 'bg-muted/60',\n titleClassName: 'text-muted-foreground line-through',\n subClassName: 'text-muted-foreground/70',\n }\n }\n if (item.status === 'done' || item.end.getTime() < nowMs) {\n return {\n surfaceClassName: 'bg-muted',\n titleClassName: 'text-muted-foreground',\n subClassName: 'text-muted-foreground',\n }\n }\n if (item.color) {\n return {\n surfaceClassName: '',\n titleClassName: 'text-foreground',\n subClassName: 'text-muted-foreground',\n style: softTintStyle(item.color),\n }\n }\n return {\n surfaceClassName: 'bg-muted/60',\n titleClassName: 'text-foreground',\n subClassName: 'text-muted-foreground',\n }\n}\n\nexport function formatTimeRange(locale: string, start: Date, end: Date): string {\n return formatTimeRangeLabel(locale, start, end)\n}\n\nfunction participantLabel(participant: CalendarItem['participants'][number]): string {\n return participant.name ?? participant.email ?? '?'\n}\n\ntype LocationMetaProps = {\n item: CalendarItem\n subClassName: string\n accentColor: string | null\n}\n\nfunction LocationMeta({ item, subClassName, accentColor }: LocationMetaProps) {\n const t = useT()\n if (!item.location || !item.locationKind) return null\n const iconStyle = accentColor ? { color: accentColor } : undefined\n if (item.locationKind === 'platform' && item.platform) {\n const platform = PLATFORM_LABELS[item.platform]\n return (\n <span className={cn('truncate text-xs', subClassName)}>\n {t('customers.calendar.grid.onPlatform', 'on {platform}', { platform: t(platform.key, platform.fallback) })}\n </span>\n )\n }\n if (item.locationKind === 'url') {\n return (\n <>\n <Globe className={cn('size-4 shrink-0', !accentColor && subClassName)} style={iconStyle} aria-hidden />\n <span className=\"truncate text-xs text-foreground\">{item.location}</span>\n </>\n )\n }\n return (\n <>\n <MapPin className={cn('size-4 shrink-0', !accentColor && subClassName)} style={iconStyle} aria-hidden />\n <span className=\"truncate text-xs text-foreground\">\n {t('customers.calendar.grid.venue', 'Venue: {name}', { name: item.location })}\n </span>\n </>\n )\n}\n\nexport type EventBlockProps = {\n item: CalendarItem\n top: number\n height: number\n insetInlineStart: string\n width: string\n conflicted: boolean\n highlighted: boolean\n selected?: boolean\n nowMs: number\n} & Omit<React.ComponentProps<typeof Button>, 'style' | 'children'>\n\nexport const EventBlock = React.forwardRef<HTMLButtonElement, EventBlockProps>(function EventBlock(\n {\n item,\n top,\n height,\n insetInlineStart,\n width,\n conflicted,\n highlighted,\n selected,\n nowMs,\n className,\n ...buttonProps\n },\n ref,\n) {\n const t = useT()\n const locale = useLocale()\n const tone = resolveEventTone(item, nowMs)\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n const timeRange = formatTimeRange(locale, item.start, item.end)\n const showTime = height >= SHOW_TIME_MIN_HEIGHT_PX\n const wrapTitle = height >= WRAP_TITLE_MIN_HEIGHT_PX\n const hasMetaContent = item.participants.length > 0 || (item.location !== null && item.locationKind !== null)\n const showMeta = height >= SHOW_META_MIN_HEIGHT_PX && hasMetaContent\n const visibleParticipants = item.participants.slice(0, MAX_VISIBLE_AVATARS)\n const overflowCount = item.participants.length - visibleParticipants.length\n\n return (\n <Button\n ref={ref}\n type=\"button\"\n variant=\"ghost\"\n aria-label={`${title}, ${timeRange}`}\n className={cn(\n 'pointer-events-auto absolute h-auto flex-col items-start justify-start gap-1 whitespace-normal overflow-hidden rounded-md px-1.5 text-start outline-none hover:bg-muted/70 sm:px-3',\n height >= SHOW_TIME_MIN_HEIGHT_PX ? 'py-1 sm:py-2' : 'py-0.5 sm:py-1',\n tone.surfaceClassName,\n conflicted && 'ring-1 ring-status-warning-icon',\n selected && 'shadow-md ring-2 ring-foreground',\n highlighted && 'motion-safe:animate-pulse',\n conflicted || highlighted || selected ? 'z-30' : 'z-10',\n 'focus-visible:z-30 focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n style={{ top, height, insetInlineStart, width, ...tone.style }}\n {...buttonProps}\n >\n <span className={cn('w-full text-xs font-medium leading-4', wrapTitle ? 'line-clamp-2' : 'truncate', tone.titleClassName)}>\n {title}\n </span>\n {showTime ? (\n <span className={cn('w-full truncate text-overline uppercase tracking-wide', tone.subClassName)}>\n {timeRange}\n </span>\n ) : null}\n {showMeta ? (\n <span className=\"mt-auto flex w-full min-w-0 items-center gap-1.5\">\n {visibleParticipants.length > 0 ? (\n <>\n <AvatarStack size=\"xs\" max={MAX_VISIBLE_AVATARS} className=\"shrink-0 gap-px [&>*:not(:first-child)]:-ml-1\">\n {visibleParticipants.map((participant) => (\n <Avatar key={participant.userId} size=\"xs\" label={participantLabel(participant)} />\n ))}\n </AvatarStack>\n {overflowCount > 0 ? (\n <span className={cn('shrink-0 text-xs', tone.subClassName)}>+{overflowCount}</span>\n ) : null}\n </>\n ) : null}\n <span className=\"ms-auto flex min-w-0 items-center gap-1.5\">\n <LocationMeta item={item} subClassName={tone.subClassName} accentColor={item.color} />\n </span>\n </span>\n ) : null}\n </Button>\n )\n})\n\nEventBlock.displayName = 'EventBlock'\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["EventBlock"]
|
|
7
7
|
}
|