feat(clients): show contacts on client cards, auto-persist seeded signers to DB
This commit is contained in:
@@ -9,13 +9,14 @@ export default async function ClientsPage() {
|
|||||||
id: clients.id,
|
id: clients.id,
|
||||||
name: clients.name,
|
name: clients.name,
|
||||||
email: clients.email,
|
email: clients.email,
|
||||||
|
contacts: clients.contacts,
|
||||||
createdAt: clients.createdAt,
|
createdAt: clients.createdAt,
|
||||||
docCount: sql<number>`count(${documents.id})::int`,
|
docCount: sql<number>`count(${documents.id})::int`,
|
||||||
lastActivity: sql<Date | null>`max(${documents.sentAt})`,
|
lastActivity: sql<Date | null>`max(${documents.sentAt})`,
|
||||||
})
|
})
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.leftJoin(documents, eq(documents.clientId, clients.id))
|
.leftJoin(documents, eq(documents.clientId, clients.id))
|
||||||
.groupBy(clients.id, clients.name, clients.email, clients.createdAt)
|
.groupBy(clients.id, clients.name, clients.email, clients.contacts, clients.createdAt)
|
||||||
.orderBy(desc(clients.createdAt));
|
.orderBy(desc(clients.createdAt));
|
||||||
|
|
||||||
return <ClientsPageClient clients={clientRows} />;
|
return <ClientsPageClient clients={clientRows} />;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { PdfViewerWrapper } from './PdfViewerWrapper';
|
import { PdfViewerWrapper } from './PdfViewerWrapper';
|
||||||
import { PreparePanel } from './PreparePanel';
|
import { PreparePanel } from './PreparePanel';
|
||||||
import type { DocumentSigner } from '@/lib/db/schema';
|
import type { DocumentSigner } from '@/lib/db/schema';
|
||||||
@@ -45,6 +45,18 @@ export function DocumentPageClient({
|
|||||||
const [signers, setSigners] = useState<DocumentSigner[]>(defaultSigners);
|
const [signers, setSigners] = useState<DocumentSigner[]>(defaultSigners);
|
||||||
const [unassignedFieldIds, setUnassignedFieldIds] = useState<Set<string>>(new Set());
|
const [unassignedFieldIds, setUnassignedFieldIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Persist auto-seeded signers to DB so they survive refresh
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialSigners.length === 0 && defaultSigners.length > 0) {
|
||||||
|
fetch(`/api/documents/${docId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ signers: defaultSigners }),
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleFieldsChanged = useCallback(() => {
|
const handleFieldsChanged = useCallback(() => {
|
||||||
setPreviewToken(null);
|
setPreviewToken(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,38 +1,50 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import type { ClientContact } from "@/lib/db/schema";
|
||||||
|
|
||||||
type ClientCardProps = {
|
type ClientCardProps = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
contacts?: ClientContact[];
|
||||||
docCount: number;
|
docCount: number;
|
||||||
lastActivity: Date | null;
|
lastActivity: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ClientCard({
|
export function ClientCard({ id, name, email, contacts = [], docCount, lastActivity }: ClientCardProps) {
|
||||||
id,
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
docCount,
|
|
||||||
lastActivity,
|
|
||||||
}: ClientCardProps) {
|
|
||||||
return (
|
return (
|
||||||
<Link href={"/portal/clients/" + id}>
|
<Link href={"/portal/clients/" + id} style={{ textDecoration: "none" }}>
|
||||||
<div className="bg-white rounded-xl shadow-sm p-5 hover:shadow-md transition-shadow cursor-pointer">
|
<div
|
||||||
<p className="text-[var(--navy)] font-semibold text-base">{name}</p>
|
style={{ backgroundColor: "white", borderRadius: "1rem", boxShadow: "0 1px 4px rgba(0,0,0,0.07)", padding: "1.25rem", cursor: "pointer", transition: "box-shadow 0.2s" }}
|
||||||
<p className="text-gray-500 text-sm mt-1">{email}</p>
|
onMouseEnter={e => (e.currentTarget.style.boxShadow = "0 4px 12px rgba(0,0,0,0.12)")}
|
||||||
<p className="text-gray-400 text-xs mt-3">
|
onMouseLeave={e => (e.currentTarget.style.boxShadow = "0 1px 4px rgba(0,0,0,0.07)")}
|
||||||
Documents: {docCount}
|
>
|
||||||
</p>
|
{/* Primary contact */}
|
||||||
<p className="text-gray-400 text-xs mt-1">
|
<p style={{ color: "#1B2B4B", fontWeight: 600, fontSize: "1rem", marginBottom: "0.125rem" }}>{name}</p>
|
||||||
Last activity:{" "}
|
<p style={{ color: "#6B7280", fontSize: "0.875rem", marginBottom: contacts.length > 0 ? "0.5rem" : "0.75rem" }}>{email}</p>
|
||||||
|
|
||||||
|
{/* Additional contacts */}
|
||||||
|
{contacts.length > 0 && (
|
||||||
|
<div style={{ borderTop: "1px solid #F3F4F6", paddingTop: "0.5rem", marginBottom: "0.5rem", display: "flex", flexDirection: "column", gap: "0.25rem" }}>
|
||||||
|
{contacts.map(c => (
|
||||||
|
<div key={c.email} style={{ display: "flex", alignItems: "baseline", gap: "0.375rem" }}>
|
||||||
|
<span style={{ fontSize: "0.75rem", color: "#9CA3AF", flexShrink: 0 }}>+</span>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<span style={{ fontSize: "0.8125rem", color: "#374151", fontWeight: 500 }}>{c.name}</span>
|
||||||
|
<span style={{ fontSize: "0.75rem", color: "#9CA3AF", marginLeft: "0.375rem" }}>{c.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ borderTop: "1px solid #F3F4F6", paddingTop: "0.75rem", display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<span style={{ color: "#6B7280", fontSize: "0.75rem" }}>{docCount} document{docCount !== 1 ? "s" : ""}</span>
|
||||||
|
<span style={{ color: "#9CA3AF", fontSize: "0.75rem" }}>
|
||||||
{lastActivity
|
{lastActivity
|
||||||
? lastActivity.toLocaleDateString("en-US", {
|
? new Date(lastActivity).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
})
|
|
||||||
: "No activity yet"}
|
: "No activity yet"}
|
||||||
</p>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,41 +3,44 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ClientCard } from "./ClientCard";
|
import { ClientCard } from "./ClientCard";
|
||||||
import { ClientModal } from "./ClientModal";
|
import { ClientModal } from "./ClientModal";
|
||||||
|
import type { ClientContact } from "@/lib/db/schema";
|
||||||
|
|
||||||
type ClientRow = {
|
type ClientRow = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
contacts?: ClientContact[] | null;
|
||||||
docCount: number;
|
docCount: number;
|
||||||
lastActivity: Date | null;
|
lastActivity: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClientsPageClientProps = {
|
export function ClientsPageClient({ clients }: { clients: ClientRow[] }) {
|
||||||
clients: ClientRow[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ClientsPageClient({ clients }: ClientsPageClientProps) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "1.5rem" }}>
|
||||||
<h1 className="text-[var(--navy)] text-2xl font-semibold">Clients</h1>
|
<div>
|
||||||
|
<h1 style={{ color: "#1B2B4B", fontSize: "1.5rem", fontWeight: 700, marginBottom: "0.25rem" }}>Clients</h1>
|
||||||
|
<p style={{ color: "#6B7280", fontSize: "0.875rem" }}>{clients.length} client{clients.length !== 1 ? "s" : ""}</p>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
className="bg-[var(--gold)] text-white px-4 py-2 rounded-lg text-sm font-medium hover:opacity-90"
|
className="rounded-lg px-4 py-2 text-sm font-semibold text-white transition hover:brightness-110"
|
||||||
|
style={{ backgroundColor: "#C9A84C" }}
|
||||||
>
|
>
|
||||||
+ Add Client
|
+ Add Client
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{clients.length === 0 ? (
|
{clients.length === 0 ? (
|
||||||
<div className="text-center py-16">
|
<div style={{ textAlign: "center", padding: "4rem 0" }}>
|
||||||
<p className="text-gray-500 mb-4">No clients yet.</p>
|
<p style={{ color: "#6B7280", marginBottom: "1rem" }}>No clients yet.</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
className="bg-[var(--gold)] text-white px-4 py-2 rounded-lg text-sm font-medium hover:opacity-90"
|
className="rounded-lg px-4 py-2 text-sm font-semibold text-white transition hover:brightness-110"
|
||||||
|
style={{ backgroundColor: "#C9A84C" }}
|
||||||
>
|
>
|
||||||
+ Add your first client
|
+ Add your first client
|
||||||
</button>
|
</button>
|
||||||
@@ -50,6 +53,7 @@ export function ClientsPageClient({ clients }: ClientsPageClientProps) {
|
|||||||
id={client.id}
|
id={client.id}
|
||||||
name={client.name}
|
name={client.name}
|
||||||
email={client.email}
|
email={client.email}
|
||||||
|
contacts={client.contacts ?? []}
|
||||||
docCount={client.docCount}
|
docCount={client.docCount}
|
||||||
lastActivity={client.lastActivity}
|
lastActivity={client.lastActivity}
|
||||||
/>
|
/>
|
||||||
@@ -57,11 +61,7 @@ export function ClientsPageClient({ clients }: ClientsPageClientProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ClientModal
|
<ClientModal isOpen={isOpen} onClose={() => setIsOpen(false)} mode="create" />
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => setIsOpen(false)}
|
|
||||||
mode="create"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user