feat(04-01): multi-project schema migration — projects, settings, FK pivot
- schema.ts: add projects table, settings kv table, slug on clients; migrate 6 FK from client_id to project_id (phases, payments, documents, notes, time_entries, quote_items); update all relations and TypeScript types - admin-queries.ts: fix getAllClientsWithPayments + getClientFullDetail to aggregate through projects; add getAllProjectsWithPayments, getProjectFullDetail, getClientWithProjects, ClientWithProjects type - settings.ts: new file — getSetting, updateSetting, getTargetHourlyRate, SETTINGS_KEYS - Fix all downstream files: actions.ts, quote-actions.ts, new/actions.ts, timer-actions.ts, approve/route.ts, comment/route.ts, TimerCell.tsx, analytics-queries.ts, client-view.ts, seed.ts - DB migration applied to Coolify Postgres (all test data cleared, schema rebuilt) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { clients, deliverables, tasks, phases } from "@/db/schema";
|
||||
import { clients, deliverables, tasks, phases, projects } from "@/db/schema";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -26,12 +26,13 @@ export async function POST(request: NextRequest) {
|
||||
const clientId = clientRows[0].id;
|
||||
|
||||
// Verify deliverable belongs to this client (prevents cross-client approval)
|
||||
// deliverable → task → phase → client
|
||||
// deliverable → task → phase → project → client
|
||||
const ownershipCheck = await db
|
||||
.select({ deliverable_id: deliverables.id, approved_at: deliverables.approved_at })
|
||||
.from(deliverables)
|
||||
.innerJoin(tasks, eq(deliverables.task_id, tasks.id))
|
||||
.innerJoin(phases, and(eq(tasks.phase_id, phases.id), eq(phases.client_id, clientId)))
|
||||
.innerJoin(phases, eq(tasks.phase_id, phases.id))
|
||||
.innerJoin(projects, and(eq(phases.project_id, projects.id), eq(projects.client_id, clientId)))
|
||||
.where(eq(deliverables.id, deliverableId))
|
||||
.limit(1);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/db";
|
||||
import { clients, comments, tasks, phases, deliverables } from "@/db/schema";
|
||||
import { clients, comments, tasks, phases, deliverables, projects } from "@/db/schema";
|
||||
|
||||
const commentSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
@@ -43,46 +43,50 @@ export async function POST(request: NextRequest) {
|
||||
if (entity_id !== clientId) {
|
||||
return NextResponse.json({ error: "Entity non valida" }, { status: 403 });
|
||||
}
|
||||
} else if (entity_type === "task") {
|
||||
const phasesForClient = await db
|
||||
.select({ id: phases.id })
|
||||
.from(phases)
|
||||
.where(eq(phases.client_id, clientId));
|
||||
const phaseIds = phasesForClient.map((p) => p.id);
|
||||
if (phaseIds.length === 0) {
|
||||
return NextResponse.json({ error: "Nessuna fase trovata" }, { status: 404 });
|
||||
}
|
||||
const taskRows = await db
|
||||
.select({ id: tasks.id })
|
||||
.from(tasks)
|
||||
.where(inArray(tasks.phase_id, phaseIds));
|
||||
if (!taskRows.find((r) => r.id === entity_id)) {
|
||||
return NextResponse.json({ error: "Task non trovato" }, { status: 404 });
|
||||
}
|
||||
} else {
|
||||
// deliverable
|
||||
// Scope phases through projects → client
|
||||
const clientProjects = await db
|
||||
.select({ id: projects.id })
|
||||
.from(projects)
|
||||
.where(eq(projects.client_id, clientId));
|
||||
const projectIds = clientProjects.map((p) => p.id);
|
||||
|
||||
if (projectIds.length === 0) {
|
||||
return NextResponse.json({ error: "Nessuna fase trovata" }, { status: 404 });
|
||||
}
|
||||
|
||||
const phasesForClient = await db
|
||||
.select({ id: phases.id })
|
||||
.from(phases)
|
||||
.where(eq(phases.client_id, clientId));
|
||||
.where(inArray(phases.project_id, projectIds));
|
||||
const phaseIds = phasesForClient.map((p) => p.id);
|
||||
|
||||
if (phaseIds.length === 0) {
|
||||
return NextResponse.json({ error: "Nessuna fase trovata" }, { status: 404 });
|
||||
}
|
||||
|
||||
const taskRows = await db
|
||||
.select({ id: tasks.id })
|
||||
.from(tasks)
|
||||
.where(inArray(tasks.phase_id, phaseIds));
|
||||
const taskIds = taskRows.map((r) => r.id);
|
||||
if (taskIds.length === 0) {
|
||||
return NextResponse.json({ error: "Nessun task trovato" }, { status: 404 });
|
||||
}
|
||||
const delivRows = await db
|
||||
.select({ id: deliverables.id })
|
||||
.from(deliverables)
|
||||
.where(inArray(deliverables.task_id, taskIds));
|
||||
if (!delivRows.find((r) => r.id === entity_id)) {
|
||||
return NextResponse.json({ error: "Deliverable non trovato" }, { status: 404 });
|
||||
|
||||
if (entity_type === "task") {
|
||||
if (!taskRows.find((r) => r.id === entity_id)) {
|
||||
return NextResponse.json({ error: "Task non trovato" }, { status: 404 });
|
||||
}
|
||||
} else {
|
||||
// deliverable
|
||||
const taskIds = taskRows.map((r) => r.id);
|
||||
if (taskIds.length === 0) {
|
||||
return NextResponse.json({ error: "Nessun task trovato" }, { status: 404 });
|
||||
}
|
||||
const delivRows = await db
|
||||
.select({ id: deliverables.id })
|
||||
.from(deliverables)
|
||||
.where(inArray(deliverables.task_id, taskIds));
|
||||
if (!delivRows.find((r) => r.id === entity_id)) {
|
||||
return NextResponse.json({ error: "Deliverable non trovato" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user