feat(02-03): install @radix-ui/react-tabs, add getClientFullDetail, create Server Actions
- Add shadcn tabs component (src/components/ui/tabs.tsx) backed by @radix-ui/react-tabs - Extend admin-queries.ts with getClientFullDetail() — fetches client + phases + tasks + deliverables + payments + documents + notes + comments in one call - Create src/app/admin/clients/[id]/actions.ts with all mutations: addPhase, updatePhaseStatus, addTask, updateTaskStatus, addDeliverable, addDocument, deleteDocument, updatePaymentStatus, updateAcceptedTotal, postAdminComment - All actions include server-side allowlist validation and revalidatePath - approved_at immutability enforced by omission in addDeliverable
This commit is contained in:
+112
-2
@@ -1,6 +1,25 @@
|
||||
import { db } from "@/db";
|
||||
import { clients, payments } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
clients,
|
||||
payments,
|
||||
phases,
|
||||
tasks,
|
||||
deliverables,
|
||||
comments,
|
||||
documents,
|
||||
notes,
|
||||
} from "@/db/schema";
|
||||
import { eq, inArray, asc } from "drizzle-orm";
|
||||
import type {
|
||||
Client,
|
||||
Phase,
|
||||
Task,
|
||||
Deliverable,
|
||||
Payment,
|
||||
Document,
|
||||
Note,
|
||||
Comment,
|
||||
} from "@/db/schema";
|
||||
|
||||
export type ClientWithPayments = {
|
||||
id: string;
|
||||
@@ -55,3 +74,94 @@ export async function getClientById(id: string) {
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
// ── ClientFullDetail — used by /admin/clients/[id] workspace ─────────────────
|
||||
|
||||
export type ClientFullDetail = {
|
||||
client: Client;
|
||||
phases: Array<Phase & { tasks: Array<Task & { deliverables: Deliverable[] }> }>;
|
||||
payments: Payment[];
|
||||
documents: Document[];
|
||||
notes: Note[];
|
||||
comments: Comment[];
|
||||
};
|
||||
|
||||
export async function getClientFullDetail(id: string): Promise<ClientFullDetail | null> {
|
||||
const clientRows = await db.select().from(clients).where(eq(clients.id, id)).limit(1);
|
||||
if (clientRows.length === 0) return null;
|
||||
const client = clientRows[0];
|
||||
|
||||
const phasesRows = await db
|
||||
.select()
|
||||
.from(phases)
|
||||
.where(eq(phases.client_id, id))
|
||||
.orderBy(asc(phases.sort_order));
|
||||
|
||||
const phaseIds = phasesRows.map((p) => p.id);
|
||||
|
||||
const tasksRows =
|
||||
phaseIds.length === 0
|
||||
? []
|
||||
: await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(inArray(tasks.phase_id, phaseIds))
|
||||
.orderBy(asc(tasks.sort_order));
|
||||
|
||||
const taskIds = tasksRows.map((t) => t.id);
|
||||
|
||||
const deliverablesRows =
|
||||
taskIds.length === 0
|
||||
? []
|
||||
: await db
|
||||
.select()
|
||||
.from(deliverables)
|
||||
.where(inArray(deliverables.task_id, taskIds));
|
||||
|
||||
const paymentsRows = await db
|
||||
.select()
|
||||
.from(payments)
|
||||
.where(eq(payments.client_id, id));
|
||||
|
||||
const documentsRows = await db
|
||||
.select()
|
||||
.from(documents)
|
||||
.where(eq(documents.client_id, id))
|
||||
.orderBy(asc(documents.created_at));
|
||||
|
||||
const notesRows = await db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(eq(notes.client_id, id))
|
||||
.orderBy(asc(notes.created_at));
|
||||
|
||||
// Fetch all comments for tasks and deliverables belonging to this client
|
||||
const allEntityIds = [...taskIds, ...deliverablesRows.map((d) => d.id)];
|
||||
const commentsRows =
|
||||
allEntityIds.length === 0
|
||||
? []
|
||||
: await db
|
||||
.select()
|
||||
.from(comments)
|
||||
.where(inArray(comments.entity_id, allEntityIds))
|
||||
.orderBy(asc(comments.created_at));
|
||||
|
||||
const phasesWithTasks = phasesRows.map((phase) => {
|
||||
const phaseTasks = tasksRows
|
||||
.filter((t) => t.phase_id === phase.id)
|
||||
.map((task) => ({
|
||||
...task,
|
||||
deliverables: deliverablesRows.filter((d) => d.task_id === task.id),
|
||||
}));
|
||||
return { ...phase, tasks: phaseTasks };
|
||||
});
|
||||
|
||||
return {
|
||||
client,
|
||||
phases: phasesWithTasks,
|
||||
payments: paymentsRows,
|
||||
documents: documentsRows,
|
||||
notes: notesRows,
|
||||
comments: commentsRows,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user