diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
new file mode 100644
index 0000000..3344c37
--- /dev/null
+++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx
@@ -0,0 +1,66 @@
+'use client';
+import { useState } from 'react';
+import { Document, Page, pdfjs } from 'react-pdf';
+import 'react-pdf/dist/Page/AnnotationLayer.css';
+import 'react-pdf/dist/Page/TextLayer.css';
+
+// Worker setup — must use import.meta.url for local/Docker environments (no CDN)
+pdfjs.GlobalWorkerOptions.workerSrc = new URL(
+ 'pdfjs-dist/build/pdf.worker.min.mjs',
+ import.meta.url,
+).toString();
+
+export function PdfViewer({ docId }: { docId: string }) {
+ const [numPages, setNumPages] = useState(0);
+ const [pageNumber, setPageNumber] = useState(1);
+ const [scale, setScale] = useState(1.0);
+
+ return (
+
+
+
+
{pageNumber} / {numPages || '?'}
+
+
+
+
+ Download
+
+
+
+
setNumPages(numPages)}
+ className="shadow-lg"
+ >
+
+
+
+ );
+}
diff --git a/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
new file mode 100644
index 0000000..09a9f22
--- /dev/null
+++ b/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx
@@ -0,0 +1,43 @@
+import { auth } from '@/lib/auth';
+import { redirect } from 'next/navigation';
+import { db } from '@/lib/db';
+import { documents, clients } from '@/lib/db/schema';
+import { eq } from 'drizzle-orm';
+import Link from 'next/link';
+import { PdfViewer } from './_components/PdfViewer';
+
+export default async function DocumentPage({
+ params,
+}: {
+ params: Promise<{ docId: string }>;
+}) {
+ const session = await auth();
+ if (!session) redirect('/login');
+
+ const { docId } = await params;
+
+ const doc = await db.query.documents.findFirst({
+ where: eq(documents.id, docId),
+ with: { client: true },
+ });
+
+ if (!doc) redirect('/portal/dashboard');
+
+ return (
+
+
+
+
+ ← Back to {doc.client?.name ?? 'Client'}
+
+
{doc.name}
+
{doc.client?.name}
+
+
+
+
+ );
+}
diff --git a/teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx b/teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx
new file mode 100644
index 0000000..2936313
--- /dev/null
+++ b/teressa-copeland-homes/src/app/portal/_components/AddDocumentModal.tsx
@@ -0,0 +1,129 @@
+'use client';
+import { useState, useEffect, useTransition } from 'react';
+import { useRouter } from 'next/navigation';
+
+type FormTemplate = { id: string; name: string; filename: string };
+
+export function AddDocumentModal({ clientId, onClose }: { clientId: string; onClose: () => void }) {
+ const [templates, setTemplates] = useState([]);
+ const [query, setQuery] = useState('');
+ const [docName, setDocName] = useState('');
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [customFile, setCustomFile] = useState(null);
+ const [isPending, startTransition] = useTransition();
+ const [saving, setSaving] = useState(false);
+ const router = useRouter();
+
+ useEffect(() => {
+ fetch('/api/forms-library')
+ .then(r => r.json())
+ .then(setTemplates)
+ .catch(console.error);
+ }, []);
+
+ const filtered = templates.filter(t =>
+ t.name.toLowerCase().includes(query.toLowerCase())
+ );
+
+ const handleSelectTemplate = (t: FormTemplate) => {
+ setSelectedTemplate(t);
+ setCustomFile(null);
+ setDocName(t.name);
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0] ?? null;
+ setCustomFile(file);
+ setSelectedTemplate(null);
+ if (file) setDocName(file.name.replace(/\.pdf$/i, ''));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!docName.trim() || (!selectedTemplate && !customFile)) return;
+
+ setSaving(true);
+ try {
+ if (customFile) {
+ const fd = new FormData();
+ fd.append('clientId', clientId);
+ fd.append('name', docName.trim());
+ fd.append('file', customFile);
+ await fetch('/api/documents', { method: 'POST', body: fd });
+ } else {
+ await fetch('/api/documents', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ clientId, name: docName.trim(), formTemplateId: selectedTemplate!.id }),
+ });
+ }
+ startTransition(() => router.refresh());
+ onClose();
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+
+
Add Document
+
+
setQuery(e.target.value)}
+ className="w-full border rounded px-3 py-2 mb-3 text-sm"
+ />
+
+
+ {filtered.length === 0 && (
+ - No forms found
+ )}
+ {filtered.map(t => (
+ - handleSelectTemplate(t)}
+ className={`px-3 py-2 text-sm cursor-pointer hover:bg-gray-100 ${selectedTemplate?.id === t.id ? 'bg-blue-50 font-medium' : ''}`}
+ >
+ {t.name}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/teressa-copeland-homes/src/app/portal/_components/ClientProfileClient.tsx b/teressa-copeland-homes/src/app/portal/_components/ClientProfileClient.tsx
index 5f750ba..9035189 100644
--- a/teressa-copeland-homes/src/app/portal/_components/ClientProfileClient.tsx
+++ b/teressa-copeland-homes/src/app/portal/_components/ClientProfileClient.tsx
@@ -6,6 +6,7 @@ import Link from "next/link";
import { ClientModal } from "./ClientModal";
import { ConfirmDialog } from "./ConfirmDialog";
import { DocumentsTable } from "./DocumentsTable";
+import { AddDocumentModal } from "./AddDocumentModal";
import { deleteClient } from "@/lib/actions/clients";
type DocumentRow = {
@@ -26,6 +27,7 @@ export function ClientProfileClient({ client, docs }: Props) {
const router = useRouter();
const [isEditOpen, setIsEditOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
+ const [isAddDocOpen, setIsAddDocOpen] = useState(false);
async function handleDelete() {
await deleteClient(client.id);
@@ -35,34 +37,30 @@ export function ClientProfileClient({ client, docs }: Props) {
return (
{/* Header card */}
-
-
-
+
+
+
← Back to Clients
-
+
-
- {client.name}
-
-
{client.email}
+
{client.name}
+
{client.email}
-
+
@@ -71,36 +69,33 @@ export function ClientProfileClient({ client, docs }: Props) {
{/* Documents card */}
-
-
- Documents
-
+
+
+
Documents
+
+
{docs.length === 0 ? (
-
No documents yet.
+
No documents yet.
) : (
)}
- {/* Edit modal */}
-
setIsEditOpen(false)}
- mode="edit"
- clientId={client.id}
- defaultName={client.name}
- defaultEmail={client.email}
- />
-
- {/* Delete confirmation */}
+ setIsEditOpen(false)} mode="edit" clientId={client.id} defaultName={client.name} defaultEmail={client.email} />
+ {isAddDocOpen && (
+ setIsAddDocOpen(false)} />
+ )}
setIsDeleteOpen(false)}
/>
diff --git a/teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx b/teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
index 3101051..b0be365 100644
--- a/teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
+++ b/teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
@@ -1,3 +1,4 @@
+import Link from "next/link";
import { StatusBadge } from "./StatusBadge";
type DocumentRow = {
@@ -12,45 +13,55 @@ type DocumentRow = {
type Props = { rows: DocumentRow[]; showClientColumn?: boolean };
export function DocumentsTable({ rows, showClientColumn = true }: Props) {
+ if (rows.length === 0) {
+ return (
+
+ No documents found.
+
+ );
+ }
+
return (
-
+
-
- |
+ |
+ |
Document Name
|
{showClientColumn && (
-
+ |
Client
|
)}
-
+ |
Status
|
-
+ |
Date Sent
|
{rows.map((row) => (
-
- | {row.name} |
+
+ |
+
+ {row.name}
+
+ |
{showClientColumn && (
-
- {row.clientName ?? "—"}
- |
+ {row.clientName ?? "—"} |
)}
-
+ |
|
-
+ |
{row.sentAt
- ? row.sentAt.toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- year: "numeric",
- })
+ ? new Date(row.sentAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
: "—"}
|
diff --git a/teressa-copeland-homes/src/lib/db/schema.ts b/teressa-copeland-homes/src/lib/db/schema.ts
index 95ef1c0..3711a3f 100644
--- a/teressa-copeland-homes/src/lib/db/schema.ts
+++ b/teressa-copeland-homes/src/lib/db/schema.ts
@@ -1,4 +1,5 @@
import { pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
+import { relations } from "drizzle-orm";
export const users = pgTable("users", {
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
@@ -42,3 +43,11 @@ export const documents = pgTable("documents", {
formTemplateId: text("form_template_id").references(() => formTemplates.id),
filePath: text("file_path"),
});
+
+export const documentsRelations = relations(documents, ({ one }) => ({
+ client: one(clients, { fields: [documents.clientId], references: [clients.id] }),
+}));
+
+export const clientsRelations = relations(clients, ({ many }) => ({
+ documents: many(documents),
+}));