feat(14-01): add updateLeadField and lead tag CRUD server actions
- requireAdmin() guard (mirrors catalog/actions.ts pattern), throws "Non autorizzato" before any DB access - updateLeadField: EDITABLE_FIELDS allowlist, name/status validated server-side (LEAD_STAGES.includes), nullable fields cleared on empty - addLeadTag/removeLeadTag/renameLeadTag: polymorphic tags table scoped to entity_type="leads" (LEADS_TAG_ENTITY), never touches entity_type="services" rows - All four actions call revalidatePath for /admin/leads (+ detail page where a single leadId applies) - Log pre-existing lint issues (unrelated to this task) to deferred-items.md per scope-boundary rule
This commit is contained in:
@@ -2,11 +2,18 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { db } from "@/db";
|
||||
import { leads, quotes } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { createLeadSchema, updateLeadSchema, createActivitySchema } from "@/lib/lead-validators";
|
||||
import { leads, quotes, tags } from "@/db/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import {
|
||||
createLeadSchema,
|
||||
updateLeadSchema,
|
||||
createActivitySchema,
|
||||
LEAD_STAGES,
|
||||
} from "@/lib/lead-validators";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { createActivity, updateLeadStage } from "@/lib/lead-service";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
|
||||
export async function createLead(data: z.infer<typeof createLeadSchema>) {
|
||||
const parsed = createLeadSchema.safeParse(data);
|
||||
@@ -159,3 +166,95 @@ export async function assignQuoteToLead(data: z.infer<typeof assignQuoteSchema>)
|
||||
return { success: false, error: "Errore nell'assegnazione" };
|
||||
}
|
||||
}
|
||||
|
||||
async function requireAdmin() {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new Error("Non autorizzato");
|
||||
}
|
||||
|
||||
// ── Inline-edit field update (Phase 14 database-view) ───────────────────────
|
||||
|
||||
const EDITABLE_FIELDS = ["name", "email", "phone", "company", "status", "next_action"] as const;
|
||||
type EditableField = (typeof EDITABLE_FIELDS)[number];
|
||||
|
||||
export async function updateLeadField(
|
||||
leadId: string,
|
||||
fieldName: EditableField,
|
||||
value: string
|
||||
) {
|
||||
await requireAdmin();
|
||||
|
||||
if (!EDITABLE_FIELDS.includes(fieldName)) {
|
||||
throw new Error(`Campo non editabile: ${fieldName}`);
|
||||
}
|
||||
|
||||
if (fieldName === "name") {
|
||||
const s = value.trim();
|
||||
if (s.length === 0) throw new Error("Nome richiesto");
|
||||
await db.update(leads).set({ name: s, updated_at: new Date() }).where(eq(leads.id, leadId));
|
||||
} else if (fieldName === "status") {
|
||||
if (!LEAD_STAGES.includes(value as (typeof LEAD_STAGES)[number])) {
|
||||
throw new Error("Stato non valido");
|
||||
}
|
||||
await db.update(leads).set({ status: value, updated_at: new Date() }).where(eq(leads.id, leadId));
|
||||
} else {
|
||||
// email | phone | company | next_action — nullable text fields, empty clears
|
||||
const s = value.trim();
|
||||
await db
|
||||
.update(leads)
|
||||
.set({ [fieldName]: s || null, updated_at: new Date() })
|
||||
.where(eq(leads.id, leadId));
|
||||
}
|
||||
|
||||
revalidatePath("/admin/leads");
|
||||
revalidatePath(`/admin/leads/${leadId}`);
|
||||
}
|
||||
|
||||
// ── Lead tags (Phase 14, CRM-09) — polymorphic `tags` table, entity_type="leads" ─
|
||||
|
||||
const LEADS_TAG_ENTITY = "leads";
|
||||
|
||||
export async function addLeadTag(leadId: string, value: string) {
|
||||
await requireAdmin();
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length === 0) throw new Error("Valore richiesto");
|
||||
|
||||
await db
|
||||
.insert(tags)
|
||||
.values({ entity_type: LEADS_TAG_ENTITY, entity_id: leadId, name: trimmed })
|
||||
.onConflictDoNothing();
|
||||
|
||||
revalidatePath("/admin/leads");
|
||||
revalidatePath(`/admin/leads/${leadId}`);
|
||||
}
|
||||
|
||||
export async function removeLeadTag(leadId: string, value: string) {
|
||||
await requireAdmin();
|
||||
|
||||
await db
|
||||
.delete(tags)
|
||||
.where(
|
||||
and(
|
||||
eq(tags.entity_type, LEADS_TAG_ENTITY),
|
||||
eq(tags.entity_id, leadId),
|
||||
eq(tags.name, value)
|
||||
)
|
||||
);
|
||||
|
||||
revalidatePath("/admin/leads");
|
||||
revalidatePath(`/admin/leads/${leadId}`);
|
||||
}
|
||||
|
||||
export async function renameLeadTag(oldValue: string, newValue: string) {
|
||||
await requireAdmin();
|
||||
const next = newValue.trim();
|
||||
if (next.length === 0) throw new Error("Nuovo nome richiesto");
|
||||
if (next === oldValue) return;
|
||||
|
||||
await db
|
||||
.update(tags)
|
||||
.set({ name: next })
|
||||
.where(and(eq(tags.entity_type, LEADS_TAG_ENTITY), eq(tags.name, oldValue)));
|
||||
|
||||
revalidatePath("/admin/leads");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user