Files
flux-srl/src/app/hq-command/dashboard/applications/actions.ts
T
davidherran afcaf991b5 feat(applications): drag-to-reorder on the public site
Editors can now control the order applications appear in, the same way they
already reorder Hero slides.

- Application gains an `order Int @default(0)` column (additive migration
  20260605120000_add_application_order, IF NOT EXISTS, safe for deploy) plus
  an (isActive, order) index.
- New reorderApplications(orderedSlugs) server action — single $transaction
  renumbering, mirrors reorderHeroSlides.
- HQ applications panel: rows are now draggable by a grip handle (HTML5 DnD,
  optimistic local reorder, persisted on drop, toast feedback).
- All public-facing queries now order by [order asc, createdAt asc]: home
  ApplicationsDashboard + GlobalOperations, the footer apps list, and the
  HQ list itself. Existing rows default to 0 so current order is preserved
  until the editor drags something.

Verified: production build compiles, TypeScript clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:04:10 -05:00

186 lines
6.3 KiB
TypeScript

//src/app/hq-command/dashboard/applications/actions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { unstable_noStore as noStore } from "next/cache"; // 🔥 ANTI-CACHÉ
// 🔥 IMPORTAMOS EL TRADUCTOR DE IA
import { translateContentForCMS } from "@/lib/aiTranslator";
import { ensureAssetFolders } from "@/lib/assetFolders";
const generateSlug = (title: string) => {
return title.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
};
// 1. OBTENER LA LISTA DE APLICACIONES
export async function getApplications() {
noStore();
try {
const apps = await prisma.application.findMany({
orderBy: [{ order: "asc" }, { createdAt: "asc" }]
});
return { success: true, apps };
} catch (error) {
return { error: "Failed to fetch applications." };
}
}
// 2. OBTENER UNA APLICACIÓN ESPECÍFICA
export async function getApplicationBySlug(slug: string) {
try {
const app = await prisma.application.findUnique({
where: { slug }
});
if (!app) return { error: "Application not found." };
return { success: true, app };
} catch (error) {
return { error: "Failed to fetch application details." };
}
}
// 3. CREAR NUEVA APLICACIÓN (¡Ahora con IA!)
export async function createApplication(formData: FormData) {
try {
const title = formData.get("title") as string;
const subtitle = formData.get("subtitle") as string;
const category = formData.get("category") as string;
// Capturamos el switch de IA
const autoTranslate = formData.get("autoTranslate") === "on";
const shortDescription = "New application ready to be configured.";
const slug = generateSlug(title);
let translationsJson = "{}";
// 🔥 LA MAGIA DE LA IA EN LA CREACIÓN 🔥
if (autoTranslate) {
const aiResult = await translateContentForCMS({
title, subtitle, category, shortDescription
});
if (aiResult) translationsJson = JSON.stringify(aiResult);
}
await prisma.application.create({
data: {
slug, title, subtitle, category,
shortDescription,
heroDescription: "", sectionsJson: "[]", advantagesJson: "[]", datasheetJson: "{}", dashboardMetricsJson: "[]",
isActive: true,
translationsJson
}
});
// Pre-create the asset bucket folders so the editor's first upload
// (videos, renders, gallery, datasheet) lands somewhere that exists.
ensureAssetFolders("applications", slug);
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (error) {
return { error: "Failed to create application. Title might already exist." };
}
}
// 4. ACTUALIZAR TODA LA INFORMACIÓN (¡Traduciendo JSONs completos!)
export async function updateApplicationData(formData: FormData) {
try {
const slug = formData.get("slug") as string;
const title = formData.get("title") as string;
const subtitle = formData.get("subtitle") as string;
const category = formData.get("category") as string;
const shortDescription = formData.get("shortDescription") as string;
const heroDescription = formData.get("heroDescription") as string;
const sectionsJson = formData.get("sectionsJson") as string;
const advantagesJson = formData.get("advantagesJson") as string;
const datasheetJson = formData.get("datasheetJson") as string || "{}";
const dashboardMetricsJson = formData.get("dashboardMetricsJson") as string || "[]";
const autoTranslate = formData.get("autoTranslate") === "on";
let updateData: any = {
title, subtitle, category, shortDescription, heroDescription,
sectionsJson, advantagesJson, datasheetJson, dashboardMetricsJson
};
// 🔥 LA MAGIA DE LA IA PARA EL CONTENIDO PROFUNDO 🔥
// Nota: Le mandamos los JSON stringificados. GPT-4o los traducirá y nos los devolverá con la misma estructura.
if (autoTranslate) {
const aiResult = await translateContentForCMS({
title, subtitle, category, shortDescription, heroDescription,
sectionsJson, advantagesJson, dashboardMetricsJson
});
if (aiResult) {
updateData.translationsJson = JSON.stringify(aiResult);
}
}
await prisma.application.update({
where: { slug },
data: updateData
});
revalidatePath(`/applications/${slug}`);
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (error) {
console.error("Update Error:", error);
return { error: "Failed to update application data." };
}
}
// 5. OCULTAR / MOSTRAR APLICACIÓN
export async function toggleApplication(slug: string, currentStatus: boolean) {
try {
await prisma.application.update({
where: { slug },
data: { isActive: !currentStatus }
});
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (e) {
return { error: "Failed to toggle status." };
}
}
// 6. ELIMINAR APLICACIÓN
export async function deleteApplication(slug: string) {
try {
await prisma.application.delete({ where: { slug } });
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (e) {
return { error: "Failed to delete application." };
}
}
// 6b. REORDENAR APLICACIONES (drag-to-reorder, mismo patrón que HeroSlide)
// Recibe la lista de slugs en el nuevo orden y renumera el campo `order`
// en una sola transacción atómica.
export async function reorderApplications(orderedSlugs: string[]) {
try {
await prisma.$transaction(
orderedSlugs.map((slug, idx) =>
prisma.application.update({ where: { slug }, data: { order: idx } })
)
);
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (e) {
return { error: "Failed to reorder applications." };
}
}
// 7. LA SEMILLA: AUTO-POBLAR LA BASE DE DATOS (Intacto)
export async function seedInitialApplications() {
// ... Tu código actual de la semilla se queda exactamente igual ...
// (Para mantener este mensaje limpio, asume que la función de seedInitialApplications() que ya tienes va aquí)
}