feat(documents): universal upload-with-fields UI wiring (B3 #11)
Backend foundations were already in place ('generic' CustomDocumentType,
storage-path routing). This wires the UI surface across Documents Hub +
entity file tabs.
- UploadForSigningDialog: interestId now string | null; new entity?,
folderId?, onCreated? props. Generic path POSTs to new endpoint
/api/v1/upload-for-signing; interest-scoped paths unchanged.
- uploadDocumentForSigning service: interestId nullable; skips interest
lookup, pipeline-stage advance, doc-status flip on the generic path.
Routes file FK + auto-filed folder via either interest.clientId or the
caller-supplied entity. Validation enforces the matching invariant
(generic must be interestId=null, type-specific must carry one).
- New menu item in NewDocumentMenu ("Upload & send for signature") on
Documents Hub root + folder views.
- Upload & send-for-signature button on ClientFilesTab + CompanyFilesTab,
gated by documents.send_for_signing.
Existing unit tests for the service still pass (validation paths unchanged).
This commit is contained in:
@@ -2,11 +2,14 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Pen } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { FileGrid } from '@/components/files/file-grid';
|
||||
import { FileUploadZone } from '@/components/files/file-upload-zone';
|
||||
import { FilePreviewDialog } from '@/components/files/file-preview-dialog';
|
||||
import { PermissionGate } from '@/components/shared/permission-gate';
|
||||
import { UploadForSigningDialog } from '@/components/documents/upload-for-signing-dialog';
|
||||
import { usePaginatedQuery } from '@/hooks/use-paginated-query';
|
||||
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
||||
import { useConfirmation } from '@/hooks/use-confirmation';
|
||||
@@ -21,6 +24,7 @@ interface ClientFilesTabProps {
|
||||
export function ClientFilesTab({ clientId }: ClientFilesTabProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [previewFile, setPreviewFile] = useState<FileRow | null>(null);
|
||||
const [uploadForSigningOpen, setUploadForSigningOpen] = useState(false);
|
||||
const { confirm, dialog: confirmDialog } = useConfirmation();
|
||||
|
||||
const { data, isLoading } = usePaginatedQuery<FileRow>({
|
||||
@@ -64,12 +68,28 @@ export function ClientFilesTab({ clientId }: ClientFilesTabProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PermissionGate resource="files" action="upload">
|
||||
<FileUploadZone
|
||||
clientId={clientId}
|
||||
onUploadComplete={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', { clientId }] });
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<FileUploadZone
|
||||
clientId={clientId}
|
||||
onUploadComplete={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', { clientId }] });
|
||||
}}
|
||||
/>
|
||||
<PermissionGate resource="documents" action="send_for_signing">
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setUploadForSigningOpen(true)}
|
||||
className="gap-2"
|
||||
>
|
||||
<Pen className="h-4 w-4" aria-hidden />
|
||||
Upload & send for signature
|
||||
</Button>
|
||||
</div>
|
||||
</PermissionGate>
|
||||
</div>
|
||||
</PermissionGate>
|
||||
|
||||
<FileGrid
|
||||
@@ -88,6 +108,17 @@ export function ClientFilesTab({ clientId }: ClientFilesTabProps) {
|
||||
fileName={previewFile?.filename}
|
||||
mimeType={previewFile?.mimeType ?? undefined}
|
||||
/>
|
||||
|
||||
{uploadForSigningOpen && (
|
||||
<UploadForSigningDialog
|
||||
open={uploadForSigningOpen}
|
||||
onOpenChange={setUploadForSigningOpen}
|
||||
interestId={null}
|
||||
documentType="generic"
|
||||
entity={{ type: 'client', id: clientId }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{confirmDialog}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user