diff --git a/src/app/api/v1/companies/autocomplete/handlers.ts b/src/app/api/v1/companies/autocomplete/handlers.ts
index f08b3e70..25dabd24 100644
--- a/src/app/api/v1/companies/autocomplete/handlers.ts
+++ b/src/app/api/v1/companies/autocomplete/handlers.ts
@@ -6,10 +6,13 @@ import { autocomplete } from '@/lib/services/companies.service';
export const autocompleteHandler: RouteHandler = async (req, ctx) => {
try {
- const q = req.nextUrl.searchParams.get('q');
- if (!q) {
- return NextResponse.json({ data: [] });
- }
+ // Empty `q` returns the most recent companies (capped to the same
+ // 10-row limit the search path uses). The companion CompanyPicker
+ // initially renders with an empty query because the popover opens
+ // before the rep has typed anything — returning [] there hid every
+ // company even when the system had rows. Returning a recent list
+ // gives the rep something to scan or select immediately.
+ const q = req.nextUrl.searchParams.get('q') ?? '';
const companies = await autocomplete(ctx.portId, q);
return NextResponse.json({ data: companies });
} catch (error) {
diff --git a/src/components/clients/client-form.tsx b/src/components/clients/client-form.tsx
index a4663e24..0cb1976e 100644
--- a/src/components/clients/client-form.tsx
+++ b/src/components/clients/client-form.tsx
@@ -44,6 +44,11 @@ interface ClientFormProps {
* or opening the create-interest dialog pre-filled with that
* clientId. Skipped in edit mode. */
onUseExistingClient?: (clientId: string) => void;
+ /** Optional callback fired with the newly-created client's id after a
+ * successful create. Lets a parent flow (e.g. the new-interest drawer)
+ * chain a "use the client I just created" follow-up without making the
+ * rep re-select. Not called in edit mode. */
+ onCreated?: (clientId: string) => void;
/** Optional initial values for the create flow - used by the
* inquiry-inbox "Convert to client" triage step (P-4.5) so the rep
* doesn't retype values they just read in the inbox. The
@@ -84,6 +89,7 @@ export function ClientForm({
onOpenChange,
client,
onUseExistingClient,
+ onCreated,
prefill,
}: ClientFormProps) {
const queryClient = useQueryClient();
@@ -253,6 +259,7 @@ export function ClientForm({
body: { tagIds: tIds },
});
}
+ return null;
} else {
const res = await apiFetch<{ data: { id: string } }>('/api/v1/clients', {
method: 'POST',
@@ -287,13 +294,19 @@ export function ClientForm({
);
}
}
+ return { id: res.data.id };
}
},
- onSuccess: () => {
+ onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['clients'] });
// M-U10: confirm the write landed. Without this the rep closes
// the sheet not sure whether the create/edit actually saved.
toast.success(isEdit ? 'Client updated' : 'Client created');
+ // Parent flow gets a chance to chain off the new id (e.g. the
+ // new-interest drawer auto-selects the freshly-created client).
+ if (result && 'id' in result && result.id && onCreated) {
+ onCreated(result.id);
+ }
onOpenChange(false);
},
});
diff --git a/src/components/documents/create-document-wizard.tsx b/src/components/documents/create-document-wizard.tsx
index 07c72778..00e0b641 100644
--- a/src/components/documents/create-document-wizard.tsx
+++ b/src/components/documents/create-document-wizard.tsx
@@ -25,7 +25,7 @@ import { DocumentTemplatePicker } from '@/components/documents/document-template
import { FileUploadZone } from '@/components/files/file-upload-zone';
import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error';
-import { DOCUMENT_TYPES } from '@/lib/constants';
+import { DOCUMENT_TYPES, DOCUMENT_TYPE_LABELS } from '@/lib/constants';
// Display labels for SIGNER_ROLES - internal values stay lowercase, UI shows
// capitalized. Falls back to capitalize-first-letter for any value not in the
@@ -311,11 +311,17 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
{DOCUMENT_TYPES.map((t) => (
- {t.replace(/_/g, ' ')}
+ {DOCUMENT_TYPE_LABELS[t]}
))}
+ {documentType === 'other' ? (
+
+ Use the Title below to describe the document - that's how it'll appear
+ everywhere it's referenced.
+
+ ) : null}
diff --git a/src/components/documents/documents-hub.tsx b/src/components/documents/documents-hub.tsx
index d8ce66e8..34ddf6d5 100644
--- a/src/components/documents/documents-hub.tsx
+++ b/src/components/documents/documents-hub.tsx
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
-import { useQueryClient } from '@tanstack/react-query';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
import { ChevronDown, ChevronRight, FileText, Folder, Lock, Plus, Upload } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -43,6 +43,24 @@ interface HubDoc {
signers?: Array<{ id: string; signerEmail: string; signerName: string; status: string }>;
}
+/**
+ * Lightweight files-table row used by FlatFolderListing's parallel files
+ * query. Folder views show both signature documents (the `documents`
+ * table) AND plain uploaded files (the `files` table) so reps see
+ * everything that physically lives in the folder in one list. The two
+ * sources merge into a unified `HubRow` for rendering.
+ */
+interface HubFile {
+ id: string;
+ filename: string;
+ originalName: string | null;
+ mimeType: string | null;
+ sizeBytes: number;
+ createdAt: string;
+}
+
+type HubRow = { kind: 'doc'; doc: HubDoc } | { kind: 'file'; file: HubFile };
+
const TYPE_LABELS: Record
= {
eoi: 'EOI',
contract: 'Contract',
@@ -335,6 +353,57 @@ function FlatFolderListing({
filterDefinitions: [],
});
+ // Plain uploaded files in this folder. Lives in a separate table from
+ // signature documents, so we query it in parallel and merge into the
+ // unified row list. Skip the search/typeFilter for files (those filters
+ // are doc-specific); applying them client-side would create a confusing
+ // partial view where the search bar hides files unrelated to the query.
+ // folderId === null → root; UUID → that folder; the endpoint resolves
+ // `folderId=` as null to keep the URL idempotent across folder switches.
+ const filesQueryString = useMemo(() => {
+ const p = new URLSearchParams();
+ p.set('folderId', folderId ?? '');
+ p.set('limit', '50');
+ p.set('sort', 'createdAt');
+ p.set('order', 'desc');
+ return p.toString();
+ }, [folderId]);
+ const { data: filesResp, isLoading: filesLoading } = useQuery<{ data: HubFile[] }>({
+ queryKey: ['files', 'hub', 'folder', filesQueryString],
+ queryFn: async (): Promise<{ data: HubFile[] }> => {
+ const res = await fetch(`/api/v1/files?${filesQueryString}`, { credentials: 'include' });
+ if (!res.ok) return { data: [] };
+ return (await res.json()) as { data: HubFile[] };
+ },
+ });
+ const folderFiles: HubFile[] = useMemo(() => filesResp?.data ?? [], [filesResp]);
+
+ // Merge docs + files, sorted by createdAt desc so the most-recent item
+ // (regardless of source) is at the top. Filter file rows out when the
+ // type-chip is active (chips filter docs only — files have no
+ // documentType column, treating them as "uploads" type means they'd
+ // disappear under any chip selection, which is the right UX).
+ const mergedRows: HubRow[] = useMemo(() => {
+ const docRows = documents.map((doc) => ({ kind: 'doc' as const, doc }));
+ const fileRows = typeFilter
+ ? []
+ : folderFiles
+ // Client-side search across filenames so the search box covers
+ // both sources uniformly.
+ .filter((f) =>
+ search
+ ? (f.filename ?? '').toLowerCase().includes(search.toLowerCase()) ||
+ (f.originalName ?? '').toLowerCase().includes(search.toLowerCase())
+ : true,
+ )
+ .map((file) => ({ kind: 'file' as const, file }));
+ return [...docRows, ...fileRows].sort((a, b) => {
+ const aTime = a.kind === 'doc' ? a.doc.createdAt : a.file.createdAt;
+ const bTime = b.kind === 'doc' ? b.doc.createdAt : b.file.createdAt;
+ return bTime.localeCompare(aTime);
+ });
+ }, [documents, folderFiles, typeFilter, search]);
+
// Realtime invalidation is lifted to DocumentsHub so it survives mode
// switches (root / entity-folder / flat-folder). Don't re-subscribe here.
@@ -357,19 +426,25 @@ function FlatFolderListing({
className="border-b last:border-b-0 transition-colors hover:bg-gradient-brand-soft/40"
>
-
+ {totalSigners > 0 ? (
+
+ ) : (
+ // Keep the column reserved so the grid layout stays aligned
+ // across rows; this row has no signers to expand into.
+
+ )}
{
+ return (
+
+
+ {/* Empty action column to align with doc-row layout */}
+
+
+ {file.originalName ?? file.filename}
+
+
Uploaded file
+
+ Stored
+
+
+ {(file.sizeBytes / 1024).toFixed(0)} KB
+
+
+ {new Date(file.createdAt).toLocaleDateString(undefined)}
+
+
+
+ );
+ };
+
return (
- <>
+
) : null}
- {isLoading ? (
+ {isLoading || filesLoading ? (
{[0, 1, 2, 3, 4].map((i) => (
))}
- ) : documents.length === 0 && childFolders.length === 0 ? (
+ ) : mergedRows.length === 0 && childFolders.length === 0 ? (
}
title="No documents in this folder"
@@ -510,7 +621,11 @@ function FlatFolderListing({
}
/>
) : (
-
{documents.map(renderRow)}
+
+ {mergedRows.map((row) =>
+ row.kind === 'doc' ? renderRow(row.doc) : renderFileRow(row.file),
+ )}
+
)}
- >
+
);
}
diff --git a/src/components/files/file-preview-dialog.tsx b/src/components/files/file-preview-dialog.tsx
index eb6a0307..b9f64816 100644
--- a/src/components/files/file-preview-dialog.tsx
+++ b/src/components/files/file-preview-dialog.tsx
@@ -106,7 +106,7 @@ export function FilePreviewDialog({
return (