@open-mercato/core 0.10.1-develop.7922.1.417ebe275c → 0.10.1-develop.7925.1.ec40f75262
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/catalog/backend/catalog/products/create/page.js +16 -4
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductMediaManager.js +5 -2
- package/dist/modules/catalog/components/products/ProductMediaManager.js.map +2 -2
- package/dist/modules/entities/api/records.js +18 -2
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/route.js +65 -5
- package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +49 -25
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js +13 -6
- package/dist/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js +23 -0
- package/dist/modules/staff/lib/time-tracking-ui/timeEntryListData.js.map +2 -2
- package/dist/modules/staff/lib/timesheets/timeEntryTotals.js +149 -0
- package/dist/modules/staff/lib/timesheets/timeEntryTotals.js.map +7 -0
- package/dist/modules/staff/widgets/injection/time-entries-summary-footer/widget.js +24 -0
- package/dist/modules/staff/widgets/injection/time-entries-summary-footer/widget.js.map +7 -0
- package/dist/modules/staff/widgets/injection-table.js +4 -0
- package/dist/modules/staff/widgets/injection-table.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +14 -1
- package/src/modules/catalog/components/products/ProductMediaManager.tsx +5 -1
- package/src/modules/entities/api/records.ts +18 -2
- package/src/modules/entities/i18n/de.json +1 -0
- package/src/modules/entities/i18n/en.json +1 -0
- package/src/modules/entities/i18n/es.json +1 -0
- package/src/modules/entities/i18n/ko.json +1 -0
- package/src/modules/entities/i18n/pl.json +1 -0
- package/src/modules/staff/AGENTS.md +1 -0
- package/src/modules/staff/api/timesheets/time-entries/route.ts +77 -4
- package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +29 -7
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/ko.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/lib/time-tracking-ui/TimeEntriesSummaryFooter.tsx +25 -8
- package/src/modules/staff/lib/time-tracking-ui/timeEntryListData.ts +30 -0
- package/src/modules/staff/lib/timesheets/timeEntryTotals.ts +271 -0
- package/src/modules/staff/widgets/injection/time-entries-summary-footer/widget.tsx +32 -0
- package/src/modules/staff/widgets/injection-table.ts +4 -0
|
@@ -24,7 +24,8 @@ function ProductMediaManager({
|
|
|
24
24
|
items,
|
|
25
25
|
defaultMediaId,
|
|
26
26
|
onItemsChange,
|
|
27
|
-
onDefaultChange
|
|
27
|
+
onDefaultChange,
|
|
28
|
+
onUploadingChange
|
|
28
29
|
}) {
|
|
29
30
|
const t = useT();
|
|
30
31
|
const fileInputRef = React.useRef(null);
|
|
@@ -36,6 +37,7 @@ function ProductMediaManager({
|
|
|
36
37
|
if (!files || !files.length || !draftRecordId) return;
|
|
37
38
|
setError(null);
|
|
38
39
|
setUploading(true);
|
|
40
|
+
onUploadingChange?.(true);
|
|
39
41
|
let nextItems = items;
|
|
40
42
|
let defaultId = defaultMediaId;
|
|
41
43
|
try {
|
|
@@ -78,12 +80,13 @@ function ProductMediaManager({
|
|
|
78
80
|
}
|
|
79
81
|
} finally {
|
|
80
82
|
setUploading(false);
|
|
83
|
+
onUploadingChange?.(false);
|
|
81
84
|
if (fileInputRef.current) {
|
|
82
85
|
fileInputRef.current.value = "";
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
},
|
|
86
|
-
[defaultMediaId, draftRecordId, entityId, items, onDefaultChange, onItemsChange, t]
|
|
89
|
+
[defaultMediaId, draftRecordId, entityId, items, onDefaultChange, onItemsChange, onUploadingChange, t]
|
|
87
90
|
);
|
|
88
91
|
const handleRemove = React.useCallback(
|
|
89
92
|
async (attachmentId) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/catalog/components/products/ProductMediaManager.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Upload, Image as ImageIcon, Trash2, Star } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\n\nexport type ProductMediaItem = {\n id: string\n url: string\n fileName: string\n fileSize: number\n thumbnailUrl?: string | null\n}\n\ntype Props = {\n entityId: string\n draftRecordId: string\n items: ProductMediaItem[]\n defaultMediaId: string | null\n onItemsChange: (items: ProductMediaItem[]) => void\n onDefaultChange: (attachmentId: string | null) => void\n}\n\nfunction humanFileSize(size: number): string {\n if (!Number.isFinite(size)) return `${size}`\n const units = ['B', 'KB', 'MB', 'GB']\n let idx = 0\n let value = size\n while (value >= 1024 && idx < units.length - 1) {\n value /= 1024\n idx++\n }\n return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`\n}\n\nexport function ProductMediaManager({\n entityId,\n draftRecordId,\n items,\n defaultMediaId,\n onItemsChange,\n onDefaultChange,\n}: Props) {\n const t = useT()\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [isUploading, setUploading] = React.useState(false)\n const [isDragOver, setDragOver] = React.useState(false)\n\n const acceptFiles = React.useCallback(\n async (files: FileList | null) => {\n if (!files || !files.length || !draftRecordId) return\n setError(null)\n setUploading(true)\n let nextItems = items\n let defaultId = defaultMediaId\n try {\n for (const file of Array.from(files)) {\n if (!file.type.startsWith('image/')) {\n setError(t('catalog.products.media.errors.imagesOnly', 'Only image files are supported.'))\n continue\n }\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', draftRecordId)\n fd.set('file', file)\n // optimistic-lock-exempt: media attach/detach/reorder\n const call = await apiCall<{ ok?: boolean; item?: ProductMediaItem; error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\n )\n if (!call.ok || !call.result?.item) {\n const message = call.result?.error ?? t('catalog.products.media.errors.uploadFailed', 'Upload failed.')\n setError(message)\n break\n }\n const existingIds = new Set(nextItems.map((entry) => entry.id))\n if (existingIds.has(call.result.item.id)) continue\n const slug = slugifyAttachmentFileName(call.result.item.fileName)\n const uploaded = {\n ...call.result.item,\n thumbnailUrl:\n call.result.item.thumbnailUrl ??\n buildAttachmentImageUrl(call.result.item.id, { width: 360, height: 360, slug }),\n }\n nextItems = [...nextItems, uploaded]\n if (!defaultId) {\n defaultId = uploaded.id\n }\n }\n if (nextItems !== items) {\n onItemsChange(nextItems)\n }\n if (!defaultMediaId && defaultId) {\n onDefaultChange(defaultId)\n }\n } finally {\n setUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n },\n [defaultMediaId, draftRecordId, entityId, items, onDefaultChange, onItemsChange, t],\n )\n\n const handleRemove = React.useCallback(\n async (attachmentId: string) => {\n setError(null)\n // optimistic-lock-exempt: media attach/detach/reorder\n const call = await apiCall<{ ok?: boolean; error?: string }>(\n `/api/attachments?id=${encodeURIComponent(attachmentId)}`,\n { method: 'DELETE' },\n { fallback: null },\n )\n if (!call.ok) {\n setError(call.result?.error ?? t('catalog.products.media.errors.deleteFailed', 'Failed to delete media.'))\n return\n }\n const next = items.filter((item) => item.id !== attachmentId)\n onItemsChange(next)\n if (defaultMediaId === attachmentId) {\n onDefaultChange(next[0]?.id ?? null)\n }\n },\n [defaultMediaId, items, onDefaultChange, onItemsChange, t],\n )\n\n const onDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n void acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles],\n )\n\n const onDragOver = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(true)\n }, [])\n\n const onDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n }, [])\n\n const pickFiles = React.useCallback(() => {\n fileInputRef.current?.click()\n }, [])\n\n return (\n <div className=\"space-y-3\">\n <label className=\"text-sm font-medium\">{t('catalog.products.media.label', 'Media')}</label>\n <div\n className={cn(\n 'flex flex-col items-center justify-center rounded-lg border border-dashed p-6 text-center transition-colors',\n isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/30',\n )}\n onDrop={onDrop}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n role=\"presentation\"\n >\n <Upload className=\"mx-auto h-6 w-6 text-muted-foreground\" />\n <p className=\"mt-2 text-sm text-muted-foreground\">\n {t('catalog.products.media.dropHint', 'Drag and drop images here or click to upload.')}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={pickFiles} disabled={isUploading}>\n {isUploading ? t('catalog.products.media.uploading', 'Uploading\u2026') : t('catalog.products.media.choose', 'Choose files')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n className=\"hidden\"\n onChange={(event) => void acceptFiles(event.target.files)}\n />\n </div>\n {error ? <p className=\"text-xs font-medium text-status-error-text\">{error}</p> : null}\n {items.length ? (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => {\n const isDefault = defaultMediaId === item.id\n const slug = slugifyAttachmentFileName(item.fileName)\n const thumbnail =\n item.thumbnailUrl ||\n buildAttachmentImageUrl(item.id, { width: 360, height: 360, slug })\n return (\n <div key={item.id} className=\"flex flex-col rounded-md border bg-card\">\n <div className=\"relative aspect-square overflow-hidden rounded-t-md bg-muted\">\n {thumbnail ? (\n <img src={thumbnail} alt={item.fileName} className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full items-center justify-center text-muted-foreground\">\n <ImageIcon className=\"h-8 w-8\" />\n </div>\n )}\n <div className=\"absolute right-1 top-1 flex gap-1\">\n <Button\n type=\"button\"\n variant={isDefault ? 'default' : 'secondary'}\n size=\"icon\"\n className={cn('h-7 w-7', isDefault ? '' : 'opacity-80')}\n onClick={() => onDefaultChange(item.id)}\n >\n <Star className=\"h-3.5 w-3.5\" />\n </Button>\n <Button type=\"button\" variant=\"secondary\" size=\"icon\" className=\"h-7 w-7\" onClick={() => void handleRemove(item.id)}>\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </div>\n <div className=\"p-2\">\n <p className=\"line-clamp-1 text-sm font-medium\">{item.fileName}</p>\n <p className=\"text-xs text-muted-foreground\">{humanFileSize(item.fileSize)}</p>\n {isDefault ? (\n <p className=\"text-xs font-semibold text-primary\">\n {t('catalog.products.media.default', 'Default preview')}\n </p>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('catalog.products.media.empty', 'No media uploaded yet.')}\n </p>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Upload, Image as ImageIcon, Trash2, Star } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '@open-mercato/core/modules/attachments/lib/imageUrls'\n\nexport type ProductMediaItem = {\n id: string\n url: string\n fileName: string\n fileSize: number\n thumbnailUrl?: string | null\n}\n\ntype Props = {\n entityId: string\n draftRecordId: string\n items: ProductMediaItem[]\n defaultMediaId: string | null\n onItemsChange: (items: ProductMediaItem[]) => void\n onDefaultChange: (attachmentId: string | null) => void\n onUploadingChange?: (isUploading: boolean) => void\n}\n\nfunction humanFileSize(size: number): string {\n if (!Number.isFinite(size)) return `${size}`\n const units = ['B', 'KB', 'MB', 'GB']\n let idx = 0\n let value = size\n while (value >= 1024 && idx < units.length - 1) {\n value /= 1024\n idx++\n }\n return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`\n}\n\nexport function ProductMediaManager({\n entityId,\n draftRecordId,\n items,\n defaultMediaId,\n onItemsChange,\n onDefaultChange,\n onUploadingChange,\n}: Props) {\n const t = useT()\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [isUploading, setUploading] = React.useState(false)\n const [isDragOver, setDragOver] = React.useState(false)\n\n const acceptFiles = React.useCallback(\n async (files: FileList | null) => {\n if (!files || !files.length || !draftRecordId) return\n setError(null)\n setUploading(true)\n onUploadingChange?.(true)\n let nextItems = items\n let defaultId = defaultMediaId\n try {\n for (const file of Array.from(files)) {\n if (!file.type.startsWith('image/')) {\n setError(t('catalog.products.media.errors.imagesOnly', 'Only image files are supported.'))\n continue\n }\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', draftRecordId)\n fd.set('file', file)\n // optimistic-lock-exempt: media attach/detach/reorder\n const call = await apiCall<{ ok?: boolean; item?: ProductMediaItem; error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\n )\n if (!call.ok || !call.result?.item) {\n const message = call.result?.error ?? t('catalog.products.media.errors.uploadFailed', 'Upload failed.')\n setError(message)\n break\n }\n const existingIds = new Set(nextItems.map((entry) => entry.id))\n if (existingIds.has(call.result.item.id)) continue\n const slug = slugifyAttachmentFileName(call.result.item.fileName)\n const uploaded = {\n ...call.result.item,\n thumbnailUrl:\n call.result.item.thumbnailUrl ??\n buildAttachmentImageUrl(call.result.item.id, { width: 360, height: 360, slug }),\n }\n nextItems = [...nextItems, uploaded]\n if (!defaultId) {\n defaultId = uploaded.id\n }\n }\n if (nextItems !== items) {\n onItemsChange(nextItems)\n }\n if (!defaultMediaId && defaultId) {\n onDefaultChange(defaultId)\n }\n } finally {\n setUploading(false)\n onUploadingChange?.(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n },\n [defaultMediaId, draftRecordId, entityId, items, onDefaultChange, onItemsChange, onUploadingChange, t],\n )\n\n const handleRemove = React.useCallback(\n async (attachmentId: string) => {\n setError(null)\n // optimistic-lock-exempt: media attach/detach/reorder\n const call = await apiCall<{ ok?: boolean; error?: string }>(\n `/api/attachments?id=${encodeURIComponent(attachmentId)}`,\n { method: 'DELETE' },\n { fallback: null },\n )\n if (!call.ok) {\n setError(call.result?.error ?? t('catalog.products.media.errors.deleteFailed', 'Failed to delete media.'))\n return\n }\n const next = items.filter((item) => item.id !== attachmentId)\n onItemsChange(next)\n if (defaultMediaId === attachmentId) {\n onDefaultChange(next[0]?.id ?? null)\n }\n },\n [defaultMediaId, items, onDefaultChange, onItemsChange, t],\n )\n\n const onDrop = React.useCallback(\n (event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n void acceptFiles(event.dataTransfer?.files ?? null)\n },\n [acceptFiles],\n )\n\n const onDragOver = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(true)\n }, [])\n\n const onDragLeave = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n event.stopPropagation()\n setDragOver(false)\n }, [])\n\n const pickFiles = React.useCallback(() => {\n fileInputRef.current?.click()\n }, [])\n\n return (\n <div className=\"space-y-3\">\n <label className=\"text-sm font-medium\">{t('catalog.products.media.label', 'Media')}</label>\n <div\n className={cn(\n 'flex flex-col items-center justify-center rounded-lg border border-dashed p-6 text-center transition-colors',\n isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/30',\n )}\n onDrop={onDrop}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n role=\"presentation\"\n >\n <Upload className=\"mx-auto h-6 w-6 text-muted-foreground\" />\n <p className=\"mt-2 text-sm text-muted-foreground\">\n {t('catalog.products.media.dropHint', 'Drag and drop images here or click to upload.')}\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={pickFiles} disabled={isUploading}>\n {isUploading ? t('catalog.products.media.uploading', 'Uploading\u2026') : t('catalog.products.media.choose', 'Choose files')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n className=\"hidden\"\n onChange={(event) => void acceptFiles(event.target.files)}\n />\n </div>\n {error ? <p className=\"text-xs font-medium text-status-error-text\">{error}</p> : null}\n {items.length ? (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => {\n const isDefault = defaultMediaId === item.id\n const slug = slugifyAttachmentFileName(item.fileName)\n const thumbnail =\n item.thumbnailUrl ||\n buildAttachmentImageUrl(item.id, { width: 360, height: 360, slug })\n return (\n <div key={item.id} className=\"flex flex-col rounded-md border bg-card\">\n <div className=\"relative aspect-square overflow-hidden rounded-t-md bg-muted\">\n {thumbnail ? (\n <img src={thumbnail} alt={item.fileName} className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full items-center justify-center text-muted-foreground\">\n <ImageIcon className=\"h-8 w-8\" />\n </div>\n )}\n <div className=\"absolute right-1 top-1 flex gap-1\">\n <Button\n type=\"button\"\n variant={isDefault ? 'default' : 'secondary'}\n size=\"icon\"\n className={cn('h-7 w-7', isDefault ? '' : 'opacity-80')}\n onClick={() => onDefaultChange(item.id)}\n >\n <Star className=\"h-3.5 w-3.5\" />\n </Button>\n <Button type=\"button\" variant=\"secondary\" size=\"icon\" className=\"h-7 w-7\" onClick={() => void handleRemove(item.id)}>\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n </div>\n <div className=\"p-2\">\n <p className=\"line-clamp-1 text-sm font-medium\">{item.fileName}</p>\n <p className=\"text-xs text-muted-foreground\">{humanFileSize(item.fileSize)}</p>\n {isDefault ? (\n <p className=\"text-xs font-semibold text-primary\">\n {t('catalog.products.media.default', 'Default preview')}\n </p>\n ) : null}\n </div>\n </div>\n )\n })}\n </div>\n ) : (\n <p className=\"text-xs text-muted-foreground\">\n {t('catalog.products.media.empty', 'No media uploaded yet.')}\n </p>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -10,6 +10,7 @@ import { parseBooleanToken, parseBooleanWithDefault } from "@open-mercato/shared
|
|
|
10
10
|
import { parseCommaSeparatedList } from "@open-mercato/shared/lib/string";
|
|
11
11
|
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
12
12
|
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
13
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
13
14
|
import { assertEntityAclForRequest } from "../lib/entityAcl.js";
|
|
14
15
|
import { classifyRecordsEntity } from "../lib/entityClassification.js";
|
|
15
16
|
import { loadCustomFieldKinds } from "../lib/scoped-field-defs.js";
|
|
@@ -335,13 +336,23 @@ async function POST(req) {
|
|
|
335
336
|
const { id } = await de.createCustomEntityRecord({
|
|
336
337
|
entityId,
|
|
337
338
|
recordId: normalizedRecordId,
|
|
339
|
+
createOnly: true,
|
|
338
340
|
organizationId: targetOrgId,
|
|
339
341
|
tenantId: auth.tenantId,
|
|
340
342
|
values: norm
|
|
341
343
|
});
|
|
342
344
|
return NextResponse.json({ ok: true, item: { entityId, recordId: id } });
|
|
343
345
|
} catch (e) {
|
|
344
|
-
if (isCrudHttpError(e))
|
|
346
|
+
if (isCrudHttpError(e)) {
|
|
347
|
+
if (e.body.code === "custom_entity_record_exists") {
|
|
348
|
+
const { translate } = await resolveTranslations();
|
|
349
|
+
return NextResponse.json({
|
|
350
|
+
...e.body,
|
|
351
|
+
error: translate("entities.records.errors.alreadyExists", "A record with this ID already exists. Use PUT to update it.")
|
|
352
|
+
}, { status: e.status });
|
|
353
|
+
}
|
|
354
|
+
return NextResponse.json(e.body, { status: e.status });
|
|
355
|
+
}
|
|
345
356
|
logger.error("Records POST failed", { err: e });
|
|
346
357
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
347
358
|
}
|
|
@@ -541,7 +552,7 @@ const openApi = {
|
|
|
541
552
|
},
|
|
542
553
|
POST: {
|
|
543
554
|
summary: "Create record",
|
|
544
|
-
description: "Creates a record for the given entity. When
|
|
555
|
+
description: "Creates a record for the given entity. An existing recordId returns 409; use PUT to update a record. When recordId is omitted or not a UUID the data engine will generate one automatically.",
|
|
545
556
|
requestBody: {
|
|
546
557
|
contentType: "application/json",
|
|
547
558
|
schema: postBodySchema
|
|
@@ -552,6 +563,11 @@ const openApi = {
|
|
|
552
563
|
description: "Record created",
|
|
553
564
|
schema: mutationResponseSchema
|
|
554
565
|
},
|
|
566
|
+
{
|
|
567
|
+
status: 409,
|
|
568
|
+
description: "Record ID already exists in the current scope",
|
|
569
|
+
schema: errorSchema
|
|
570
|
+
},
|
|
555
571
|
{
|
|
556
572
|
status: 400,
|
|
557
573
|
description: "Validation failure",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/api/records.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { QueryEngine, QueryOptions, Where, Sort } from '@open-mercato/shared/lib/query/types'\nimport { normalizeExportFormat, serializeExport, defaultExportFilename, ensureColumns } from '@open-mercato/shared/lib/crud/exporters'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE } from '@open-mercato/shared/lib/data/engine'\nimport { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\nimport { setRecordCustomFields } from '../lib/helpers'\nimport { CustomFieldValue } from '../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { assertEntityAclForRequest } from '../lib/entityAcl'\nimport { classifyRecordsEntity } from '../lib/entityClassification'\nimport { loadCustomFieldKinds } from '../lib/scoped-field-defs'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\n// Kept exported from this path: the classifier used to be private to this file\n// and the workflows task-visibility resolver reuses it, so both callers share\n// one precedence chain instead of growing a second, drifting copy.\nexport {\n classifyRecordsEntity,\n isDeclaredCustomEntity,\n loadDeclaredCustomEntities,\n findScopedCustomEntity,\n} from '../lib/entityClassification'\nexport type {\n RecordsEntityScope,\n RecordsEntityKind,\n RecordsEntityClassification,\n} from '../lib/entityClassification'\n\nconst logger = createLogger('entities').child({ component: 'records' })\n\nconst CUSTOM_ENTITY_RECORD_RESOURCE_KIND = 'entities.record'\n\nfunction systemEntityRecordsRejection(entityId: string) {\n return NextResponse.json(\n { error: 'Records are available for custom entities only', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId },\n { status: 400 },\n )\n}\n\nasync function readCustomEntityRecordUpdatedAt(\n em: any,\n input: { entityType: string; entityId: string; organizationId: string | null },\n): Promise<string | null> {\n try {\n const db = em.getKysely()\n let query = db\n .selectFrom('custom_entities_storage' as any)\n .select(['updated_at' as any])\n .where('entity_type' as any, '=', input.entityType)\n .where('entity_id' as any, '=', input.entityId)\n query = input.organizationId === null\n ? query.where('organization_id' as any, 'is', null as any)\n : query.where('organization_id' as any, '=', input.organizationId)\n const row = await query.executeTakeFirst()\n const value = (row as any)?.updated_at\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string' && value.length > 0) return value\n return null\n } catch {\n return null\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.records.view'] },\n POST: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n}\n\nconst DEFAULT_EXPORT_PAGE_SIZE = 1000\nconst EXPORT_MAX_PAGES = 1000\n\nconst listRecordsQuerySchema = z\n .object({\n entityId: z.string().min(1),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n search: z.string().optional(),\n searchFields: z.string().optional(),\n withDeleted: z.coerce.boolean().optional(),\n format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),\n exportScope: z.enum(['full']).optional(),\n export_scope: z.enum(['full']).optional(),\n all: z.coerce.boolean().optional(),\n full: z.coerce.boolean().optional(),\n })\n .passthrough()\n\nconst recordItemSchema = z.record(z.string(), z.any())\n\nconst listRecordsResponseSchema = z.object({\n items: z.array(recordItemSchema),\n total: z.number(),\n page: z.number(),\n pageSize: z.number(),\n totalPages: z.number(),\n totalIsCapped: z.boolean().optional(),\n})\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const requestedExport = normalizeExportFormat(url.searchParams.get('format'))\n const exportScopeRaw = (url.searchParams.get('exportScope') || url.searchParams.get('export_scope') || '').toLowerCase()\n const exportFullRequested = requestedExport != null && (exportScopeRaw === 'full' || parseBooleanWithDefault(url.searchParams.get('full'), false))\n const exportAll = parseBooleanWithDefault(url.searchParams.get('all'), false)\n const noPagination = exportAll || requestedExport != null\n const page = noPagination ? 1 : Math.max(parseInt(url.searchParams.get('page') || '1', 10) || 1, 1)\n const basePageSize = Math.min(Math.max(parseInt(url.searchParams.get('pageSize') || '50', 10) || 50, 1), 100)\n const pageSize = noPagination ? Math.max(basePageSize, DEFAULT_EXPORT_PAGE_SIZE) : basePageSize\n const sortField = url.searchParams.get('sortField') || 'id'\n const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'\n const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)\n const searchTerm = (url.searchParams.get('search') || '').trim()\n const searchFields = parseCommaSeparatedList(url.searchParams.get('searchFields'))\n\n const qpEntries: Array<[string, string]> = []\n for (const [key, val] of url.searchParams.entries()) {\n if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue\n qpEntries.push([key, val])\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const qe = resolve('queryEngine') as QueryEngine\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n let organizationIds: string[] | null = scope.filterIds\n // Module-declared custom entities (ce.ts) carry frozen system-style ids and are never\n // registered in `custom_entities`, so classification checks the declared registry plus\n // active registrations. System (table-backed) ids are rejected above; for the allowed\n // set `isCustomEntity` drives mapRow's cf_ stripping so the edit form reads back values.\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'view', isCustomEntity, isRestricted, rbac })\n if (organizationIds && organizationIds.length === 0) {\n return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n // Doc storage keeps custom-field values as JSON, so a checkbox can arrive as the\n // string \"true\"/\"1\" and must be handed back to the form as a real boolean. That\n // parse is only ever correct for fields DECLARED boolean: applied blind it also\n // rewrites legitimate string values whose text happens to be a boolean token \u2014\n // a `select` option `no` came back as `false`, and the edit form then wrote that\n // boolean back over the stored option (#5791). Consult the declared kind instead.\n const needsFieldKinds = isCustomEntity || qpEntries.some(([key]) => key.startsWith('cf_'))\n const fieldKinds = needsFieldKinds\n ? await loadCustomFieldKinds(em, { entityId, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null, tenantId: auth.tenantId ?? null })\n : new Map<string, string>()\n const isBooleanField = (key: string) => fieldKinds.get(key) === 'boolean'\n const parseBooleanField = (key: string, value: unknown) => {\n if (typeof value !== 'string' || !isBooleanField(key)) return value\n const parsed = parseBooleanToken(value)\n return parsed === null ? value : parsed\n }\n const normalizeCustomEntityValue = (key: string, value: unknown) => {\n if (Array.isArray(value)) return value.map((entry) => parseBooleanField(key, entry))\n return parseBooleanField(key, value)\n }\n const mapRow = (row: any) => {\n if (!isCustomEntity || !row || typeof row !== 'object') return row\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(row)) {\n if (k.startsWith('cf_')) {\n const bare = k.replace(/^cf_/, '')\n out[bare] = normalizeCustomEntityValue(bare, v)\n } else out[k] = v\n }\n return out\n }\n const mapFullRow = (row: any) => {\n if (!row || typeof row !== 'object') return row\n return { ...(row as Record<string, unknown>) }\n }\n // Build filters with awareness of custom-entity mode\n const filtersObj: Where<any> = {}\n const buildFilter = (key: string, val: string, allowAnyKey: boolean) => {\n if (key.startsWith('cf_')) {\n if (key.endsWith('In')) {\n const base = key.slice(0, -2)\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[base] = { $in: values }\n } else {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n ;(filtersObj as any)[key] = parseBooleanField(key.replace(/^cf_/, ''), val)\n }\n }\n } else if (allowAnyKey) {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n ;(filtersObj as any)[key] = parseBooleanField(key, val)\n }\n } else {\n if (['id', 'created_at', 'updated_at', 'deleted_at', 'name', 'title', 'email'].includes(key)) {\n ;(filtersObj as any)[key] = val\n }\n }\n }\n\n if (organizationIds && organizationIds.length) {\n (filtersObj as any).organization_id = { $in: organizationIds }\n }\n const qopts: QueryOptions = {\n tenantId: auth.tenantId!,\n includeCustomFields: true,\n page: { page, pageSize },\n sort: [{ field: sortField as any, dir: sortDir as any }] as Sort[],\n filters: filtersObj as any,\n withDeleted,\n }\n if (organizationIds && organizationIds.length) {\n qopts.organizationIds = organizationIds\n }\n // Allowed entities are doc-storage-backed by definition (system ids were rejected\n // above) \u2014 direct the engine to doc storage explicitly so reads stay deterministic\n // even before the first record exists.\n if (isCustomEntity) qopts.forceCustomEntityStorage = true\n for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)\n // Server-side full-result search: match the term against the requested fields\n // (defaults to `id`) before pagination so totals/exports stay consistent with\n // the active search instead of filtering only the current client page (#3229).\n if (searchTerm) {\n const fields = searchFields.length ? searchFields : ['id']\n const pattern = `%${searchTerm}%`\n const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))\n ;(filtersObj as any).$or = orClauses\n }\n const res = await qe.query(entityId as any, qopts)\n const rawItems = res.items || []\n const viewPageItems = rawItems.map(mapRow)\n const fullPageItems = rawItems.map(mapFullRow)\n\n // Expose `updated_at` on custom-entity records. The query engine returns only\n // the `doc` fields + `id`, dropping the base `updated_at` column \u2014 which made\n // optimistic locking impossible end-to-end (no version for the edit page to\n // round-trip as the lock header). Batch-read it from storage and merge it in.\n if (isCustomEntity && viewPageItems.length) {\n try {\n const recordIds = viewPageItems\n .map((it: any) => it?.id)\n .filter((v: any): v is string => typeof v === 'string' && v.length > 0)\n if (recordIds.length) {\n const db = em.getKysely()\n const rows = await db\n .selectFrom('custom_entities_storage' as any)\n .select(['entity_id' as any, 'updated_at' as any])\n .where('entity_type' as any, '=', entityId)\n .where('entity_id' as any, 'in', recordIds as any)\n .execute()\n const updatedById = new Map<string, string>()\n for (const row of rows as any[]) {\n const value = row?.updated_at\n const iso = value instanceof Date ? value.toISOString() : (typeof value === 'string' && value.length > 0 ? value : null)\n if (iso && row?.entity_id) updatedById.set(String(row.entity_id), iso)\n }\n for (const item of viewPageItems as any[]) {\n const iso = updatedById.get(String(item?.id))\n if (iso) {\n item.updated_at = iso\n item.updatedAt = iso\n }\n }\n }\n } catch { /* best-effort: locking simply will not engage if storage is unavailable */ }\n }\n\n const total = typeof res.total === 'number' ? res.total : rawItems.length\n const effectivePageSize = res.pageSize || pageSize\n const payload = {\n items: viewPageItems,\n total,\n page: res.page || page,\n pageSize: effectivePageSize,\n totalPages: Math.ceil(total / (effectivePageSize || 1)),\n ...(res.meta?.listCountCapWarning ? { totalIsCapped: true } : {}),\n }\n\n if (requestedExport) {\n const exportItems: any[] = exportFullRequested ? [...fullPageItems] : [...viewPageItems]\n // Short-page termination: `total` is a display value, not a loop bound \u2014 it can\n // under-report (capped counts) or drift while rows are inserted/deleted mid-export.\n // Keep fetching while pages come back full; fail closed at the page ceiling rather\n // than serializing a partial export.\n if (rawItems.length >= pageSize) {\n let nextPage = 2\n for (;;) {\n const nextRes = await qe.query(entityId as any, {\n ...qopts,\n page: { page: nextPage, pageSize },\n })\n const nextRawItems = nextRes.items || []\n if (!nextRawItems.length) break\n const nextViewItems = nextRawItems.map(mapRow)\n const nextFullItems = nextRawItems.map(mapFullRow)\n const nextBatch = exportFullRequested ? nextFullItems : nextViewItems\n exportItems.push(...nextBatch)\n if (nextRawItems.length < pageSize) break\n if (nextPage >= EXPORT_MAX_PAGES) {\n throw new Error(`[internal] export exceeded ${EXPORT_MAX_PAGES} pages; refusing to return a partial export`)\n }\n nextPage += 1\n }\n }\n const prepared = {\n columns: ensureColumns(exportItems),\n rows: exportItems,\n }\n const filenameBase = exportFullRequested ? `${entityId || 'records'}_full` : entityId || 'records'\n const serialized = serializeExport(prepared, requestedExport)\n const filename = defaultExportFilename(filenameBase, requestedExport)\n return new Response(serialized.body, {\n headers: {\n 'content-type': serialized.contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n },\n })\n }\n\n return NextResponse.json(payload)\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records GET failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst postBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1).optional(),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst putBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst mutationResponseSchema = z.object({\n ok: z.literal(true),\n item: z\n .object({\n entityId: z.string(),\n recordId: z.string(),\n })\n .optional(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: unknown\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = postBodySchema.safeParse(json)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId } = parsed.data\n let { recordId, values } = parsed.data as { recordId?: string; values: Record<string, any> }\n\n try {\n const { resolve } = await createRequestContainer()\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n const normalizedRecordId = (() => {\n const raw = String(recordId || '').trim()\n if (!raw) return undefined\n const low = raw.toLowerCase()\n if (low === 'create' || low === 'new' || low === 'null' || low === 'undefined') return undefined\n // Enforce UUID only; any non-uuid is ignored so we generate one in the DE\n const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n return uuid.test(raw) ? raw : undefined\n })()\n const { id } = await de.createCustomEntityRecord({\n entityId,\n recordId: normalizedRecordId,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n\n return NextResponse.json({ ok: true, item: { entityId, recordId: id } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records POST failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\n// Avoid zod here to prevent runtime import issues in some environments\nfunction parsePutBody(json: any): { ok: true; data: { entityId: string; recordId: string; values: Record<string, any> } } | { ok: false; error: string } {\n if (!json || typeof json !== 'object') return { ok: false, error: 'Invalid JSON' }\n const entityId = typeof json.entityId === 'string' && json.entityId.length ? json.entityId : ''\n const recordId = typeof json.recordId === 'string' && json.recordId.length ? json.recordId : ''\n const values = (json.values && typeof json.values === 'object') ? json.values as Record<string, any> : {}\n if (!entityId || !recordId) return { ok: false, error: 'entityId and recordId are required' }\n return { ok: true, data: { entityId, recordId, values } }\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: any\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = parsePutBody(json)\n if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 })\n const { entityId, recordId, values } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n // Normalize recordId: if blank/sentinel/non-uuid => create instead of update\n const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n const rid = String(recordId || '').trim()\n const low = rid.toLowerCase()\n const isSentinel = !rid || low === 'create' || low === 'new' || low === 'null' || low === 'undefined'\n const isUuid = uuidRe.test(rid)\n if (isSentinel || !isUuid) {\n const created = await de.createCustomEntityRecord({\n entityId,\n recordId: undefined,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: created.id } })\n }\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: rid,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: rid,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.updateCustomEntityRecord({\n entityId,\n recordId: rid,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: rid } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst deleteBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n})\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const qpEntityId = url.searchParams.get('entityId')\n const qpRecordId = url.searchParams.get('recordId')\n let payload: any = qpEntityId && qpRecordId ? { entityId: qpEntityId, recordId: qpRecordId } : null\n if (!payload) {\n try { payload = await req.json() } catch { payload = null }\n }\n const parsed = deleteBodySchema.safeParse(payload)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, recordId } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: recordId,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: recordId,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })\n return NextResponse.json({ ok: true })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nfunction normalizeValues(input: Record<string, any>): Record<string, any> {\n const out: Record<string, any> = {}\n for (const [k, v] of Object.entries(input || {})) {\n const key = k.startsWith('cf_') ? k.replace(/^cf_/, '') : k\n out[key] = v\n }\n return out\n}\n\nconst deleteResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'CRUD operations on entity records',\n methods: {\n GET: {\n summary: 'List records',\n description:\n 'Returns paginated records for the supplied entity. Supports custom field filters, exports, and soft-delete toggles.',\n query: listRecordsQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated records',\n schema: listRecordsResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n POST: {\n summary: 'Create record',\n description:\n 'Creates a record for the given entity. When `recordId` is omitted or not a UUID the data engine will generate one automatically.',\n requestBody: {\n contentType: 'application/json',\n schema: postBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record created',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n PUT: {\n summary: 'Update record',\n description:\n 'Updates an existing record. If the provided recordId is not a UUID the record will be created instead to support optimistic flows.',\n requestBody: {\n contentType: 'application/json',\n schema: putBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record updated',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n DELETE: {\n summary: 'Delete record',\n description: 'Soft deletes the specified record within the current tenant/org scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record deleted',\n schema: deleteResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id or record id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 404,\n description: 'Record not found',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport type { QueryEngine, QueryOptions, Where, Sort } from '@open-mercato/shared/lib/query/types'\nimport { normalizeExportFormat, serializeExport, defaultExportFilename, ensureColumns } from '@open-mercato/shared/lib/crud/exporters'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE } from '@open-mercato/shared/lib/data/engine'\nimport { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\nimport { setRecordCustomFields } from '../lib/helpers'\nimport { CustomFieldValue } from '../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { assertEntityAclForRequest } from '../lib/entityAcl'\nimport { classifyRecordsEntity } from '../lib/entityClassification'\nimport { loadCustomFieldKinds } from '../lib/scoped-field-defs'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\n// Kept exported from this path: the classifier used to be private to this file\n// and the workflows task-visibility resolver reuses it, so both callers share\n// one precedence chain instead of growing a second, drifting copy.\nexport {\n classifyRecordsEntity,\n isDeclaredCustomEntity,\n loadDeclaredCustomEntities,\n findScopedCustomEntity,\n} from '../lib/entityClassification'\nexport type {\n RecordsEntityScope,\n RecordsEntityKind,\n RecordsEntityClassification,\n} from '../lib/entityClassification'\n\nconst logger = createLogger('entities').child({ component: 'records' })\n\nconst CUSTOM_ENTITY_RECORD_RESOURCE_KIND = 'entities.record'\n\nfunction systemEntityRecordsRejection(entityId: string) {\n return NextResponse.json(\n { error: 'Records are available for custom entities only', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId },\n { status: 400 },\n )\n}\n\nasync function readCustomEntityRecordUpdatedAt(\n em: any,\n input: { entityType: string; entityId: string; organizationId: string | null },\n): Promise<string | null> {\n try {\n const db = em.getKysely()\n let query = db\n .selectFrom('custom_entities_storage' as any)\n .select(['updated_at' as any])\n .where('entity_type' as any, '=', input.entityType)\n .where('entity_id' as any, '=', input.entityId)\n query = input.organizationId === null\n ? query.where('organization_id' as any, 'is', null as any)\n : query.where('organization_id' as any, '=', input.organizationId)\n const row = await query.executeTakeFirst()\n const value = (row as any)?.updated_at\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string' && value.length > 0) return value\n return null\n } catch {\n return null\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.records.view'] },\n POST: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.records.manage'] },\n}\n\nconst DEFAULT_EXPORT_PAGE_SIZE = 1000\nconst EXPORT_MAX_PAGES = 1000\n\nconst listRecordsQuerySchema = z\n .object({\n entityId: z.string().min(1),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n search: z.string().optional(),\n searchFields: z.string().optional(),\n withDeleted: z.coerce.boolean().optional(),\n format: z.enum(['csv', 'json', 'xml', 'markdown']).optional(),\n exportScope: z.enum(['full']).optional(),\n export_scope: z.enum(['full']).optional(),\n all: z.coerce.boolean().optional(),\n full: z.coerce.boolean().optional(),\n })\n .passthrough()\n\nconst recordItemSchema = z.record(z.string(), z.any())\n\nconst listRecordsResponseSchema = z.object({\n items: z.array(recordItemSchema),\n total: z.number(),\n page: z.number(),\n pageSize: z.number(),\n totalPages: z.number(),\n totalIsCapped: z.boolean().optional(),\n})\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const entityId = url.searchParams.get('entityId') || ''\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const requestedExport = normalizeExportFormat(url.searchParams.get('format'))\n const exportScopeRaw = (url.searchParams.get('exportScope') || url.searchParams.get('export_scope') || '').toLowerCase()\n const exportFullRequested = requestedExport != null && (exportScopeRaw === 'full' || parseBooleanWithDefault(url.searchParams.get('full'), false))\n const exportAll = parseBooleanWithDefault(url.searchParams.get('all'), false)\n const noPagination = exportAll || requestedExport != null\n const page = noPagination ? 1 : Math.max(parseInt(url.searchParams.get('page') || '1', 10) || 1, 1)\n const basePageSize = Math.min(Math.max(parseInt(url.searchParams.get('pageSize') || '50', 10) || 50, 1), 100)\n const pageSize = noPagination ? Math.max(basePageSize, DEFAULT_EXPORT_PAGE_SIZE) : basePageSize\n const sortField = url.searchParams.get('sortField') || 'id'\n const sortDir = (url.searchParams.get('sortDir') || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'\n const withDeleted = parseBooleanWithDefault(url.searchParams.get('withDeleted'), false)\n const searchTerm = (url.searchParams.get('search') || '').trim()\n const searchFields = parseCommaSeparatedList(url.searchParams.get('searchFields'))\n\n const qpEntries: Array<[string, string]> = []\n for (const [key, val] of url.searchParams.entries()) {\n if (['entityId','page','pageSize','sortField','sortDir','withDeleted','format','exportScope','export_scope','all','full','search','searchFields'].includes(key)) continue\n qpEntries.push([key, val])\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const qe = resolve('queryEngine') as QueryEngine\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n let organizationIds: string[] | null = scope.filterIds\n // Module-declared custom entities (ce.ts) carry frozen system-style ids and are never\n // registered in `custom_entities`, so classification checks the declared registry plus\n // active registrations. System (table-backed) ids are rejected above; for the allowed\n // set `isCustomEntity` drives mapRow's cf_ stripping so the edit form reads back values.\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'view', isCustomEntity, isRestricted, rbac })\n if (organizationIds && organizationIds.length === 0) {\n return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n // Doc storage keeps custom-field values as JSON, so a checkbox can arrive as the\n // string \"true\"/\"1\" and must be handed back to the form as a real boolean. That\n // parse is only ever correct for fields DECLARED boolean: applied blind it also\n // rewrites legitimate string values whose text happens to be a boolean token \u2014\n // a `select` option `no` came back as `false`, and the edit form then wrote that\n // boolean back over the stored option (#5791). Consult the declared kind instead.\n const needsFieldKinds = isCustomEntity || qpEntries.some(([key]) => key.startsWith('cf_'))\n const fieldKinds = needsFieldKinds\n ? await loadCustomFieldKinds(em, { entityId, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null, tenantId: auth.tenantId ?? null })\n : new Map<string, string>()\n const isBooleanField = (key: string) => fieldKinds.get(key) === 'boolean'\n const parseBooleanField = (key: string, value: unknown) => {\n if (typeof value !== 'string' || !isBooleanField(key)) return value\n const parsed = parseBooleanToken(value)\n return parsed === null ? value : parsed\n }\n const normalizeCustomEntityValue = (key: string, value: unknown) => {\n if (Array.isArray(value)) return value.map((entry) => parseBooleanField(key, entry))\n return parseBooleanField(key, value)\n }\n const mapRow = (row: any) => {\n if (!isCustomEntity || !row || typeof row !== 'object') return row\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(row)) {\n if (k.startsWith('cf_')) {\n const bare = k.replace(/^cf_/, '')\n out[bare] = normalizeCustomEntityValue(bare, v)\n } else out[k] = v\n }\n return out\n }\n const mapFullRow = (row: any) => {\n if (!row || typeof row !== 'object') return row\n return { ...(row as Record<string, unknown>) }\n }\n // Build filters with awareness of custom-entity mode\n const filtersObj: Where<any> = {}\n const buildFilter = (key: string, val: string, allowAnyKey: boolean) => {\n if (key.startsWith('cf_')) {\n if (key.endsWith('In')) {\n const base = key.slice(0, -2)\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[base] = { $in: values }\n } else {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n ;(filtersObj as any)[key] = parseBooleanField(key.replace(/^cf_/, ''), val)\n }\n }\n } else if (allowAnyKey) {\n if (val.includes(',')) {\n const values = parseCommaSeparatedList(val)\n ;(filtersObj as any)[key] = { $in: values }\n } else {\n ;(filtersObj as any)[key] = parseBooleanField(key, val)\n }\n } else {\n if (['id', 'created_at', 'updated_at', 'deleted_at', 'name', 'title', 'email'].includes(key)) {\n ;(filtersObj as any)[key] = val\n }\n }\n }\n\n if (organizationIds && organizationIds.length) {\n (filtersObj as any).organization_id = { $in: organizationIds }\n }\n const qopts: QueryOptions = {\n tenantId: auth.tenantId!,\n includeCustomFields: true,\n page: { page, pageSize },\n sort: [{ field: sortField as any, dir: sortDir as any }] as Sort[],\n filters: filtersObj as any,\n withDeleted,\n }\n if (organizationIds && organizationIds.length) {\n qopts.organizationIds = organizationIds\n }\n // Allowed entities are doc-storage-backed by definition (system ids were rejected\n // above) \u2014 direct the engine to doc storage explicitly so reads stay deterministic\n // even before the first record exists.\n if (isCustomEntity) qopts.forceCustomEntityStorage = true\n for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)\n // Server-side full-result search: match the term against the requested fields\n // (defaults to `id`) before pagination so totals/exports stay consistent with\n // the active search instead of filtering only the current client page (#3229).\n if (searchTerm) {\n const fields = searchFields.length ? searchFields : ['id']\n const pattern = `%${searchTerm}%`\n const orClauses = fields.map((field) => ({ [field]: { $ilike: pattern } }))\n ;(filtersObj as any).$or = orClauses\n }\n const res = await qe.query(entityId as any, qopts)\n const rawItems = res.items || []\n const viewPageItems = rawItems.map(mapRow)\n const fullPageItems = rawItems.map(mapFullRow)\n\n // Expose `updated_at` on custom-entity records. The query engine returns only\n // the `doc` fields + `id`, dropping the base `updated_at` column \u2014 which made\n // optimistic locking impossible end-to-end (no version for the edit page to\n // round-trip as the lock header). Batch-read it from storage and merge it in.\n if (isCustomEntity && viewPageItems.length) {\n try {\n const recordIds = viewPageItems\n .map((it: any) => it?.id)\n .filter((v: any): v is string => typeof v === 'string' && v.length > 0)\n if (recordIds.length) {\n const db = em.getKysely()\n const rows = await db\n .selectFrom('custom_entities_storage' as any)\n .select(['entity_id' as any, 'updated_at' as any])\n .where('entity_type' as any, '=', entityId)\n .where('entity_id' as any, 'in', recordIds as any)\n .execute()\n const updatedById = new Map<string, string>()\n for (const row of rows as any[]) {\n const value = row?.updated_at\n const iso = value instanceof Date ? value.toISOString() : (typeof value === 'string' && value.length > 0 ? value : null)\n if (iso && row?.entity_id) updatedById.set(String(row.entity_id), iso)\n }\n for (const item of viewPageItems as any[]) {\n const iso = updatedById.get(String(item?.id))\n if (iso) {\n item.updated_at = iso\n item.updatedAt = iso\n }\n }\n }\n } catch { /* best-effort: locking simply will not engage if storage is unavailable */ }\n }\n\n const total = typeof res.total === 'number' ? res.total : rawItems.length\n const effectivePageSize = res.pageSize || pageSize\n const payload = {\n items: viewPageItems,\n total,\n page: res.page || page,\n pageSize: effectivePageSize,\n totalPages: Math.ceil(total / (effectivePageSize || 1)),\n ...(res.meta?.listCountCapWarning ? { totalIsCapped: true } : {}),\n }\n\n if (requestedExport) {\n const exportItems: any[] = exportFullRequested ? [...fullPageItems] : [...viewPageItems]\n // Short-page termination: `total` is a display value, not a loop bound \u2014 it can\n // under-report (capped counts) or drift while rows are inserted/deleted mid-export.\n // Keep fetching while pages come back full; fail closed at the page ceiling rather\n // than serializing a partial export.\n if (rawItems.length >= pageSize) {\n let nextPage = 2\n for (;;) {\n const nextRes = await qe.query(entityId as any, {\n ...qopts,\n page: { page: nextPage, pageSize },\n })\n const nextRawItems = nextRes.items || []\n if (!nextRawItems.length) break\n const nextViewItems = nextRawItems.map(mapRow)\n const nextFullItems = nextRawItems.map(mapFullRow)\n const nextBatch = exportFullRequested ? nextFullItems : nextViewItems\n exportItems.push(...nextBatch)\n if (nextRawItems.length < pageSize) break\n if (nextPage >= EXPORT_MAX_PAGES) {\n throw new Error(`[internal] export exceeded ${EXPORT_MAX_PAGES} pages; refusing to return a partial export`)\n }\n nextPage += 1\n }\n }\n const prepared = {\n columns: ensureColumns(exportItems),\n rows: exportItems,\n }\n const filenameBase = exportFullRequested ? `${entityId || 'records'}_full` : entityId || 'records'\n const serialized = serializeExport(prepared, requestedExport)\n const filename = defaultExportFilename(filenameBase, requestedExport)\n return new Response(serialized.body, {\n headers: {\n 'content-type': serialized.contentType,\n 'content-disposition': `attachment; filename=\"${filename}\"`,\n },\n })\n }\n\n return NextResponse.json(payload)\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n logger.error('Records GET failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst postBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1).optional(),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst putBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n values: z.record(z.string(), z.any()).default({}),\n})\n\nconst mutationResponseSchema = z.object({\n ok: z.literal(true),\n item: z\n .object({\n entityId: z.string(),\n recordId: z.string(),\n })\n .optional(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: unknown\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = postBodySchema.safeParse(json)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId } = parsed.data\n let { recordId, values } = parsed.data as { recordId?: string; values: Record<string, any> }\n\n try {\n const { resolve } = await createRequestContainer()\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n const normalizedRecordId = (() => {\n const raw = String(recordId || '').trim()\n if (!raw) return undefined\n const low = raw.toLowerCase()\n if (low === 'create' || low === 'new' || low === 'null' || low === 'undefined') return undefined\n // Enforce UUID only; any non-uuid is ignored so we generate one in the DE\n const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n return uuid.test(raw) ? raw : undefined\n })()\n const { id } = await de.createCustomEntityRecord({\n entityId,\n recordId: normalizedRecordId,\n createOnly: true,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n\n return NextResponse.json({ ok: true, item: { entityId, recordId: id } })\n } catch (e) {\n if (isCrudHttpError(e)) {\n if (e.body.code === 'custom_entity_record_exists') {\n const { translate } = await resolveTranslations()\n return NextResponse.json({\n ...e.body,\n error: translate('entities.records.errors.alreadyExists', 'A record with this ID already exists. Use PUT to update it.'),\n }, { status: e.status })\n }\n return NextResponse.json(e.body, { status: e.status })\n }\n logger.error('Records POST failed', { err: e })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\n// Avoid zod here to prevent runtime import issues in some environments\nfunction parsePutBody(json: any): { ok: true; data: { entityId: string; recordId: string; values: Record<string, any> } } | { ok: false; error: string } {\n if (!json || typeof json !== 'object') return { ok: false, error: 'Invalid JSON' }\n const entityId = typeof json.entityId === 'string' && json.entityId.length ? json.entityId : ''\n const recordId = typeof json.recordId === 'string' && json.recordId.length ? json.recordId : ''\n const values = (json.values && typeof json.values === 'object') ? json.values as Record<string, any> : {}\n if (!entityId || !recordId) return { ok: false, error: 'entityId and recordId are required' }\n return { ok: true, data: { entityId, recordId, values } }\n}\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let json: any\n try { json = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = parsePutBody(json)\n if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 })\n const { entityId, recordId, values } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n // Strip reserved record/system columns the edit form echoes back from the loaded record\n // (`id`, plus `updated_at`/`updatedAt` used for optimistic locking). They are not custom\n // fields; without this they validate as cf_id / cf_updated_at / cf_updatedAt and are\n // rejected as \"Unknown custom field\", which fails EVERY custom-entity edit-form save.\n for (const reservedKey of ['id', 'created_at', 'createdAt', 'updated_at', 'updatedAt', 'deleted_at', 'deletedAt']) {\n delete (values as any)[reservedKey]\n }\n const norm = normalizeValues(values)\n\n // Validate against custom field definitions\n try {\n const { validateCustomFieldValuesServer } = await import('../lib/validation')\n const check = await validateCustomFieldValuesServer(em, { entityId, organizationId: targetOrgId, tenantId: auth.tenantId!, values: norm, rejectUndeclaredKeys: true })\n if (!check.ok) return NextResponse.json({ error: 'Validation failed', fields: check.fieldErrors }, { status: 400 })\n } catch { /* ignore if helper missing */ }\n\n // Normalize recordId: if blank/sentinel/non-uuid => create instead of update\n const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\n const rid = String(recordId || '').trim()\n const low = rid.toLowerCase()\n const isSentinel = !rid || low === 'create' || low === 'new' || low === 'null' || low === 'undefined'\n const isUuid = uuidRe.test(rid)\n if (isSentinel || !isUuid) {\n const created = await de.createCustomEntityRecord({\n entityId,\n recordId: undefined,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: created.id } })\n }\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: rid,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: rid,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.updateCustomEntityRecord({\n entityId,\n recordId: rid,\n organizationId: targetOrgId,\n tenantId: auth.tenantId!,\n values: norm,\n })\n return NextResponse.json({ ok: true, item: { entityId, recordId: rid } })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nconst deleteBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n})\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const url = new URL(req.url)\n const qpEntityId = url.searchParams.get('entityId')\n const qpRecordId = url.searchParams.get('recordId')\n let payload: any = qpEntityId && qpRecordId ? { entityId: qpEntityId, recordId: qpRecordId } : null\n if (!payload) {\n try { payload = await req.json() } catch { payload = null }\n }\n const parsed = deleteBodySchema.safeParse(payload)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, recordId } = parsed.data\n\n try {\n const container = await createRequestContainer()\n const { resolve } = container\n const de = resolve('dataEngine') as any\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as RbacService\n const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })\n const targetOrgId = resolveSingleOrganizationIdOrDeny(scope, auth)\n if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })\n const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null })\n if (entityKind === 'system') return systemEntityRecordsRejection(entityId)\n const isCustomEntity = entityKind === 'custom'\n await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, isRestricted, rbac })\n\n try {\n const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {\n entityType: entityId,\n entityId: recordId,\n organizationId: targetOrgId,\n })\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_RECORD_RESOURCE_KIND,\n resourceId: recordId,\n current: currentUpdatedAt,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n\n await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })\n return NextResponse.json({ ok: true })\n } catch (e) {\n if (isCrudHttpError(e)) return NextResponse.json(e.body, { status: e.status })\n return NextResponse.json({ error: 'Internal server error' }, { status: 500 })\n }\n}\n\nfunction normalizeValues(input: Record<string, any>): Record<string, any> {\n const out: Record<string, any> = {}\n for (const [k, v] of Object.entries(input || {})) {\n const key = k.startsWith('cf_') ? k.replace(/^cf_/, '') : k\n out[key] = v\n }\n return out\n}\n\nconst deleteResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n}).passthrough()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'CRUD operations on entity records',\n methods: {\n GET: {\n summary: 'List records',\n description:\n 'Returns paginated records for the supplied entity. Supports custom field filters, exports, and soft-delete toggles.',\n query: listRecordsQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated records',\n schema: listRecordsResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n POST: {\n summary: 'Create record',\n description:\n 'Creates a record for the given entity. An existing recordId returns 409; use PUT to update a record. When recordId is omitted or not a UUID the data engine will generate one automatically.',\n requestBody: {\n contentType: 'application/json',\n schema: postBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record created',\n schema: mutationResponseSchema,\n },\n {\n status: 409,\n description: 'Record ID already exists in the current scope',\n schema: errorSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n PUT: {\n summary: 'Update record',\n description:\n 'Updates an existing record. If the provided recordId is not a UUID the record will be created instead to support optimistic flows.',\n requestBody: {\n contentType: 'application/json',\n schema: putBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record updated',\n schema: mutationResponseSchema,\n },\n {\n status: 400,\n description: 'Validation failure',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n DELETE: {\n summary: 'Delete record',\n description: 'Soft deletes the specified record within the current tenant/org scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteBodySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Record deleted',\n schema: deleteResponseSchema,\n },\n {\n status: 400,\n description: 'Missing entity id or record id',\n schema: errorSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: errorSchema,\n },\n {\n status: 404,\n description: 'Record not found',\n schema: errorSchema,\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["classifyRecordsEntity"]
|
|
7
7
|
}
|
|
@@ -3,7 +3,11 @@ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
|
|
|
3
3
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
4
4
|
import { resolveCrudRecordId, parseScopedCommandInput } from "@open-mercato/shared/lib/api/scoped";
|
|
5
5
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
6
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
6
7
|
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
|
|
8
|
+
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
|
|
9
|
+
import { isIdsParamProvided, mergeIdFilter, parseIdsParam } from "@open-mercato/shared/lib/crud/ids";
|
|
10
|
+
import { mergeAdvancedFiltersFromQuery } from "@open-mercato/shared/lib/crud/advanced-filter-integration";
|
|
7
11
|
import { sanitizeSearchTerm } from "../../helpers.js";
|
|
8
12
|
import { StaffTimeEntry, StaffTimeEntryTag, StaffTimeProject } from "../../../data/entities.js";
|
|
9
13
|
import { staffTimeEntryCreateSchema, staffTimeEntryUpdateSchema } from "../../../data/validators.js";
|
|
@@ -13,6 +17,7 @@ import { resolveFeatureAccess } from "../../../lib/time-tracking/featureAccess.j
|
|
|
13
17
|
import { MANAGE_PROJECTS_FEATURE, resolveProjectAccess } from "../../../lib/time-tracking/access.js";
|
|
14
18
|
import { readTimeTrackingSettings } from "../../../lib/time-tracking/settings.js";
|
|
15
19
|
import { decorateTimeEntryRows } from "../../../lib/timesheets/timeEntryDecoration.js";
|
|
20
|
+
import { computeTimeEntryTotals } from "../../../lib/timesheets/timeEntryTotals.js";
|
|
16
21
|
import { createStaffCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from "../../openapi.js";
|
|
17
22
|
const logger = createLogger("staff").child({ component: "api/timesheets/time-entries" });
|
|
18
23
|
const F = {
|
|
@@ -78,7 +83,12 @@ const listSchema = z.object({
|
|
|
78
83
|
/** One tag id, or a comma-separated list; an entry matching any of them. */
|
|
79
84
|
tagIds: z.string().optional(),
|
|
80
85
|
sortField: z.string().optional(),
|
|
81
|
-
sortDir: z.enum(["asc", "desc"]).optional()
|
|
86
|
+
sortDir: z.enum(["asc", "desc"]).optional(),
|
|
87
|
+
/**
|
|
88
|
+
* `true` adds a `totals` object computed over the WHOLE filtered set (not the
|
|
89
|
+
* page). Opt-in so callers that only need rows pay for no aggregate query.
|
|
90
|
+
*/
|
|
91
|
+
includeTotals: z.string().optional()
|
|
82
92
|
}).passthrough();
|
|
83
93
|
function parseBooleanFlagOrNull(value) {
|
|
84
94
|
if (typeof value !== "string") return null;
|
|
@@ -180,7 +190,7 @@ async function buildScopedTimeEntryListFilters(query, ctx) {
|
|
|
180
190
|
filters[F.id] = { $in: entryIds.length > 0 ? entryIds : [IMPOSSIBLE_ID] };
|
|
181
191
|
}
|
|
182
192
|
const access = await resolveListProjectAccess(ctx);
|
|
183
|
-
if (access.canManageAll) return filters;
|
|
193
|
+
if (access.canManageAll) return mergeAdvancedFiltersFromQuery(filters, query);
|
|
184
194
|
const branches = [];
|
|
185
195
|
if (access.projectIds.length > 0) {
|
|
186
196
|
branches.push({ [F.time_project_id]: { $in: access.projectIds } });
|
|
@@ -190,10 +200,10 @@ async function buildScopedTimeEntryListFilters(query, ctx) {
|
|
|
190
200
|
}
|
|
191
201
|
if (branches.length === 0) {
|
|
192
202
|
filters[F.id] = { $in: [IMPOSSIBLE_ID] };
|
|
193
|
-
return filters;
|
|
203
|
+
return mergeAdvancedFiltersFromQuery(filters, query);
|
|
194
204
|
}
|
|
195
205
|
filters.$or = branches;
|
|
196
|
-
return filters;
|
|
206
|
+
return mergeAdvancedFiltersFromQuery(filters, query);
|
|
197
207
|
}
|
|
198
208
|
const timeEntryListFields = [
|
|
199
209
|
F.id,
|
|
@@ -242,6 +252,40 @@ async function decorateTimeEntryList(payload, ctx) {
|
|
|
242
252
|
onError: (err) => logger.error("staff.timesheets.time-entries response decoration failed", { err })
|
|
243
253
|
});
|
|
244
254
|
}
|
|
255
|
+
function resolveListOrganizationIds(ctx) {
|
|
256
|
+
if (Array.isArray(ctx.organizationIds)) return Array.from(new Set(ctx.organizationIds));
|
|
257
|
+
const fallback = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
|
|
258
|
+
return fallback ? [fallback] : [];
|
|
259
|
+
}
|
|
260
|
+
async function attachTimeEntryTotals(payload, ctx) {
|
|
261
|
+
if (parseBooleanToken(ctx.query?.includeTotals ?? null) !== true) return;
|
|
262
|
+
if (!payload || typeof payload !== "object" || !Array.isArray(payload.items)) return;
|
|
263
|
+
delete payload.totals;
|
|
264
|
+
const tenantId = ctx.auth?.tenantId ?? null;
|
|
265
|
+
if (!tenantId) return;
|
|
266
|
+
try {
|
|
267
|
+
const { organizationId } = resolveCtxScope(ctx);
|
|
268
|
+
const query = ctx.query;
|
|
269
|
+
const scopedFilters = await buildScopedTimeEntryListFilters(ctx.query, ctx);
|
|
270
|
+
const filters = mergeIdFilter(scopedFilters, parseIdsParam(query.ids), {
|
|
271
|
+
idsParamProvided: isIdsParamProvided(query.ids)
|
|
272
|
+
});
|
|
273
|
+
const rates = await resolveFeatureAccess(ctx.container, ctx.auth?.sub ?? null, [RATES_FEATURE], {
|
|
274
|
+
tenantId,
|
|
275
|
+
organizationId
|
|
276
|
+
});
|
|
277
|
+
const em = ctx.container.resolve("em");
|
|
278
|
+
payload.totals = await computeTimeEntryTotals(em.getKysely(), filters, {
|
|
279
|
+
tenantId,
|
|
280
|
+
organizationIds: resolveListOrganizationIds(ctx),
|
|
281
|
+
canSeeRates: rates.allowed,
|
|
282
|
+
includeDeleted: parseBooleanToken(typeof query.withDeleted === "string" ? query.withDeleted : null) === true
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
logger.error("staff.timesheets.time-entries totals failed", { err });
|
|
286
|
+
getTelemetryRuntime()?.reportError(err, { module: "staff", code: "staff.time_entry_totals_failed" });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
245
289
|
const crud = makeCrudRoute({
|
|
246
290
|
metadata: routeMetadata,
|
|
247
291
|
orm: {
|
|
@@ -267,6 +311,9 @@ const crud = makeCrudRoute({
|
|
|
267
311
|
},
|
|
268
312
|
buildFilters: buildScopedTimeEntryListFilters
|
|
269
313
|
},
|
|
314
|
+
hooks: {
|
|
315
|
+
afterList: attachTimeEntryTotals
|
|
316
|
+
},
|
|
270
317
|
actions: {
|
|
271
318
|
create: {
|
|
272
319
|
commandId: staffTimeEntryCommandIds.create,
|
|
@@ -347,11 +394,23 @@ const timeEntryListItemSchema = z.object({
|
|
|
347
394
|
/** Present only for a caller holding `staff.timesheets.rates.view`. */
|
|
348
395
|
currencyCode: z.string().nullable().optional()
|
|
349
396
|
});
|
|
397
|
+
const timeEntryTotalsSchema = z.object({
|
|
398
|
+
entryCount: z.number(),
|
|
399
|
+
/** Sum of `duration_minutes` across the whole filtered set. */
|
|
400
|
+
durationMinutes: z.number(),
|
|
401
|
+
/** Sum of `rounded_minutes` across the whole filtered set. */
|
|
402
|
+
roundedMinutes: z.number(),
|
|
403
|
+
/** Present only for a caller holding `staff.timesheets.rates.view`; one entry per currency, never added together. */
|
|
404
|
+
money: z.array(z.object({ currencyCode: z.string().nullable(), amount: z.number() })).optional()
|
|
405
|
+
});
|
|
350
406
|
const openApi = createStaffCrudOpenApi({
|
|
351
407
|
resourceName: "TimeEntry",
|
|
352
408
|
pluralName: "TimeEntries",
|
|
353
409
|
querySchema: listSchema,
|
|
354
|
-
listResponseSchema: createPagedListResponseSchema(timeEntryListItemSchema)
|
|
410
|
+
listResponseSchema: createPagedListResponseSchema(timeEntryListItemSchema).extend({
|
|
411
|
+
/** Present only when the request sends `includeTotals=true`; covers every row matching the filters, not just the page. */
|
|
412
|
+
totals: timeEntryTotalsSchema.optional()
|
|
413
|
+
}),
|
|
355
414
|
create: {
|
|
356
415
|
schema: staffTimeEntryCreateSchema,
|
|
357
416
|
description: "Creates a time entry for a staff member. The note is accepted under either `notes` or `description` (`description` wins when both are sent). `roundedMinutes` is derived from the tenant rounding rule at write time and stored; `isBillable` falls back to the project default then the tenant default; `rateCurrencyCode` snapshots the project currency. `tagIds` is applied through the tag assignment command."
|
|
@@ -373,6 +432,7 @@ export {
|
|
|
373
432
|
POST,
|
|
374
433
|
PUT,
|
|
375
434
|
RATES_FEATURE,
|
|
435
|
+
attachTimeEntryTotals,
|
|
376
436
|
buildScopedTimeEntryListFilters,
|
|
377
437
|
decorateTimeEntryList,
|
|
378
438
|
metadata,
|