From a9ca3f59f3e448e53b36a24434f2d731683f2052 Mon Sep 17 00:00:00 2001 From: esteban Date: Sun, 2 Aug 2026 23:51:44 -0500 Subject: [PATCH] add firts commit for lab data base --- .env | 10 +- .env.example | 7 + prisma.config.ts | 2 +- .../20260220004001_init/migration.sql | 0 .../20260220004224_add_contact/migration.sql | 0 .../migration.sql | 0 .../migration_lock.toml | 3 + .../20260803025550_init/migration.sql | 17 ++ .../migration.sql | 174 ++++++++++++++++++ prisma/migrations/migration_lock.toml | 2 +- prisma/schema.prisma | 118 +++++++++++- src/pages/api/formulario/send.ts | 11 +- src/pages/api/lib/formularioDb.ts | 103 +++++++++++ src/pages/api/lib/prisma.ts | 2 +- 14 files changed, 440 insertions(+), 9 deletions(-) rename prisma/{migrations => migrations-sqlite-backup}/20260220004001_init/migration.sql (100%) rename prisma/{migrations => migrations-sqlite-backup}/20260220004224_add_contact/migration.sql (100%) rename prisma/{migrations => migrations-sqlite-backup}/20260220005204_make_email_unique/migration.sql (100%) create mode 100644 prisma/migrations-sqlite-backup/migration_lock.toml create mode 100644 prisma/migrations/20260803025550_init/migration.sql create mode 100644 prisma/migrations/20260803032753_add_form_models/migration.sql create mode 100644 src/pages/api/lib/formularioDb.ts diff --git a/.env b/.env index b8af0aa..eba0594 100644 --- a/.env +++ b/.env @@ -18,8 +18,6 @@ EMAIL_API_KEY=9UShpS8oh5Iun92TJSfevElI3Lp99TCv # server with the prisma dev CLI command, when not choosing any non-default ports or settings. The API key, unlike the # one found in a remote Prisma Postgres URL, does not contain any sensitive information. -DATABASE_URL="file:./dev.db" - N8N_WEBHOOK_URL="https://flows2.carpa.com/webhook/news-summary" N8N_API_KEY="sk_live_JwjpTdZrLVvijCKuWylWZbUuhBm6zPDH" @@ -33,3 +31,11 @@ TURNSTILE_SITE_KEY=0x4AAAAAAD9IYWC6HPflpneO # Cloudflare Turnstile — secret key (server-side, keep safe!) # In production, set this as a Gitea Actions secret named TURNSTILE_SECRET_KEY TURNSTILE_SECRET_KEY=0x4AAAAAAD9IYXASjQSpmo2iS902SDoXnT4 + +# Supabase PostgreSQL +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public" +DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public" +SUPABASE_URL="http://localhost:8000" +SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" +SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" +POSTGRES_PASSWORD=postgres diff --git a/.env.example b/.env.example index 86e3c4d..335063f 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,10 @@ TURNSTILE_SITE_KEY=0x4AA... # Cloudflare Turnstile — secret key (server-side, keep safe!) # In production, set this as a Gitea Actions secret named TURNSTILE_SECRET_KEY TURNSTILE_SECRET_KEY=0x4AA... + +# Supabase PostgreSQL +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public" +DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public" +SUPABASE_URL="http://localhost:8000" +SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" +SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" diff --git a/prisma.config.ts b/prisma.config.ts index 8ded1a5..8705955 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -11,6 +11,6 @@ export default defineConfig({ }, engine: "classic", datasource: { - url: env("DATABASE_URL"), + url: env("DIRECT_URL") || env("DATABASE_URL"), }, }); diff --git a/prisma/migrations/20260220004001_init/migration.sql b/prisma/migrations-sqlite-backup/20260220004001_init/migration.sql similarity index 100% rename from prisma/migrations/20260220004001_init/migration.sql rename to prisma/migrations-sqlite-backup/20260220004001_init/migration.sql diff --git a/prisma/migrations/20260220004224_add_contact/migration.sql b/prisma/migrations-sqlite-backup/20260220004224_add_contact/migration.sql similarity index 100% rename from prisma/migrations/20260220004224_add_contact/migration.sql rename to prisma/migrations-sqlite-backup/20260220004224_add_contact/migration.sql diff --git a/prisma/migrations/20260220005204_make_email_unique/migration.sql b/prisma/migrations-sqlite-backup/20260220005204_make_email_unique/migration.sql similarity index 100% rename from prisma/migrations/20260220005204_make_email_unique/migration.sql rename to prisma/migrations-sqlite-backup/20260220005204_make_email_unique/migration.sql diff --git a/prisma/migrations-sqlite-backup/migration_lock.toml b/prisma/migrations-sqlite-backup/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations-sqlite-backup/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/migrations/20260803025550_init/migration.sql b/prisma/migrations/20260803025550_init/migration.sql new file mode 100644 index 0000000..b0fd2e1 --- /dev/null +++ b/prisma/migrations/20260803025550_init/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Contact" ( + "id" SERIAL NOT NULL, + "nombre" TEXT NOT NULL, + "email" TEXT NOT NULL, + "mensaje" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Contact_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Contact_email_key" ON "Contact"("email"); + +-- CreateIndex +CREATE INDEX "Contact_email_createdAt_idx" ON "Contact"("email", "createdAt"); diff --git a/prisma/migrations/20260803032753_add_form_models/migration.sql b/prisma/migrations/20260803032753_add_form_models/migration.sql new file mode 100644 index 0000000..4bef4e8 --- /dev/null +++ b/prisma/migrations/20260803032753_add_form_models/migration.sql @@ -0,0 +1,174 @@ +/* + Warnings: + + - You are about to drop the `Contact` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "Contact"; + +-- CreateTable +CREATE TABLE "contact" ( + "id" SERIAL NOT NULL, + "nombre" TEXT NOT NULL, + "email" TEXT NOT NULL, + "mensaje" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "contact_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "formularios" ( + "id" UUID NOT NULL, + "numero_voluntario" SERIAL NOT NULL, + "nombre" TEXT NOT NULL, + "segundo_nombre" TEXT, + "apellido" TEXT NOT NULL, + "documento_nro" TEXT, + "documento_tipo" TEXT, + "correo" TEXT NOT NULL, + "fecha_nacimiento" DATE, + "nacionalidad" TEXT, + "sexo" TEXT, + "direccion" TEXT, + "ciudad" TEXT, + "estado" TEXT, + "pais" TEXT, + "codigo_postal" TEXT, + "telefono" TEXT, + "whatsapp" TEXT, + "profesion" TEXT, + "lugar_trabajo" TEXT, + "nivel_academico" TEXT, + "status" TEXT NOT NULL DEFAULT 'pendiente', + "form_version" TEXT, + "email_sent_at" TIMESTAMPTZ, + "submitted_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "formularios_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "respuestas" ( + "id" UUID NOT NULL, + "formulario_id" UUID NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "seccion" TEXT NOT NULL DEFAULT '', + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "respuestas_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "secciones" ( + "id" UUID NOT NULL, + "title_key" TEXT NOT NULL, + "title" TEXT, + "orden" INTEGER NOT NULL DEFAULT 0, + "columns" INTEGER NOT NULL DEFAULT 1, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "secciones_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "campos" ( + "id" UUID NOT NULL, + "key" TEXT NOT NULL, + "label_key" TEXT NOT NULL, + "tipo" TEXT NOT NULL, + "seccion_id" UUID, + "required" BOOLEAN NOT NULL DEFAULT false, + "orden" INTEGER NOT NULL DEFAULT 0, + "options" JSONB, + "source" TEXT, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "campos_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "form_versions" ( + "id" UUID NOT NULL, + "version" TEXT NOT NULL, + "config" JSONB NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "form_versions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "admins" ( + "id" UUID NOT NULL, + "auth_user_id" UUID NOT NULL, + "email" TEXT NOT NULL, + "nombre" TEXT NOT NULL, + "rol" TEXT NOT NULL DEFAULT 'coordinador', + "activo" BOOLEAN NOT NULL DEFAULT true, + "ultimo_acceso" TIMESTAMPTZ, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "admins_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "contact_email_key" ON "contact"("email"); + +-- CreateIndex +CREATE INDEX "contact_email_createdAt_idx" ON "contact"("email", "createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "formularios_numero_voluntario_key" ON "formularios"("numero_voluntario"); + +-- CreateIndex +CREATE INDEX "formularios_correo_idx" ON "formularios"("correo"); + +-- CreateIndex +CREATE INDEX "formularios_status_idx" ON "formularios"("status"); + +-- CreateIndex +CREATE INDEX "formularios_pais_idx" ON "formularios"("pais"); + +-- CreateIndex +CREATE INDEX "formularios_submitted_at_idx" ON "formularios"("submitted_at"); + +-- CreateIndex +CREATE INDEX "respuestas_formulario_id_idx" ON "respuestas"("formulario_id"); + +-- CreateIndex +CREATE INDEX "respuestas_key_idx" ON "respuestas"("key"); + +-- CreateIndex +CREATE INDEX "respuestas_seccion_idx" ON "respuestas"("seccion"); + +-- CreateIndex +CREATE INDEX "secciones_orden_idx" ON "secciones"("orden"); + +-- CreateIndex +CREATE UNIQUE INDEX "campos_key_key" ON "campos"("key"); + +-- CreateIndex +CREATE INDEX "campos_seccion_id_idx" ON "campos"("seccion_id"); + +-- CreateIndex +CREATE INDEX "campos_tipo_idx" ON "campos"("tipo"); + +-- CreateIndex +CREATE UNIQUE INDEX "admins_auth_user_id_key" ON "admins"("auth_user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "admins_email_key" ON "admins"("email"); + +-- CreateIndex +CREATE INDEX "admins_email_idx" ON "admins"("email"); + +-- AddForeignKey +ALTER TABLE "respuestas" ADD CONSTRAINT "respuestas_formulario_id_fkey" FOREIGN KEY ("formulario_id") REFERENCES "formularios"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "campos" ADD CONSTRAINT "campos_seccion_id_fkey" FOREIGN KEY ("seccion_id") REFERENCES "secciones"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index 2a5a444..044d57c 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) -provider = "sqlite" +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2aa9ec2..2516d92 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,8 +3,9 @@ generator client { } datasource db { - provider = "sqlite" - url = env("DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") } model Contact { @@ -16,4 +17,115 @@ model Contact { updatedAt DateTime @updatedAt @@index([email, createdAt]) -} \ No newline at end of file + @@map("contact") +} + +model Formulario { + id String @id @default(uuid()) @db.Uuid + numero_voluntario Int @unique @default(autoincrement()) + nombre String + segundo_nombre String? + apellido String + documento_nro String? + documento_tipo String? + correo String + fecha_nacimiento DateTime? @db.Date + nacionalidad String? + sexo String? + direccion String? + ciudad String? + estado String? + pais String? + codigo_postal String? + telefono String? + whatsapp String? + profesion String? + lugar_trabajo String? + nivel_academico String? + status String @default("pendiente") + form_version String? + email_sent_at DateTime? @db.Timestamptz + submitted_at DateTime @default(now()) @db.Timestamptz + updated_at DateTime @updatedAt @db.Timestamptz + + respuestas Respuesta[] + + @@index([correo]) + @@index([status]) + @@index([pais]) + @@index([submitted_at]) + @@map("formularios") +} + +model Respuesta { + id String @id @default(uuid()) @db.Uuid + formulario_id String @db.Uuid + key String + value String + seccion String @default("") + created_at DateTime @default(now()) @db.Timestamptz + + formulario Formulario @relation(fields: [formulario_id], references: [id], onDelete: Cascade) + + @@index([formulario_id]) + @@index([key]) + @@index([seccion]) + @@map("respuestas") +} + +model Seccion { + id String @id @default(uuid()) @db.Uuid + title_key String + title String? + orden Int @default(0) + columns Int @default(1) + created_at DateTime @default(now()) @db.Timestamptz + + campos Campo[] + + @@index([orden]) + @@map("secciones") +} + +model Campo { + id String @id @default(uuid()) @db.Uuid + key String @unique + label_key String + tipo String + seccion_id String? @db.Uuid + required Boolean @default(false) + orden Int @default(0) + options Json? @db.JsonB + source String? + created_at DateTime @default(now()) @db.Timestamptz + + seccion Seccion? @relation(fields: [seccion_id], references: [id], onDelete: SetNull) + + @@index([seccion_id]) + @@index([tipo]) + @@map("campos") +} + +model FormVersion { + id String @id @default(uuid()) @db.Uuid + version String + config Json @db.JsonB + active Boolean @default(false) + created_at DateTime @default(now()) @db.Timestamptz + + @@map("form_versions") +} + +model Admin { + id String @id @default(uuid()) @db.Uuid + auth_user_id String @unique @db.Uuid + email String @unique + nombre String + rol String @default("coordinador") + activo Boolean @default(true) + ultimo_acceso DateTime? @db.Timestamptz + created_at DateTime @default(now()) @db.Timestamptz + + @@index([email]) + @@map("admins") +} diff --git a/src/pages/api/formulario/send.ts b/src/pages/api/formulario/send.ts index fdb9b40..394b917 100644 --- a/src/pages/api/formulario/send.ts +++ b/src/pages/api/formulario/send.ts @@ -1,6 +1,7 @@ import type { APIRoute } from "astro"; import { appendRegistrationToSheet } from "../lib/googleSheets"; import { sendEmailCf } from "../lib/email"; +import { saveFormularioToSupabase } from "../lib/formularioDb"; export const prerender = false; const COLUMNS = [ @@ -101,6 +102,11 @@ export const POST: APIRoute = async ({ request }) => { timeZone: "America/Puerto_Rico", }); + const form = await saveFormularioToSupabase( + formData, + body.responses || [] + ); + const row = COLUMNS.map((key) => flattenValue(formData[key])); row.push(timestamp); @@ -111,7 +117,10 @@ export const POST: APIRoute = async ({ request }) => { apellidos: formData.apellido, }, formData.correo, "CENTRO_DEL_REINO_PAZ_Y_JUSTICIA_POSTULACION"); - return Response.json(result); + return Response.json({ + ...result, + numero_voluntario: form.numero_voluntario, + }); } catch (error) { console.error("Error en /api/formulario/send:", error); return Response.json( diff --git a/src/pages/api/lib/formularioDb.ts b/src/pages/api/lib/formularioDb.ts new file mode 100644 index 0000000..ea9ffec --- /dev/null +++ b/src/pages/api/lib/formularioDb.ts @@ -0,0 +1,103 @@ +import { prisma } from "./prisma"; + +type FormData = Record; +type RespuestaPayload = { + key: string; + value: unknown; + section_id?: string; +}; + +const CORE_KEYS = new Set([ + "nombre", + "segundo_nombre", + "apellido", + "documentos", + "correo", + "fecha_nacimiento", + "nacionalidad", + "sexo", + "direccion_completa", + "ciudad", + "estado", + "pais", + "postal", + "telefono", + "whatsapp", + "profesion", + "lugar_trabajo_actual", + "nivel_academico", + "nombre_completo", + "confirmar_nombre", + "confirmar_firma", +]); + +function isBlank(v: unknown): boolean { + return v === undefined || v === null || v === ""; +} + +export async function saveFormularioToSupabase( + formData: FormData, + responses: RespuestaPayload[] +) { + const core = { + nombre: (formData.nombre as string) ?? "", + segundo_nombre: (formData.segundo_nombre as string) || null, + apellido: (formData.apellido as string) ?? "", + documento_nro: (formData.documentos as string) || null, + correo: (formData.correo as string) ?? "", + fecha_nacimiento: formData.fecha_nacimiento + ? new Date(formData.fecha_nacimiento as string) + : null, + nacionalidad: (formData.nacionalidad as string) || null, + sexo: (formData.sexo as string) || null, + direccion: (formData.direccion_completa as string) || null, + ciudad: (formData.ciudad as string) || null, + estado: (formData.estado as string) || null, + pais: (formData.pais as string) || null, + codigo_postal: (formData.postal as string) || null, + telefono: (formData.telefono as string) || null, + whatsapp: (formData.whatsapp as string) || null, + profesion: (formData.profesion as string) || null, + lugar_trabajo: (formData.lugar_trabajo_actual as string) || null, + nivel_academico: (formData.nivel_academico as string) || null, + }; + + const respRows: { + key: string; + value: string; + seccion: string; + }[] = []; + + for (const r of responses || []) { + if (CORE_KEYS.has(r.key)) continue; + if (isBlank(r.value)) continue; + + if (Array.isArray(r.value)) { + for (const v of r.value) { + if (!isBlank(v)) { + respRows.push({ + key: r.key, + value: String(v), + seccion: r.section_id ?? "", + }); + } + } + } else { + respRows.push({ + key: r.key, + value: String(r.value), + seccion: r.section_id ?? "", + }); + } + } + + return prisma.$transaction(async (tx) => { + const form = await tx.formulario.create({ data: core }); + if (respRows.length) { + await tx.respuesta.createMany({ + data: respRows.map((r) => ({ ...r, formulario_id: form.id })), + }); + } + return form; + }); +} diff --git a/src/pages/api/lib/prisma.ts b/src/pages/api/lib/prisma.ts index fa60f1f..b950897 100644 --- a/src/pages/api/lib/prisma.ts +++ b/src/pages/api/lib/prisma.ts @@ -7,4 +7,4 @@ const globalForPrisma = globalThis as unknown as { export const prisma = globalForPrisma.prisma ?? new PrismaClient(); -if (import.meta.env.DEV) globalForPrisma.prisma = prisma; \ No newline at end of file +if (import.meta.env?.DEV) globalForPrisma.prisma = prisma; \ No newline at end of file