@open-mercato/core 0.10.1-develop.7701.1.87b1ad4224 → 0.10.1-develop.7706.1.9cb1f164c4
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/payment_gateways/api/transactions/route.js +7 -7
- package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +16 -12
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/timesheet/GridView.js +1 -1
- package/dist/modules/staff/backend/staff/time-tracking/timesheet/GridView.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/payment_gateways/api/transactions/route.ts +7 -7
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +22 -12
- package/src/modules/staff/backend/staff/time-tracking/timesheet/GridView.tsx +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -49,13 +49,13 @@ async function GET(req) {
|
|
|
49
49
|
if (search) {
|
|
50
50
|
const pattern = buildIlikeTerm(search);
|
|
51
51
|
qb.andWhere(`(
|
|
52
|
-
cast(gt.id as text) ilike ?
|
|
53
|
-
or cast(gt.payment_id as text) ilike ?
|
|
54
|
-
or coalesce(gt.provider_key, '') ilike ?
|
|
55
|
-
or coalesce(gt.provider_session_id, '') ilike ?
|
|
56
|
-
or coalesce(gt.gateway_payment_id, '') ilike ?
|
|
57
|
-
or coalesce(gt.gateway_refund_id, '') ilike ?
|
|
58
|
-
)
|
|
52
|
+
cast(gt.id as text) ilike ? escape '\\'
|
|
53
|
+
or cast(gt.payment_id as text) ilike ? escape '\\'
|
|
54
|
+
or coalesce(gt.provider_key, '') ilike ? escape '\\'
|
|
55
|
+
or coalesce(gt.provider_session_id, '') ilike ? escape '\\'
|
|
56
|
+
or coalesce(gt.gateway_payment_id, '') ilike ? escape '\\'
|
|
57
|
+
or coalesce(gt.gateway_refund_id, '') ilike ? escape '\\'
|
|
58
|
+
)`, [pattern, pattern, pattern, pattern, pattern, pattern]);
|
|
59
59
|
}
|
|
60
60
|
const countQb = qb.clone();
|
|
61
61
|
qb.orderBy({ createdAt: "desc" });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/payment_gateways/api/transactions/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { GatewayTransaction } from '../../data/entities'\nimport { listTransactionsQuerySchema } from '../../data/validators'\nimport { paymentGatewaysTag } from '../openapi'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\n\nexport const metadata = {\n path: '/payment_gateways/transactions',\n GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },\n}\n\nfunction formatDateValue(value: unknown): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed.toISOString()\n return value\n }\n const fallback = new Date(value as string | number)\n return Number.isNaN(fallback.getTime()) ? null : fallback.toISOString()\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = listTransactionsQuerySchema.safeParse(Object.fromEntries(url.searchParams.entries()))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const { page, pageSize, search, providerKey, status } = parsed.data\n const offset = (page - 1) * pageSize\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const qb = em.createQueryBuilder(GatewayTransaction, 'gt')\n\n qb.where({\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n deletedAt: null,\n })\n\n if (providerKey) {\n qb.andWhere({ providerKey })\n }\n if (status) {\n qb.andWhere({ unifiedStatus: status })\n }\n if (search) {\n const pattern = buildIlikeTerm(search)\n qb.andWhere(`(\n cast(gt.id as text) ilike
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,mCAAmC;AAC5C,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAExB,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEA,SAAS,gBAAgB,OAA+B;AACtD,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,SAAS,IAAI,KAAK,KAAK;AAC7B,QAAI,CAAC,OAAO,MAAM,OAAO,QAAQ,CAAC,EAAG,QAAO,OAAO,YAAY;AAC/D,WAAO;AAAA,EACT;AACA,QAAM,WAAW,IAAI,KAAK,KAAwB;AAClD,SAAO,OAAO,MAAM,SAAS,QAAQ,CAAC,IAAI,OAAO,SAAS,YAAY;AACxE;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,SAAS,4BAA4B,UAAU,OAAO,YAAY,IAAI,aAAa,QAAQ,CAAC,CAAC;AACnG,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,SAAS,OAAO,MAAM,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvG;AAEA,QAAM,EAAE,MAAM,UAAU,QAAQ,aAAa,OAAO,IAAI,OAAO;AAC/D,QAAM,UAAU,OAAO,KAAK;AAC5B,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,QAAM,KAAK,QAAQ,IAAI;AACvB,QAAM,KAAK,GAAG,mBAAmB,oBAAoB,IAAI;AAEzD,KAAG,MAAM;AAAA,IACP,gBAAgB,KAAK;AAAA,IACrB,UAAU,KAAK;AAAA,IACf,WAAW;AAAA,EACb,CAAC;AAED,MAAI,aAAa;AACf,OAAG,SAAS,EAAE,YAAY,CAAC;AAAA,EAC7B;AACA,MAAI,QAAQ;AACV,OAAG,SAAS,EAAE,eAAe,OAAO,CAAC;AAAA,EACvC;AACA,MAAI,QAAQ;AACV,UAAM,UAAU,eAAe,MAAM;AACrC,OAAG,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { GatewayTransaction } from '../../data/entities'\nimport { listTransactionsQuerySchema } from '../../data/validators'\nimport { paymentGatewaysTag } from '../openapi'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\n\nexport const metadata = {\n path: '/payment_gateways/transactions',\n GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },\n}\n\nfunction formatDateValue(value: unknown): string | null {\n if (!value) return null\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed.toISOString()\n return value\n }\n const fallback = new Date(value as string | number)\n return Number.isNaN(fallback.getTime()) ? null : fallback.toISOString()\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = listTransactionsQuerySchema.safeParse(Object.fromEntries(url.searchParams.entries()))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const { page, pageSize, search, providerKey, status } = parsed.data\n const offset = (page - 1) * pageSize\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const qb = em.createQueryBuilder(GatewayTransaction, 'gt')\n\n qb.where({\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n deletedAt: null,\n })\n\n if (providerKey) {\n qb.andWhere({ providerKey })\n }\n if (status) {\n qb.andWhere({ unifiedStatus: status })\n }\n if (search) {\n const pattern = buildIlikeTerm(search)\n qb.andWhere(`(\n cast(gt.id as text) ilike ? escape '\\\\'\n or cast(gt.payment_id as text) ilike ? escape '\\\\'\n or coalesce(gt.provider_key, '') ilike ? escape '\\\\'\n or coalesce(gt.provider_session_id, '') ilike ? escape '\\\\'\n or coalesce(gt.gateway_payment_id, '') ilike ? escape '\\\\'\n or coalesce(gt.gateway_refund_id, '') ilike ? escape '\\\\'\n )`, [pattern, pattern, pattern, pattern, pattern, pattern])\n }\n\n const countQb = qb.clone()\n qb.orderBy({ createdAt: 'desc' })\n qb.limit(pageSize).offset(offset)\n\n const [items, total] = await Promise.all([\n qb.getResultList(),\n countQb.getCount('gt.id', true),\n ])\n\n return NextResponse.json({\n items: items.map((item) => ({\n id: item.id,\n paymentId: item.paymentId,\n providerKey: item.providerKey,\n providerSessionId: item.providerSessionId ?? null,\n gatewayPaymentId: item.gatewayPaymentId ?? null,\n gatewayRefundId: item.gatewayRefundId ?? null,\n unifiedStatus: item.unifiedStatus,\n gatewayStatus: item.gatewayStatus ?? null,\n amount: item.amount,\n currencyCode: item.currencyCode,\n redirectUrl: item.redirectUrl ?? null,\n lastWebhookAt: formatDateValue(item.lastWebhookAt),\n lastPolledAt: formatDateValue(item.lastPolledAt),\n createdAt: formatDateValue(item.createdAt),\n updatedAt: formatDateValue(item.updatedAt),\n })),\n total,\n page,\n pageSize,\n totalPages: Math.max(1, Math.ceil(Number(total) / pageSize)),\n })\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'List payment transactions',\n methods: {\n GET: {\n summary: 'List payment transactions',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 200, description: 'Payment transaction list' },\n ],\n },\n },\n}\n\nexport default GET\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -15,6 +15,7 @@ const metadata = {
|
|
|
15
15
|
POST: { requireAuth: false }
|
|
16
16
|
};
|
|
17
17
|
const WEBHOOK_VERIFICATION_FAILED = "Webhook verification failed";
|
|
18
|
+
const MAX_WEBHOOK_CANDIDATES = 10;
|
|
18
19
|
const shippingCarrierWebhookRateLimitConfig = {
|
|
19
20
|
points: 60,
|
|
20
21
|
duration: 60,
|
|
@@ -68,32 +69,35 @@ async function POST(req, { params }) {
|
|
|
68
69
|
carrierShipmentId,
|
|
69
70
|
deletedAt: null
|
|
70
71
|
},
|
|
71
|
-
{ limit:
|
|
72
|
+
{ limit: MAX_WEBHOOK_CANDIDATES + 1, orderBy: { createdAt: "desc" } }
|
|
72
73
|
) : [];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
if (candidates.length > MAX_WEBHOOK_CANDIDATES) {
|
|
75
|
+
throw new Error("[internal] Webhook verification failed: candidate limit exceeded");
|
|
76
|
+
}
|
|
77
|
+
const verifiedMatches = [];
|
|
76
78
|
let lastVerificationError = null;
|
|
77
79
|
for (const candidate of candidates) {
|
|
78
80
|
const candidateScope = { organizationId: candidate.organizationId, tenantId: candidate.tenantId };
|
|
79
81
|
const credentials = await integrationCredentialsService.resolve(`carrier_${providerKey}`, candidateScope) ?? {};
|
|
80
82
|
try {
|
|
81
|
-
event = await adapter.verifyWebhook({ rawBody, headers, credentials });
|
|
82
|
-
shipment
|
|
83
|
-
matchedScope = candidateScope;
|
|
84
|
-
break;
|
|
83
|
+
const event = await adapter.verifyWebhook({ rawBody, headers, credentials });
|
|
84
|
+
verifiedMatches.push({ shipment: candidate, scope: candidateScope, event });
|
|
85
85
|
} catch (error) {
|
|
86
86
|
lastVerificationError = error;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
if (
|
|
89
|
+
if (verifiedMatches.length > 1) {
|
|
90
|
+
throw new Error("[internal] Webhook verification failed: multiple matching shipments");
|
|
91
|
+
}
|
|
92
|
+
const [match] = verifiedMatches;
|
|
93
|
+
if (!match) {
|
|
90
94
|
throw lastVerificationError ?? new Error("Webhook verification failed: no matching shipment");
|
|
91
95
|
}
|
|
92
96
|
await queue.enqueue({
|
|
93
97
|
providerKey,
|
|
94
|
-
event,
|
|
95
|
-
shipmentId: shipment.id,
|
|
96
|
-
scope:
|
|
98
|
+
event: match.event,
|
|
99
|
+
shipmentId: match.shipment.id,
|
|
100
|
+
scope: match.scope
|
|
97
101
|
});
|
|
98
102
|
return NextResponse.json({ received: true, queued: true }, { status: 202 });
|
|
99
103
|
} catch (error) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/shipping_carriers/api/webhook/%5Bprovider%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CredentialsService } from '../../../../integrations/lib/credentials-service'\nimport { CarrierShipment } from '../../../data/entities'\nimport { getShippingAdapter } from '../../../lib/adapter-registry'\nimport { getShippingCarrierQueue } from '../../../lib/queue'\nimport { shippingCarriersTag } from '../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'\n\nconst logger = createLogger('shipping_carriers').child({ component: 'webhook' })\n\nexport const metadata = {\n path: '/shipping-carriers/webhook/[provider]',\n POST: { requireAuth: false },\n}\n\nconst WEBHOOK_VERIFICATION_FAILED = 'Webhook verification failed'\n\nconst shippingCarrierWebhookRateLimitConfig = {\n points: 60,\n duration: 60,\n keyPrefix: 'shipping_carriers:webhook',\n}\n\nfunction readCarrierShipmentId(payload: Record<string, unknown> | null): string | null {\n if (!payload) return null\n const shipmentId = payload.shipmentId\n if (typeof shipmentId === 'string' && shipmentId.trim().length > 0) return shipmentId.trim()\n const data = payload.data\n if (data && typeof data === 'object') {\n const nested = (data as Record<string, unknown>).shipmentId\n if (typeof nested === 'string' && nested.trim().length > 0) return nested.trim()\n }\n return null\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ provider: string }> | { provider: string } }) {\n const resolvedParams = await params\n const providerKey = resolvedParams.provider\n const adapter = getShippingAdapter(providerKey)\n if (!adapter) {\n return NextResponse.json({ error: `No shipping adapter for provider: ${providerKey}` }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey)\n if (rateLimitResponse) return rateLimitResponse\n\n let rawBody: string\n try {\n rawBody = await readBoundedRequestBody(req)\n } catch (error) {\n if (error instanceof WebhookBodyTooLargeError) {\n return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })\n }\n throw error\n }\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n const em = container.resolve('em') as EntityManager\n const integrationCredentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const queue = getShippingCarrierQueue('shipping-carriers-webhook')\n const payload = await readJsonSafe<Record<string, unknown>>(rawBody)\n const carrierShipmentId = (payload ? adapter.extractWebhookShipmentId?.(payload) : null) || readCarrierShipmentId(payload)\n\n try {\n // The webhook endpoint is unauthenticated. Tenant/organization scope MUST come from a\n // CarrierShipment whose per-tenant credentials successfully verify the inbound\n // signature \u2014 NEVER from attacker-controlled payload metadata or an unsigned retry.\n // If no candidate shipment can be located by the provider-reported carrierShipmentId,\n // or no candidate's credentials can verify the signature, we fail closed with 401.\n // This mirrors the fix landed for payment_gateways in PR #1311.\n const candidates = carrierShipmentId\n ? await findWithDecryption(\n em,\n CarrierShipment,\n {\n providerKey,\n carrierShipmentId,\n deletedAt: null,\n },\n { limit:
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB,aAAa,iCAAiC;AAIvE,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB,gCAAgC;AAEjE,MAAM,SAAS,aAAa,mBAAmB,EAAE,MAAM,EAAE,WAAW,UAAU,CAAC;AAExE,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM,EAAE,aAAa,MAAM;AAC7B;AAEA,MAAM,8BAA8B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_FALLBACK } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CredentialsService } from '../../../../integrations/lib/credentials-service'\nimport { CarrierShipment } from '../../../data/entities'\nimport { getShippingAdapter } from '../../../lib/adapter-registry'\nimport { getShippingCarrierQueue } from '../../../lib/queue'\nimport { shippingCarriersTag } from '../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'\n\nconst logger = createLogger('shipping_carriers').child({ component: 'webhook' })\n\nexport const metadata = {\n path: '/shipping-carriers/webhook/[provider]',\n POST: { requireAuth: false },\n}\n\nconst WEBHOOK_VERIFICATION_FAILED = 'Webhook verification failed'\nconst MAX_WEBHOOK_CANDIDATES = 10\n\nconst shippingCarrierWebhookRateLimitConfig = {\n points: 60,\n duration: 60,\n keyPrefix: 'shipping_carriers:webhook',\n}\n\nfunction readCarrierShipmentId(payload: Record<string, unknown> | null): string | null {\n if (!payload) return null\n const shipmentId = payload.shipmentId\n if (typeof shipmentId === 'string' && shipmentId.trim().length > 0) return shipmentId.trim()\n const data = payload.data\n if (data && typeof data === 'object') {\n const nested = (data as Record<string, unknown>).shipmentId\n if (typeof nested === 'string' && nested.trim().length > 0) return nested.trim()\n }\n return null\n}\n\nexport async function POST(req: Request, { params }: { params: Promise<{ provider: string }> | { provider: string } }) {\n const resolvedParams = await params\n const providerKey = resolvedParams.provider\n const adapter = getShippingAdapter(providerKey)\n if (!adapter) {\n return NextResponse.json({ error: `No shipping adapter for provider: ${providerKey}` }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const rateLimitResponse = await checkProviderWebhookRateLimit(container, req, providerKey)\n if (rateLimitResponse) return rateLimitResponse\n\n let rawBody: string\n try {\n rawBody = await readBoundedRequestBody(req)\n } catch (error) {\n if (error instanceof WebhookBodyTooLargeError) {\n return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })\n }\n throw error\n }\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n const em = container.resolve('em') as EntityManager\n const integrationCredentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const queue = getShippingCarrierQueue('shipping-carriers-webhook')\n const payload = await readJsonSafe<Record<string, unknown>>(rawBody)\n const carrierShipmentId = (payload ? adapter.extractWebhookShipmentId?.(payload) : null) || readCarrierShipmentId(payload)\n\n try {\n // The webhook endpoint is unauthenticated. Tenant/organization scope MUST come from a\n // CarrierShipment whose per-tenant credentials successfully verify the inbound\n // signature \u2014 NEVER from attacker-controlled payload metadata or an unsigned retry.\n // If no candidate shipment can be located by the provider-reported carrierShipmentId,\n // or no candidate's credentials can verify the signature, we fail closed with 401.\n // This mirrors the fix landed for payment_gateways in PR #1311.\n const candidates = carrierShipmentId\n ? await findWithDecryption(\n em,\n CarrierShipment,\n {\n providerKey,\n carrierShipmentId,\n deletedAt: null,\n },\n { limit: MAX_WEBHOOK_CANDIDATES + 1, orderBy: { createdAt: 'desc' } },\n )\n : []\n\n if (candidates.length > MAX_WEBHOOK_CANDIDATES) {\n throw new Error('[internal] Webhook verification failed: candidate limit exceeded')\n }\n\n const verifiedMatches: Array<{\n shipment: CarrierShipment\n scope: { organizationId: string; tenantId: string }\n event: Awaited<ReturnType<typeof adapter.verifyWebhook>>\n }> = []\n let lastVerificationError: unknown = null\n\n for (const candidate of candidates) {\n const candidateScope = { organizationId: candidate.organizationId, tenantId: candidate.tenantId }\n const credentials = await integrationCredentialsService.resolve(`carrier_${providerKey}`, candidateScope) ?? {}\n try {\n const event = await adapter.verifyWebhook({ rawBody, headers, credentials })\n verifiedMatches.push({ shipment: candidate, scope: candidateScope, event })\n } catch (error: unknown) {\n lastVerificationError = error\n }\n }\n\n if (verifiedMatches.length > 1) {\n throw new Error('[internal] Webhook verification failed: multiple matching shipments')\n }\n\n const [match] = verifiedMatches\n if (!match) {\n throw lastVerificationError ?? new Error('Webhook verification failed: no matching shipment')\n }\n\n await queue.enqueue({\n providerKey,\n event: match.event,\n shipmentId: match.shipment.id,\n scope: match.scope,\n })\n\n return NextResponse.json({ received: true, queued: true }, { status: 202 })\n } catch (error: unknown) {\n logger.warn('Webhook verification failed', { providerKey, err: error })\n return NextResponse.json({ error: WEBHOOK_VERIFICATION_FAILED }, { status: 401 })\n }\n}\n\nasync function checkProviderWebhookRateLimit(\n container: { resolve: (name: string) => unknown },\n req: Request,\n providerKey: string,\n): Promise<NextResponse | null> {\n const rateLimiterService = tryResolve<RateLimiterService>(container, 'rateLimiterService')\n if (!rateLimiterService) return null\n\n return checkRateLimit(\n rateLimiterService,\n shippingCarrierWebhookRateLimitConfig,\n `${providerKey}:${getClientIp(req, rateLimiterService.trustProxyDepth) ?? 'unknown'}`,\n RATE_LIMIT_ERROR_FALLBACK,\n )\n}\n\nfunction tryResolve<T>(container: { resolve: (name: string) => unknown }, name: string): T | null {\n try {\n return container.resolve(name) as T\n } catch {\n return null\n }\n}\n\nexport const openApi = {\n tags: [shippingCarriersTag],\n summary: 'Receive shipping carrier webhook',\n methods: {\n POST: {\n summary: 'Process inbound carrier webhook',\n tags: [shippingCarriersTag],\n responses: [\n { status: 202, description: 'Webhook accepted for async processing' },\n { status: 401, description: 'Signature verification failed' },\n { status: 413, description: 'Webhook payload too large' },\n { status: 404, description: 'Unknown provider' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -510,7 +510,7 @@ function DefaultGridView({
|
|
|
510
510
|
]
|
|
511
511
|
}
|
|
512
512
|
) }) : null,
|
|
513
|
-
/* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-lg border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm table-fixed", "aria-busy": isLoading || void 0, children: [
|
|
513
|
+
/* @__PURE__ */ jsx("div", { className: "isolate overflow-x-auto rounded-lg border", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm table-fixed", "aria-busy": isLoading || void 0, children: [
|
|
514
514
|
/* @__PURE__ */ jsx("caption", { className: "sr-only", children: t(
|
|
515
515
|
"staff.time_tracking.timesheet.grid.caption",
|
|
516
516
|
"Timesheet grid: one row per project or task, one column per day of the period."
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/staff/backend/staff/time-tracking/timesheet/GridView.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\n/**\n * The kept editable grid (T5.4) \u2014 project \u00D7 day cells, typed straight into, saved\n * in one pass.\n *\n * This is the one surface of the old timesheet the redesign keeps, because it is\n * the fastest way in the product to fill a week. The table's markup, column\n * widths, weekend treatment, footer and totals are carried over unchanged; the\n * functional upgrade is three things and no more:\n *\n * 1. **The shared duration parser.** Cells were a private `decimalToMinutes`\n * that understood `1.5` and silently answered `0` for `1h 40m`. They are now\n * `DurationInput`, so the grid understands exactly what the entry dialog, the\n * inline cell edit and the quick-add rows understand. The look is preserved\n * by `formatValue={formatTrimmedDecimalDuration}` \u2014 120 minutes renders `2`,\n * not `2.00` (watch item W3, closed by T4.5 with a test pinning the\n * divergence from `formatDuration(\u2026, 'decimal')`).\n *\n * A consequence worth stating: text that does not parse is **kept**, the cell\n * is flagged, and Save is disabled until it is fixed. The old grid stripped\n * every character it did not like as you typed.\n *\n * 2. **Rows can be a project or a project + task.** The row-mode toggle splits\n * each project row into its task-less entries plus one row per task with time\n * in the period, so grid entries carry `taskId` like every other entry.\n *\n * 3. **Locked cells are read-only**, badged, and never sent. The bulk endpoint\n * refuses the whole batch with `409 time_entry_locked` naming the offending\n * ids (T4.2); those ids are marked here so the refusal lands on the cells\n * that caused it rather than as an opaque batch failure.\n *\n * **Table semantics and states.** The grid is a data table, so it carries an\n * sr-only `<caption>`, `scope`d column headers and a `<th scope=\"row\">` row\n * label \u2014 without them a screen reader reads a row as eight unlabelled numbers.\n * Weekends keep their background tint but no longer depend on it alone: the\n * header also carries the day and the word in its `title` and an sr-only marker.\n * `isLoading` draws skeleton rows in the real row geometry rather than a\n * centered one-liner, and a period with no rows gets an in-table empty state\n * instead of headers over an empty body.\n *\n * **Save path \u2014 two routes, and why.**\n * The bulk endpoint now assigns `task_id` on both its create and update\n * branches, so the silent-drop defect this file used to document is fixed and\n * the note is retired. Task-row cells nonetheless keep writing through the\n * single-entry CRUD route, for a different reason: that route carries a\n * per-entry `If-Match`-style expected version, so a cell edited from another\n * tab surfaces as a conflict on the cell that caused it. The batch has no\n * per-row version contract \u2014 it is all-or-nothing and reconciles only the\n * report lock (`409 time_entry_locked`) \u2014 so project rows sent through it are\n * deliberately last-write-wins. optimistic-lock-exempt: the bulk batch has no\n * per-row version field; per-cell locking is provided by the single-entry path\n * above, and the batch's own gate is the closed-report lock.\n */\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport {\n callbackProp,\n opaqueProp,\n} from '../../../../lib/time-tracking/componentContracts'\nimport { Lock, X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrud, deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { AddRowDropdown } from '../../../../lib/timesheets-ui/AddRowDropdown'\nimport { ProjectColorDot } from '../../../../lib/timesheets-ui/ProjectColorDot'\nimport { DurationInput, formatTrimmedDecimalDuration } from '../../../../lib/time-tracking-ui/DurationInput'\nimport {\n readErrorCode,\n readErrorMessage,\n readRejectedRowIndexes,\n isEntryLockedError,\n} from '../../../../lib/time-tracking-ui/timeEntryListData'\nimport type { TimesheetEntry } from '../../../../lib/time-tracking-ui/timesheetData'\nimport { isWeekendIso, parseIsoDay } from '../../../../lib/time-tracking-ui/timesheetPeriod'\nimport {\n buildTargetKey,\n targetLabel,\n type TimesheetLogTarget,\n type TimesheetProjectRef,\n type TimesheetTaskRef,\n} from '../../../../lib/time-tracking-ui/timesheetTargets'\n\nconst logger = createLogger('staff').child({ component: 'time-tracking/timesheet/grid' })\n\n/** Placeholder rows drawn while the period loads \u2014 same geometry as a real row. */\nconst SKELETON_ROW_COUNT = 4\n\nconst ENTRIES_API_PATH = 'staff/timesheets/time-entries'\nconst BULK_ENDPOINT = '/api/staff/timesheets/time-entries/bulk'\nconst RESOURCE_KIND = 'staff.timesheets.time_entry'\n\nexport type GridRowMode = 'projects' | 'tasks'\n\nexport type GridViewProps = {\n /** Every day column of the current period, in order. */\n days: readonly string[]\n projects: readonly TimesheetProjectRef[]\n allAssignedProjects: readonly TimesheetProjectRef[]\n tasks: readonly TimesheetTaskRef[]\n entries: readonly TimesheetEntry[]\n staffMemberId: string | null\n canManage: boolean\n canManageProjects: boolean\n /** Someone else's timesheet is readable but not editable from the grid. */\n readOnly: boolean\n /** Draws skeleton rows instead of data while the period is being fetched. */\n isLoading?: boolean\n rowMode: GridRowMode\n onRowModeChange: (mode: GridRowMode) => void\n onAddProject: (project: TimesheetProjectRef) => void | Promise<void>\n onRemoveProject: (project: TimesheetProjectRef) => void | Promise<void>\n onCreateProject: () => void\n onSaved: () => void | Promise<void>\n}\n\ntype GridRow = TimesheetLogTarget & {\n removable: boolean\n}\n\ntype GridCell = {\n minutes: number\n entryIds: string[]\n lockedEntryIds: string[]\n /** `updated_at` per entry id \u2014 the expected version a task-cell write sends. */\n entryVersions: Record<string, string | null>\n}\n\ntype DirtyCell = {\n rowKey: string\n date: string\n minutes: number\n}\n\nfunction cellKey(rowKey: string, date: string): string {\n return `${rowKey}|${date}`\n}\n\n/** The grid's column header: `Mon` over `13`. */\nfunction dayHeaderParts(date: string, locale?: string): { weekday: string; dayOfMonth: string } {\n const parsed = parseIsoDay(date)\n if (!parsed) return { weekday: '', dayOfMonth: date }\n let weekday = ''\n try {\n weekday = new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(parsed)\n } catch {\n weekday = ''\n }\n return { weekday, dayOfMonth: String(parsed.getDate()) }\n}\n\nexport function buildGridRows(\n projects: readonly TimesheetProjectRef[],\n tasks: readonly TimesheetTaskRef[],\n entries: readonly TimesheetEntry[],\n rowMode: GridRowMode,\n extraTaskKeys: ReadonlySet<string>,\n): GridRow[] {\n if (rowMode === 'projects') {\n return projects.map((project) => ({\n key: buildTargetKey(project.id, null),\n timeProjectId: project.id,\n taskId: null,\n projectName: project.name,\n projectCode: project.code,\n projectColor: project.color,\n taskTitle: null,\n removable: true,\n }))\n }\n\n const tasksById = new Map(tasks.map((task) => [task.id, task]))\n const usedTaskIds = new Map<string, Set<string>>()\n for (const entry of entries) {\n if (!entry.taskId || !entry.timeProjectId) continue\n const bucket = usedTaskIds.get(entry.timeProjectId) ?? new Set<string>()\n bucket.add(entry.taskId)\n usedTaskIds.set(entry.timeProjectId, bucket)\n }\n for (const key of extraTaskKeys) {\n const separator = key.indexOf(':')\n if (separator < 0) continue\n const projectId = key.slice(0, separator)\n const taskId = key.slice(separator + 1)\n const bucket = usedTaskIds.get(projectId) ?? new Set<string>()\n bucket.add(taskId)\n usedTaskIds.set(projectId, bucket)\n }\n\n const rows: GridRow[] = []\n for (const project of projects) {\n rows.push({\n key: buildTargetKey(project.id, null),\n timeProjectId: project.id,\n taskId: null,\n projectName: project.name,\n projectCode: project.code,\n projectColor: project.color,\n taskTitle: null,\n removable: true,\n })\n const taskIds = Array.from(usedTaskIds.get(project.id) ?? [])\n const named = taskIds\n .map((taskId) => ({ taskId, title: tasksById.get(taskId)?.title ?? null }))\n .sort((left, right) => (left.title ?? '').localeCompare(right.title ?? ''))\n for (const task of named) {\n rows.push({\n key: buildTargetKey(project.id, task.taskId),\n timeProjectId: project.id,\n taskId: task.taskId,\n projectName: project.name,\n projectCode: project.code,\n projectColor: project.color,\n taskTitle: task.title,\n removable: false,\n })\n }\n }\n return rows\n}\n\nexport function buildGridCells(\n entries: readonly TimesheetEntry[],\n rowMode: GridRowMode,\n): Map<string, GridCell> {\n const cells = new Map<string, GridCell>()\n for (const entry of entries) {\n if (!entry.timeProjectId) continue\n const rowKey =\n rowMode === 'projects'\n ? buildTargetKey(entry.timeProjectId, null)\n : buildTargetKey(entry.timeProjectId, entry.taskId)\n const key = cellKey(rowKey, entry.date)\n const current = cells.get(key) ?? { minutes: 0, entryIds: [], lockedEntryIds: [], entryVersions: {} }\n current.minutes += Number.isFinite(entry.durationMinutes) ? entry.durationMinutes : 0\n current.entryIds.push(entry.id)\n current.entryVersions[entry.id] = entry.updatedAt\n if (entry.isLocked) current.lockedEntryIds.push(entry.id)\n cells.set(key, current)\n }\n return cells\n}\n\nfunction DefaultGridView({\n days,\n projects,\n allAssignedProjects,\n tasks,\n entries,\n staffMemberId,\n canManage,\n canManageProjects,\n readOnly,\n isLoading = false,\n rowMode,\n onRowModeChange,\n onAddProject,\n onRemoveProject,\n onCreateProject,\n onSaved,\n}: GridViewProps) {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [dirty, setDirty] = React.useState<Record<string, DirtyCell>>({})\n const [invalidCells, setInvalidCells] = React.useState<Record<string, true>>({})\n const [rejectedCells, setRejectedCells] = React.useState<Record<string, string>>({})\n const [extraLockedEntryIds, setExtraLockedEntryIds] = React.useState<Record<string, true>>({})\n const [extraTaskKeys, setExtraTaskKeys] = React.useState<Set<string>>(() => new Set())\n const [isSaving, setIsSaving] = React.useState(false)\n\n const mutationContextId = staffMemberId ? `staff-timesheets:${staffMemberId}` : 'staff-timesheets:pending'\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId?: string\n staffMemberId: string | null\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: mutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const mutationContext = React.useCallback(\n (resourceId?: string) => ({\n formId: mutationContextId,\n resourceKind: RESOURCE_KIND,\n resourceId,\n staffMemberId,\n retryLastMutation,\n }),\n [mutationContextId, retryLastMutation, staffMemberId],\n )\n\n const rows = React.useMemo(\n () => buildGridRows(projects, tasks, entries, rowMode, extraTaskKeys),\n [entries, extraTaskKeys, projects, rowMode, tasks],\n )\n const cells = React.useMemo(() => buildGridCells(entries, rowMode), [entries, rowMode])\n const rowsByKey = React.useMemo(() => new Map(rows.map((row) => [row.key, row])), [rows])\n\n // A fresh period, a different person or a row-mode switch invalidates every\n // pending edit: the cells they belong to are no longer on screen.\n const resetToken = `${rowMode}|${days[0] ?? ''}|${days[days.length - 1] ?? ''}|${staffMemberId ?? ''}`\n const resetTokenRef = React.useRef(resetToken)\n React.useEffect(() => {\n if (resetTokenRef.current === resetToken) return\n resetTokenRef.current = resetToken\n setDirty({})\n setInvalidCells({})\n setRejectedCells({})\n }, [resetToken])\n\n const isCellLocked = React.useCallback(\n (key: string) => {\n const cell = cells.get(key)\n if (!cell) return false\n if (cell.lockedEntryIds.length > 0) return true\n return cell.entryIds.some((id) => extraLockedEntryIds[id])\n },\n [cells, extraLockedEntryIds],\n )\n\n const cellMinutes = React.useCallback(\n (key: string) => {\n const pending = dirty[key]\n if (pending) return pending.minutes\n return cells.get(key)?.minutes ?? 0\n },\n [cells, dirty],\n )\n\n const handleCellChange = React.useCallback(\n (row: GridRow, date: string, minutes: number | null, invalid: boolean) => {\n const key = cellKey(row.key, date)\n setRejectedCells((current) => {\n if (!current[key]) return current\n const next = { ...current }\n delete next[key]\n return next\n })\n setInvalidCells((current) => {\n if (invalid === Boolean(current[key])) return current\n const next = { ...current }\n if (invalid) next[key] = true\n else delete next[key]\n return next\n })\n // Unparseable text is never turned into a number \u2014 the previous value stands\n // and Save stays blocked until the text is fixed or cleared.\n if (invalid) return\n const nextMinutes = minutes ?? 0\n const stored = cells.get(key)\n const storedMinutes = stored?.minutes ?? 0\n const hadEntries = (stored?.entryIds.length ?? 0) > 0\n setDirty((current) => {\n const next = { ...current }\n if (nextMinutes === storedMinutes && !hadEntries) {\n delete next[key]\n return next\n }\n next[key] = { rowKey: row.key, date, minutes: nextMinutes }\n return next\n })\n },\n [cells],\n )\n\n const rowTotal = React.useCallback(\n (rowKey: string) => days.reduce((sum, date) => sum + cellMinutes(cellKey(rowKey, date)), 0),\n [cellMinutes, days],\n )\n const dayTotal = React.useCallback(\n (date: string) => rows.reduce((sum, row) => sum + cellMinutes(cellKey(row.key, date)), 0),\n [cellMinutes, rows],\n )\n const grandTotal = React.useMemo(\n () => rows.reduce((sum, row) => sum + rowTotal(row.key), 0),\n [rowTotal, rows],\n )\n\n const dirtyCount = Object.keys(dirty).length\n const invalidCount = Object.keys(invalidCells).length\n const hasChanges = dirtyCount > 0\n const dense = days.length > 10\n\n const markLockedFromError = React.useCallback((error: unknown) => {\n const body = (error as { body?: { lockedEntryIds?: unknown } } | null)?.body\n const direct = (error as { lockedEntryIds?: unknown } | null)?.lockedEntryIds\n const ids = Array.isArray(body?.lockedEntryIds)\n ? body?.lockedEntryIds\n : Array.isArray(direct)\n ? direct\n : []\n const marked = ids.filter((id): id is string => typeof id === 'string')\n if (marked.length === 0) return false\n setExtraLockedEntryIds((current) => {\n const next = { ...current }\n for (const id of marked) next[id] = true\n return next\n })\n return true\n }, [])\n\n const reportSaveFailure = React.useCallback(\n (error: unknown, rejectedKeys: readonly string[]) => {\n if (surfaceRecordConflict(error, t)) return\n if (isEntryLockedError(error)) {\n markLockedFromError(error)\n flash(\n t(\n 'staff.time_tracking.timesheet.grid.errors.locked',\n 'Some of these cells belong to a closed report and cannot be changed. They are now marked as locked.',\n ),\n 'error',\n )\n return\n }\n logger.error('staff.time_tracking.timesheet.grid save failed', { err: error, code: readErrorCode(error) })\n const message = readErrorMessage(error, t('staff.timesheets.my.errors.save', 'Failed to save timesheets.'))\n if (rejectedKeys.length > 0) {\n setRejectedCells((current) => {\n const next = { ...current }\n for (const key of rejectedKeys) next[key] = message\n return next\n })\n }\n flash(message, 'error')\n },\n [markLockedFromError, t],\n )\n\n /**\n * Task-row cells, one write each through the single-entry route \u2014 see the file\n * header for why they cannot ride the batch yet. A locked cell never reaches\n * here (its input is read-only), so a 409 from this path means the entry was\n * frozen after the page loaded, which is exactly when the user needs to be\n * told which cell it was. Each cell is reported through `onCellSaved` as soon\n * as it lands, so a refusal further down the list leaves the caller knowing\n * exactly which cells are already stored.\n */\n const saveTaskCells = React.useCallback(\n async (changes: Array<{ row: GridRow; cell: DirtyCell }>, onCellSaved: (key: string) => void) => {\n for (const { row, cell } of changes) {\n const key = cellKey(row.key, cell.date)\n const storedCell = cells.get(key)\n const existingId = storedCell?.entryIds[0] ?? null\n const existingVersion = existingId ? storedCell?.entryVersions[existingId] ?? null : null\n const errorMessage = t('staff.timesheets.my.errors.save', 'Failed to save timesheets.')\n if (existingId && cell.minutes <= 0) {\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(existingVersion), () =>\n deleteCrud(ENTRIES_API_PATH, existingId, { errorMessage }),\n ),\n context: mutationContext(existingId),\n mutationPayload: { id: existingId },\n })\n onCellSaved(key)\n continue\n }\n if (cell.minutes <= 0) {\n onCellSaved(key)\n continue\n }\n if (existingId) {\n const payload = { id: existingId, durationMinutes: cell.minutes }\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(existingVersion), () =>\n updateCrud(ENTRIES_API_PATH, payload, { errorMessage }),\n ),\n context: mutationContext(existingId),\n mutationPayload: payload,\n })\n onCellSaved(key)\n continue\n }\n const payload = {\n staffMemberId,\n date: cell.date,\n timeProjectId: row.timeProjectId,\n taskId: row.taskId,\n durationMinutes: cell.minutes,\n source: 'manual',\n }\n await runMutation({\n operation: () => createCrud(ENTRIES_API_PATH, payload, { errorMessage }),\n context: mutationContext(),\n mutationPayload: payload,\n })\n onCellSaved(key)\n }\n },\n [cells, mutationContext, runMutation, staffMemberId, t],\n )\n\n const handleSave = React.useCallback(async () => {\n if (!hasChanges || invalidCount > 0) return\n const confirmed = await confirm({\n title: t('staff.timesheets.my.confirm_save.title', 'Save changes?'),\n text: t('staff.timesheets.my.confirm_save.body', 'Your timesheet entries will be saved.'),\n })\n if (!confirmed) return\n\n const bulkEntries: Array<{ id?: string; date: string; timeProjectId: string; durationMinutes: number }> = []\n const bulkKeys: string[] = []\n const taskChanges: Array<{ row: GridRow; cell: DirtyCell }> = []\n for (const cell of Object.values(dirty)) {\n const row = rowsByKey.get(cell.rowKey)\n if (!row) continue\n if (row.taskId) {\n taskChanges.push({ row, cell })\n continue\n }\n const key = cellKey(row.key, cell.date)\n bulkKeys.push(key)\n bulkEntries.push({\n id: cells.get(key)?.entryIds[0],\n date: cell.date,\n timeProjectId: row.timeProjectId,\n durationMinutes: cell.minutes,\n })\n }\n if (bulkEntries.length === 0 && taskChanges.length === 0) return\n\n // The project rows and the task rows commit separately (see the file\n // header), so a refusal can arrive after some cells are already stored. Those\n // cells leave the pending set and the period is reloaded, so the stored rows\n // pick up their ids \u2014 otherwise the next Save would create them a second time.\n // The bulk request opts out of the shared 403 handling, which keeps only the\n // message: the access refusal's `errors[].index` is what marks the cell.\n const savedKeys = new Set<string>()\n let bulkCommitted = bulkEntries.length === 0\n setIsSaving(true)\n try {\n if (bulkEntries.length > 0) {\n const payload = { entries: bulkEntries }\n await runMutation({\n operation: () =>\n apiCallOrThrow(\n BULK_ENDPOINT,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-om-forbidden-redirect': '0' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('staff.timesheets.my.errors.save', 'Failed to save timesheets.') },\n ),\n context: mutationContext(staffMemberId ?? undefined),\n mutationPayload: payload as unknown as Record<string, unknown>,\n })\n bulkCommitted = true\n for (const key of bulkKeys) savedKeys.add(key)\n }\n if (taskChanges.length > 0) await saveTaskCells(taskChanges, (key) => savedKeys.add(key))\n setDirty({})\n setRejectedCells({})\n flash(t('staff.timesheets.my.saved', 'Timesheet saved.'), 'success')\n await onSaved()\n } catch (error) {\n const rejectedKeys = bulkCommitted\n ? taskChanges\n .map(({ row, cell }) => cellKey(row.key, cell.date))\n .filter((key) => !savedKeys.has(key))\n .slice(0, 1)\n : readRejectedRowIndexes(error)\n .map((index) => bulkKeys[index])\n .filter((key): key is string => typeof key === 'string')\n reportSaveFailure(error, rejectedKeys)\n if (savedKeys.size > 0) {\n setDirty((current) => {\n const next = { ...current }\n for (const key of savedKeys) delete next[key]\n return next\n })\n try {\n await onSaved()\n } catch (reloadError) {\n logger.error('staff.time_tracking.timesheet.grid reload after partial save failed', { err: reloadError })\n }\n }\n } finally {\n setIsSaving(false)\n }\n }, [\n cells,\n confirm,\n dirty,\n hasChanges,\n invalidCount,\n mutationContext,\n onSaved,\n reportSaveFailure,\n rowsByKey,\n runMutation,\n saveTaskCells,\n staffMemberId,\n t,\n ])\n\n const taskRowOptions = React.useMemo(() => {\n const visibleProjectIds = new Set(projects.map((project) => project.id))\n const existing = new Set(rows.map((row) => row.key))\n return tasks\n .filter((task) => visibleProjectIds.has(task.timeProjectId))\n .map((task) => {\n const project = projects.find((candidate) => candidate.id === task.timeProjectId)!\n return {\n key: buildTargetKey(task.timeProjectId, task.id),\n label: targetLabel({\n key: buildTargetKey(task.timeProjectId, task.id),\n timeProjectId: task.timeProjectId,\n taskId: task.id,\n projectName: project.name,\n projectCode: project.code,\n projectColor: project.color,\n taskTitle: task.title,\n }),\n }\n })\n .filter((option) => !existing.has(option.key))\n }, [projects, rows, tasks])\n\n const editable = canManage && !readOnly\n const loadingLabel = t('staff.time_tracking.timesheet.grid.loading', 'Loading the timesheet grid\\u2026')\n\n return (\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <SegmentedControl\n value={rowMode}\n onValueChange={(value) => {\n if (value !== rowMode) onRowModeChange(value as GridRowMode)\n }}\n size=\"sm\"\n aria-label={t('staff.time_tracking.timesheet.grid.rowMode.label', 'Grid rows')}\n >\n <SegmentedControlItem value=\"projects\">\n {t('staff.time_tracking.timesheet.grid.rowMode.projects', 'Projects')}\n </SegmentedControlItem>\n <SegmentedControlItem value=\"tasks\">\n {t('staff.time_tracking.timesheet.grid.rowMode.tasks', 'Projects + tasks')}\n </SegmentedControlItem>\n </SegmentedControl>\n\n <div className=\"flex items-center gap-3\">\n {invalidCount > 0 ? (\n <span className=\"text-xs font-medium text-status-error-text\">\n {t('staff.time_tracking.timesheet.grid.invalid', 'Fix the highlighted cell before saving.')}\n </span>\n ) : hasChanges ? (\n <span className=\"text-xs font-medium text-status-warning-text\">\n {t('staff.timesheets.my.unsaved', 'Unsaved changes')}\n </span>\n ) : null}\n {editable ? (\n <Button\n size=\"sm\"\n type=\"button\"\n onClick={() => void handleSave()}\n disabled={!hasChanges || invalidCount > 0 || isSaving}\n >\n {isSaving\n ? t('staff.timesheets.my.saving', 'Saving...')\n : t('staff.timesheets.my.save_changes', 'Save Changes')}\n </Button>\n ) : null}\n </div>\n </div>\n\n {rowMode === 'tasks' && editable && taskRowOptions.length > 0 ? (\n <div className=\"flex items-center gap-2\">\n <Select\n value=\"\"\n onValueChange={(value) => setExtraTaskKeys((current) => new Set(current).add(value))}\n >\n <SelectTrigger\n className=\"w-72\"\n aria-label={t('staff.time_tracking.timesheet.grid.addTaskRow', 'Add a task row')}\n >\n <SelectValue placeholder={t('staff.time_tracking.timesheet.grid.addTaskRow', 'Add a task row')} />\n </SelectTrigger>\n <SelectContent>\n {taskRowOptions.map((option) => (\n <SelectItem key={option.key} value={option.key}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : null}\n\n <div className=\"overflow-x-auto rounded-lg border\">\n <table className=\"w-full text-sm table-fixed\" aria-busy={isLoading || undefined}>\n <caption className=\"sr-only\">\n {t(\n 'staff.time_tracking.timesheet.grid.caption',\n 'Timesheet grid: one row per project or task, one column per day of the period.',\n )}\n </caption>\n <colgroup>\n <col className={dense ? 'w-[140px] min-w-[140px]' : 'w-[35%]'} />\n {days.map((date) => (\n <col key={date} className={dense ? 'w-[36px] min-w-[36px]' : ''} />\n ))}\n <col className={dense ? 'w-[56px] min-w-[56px]' : 'w-[72px]'} />\n </colgroup>\n <thead>\n <tr className=\"border-b bg-muted/50\">\n <th scope=\"col\" className=\"sticky left-0 z-10 bg-muted px-3 py-2 text-left font-medium\">\n {t('staff.timesheets.my.project', 'Project')}\n </th>\n {days.map((date) => {\n const { weekday, dayOfMonth } = dayHeaderParts(date)\n const weekend = isWeekendIso(date)\n // Weekends were signalled by a background tint alone, which a\n // screen reader and a monochrome display both miss. The tint stays;\n // the meaning now also travels as a title and an sr-only word.\n const weekendLabel = t('staff.time_tracking.timesheet.grid.weekend', 'Weekend day')\n return (\n <th\n scope=\"col\"\n key={date}\n title={weekend ? `${date} \u00B7 ${weekendLabel}` : date}\n className={cn(\n 'py-2 text-center font-medium px-1',\n weekend ? 'bg-muted/80 text-muted-foreground' : null,\n )}\n >\n <div className=\"text-xs uppercase text-muted-foreground\">{weekday}</div>\n <div className=\"text-xs\">{dayOfMonth}</div>\n {weekend ? <span className=\"sr-only\">{weekendLabel}</span> : null}\n </th>\n )\n })}\n <th scope=\"col\" className=\"px-3 py-2 text-right font-medium\">\n {t('staff.timesheets.my.total', 'Total')}\n </th>\n </tr>\n </thead>\n <tbody>\n {isLoading\n ? Array.from({ length: SKELETON_ROW_COUNT }).map((_, index) => (\n <tr key={`grid-skeleton-${index}`} data-testid=\"grid-skeleton-row\" className=\"border-b\">\n <th scope=\"row\" className=\"sticky left-0 z-10 bg-background px-3 py-1.5 text-left font-medium\">\n {index === 0 ? <span className=\"sr-only\">{loadingLabel}</span> : null}\n <span aria-hidden=\"true\" className=\"block h-4 w-32 animate-pulse rounded bg-muted\" />\n </th>\n {days.map((date) => (\n <td key={date} className=\"px-0.5 py-0.5\">\n <span\n aria-hidden=\"true\"\n className={cn('mx-auto block h-6 animate-pulse rounded bg-muted', dense ? 'w-10' : 'w-16')}\n />\n </td>\n ))}\n <td className=\"px-3 py-1.5\">\n <span aria-hidden=\"true\" className=\"ml-auto block h-4 w-8 animate-pulse rounded bg-muted\" />\n </td>\n </tr>\n ))\n : null}\n {!isLoading && rows.length === 0 ? (\n <tr data-testid=\"grid-empty-row\" className=\"border-b\">\n <td colSpan={days.length + 2} className=\"px-3 py-10 text-center\">\n <p className=\"text-sm font-medium text-foreground\">\n {t('staff.time_tracking.timesheet.grid.empty.title', 'This grid has no rows yet')}\n </p>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {t(\n 'staff.time_tracking.timesheet.grid.noRows',\n 'Add a project row to start filling the grid.',\n )}\n </p>\n </td>\n </tr>\n ) : null}\n {isLoading ? null : rows.map((row) => (\n <tr key={row.key} className=\"group border-b hover:bg-muted/30\">\n <th\n scope=\"row\"\n className={cn(\n 'sticky left-0 z-10 bg-background px-3 py-1.5 text-left font-medium text-foreground',\n row.taskId ? 'pl-6' : null,\n )}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 truncate\" title={targetLabel(row)}>\n {row.taskId ? null : (\n <ProjectColorDot colorKey={row.projectColor} projectName={row.projectName} size=\"sm\" />\n )}\n <span className=\"truncate\">{row.taskId ? row.taskTitle ?? targetLabel(row) : row.projectName}</span>\n </div>\n {row.taskId ? (\n <div className=\"text-xs text-muted-foreground truncate\">{row.projectName}</div>\n ) : row.projectCode ? (\n <div className=\"text-xs text-muted-foreground\">{row.projectCode}</div>\n ) : null}\n </div>\n {row.removable && editable ? (\n <button\n type=\"button\"\n onClick={() => {\n const project = projects.find((candidate) => candidate.id === row.timeProjectId)\n if (!project) return\n // Drop the row's pending errors with the row. An invalid\n // cell left behind keeps Save blocked over a row that is\n // no longer on screen, so the reader cannot reach the\n // thing they are being told to fix.\n setInvalidCells((current) => {\n const prefix = cellKey(row.key, '')\n const next = Object.fromEntries(\n Object.entries(current).filter(([key]) => !key.startsWith(prefix)),\n ) as Record<string, true>\n return Object.keys(next).length === Object.keys(current).length ? current : next\n })\n setRejectedCells((current) => {\n const prefix = cellKey(row.key, '')\n const next = Object.fromEntries(\n Object.entries(current).filter(([key]) => !key.startsWith(prefix)),\n ) as Record<string, string>\n return Object.keys(next).length === Object.keys(current).length ? current : next\n })\n setDirty((current) => {\n const prefix = cellKey(row.key, '')\n const next = Object.fromEntries(\n Object.entries(current).filter(([key]) => !key.startsWith(prefix)),\n ) as typeof current\n return Object.keys(next).length === Object.keys(current).length ? current : next\n })\n void onRemoveProject(project)\n }}\n aria-label={t('staff.timesheets.my.removeRow', 'Remove from grid')}\n title={t('staff.timesheets.my.removeRow', 'Remove from grid')}\n className=\"opacity-0 group-hover:opacity-100 focus:opacity-100 inline-flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground transition-opacity\"\n >\n <X className=\"size-3.5\" />\n </button>\n ) : null}\n </div>\n </th>\n {days.map((date) => {\n const key = cellKey(row.key, date)\n const weekend = isWeekendIso(date)\n const minutes = cellMinutes(key)\n const locked = isCellLocked(key)\n const isDirty = dirty[key] !== undefined\n const rejection = rejectedCells[key]\n const isInvalid = Boolean(invalidCells[key]) || Boolean(rejection)\n return (\n <td\n key={date}\n className={cn(\n 'px-0.5 py-0.5',\n weekend ? 'bg-muted/40' : null,\n // The old grid tinted the input itself amber; the border and\n // background of a `DurationInput` belong to the primitive, so\n // the pending-edit cue moved to the cell \u2014 same signal, DS\n // status tokens instead of raw `amber-*` (Boy Scout rule).\n isInvalid\n ? 'bg-status-error-bg'\n : isDirty\n ? 'bg-status-warning-bg'\n : null,\n )}\n >\n {weekend && minutes === 0 ? (\n <div className=\"rounded px-1 py-1 text-center text-xs text-muted-foreground/50\">-</div>\n ) : locked || !editable ? (\n <div\n className=\"flex items-center justify-center gap-1 rounded px-1 py-1 text-center text-xs tabular-nums text-muted-foreground\"\n title={\n locked\n ? t(\n 'staff.time_tracking.timesheet.grid.lockedCell',\n 'Locked by a closed report',\n )\n : undefined\n }\n >\n {locked ? <Lock className=\"size-3 shrink-0\" aria-hidden=\"true\" /> : null}\n <span>{formatTrimmedDecimalDuration(minutes) || '-'}</span>\n </div>\n ) : (\n <DurationInput\n variant=\"compact\"\n inputSize=\"sm\"\n value={minutes > 0 ? minutes : null}\n formatValue={formatTrimmedDecimalDuration}\n error={rejection}\n onChange={(nextMinutes, state) =>\n handleCellChange(row, date, nextMinutes, state.status === 'invalid')\n }\n placeholder={t('staff.timesheets.my.durationPlaceholder', '0')}\n ariaLabel={`${targetLabel(row)} \u00B7 ${date}`}\n className={cn('mx-auto', dense ? 'w-10' : 'w-16')}\n inputClassName={cn(\n 'text-center px-1 tabular-nums',\n dense ? 'text-xs' : null,\n minutes > 0 ? 'font-semibold' : 'text-muted-foreground',\n )}\n />\n )}\n </td>\n )\n })}\n <td className=\"px-3 py-1.5 text-right font-semibold text-xs tabular-nums\">\n {formatTrimmedDecimalDuration(rowTotal(row.key)) || '0'}\n </td>\n </tr>\n ))}\n {editable && !isLoading ? (\n <tr className=\"border-b\">\n <td colSpan={days.length + 2} className=\"sticky left-0 bg-background px-1 py-0.5\">\n <AddRowDropdown\n assignedProjects={allAssignedProjects.map((project) => ({\n id: project.id,\n name: project.name,\n code: project.code,\n color: project.color,\n }))}\n visibleProjectIds={new Set(projects.map((project) => project.id))}\n canCreateProject={canManageProjects}\n onAddProject={(project) => {\n void onAddProject({\n id: project.id,\n name: project.name,\n code: project.code ?? null,\n color: project.color ?? null,\n })\n }}\n onCreateProject={onCreateProject}\n />\n </td>\n </tr>\n ) : null}\n </tbody>\n <tfoot>\n <tr className=\"border-t bg-muted/50 font-semibold\">\n <th scope=\"row\" className=\"sticky left-0 z-10 bg-muted px-3 py-2 text-left\">\n {t('staff.timesheets.my.daily_total', 'Daily Total')}\n </th>\n {days.map((date) => {\n const weekend = isWeekendIso(date)\n const minutes = dayTotal(date)\n return (\n <td\n key={date}\n className={cn(\n 'py-2 text-center text-xs tabular-nums',\n weekend ? 'text-muted-foreground/50' : null,\n )}\n >\n {weekend && minutes === 0 ? '-' : formatTrimmedDecimalDuration(minutes) || '-'}\n </td>\n )\n })}\n <td className=\"px-3 py-2 text-right tabular-nums font-semibold\">\n {formatTrimmedDecimalDuration(grandTotal) || '0'}\n </td>\n </tr>\n </tfoot>\n </table>\n </div>\n\n {ConfirmDialogElement}\n </div>\n )\n}\n\nconst gridViewPropsSchema: z.ZodType<GridViewProps> = z.object({\n days: z.array(z.string()),\n projects: z.array(opaqueProp<TimesheetProjectRef>()),\n allAssignedProjects: z.array(opaqueProp<TimesheetProjectRef>()),\n tasks: z.array(opaqueProp<TimesheetTaskRef>()),\n entries: z.array(opaqueProp<TimesheetEntry>()),\n staffMemberId: z.string().nullable(),\n canManage: z.boolean(),\n canManageProjects: z.boolean(),\n readOnly: z.boolean(),\n isLoading: z.boolean().optional(),\n rowMode: z.union([z.literal('projects'), z.literal('tasks')]),\n onRowModeChange: callbackProp<(mode: GridRowMode) => void>(),\n onAddProject: callbackProp<(project: TimesheetProjectRef) => void | Promise<void>>(),\n onRemoveProject: callbackProp<(project: TimesheetProjectRef) => void | Promise<void>>(),\n onCreateProject: callbackProp<() => void>(),\n onSaved: callbackProp<() => void | Promise<void>>(),\n})\n\nregisterComponent<GridViewProps>({\n id: extensionPoints.hosts.timesheetGridComponent.componentId,\n component: DefaultGridView,\n metadata: {\n module: 'staff',\n description: 'Timesheet grid view \u2014 projects/tasks by day, editable cells.',\n propsSchema: gridViewPropsSchema,\n },\n})\n\nexport function GridView(props: GridViewProps) {\n const Resolved = useRegisteredComponent<GridViewProps>(\n extensionPoints.hosts.timesheetGridComponent.componentId,\n DefaultGridView,\n )\n return <Resolved {...props} />\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\n/**\n * The kept editable grid (T5.4) \u2014 project \u00D7 day cells, typed straight into, saved\n * in one pass.\n *\n * This is the one surface of the old timesheet the redesign keeps, because it is\n * the fastest way in the product to fill a week. The table's markup, column\n * widths, weekend treatment, footer and totals are carried over unchanged; the\n * functional upgrade is three things and no more:\n *\n * 1. **The shared duration parser.** Cells were a private `decimalToMinutes`\n * that understood `1.5` and silently answered `0` for `1h 40m`. They are now\n * `DurationInput`, so the grid understands exactly what the entry dialog, the\n * inline cell edit and the quick-add rows understand. The look is preserved\n * by `formatValue={formatTrimmedDecimalDuration}` \u2014 120 minutes renders `2`,\n * not `2.00` (watch item W3, closed by T4.5 with a test pinning the\n * divergence from `formatDuration(\u2026, 'decimal')`).\n *\n * A consequence worth stating: text that does not parse is **kept**, the cell\n * is flagged, and Save is disabled until it is fixed. The old grid stripped\n * every character it did not like as you typed.\n *\n * 2. **Rows can be a project or a project + task.** The row-mode toggle splits\n * each project row into its task-less entries plus one row per task with time\n * in the period, so grid entries carry `taskId` like every other entry.\n *\n * 3. **Locked cells are read-only**, badged, and never sent. The bulk endpoint\n * refuses the whole batch with `409 time_entry_locked` naming the offending\n * ids (T4.2); those ids are marked here so the refusal lands on the cells\n * that caused it rather than as an opaque batch failure.\n *\n * **Table semantics and states.** The grid is a data table, so it carries an\n * sr-only `<caption>`, `scope`d column headers and a `<th scope=\"row\">` row\n * label \u2014 without them a screen reader reads a row as eight unlabelled numbers.\n * Weekends keep their background tint but no longer depend on it alone: the\n * header also carries the day and the word in its `title` and an sr-only marker.\n * `isLoading` draws skeleton rows in the real row geometry rather than a\n * centered one-liner, and a period with no rows gets an in-table empty state\n * instead of headers over an empty body.\n *\n * **Save path \u2014 two routes, and why.**\n * The bulk endpoint now assigns `task_id` on both its create and update\n * branches, so the silent-drop defect this file used to document is fixed and\n * the note is retired. Task-row cells nonetheless keep writing through the\n * single-entry CRUD route, for a different reason: that route carries a\n * per-entry `If-Match`-style expected version, so a cell edited from another\n * tab surfaces as a conflict on the cell that caused it. The batch has no\n * per-row version contract \u2014 it is all-or-nothing and reconciles only the\n * report lock (`409 time_entry_locked`) \u2014 so project rows sent through it are\n * deliberately last-write-wins. optimistic-lock-exempt: the bulk batch has no\n * per-row version field; per-cell locking is provided by the single-entry path\n * above, and the batch's own gate is the closed-report lock.\n */\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'\nimport { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'\nimport { extensionPoints } from '@open-mercato/core/modules/staff/extension-points'\nimport {\n callbackProp,\n opaqueProp,\n} from '../../../../lib/time-tracking/componentContracts'\nimport { Lock, X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { SegmentedControl, SegmentedControlItem } from '@open-mercato/ui/primitives/segmented-control'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { apiCallOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrud, deleteCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { AddRowDropdown } from '../../../../lib/timesheets-ui/AddRowDropdown'\nimport { ProjectColorDot } from '../../../../lib/timesheets-ui/ProjectColorDot'\nimport { DurationInput, formatTrimmedDecimalDuration } from '../../../../lib/time-tracking-ui/DurationInput'\nimport {\n readErrorCode,\n readErrorMessage,\n readRejectedRowIndexes,\n isEntryLockedError,\n} from '../../../../lib/time-tracking-ui/timeEntryListData'\nimport type { TimesheetEntry } from '../../../../lib/time-tracking-ui/timesheetData'\nimport { isWeekendIso, parseIsoDay } from '../../../../lib/time-tracking-ui/timesheetPeriod'\nimport {\n buildTargetKey,\n targetLabel,\n type TimesheetLogTarget,\n type TimesheetProjectRef,\n type TimesheetTaskRef,\n} from '../../../../lib/time-tracking-ui/timesheetTargets'\n\nconst logger = createLogger('staff').child({ component: 'time-tracking/timesheet/grid' })\n\n/** Placeholder rows drawn while the period loads \u2014 same geometry as a real row. */\nconst SKELETON_ROW_COUNT = 4\n\nconst ENTRIES_API_PATH = 'staff/timesheets/time-entries'\nconst BULK_ENDPOINT = '/api/staff/timesheets/time-entries/bulk'\nconst RESOURCE_KIND = 'staff.timesheets.time_entry'\n\nexport type GridRowMode = 'projects' | 'tasks'\n\nexport type GridViewProps = {\n /** Every day column of the current period, in order. */\n days: readonly string[]\n projects: readonly TimesheetProjectRef[]\n allAssignedProjects: readonly TimesheetProjectRef[]\n tasks: readonly TimesheetTaskRef[]\n entries: readonly TimesheetEntry[]\n staffMemberId: string | null\n canManage: boolean\n canManageProjects: boolean\n /** Someone else's timesheet is readable but not editable from the grid. */\n readOnly: boolean\n /** Draws skeleton rows instead of data while the period is being fetched. */\n isLoading?: boolean\n rowMode: GridRowMode\n onRowModeChange: (mode: GridRowMode) => void\n onAddProject: (project: TimesheetProjectRef) => void | Promise<void>\n onRemoveProject: (project: TimesheetProjectRef) => void | Promise<void>\n onCreateProject: () => void\n onSaved: () => void | Promise<void>\n}\n\ntype GridRow = TimesheetLogTarget & {\n removable: boolean\n}\n\ntype GridCell = {\n minutes: number\n entryIds: string[]\n lockedEntryIds: string[]\n /** `updated_at` per entry id \u2014 the expected version a task-cell write sends. */\n entryVersions: Record<string, string | null>\n}\n\ntype DirtyCell = {\n rowKey: string\n date: string\n minutes: number\n}\n\nfunction cellKey(rowKey: string, date: string): string {\n return `${rowKey}|${date}`\n}\n\n/** The grid's column header: `Mon` over `13`. */\nfunction dayHeaderParts(date: string, locale?: string): { weekday: string; dayOfMonth: string } {\n const parsed = parseIsoDay(date)\n if (!parsed) return { weekday: '', dayOfMonth: date }\n let weekday = ''\n try {\n weekday = new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(parsed)\n } catch {\n weekday = ''\n }\n return { weekday, dayOfMonth: String(parsed.getDate()) }\n}\n\nexport function buildGridRows(\n projects: readonly TimesheetProjectRef[],\n tasks: readonly TimesheetTaskRef[],\n entries: readonly TimesheetEntry[],\n rowMode: GridRowMode,\n extraTaskKeys: ReadonlySet<string>,\n): GridRow[] {\n if (rowMode === 'projects') {\n return projects.map((project) => ({\n key: buildTargetKey(project.id, null),\n timeProjectId: project.id,\n taskId: null,\n projectName: project.name,\n projectCode: project.code,\n projectColor: project.color,\n taskTitle: null,\n removable: true,\n }))\n }\n\n const tasksById = new Map(tasks.map((task) => [task.id, task]))\n const usedTaskIds = new Map<string, Set<string>>()\n for (const entry of entries) {\n if (!entry.taskId || !entry.timeProjectId) continue\n const bucket = usedTaskIds.get(entry.timeProjectId) ?? new Set<string>()\n bucket.add(entry.taskId)\n usedTaskIds.set(entry.timeProjectId, bucket)\n }\n for (const key of extraTaskKeys) {\n const separator = key.indexOf(':')\n if (separator < 0) continue\n const projectId = key.slice(0, separator)\n const taskId = key.slice(separator + 1)\n const bucket = usedTaskIds.get(projectId) ?? new Set<string>()\n bucket.add(taskId)\n usedTaskIds.set(projectId, bucket)\n }\n\n const rows: GridRow[] = []\n for (const project of projects) {\n rows.push({\n key: buildTargetKey(project.id, null),\n timeProjectId: project.id,\n taskId: null,\n projectName: project.name,\n projectCode: project.code,\n projectColor: project.color,\n taskTitle: null,\n removable: true,\n })\n const taskIds = Array.from(usedTaskIds.get(project.id) ?? [])\n const named = taskIds\n .map((taskId) => ({ taskId, title: tasksById.get(taskId)?.title ?? null }))\n .sort((left, right) => (left.title ?? '').localeCompare(right.title ?? ''))\n for (const task of named) {\n rows.push({\n key: buildTargetKey(project.id, task.taskId),\n timeProjectId: project.id,\n taskId: task.taskId,\n projectName: project.name,\n projectCode: project.code,\n projectColor: project.color,\n taskTitle: task.title,\n removable: false,\n })\n }\n }\n return rows\n}\n\nexport function buildGridCells(\n entries: readonly TimesheetEntry[],\n rowMode: GridRowMode,\n): Map<string, GridCell> {\n const cells = new Map<string, GridCell>()\n for (const entry of entries) {\n if (!entry.timeProjectId) continue\n const rowKey =\n rowMode === 'projects'\n ? buildTargetKey(entry.timeProjectId, null)\n : buildTargetKey(entry.timeProjectId, entry.taskId)\n const key = cellKey(rowKey, entry.date)\n const current = cells.get(key) ?? { minutes: 0, entryIds: [], lockedEntryIds: [], entryVersions: {} }\n current.minutes += Number.isFinite(entry.durationMinutes) ? entry.durationMinutes : 0\n current.entryIds.push(entry.id)\n current.entryVersions[entry.id] = entry.updatedAt\n if (entry.isLocked) current.lockedEntryIds.push(entry.id)\n cells.set(key, current)\n }\n return cells\n}\n\nfunction DefaultGridView({\n days,\n projects,\n allAssignedProjects,\n tasks,\n entries,\n staffMemberId,\n canManage,\n canManageProjects,\n readOnly,\n isLoading = false,\n rowMode,\n onRowModeChange,\n onAddProject,\n onRemoveProject,\n onCreateProject,\n onSaved,\n}: GridViewProps) {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [dirty, setDirty] = React.useState<Record<string, DirtyCell>>({})\n const [invalidCells, setInvalidCells] = React.useState<Record<string, true>>({})\n const [rejectedCells, setRejectedCells] = React.useState<Record<string, string>>({})\n const [extraLockedEntryIds, setExtraLockedEntryIds] = React.useState<Record<string, true>>({})\n const [extraTaskKeys, setExtraTaskKeys] = React.useState<Set<string>>(() => new Set())\n const [isSaving, setIsSaving] = React.useState(false)\n\n const mutationContextId = staffMemberId ? `staff-timesheets:${staffMemberId}` : 'staff-timesheets:pending'\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId?: string\n staffMemberId: string | null\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: mutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const mutationContext = React.useCallback(\n (resourceId?: string) => ({\n formId: mutationContextId,\n resourceKind: RESOURCE_KIND,\n resourceId,\n staffMemberId,\n retryLastMutation,\n }),\n [mutationContextId, retryLastMutation, staffMemberId],\n )\n\n const rows = React.useMemo(\n () => buildGridRows(projects, tasks, entries, rowMode, extraTaskKeys),\n [entries, extraTaskKeys, projects, rowMode, tasks],\n )\n const cells = React.useMemo(() => buildGridCells(entries, rowMode), [entries, rowMode])\n const rowsByKey = React.useMemo(() => new Map(rows.map((row) => [row.key, row])), [rows])\n\n // A fresh period, a different person or a row-mode switch invalidates every\n // pending edit: the cells they belong to are no longer on screen.\n const resetToken = `${rowMode}|${days[0] ?? ''}|${days[days.length - 1] ?? ''}|${staffMemberId ?? ''}`\n const resetTokenRef = React.useRef(resetToken)\n React.useEffect(() => {\n if (resetTokenRef.current === resetToken) return\n resetTokenRef.current = resetToken\n setDirty({})\n setInvalidCells({})\n setRejectedCells({})\n }, [resetToken])\n\n const isCellLocked = React.useCallback(\n (key: string) => {\n const cell = cells.get(key)\n if (!cell) return false\n if (cell.lockedEntryIds.length > 0) return true\n return cell.entryIds.some((id) => extraLockedEntryIds[id])\n },\n [cells, extraLockedEntryIds],\n )\n\n const cellMinutes = React.useCallback(\n (key: string) => {\n const pending = dirty[key]\n if (pending) return pending.minutes\n return cells.get(key)?.minutes ?? 0\n },\n [cells, dirty],\n )\n\n const handleCellChange = React.useCallback(\n (row: GridRow, date: string, minutes: number | null, invalid: boolean) => {\n const key = cellKey(row.key, date)\n setRejectedCells((current) => {\n if (!current[key]) return current\n const next = { ...current }\n delete next[key]\n return next\n })\n setInvalidCells((current) => {\n if (invalid === Boolean(current[key])) return current\n const next = { ...current }\n if (invalid) next[key] = true\n else delete next[key]\n return next\n })\n // Unparseable text is never turned into a number \u2014 the previous value stands\n // and Save stays blocked until the text is fixed or cleared.\n if (invalid) return\n const nextMinutes = minutes ?? 0\n const stored = cells.get(key)\n const storedMinutes = stored?.minutes ?? 0\n const hadEntries = (stored?.entryIds.length ?? 0) > 0\n setDirty((current) => {\n const next = { ...current }\n if (nextMinutes === storedMinutes && !hadEntries) {\n delete next[key]\n return next\n }\n next[key] = { rowKey: row.key, date, minutes: nextMinutes }\n return next\n })\n },\n [cells],\n )\n\n const rowTotal = React.useCallback(\n (rowKey: string) => days.reduce((sum, date) => sum + cellMinutes(cellKey(rowKey, date)), 0),\n [cellMinutes, days],\n )\n const dayTotal = React.useCallback(\n (date: string) => rows.reduce((sum, row) => sum + cellMinutes(cellKey(row.key, date)), 0),\n [cellMinutes, rows],\n )\n const grandTotal = React.useMemo(\n () => rows.reduce((sum, row) => sum + rowTotal(row.key), 0),\n [rowTotal, rows],\n )\n\n const dirtyCount = Object.keys(dirty).length\n const invalidCount = Object.keys(invalidCells).length\n const hasChanges = dirtyCount > 0\n const dense = days.length > 10\n\n const markLockedFromError = React.useCallback((error: unknown) => {\n const body = (error as { body?: { lockedEntryIds?: unknown } } | null)?.body\n const direct = (error as { lockedEntryIds?: unknown } | null)?.lockedEntryIds\n const ids = Array.isArray(body?.lockedEntryIds)\n ? body?.lockedEntryIds\n : Array.isArray(direct)\n ? direct\n : []\n const marked = ids.filter((id): id is string => typeof id === 'string')\n if (marked.length === 0) return false\n setExtraLockedEntryIds((current) => {\n const next = { ...current }\n for (const id of marked) next[id] = true\n return next\n })\n return true\n }, [])\n\n const reportSaveFailure = React.useCallback(\n (error: unknown, rejectedKeys: readonly string[]) => {\n if (surfaceRecordConflict(error, t)) return\n if (isEntryLockedError(error)) {\n markLockedFromError(error)\n flash(\n t(\n 'staff.time_tracking.timesheet.grid.errors.locked',\n 'Some of these cells belong to a closed report and cannot be changed. They are now marked as locked.',\n ),\n 'error',\n )\n return\n }\n logger.error('staff.time_tracking.timesheet.grid save failed', { err: error, code: readErrorCode(error) })\n const message = readErrorMessage(error, t('staff.timesheets.my.errors.save', 'Failed to save timesheets.'))\n if (rejectedKeys.length > 0) {\n setRejectedCells((current) => {\n const next = { ...current }\n for (const key of rejectedKeys) next[key] = message\n return next\n })\n }\n flash(message, 'error')\n },\n [markLockedFromError, t],\n )\n\n /**\n * Task-row cells, one write each through the single-entry route \u2014 see the file\n * header for why they cannot ride the batch yet. A locked cell never reaches\n * here (its input is read-only), so a 409 from this path means the entry was\n * frozen after the page loaded, which is exactly when the user needs to be\n * told which cell it was. Each cell is reported through `onCellSaved` as soon\n * as it lands, so a refusal further down the list leaves the caller knowing\n * exactly which cells are already stored.\n */\n const saveTaskCells = React.useCallback(\n async (changes: Array<{ row: GridRow; cell: DirtyCell }>, onCellSaved: (key: string) => void) => {\n for (const { row, cell } of changes) {\n const key = cellKey(row.key, cell.date)\n const storedCell = cells.get(key)\n const existingId = storedCell?.entryIds[0] ?? null\n const existingVersion = existingId ? storedCell?.entryVersions[existingId] ?? null : null\n const errorMessage = t('staff.timesheets.my.errors.save', 'Failed to save timesheets.')\n if (existingId && cell.minutes <= 0) {\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(existingVersion), () =>\n deleteCrud(ENTRIES_API_PATH, existingId, { errorMessage }),\n ),\n context: mutationContext(existingId),\n mutationPayload: { id: existingId },\n })\n onCellSaved(key)\n continue\n }\n if (cell.minutes <= 0) {\n onCellSaved(key)\n continue\n }\n if (existingId) {\n const payload = { id: existingId, durationMinutes: cell.minutes }\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(buildOptimisticLockHeader(existingVersion), () =>\n updateCrud(ENTRIES_API_PATH, payload, { errorMessage }),\n ),\n context: mutationContext(existingId),\n mutationPayload: payload,\n })\n onCellSaved(key)\n continue\n }\n const payload = {\n staffMemberId,\n date: cell.date,\n timeProjectId: row.timeProjectId,\n taskId: row.taskId,\n durationMinutes: cell.minutes,\n source: 'manual',\n }\n await runMutation({\n operation: () => createCrud(ENTRIES_API_PATH, payload, { errorMessage }),\n context: mutationContext(),\n mutationPayload: payload,\n })\n onCellSaved(key)\n }\n },\n [cells, mutationContext, runMutation, staffMemberId, t],\n )\n\n const handleSave = React.useCallback(async () => {\n if (!hasChanges || invalidCount > 0) return\n const confirmed = await confirm({\n title: t('staff.timesheets.my.confirm_save.title', 'Save changes?'),\n text: t('staff.timesheets.my.confirm_save.body', 'Your timesheet entries will be saved.'),\n })\n if (!confirmed) return\n\n const bulkEntries: Array<{ id?: string; date: string; timeProjectId: string; durationMinutes: number }> = []\n const bulkKeys: string[] = []\n const taskChanges: Array<{ row: GridRow; cell: DirtyCell }> = []\n for (const cell of Object.values(dirty)) {\n const row = rowsByKey.get(cell.rowKey)\n if (!row) continue\n if (row.taskId) {\n taskChanges.push({ row, cell })\n continue\n }\n const key = cellKey(row.key, cell.date)\n bulkKeys.push(key)\n bulkEntries.push({\n id: cells.get(key)?.entryIds[0],\n date: cell.date,\n timeProjectId: row.timeProjectId,\n durationMinutes: cell.minutes,\n })\n }\n if (bulkEntries.length === 0 && taskChanges.length === 0) return\n\n // The project rows and the task rows commit separately (see the file\n // header), so a refusal can arrive after some cells are already stored. Those\n // cells leave the pending set and the period is reloaded, so the stored rows\n // pick up their ids \u2014 otherwise the next Save would create them a second time.\n // The bulk request opts out of the shared 403 handling, which keeps only the\n // message: the access refusal's `errors[].index` is what marks the cell.\n const savedKeys = new Set<string>()\n let bulkCommitted = bulkEntries.length === 0\n setIsSaving(true)\n try {\n if (bulkEntries.length > 0) {\n const payload = { entries: bulkEntries }\n await runMutation({\n operation: () =>\n apiCallOrThrow(\n BULK_ENDPOINT,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-om-forbidden-redirect': '0' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('staff.timesheets.my.errors.save', 'Failed to save timesheets.') },\n ),\n context: mutationContext(staffMemberId ?? undefined),\n mutationPayload: payload as unknown as Record<string, unknown>,\n })\n bulkCommitted = true\n for (const key of bulkKeys) savedKeys.add(key)\n }\n if (taskChanges.length > 0) await saveTaskCells(taskChanges, (key) => savedKeys.add(key))\n setDirty({})\n setRejectedCells({})\n flash(t('staff.timesheets.my.saved', 'Timesheet saved.'), 'success')\n await onSaved()\n } catch (error) {\n const rejectedKeys = bulkCommitted\n ? taskChanges\n .map(({ row, cell }) => cellKey(row.key, cell.date))\n .filter((key) => !savedKeys.has(key))\n .slice(0, 1)\n : readRejectedRowIndexes(error)\n .map((index) => bulkKeys[index])\n .filter((key): key is string => typeof key === 'string')\n reportSaveFailure(error, rejectedKeys)\n if (savedKeys.size > 0) {\n setDirty((current) => {\n const next = { ...current }\n for (const key of savedKeys) delete next[key]\n return next\n })\n try {\n await onSaved()\n } catch (reloadError) {\n logger.error('staff.time_tracking.timesheet.grid reload after partial save failed', { err: reloadError })\n }\n }\n } finally {\n setIsSaving(false)\n }\n }, [\n cells,\n confirm,\n dirty,\n hasChanges,\n invalidCount,\n mutationContext,\n onSaved,\n reportSaveFailure,\n rowsByKey,\n runMutation,\n saveTaskCells,\n staffMemberId,\n t,\n ])\n\n const taskRowOptions = React.useMemo(() => {\n const visibleProjectIds = new Set(projects.map((project) => project.id))\n const existing = new Set(rows.map((row) => row.key))\n return tasks\n .filter((task) => visibleProjectIds.has(task.timeProjectId))\n .map((task) => {\n const project = projects.find((candidate) => candidate.id === task.timeProjectId)!\n return {\n key: buildTargetKey(task.timeProjectId, task.id),\n label: targetLabel({\n key: buildTargetKey(task.timeProjectId, task.id),\n timeProjectId: task.timeProjectId,\n taskId: task.id,\n projectName: project.name,\n projectCode: project.code,\n projectColor: project.color,\n taskTitle: task.title,\n }),\n }\n })\n .filter((option) => !existing.has(option.key))\n }, [projects, rows, tasks])\n\n const editable = canManage && !readOnly\n const loadingLabel = t('staff.time_tracking.timesheet.grid.loading', 'Loading the timesheet grid\\u2026')\n\n return (\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <SegmentedControl\n value={rowMode}\n onValueChange={(value) => {\n if (value !== rowMode) onRowModeChange(value as GridRowMode)\n }}\n size=\"sm\"\n aria-label={t('staff.time_tracking.timesheet.grid.rowMode.label', 'Grid rows')}\n >\n <SegmentedControlItem value=\"projects\">\n {t('staff.time_tracking.timesheet.grid.rowMode.projects', 'Projects')}\n </SegmentedControlItem>\n <SegmentedControlItem value=\"tasks\">\n {t('staff.time_tracking.timesheet.grid.rowMode.tasks', 'Projects + tasks')}\n </SegmentedControlItem>\n </SegmentedControl>\n\n <div className=\"flex items-center gap-3\">\n {invalidCount > 0 ? (\n <span className=\"text-xs font-medium text-status-error-text\">\n {t('staff.time_tracking.timesheet.grid.invalid', 'Fix the highlighted cell before saving.')}\n </span>\n ) : hasChanges ? (\n <span className=\"text-xs font-medium text-status-warning-text\">\n {t('staff.timesheets.my.unsaved', 'Unsaved changes')}\n </span>\n ) : null}\n {editable ? (\n <Button\n size=\"sm\"\n type=\"button\"\n onClick={() => void handleSave()}\n disabled={!hasChanges || invalidCount > 0 || isSaving}\n >\n {isSaving\n ? t('staff.timesheets.my.saving', 'Saving...')\n : t('staff.timesheets.my.save_changes', 'Save Changes')}\n </Button>\n ) : null}\n </div>\n </div>\n\n {rowMode === 'tasks' && editable && taskRowOptions.length > 0 ? (\n <div className=\"flex items-center gap-2\">\n <Select\n value=\"\"\n onValueChange={(value) => setExtraTaskKeys((current) => new Set(current).add(value))}\n >\n <SelectTrigger\n className=\"w-72\"\n aria-label={t('staff.time_tracking.timesheet.grid.addTaskRow', 'Add a task row')}\n >\n <SelectValue placeholder={t('staff.time_tracking.timesheet.grid.addTaskRow', 'Add a task row')} />\n </SelectTrigger>\n <SelectContent>\n {taskRowOptions.map((option) => (\n <SelectItem key={option.key} value={option.key}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n ) : null}\n\n <div className=\"isolate overflow-x-auto rounded-lg border\">\n <table className=\"w-full text-sm table-fixed\" aria-busy={isLoading || undefined}>\n <caption className=\"sr-only\">\n {t(\n 'staff.time_tracking.timesheet.grid.caption',\n 'Timesheet grid: one row per project or task, one column per day of the period.',\n )}\n </caption>\n <colgroup>\n <col className={dense ? 'w-[140px] min-w-[140px]' : 'w-[35%]'} />\n {days.map((date) => (\n <col key={date} className={dense ? 'w-[36px] min-w-[36px]' : ''} />\n ))}\n <col className={dense ? 'w-[56px] min-w-[56px]' : 'w-[72px]'} />\n </colgroup>\n <thead>\n <tr className=\"border-b bg-muted/50\">\n <th scope=\"col\" className=\"sticky left-0 z-10 bg-muted px-3 py-2 text-left font-medium\">\n {t('staff.timesheets.my.project', 'Project')}\n </th>\n {days.map((date) => {\n const { weekday, dayOfMonth } = dayHeaderParts(date)\n const weekend = isWeekendIso(date)\n // Weekends were signalled by a background tint alone, which a\n // screen reader and a monochrome display both miss. The tint stays;\n // the meaning now also travels as a title and an sr-only word.\n const weekendLabel = t('staff.time_tracking.timesheet.grid.weekend', 'Weekend day')\n return (\n <th\n scope=\"col\"\n key={date}\n title={weekend ? `${date} \u00B7 ${weekendLabel}` : date}\n className={cn(\n 'py-2 text-center font-medium px-1',\n weekend ? 'bg-muted/80 text-muted-foreground' : null,\n )}\n >\n <div className=\"text-xs uppercase text-muted-foreground\">{weekday}</div>\n <div className=\"text-xs\">{dayOfMonth}</div>\n {weekend ? <span className=\"sr-only\">{weekendLabel}</span> : null}\n </th>\n )\n })}\n <th scope=\"col\" className=\"px-3 py-2 text-right font-medium\">\n {t('staff.timesheets.my.total', 'Total')}\n </th>\n </tr>\n </thead>\n <tbody>\n {isLoading\n ? Array.from({ length: SKELETON_ROW_COUNT }).map((_, index) => (\n <tr key={`grid-skeleton-${index}`} data-testid=\"grid-skeleton-row\" className=\"border-b\">\n <th scope=\"row\" className=\"sticky left-0 z-10 bg-background px-3 py-1.5 text-left font-medium\">\n {index === 0 ? <span className=\"sr-only\">{loadingLabel}</span> : null}\n <span aria-hidden=\"true\" className=\"block h-4 w-32 animate-pulse rounded bg-muted\" />\n </th>\n {days.map((date) => (\n <td key={date} className=\"px-0.5 py-0.5\">\n <span\n aria-hidden=\"true\"\n className={cn('mx-auto block h-6 animate-pulse rounded bg-muted', dense ? 'w-10' : 'w-16')}\n />\n </td>\n ))}\n <td className=\"px-3 py-1.5\">\n <span aria-hidden=\"true\" className=\"ml-auto block h-4 w-8 animate-pulse rounded bg-muted\" />\n </td>\n </tr>\n ))\n : null}\n {!isLoading && rows.length === 0 ? (\n <tr data-testid=\"grid-empty-row\" className=\"border-b\">\n <td colSpan={days.length + 2} className=\"px-3 py-10 text-center\">\n <p className=\"text-sm font-medium text-foreground\">\n {t('staff.time_tracking.timesheet.grid.empty.title', 'This grid has no rows yet')}\n </p>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {t(\n 'staff.time_tracking.timesheet.grid.noRows',\n 'Add a project row to start filling the grid.',\n )}\n </p>\n </td>\n </tr>\n ) : null}\n {isLoading ? null : rows.map((row) => (\n <tr key={row.key} className=\"group border-b hover:bg-muted/30\">\n <th\n scope=\"row\"\n className={cn(\n 'sticky left-0 z-10 bg-background px-3 py-1.5 text-left font-medium text-foreground',\n row.taskId ? 'pl-6' : null,\n )}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 truncate\" title={targetLabel(row)}>\n {row.taskId ? null : (\n <ProjectColorDot colorKey={row.projectColor} projectName={row.projectName} size=\"sm\" />\n )}\n <span className=\"truncate\">{row.taskId ? row.taskTitle ?? targetLabel(row) : row.projectName}</span>\n </div>\n {row.taskId ? (\n <div className=\"text-xs text-muted-foreground truncate\">{row.projectName}</div>\n ) : row.projectCode ? (\n <div className=\"text-xs text-muted-foreground\">{row.projectCode}</div>\n ) : null}\n </div>\n {row.removable && editable ? (\n <button\n type=\"button\"\n onClick={() => {\n const project = projects.find((candidate) => candidate.id === row.timeProjectId)\n if (!project) return\n // Drop the row's pending errors with the row. An invalid\n // cell left behind keeps Save blocked over a row that is\n // no longer on screen, so the reader cannot reach the\n // thing they are being told to fix.\n setInvalidCells((current) => {\n const prefix = cellKey(row.key, '')\n const next = Object.fromEntries(\n Object.entries(current).filter(([key]) => !key.startsWith(prefix)),\n ) as Record<string, true>\n return Object.keys(next).length === Object.keys(current).length ? current : next\n })\n setRejectedCells((current) => {\n const prefix = cellKey(row.key, '')\n const next = Object.fromEntries(\n Object.entries(current).filter(([key]) => !key.startsWith(prefix)),\n ) as Record<string, string>\n return Object.keys(next).length === Object.keys(current).length ? current : next\n })\n setDirty((current) => {\n const prefix = cellKey(row.key, '')\n const next = Object.fromEntries(\n Object.entries(current).filter(([key]) => !key.startsWith(prefix)),\n ) as typeof current\n return Object.keys(next).length === Object.keys(current).length ? current : next\n })\n void onRemoveProject(project)\n }}\n aria-label={t('staff.timesheets.my.removeRow', 'Remove from grid')}\n title={t('staff.timesheets.my.removeRow', 'Remove from grid')}\n className=\"opacity-0 group-hover:opacity-100 focus:opacity-100 inline-flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground transition-opacity\"\n >\n <X className=\"size-3.5\" />\n </button>\n ) : null}\n </div>\n </th>\n {days.map((date) => {\n const key = cellKey(row.key, date)\n const weekend = isWeekendIso(date)\n const minutes = cellMinutes(key)\n const locked = isCellLocked(key)\n const isDirty = dirty[key] !== undefined\n const rejection = rejectedCells[key]\n const isInvalid = Boolean(invalidCells[key]) || Boolean(rejection)\n return (\n <td\n key={date}\n className={cn(\n 'px-0.5 py-0.5',\n weekend ? 'bg-muted/40' : null,\n // The old grid tinted the input itself amber; the border and\n // background of a `DurationInput` belong to the primitive, so\n // the pending-edit cue moved to the cell \u2014 same signal, DS\n // status tokens instead of raw `amber-*` (Boy Scout rule).\n isInvalid\n ? 'bg-status-error-bg'\n : isDirty\n ? 'bg-status-warning-bg'\n : null,\n )}\n >\n {weekend && minutes === 0 ? (\n <div className=\"rounded px-1 py-1 text-center text-xs text-muted-foreground/50\">-</div>\n ) : locked || !editable ? (\n <div\n className=\"flex items-center justify-center gap-1 rounded px-1 py-1 text-center text-xs tabular-nums text-muted-foreground\"\n title={\n locked\n ? t(\n 'staff.time_tracking.timesheet.grid.lockedCell',\n 'Locked by a closed report',\n )\n : undefined\n }\n >\n {locked ? <Lock className=\"size-3 shrink-0\" aria-hidden=\"true\" /> : null}\n <span>{formatTrimmedDecimalDuration(minutes) || '-'}</span>\n </div>\n ) : (\n <DurationInput\n variant=\"compact\"\n inputSize=\"sm\"\n value={minutes > 0 ? minutes : null}\n formatValue={formatTrimmedDecimalDuration}\n error={rejection}\n onChange={(nextMinutes, state) =>\n handleCellChange(row, date, nextMinutes, state.status === 'invalid')\n }\n placeholder={t('staff.timesheets.my.durationPlaceholder', '0')}\n ariaLabel={`${targetLabel(row)} \u00B7 ${date}`}\n className={cn('mx-auto', dense ? 'w-10' : 'w-16')}\n inputClassName={cn(\n 'text-center px-1 tabular-nums',\n dense ? 'text-xs' : null,\n minutes > 0 ? 'font-semibold' : 'text-muted-foreground',\n )}\n />\n )}\n </td>\n )\n })}\n <td className=\"px-3 py-1.5 text-right font-semibold text-xs tabular-nums\">\n {formatTrimmedDecimalDuration(rowTotal(row.key)) || '0'}\n </td>\n </tr>\n ))}\n {editable && !isLoading ? (\n <tr className=\"border-b\">\n <td colSpan={days.length + 2} className=\"sticky left-0 bg-background px-1 py-0.5\">\n <AddRowDropdown\n assignedProjects={allAssignedProjects.map((project) => ({\n id: project.id,\n name: project.name,\n code: project.code,\n color: project.color,\n }))}\n visibleProjectIds={new Set(projects.map((project) => project.id))}\n canCreateProject={canManageProjects}\n onAddProject={(project) => {\n void onAddProject({\n id: project.id,\n name: project.name,\n code: project.code ?? null,\n color: project.color ?? null,\n })\n }}\n onCreateProject={onCreateProject}\n />\n </td>\n </tr>\n ) : null}\n </tbody>\n <tfoot>\n <tr className=\"border-t bg-muted/50 font-semibold\">\n <th scope=\"row\" className=\"sticky left-0 z-10 bg-muted px-3 py-2 text-left\">\n {t('staff.timesheets.my.daily_total', 'Daily Total')}\n </th>\n {days.map((date) => {\n const weekend = isWeekendIso(date)\n const minutes = dayTotal(date)\n return (\n <td\n key={date}\n className={cn(\n 'py-2 text-center text-xs tabular-nums',\n weekend ? 'text-muted-foreground/50' : null,\n )}\n >\n {weekend && minutes === 0 ? '-' : formatTrimmedDecimalDuration(minutes) || '-'}\n </td>\n )\n })}\n <td className=\"px-3 py-2 text-right tabular-nums font-semibold\">\n {formatTrimmedDecimalDuration(grandTotal) || '0'}\n </td>\n </tr>\n </tfoot>\n </table>\n </div>\n\n {ConfirmDialogElement}\n </div>\n )\n}\n\nconst gridViewPropsSchema: z.ZodType<GridViewProps> = z.object({\n days: z.array(z.string()),\n projects: z.array(opaqueProp<TimesheetProjectRef>()),\n allAssignedProjects: z.array(opaqueProp<TimesheetProjectRef>()),\n tasks: z.array(opaqueProp<TimesheetTaskRef>()),\n entries: z.array(opaqueProp<TimesheetEntry>()),\n staffMemberId: z.string().nullable(),\n canManage: z.boolean(),\n canManageProjects: z.boolean(),\n readOnly: z.boolean(),\n isLoading: z.boolean().optional(),\n rowMode: z.union([z.literal('projects'), z.literal('tasks')]),\n onRowModeChange: callbackProp<(mode: GridRowMode) => void>(),\n onAddProject: callbackProp<(project: TimesheetProjectRef) => void | Promise<void>>(),\n onRemoveProject: callbackProp<(project: TimesheetProjectRef) => void | Promise<void>>(),\n onCreateProject: callbackProp<() => void>(),\n onSaved: callbackProp<() => void | Promise<void>>(),\n})\n\nregisterComponent<GridViewProps>({\n id: extensionPoints.hosts.timesheetGridComponent.componentId,\n component: DefaultGridView,\n metadata: {\n module: 'staff',\n description: 'Timesheet grid view \u2014 projects/tasks by day, editable cells.',\n propsSchema: gridViewPropsSchema,\n },\n})\n\nexport function GridView(props: GridViewProps) {\n const Resolved = useRegisteredComponent<GridViewProps>(\n extensionPoints.hosts.timesheetGridComponent.componentId,\n DefaultGridView,\n )\n return <Resolved {...props} />\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["payload"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7706.1.9cb1f164c4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -265,16 +265,16 @@
|
|
|
265
265
|
"zod": "^4.4.3"
|
|
266
266
|
},
|
|
267
267
|
"peerDependencies": {
|
|
268
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
269
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
268
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7706.1.9cb1f164c4",
|
|
269
|
+
"@open-mercato/shared": "0.10.1-develop.7706.1.9cb1f164c4",
|
|
270
|
+
"@open-mercato/ui": "0.10.1-develop.7706.1.9cb1f164c4",
|
|
271
271
|
"react": "^19.0.0",
|
|
272
272
|
"react-dom": "^19.0.0"
|
|
273
273
|
},
|
|
274
274
|
"devDependencies": {
|
|
275
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
276
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
275
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7706.1.9cb1f164c4",
|
|
276
|
+
"@open-mercato/shared": "0.10.1-develop.7706.1.9cb1f164c4",
|
|
277
|
+
"@open-mercato/ui": "0.10.1-develop.7706.1.9cb1f164c4",
|
|
278
278
|
"@testing-library/dom": "^10.4.1",
|
|
279
279
|
"@testing-library/jest-dom": "^7.0.1",
|
|
280
280
|
"@testing-library/react": "^16.3.3",
|
|
@@ -57,13 +57,13 @@ export async function GET(req: Request) {
|
|
|
57
57
|
if (search) {
|
|
58
58
|
const pattern = buildIlikeTerm(search)
|
|
59
59
|
qb.andWhere(`(
|
|
60
|
-
cast(gt.id as text) ilike ?
|
|
61
|
-
or cast(gt.payment_id as text) ilike ?
|
|
62
|
-
or coalesce(gt.provider_key, '') ilike ?
|
|
63
|
-
or coalesce(gt.provider_session_id, '') ilike ?
|
|
64
|
-
or coalesce(gt.gateway_payment_id, '') ilike ?
|
|
65
|
-
or coalesce(gt.gateway_refund_id, '') ilike ?
|
|
66
|
-
)
|
|
60
|
+
cast(gt.id as text) ilike ? escape '\\'
|
|
61
|
+
or cast(gt.payment_id as text) ilike ? escape '\\'
|
|
62
|
+
or coalesce(gt.provider_key, '') ilike ? escape '\\'
|
|
63
|
+
or coalesce(gt.provider_session_id, '') ilike ? escape '\\'
|
|
64
|
+
or coalesce(gt.gateway_payment_id, '') ilike ? escape '\\'
|
|
65
|
+
or coalesce(gt.gateway_refund_id, '') ilike ? escape '\\'
|
|
66
|
+
)`, [pattern, pattern, pattern, pattern, pattern, pattern])
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const countQb = qb.clone()
|
|
@@ -21,6 +21,7 @@ export const metadata = {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const WEBHOOK_VERIFICATION_FAILED = 'Webhook verification failed'
|
|
24
|
+
const MAX_WEBHOOK_CANDIDATES = 10
|
|
24
25
|
|
|
25
26
|
const shippingCarrierWebhookRateLimitConfig = {
|
|
26
27
|
points: 60,
|
|
@@ -88,37 +89,46 @@ export async function POST(req: Request, { params }: { params: Promise<{ provide
|
|
|
88
89
|
carrierShipmentId,
|
|
89
90
|
deletedAt: null,
|
|
90
91
|
},
|
|
91
|
-
{ limit:
|
|
92
|
+
{ limit: MAX_WEBHOOK_CANDIDATES + 1, orderBy: { createdAt: 'desc' } },
|
|
92
93
|
)
|
|
93
94
|
: []
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
if (candidates.length > MAX_WEBHOOK_CANDIDATES) {
|
|
97
|
+
throw new Error('[internal] Webhook verification failed: candidate limit exceeded')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const verifiedMatches: Array<{
|
|
101
|
+
shipment: CarrierShipment
|
|
102
|
+
scope: { organizationId: string; tenantId: string }
|
|
103
|
+
event: Awaited<ReturnType<typeof adapter.verifyWebhook>>
|
|
104
|
+
}> = []
|
|
98
105
|
let lastVerificationError: unknown = null
|
|
99
106
|
|
|
100
107
|
for (const candidate of candidates) {
|
|
101
108
|
const candidateScope = { organizationId: candidate.organizationId, tenantId: candidate.tenantId }
|
|
102
109
|
const credentials = await integrationCredentialsService.resolve(`carrier_${providerKey}`, candidateScope) ?? {}
|
|
103
110
|
try {
|
|
104
|
-
event = await adapter.verifyWebhook({ rawBody, headers, credentials })
|
|
105
|
-
shipment
|
|
106
|
-
matchedScope = candidateScope
|
|
107
|
-
break
|
|
111
|
+
const event = await adapter.verifyWebhook({ rawBody, headers, credentials })
|
|
112
|
+
verifiedMatches.push({ shipment: candidate, scope: candidateScope, event })
|
|
108
113
|
} catch (error: unknown) {
|
|
109
114
|
lastVerificationError = error
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
if (
|
|
118
|
+
if (verifiedMatches.length > 1) {
|
|
119
|
+
throw new Error('[internal] Webhook verification failed: multiple matching shipments')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const [match] = verifiedMatches
|
|
123
|
+
if (!match) {
|
|
114
124
|
throw lastVerificationError ?? new Error('Webhook verification failed: no matching shipment')
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
await queue.enqueue({
|
|
118
128
|
providerKey,
|
|
119
|
-
event,
|
|
120
|
-
shipmentId: shipment.id,
|
|
121
|
-
scope:
|
|
129
|
+
event: match.event,
|
|
130
|
+
shipmentId: match.shipment.id,
|
|
131
|
+
scope: match.scope,
|
|
122
132
|
})
|
|
123
133
|
|
|
124
134
|
return NextResponse.json({ received: true, queued: true }, { status: 202 })
|
|
@@ -711,7 +711,7 @@ function DefaultGridView({
|
|
|
711
711
|
</div>
|
|
712
712
|
) : null}
|
|
713
713
|
|
|
714
|
-
<div className="overflow-x-auto rounded-lg border">
|
|
714
|
+
<div className="isolate overflow-x-auto rounded-lg border">
|
|
715
715
|
<table className="w-full text-sm table-fixed" aria-busy={isLoading || undefined}>
|
|
716
716
|
<caption className="sr-only">
|
|
717
717
|
{t(
|