feat(05-03): extend getProjectFullDetail + add offer assignment server actions

- Add offer_micros, project_offers imports to admin-queries.ts
- Add ProjectOfferWithMicro type to admin-queries.ts
- Extend ProjectFullDetail type with projectOffers + availableMicros fields
- Extend getProjectFullDetail() to query project offers and available micros in parallel
- Add z import and project_offers import to project-actions.ts
- Add assignOfferToProject, removeProjectOffer, updateProjectOfferTotal server actions
- All three actions call requireAdmin() and revalidate /admin/forecast
This commit is contained in:
2026-05-30 16:25:58 +02:00
parent 2faf2fedf1
commit b3f781b9b5
2 changed files with 103 additions and 2 deletions
+53 -1
View File
@@ -4,8 +4,9 @@ import { revalidatePath } from "next/cache";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { db } from "@/db";
import { projects, clients, payments } from "@/db/schema";
import { projects, clients, payments, project_offers } from "@/db/schema";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { nanoid } from "nanoid";
async function requireAdmin() {
@@ -107,4 +108,55 @@ export async function splitPayment(
});
revalidatePath(`/admin/projects/${projectId}`);
}
// ── Offer assignment actions ──────────────────────────────────────────────────
const assignOfferSchema = z.object({
project_id: z.string().min(1),
micro_id: z.string().min(1, "Seleziona una micro-offerta"),
accepted_total: z.coerce.number().min(0).optional(),
});
export async function assignOfferToProject(formData: FormData) {
await requireAdmin();
const parsed = assignOfferSchema.safeParse({
project_id: formData.get("project_id"),
micro_id: formData.get("micro_id"),
accepted_total: formData.get("accepted_total") || undefined,
});
if (!parsed.success) throw new Error(parsed.error.issues[0].message);
await db.insert(project_offers).values({
project_id: parsed.data.project_id,
micro_id: parsed.data.micro_id,
accepted_total:
parsed.data.accepted_total !== undefined
? parsed.data.accepted_total.toFixed(2)
: null,
});
revalidatePath(`/admin/projects/${parsed.data.project_id}`);
revalidatePath("/admin/forecast");
}
export async function removeProjectOffer(projectOfferId: string, projectId: string) {
await requireAdmin();
await db.delete(project_offers).where(eq(project_offers.id, projectOfferId));
revalidatePath(`/admin/projects/${projectId}`);
revalidatePath("/admin/forecast");
}
export async function updateProjectOfferTotal(
projectOfferId: string,
projectId: string,
accepted_total: string
) {
await requireAdmin();
const amount = parseFloat(accepted_total);
if (isNaN(amount) || amount < 0) throw new Error("Importo non valido");
await db
.update(project_offers)
.set({ accepted_total: amount.toFixed(2) })
.where(eq(project_offers.id, projectOfferId));
revalidatePath(`/admin/projects/${projectId}`);
revalidatePath("/admin/forecast");
}