afcaf991b5
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>
186 lines
6.3 KiB
TypeScript
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í)
|
|
} |