@burdenoff/microfe-assethandler 2026.1110.1 → 2026.1111.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assethandler/components/AttachmentsPanel.d.ts +23 -0
- package/dist/assethandler/components/AttachmentsPanel.js +189 -111
- package/dist/assethandler/components/AttachmentsPanel.js.map +1 -1
- package/dist/assethandler/components/FinanceRegisterExportDialog.d.ts +6 -0
- package/dist/assethandler/components/FinanceRegisterExportDialog.js +189 -0
- package/dist/assethandler/components/FinanceRegisterExportDialog.js.map +1 -0
- package/dist/assethandler/pages/FinanceFinanceHubPage.js +146 -208
- package/dist/assethandler/pages/FinanceFinanceHubPage.js.map +1 -1
- package/dist/assethandler/utils/financeExport.d.ts +32 -0
- package/dist/assethandler/utils/financeExport.js +85 -0
- package/dist/assethandler/utils/financeExport.js.map +1 -0
- package/dist/assethandler/utils/graphqlErrors.d.ts +25 -0
- package/dist/assethandler/utils/graphqlErrors.js +37 -0
- package/dist/assethandler/utils/graphqlErrors.js.map +1 -0
- package/dist/generated/wspace-operations.d.ts +144 -0
- package/dist/generated/wspace-operations.js +301 -0
- package/dist/generated/wspace-operations.js.map +1 -0
- package/dist/test/mocks/fe-libs-ui.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachmentsPanel.js","names":["ImageIcon","FileIcon","GET_FILES_QUERY","INITIATE_UPLOAD_MUTATION","COMPLETE_UPLOAD_MUTATION","CREATE_SHARE_LINK_MUTATION","formatSize","bytes","units","exponent","extensionOf","dot","fileName","relativeTime","then","iso","diff","sec","min","hr","day","kindOf","mime","file","ext","KIND_STYLE","Image","FileText","FileSpreadsheet","FileArchive","FileCode2","FileVideo","FileAudio","File","FileBadge","kind","tileClass","Icon","iconClass","AttachmentsPanel","fileInputRef","useRef","uploading","setUploading","useState","sharingId","setSharingId","useQuery","initiateUpload","useMutation","completeUpload","createShareLink","files","useMemo","data","handleFileSelected","event","initiated","uploadUrl","sessionId","putResponse","notify","refetch","error","handleShare","fileId","url","total","hasFiles","Card","CardHeader","CardTitle","Paperclip","CardContent","entityType","entityId","loading","i","isSharing","Download","Loader2","Link2","DropdownMenu","DropdownMenuTrigger","MoreHorizontal","DropdownMenuContent","DropdownMenuItem","e","IllustratedEmptyState","Button","Upload"],"sources":["../../../src/assethandler/components/AttachmentsPanel.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n IllustratedEmptyState,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Paperclip,\n Upload,\n Link2,\n FileText,\n FileSpreadsheet,\n FileArchive,\n FileCode2,\n FileVideo,\n FileAudio,\n Image as ImageIcon,\n File as FileIcon,\n Loader2,\n Download,\n MoreHorizontal,\n} from 'lucide-react';\nimport { notify } from '../utils/toast';\n\n/**\n * Attach and reference files for an AssetHandler entity, backed by the\n * federated wspace-files-svc (same wspace gateway/Apollo client). Fully real (no fabricated data) —\n * lists workspace files via `getFiles`, uploads new files through the\n * `initiateUpload` → presigned PUT → `completeUpload` flow, and mints public\n * share links via `createFileShareLink`. All entries are live data.\n *\n * `entityType`/`entityId` scope the panel to the record it is mounted on and\n * are used for context labelling; uploaded files land in the workspace file\n * store so they can be shared and reused.\n */\nexport interface AttachmentsPanelProps {\n entityType: string;\n entityId: string;\n}\n\ninterface FileNode {\n id: string;\n name: string;\n extension: string;\n mimeType: string;\n size: number;\n downloadUrl?: string | null;\n createdAt: string;\n}\n\ninterface GetFilesResult {\n getFiles: {\n files: FileNode[];\n total: number;\n hasMore: boolean;\n };\n}\n\nconst GET_FILES_QUERY = gql`\n query AssetHandlerGetFiles($pagination: PaginationInput, $sort: FileSortInput) {\n getFiles(pagination: $pagination, sort: $sort) {\n total\n hasMore\n files {\n id\n name\n extension\n mimeType\n size\n downloadUrl\n createdAt\n }\n }\n }\n`;\n\nconst INITIATE_UPLOAD_MUTATION = gql`\n mutation AssetHandlerInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session {\n id\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD_MUTATION = gql`\n mutation AssetHandlerCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n name\n }\n }\n }\n`;\n\nconst CREATE_SHARE_LINK_MUTATION = gql`\n mutation AssetHandlerCreateFileShareLink($input: CreateShareGrantLinkInput!) {\n createFileShareLink(input: $input) {\n id\n shareUrl\n }\n }\n`;\n\nfunction formatSize(bytes: number): string {\n if (!bytes) return '0 B';\n const units = ['B', 'KB', 'MB', 'GB'];\n const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);\n const value = bytes / Math.pow(1024, exponent);\n return `${value.toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`;\n}\n\nfunction extensionOf(fileName: string): string {\n const dot = fileName.lastIndexOf('.');\n return dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n}\n\n/** Compact relative time (\"2 hrs ago\"), with the full timestamp on hover. */\nfunction relativeTime(iso: string): string {\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return '';\n const diff = Date.now() - then;\n const sec = Math.round(diff / 1000);\n if (sec < 45) return 'just now';\n const min = Math.round(sec / 60);\n if (min < 60) return `${min} min ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr} hr${hr === 1 ? '' : 's'} ago`;\n const day = Math.round(hr / 24);\n if (day < 30) return `${day} day${day === 1 ? '' : 's'} ago`;\n return new Date(iso).toLocaleDateString();\n}\n\ntype FileKind =\n 'image' | 'pdf' | 'sheet' | 'doc' | 'archive' | 'code' | 'video' | 'audio' | 'other';\n\n/** Map a file's extension/mime to a recognisable kind for icon + tint. */\nfunction kindOf(file: FileNode): FileKind {\n const mime = (file.mimeType || '').toLowerCase();\n const ext = (file.extension || extensionOf(file.name)).toLowerCase();\n if (\n mime.startsWith('image/') ||\n ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif', 'bmp'].includes(ext)\n )\n return 'image';\n if (mime.startsWith('video/') || ['mp4', 'mov', 'webm', 'avi', 'mkv'].includes(ext))\n return 'video';\n if (mime.startsWith('audio/') || ['mp3', 'wav', 'ogg', 'm4a', 'flac'].includes(ext))\n return 'audio';\n if (mime === 'application/pdf' || ext === 'pdf') return 'pdf';\n if (['xls', 'xlsx', 'csv', 'tsv', 'ods'].includes(ext)) return 'sheet';\n if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(ext)) return 'archive';\n if (\n [\n 'js',\n 'ts',\n 'tsx',\n 'jsx',\n 'json',\n 'xml',\n 'yml',\n 'yaml',\n 'html',\n 'css',\n 'sh',\n 'py',\n 'sql',\n ].includes(ext)\n )\n return 'code';\n if (['doc', 'docx', 'txt', 'md', 'rtf', 'odt', 'pages'].includes(ext)) return 'doc';\n return 'other';\n}\n\nconst KIND_STYLE: Record<\n FileKind,\n { Icon: typeof FileText; iconClass: string; tileClass: string }\n> = {\n image: {\n Icon: ImageIcon,\n iconClass: 'text-status-success-text',\n tileClass: 'bg-status-success-bg-subtle',\n },\n pdf: {\n Icon: FileText,\n iconClass: 'text-status-error-text',\n tileClass: 'bg-status-error-bg-subtle',\n },\n sheet: {\n Icon: FileSpreadsheet,\n iconClass: 'text-status-success-text',\n tileClass: 'bg-status-success-bg-subtle',\n },\n doc: {\n Icon: FileText,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n archive: {\n Icon: FileArchive,\n iconClass: 'text-status-warning-text',\n tileClass: 'bg-status-warning-bg-subtle',\n },\n code: {\n Icon: FileCode2,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n video: {\n Icon: FileVideo,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n audio: {\n Icon: FileAudio,\n iconClass: 'text-status-warning-text',\n tileClass: 'bg-status-warning-bg-subtle',\n },\n other: { Icon: FileIcon, iconClass: 'text-text-secondary', tileClass: 'bg-bg-sunken' },\n};\n\n/** File-type badge tile — an inline thumbnail for images, a tinted glyph otherwise. */\nfunction FileBadge({ file }: { file: FileNode }) {\n const kind = kindOf(file);\n const { Icon, iconClass, tileClass } = KIND_STYLE[kind];\n if (kind === 'image' && file.downloadUrl) {\n return (\n <img\n src={file.downloadUrl}\n alt=\"\"\n loading=\"lazy\"\n className=\"h-10 w-10 shrink-0 rounded-lg border border-border-subtle object-cover\"\n />\n );\n }\n return (\n <span\n className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${tileClass}`}\n aria-hidden=\"true\"\n >\n <Icon className={`h-5 w-5 ${iconClass}`} />\n </span>\n );\n}\n\nexport function AttachmentsPanel({ entityType, entityId }: AttachmentsPanelProps) {\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [uploading, setUploading] = useState(false);\n const [sharingId, setSharingId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useQuery<GetFilesResult>(GET_FILES_QUERY, {\n variables: {\n pagination: { limit: 25, offset: 0 },\n sort: { field: 'CREATED_AT', order: 'DESC' },\n },\n fetchPolicy: 'cache-and-network',\n errorPolicy: 'all',\n });\n\n const [initiateUpload] = useMutation<{\n initiateUpload: { uploadUrl?: string | null; session: { id: string } };\n }>(INITIATE_UPLOAD_MUTATION);\n const [completeUpload] = useMutation(COMPLETE_UPLOAD_MUTATION);\n const [createShareLink] = useMutation<{\n createFileShareLink: { id: string; shareUrl?: string | null };\n }>(CREATE_SHARE_LINK_MUTATION);\n\n const files = useMemo(() => data?.getFiles.files ?? [], [data]);\n\n const handleFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset the input so selecting the same file again re-triggers change.\n event.target.value = '';\n if (!file) return;\n\n setUploading(true);\n try {\n const initiated = await initiateUpload({\n variables: {\n input: {\n fileName: file.name,\n extension: extensionOf(file.name),\n fileSize: file.size,\n mimeType: file.type || 'application/octet-stream',\n visibility: 'WORKSPACE',\n },\n },\n });\n\n const uploadUrl = initiated.data?.initiateUpload.uploadUrl;\n const sessionId = initiated.data?.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) {\n throw new Error('Upload could not be initialised.');\n }\n\n const putResponse = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': file.type || 'application/octet-stream' },\n });\n if (!putResponse.ok) {\n throw new Error(`Storage rejected the upload (HTTP ${putResponse.status}).`);\n }\n\n await completeUpload({\n variables: { input: { sessionId, fileSize: file.size } },\n });\n\n notify.success('File uploaded', `${file.name} is now available on this record.`);\n await refetch();\n } catch (error) {\n notify.error(\n 'Upload failed',\n error instanceof Error ? error.message : 'The file could not be uploaded.'\n );\n } finally {\n setUploading(false);\n }\n };\n\n const handleShare = async (fileId: string) => {\n setSharingId(fileId);\n try {\n const result = await createShareLink({\n variables: { input: { resourceId: fileId, role: 'VIEWER' } },\n });\n const url = result.data?.createFileShareLink.shareUrl;\n if (!url) {\n throw new Error('No share link was returned.');\n }\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(url);\n notify.success('Share link copied', 'A view-only link is on your clipboard.');\n } else {\n notify.info('Share link ready', url);\n }\n } catch (error) {\n notify.error(\n 'Could not create share link',\n error instanceof Error ? error.message : 'Please try again.'\n );\n } finally {\n setSharingId(null);\n }\n };\n\n const total = data?.getFiles.total ?? 0;\n const hasFiles = files.length > 0;\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between gap-2 space-y-0\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Paperclip className=\"h-4 w-4\" />\n Attachments\n {total ? (\n <span className=\"text-sm font-normal text-text-secondary\">\n {total} file{total === 1 ? '' : 's'}\n </span>\n ) : null}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <input\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n onChange={(event) => void handleFileSelected(event)}\n aria-label={`Upload a file for ${entityType} ${entityId}`}\n />\n\n {loading && !hasFiles ? (\n <div className=\"space-y-2\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div\n key={i}\n className=\"h-14 rounded-lg bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : !hasFiles ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No files yet\"\n description=\"Upload photos, receipts, warranties or documents to keep them with this record.\"\n />\n ) : (\n <ul className=\"space-y-1.5\">\n {files.map((file) => {\n const isSharing = sharingId === file.id;\n return (\n <li\n key={file.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-2.5\"\n >\n <FileBadge file={file} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium\" title={file.name}>\n {file.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {formatSize(Number(file.size))} · {relativeTime(file.createdAt)}\n </p>\n </div>\n {/* Desktop: inline actions. Mobile: kebab menu to avoid crowding. */}\n <div className=\"hidden items-center gap-1 sm:flex\">\n {file.downloadUrl ? (\n <a\n href={file.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={`Download ${file.name}`}\n title=\"Download\"\n >\n <Download className=\"h-4 w-4\" />\n </a>\n ) : null}\n <button\n type=\"button\"\n disabled={isSharing}\n onClick={() => void handleShare(file.id)}\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary disabled:opacity-60\"\n aria-label={`Copy share link for ${file.name}`}\n title=\"Copy share link\"\n >\n {isSharing ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Link2 className=\"h-4 w-4\" />\n )}\n </button>\n </div>\n <div className=\"sm:hidden\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={`Actions for ${file.name}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {file.downloadUrl ? (\n <DropdownMenuItem asChild>\n <a href={file.downloadUrl} target=\"_blank\" rel=\"noreferrer\">\n <Download className=\"mr-2 h-4 w-4\" />\n Download\n </a>\n </DropdownMenuItem>\n ) : null}\n <DropdownMenuItem\n disabled={isSharing}\n onSelect={(e) => {\n e.preventDefault();\n void handleShare(file.id);\n }}\n >\n <Link2 className=\"mr-2 h-4 w-4\" />\n Copy share link\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n\n {/* Prominent full-width primary upload CTA (matches the reference concept). */}\n <Button\n className=\"w-full\"\n disabled={uploading}\n onClick={() => fileInputRef.current?.click()}\n >\n {uploading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Upload className=\"mr-2 h-4 w-4\" />\n )}\n {uploading ? 'Uploading…' : 'Upload files'}\n </Button>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AttachmentsPanel.js","names":["ImageIcon","FileIcon","GET_FILES_QUERY","INITIATE_UPLOAD_MUTATION","COMPLETE_UPLOAD_MUTATION","CREATE_SHARE_LINK_MUTATION","USER_FILE_PERMISSIONS_QUERY","requiresShareCheck","visibility","SHARE_DENIED_TITLE","RESTRICTED_SHARE_DENIED_DESCRIPTION","ROLE_SHARE_DENIED_DESCRIPTION","OTHER_SHARE_DENIED_DESCRIPTION","isShareDenial","access","shareDeniedDescription","denial","shareDenialFor","source","getForbiddenSource","error","formatSize","bytes","units","exponent","extensionOf","dot","fileName","relativeTime","then","iso","diff","sec","min","hr","day","kindOf","mime","file","ext","KIND_STYLE","Image","FileText","FileSpreadsheet","FileArchive","FileCode2","FileVideo","FileAudio","File","FileBadge","kind","tileClass","Icon","iconClass","AttachmentsPanel","fileInputRef","useRef","uploading","setUploading","useState","sharingId","setSharingId","shareAccess","setShareAccess","apolloClient","useApolloClient","useQuery","initiateUpload","useMutation","completeUpload","createShareLink","files","useMemo","data","handleFileSelected","event","initiated","uploadUrl","sessionId","putResponse","notify","refetch","setAccess","useCallback","next","current","fileId","checkCanShare","outcome","isForbiddenError","handleMenuOpenChange","open","handleShare","known","recheck","url","total","hasFiles","Card","CardHeader","CardTitle","Paperclip","CardContent","entityType","entityId","loading","i","isSharing","isChecking","isDenied","Download","Loader2","Lock","Link2","DropdownMenu","DropdownMenuTrigger","MoreHorizontal","DropdownMenuContent","DropdownMenuItem","e","IllustratedEmptyState","Button","Upload"],"sources":["../../../src/assethandler/components/AttachmentsPanel.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient, useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n IllustratedEmptyState,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport {\n Paperclip,\n Upload,\n Link2,\n FileText,\n FileSpreadsheet,\n FileArchive,\n FileCode2,\n FileVideo,\n FileAudio,\n Image as ImageIcon,\n File as FileIcon,\n Loader2,\n Download,\n MoreHorizontal,\n Lock,\n} from 'lucide-react';\nimport { notify } from '../utils/toast';\nimport { getForbiddenSource, isForbiddenError } from '../utils/graphqlErrors';\n\n/**\n * Attach and reference files for an AssetHandler entity, backed by the\n * federated wspace-files-svc (same wspace gateway/Apollo client). Fully real (no fabricated data) —\n * lists workspace files via `getFiles`, uploads new files through the\n * `initiateUpload` → presigned PUT → `completeUpload` flow, and mints public\n * share links via `createFileShareLink`. All entries are live data.\n *\n * Sharing follows the files service's rules, which depend on visibility:\n *\n * - PRIVATE and TEAM files can only be shared by their uploader or someone\n * holding a SHARE/MANAGE grant or a MANAGER share. For these the panel asks\n * `getUserFilePermissions(fileId).canShare` before minting a link. The check\n * runs lazily (on each desktop click and each time the mobile menu opens), so\n * a long file list never fans out into one permission query per row.\n * - WORKSPACE and PUBLIC files are governed only by the gateway's\n * `@rbac(share, file)` check. `canShare` reflects explicit grants alone for\n * these files, so it is not consulted: the panel calls `createFileShareLink`\n * directly and treats its FORBIDDEN as the denial.\n *\n * The two denials arrive differently and get different copy. The gateway's\n * role check answers HTTP 403 (an Apollo `ServerError`), so the caller is told\n * their workspace role cannot share. The service's per-file rule answers HTTP\n * 200 with a `FORBIDDEN` GraphQL error, so the caller is told who can share\n * the file. A 503 `RBAC_SERVICE_UNAVAILABLE` is an outage, not a denial.\n *\n * `getUserFilePermissions` needs its own `@rbac(read, file-permission)`, which\n * some roles lack. A FORBIDDEN from that query, from either source, means\n * \"unknown\", never \"cannot share\": the panel falls through and lets the share\n * mutation decide.\n *\n * `entityType`/`entityId` scope the panel to the record it is mounted on and\n * are used for context labelling; uploaded files land in the workspace file\n * store so they can be shared and reused.\n */\nexport interface AttachmentsPanelProps {\n entityType: string;\n entityId: string;\n}\n\ninterface FileNode {\n id: string;\n name: string;\n extension: string;\n mimeType: string;\n size: number;\n downloadUrl?: string | null;\n createdAt: string;\n visibility: FileVisibility;\n}\n\ninterface GetFilesResult {\n getFiles: {\n files: FileNode[];\n total: number;\n hasMore: boolean;\n };\n}\n\nconst GET_FILES_QUERY = gql`\n query AssetHandlerGetFiles($pagination: PaginationInput, $sort: FileSortInput) {\n getFiles(pagination: $pagination, sort: $sort) {\n total\n hasMore\n files {\n id\n name\n extension\n mimeType\n size\n downloadUrl\n createdAt\n visibility\n }\n }\n }\n`;\n\nconst INITIATE_UPLOAD_MUTATION = gql`\n mutation AssetHandlerInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session {\n id\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD_MUTATION = gql`\n mutation AssetHandlerCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n name\n }\n }\n }\n`;\n\nconst CREATE_SHARE_LINK_MUTATION = gql`\n mutation AssetHandlerCreateFileShareLink($input: CreateShareGrantLinkInput!) {\n createFileShareLink(input: $input) {\n id\n shareUrl\n }\n }\n`;\n\nconst USER_FILE_PERMISSIONS_QUERY = gql`\n query AssetHandlerUserFilePermissions($fileId: ID!) {\n getUserFilePermissions(fileId: $fileId) {\n fileId\n canShare\n }\n }\n`;\n\ninterface UserFilePermissionsResult {\n getUserFilePermissions: { fileId: string; canShare: boolean };\n}\n\ntype FileVisibility = 'PRIVATE' | 'TEAM' | 'WORKSPACE' | 'PUBLIC';\n\n/**\n * Why sharing a file was refused, which decides the copy shown:\n *\n * - `denied-role`: the gateway's `@rbac(share, file)` role check (HTTP 403).\n * - `denied-restricted`: the files service's per-file SHARE rule on a PRIVATE\n * or TEAM file, from the `canShare` pre-check or a service FORBIDDEN.\n * - `denied-other`: a service FORBIDDEN on a WORKSPACE or PUBLIC file, which\n * the per-file rule does not cover, so no specific reason can be given.\n */\ntype ShareDenial = 'denied-role' | 'denied-restricted' | 'denied-other';\n\n/**\n * Per-file share state. `unverified` means the permission query itself was\n * FORBIDDEN, so the share mutation is the only authority left to ask.\n */\ntype ShareAccess = 'checking' | 'allowed' | 'unverified' | ShareDenial;\n\n/** Result of one capability check; `failed` is a non-FORBIDDEN query error. */\ntype ShareCheckOutcome = 'allowed' | 'denied-restricted' | 'unverified' | 'failed';\n\n/**\n * Only PRIVATE and TEAM files have a per-file share rule on the server, so only\n * they are pre-checked. WORKSPACE and PUBLIC sharing is decided by the\n * gateway's role check, which `canShare` does not reflect.\n */\nfunction requiresShareCheck(visibility: FileVisibility | null | undefined): boolean {\n return visibility === 'PRIVATE' || visibility === 'TEAM';\n}\n\nconst SHARE_DENIED_TITLE = \"You can't share this file\";\nconst RESTRICTED_SHARE_DENIED_DESCRIPTION =\n 'Only the uploader, or someone with share or manage access to this file, can create a share link. Ask one of them to share it or to grant you access.';\nconst ROLE_SHARE_DENIED_DESCRIPTION =\n \"Your workspace role doesn't allow sharing files. Ask a workspace admin for share access.\";\nconst OTHER_SHARE_DENIED_DESCRIPTION =\n \"You don't have permission to share this file. Ask the file's uploader or a workspace admin.\";\n\nfunction isShareDenial(access: ShareAccess | undefined): access is ShareDenial {\n return access === 'denied-role' || access === 'denied-restricted' || access === 'denied-other';\n}\n\nfunction shareDeniedDescription(denial: ShareDenial): string {\n switch (denial) {\n case 'denied-role':\n return ROLE_SHARE_DENIED_DESCRIPTION;\n case 'denied-restricted':\n return RESTRICTED_SHARE_DENIED_DESCRIPTION;\n case 'denied-other':\n return OTHER_SHARE_DENIED_DESCRIPTION;\n }\n}\n\n/**\n * Maps a FORBIDDEN from `createFileShareLink` to its denial. The gateway's\n * HTTP 403 is always the role check; a service FORBIDDEN is the per-file\n * SHARE rule only for PRIVATE and TEAM files.\n */\nfunction shareDenialFor(\n error: unknown,\n visibility: FileVisibility | null | undefined\n): ShareDenial | null {\n const source = getForbiddenSource(error);\n if (source === 'gateway') return 'denied-role';\n if (source === 'service') {\n return requiresShareCheck(visibility) ? 'denied-restricted' : 'denied-other';\n }\n return null;\n}\n\nfunction formatSize(bytes: number): string {\n if (!bytes) return '0 B';\n const units = ['B', 'KB', 'MB', 'GB'];\n const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);\n const value = bytes / Math.pow(1024, exponent);\n return `${value.toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`;\n}\n\nfunction extensionOf(fileName: string): string {\n const dot = fileName.lastIndexOf('.');\n return dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n}\n\n/** Compact relative time (\"2 hrs ago\"), with the full timestamp on hover. */\nfunction relativeTime(iso: string): string {\n const then = new Date(iso).getTime();\n if (Number.isNaN(then)) return '';\n const diff = Date.now() - then;\n const sec = Math.round(diff / 1000);\n if (sec < 45) return 'just now';\n const min = Math.round(sec / 60);\n if (min < 60) return `${min} min ago`;\n const hr = Math.round(min / 60);\n if (hr < 24) return `${hr} hr${hr === 1 ? '' : 's'} ago`;\n const day = Math.round(hr / 24);\n if (day < 30) return `${day} day${day === 1 ? '' : 's'} ago`;\n return new Date(iso).toLocaleDateString();\n}\n\ntype FileKind =\n 'image' | 'pdf' | 'sheet' | 'doc' | 'archive' | 'code' | 'video' | 'audio' | 'other';\n\n/** Map a file's extension/mime to a recognisable kind for icon + tint. */\nfunction kindOf(file: FileNode): FileKind {\n const mime = (file.mimeType || '').toLowerCase();\n const ext = (file.extension || extensionOf(file.name)).toLowerCase();\n if (\n mime.startsWith('image/') ||\n ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif', 'bmp'].includes(ext)\n )\n return 'image';\n if (mime.startsWith('video/') || ['mp4', 'mov', 'webm', 'avi', 'mkv'].includes(ext))\n return 'video';\n if (mime.startsWith('audio/') || ['mp3', 'wav', 'ogg', 'm4a', 'flac'].includes(ext))\n return 'audio';\n if (mime === 'application/pdf' || ext === 'pdf') return 'pdf';\n if (['xls', 'xlsx', 'csv', 'tsv', 'ods'].includes(ext)) return 'sheet';\n if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(ext)) return 'archive';\n if (\n [\n 'js',\n 'ts',\n 'tsx',\n 'jsx',\n 'json',\n 'xml',\n 'yml',\n 'yaml',\n 'html',\n 'css',\n 'sh',\n 'py',\n 'sql',\n ].includes(ext)\n )\n return 'code';\n if (['doc', 'docx', 'txt', 'md', 'rtf', 'odt', 'pages'].includes(ext)) return 'doc';\n return 'other';\n}\n\nconst KIND_STYLE: Record<\n FileKind,\n { Icon: typeof FileText; iconClass: string; tileClass: string }\n> = {\n image: {\n Icon: ImageIcon,\n iconClass: 'text-status-success-text',\n tileClass: 'bg-status-success-bg-subtle',\n },\n pdf: {\n Icon: FileText,\n iconClass: 'text-status-error-text',\n tileClass: 'bg-status-error-bg-subtle',\n },\n sheet: {\n Icon: FileSpreadsheet,\n iconClass: 'text-status-success-text',\n tileClass: 'bg-status-success-bg-subtle',\n },\n doc: {\n Icon: FileText,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n archive: {\n Icon: FileArchive,\n iconClass: 'text-status-warning-text',\n tileClass: 'bg-status-warning-bg-subtle',\n },\n code: {\n Icon: FileCode2,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n video: {\n Icon: FileVideo,\n iconClass: 'text-status-info-text',\n tileClass: 'bg-status-info-bg-subtle',\n },\n audio: {\n Icon: FileAudio,\n iconClass: 'text-status-warning-text',\n tileClass: 'bg-status-warning-bg-subtle',\n },\n other: { Icon: FileIcon, iconClass: 'text-text-secondary', tileClass: 'bg-bg-sunken' },\n};\n\n/** File-type badge tile — an inline thumbnail for images, a tinted glyph otherwise. */\nfunction FileBadge({ file }: { file: FileNode }) {\n const kind = kindOf(file);\n const { Icon, iconClass, tileClass } = KIND_STYLE[kind];\n if (kind === 'image' && file.downloadUrl) {\n return (\n <img\n src={file.downloadUrl}\n alt=\"\"\n loading=\"lazy\"\n className=\"h-10 w-10 shrink-0 rounded-lg border border-border-subtle object-cover\"\n />\n );\n }\n return (\n <span\n className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${tileClass}`}\n aria-hidden=\"true\"\n >\n <Icon className={`h-5 w-5 ${iconClass}`} />\n </span>\n );\n}\n\nexport function AttachmentsPanel({ entityType, entityId }: AttachmentsPanelProps) {\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [uploading, setUploading] = useState(false);\n const [sharingId, setSharingId] = useState<string | null>(null);\n const [shareAccess, setShareAccess] = useState<Record<string, ShareAccess>>({});\n const apolloClient = useApolloClient();\n\n const { data, loading, refetch } = useQuery<GetFilesResult>(GET_FILES_QUERY, {\n variables: {\n pagination: { limit: 25, offset: 0 },\n sort: { field: 'CREATED_AT', order: 'DESC' },\n },\n fetchPolicy: 'cache-and-network',\n errorPolicy: 'all',\n });\n\n const [initiateUpload] = useMutation<{\n initiateUpload: { uploadUrl?: string | null; session: { id: string } };\n }>(INITIATE_UPLOAD_MUTATION);\n const [completeUpload] = useMutation(COMPLETE_UPLOAD_MUTATION);\n const [createShareLink] = useMutation<{\n createFileShareLink: { id: string; shareUrl?: string | null };\n }>(CREATE_SHARE_LINK_MUTATION);\n\n const files = useMemo(() => data?.getFiles.files ?? [], [data]);\n\n const handleFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Reset the input so selecting the same file again re-triggers change.\n event.target.value = '';\n if (!file) return;\n\n setUploading(true);\n try {\n const initiated = await initiateUpload({\n variables: {\n input: {\n fileName: file.name,\n extension: extensionOf(file.name),\n fileSize: file.size,\n mimeType: file.type || 'application/octet-stream',\n visibility: 'WORKSPACE',\n },\n },\n });\n\n const uploadUrl = initiated.data?.initiateUpload.uploadUrl;\n const sessionId = initiated.data?.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) {\n throw new Error('Upload could not be initialised.');\n }\n\n const putResponse = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': file.type || 'application/octet-stream' },\n });\n if (!putResponse.ok) {\n throw new Error(`Storage rejected the upload (HTTP ${putResponse.status}).`);\n }\n\n await completeUpload({\n variables: { input: { sessionId, fileSize: file.size } },\n });\n\n notify.success('File uploaded', `${file.name} is now available on this record.`);\n await refetch();\n } catch (error) {\n notify.error(\n 'Upload failed',\n error instanceof Error ? error.message : 'The file could not be uploaded.'\n );\n } finally {\n setUploading(false);\n }\n };\n\n const setAccess = useCallback((fileId: string, access: ShareAccess | null) => {\n setShareAccess((current) => {\n const next = { ...current };\n if (access) next[fileId] = access;\n else delete next[fileId];\n return next;\n });\n }, []);\n\n /**\n * Asks the files service whether the caller may share a PRIVATE or TEAM file.\n * Always hits the network so a grant or revoke made elsewhere is picked up on\n * the next attempt. A FORBIDDEN from the query, whether the gateway's 403 or\n * a service error, means the caller cannot read file permissions, which says\n * nothing about sharing, so it resolves to `unverified` and the share\n * mutation decides. Any other failure (a 503 included) is `failed`.\n */\n const checkCanShare = useCallback(\n async (fileId: string): Promise<ShareCheckOutcome> => {\n setAccess(fileId, 'checking');\n try {\n const result = await apolloClient.query<UserFilePermissionsResult>({\n query: USER_FILE_PERMISSIONS_QUERY,\n variables: { fileId },\n fetchPolicy: 'network-only',\n });\n const outcome =\n result.data?.getUserFilePermissions.canShare === true ? 'allowed' : 'denied-restricted';\n setAccess(fileId, outcome);\n return outcome;\n } catch (error) {\n if (isForbiddenError(error)) {\n setAccess(fileId, 'unverified');\n return 'unverified';\n }\n setAccess(fileId, null);\n return 'failed';\n }\n },\n [apolloClient, setAccess]\n );\n\n const handleMenuOpenChange = (file: FileNode, open: boolean) => {\n if (!open) return;\n if (requiresShareCheck(file.visibility)) {\n if (shareAccess[file.id] !== 'checking') void checkCanShare(file.id);\n } else if (isShareDenial(shareAccess[file.id])) {\n // No pre-check exists for WORKSPACE/PUBLIC files: re-opening the menu\n // re-enables the action so the next attempt asks the server again.\n setAccess(file.id, null);\n }\n };\n\n const handleShare = async (file: FileNode, { recheck }: { recheck: boolean }) => {\n const fileId = file.id;\n setSharingId(fileId);\n try {\n if (requiresShareCheck(file.visibility)) {\n // The mobile menu already checked on open; the desktop action checks on\n // click. Anything other than a settled outcome is checked again here.\n const known = shareAccess[fileId];\n const outcome =\n !recheck && (known === 'allowed' || known === 'unverified')\n ? known\n : await checkCanShare(fileId);\n if (outcome === 'failed') {\n throw new Error('Your share access could not be checked. Please try again.');\n }\n if (outcome === 'denied-restricted') {\n notify.error(SHARE_DENIED_TITLE, shareDeniedDescription(outcome));\n return;\n }\n }\n const result = await createShareLink({\n variables: { input: { resourceId: fileId, role: 'VIEWER' } },\n });\n const url = result.data?.createFileShareLink.shareUrl;\n if (!url) {\n throw new Error('No share link was returned.');\n }\n // The server minted a link, so any earlier denied state is stale.\n setAccess(fileId, 'allowed');\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(url);\n notify.success('Share link copied', 'A view-only link is on your clipboard.');\n } else {\n notify.info('Share link ready', url);\n }\n } catch (error) {\n const denial = shareDenialFor(error, file.visibility);\n if (denial) {\n // The mutation is the authoritative denial: the gateway's role check\n // (HTTP 403), or the service's per-file rule, e.g. a grant revoked\n // since the pre-check (HTTP 200 FORBIDDEN).\n setAccess(fileId, denial);\n notify.error(SHARE_DENIED_TITLE, shareDeniedDescription(denial));\n return;\n }\n notify.error(\n 'Could not create share link',\n error instanceof Error ? error.message : 'Please try again.'\n );\n } finally {\n setSharingId(null);\n }\n };\n\n const total = data?.getFiles.total ?? 0;\n const hasFiles = files.length > 0;\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between gap-2 space-y-0\">\n <CardTitle className=\"flex items-center gap-2 text-base\">\n <Paperclip className=\"h-4 w-4\" />\n Attachments\n {total ? (\n <span className=\"text-sm font-normal text-text-secondary\">\n {total} file{total === 1 ? '' : 's'}\n </span>\n ) : null}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <input\n ref={fileInputRef}\n type=\"file\"\n className=\"hidden\"\n onChange={(event) => void handleFileSelected(event)}\n aria-label={`Upload a file for ${entityType} ${entityId}`}\n />\n\n {loading && !hasFiles ? (\n <div className=\"space-y-2\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div\n key={i}\n className=\"h-14 rounded-lg bg-bg-sunken motion-safe:animate-vc-shimmer\"\n />\n ))}\n </div>\n ) : !hasFiles ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No files yet\"\n description=\"Upload photos, receipts, warranties or documents to keep them with this record.\"\n />\n ) : (\n <ul className=\"space-y-1.5\">\n {files.map((file) => {\n const isSharing = sharingId === file.id;\n const access = shareAccess[file.id];\n const isChecking = access === 'checking';\n const denial = isShareDenial(access) ? access : null;\n const isDenied = denial !== null;\n return (\n <li\n key={file.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-2.5\"\n >\n <FileBadge file={file} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium\" title={file.name}>\n {file.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {formatSize(Number(file.size))} · {relativeTime(file.createdAt)}\n </p>\n </div>\n {/* Desktop: inline actions. Mobile: kebab menu to avoid crowding. */}\n <div className=\"hidden items-center gap-1 sm:flex\">\n {file.downloadUrl ? (\n <a\n href={file.downloadUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"rounded-md p-1.5 text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={`Download ${file.name}`}\n title=\"Download\"\n >\n <Download className=\"h-4 w-4\" />\n </a>\n ) : null}\n {/* A denied action looks disabled (lock icon, muted, not-allowed\n cursor) but stays clickable so the next click asks again. */}\n <button\n type=\"button\"\n disabled={isSharing || isChecking}\n aria-disabled={isDenied ? true : undefined}\n data-share-state={isDenied ? 'denied' : undefined}\n onClick={() => void handleShare(file, { recheck: true })}\n className={`rounded-md p-1.5 transition-colors disabled:opacity-60 ${\n isDenied\n ? 'cursor-not-allowed text-text-tertiary opacity-60'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n aria-label={`Copy share link for ${file.name}`}\n title={\n denial\n ? `${SHARE_DENIED_TITLE}. ${shareDeniedDescription(denial)}`\n : 'Copy share link'\n }\n >\n {isSharing || isChecking ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : isDenied ? (\n <Lock className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <Link2 className=\"h-4 w-4\" />\n )}\n </button>\n </div>\n <div className=\"sm:hidden\">\n <DropdownMenu onOpenChange={(open) => handleMenuOpenChange(file, open)}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n className=\"rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n aria-label={`Actions for ${file.name}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {file.downloadUrl ? (\n <DropdownMenuItem asChild>\n <a href={file.downloadUrl} target=\"_blank\" rel=\"noreferrer\">\n <Download className=\"mr-2 h-4 w-4\" />\n Download\n </a>\n </DropdownMenuItem>\n ) : null}\n <DropdownMenuItem\n disabled={isSharing || isChecking || isDenied}\n onSelect={(e) => {\n e.preventDefault();\n void handleShare(file, { recheck: false });\n }}\n >\n {isChecking ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : isDenied ? (\n <Lock className=\"mr-2 h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <Link2 className=\"mr-2 h-4 w-4\" />\n )}\n {isChecking\n ? 'Checking share access…'\n : isDenied\n ? 'No share access'\n : 'Copy share link'}\n </DropdownMenuItem>\n {denial ? (\n <p className=\"max-w-56 px-2 py-1.5 text-xs text-text-secondary\">\n {shareDeniedDescription(denial)}\n </p>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n\n {/* Prominent full-width primary upload CTA (matches the reference concept). */}\n <Button\n className=\"w-full\"\n disabled={uploading}\n onClick={() => fileInputRef.current?.click()}\n >\n {uploading ? (\n <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n <Upload className=\"mr-2 h-4 w-4\" />\n )}\n {uploading ? 'Uploading…' : 'Upload files'}\n </Button>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;;AA6FA,IAAME,KAAkB,CAAG;;;;;;;;;;;;;;;;;GAmBrBC,KAA2B,CAAG;;;;;;;;;GAW9BC,KAA2B,CAAG;;;;;;;;;;GAY9BC,KAA6B,CAAG;;;;;;;GAShCC,IAA8B,CAAG;;;;;;;;AAwCvC,SAASC,EAAmB,GAAwD;CAClF,OAAOC,MAAe,aAAaA,MAAe;AACpD;AAEA,IAAMC,IAAqB,6BACrBC,IACJ,wJACIC,IACJ,4FACIC,IACJ;AAEF,SAASC,EAAc,GAAwD;CAC7E,OAAOC,MAAW,iBAAiBA,MAAW,uBAAuBA,MAAW;AAClF;AAEA,SAASC,EAAuB,GAA6B;CAC3D,QAAQC,GAAR;EACE,KAAK,eACH,OAAOL;EACT,KAAK,qBACH,OAAOD;EACT,KAAK,gBACH,OAAOE;CACX;AACF;AAOA,SAASK,EACP,GACA,GACoB;CACpB,IAAMC,IAASC,EAAmBC,CAAK;CAKvC,OAJIF,MAAW,YAAkB,gBAC7BA,MAAW,YACNX,EAAmBC,CAAU,IAAI,sBAAsB,iBAEzD;AACT;AAEA,SAASa,EAAW,GAAuB;CACzC,IAAI,CAACC,GAAO,OAAO;CACnB,IAAMC,IAAQ;EAAC;EAAK;EAAM;EAAM;CAAI,GAC9BC,IAAW,KAAK,IAAI,KAAK,MAAM,KAAK,IAAIF,CAAK,IAAI,KAAK,IAAI,IAAI,CAAC,GAAGC,EAAM,SAAS,CAAC;CAExF,OAAO,IADOD,IAAiB,QAAME,EAAQ,CAC7B,QAAQA,MAAa,IAAI,IAAI,CAAC,EAAE,GAAGD,EAAMC;AAC3D;AAEA,SAASC,EAAY,GAA0B;CAC7C,IAAMC,IAAMC,EAAS,YAAY,GAAG;CACpC,OAAOD,KAAO,IAAIC,EAAS,MAAMD,IAAM,CAAC,CAAC,CAAC,YAAY,IAAI;AAC5D;AAGA,SAASE,EAAa,GAAqB;CACzC,IAAMC,IAAO,IAAI,KAAKC,CAAG,CAAC,CAAC,QAAQ;CACnC,IAAI,OAAO,MAAMD,CAAI,GAAG,OAAO;CAC/B,IAAME,IAAO,KAAK,IAAI,IAAIF,GACpBG,IAAM,KAAK,MAAMD,IAAO,GAAI;CAClC,IAAIC,IAAM,IAAI,OAAO;CACrB,IAAMC,IAAM,KAAK,MAAMD,IAAM,EAAE;CAC/B,IAAIC,IAAM,IAAI,OAAO,GAAGA,EAAI;CAC5B,IAAMC,IAAK,KAAK,MAAMD,IAAM,EAAE;CAC9B,IAAIC,IAAK,IAAI,OAAO,GAAGA,EAAG,KAAKA,MAAO,IAAI,KAAK,IAAI;CACnD,IAAMC,IAAM,KAAK,MAAMD,IAAK,EAAE;CAE9B,OADIC,IAAM,KAAW,GAAGA,EAAI,MAAMA,MAAQ,IAAI,KAAK,IAAI,QAChD,IAAI,KAAKL,CAAG,CAAC,CAAC,mBAAmB;AAC1C;AAMA,SAASM,EAAO,GAA0B;CACxC,IAAMC,KAAQC,EAAK,YAAY,GAAA,CAAI,YAAY,GACzCC,KAAOD,EAAK,aAAab,EAAYa,EAAK,IAAI,EAAA,CAAG,YAAY;CAgCnE,OA9BED,EAAK,WAAW,QAAQ,KACxB;EAAC;EAAO;EAAO;EAAQ;EAAO;EAAQ;EAAO;EAAQ;CAAK,CAAC,CAAC,SAASE,CAAG,IAEjE,UACLF,EAAK,WAAW,QAAQ,KAAK;EAAC;EAAO;EAAO;EAAQ;EAAO;CAAK,CAAC,CAAC,SAASE,CAAG,IACzE,UACLF,EAAK,WAAW,QAAQ,KAAK;EAAC;EAAO;EAAO;EAAO;EAAO;CAAM,CAAC,CAAC,SAASE,CAAG,IACzE,UACLF,MAAS,qBAAqBE,MAAQ,QAAc,QACpD;EAAC;EAAO;EAAQ;EAAO;EAAO;CAAK,CAAC,CAAC,SAASA,CAAG,IAAU,UAC3D;EAAC;EAAO;EAAO;EAAM;EAAO;EAAM;CAAK,CAAC,CAAC,SAASA,CAAG,IAAU,YAEjE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,CAAC,SAASA,CAAG,IAEP,SACL;EAAC;EAAO;EAAQ;EAAO;EAAM;EAAO;EAAO;CAAO,CAAC,CAAC,SAASA,CAAG,IAAU,QACvE;AACT;AAEA,IAAMC,IAGF;CACF,OAAO;EACL,MAAMxC;EACN,WAAW;EACX,WAAW;CACb;CACA,KAAK;EACH,MAAM0C;EACN,WAAW;EACX,WAAW;CACb;CACA,OAAO;EACL,MAAMC;EACN,WAAW;EACX,WAAW;CACb;CACA,KAAK;EACH,MAAMD;EACN,WAAW;EACX,WAAW;CACb;CACA,SAAS;EACP,MAAME;EACN,WAAW;EACX,WAAW;CACb;CACA,MAAM;EACJ,MAAMC;EACN,WAAW;EACX,WAAW;CACb;CACA,OAAO;EACL,MAAMC;EACN,WAAW;EACX,WAAW;CACb;CACA,OAAO;EACL,MAAMC;EACN,WAAW;EACX,WAAW;CACb;CACA,OAAO;EAAE,MAAM9C;EAAU,WAAW;EAAuB,WAAW;CAAe;AACvF;AAGA,SAASgD,GAAU,EAAE,WAA4B;CAC/C,IAAMC,IAAOd,EAAOE,CAAI,GAClB,EAAE,SAAM,cAAW,iBAAcE,EAAWU;CAWlD,OAVIA,MAAS,WAAWZ,EAAK,cAEzB,kBAAC,OAAD;EACE,KAAKA,EAAK;EACV,KAAI;EACJ,SAAQ;EACR,WAAU;CACX,CAAA,IAIH,kBAAC,QAAD;EACE,WAAW,kEAAkEa;EAC7E,eAAY;EAEZ,UAAA,kBAACC,GAAD,EAAM,WAAW,WAAWC,IAAc,CAAA;CACtC,CAAA;AAEV;AAEA,SAAgBC,EAAiB,EAAE,eAAY,eAAmC;CAChF,IAAMC,IAAeC,EAAyB,IAAI,GAC5C,CAACC,GAAWC,KAAgBC,EAAS,EAAK,GAC1C,CAACC,GAAWC,KAAgBF,EAAwB,IAAI,GACxD,CAACG,GAAaC,KAAkBJ,EAAsC,CAAC,CAAC,GACxEK,IAAeC,GAAgB,GAE/B,EAAE,SAAM,YAAS,eAAYC,EAAyBhE,IAAiB;EAC3E,WAAW;GACT,YAAY;IAAE,OAAO;IAAI,QAAQ;GAAE;GACnC,MAAM;IAAE,OAAO;IAAc,OAAO;GAAO;EAC7C;EACA,aAAa;EACb,aAAa;CACf,CAAC,GAEK,CAACiE,KAAkBC,EAEtBjE,EAAwB,GACrB,CAACkE,KAAkBD,EAAYhE,EAAwB,GACvD,CAACkE,KAAmBF,EAEvB/D,EAA0B,GAEvBkE,IAAQC,QAAcC,GAAM,SAAS,SAAS,CAAC,GAAG,CAACA,CAAI,CAAC,GAExDC,KAAqB,OAAO,MAA+C;EAC/E,IAAMpC,IAAOqC,EAAM,OAAO,QAAQ;EAGlC,IADA,EAAM,OAAO,QAAQ,IAChBrC,GAEL;KAAa,EAAI;GACjB,IAAI;IACF,IAAMsC,IAAY,MAAMT,EAAe,EACrC,WAAW,EACT,OAAO;KACL,UAAU7B,EAAK;KACf,WAAWb,EAAYa,EAAK,IAAI;KAChC,UAAUA,EAAK;KACf,UAAUA,EAAK,QAAQ;KACvB,YAAY;IACd,EACF,EACF,CAAC,GAEKuC,IAAYD,EAAU,MAAM,eAAe,WAC3CE,IAAYF,EAAU,MAAM,eAAe,QAAQ;IACzD,IAAI,CAACC,KAAa,CAACC,GACjB,MAAU,MAAM,kCAAkC;IAGpD,IAAMC,IAAc,MAAM,MAAMF,GAAW;KACzC,QAAQ;KACR,MAAMvC;KACN,SAAS,EAAE,gBAAgBA,EAAK,QAAQ,2BAA2B;IACrE,CAAC;IACD,IAAI,CAACyC,EAAY,IACf,MAAU,MAAM,qCAAqCA,EAAY,OAAO,GAAG;IAQ7E,AALA,MAAMV,EAAe,EACnB,WAAW,EAAE,OAAO;KAAE;KAAW,UAAU/B,EAAK;IAAK,EAAE,EACzD,CAAC,GAED0C,EAAO,QAAQ,iBAAiB,GAAG1C,EAAK,KAAK,kCAAkC,GAC/E,MAAM2C,EAAQ;GAChB,SAAS7D,GAAO;IACd,EAAO,MACL,iBACAA,aAAiB,QAAQA,EAAM,UAAU,iCAC3C;GACF,UAAU;IACR,EAAa,EAAK;GACpB;EA1CiB;CA2CnB,GAEM8D,IAAYC,GAAa,GAAgB,MAA+B;EAC5E,GAAgB,MAAY;GAC1B,IAAMC,IAAO,EAAE,GAAGC,EAAQ;GAG1B,OAFIvE,IAAQ,EAAKwE,KAAUxE,IACtB,OAAOsE,EAAKE,IACVF;EACT,CAAC;CACH,GAAG,CAAC,CAAC,GAUCG,IAAgBJ,EACpB,OAAO,MAA+C;EACpD,EAAUG,GAAQ,UAAU;EAC5B,IAAI;GAMF,IAAME,KACJ,MANmBxB,EAAa,MAAiC;IACjE,OAAO1D;IACP,WAAW,EAAE,UAAO;IACpB,aAAa;GACf,CAAC,EAAA,CAEQ,MAAM,uBAAuB,aAAa,KAAO,YAAY;GAEtE,OADA4E,EAAUI,GAAQE,CAAO,GAClBA;EACT,SAASpE,GAAO;GAMd,OALIqE,EAAiBrE,CAAK,KACxB8D,EAAUI,GAAQ,YAAY,GACvB,iBAETJ,EAAUI,GAAQ,IAAI,GACf;EACT;CACF,GACA,CAACtB,GAAckB,CAAS,CAC1B,GAEMQ,MAAwB,GAAgB,MAAkB;EAC9D,AAAKC,MACDpF,EAAmB+B,EAAK,UAAU,IAChCwB,EAAYxB,EAAK,QAAQ,cAAYiD,EAAmBjD,EAAK,EAAE,IAC1DzB,EAAciD,EAAYxB,EAAK,GAAG,KAG3C4C,EAAU5C,EAAK,IAAI,IAAI;CAE3B,GAEMsD,IAAc,OAAO,GAAgB,EAAE,iBAAoC;EAC/E,IAAMN,IAAShD,EAAK;EACpB,EAAagD,CAAM;EACnB,IAAI;GACF,IAAI/E,EAAmB+B,EAAK,UAAU,GAAG;IAGvC,IAAMuD,IAAQ/B,EAAYwB,IACpBE,IACJ,CAACM,MAAYD,MAAU,aAAaA,MAAU,gBAC1CA,IACA,MAAMN,EAAcD,CAAM;IAChC,IAAIE,MAAY,UACd,MAAU,MAAM,2DAA2D;IAE7E,IAAIA,MAAY,qBAAqB;KACnC,EAAO,MAAM/E,GAAoBM,EAAuByE,CAAO,CAAC;KAChE;IACF;GACF;GAIA,IAAMO,KAAM,MAHSzB,EAAgB,EACnC,WAAW,EAAE,OAAO;IAAE,YAAYgB;IAAQ,MAAM;GAAS,EAAE,EAC7D,CAAC,EAAA,CACkB,MAAM,oBAAoB;GAC7C,IAAI,CAACS,GACH,MAAU,MAAM,6BAA6B;GAI/C,AADAb,EAAUI,GAAQ,SAAS,GACvB,UAAU,WAAW,aACvB,MAAM,UAAU,UAAU,UAAUS,CAAG,GACvCf,EAAO,QAAQ,qBAAqB,wCAAwC,KAE5EA,EAAO,KAAK,oBAAoBe,CAAG;EAEvC,SAAS3E,GAAO;GACd,IAAMJ,IAASC,EAAeG,GAAOkB,EAAK,UAAU;GACpD,IAAItB,GAAQ;IAKV,AADAkE,EAAUI,GAAQtE,CAAM,GACxBgE,EAAO,MAAMvE,GAAoBM,EAAuBC,CAAM,CAAC;IAC/D;GACF;GACA,EAAO,MACL,+BACAI,aAAiB,QAAQA,EAAM,UAAU,mBAC3C;EACF,UAAU;GACR,EAAa,IAAI;EACnB;CACF,GAEM4E,IAAQvB,GAAM,SAAS,SAAS,GAChCwB,IAAW1B,EAAM,SAAS;CAEhC,OACE,kBAAC2B,GAAD,EAAA,UAAA,CACE,kBAACC,GAAD;EAAY,WAAU;EACpB,UAAA,kBAACC,GAAD;GAAW,WAAU;GAArB,UAAA;IACE,kBAACC,GAAD,EAAW,WAAU,UAAW,CAAA;IAAC;IAEhCL,IACC,kBAAC,QAAD;KAAM,WAAU;KAAhB,UAAA;MACGA;MAAM;MAAMA,MAAU,IAAI,KAAK;KAC5B;IACJ,CAAA,IAAA;GACK;;CACD,CAAA,GACZ,kBAACM,GAAD;EAAa,WAAU;EAAvB,UAAA;GACE,kBAAC,SAAD;IACE,KAAK/C;IACL,MAAK;IACL,WAAU;IACV,WAAW,MAAU,KAAKmB,GAAmBC,CAAK;IAClD,cAAY,qBAAqB4B,EAAW,GAAGC;GAChD,CAAA;GAEAC,KAAW,CAACR,IACX,kBAAC,OAAD;IAAK,WAAU;IACZ,UAAA,MAAM,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,MACjC,kBAAC,OAAD,EAEE,WAAU,8DACX,GAFMS,CAEN,CACF;GACE,CAAA,IACFT,IAOH,kBAAC,MAAD;IAAI,WAAU;IACX,UAAA1B,EAAM,KAAK,MAAS;KACnB,IAAMoC,IAAY/C,MAActB,EAAK,IAC/BxB,IAASgD,EAAYxB,EAAK,KAC1BsE,IAAa9F,MAAW,YACxBE,IAASH,EAAcC,CAAM,IAAIA,IAAS,MAC1C+F,IAAW7F,MAAW;KAC5B,OACE,kBAAC,MAAD;MAEE,WAAU;MAFZ,UAAA;OAIE,kBAACiC,IAAD,EAAiB,QAAO,CAAA;OACxB,kBAAC,OAAD;QAAK,WAAU;QAAf,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;SAA+B,OAAOX,EAAK;SACrD,UAAAA,EAAK;QACL,CAAA,GACH,kBAAC,KAAD;SAAG,WAAU;SAAb,UAAA;UACGjB,EAAW,OAAOiB,EAAK,IAAI,CAAC;UAAE;UAAIV,EAAaU,EAAK,SAAS;SAC7D;QACA,CAAA,CAAA;;OAEL,kBAAC,OAAD;QAAK,WAAU;QAAf,UAAA,CACGA,EAAK,cACJ,kBAAC,KAAD;SACE,MAAMA,EAAK;SACX,QAAO;SACP,KAAI;SACJ,WAAU;SACV,cAAY,YAAYA,EAAK;SAC7B,OAAM;SAEN,UAAA,kBAACwE,GAAD,EAAU,WAAU,UAAW,CAAA;QAC9B,CAAA,IACD,MAGJ,kBAAC,UAAD;SACE,MAAK;SACL,UAAUH,KAAaC;SACvB,iBAAeC,IAAW,KAAO,KAAA;SACjC,oBAAkBA,IAAW,WAAW,KAAA;SACxC,eAAe,KAAKjB,EAAYtD,GAAM,EAAE,SAAS,GAAK,CAAC;SACvD,WAAW,0DACTuE,IACI,qDACA;SAEN,cAAY,uBAAuBvE,EAAK;SACxC,OACEtB,IACI,GAAGP,EAAmB,IAAIM,EAAuBC,CAAM,MACvD;SAGL,UAAA2F,KAAaC,IACZ,kBAACG,GAAD,EAAS,WAAU,uBAAwB,CAAA,IACzCF,IACF,kBAACG,GAAD;UAAM,WAAU;UAAU,eAAY;SAAQ,CAAA,IAE9C,kBAACC,GAAD,EAAO,WAAU,UAAW,CAAA;QAExB,CAAA,CACL;;OACL,kBAAC,OAAD;QAAK,WAAU;QACb,UAAA,kBAACC,GAAD;SAAc,eAAe,MAASxB,GAAqBpD,GAAMqD,CAAI;SAArE,UAAA,CACE,kBAACwB,IAAD;UAAqB,SAAA;UACnB,UAAA,kBAAC,UAAD;WACE,MAAK;WACL,WAAU;WACV,cAAY,eAAe7E,EAAK;WAEhC,UAAA,kBAAC8E,GAAD,EAAgB,WAAU,UAAW,CAAA;UAC/B,CAAA;SACW,CAAA,GACrB,kBAACC,GAAD;UAAqB,OAAM;UAA3B,UAAA;WACG/E,EAAK,cACJ,kBAACgF,GAAD;YAAkB,SAAA;YAChB,UAAA,kBAAC,KAAD;aAAG,MAAMhF,EAAK;aAAa,QAAO;aAAS,KAAI;aAA/C,UAAA,CACE,kBAACwE,GAAD,EAAU,WAAU,eAAgB,CAAA,GAAC,UAEpC;;WACa,CAAA,IAChB;WACJ,kBAACQ,GAAD;YACE,UAAUX,KAAaC,KAAcC;YACrC,WAAW,MAAM;aAEf,AADAU,EAAE,eAAe,GACjB3B,EAAiBtD,GAAM,EAAE,SAAS,GAAM,CAAC;YAC3C;YALF,UAAA,CAOGsE,IACC,kBAACG,GAAD,EAAS,WAAU,4BAA6B,CAAA,IAC9CF,IACF,kBAACG,GAAD;aAAM,WAAU;aAAe,eAAY;YAAQ,CAAA,IAEnD,kBAACC,GAAD,EAAO,WAAU,eAAgB,CAAA,GAElCL,IACG,2BACAC,IACE,oBACA,iBACU;;WACjB7F,IACC,kBAAC,KAAD;YAAG,WAAU;YACV,UAAAD,EAAuBC,CAAM;WAC7B,CAAA,IACD;UACe;SACT,CAAA,CAAA;;OACX,CAAA;MACH;KAvGG,GAAAsB,EAAK,EAuGR;IAER,CAAC;GACC,CAAA,IAzHJ,kBAACkF,IAAD;IACE,cAAa;IACb,OAAM;IACN,aAAY;GACb,CAAA;GAyHH,kBAACC,IAAD;IACE,WAAU;IACV,UAAUhE;IACV,eAAeF,EAAa,SAAS,MAAM;IAH7C,UAAA,CAKGE,IACC,kBAACsD,GAAD,EAAS,WAAU,4BAA6B,CAAA,IAEhD,kBAACW,IAAD,EAAQ,WAAU,eAAgB,CAAA,GAEnCjE,IAAY,eAAe,cACtB;;EACG;CACT,CAAA,CAAA,EAAA,CAAA;AAEV"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { FinanceRegister } from '../utils/financeExport';
|
|
2
|
+
export declare function FinanceRegisterExportDialog({ open, onOpenChange, register, }: {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
register: FinanceRegister;
|
|
6
|
+
}): import('react').JSX.Element;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { AssetHandlerExportBudgetsDocument as e, AssetHandlerExportInvoicesDocument as t, AssetHandlerExportTransactionsDocument as n } from "../../generated/wspace-operations.js";
|
|
2
|
+
import { useAssetHandler as r } from "../providers/AssetHandlerProvider.js";
|
|
3
|
+
import { downloadCsv as i } from "../utils/csv.js";
|
|
4
|
+
import { FinanceExportError as a, exportFinanceRegister as o } from "../utils/financeExport.js";
|
|
5
|
+
import { notify as s } from "../utils/toast.js";
|
|
6
|
+
import { useLayoutEffect as c, useMemo as l, useRef as u, useState as d } from "react";
|
|
7
|
+
import { useApolloClient as f } from "@apollo/client/react";
|
|
8
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
9
|
+
import { Button as h, ResponsiveDialog as g, ResponsiveDialogContent as _, ResponsiveDialogDescription as v, ResponsiveDialogFooter as y, ResponsiveDialogHeader as b, ResponsiveDialogTitle as x } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
// src/assethandler/components/FinanceRegisterExportDialog.tsx
|
|
11
|
+
function S({ open: S, onOpenChange: C, register: w }) {
|
|
12
|
+
let T = f(), { workspaceId: E, tenantId: D, currentUser: O, authToken: k, workspaceToken: A } = r(), j = O?.id, M = l(() => ({
|
|
13
|
+
workspaceId: E,
|
|
14
|
+
tenantId: D,
|
|
15
|
+
actorId: j,
|
|
16
|
+
authToken: k,
|
|
17
|
+
workspaceToken: A,
|
|
18
|
+
register: w,
|
|
19
|
+
client: T,
|
|
20
|
+
open: S
|
|
21
|
+
}), [
|
|
22
|
+
E,
|
|
23
|
+
D,
|
|
24
|
+
j,
|
|
25
|
+
k,
|
|
26
|
+
A,
|
|
27
|
+
w,
|
|
28
|
+
T,
|
|
29
|
+
S
|
|
30
|
+
]), N = u(M), P = u(null), [F, I] = d(null), L = F?.scope === M && F.running, R = F?.scope === M ? F.count : 0, z = F?.scope === M ? F.failure : null, B = (e) => I((t) => t?.scope === M ? {
|
|
31
|
+
...t,
|
|
32
|
+
running: e
|
|
33
|
+
} : t), V = (e) => I((t) => t?.scope === M ? {
|
|
34
|
+
...t,
|
|
35
|
+
count: e
|
|
36
|
+
} : t), H = (e) => I((t) => t?.scope === M ? {
|
|
37
|
+
...t,
|
|
38
|
+
failure: e
|
|
39
|
+
} : t), U = () => {
|
|
40
|
+
P.current?.abort(), P.current = null, B(!1);
|
|
41
|
+
};
|
|
42
|
+
c(() => (N.current = M, () => {
|
|
43
|
+
P.current?.abort(), P.current = null;
|
|
44
|
+
}), [M, S]);
|
|
45
|
+
let W = async () => {
|
|
46
|
+
if (!S || P.current) return;
|
|
47
|
+
let r = new AbortController();
|
|
48
|
+
P.current = r;
|
|
49
|
+
let c = N.current, l = () => P.current === r && c === N.current;
|
|
50
|
+
I({
|
|
51
|
+
scope: c,
|
|
52
|
+
running: !0,
|
|
53
|
+
count: 0,
|
|
54
|
+
failure: null
|
|
55
|
+
});
|
|
56
|
+
let u = setTimeout(() => r.abort(), 12e4);
|
|
57
|
+
try {
|
|
58
|
+
let c = await o({
|
|
59
|
+
register: w,
|
|
60
|
+
workspaceId: E ?? "",
|
|
61
|
+
signal: r.signal,
|
|
62
|
+
isCurrentScope: l,
|
|
63
|
+
onProgress: (e) => {
|
|
64
|
+
l() && V(e);
|
|
65
|
+
},
|
|
66
|
+
download: i,
|
|
67
|
+
fetchPage: async (r, i) => {
|
|
68
|
+
let o = {
|
|
69
|
+
fetchOptions: { signal: i },
|
|
70
|
+
queryDeduplication: !1
|
|
71
|
+
}, s = { pagination: r };
|
|
72
|
+
if (w === "budgets") {
|
|
73
|
+
let t = await T.query({
|
|
74
|
+
query: e,
|
|
75
|
+
variables: s,
|
|
76
|
+
context: o,
|
|
77
|
+
fetchPolicy: "no-cache",
|
|
78
|
+
errorPolicy: "none"
|
|
79
|
+
});
|
|
80
|
+
if (t.error || !t.data?.budgets) throw new a("page");
|
|
81
|
+
return t.data.budgets.map((e) => ({
|
|
82
|
+
id: e.id,
|
|
83
|
+
workspaceId: e.workspaceId,
|
|
84
|
+
cells: [
|
|
85
|
+
e.name,
|
|
86
|
+
e.department,
|
|
87
|
+
e.category,
|
|
88
|
+
e.period,
|
|
89
|
+
e.allocated,
|
|
90
|
+
e.spent,
|
|
91
|
+
e.remaining,
|
|
92
|
+
e.status.toLowerCase()
|
|
93
|
+
]
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
if (w === "invoices") {
|
|
97
|
+
let e = await T.query({
|
|
98
|
+
query: t,
|
|
99
|
+
variables: s,
|
|
100
|
+
context: o,
|
|
101
|
+
fetchPolicy: "no-cache",
|
|
102
|
+
errorPolicy: "none"
|
|
103
|
+
});
|
|
104
|
+
if (e.error || !e.data?.invoices) throw new a("page");
|
|
105
|
+
return e.data.invoices.map((e) => ({
|
|
106
|
+
id: e.id,
|
|
107
|
+
workspaceId: e.workspaceId,
|
|
108
|
+
cells: [
|
|
109
|
+
e.number,
|
|
110
|
+
e.vendor,
|
|
111
|
+
e.amount,
|
|
112
|
+
e.currency,
|
|
113
|
+
e.issueDate,
|
|
114
|
+
e.dueDate,
|
|
115
|
+
e.status.toLowerCase()
|
|
116
|
+
]
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
let c = await T.query({
|
|
120
|
+
query: n,
|
|
121
|
+
variables: s,
|
|
122
|
+
context: o,
|
|
123
|
+
fetchPolicy: "no-cache",
|
|
124
|
+
errorPolicy: "none"
|
|
125
|
+
});
|
|
126
|
+
if (c.error || !c.data?.transactions) throw new a("page");
|
|
127
|
+
return c.data.transactions.map((e) => ({
|
|
128
|
+
id: e.id,
|
|
129
|
+
workspaceId: e.workspaceId,
|
|
130
|
+
cells: [
|
|
131
|
+
e.id,
|
|
132
|
+
e.type.toLowerCase(),
|
|
133
|
+
e.description,
|
|
134
|
+
e.department,
|
|
135
|
+
e.amount,
|
|
136
|
+
e.currency,
|
|
137
|
+
e.status.toLowerCase(),
|
|
138
|
+
e.occurredAt
|
|
139
|
+
]
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (!l()) return;
|
|
144
|
+
c === 0 ? s.warning("Nothing to export", `There are no ${w} to export.`) : s.success("Register exported", `${c} ${w} exported to CSV.`), C(!1);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
if (!l()) return;
|
|
147
|
+
H(e instanceof a && e.reason === "capacity" ? "This register exceeds the browser limit of 10,000 records or 10 MiB. No file was downloaded. Use workspace Exports for a larger export with the permissions required there." : r.signal.aborted ? "Export timed out. No partial file was downloaded. Try again or use workspace Exports." : "The complete register could not be read. No partial file was downloaded. Check your access and connection, then try again.");
|
|
148
|
+
} finally {
|
|
149
|
+
clearTimeout(u), P.current === r && (P.current = null, B(!1));
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
return /* @__PURE__ */ p(g, {
|
|
153
|
+
open: S,
|
|
154
|
+
onOpenChange: (e) => {
|
|
155
|
+
e || U(), C(e);
|
|
156
|
+
},
|
|
157
|
+
children: /* @__PURE__ */ m(_, { children: [
|
|
158
|
+
/* @__PURE__ */ m(b, { children: [/* @__PURE__ */ m(x, { children: [
|
|
159
|
+
"Export entire ",
|
|
160
|
+
w,
|
|
161
|
+
" register"
|
|
162
|
+
] }), /* @__PURE__ */ p(v, { children: "Export all records in this workspace, not just the visible page. List search and status/type filters do not limit this export. Pages are read sequentially; canceling or changing workspace discards the export without downloading a partial file." })] }),
|
|
163
|
+
/* @__PURE__ */ p("p", {
|
|
164
|
+
role: "status",
|
|
165
|
+
"aria-live": "polite",
|
|
166
|
+
children: L ? `Collecting ${R} records…` : "Ready to read the complete register."
|
|
167
|
+
}),
|
|
168
|
+
z && /* @__PURE__ */ p("p", {
|
|
169
|
+
role: "alert",
|
|
170
|
+
className: "text-status-error-text",
|
|
171
|
+
children: z
|
|
172
|
+
}),
|
|
173
|
+
/* @__PURE__ */ m(y, { children: [/* @__PURE__ */ p(h, {
|
|
174
|
+
variant: "outline",
|
|
175
|
+
onClick: () => {
|
|
176
|
+
U(), C(!1);
|
|
177
|
+
},
|
|
178
|
+
children: "Cancel"
|
|
179
|
+
}), /* @__PURE__ */ p(h, {
|
|
180
|
+
disabled: L || !E,
|
|
181
|
+
onClick: () => void W(),
|
|
182
|
+
children: L ? "Exporting…" : "Download CSV"
|
|
183
|
+
})] })
|
|
184
|
+
] })
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export { S as FinanceRegisterExportDialog };
|
|
188
|
+
|
|
189
|
+
//# sourceMappingURL=FinanceRegisterExportDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FinanceRegisterExportDialog.js","names":["FinanceRegisterExportDialog","client","useApolloClient","useAssetHandler","actorId","currentUser","scope","useMemo","workspaceId","tenantId","authToken","workspaceToken","register","open","current","useRef","active","progress","setProgress","useState","running","count","failure","setRunning","previous","value","setCount","setFailure","cancel","start","controller","startedScope","isCurrentScope","deadline","exported","exportFinanceRegister","downloadCsv","context","variables","result","AssetHandlerExportBudgetsDocument","FinanceExportError","row","AssetHandlerExportInvoicesDocument","AssetHandlerExportTransactionsDocument","notify","onOpenChange","error","ResponsiveDialog","ResponsiveDialogContent","ResponsiveDialogHeader","ResponsiveDialogTitle","ResponsiveDialogDescription","ResponsiveDialogFooter","Button"],"sources":["../../../src/assethandler/components/FinanceRegisterExportDialog.tsx"],"sourcesContent":["import { useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport {\n Button,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n AssetHandlerExportBudgetsDocument,\n AssetHandlerExportInvoicesDocument,\n AssetHandlerExportTransactionsDocument,\n type AssetHandlerExportBudgetsQuery,\n type AssetHandlerExportInvoicesQuery,\n type AssetHandlerExportTransactionsQuery,\n} from '../../generated/wspace-operations';\nimport { useAssetHandler } from '../providers/AssetHandlerProvider';\nimport { downloadCsv } from '../utils/csv';\nimport {\n exportFinanceRegister,\n FinanceExportError,\n type FinanceRegister,\n} from '../utils/financeExport';\nimport { notify } from '../utils/toast';\n\nexport function FinanceRegisterExportDialog({\n open,\n onOpenChange,\n register,\n}: {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n register: FinanceRegister;\n}) {\n const client = useApolloClient();\n const { workspaceId, tenantId, currentUser, authToken, workspaceToken } = useAssetHandler();\n const actorId = currentUser?.id;\n const scope = useMemo(\n () => ({ workspaceId, tenantId, actorId, authToken, workspaceToken, register, client, open }),\n [workspaceId, tenantId, actorId, authToken, workspaceToken, register, client, open]\n );\n const current = useRef(scope);\n const active = useRef<AbortController | null>(null);\n const [progress, setProgress] = useState<{\n scope: typeof scope;\n running: boolean;\n count: number;\n failure: string | null;\n } | null>(null);\n const running = progress?.scope === scope && progress.running;\n const count = progress?.scope === scope ? progress.count : 0;\n const failure = progress?.scope === scope ? progress.failure : null;\n const setRunning = (value: boolean) =>\n setProgress((previous) =>\n previous?.scope === scope ? { ...previous, running: value } : previous\n );\n const setCount = (value: number) =>\n setProgress((previous) =>\n previous?.scope === scope ? { ...previous, count: value } : previous\n );\n const setFailure = (value: string) =>\n setProgress((previous) =>\n previous?.scope === scope ? { ...previous, failure: value } : previous\n );\n\n const cancel = () => {\n active.current?.abort();\n active.current = null;\n setRunning(false);\n };\n useLayoutEffect(() => {\n // Changing actor, workspace, tenant, credentials, client or tab invalidates\n // every in-flight result before it can reach the download callback.\n current.current = scope;\n return () => {\n active.current?.abort();\n active.current = null;\n };\n }, [scope, open]);\n\n const start = async () => {\n if (!open || active.current) return;\n const controller = new AbortController();\n active.current = controller;\n const startedScope = current.current;\n const isCurrentScope = () => active.current === controller && startedScope === current.current;\n setProgress({ scope: startedScope, running: true, count: 0, failure: null });\n // A whole-operation deadline also bounds a server that never settles.\n const deadline = setTimeout(() => controller.abort(), 120_000);\n try {\n const exported = await exportFinanceRegister({\n register,\n workspaceId: workspaceId ?? '',\n signal: controller.signal,\n isCurrentScope,\n onProgress: (value) => {\n if (isCurrentScope()) setCount(value);\n },\n download: downloadCsv,\n fetchPage: async (pagination, signal) => {\n // A replacement run must not reuse an aborted query or another scope's\n // identical operation through Apollo's in-flight deduplication.\n const context = { fetchOptions: { signal }, queryDeduplication: false };\n const variables = { pagination };\n if (register === 'budgets') {\n const result = await client.query<AssetHandlerExportBudgetsQuery>({\n query: AssetHandlerExportBudgetsDocument,\n variables,\n context,\n fetchPolicy: 'no-cache',\n errorPolicy: 'none',\n });\n if (result.error || !result.data?.budgets) throw new FinanceExportError('page');\n return result.data.budgets.map((row) => ({\n id: row.id,\n workspaceId: row.workspaceId,\n cells: [\n row.name,\n row.department,\n row.category,\n row.period,\n row.allocated,\n row.spent,\n row.remaining,\n row.status.toLowerCase(),\n ],\n }));\n }\n if (register === 'invoices') {\n const result = await client.query<AssetHandlerExportInvoicesQuery>({\n query: AssetHandlerExportInvoicesDocument,\n variables,\n context,\n fetchPolicy: 'no-cache',\n errorPolicy: 'none',\n });\n if (result.error || !result.data?.invoices) throw new FinanceExportError('page');\n return result.data.invoices.map((row) => ({\n id: row.id,\n workspaceId: row.workspaceId,\n cells: [\n row.number,\n row.vendor,\n row.amount,\n row.currency,\n row.issueDate,\n row.dueDate,\n row.status.toLowerCase(),\n ],\n }));\n }\n const result = await client.query<AssetHandlerExportTransactionsQuery>({\n query: AssetHandlerExportTransactionsDocument,\n variables,\n context,\n fetchPolicy: 'no-cache',\n errorPolicy: 'none',\n });\n if (result.error || !result.data?.transactions) throw new FinanceExportError('page');\n return result.data.transactions.map((row) => ({\n id: row.id,\n workspaceId: row.workspaceId,\n cells: [\n row.id,\n row.type.toLowerCase(),\n row.description,\n row.department,\n row.amount,\n row.currency,\n row.status.toLowerCase(),\n row.occurredAt,\n ],\n }));\n },\n });\n if (!isCurrentScope()) return;\n if (exported === 0)\n notify.warning('Nothing to export', `There are no ${register} to export.`);\n else notify.success('Register exported', `${exported} ${register} exported to CSV.`);\n onOpenChange(false);\n } catch (error) {\n if (!isCurrentScope()) return;\n setFailure(\n error instanceof FinanceExportError && error.reason === 'capacity'\n ? 'This register exceeds the browser limit of 10,000 records or 10 MiB. No file was downloaded. Use workspace Exports for a larger export with the permissions required there.'\n : controller.signal.aborted\n ? 'Export timed out. No partial file was downloaded. Try again or use workspace Exports.'\n : 'The complete register could not be read. No partial file was downloaded. Check your access and connection, then try again.'\n );\n } finally {\n clearTimeout(deadline);\n if (active.current === controller) {\n active.current = null;\n setRunning(false);\n }\n }\n };\n\n return (\n <ResponsiveDialog\n open={open}\n onOpenChange={(value) => {\n if (!value) cancel();\n onOpenChange(value);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Export entire {register} register</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Export all records in this workspace, not just the visible page. List search and\n status/type filters do not limit this export. Pages are read sequentially; canceling or\n changing workspace discards the export without downloading a partial file.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <p role=\"status\" aria-live=\"polite\">\n {running ? `Collecting ${count} records…` : 'Ready to read the complete register.'}\n </p>\n {failure && (\n <p role=\"alert\" className=\"text-status-error-text\">\n {failure}\n </p>\n )}\n <ResponsiveDialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => {\n cancel();\n onOpenChange(false);\n }}\n >\n Cancel\n </Button>\n <Button disabled={running || !workspaceId} onClick={() => void start()}>\n {running ? 'Exporting…' : 'Download CSV'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|