initial push

This commit is contained in:
Marcelo Dares
2026-03-15 15:03:56 +01:00
parent d48b9d5352
commit 65aaf9275e
146 changed files with 70245 additions and 100 deletions

View File

@@ -0,0 +1,52 @@
import { NextResponse } from "next/server";
import { ContentPageType } from "@prisma/client";
import { requireAdminApiUser } from "@/lib/auth/admin";
import { prisma } from "@/lib/prisma";
import { buildAppUrl } from "@/lib/http/url";
function redirectTo(request: Request, status: string) {
return NextResponse.redirect(buildAppUrl(request, "/admin", { status }));
}
function asString(formData: FormData, key: string) {
const value = formData.get(key);
return typeof value === "string" ? value.trim() : "";
}
export async function POST(request: Request) {
const adminUser = await requireAdminApiUser();
if (!adminUser) {
return redirectTo(request, "admin_error");
}
const formData = await request.formData();
const typeValue = asString(formData, "type");
const slug = asString(formData, "slug");
const title = asString(formData, "title");
const content = asString(formData, "content");
const sortOrder = Number.parseInt(asString(formData, "sortOrder") || "0", 10);
const isPublished = formData.get("isPublished") === "on";
if (!slug || !title || !content || (typeValue !== ContentPageType.FAQ && typeValue !== ContentPageType.MANUAL)) {
return redirectTo(request, "admin_error");
}
try {
await prisma.contentPage.create({
data: {
type: typeValue as ContentPageType,
slug,
title,
content,
sortOrder: Number.isNaN(sortOrder) ? 0 : sortOrder,
isPublished,
},
});
return redirectTo(request, "content_created");
} catch {
return redirectTo(request, "admin_error");
}
}

View File

@@ -0,0 +1,54 @@
import { NextResponse } from "next/server";
import { ContentPageType } from "@prisma/client";
import { requireAdminApiUser } from "@/lib/auth/admin";
import { prisma } from "@/lib/prisma";
import { buildAppUrl } from "@/lib/http/url";
function redirectTo(request: Request, status: string) {
return NextResponse.redirect(buildAppUrl(request, "/admin", { status }));
}
function asString(formData: FormData, key: string) {
const value = formData.get(key);
return typeof value === "string" ? value.trim() : "";
}
export async function POST(request: Request) {
const adminUser = await requireAdminApiUser();
if (!adminUser) {
return redirectTo(request, "admin_error");
}
const formData = await request.formData();
const id = asString(formData, "id");
const typeValue = asString(formData, "type");
const slug = asString(formData, "slug");
const title = asString(formData, "title");
const content = asString(formData, "content");
const sortOrder = Number.parseInt(asString(formData, "sortOrder") || "0", 10);
const isPublished = formData.get("isPublished") === "on";
if (!id || !slug || !title || !content || (typeValue !== ContentPageType.FAQ && typeValue !== ContentPageType.MANUAL)) {
return redirectTo(request, "admin_error");
}
try {
await prisma.contentPage.update({
where: { id },
data: {
type: typeValue as ContentPageType,
slug,
title,
content,
sortOrder: Number.isNaN(sortOrder) ? 0 : sortOrder,
isPublished,
},
});
return redirectTo(request, "content_updated");
} catch {
return redirectTo(request, "admin_error");
}
}

View File

@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
import { OverallScoreMethod } from "@prisma/client";
import { requireAdminApiUser } from "@/lib/auth/admin";
import { DEFAULT_SCORING_CONFIG } from "@/lib/scoring-config";
import { prisma } from "@/lib/prisma";
import { buildAppUrl } from "@/lib/http/url";
function redirectTo(request: Request, status: string) {
return NextResponse.redirect(buildAppUrl(request, "/admin", { status }));
}
function asString(formData: FormData, key: string) {
const value = formData.get(key);
return typeof value === "string" ? value.trim() : "";
}
export async function POST(request: Request) {
const adminUser = await requireAdminApiUser();
if (!adminUser) {
return redirectTo(request, "admin_error");
}
const formData = await request.formData();
const thresholdCandidate = Number.parseInt(asString(formData, "lowScoreThreshold") || "70", 10);
const lowScoreThreshold = Number.isNaN(thresholdCandidate) ? 70 : Math.min(100, Math.max(0, thresholdCandidate));
const methodCandidate = asString(formData, "overallScoreMethod");
const allowedMethods = Object.values(OverallScoreMethod);
const overallScoreMethod = allowedMethods.includes(methodCandidate as OverallScoreMethod)
? (methodCandidate as OverallScoreMethod)
: OverallScoreMethod.EQUAL_ALL_MODULES;
const moduleWeights: Record<string, number> = {};
for (const [key, rawValue] of formData.entries()) {
if (!key.startsWith("weight:")) {
continue;
}
const moduleKey = key.replace("weight:", "").trim();
const numericValue = Number.parseFloat(typeof rawValue === "string" ? rawValue : String(rawValue));
if (moduleKey && !Number.isNaN(numericValue) && Number.isFinite(numericValue) && numericValue > 0) {
moduleWeights[moduleKey] = numericValue;
}
}
try {
await prisma.scoringConfig.upsert({
where: {
key: DEFAULT_SCORING_CONFIG.key,
},
update: {
lowScoreThreshold,
overallScoreMethod,
moduleWeights,
},
create: {
key: DEFAULT_SCORING_CONFIG.key,
lowScoreThreshold,
overallScoreMethod,
moduleWeights,
},
});
return redirectTo(request, "scoring_updated");
} catch {
return redirectTo(request, "admin_error");
}
}

View File

@@ -0,0 +1,42 @@
import { NextResponse } from "next/server";
import { requireAdminApiUser } from "@/lib/auth/admin";
import { runDailyLicitationsSync } from "@/lib/licitations/sync";
export const runtime = "nodejs";
export async function POST(request: Request) {
const user = await requireAdminApiUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const body = (await request.json().catch(() => ({}))) as {
municipalityId?: string;
limit?: number;
skip?: number;
targetYear?: number;
includePnt?: boolean;
force?: boolean;
};
const payload = await runDailyLicitationsSync({
municipalityId: typeof body.municipalityId === "string" ? body.municipalityId : undefined,
limit: typeof body.limit === "number" ? body.limit : undefined,
skip: typeof body.skip === "number" ? body.skip : undefined,
targetYear: typeof body.targetYear === "number" ? body.targetYear : undefined,
includePnt: body.includePnt === true,
force: body.force === true,
});
return NextResponse.json({ ok: true, payload });
} catch (error) {
return NextResponse.json(
{
error: error instanceof Error ? error.message : "No se pudo ejecutar el sync de licitaciones.",
},
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,38 @@
import { NextResponse } from "next/server";
import { UserRole } from "@prisma/client";
import { requireAdminApiUser } from "@/lib/auth/admin";
import { prisma } from "@/lib/prisma";
import { buildAppUrl } from "@/lib/http/url";
function redirectTo(request: Request, status: string) {
return NextResponse.redirect(buildAppUrl(request, "/admin", { status }));
}
export async function POST(request: Request) {
const adminUser = await requireAdminApiUser();
if (!adminUser) {
return redirectTo(request, "admin_error");
}
const formData = await request.formData();
const userId = typeof formData.get("userId") === "string" ? formData.get("userId")!.toString() : "";
const roleValue = typeof formData.get("role") === "string" ? formData.get("role")!.toString() : "";
if (!userId || (roleValue !== UserRole.USER && roleValue !== UserRole.ADMIN)) {
return redirectTo(request, "admin_error");
}
try {
await prisma.user.update({
where: { id: userId },
data: {
role: roleValue as UserRole,
},
});
return redirectTo(request, "role_updated");
} catch {
return redirectTo(request, "admin_error");
}
}