feat(11-02): add inline-edit, tag, and quick-add server actions

- updateServiceField: single-field inline edit (name, description,
  category, unit_price, active) with per-field validation
- addTagToService / removeTagFromService: tag assignment scoped to
  entity_type="services" via onConflictDoNothing on the unique index
- quickAddService: creates a unit_price=0.00 row from name only (D-12)
- All actions admin-gated via requireAdmin() and revalidate /admin/catalog
This commit is contained in:
2026-06-13 15:39:55 +02:00
parent f7434102de
commit 445de856e4
+87 -2
View File
@@ -1,9 +1,9 @@
"use server"; "use server";
import { db } from "@/db"; import { db } from "@/db";
import { services } from "@/db/schema"; import { services, tags } from "@/db/schema";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { eq } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
@@ -65,3 +65,88 @@ export async function toggleServiceActive(serviceId: string, active: boolean) {
await db.update(services).set({ active }).where(eq(services.id, serviceId)); await db.update(services).set({ active }).where(eq(services.id, serviceId));
revalidatePath("/admin/catalog"); revalidatePath("/admin/catalog");
} }
// ── Inline-edit, tag, and quick-add actions (Phase 11 database-view) ────────
const EDITABLE_FIELDS = ["name", "description", "category", "unit_price", "active"] as const;
type EditableField = (typeof EDITABLE_FIELDS)[number];
export async function updateServiceField(
serviceId: string,
fieldName: EditableField,
value: string | boolean
) {
await requireAdmin();
if (!EDITABLE_FIELDS.includes(fieldName)) {
throw new Error(`Campo non editabile: ${fieldName}`);
}
if (fieldName === "name") {
const s = String(value).trim();
if (s.length === 0) throw new Error("Nome richiesto");
await db.update(services).set({ name: s }).where(eq(services.id, serviceId));
} else if (fieldName === "description") {
const s = String(value).trim();
await db.update(services).set({ description: s || null }).where(eq(services.id, serviceId));
} else if (fieldName === "category") {
const s = String(value).trim();
await db.update(services).set({ category: s || null }).where(eq(services.id, serviceId));
} else if (fieldName === "unit_price") {
const num = parseFloat(String(value));
if (isNaN(num) || num < 0) throw new Error("Prezzo invalido");
await db.update(services).set({ unit_price: num.toFixed(2) }).where(eq(services.id, serviceId));
} else if (fieldName === "active") {
const boolValue = typeof value === "boolean" ? value : value === "true";
await db.update(services).set({ active: boolValue }).where(eq(services.id, serviceId));
}
revalidatePath("/admin/catalog");
}
export async function addTagToService(serviceId: string, tagName: string) {
await requireAdmin();
const trimmed = tagName.trim();
if (trimmed.length === 0) throw new Error("Nome tag richiesto");
await db
.insert(tags)
.values({
entity_type: "services",
entity_id: serviceId,
name: trimmed,
})
.onConflictDoNothing();
revalidatePath("/admin/catalog");
}
export async function removeTagFromService(serviceId: string, tagName: string) {
await requireAdmin();
await db
.delete(tags)
.where(
and(
eq(tags.entity_type, "services"),
eq(tags.entity_id, serviceId),
eq(tags.name, tagName)
)
);
revalidatePath("/admin/catalog");
}
export async function quickAddService(name: string) {
await requireAdmin();
const trimmed = name.trim();
if (trimmed.length === 0) throw new Error("Nome richiesto");
await db.insert(services).values({
name: trimmed,
unit_price: "0.00",
active: true,
});
revalidatePath("/admin/catalog");
}