From 08a5567d60cd175c5a5f8ea7cc58c9c396f30c4a Mon Sep 17 00:00:00 2001 From: Sergio Ramirez Date: Thu, 22 Jan 2026 14:28:24 -0400 Subject: [PATCH] mejoras al formulario de registro organizaciones productivas --- apps/api/.gitignore | 3 + apps/api/package.json | 3 +- .../src/common/pipes/image-processing.pipe.ts | 36 + .../migrations/0010_dashing_bishop.sql | 4 + .../migrations/meta/0010_snapshot.json | 1850 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + apps/api/src/database/schema/surveys.ts | 45 +- .../training/dto/create-training.dto.ts | 215 +- .../features/training/training.controller.ts | 156 +- .../src/features/training/training.service.ts | 479 +++-- .../dashboard/formulario/editar/[id]/page.tsx | 34 + .../app/dashboard/formulario/nuevo/page.tsx | 20 + apps/web/app/dashboard/formulario/page.tsx | 43 +- apps/web/components/layout/app-sidebar.tsx | 2 +- apps/web/constants/routes.ts | 10 +- .../auth/components/user-auth-form.tsx | 2 +- .../auth/components/user-register-form.tsx | 4 +- .../training/actions/training-actions.ts | 236 ++- .../feactures/training/components/form.tsx | 1359 +++++++----- .../training/components/training-header.tsx | 13 + .../training/components/training-list.tsx | 39 + .../training-tables/cell-action.tsx | 113 + .../components/training-tables/columns.tsx | 45 + .../training-tables/training-table-action.tsx | 31 + .../use-training-table-filters.tsx | 40 + .../components/training-view-modal.tsx | 285 +++ .../feactures/training/hooks/use-training.ts | 55 +- .../feactures/training/schemas/training.ts | 144 +- .../components/admin/create-user-form.tsx | 23 +- .../components/admin/update-user-form.tsx | 31 +- .../users/components/user-profile.tsx | 66 +- apps/web/package.json | 6 +- assets/lifecycle.png | Bin 135801 -> 0 bytes assets/preview.png | Bin 19660 -> 0 bytes 34 files changed, 4297 insertions(+), 1102 deletions(-) create mode 100644 apps/api/src/common/pipes/image-processing.pipe.ts create mode 100644 apps/api/src/database/migrations/0010_dashing_bishop.sql create mode 100644 apps/api/src/database/migrations/meta/0010_snapshot.json create mode 100644 apps/web/app/dashboard/formulario/editar/[id]/page.tsx create mode 100644 apps/web/app/dashboard/formulario/nuevo/page.tsx create mode 100644 apps/web/feactures/training/components/training-header.tsx create mode 100644 apps/web/feactures/training/components/training-list.tsx create mode 100644 apps/web/feactures/training/components/training-tables/cell-action.tsx create mode 100644 apps/web/feactures/training/components/training-tables/columns.tsx create mode 100644 apps/web/feactures/training/components/training-tables/training-table-action.tsx create mode 100644 apps/web/feactures/training/components/training-tables/use-training-table-filters.tsx create mode 100644 apps/web/feactures/training/components/training-view-modal.tsx delete mode 100644 assets/lifecycle.png delete mode 100644 assets/preview.png diff --git a/apps/api/.gitignore b/apps/api/.gitignore index 4b56acf..750f933 100644 --- a/apps/api/.gitignore +++ b/apps/api/.gitignore @@ -54,3 +54,6 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Uploads +/uploads/training/* diff --git a/apps/api/package.json b/apps/api/package.json index 8cbcf80..22218ea 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -49,7 +49,8 @@ "pg": "8.13.3", "pino-pretty": "13.0.0", "reflect-metadata": "0.2.0", - "rxjs": "7.8.1" + "rxjs": "7.8.1", + "sharp": "^0.34.5" }, "devDependencies": { "@nestjs-modules/mailer": "^2.0.2", diff --git a/apps/api/src/common/pipes/image-processing.pipe.ts b/apps/api/src/common/pipes/image-processing.pipe.ts new file mode 100644 index 0000000..cdafef2 --- /dev/null +++ b/apps/api/src/common/pipes/image-processing.pipe.ts @@ -0,0 +1,36 @@ +import { Injectable, PipeTransform } from '@nestjs/common'; +import * as path from 'path'; +import sharp from 'sharp'; + +@Injectable() +export class ImageProcessingPipe implements PipeTransform { + async transform( + files: Express.Multer.File[] | Express.Multer.File, + ): Promise { + if (!files) return files; + + const processItem = async ( + file: Express.Multer.File, + ): Promise => { + const processedBuffer = await sharp(file.buffer) + .webp({ quality: 80 }) + .toBuffer(); + + const originalName = path.parse(file.originalname).name; + + return { + ...file, + buffer: processedBuffer, + originalname: `${originalName}.webp`, + mimetype: 'image/webp', + size: processedBuffer.length, + }; + }; + + if (Array.isArray(files)) { + return await Promise.all(files.map((file) => processItem(file))); + } + + return await processItem(files); + } +} diff --git a/apps/api/src/database/migrations/0010_dashing_bishop.sql b/apps/api/src/database/migrations/0010_dashing_bishop.sql new file mode 100644 index 0000000..61327fb --- /dev/null +++ b/apps/api/src/database/migrations/0010_dashing_bishop.sql @@ -0,0 +1,4 @@ +ALTER TABLE "training_surveys" ALTER COLUMN "current_status" SET DEFAULT 'ACTIVA';--> statement-breakpoint +ALTER TABLE "training_surveys" ALTER COLUMN "photo2" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "training_surveys" ALTER COLUMN "photo3" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "training_surveys" ADD COLUMN "product_count" integer DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/0010_snapshot.json b/apps/api/src/database/migrations/meta/0010_snapshot.json new file mode 100644 index 0000000..97dd5a5 --- /dev/null +++ b/apps/api/src/database/migrations/meta/0010_snapshot.json @@ -0,0 +1,1850 @@ +{ + "id": "3311eb1b-4df8-4914-85e0-4b84403b2404", + "prevId": "98995011-7d82-432c-970f-efbb710d1335", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_logs": { + "name": "activity_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "activityLogs_idx": { + "name": "activityLogs_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_logs_user_id_users_id_fk": { + "name": "activity_logs_user_id_users_id_fk", + "tableFrom": "activity_logs", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.roles": { + "name": "roles", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "roles_idx": { + "name": "roles_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_idx": { + "name": "sessions_idx", + "columns": [ + { + "expression": "session_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality": { + "name": "municipality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parish": { + "name": "parish", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_two_factor_enabled": { + "name": "is_two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_email_verified": { + "name": "is_email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_idx": { + "name": "users_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_state_states_id_fk": { + "name": "users_state_states_id_fk", + "tableFrom": "users", + "tableTo": "states", + "columnsFrom": [ + "state" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_municipality_municipalities_id_fk": { + "name": "users_municipality_municipalities_id_fk", + "tableFrom": "users", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_parish_parishes_id_fk": { + "name": "users_parish_parishes_id_fk", + "tableFrom": "users", + "tableTo": "parishes", + "columnsFrom": [ + "parish" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.user_role": { + "name": "user_role", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_role_idx": { + "name": "user_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_role_user_id_users_id_fk": { + "name": "user_role_user_id_users_id_fk", + "tableFrom": "user_role", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_role_role_id_roles_id_fk": { + "name": "user_role_role_id_roles_id_fk", + "tableFrom": "user_role", + "tableTo": "roles", + "schemaTo": "auth", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verificationToken": { + "name": "verificationToken", + "schema": "auth", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.category_type": { + "name": "category_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group": { + "name": "group", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "category_typeIx0": { + "name": "category_typeIx0", + "columns": [ + { + "expression": "group", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "category_typeIx1": { + "name": "category_typeIx1", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.localities": { + "name": "localities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parish_id": { + "name": "parish_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "localities_index_03": { + "name": "localities_index_03", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_00": { + "name": "localities_index_00", + "columns": [ + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_01": { + "name": "localities_index_01", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "localities_index_02": { + "name": "localities_index_02", + "columns": [ + { + "expression": "parish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "localities_state_id_states_id_fk": { + "name": "localities_state_id_states_id_fk", + "tableFrom": "localities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_municipality_id_municipalities_id_fk": { + "name": "localities_municipality_id_municipalities_id_fk", + "tableFrom": "localities", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "localities_parish_id_parishes_id_fk": { + "name": "localities_parish_id_parishes_id_fk", + "tableFrom": "localities", + "tableTo": "parishes", + "columnsFrom": [ + "parish_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "localities_name_unique": { + "name": "localities_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipalities": { + "name": "municipalities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_id": { + "name": "state_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "municipalities_index_00": { + "name": "municipalities_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipalities_state_id_states_id_fk": { + "name": "municipalities_state_id_states_id_fk", + "tableFrom": "municipalities", + "tableTo": "states", + "columnsFrom": [ + "state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parishes": { + "name": "parishes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "parishes_index_00": { + "name": "parishes_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parishes_municipality_id_municipalities_id_fk": { + "name": "parishes_municipality_id_municipalities_id_fk", + "tableFrom": "parishes", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.states": { + "name": "states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "states_index_00": { + "name": "states_index_00", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gallery": { + "name": "gallery", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'BORRADOR'" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "products_user_id_users_id_fk": { + "name": "products_user_id_users_id_fk", + "tableFrom": "products", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.answers_surveys": { + "name": "answers_surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "answers_index_00": { + "name": "answers_index_00", + "columns": [ + { + "expression": "answers", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_01": { + "name": "answers_index_01", + "columns": [ + { + "expression": "survey_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "answers_index_02": { + "name": "answers_index_02", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "answers_surveys_survey_id_surveys_id_fk": { + "name": "answers_surveys_survey_id_surveys_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "surveys", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "answers_surveys_user_id_users_id_fk": { + "name": "answers_surveys_user_id_users_id_fk", + "tableFrom": "answers_surveys", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.surveys": { + "name": "surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_audience": { + "name": "target_audience", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "questions": { + "name": "questions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "surveys_index_00": { + "name": "surveys_index_00", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.training_surveys": { + "name": "training_surveys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "firstname": { + "name": "firstname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastname": { + "name": "lastname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visit_date": { + "name": "visit_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality": { + "name": "municipality", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parish": { + "name": "parish", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "situr_code_commune": { + "name": "situr_code_commune", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "communal_council": { + "name": "communal_council", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "situr_code_communal_council": { + "name": "situr_code_communal_council", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_name": { + "name": "osp_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_address": { + "name": "osp_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_rif": { + "name": "osp_rif", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_type": { + "name": "osp_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "productive_activity": { + "name": "productive_activity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "financial_requirement_description": { + "name": "financial_requirement_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_status": { + "name": "current_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVA'" + }, + "company_constitution_year": { + "name": "company_constitution_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "producer_count": { + "name": "producer_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "product_count": { + "name": "product_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "product_description": { + "name": "product_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installed_capacity": { + "name": "installed_capacity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operational_capacity": { + "name": "operational_capacity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_fullname": { + "name": "osp_responsible_fullname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_cedula": { + "name": "osp_responsible_cedula", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_rif": { + "name": "osp_responsible_rif", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_phone": { + "name": "osp_responsible_phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "osp_responsible_email": { + "name": "osp_responsible_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "civil_state": { + "name": "civil_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family_burden": { + "name": "family_burden", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_children": { + "name": "number_of_children", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "general_observations": { + "name": "general_observations", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paralysis_reason": { + "name": "paralysis_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo1": { + "name": "photo1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo2": { + "name": "photo2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo3": { + "name": "photo3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "training_surveys_index_00": { + "name": "training_surveys_index_00", + "columns": [ + { + "expression": "firstname", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "training_surveys_state_states_id_fk": { + "name": "training_surveys_state_states_id_fk", + "tableFrom": "training_surveys", + "tableTo": "states", + "columnsFrom": [ + "state" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "training_surveys_municipality_municipalities_id_fk": { + "name": "training_surveys_municipality_municipalities_id_fk", + "tableFrom": "training_surveys", + "tableTo": "municipalities", + "columnsFrom": [ + "municipality" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "training_surveys_parish_parishes_id_fk": { + "name": "training_surveys_parish_parishes_id_fk", + "tableFrom": "training_surveys", + "tableTo": "parishes", + "columnsFrom": [ + "parish" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "auth.gender": { + "name": "gender", + "schema": "auth", + "values": [ + "FEMENINO", + "MASCULINO" + ] + }, + "public.nationality": { + "name": "nationality", + "schema": "public", + "values": [ + "VENEZOLANO", + "EXTRANJERO" + ] + }, + "auth.status": { + "name": "status", + "schema": "auth", + "values": [ + "ACTIVE", + "INACTIVE" + ] + } + }, + "schemas": { + "auth": "auth" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "auth.user_access_view": { + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n u.id AS user_id,\n u.username,\n u.email,\n u.fullname,\n r.id AS role_id,\n r.name AS role_name\nFROM\n auth.users u\nLEFT JOIN\n auth.user_role ur ON u.id = ur.user_id \nLEFT JOIN\n auth.roles r ON ur.role_id = r.id", + "name": "user_access_view", + "schema": "auth", + "isExisting": false, + "materialized": false + }, + "public.v_product_store": { + "columns": { + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "stock": { + "name": "stock", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url_img": { + "name": "url_img", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gallery": { + "name": "gallery", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fullname": { + "name": "fullname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n select p.id as product_id, p.title, p.description, p.price, p.stock, p.url_img, p.gallery, p.address, p.status, p.user_id, u.fullname, u.email, u.phone\n from products p\n left join auth.users as u on u.id = p.user_id", + "name": "v_product_store", + "schema": "public", + "isExisting": false, + "materialized": false + }, + "public.v_surveys": { + "columns": { + "survey_id": { + "name": "survey_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "closing_date": { + "name": "closing_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "target_audience": { + "name": "target_audience", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select id as survey_id, title, description, created_at, closing_date, target_audience from surveys\nwhere published = true", + "name": "v_surveys", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/database/migrations/meta/_journal.json b/apps/api/src/database/migrations/meta/_journal.json index 5d520e2..60493d1 100644 --- a/apps/api/src/database/migrations/meta/_journal.json +++ b/apps/api/src/database/migrations/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1764883378610, "tag": "0009_eminent_ares", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1769097895095, + "tag": "0010_dashing_bishop", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/database/schema/surveys.ts b/apps/api/src/database/schema/surveys.ts index 22d55d0..8b321cc 100644 --- a/apps/api/src/database/schema/surveys.ts +++ b/apps/api/src/database/schema/surveys.ts @@ -1,9 +1,8 @@ +import { sql } from 'drizzle-orm'; import * as t from 'drizzle-orm/pg-core'; -import { eq, lt, gte, ne, sql } from 'drizzle-orm'; import { timestamps } from '../timestamps'; import { users } from './auth'; -import { states, municipalities, parishes } from './general'; - +import { municipalities, parishes, states } from './general'; // Tabla surveys export const surveys = t.pgTable( @@ -19,9 +18,7 @@ export const surveys = t.pgTable( ...timestamps, }, (surveys) => ({ - surveysIndex: t - .index('surveys_index_00') - .on(surveys.title), + surveysIndex: t.index('surveys_index_00').on(surveys.title), }), ); @@ -55,9 +52,15 @@ export const trainingSurveys = t.pgTable( lastname: t.text('lastname').notNull(), visitDate: t.timestamp('visit_date').notNull(), // ubicacion - state: t.integer('state').references(() => states.id, { onDelete: 'set null' }), - municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }), - parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }), + state: t + .integer('state') + .references(() => states.id, { onDelete: 'set null' }), + municipality: t + .integer('municipality') + .references(() => municipalities.id, { onDelete: 'set null' }), + parish: t + .integer('parish') + .references(() => parishes.id, { onDelete: 'set null' }), siturCodeCommune: t.text('situr_code_commune').notNull(), communalCouncil: t.text('communal_council').notNull(), siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(), @@ -67,10 +70,13 @@ export const trainingSurveys = t.pgTable( ospRif: t.text('osp_rif').notNull(), ospType: t.text('osp_type').notNull(), productiveActivity: t.text('productive_activity').notNull(), - financialRequirementDescription: t.text('financial_requirement_description').notNull(), - currentStatus: t.text('current_status').notNull(), + financialRequirementDescription: t + .text('financial_requirement_description') + .notNull(), + currentStatus: t.text('current_status').notNull().default('ACTIVA'), companyConstitutionYear: t.integer('company_constitution_year').notNull(), producerCount: t.integer('producer_count').notNull(), + productCount: t.integer('product_count').notNull().default(0), productDescription: t.text('product_description').notNull(), installedCapacity: t.text('installed_capacity').notNull(), operationalCapacity: t.text('operational_capacity').notNull(), @@ -88,13 +94,15 @@ export const trainingSurveys = t.pgTable( paralysisReason: t.text('paralysis_reason').notNull(), // fotos photo1: t.text('photo1').notNull(), - photo2: t.text('photo2').notNull(), - photo3: t.text('photo3').notNull(), + photo2: t.text('photo2'), + photo3: t.text('photo3'), ...timestamps, }, (trainingSurveys) => ({ - trainingSurveysIndex: t.index('training_surveys_index_00').on(trainingSurveys.firstname), - }) + trainingSurveysIndex: t + .index('training_surveys_index_00') + .on(trainingSurveys.firstname), + }), ); export const viewSurveys = t.pgView('v_surveys', { @@ -103,6 +111,7 @@ export const viewSurveys = t.pgView('v_surveys', { description: t.text('description'), created_at: t.timestamp('created_at'), closingDate: t.date('closing_date'), - targetAudience: t.varchar('target_audience') -}).as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys -where published = true`); \ No newline at end of file + targetAudience: t.varchar('target_audience'), +}) + .as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys +where published = true`); diff --git a/apps/api/src/features/training/dto/create-training.dto.ts b/apps/api/src/features/training/dto/create-training.dto.ts index 901f913..99551c3 100644 --- a/apps/api/src/features/training/dto/create-training.dto.ts +++ b/apps/api/src/features/training/dto/create-training.dto.ts @@ -1,140 +1,149 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsString, IsDateString, IsOptional } from 'class-validator'; +import { IsDateString, IsInt, IsOptional, IsString } from 'class-validator'; export class CreateTrainingDto { - @ApiProperty() - @IsString() - firstname: string; + @ApiProperty() + @IsString() + firstname: string; - @ApiProperty() - @IsString() - lastname: string; + @ApiProperty() + @IsString() + lastname: string; - @ApiProperty() - @IsDateString() - visitDate: string; + @ApiProperty() + @IsDateString() + visitDate: string; - @ApiProperty() - @IsString() - productiveActivity: string; + @ApiProperty() + @IsString() + productiveActivity: string; - @ApiProperty() - @IsString() - financialRequirementDescription: string; + @ApiProperty() + @IsString() + financialRequirementDescription: string; - @ApiProperty() - @IsInt() - state: number; + @ApiProperty() + @IsInt() + state: number; - @ApiProperty() - @IsInt() - municipality: number; + @ApiProperty() + @IsInt() + municipality: number; - @ApiProperty() - @IsInt() - parish: number; + @ApiProperty() + @IsInt() + parish: number; - @ApiProperty() - @IsString() - siturCodeCommune: string; + @ApiProperty() + @IsString() + siturCodeCommune: string; - @ApiProperty() - @IsString() - communalCouncil: string; + @ApiProperty() + @IsString() + communalCouncil: string; - @ApiProperty() - @IsString() - siturCodeCommunalCouncil: string; + @ApiProperty() + @IsString() + siturCodeCommunalCouncil: string; - @ApiProperty() - @IsString() - ospName: string; + @ApiProperty() + @IsString() + ospName: string; - @ApiProperty() - @IsString() - ospAddress: string; + @ApiProperty() + @IsString() + ospAddress: string; - @ApiProperty() - @IsString() - ospRif: string; + @ApiProperty() + @IsString() + ospRif: string; - @ApiProperty() - @IsString() - ospType: string; + @ApiProperty() + @IsString() + ospType: string; - @ApiProperty() - @IsString() - currentStatus: string; + @ApiProperty() + @IsString() + currentStatus: string; - @ApiProperty() - @IsInt() - companyConstitutionYear: number; + @ApiProperty() + @IsInt() + companyConstitutionYear: number; - @ApiProperty() - @IsInt() - producerCount: number; + @ApiProperty() + @IsInt() + producerCount: number; - @ApiProperty() - @IsString() - productDescription: string; + @ApiProperty() + @IsInt() + @IsOptional() + productCount: number; - @ApiProperty() - @IsString() - installedCapacity: string; + @ApiProperty() + @IsString() + productDescription: string; - @ApiProperty() - @IsString() - operationalCapacity: string; + @ApiProperty() + @IsString() + installedCapacity: string; - @ApiProperty() - @IsString() - ospResponsibleFullname: string; + @ApiProperty() + @IsString() + operationalCapacity: string; - @ApiProperty() - @IsString() - ospResponsibleCedula: string; + @ApiProperty() + @IsString() + ospResponsibleFullname: string; - @ApiProperty() - @IsString() - ospResponsibleRif: string; + @ApiProperty() + @IsString() + ospResponsibleCedula: string; - @ApiProperty() - @IsString() - ospResponsiblePhone: string; + @ApiProperty() + @IsString() + ospResponsibleRif: string; - @ApiProperty() - @IsString() - ospResponsibleEmail: string; + @ApiProperty() + @IsString() + ospResponsiblePhone: string; - @ApiProperty() - @IsString() - civilState: string; + @ApiProperty() + @IsString() + ospResponsibleEmail: string; - @ApiProperty() - @IsInt() - familyBurden: number; + @ApiProperty() + @IsString() + civilState: string; - @ApiProperty() - @IsInt() - numberOfChildren: number; + @ApiProperty() + @IsInt() + familyBurden: number; - @ApiProperty() - @IsString() - generalObservations: string; + @ApiProperty() + @IsInt() + numberOfChildren: number; - @ApiProperty() - @IsString() - photo1: string; + @ApiProperty() + @IsString() + generalObservations: string; - @ApiProperty() - @IsString() - photo2: string; + @ApiProperty() + @IsString() + @IsOptional() + photo1?: string; - @ApiProperty() - @IsString() - photo3: string; + @ApiProperty() + @IsString() + @IsOptional() + photo2?: string; - @ApiProperty() - @IsString() - paralysisReason: string; + @ApiProperty() + @IsString() + @IsOptional() + photo3?: string; + + @ApiProperty() + @IsString() + @IsOptional() + paralysisReason: string; } diff --git a/apps/api/src/features/training/training.controller.ts b/apps/api/src/features/training/training.controller.ts index 1dbe3b3..b28c2a1 100644 --- a/apps/api/src/features/training/training.controller.ts +++ b/apps/api/src/features/training/training.controller.ts @@ -1,68 +1,114 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; -import { TrainingService } from './training.service'; -import { CreateTrainingDto } from './dto/create-training.dto'; -import { UpdateTrainingDto } from './dto/update-training.dto'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, + UploadedFiles, + UseInterceptors, +} from '@nestjs/common'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import { + ApiConsumes, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { PaginationDto } from '../../common/dto/pagination.dto'; - +import { ImageProcessingPipe } from '../../common/pipes/image-processing.pipe'; +import { CreateTrainingDto } from './dto/create-training.dto'; import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto'; +import { UpdateTrainingDto } from './dto/update-training.dto'; +import { TrainingService } from './training.service'; @ApiTags('training') @Controller('training') export class TrainingController { - constructor(private readonly trainingService: TrainingService) { } + constructor(private readonly trainingService: TrainingService) {} - @Get() - @ApiOperation({ summary: 'Get all training records with pagination and filters' }) - @ApiResponse({ status: 200, description: 'Return paginated training records.' }) - async findAll(@Query() paginationDto: PaginationDto) { - const result = await this.trainingService.findAll(paginationDto); - return { - message: 'Training records fetched successfully', - data: result.data, - meta: result.meta - }; - } + @Get() + @ApiOperation({ + summary: 'Get all training records with pagination and filters', + }) + @ApiResponse({ + status: 200, + description: 'Return paginated training records.', + }) + async findAll(@Query() paginationDto: PaginationDto) { + const result = await this.trainingService.findAll(paginationDto); + return { + message: 'Training records fetched successfully', + data: result.data, + meta: result.meta, + }; + } - @Get('statistics') - @ApiOperation({ summary: 'Get training statistics' }) - @ApiResponse({ status: 200, description: 'Return training statistics.' }) - async getStatistics(@Query() filterDto: TrainingStatisticsFilterDto) { - const data = await this.trainingService.getStatistics(filterDto); - return { message: 'Training statistics fetched successfully', data }; - } + @Get('statistics') + @ApiOperation({ summary: 'Get training statistics' }) + @ApiResponse({ status: 200, description: 'Return training statistics.' }) + async getStatistics(@Query() filterDto: TrainingStatisticsFilterDto) { + const data = await this.trainingService.getStatistics(filterDto); + return { message: 'Training statistics fetched successfully', data }; + } - @Get(':id') - @ApiOperation({ summary: 'Get a training record by ID' }) - @ApiResponse({ status: 200, description: 'Return the training record.' }) - @ApiResponse({ status: 404, description: 'Training record not found.' }) - async findOne(@Param('id') id: string) { - const data = await this.trainingService.findOne(+id); - return { message: 'Training record fetched successfully', data }; - } + @Get(':id') + @ApiOperation({ summary: 'Get a training record by ID' }) + @ApiResponse({ status: 200, description: 'Return the training record.' }) + @ApiResponse({ status: 404, description: 'Training record not found.' }) + async findOne(@Param('id') id: string) { + const data = await this.trainingService.findOne(+id); + return { message: 'Training record fetched successfully', data }; + } - @Post() - @ApiOperation({ summary: 'Create a new training record' }) - @ApiResponse({ status: 201, description: 'Training record created successfully.' }) - async create(@Body() createTrainingDto: CreateTrainingDto) { - const data = await this.trainingService.create(createTrainingDto); - return { message: 'Training record created successfully', data }; - } + @Post() + @UseInterceptors(FilesInterceptor('files', 3)) + @ApiConsumes('multipart/form-data') + @ApiOperation({ summary: 'Create a new training record' }) + @ApiResponse({ + status: 201, + description: 'Training record created successfully.', + }) + async create( + @Body() createTrainingDto: CreateTrainingDto, + @UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[], + ) { + const data = await this.trainingService.create(createTrainingDto, files); + return { message: 'Training record created successfully', data }; + } - @Patch(':id') - @ApiOperation({ summary: 'Update a training record' }) - @ApiResponse({ status: 200, description: 'Training record updated successfully.' }) - @ApiResponse({ status: 404, description: 'Training record not found.' }) - async update(@Param('id') id: string, @Body() updateTrainingDto: UpdateTrainingDto) { - const data = await this.trainingService.update(+id, updateTrainingDto); - return { message: 'Training record updated successfully', data }; - } + @Patch(':id') + @UseInterceptors(FilesInterceptor('files', 3)) + @ApiConsumes('multipart/form-data') + @ApiOperation({ summary: 'Update a training record' }) + @ApiResponse({ + status: 200, + description: 'Training record updated successfully.', + }) + @ApiResponse({ status: 404, description: 'Training record not found.' }) + async update( + @Param('id') id: string, + @Body() updateTrainingDto: UpdateTrainingDto, + @UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[], + ) { + const data = await this.trainingService.update( + +id, + updateTrainingDto, + files, + ); + return { message: 'Training record updated successfully', data }; + } - @Delete(':id') - @ApiOperation({ summary: 'Delete a training record' }) - @ApiResponse({ status: 200, description: 'Training record deleted successfully.' }) - @ApiResponse({ status: 404, description: 'Training record not found.' }) - async remove(@Param('id') id: string) { - return await this.trainingService.remove(+id); - } + @Delete(':id') + @ApiOperation({ summary: 'Delete a training record' }) + @ApiResponse({ + status: 200, + description: 'Training record deleted successfully.', + }) + @ApiResponse({ status: 404, description: 'Training record not found.' }) + async remove(@Param('id') id: string) { + return await this.trainingService.remove(+id); + } } diff --git a/apps/api/src/features/training/training.service.ts b/apps/api/src/features/training/training.service.ts index 0123375..cf4ee10 100644 --- a/apps/api/src/features/training/training.service.ts +++ b/apps/api/src/features/training/training.service.ts @@ -1,223 +1,324 @@ -import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; -import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { and, eq, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as fs from 'fs'; +import * as path from 'path'; +import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider'; import * as schema from 'src/database/index'; -import { trainingSurveys } from 'src/database/index'; -import { eq, like, or, and, gte, lte, SQL, sql } from 'drizzle-orm'; -import { CreateTrainingDto } from './dto/create-training.dto'; -import { UpdateTrainingDto } from './dto/update-training.dto'; -import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto'; -import { states } from 'src/database/index'; +import { states, trainingSurveys } from 'src/database/index'; import { PaginationDto } from '../../common/dto/pagination.dto'; +import { CreateTrainingDto } from './dto/create-training.dto'; +import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto'; +import { UpdateTrainingDto } from './dto/update-training.dto'; @Injectable() export class TrainingService { - constructor( - @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, - ) { } + constructor( + @Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase, + ) {} + async findAll(paginationDto?: PaginationDto) { + const { + page = 1, + limit = 10, + search = '', + sortBy = 'id', + sortOrder = 'asc', + } = paginationDto || {}; - async findAll(paginationDto?: PaginationDto) { - const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {}; + const offset = (page - 1) * limit; - const offset = (page - 1) * limit; - - let searchCondition: SQL | undefined; - if (search) { - searchCondition = or( - like(trainingSurveys.firstname, `%${search}%`), - like(trainingSurveys.lastname, `%${search}%`), - like(trainingSurveys.ospName, `%${search}%`), - like(trainingSurveys.ospRif, `%${search}%`) - ); - } - - const orderBy = sortOrder === 'asc' - ? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc` - : sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`; - - const totalCountResult = await this.drizzle - .select({ count: sql`count(*)` }) - .from(trainingSurveys) - .where(searchCondition); - - const totalCount = Number(totalCountResult[0].count); - const totalPages = Math.ceil(totalCount / limit); - - const data = await this.drizzle - .select() - .from(trainingSurveys) - .where(searchCondition) - .orderBy(orderBy) - .limit(limit) - .offset(offset); - - const meta = { - page, - limit, - totalCount, - totalPages, - hasNextPage: page < totalPages, - hasPreviousPage: page > 1, - nextPage: page < totalPages ? page + 1 : null, - previousPage: page > 1 ? page - 1 : null, - }; - - return { data, meta }; + let searchCondition: SQL | undefined; + if (search) { + searchCondition = or(ilike(trainingSurveys.ospName, `%${search}%`)); } - async getStatistics(filterDto: TrainingStatisticsFilterDto) { - const { startDate, endDate, stateId, municipalityId, parishId, ospType } = filterDto; + const orderBy = + sortOrder === 'asc' + ? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc` + : sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`; - const filters: SQL[] = []; + const totalCountResult = await this.drizzle + .select({ count: sql`count(*)` }) + .from(trainingSurveys) + .where(searchCondition); - if (startDate) { - filters.push(gte(trainingSurveys.visitDate, new Date(startDate))); - } + const totalCount = Number(totalCountResult[0].count); + const totalPages = Math.ceil(totalCount / limit); - if (endDate) { - filters.push(lte(trainingSurveys.visitDate, new Date(endDate))); - } + const data = await this.drizzle + .select() + .from(trainingSurveys) + .where(searchCondition) + .orderBy(orderBy) + .limit(limit) + .offset(offset); - if (stateId) { - filters.push(eq(trainingSurveys.state, stateId)); - } + const meta = { + page, + limit, + totalCount, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + previousPage: page > 1 ? page - 1 : null, + }; - if (municipalityId) { - filters.push(eq(trainingSurveys.municipality, municipalityId)); - } + return { data, meta }; + } - if (parishId) { - filters.push(eq(trainingSurveys.parish, parishId)); - } + async getStatistics(filterDto: TrainingStatisticsFilterDto) { + const { startDate, endDate, stateId, municipalityId, parishId, ospType } = + filterDto; - if (ospType) { - filters.push(eq(trainingSurveys.ospType, ospType)); - } + const filters: SQL[] = []; - const whereCondition = filters.length > 0 ? and(...filters) : undefined; - - const totalOspsResult = await this.drizzle - .select({ count: sql`count(*)` }) - .from(trainingSurveys) - .where(whereCondition); - const totalOsps = Number(totalOspsResult[0].count); - - const totalProducersResult = await this.drizzle - .select({ sum: sql`sum(${trainingSurveys.producerCount})` }) - .from(trainingSurveys) - .where(whereCondition); - const totalProducers = Number(totalProducersResult[0].sum || 0); - - const statusDistribution = await this.drizzle - .select({ - name: trainingSurveys.currentStatus, - value: sql`count(*)` - }) - .from(trainingSurveys) - .where(whereCondition) - .groupBy(trainingSurveys.currentStatus); - - const activityDistribution = await this.drizzle - .select({ - name: trainingSurveys.productiveActivity, - value: sql`count(*)` - }) - .from(trainingSurveys) - .where(whereCondition) - .groupBy(trainingSurveys.productiveActivity); - - const typeDistribution = await this.drizzle - .select({ - name: trainingSurveys.ospType, - value: sql`count(*)` - }) - .from(trainingSurveys) - .where(whereCondition) - .groupBy(trainingSurveys.ospType); - - // New Aggregations - const stateDistribution = await this.drizzle - .select({ - name: states.name, - value: sql`count(${trainingSurveys.id})` - }) - .from(trainingSurveys) - .leftJoin(states, eq(trainingSurveys.state, states.id)) - .where(whereCondition) - .groupBy(states.name); - - const yearDistribution = await this.drizzle - .select({ - name: sql`cast(${trainingSurveys.companyConstitutionYear} as text)`, - value: sql`count(*)` - }) - .from(trainingSurveys) - .where(whereCondition) - .groupBy(trainingSurveys.companyConstitutionYear) - .orderBy(trainingSurveys.companyConstitutionYear); - - return { - totalOsps, - totalProducers, - statusDistribution: statusDistribution.map(item => ({ ...item, value: Number(item.value) })), - activityDistribution: activityDistribution.map(item => ({ ...item, value: Number(item.value) })), - typeDistribution: typeDistribution.map(item => ({ ...item, value: Number(item.value) })), - stateDistribution: stateDistribution.map(item => ({ ...item, value: Number(item.value) })), - yearDistribution: yearDistribution.map(item => ({ ...item, value: Number(item.value) })), - }; + if (startDate) { + filters.push(gte(trainingSurveys.visitDate, new Date(startDate))); } - async findOne(id: number) { - const find = await this.drizzle - .select() - .from(trainingSurveys) - .where(eq(trainingSurveys.id, id)); + if (endDate) { + filters.push(lte(trainingSurveys.visitDate, new Date(endDate))); + } - if (find.length === 0) { - throw new HttpException('Training record not found', HttpStatus.NOT_FOUND); + if (stateId) { + filters.push(eq(trainingSurveys.state, stateId)); + } + + if (municipalityId) { + filters.push(eq(trainingSurveys.municipality, municipalityId)); + } + + if (parishId) { + filters.push(eq(trainingSurveys.parish, parishId)); + } + + if (ospType) { + filters.push(eq(trainingSurveys.ospType, ospType)); + } + + const whereCondition = filters.length > 0 ? and(...filters) : undefined; + + const totalOspsResult = await this.drizzle + .select({ count: sql`count(*)` }) + .from(trainingSurveys) + .where(whereCondition); + const totalOsps = Number(totalOspsResult[0].count); + + const totalProducersResult = await this.drizzle + .select({ sum: sql`sum(${trainingSurveys.producerCount})` }) + .from(trainingSurveys) + .where(whereCondition); + const totalProducers = Number(totalProducersResult[0].sum || 0); + + const statusDistribution = await this.drizzle + .select({ + name: trainingSurveys.currentStatus, + value: sql`count(*)`, + }) + .from(trainingSurveys) + .where(whereCondition) + .groupBy(trainingSurveys.currentStatus); + + const activityDistribution = await this.drizzle + .select({ + name: trainingSurveys.productiveActivity, + value: sql`count(*)`, + }) + .from(trainingSurveys) + .where(whereCondition) + .groupBy(trainingSurveys.productiveActivity); + + const typeDistribution = await this.drizzle + .select({ + name: trainingSurveys.ospType, + value: sql`count(*)`, + }) + .from(trainingSurveys) + .where(whereCondition) + .groupBy(trainingSurveys.ospType); + + // New Aggregations + const stateDistribution = await this.drizzle + .select({ + name: states.name, + value: sql`count(${trainingSurveys.id})`, + }) + .from(trainingSurveys) + .leftJoin(states, eq(trainingSurveys.state, states.id)) + .where(whereCondition) + .groupBy(states.name); + + const yearDistribution = await this.drizzle + .select({ + name: sql`cast(${trainingSurveys.companyConstitutionYear} as text)`, + value: sql`count(*)`, + }) + .from(trainingSurveys) + .where(whereCondition) + .groupBy(trainingSurveys.companyConstitutionYear) + .orderBy(trainingSurveys.companyConstitutionYear); + + return { + totalOsps, + totalProducers, + statusDistribution: statusDistribution.map((item) => ({ + ...item, + value: Number(item.value), + })), + activityDistribution: activityDistribution.map((item) => ({ + ...item, + value: Number(item.value), + })), + typeDistribution: typeDistribution.map((item) => ({ + ...item, + value: Number(item.value), + })), + stateDistribution: stateDistribution.map((item) => ({ + ...item, + value: Number(item.value), + })), + yearDistribution: yearDistribution.map((item) => ({ + ...item, + value: Number(item.value), + })), + }; + } + + async findOne(id: number) { + const find = await this.drizzle + .select() + .from(trainingSurveys) + .where(eq(trainingSurveys.id, id)); + + if (find.length === 0) { + throw new HttpException( + 'Training record not found', + HttpStatus.NOT_FOUND, + ); + } + + return find[0]; + } + + private async saveFiles(files: Express.Multer.File[]): Promise { + if (!files || files.length === 0) return []; + + const uploadDir = './uploads/training'; + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + + const savedPaths: string[] = []; + for (const file of files) { + const fileName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${path.extname(file.originalname)}`; + const filePath = path.join(uploadDir, fileName); + fs.writeFileSync(filePath, file.buffer); + savedPaths.push(`/assets/training/${fileName}`); + } + return savedPaths; + } + + private deleteFile(assetPath: string) { + if (!assetPath) return; + // Map /assets/training/filename.webp back to ./uploads/training/filename.webp + const relativePath = assetPath.replace('/assets/training/', ''); + const fullPath = path.join('./uploads/training', relativePath); + + if (fs.existsSync(fullPath)) { + try { + fs.unlinkSync(fullPath); + } catch (err) { + console.error(`Error deleting file ${fullPath}:`, err); + } + } + } + + async create( + createTrainingDto: CreateTrainingDto, + files: Express.Multer.File[], + ) { + const photoPaths = await this.saveFiles(files); + + const [newRecord] = await this.drizzle + .insert(trainingSurveys) + .values({ + ...createTrainingDto, + visitDate: new Date(createTrainingDto.visitDate), + photo1: photoPaths[0] || '', + photo2: photoPaths[1] || null, + photo3: photoPaths[2] || null, + }) + .returning(); + + return newRecord; + } + + async update( + id: number, + updateTrainingDto: UpdateTrainingDto, + files: Express.Multer.File[], + ) { + const currentRecord = await this.findOne(id); + + const photoPaths = await this.saveFiles(files); + + const updateData: any = { ...updateTrainingDto }; + + // Handle photo updates/removals + const photoFields = ['photo1', 'photo2', 'photo3'] as const; + + // 1. If we have NEW files, they replace any old files or occupy empty slots + if (photoPaths.length > 0) { + photoPaths.forEach((newPath, idx) => { + const fieldName = photoFields[idx]; + const oldPath = currentRecord[fieldName]; + if (oldPath && oldPath !== newPath) { + this.deleteFile(oldPath); } - - return find[0]; + updateData[fieldName] = newPath; + }); } - async create(createTrainingDto: CreateTrainingDto) { - const [newRecord] = await this.drizzle - .insert(trainingSurveys) - .values({ - ...createTrainingDto, - visitDate: new Date(createTrainingDto.visitDate), - }) - .returning(); + // 2. If the user explicitly cleared a photo field (updateData.photoX === '') + photoFields.forEach((field) => { + if (updateData[field] === '') { + const oldPath = currentRecord[field]; + if (oldPath) this.deleteFile(oldPath); + updateData[field] = null; // Set to null in DB + } + }); - return newRecord; + if (updateTrainingDto.visitDate) { + updateData.visitDate = new Date(updateTrainingDto.visitDate); } - async update(id: number, updateTrainingDto: UpdateTrainingDto) { - await this.findOne(id); + const [updatedRecord] = await this.drizzle + .update(trainingSurveys) + .set(updateData) + .where(eq(trainingSurveys.id, id)) + .returning(); - const updateData: any = { ...updateTrainingDto }; - if (updateTrainingDto.visitDate) { - updateData.visitDate = new Date(updateTrainingDto.visitDate); - } + return updatedRecord; + } - const [updatedRecord] = await this.drizzle - .update(trainingSurveys) - .set(updateData) - .where(eq(trainingSurveys.id, id)) - .returning(); + async remove(id: number) { + const record = await this.findOne(id); - return updatedRecord; - } + // Delete associated files + if (record.photo1) this.deleteFile(record.photo1); + if (record.photo2) this.deleteFile(record.photo2); + if (record.photo3) this.deleteFile(record.photo3); - async remove(id: number) { - await this.findOne(id); + const [deletedRecord] = await this.drizzle + .delete(trainingSurveys) + .where(eq(trainingSurveys.id, id)) + .returning(); - const [deletedRecord] = await this.drizzle - .delete(trainingSurveys) - .where(eq(trainingSurveys.id, id)) - .returning(); - - return { message: 'Training record deleted successfully', data: deletedRecord }; - } + return { + message: 'Training record deleted successfully', + data: deletedRecord, + }; + } } diff --git a/apps/web/app/dashboard/formulario/editar/[id]/page.tsx b/apps/web/app/dashboard/formulario/editar/[id]/page.tsx new file mode 100644 index 0000000..c279469 --- /dev/null +++ b/apps/web/app/dashboard/formulario/editar/[id]/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import PageContainer from '@/components/layout/page-container'; +import { CreateTrainingForm } from '@/feactures/training/components/form'; +import { useTrainingByIdQuery } from '@/feactures/training/hooks/use-training'; +import { useParams, useRouter } from 'next/navigation'; + +export default function EditTrainingPage() { + const router = useRouter(); + const params = useParams(); + const id = Number(params.id); + + const { data: training, isLoading } = useTrainingByIdQuery(id); + + if (isLoading) { + return ( + +
Cargando...
+
+ ); + } + + return ( + +
+ router.push('/dashboard/formulario')} + onCancel={() => router.back()} + /> +
+
+ ); +} diff --git a/apps/web/app/dashboard/formulario/nuevo/page.tsx b/apps/web/app/dashboard/formulario/nuevo/page.tsx new file mode 100644 index 0000000..41c2dd5 --- /dev/null +++ b/apps/web/app/dashboard/formulario/nuevo/page.tsx @@ -0,0 +1,20 @@ +'use client'; + +import PageContainer from '@/components/layout/page-container'; +import { CreateTrainingForm } from '@/feactures/training/components/form'; +import { useRouter } from 'next/navigation'; + +export default function NewTrainingPage() { + const router = useRouter(); + + return ( + +
+ router.push('/dashboard/formulario')} + onCancel={() => router.back()} + /> +
+
+ ); +} diff --git a/apps/web/app/dashboard/formulario/page.tsx b/apps/web/app/dashboard/formulario/page.tsx index a612102..35a0044 100644 --- a/apps/web/app/dashboard/formulario/page.tsx +++ b/apps/web/app/dashboard/formulario/page.tsx @@ -1,15 +1,36 @@ -'use client'; - import PageContainer from '@/components/layout/page-container'; -import { CreateTrainingForm } from '@/feactures/training/components/form'; +import { TrainingHeader } from '@/feactures/training/components/training-header'; +import TrainingList from '@/feactures/training/components/training-list'; +import TrainingTableAction from '@/feactures/training/components/training-tables/training-table-action'; +import { searchParamsCache } from '@repo/shadcn/lib/searchparams'; +import { SearchParams } from 'nuqs'; -const Page = () => { - return ( - -
- -
- ); +export const metadata = { + title: 'Registro de OSP', }; -export default Page; +type PageProps = { + searchParams: Promise; +}; + +export default async function Page({ searchParams }: PageProps) { + const { + page, + q: searchQuery, + limit, + } = searchParamsCache.parse(await searchParams); + + return ( + +
+ + + +
+
+ ); +} diff --git a/apps/web/components/layout/app-sidebar.tsx b/apps/web/components/layout/app-sidebar.tsx index c28016d..f41b3bc 100644 --- a/apps/web/components/layout/app-sidebar.tsx +++ b/apps/web/components/layout/app-sidebar.tsx @@ -15,7 +15,7 @@ import { useSession } from 'next-auth/react'; export const company = { - name: 'Sistema para Productores', + name: 'Sistema de Productores', logo: GalleryVerticalEnd, plan: 'FONDEMI', }; diff --git a/apps/web/constants/routes.ts b/apps/web/constants/routes.ts index b583077..619533c 100644 --- a/apps/web/constants/routes.ts +++ b/apps/web/constants/routes.ts @@ -26,7 +26,7 @@ export const AdministrationItems: NavItem[] = [ url: '#', // Placeholder as there is no direct link for the parent icon: 'settings2', isActive: true, - role: ['admin', 'superadmin', 'manager', 'autoridad'], // sumatoria de los roles que si tienen acceso + role: ['admin', 'superadmin', 'manager', 'autoridad', 'coordinators'], // sumatoria de los roles que si tienen acceso items: [ { @@ -41,14 +41,14 @@ export const AdministrationItems: NavItem[] = [ shortcut: ['l', 'l'], url: '/dashboard/administracion/encuestas', icon: 'login', - role: ['admin', 'superadmin', 'autoridad', 'manager'], + role: ['admin', 'superadmin', 'autoridad'], }, { title: 'Registro OSP', shortcut: ['p', 'p'], url: '/dashboard/formulario/', icon: 'notepadText', - role: ['admin', 'superadmin', 'manager', 'autoridad'], + role: ['admin', 'superadmin', 'manager', 'autoridad', 'coordinators'], }, ], }, @@ -60,7 +60,7 @@ export const StatisticsItems: NavItem[] = [ url: '#', // Placeholder as there is no direct link for the parent icon: 'chartColumn', isActive: true, - role: ['admin', 'superadmin', 'autoridad'], + role: ['admin', 'superadmin', 'autoridad', 'manager'], items: [ // { @@ -82,7 +82,7 @@ export const StatisticsItems: NavItem[] = [ shortcut: ['s', 's'], url: '/dashboard/estadisticas/socioproductiva', icon: 'blocks', - role: ['admin', 'superadmin', 'autoridad'], + role: ['admin', 'superadmin', 'autoridad', 'manager'], }, ], }, diff --git a/apps/web/feactures/auth/components/user-auth-form.tsx b/apps/web/feactures/auth/components/user-auth-form.tsx index 9363626..47c879c 100644 --- a/apps/web/feactures/auth/components/user-auth-form.tsx +++ b/apps/web/feactures/auth/components/user-auth-form.tsx @@ -72,7 +72,7 @@ export default function UserAuthForm() {
-

Sistema para productores

+

Sistema Gestión de Productores

Ingresa tus datos

diff --git a/apps/web/feactures/auth/components/user-register-form.tsx b/apps/web/feactures/auth/components/user-register-form.tsx index d2d9266..c2cf882 100644 --- a/apps/web/feactures/auth/components/user-register-form.tsx +++ b/apps/web/feactures/auth/components/user-register-form.tsx @@ -92,7 +92,7 @@ export default function UserAuthForm() {
-

Sistema para productores

+

Sistema Gestión de Productores

Ingresa tus datos

@@ -303,7 +303,7 @@ export default function UserAuthForm() { {error} )}{' '}
¿Ya tienes una cuenta?{" "} diff --git a/apps/web/feactures/training/actions/training-actions.ts b/apps/web/feactures/training/actions/training-actions.ts index de8efb8..e73806a 100644 --- a/apps/web/feactures/training/actions/training-actions.ts +++ b/apps/web/feactures/training/actions/training-actions.ts @@ -1,123 +1,161 @@ 'use server'; import { safeFetchApi } from '@/lib/fetch.api'; -import { - TrainingSchema, - TrainingMutate, - trainingApiResponseSchema -} from '../schemas/training'; import { trainingStatisticsResponseSchema } from '../schemas/statistics'; +import { + TrainingMutate, + TrainingSchema, + trainingApiResponseSchema, +} from '../schemas/training'; -export const getTrainingStatisticsAction = async (params: { +export const getTrainingStatisticsAction = async ( + params: { startDate?: string; endDate?: string; stateId?: number; municipalityId?: number; parishId?: number; ospType?: string; -} = {}) => { - const searchParams = new URLSearchParams(); - if (params.startDate) searchParams.append('startDate', params.startDate); - if (params.endDate) searchParams.append('endDate', params.endDate); - if (params.stateId) searchParams.append('stateId', params.stateId.toString()); - if (params.municipalityId) searchParams.append('municipalityId', params.municipalityId.toString()); - if (params.parishId) searchParams.append('parishId', params.parishId.toString()); - if (params.ospType) searchParams.append('ospType', params.ospType); + } = {}, +) => { + const searchParams = new URLSearchParams(); + if (params.startDate) searchParams.append('startDate', params.startDate); + if (params.endDate) searchParams.append('endDate', params.endDate); + if (params.stateId) searchParams.append('stateId', params.stateId.toString()); + if (params.municipalityId) + searchParams.append('municipalityId', params.municipalityId.toString()); + if (params.parishId) + searchParams.append('parishId', params.parishId.toString()); + if (params.ospType) searchParams.append('ospType', params.ospType); - const [error, response] = await safeFetchApi( - trainingStatisticsResponseSchema, - `/training/statistics?${searchParams.toString()}`, - 'GET', - ); + const [error, response] = await safeFetchApi( + trainingStatisticsResponseSchema, + `/training/statistics?${searchParams.toString()}`, + 'GET', + ); - if (error) throw new Error(error.message); + if (error) throw new Error(error.message); - return response?.data; -} - - -export const getTrainingAction = async (params: { - page?: number; - limit?: number; - search?: string; - sortBy?: string; - sortOrder?: 'asc' | 'desc'; -}) => { - - const searchParams = new URLSearchParams({ - page: (params.page || 1).toString(), - limit: (params.limit || 10).toString(), - ...(params.search && { search: params.search }), - ...(params.sortBy && { sortBy: params.sortBy }), - ...(params.sortOrder && { sortOrder: params.sortOrder }), - }); - - const [error, response] = await safeFetchApi( - trainingApiResponseSchema, - `/training?${searchParams}`, - 'GET', - ); - - if (error) throw new Error(error.message); - - return { - data: response?.data || [], - meta: response?.meta || { - page: 1, - limit: 10, - totalCount: 0, - totalPages: 1, - hasNextPage: false, - hasPreviousPage: false, - nextPage: null, - previousPage: null, - }, - }; -} - -export const createTrainingAction = async (payload: TrainingSchema) => { - const { id, ...payloadWithoutId } = payload; - - const [error, data] = await safeFetchApi( - TrainingMutate, - '/training', - 'POST', - payloadWithoutId, - ); - - if (error) { - throw new Error(error.message || 'Error al crear el registro'); - } - - return data; + return response?.data; }; -export const updateTrainingAction = async (payload: TrainingSchema) => { - const { id, ...payloadWithoutId } = payload; +export const getTrainingAction = async (params: { + page?: number; + limit?: number; + search?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +}) => { + const searchParams = new URLSearchParams({ + page: (params.page || 1).toString(), + limit: (params.limit || 10).toString(), + ...(params.search && { search: params.search }), + ...(params.sortBy && { sortBy: params.sortBy }), + ...(params.sortOrder && { sortOrder: params.sortOrder }), + }); - if (!id) throw new Error('ID es requerido para actualizar'); + const [error, response] = await safeFetchApi( + trainingApiResponseSchema, + `/training?${searchParams}`, + 'GET', + ); - const [error, data] = await safeFetchApi( - TrainingMutate, - `/training/${id}`, - 'PATCH', - payloadWithoutId, - ); + if (error) throw new Error(error.message); - if (error) { - throw new Error(error.message || 'Error al actualizar el registro'); - } + return { + data: response?.data || [], + meta: response?.meta || { + page: 1, + limit: 10, + totalCount: 0, + totalPages: 1, + hasNextPage: false, + hasPreviousPage: false, + nextPage: null, + previousPage: null, + }, + }; +}; - return data; +export const createTrainingAction = async ( + payload: TrainingSchema | FormData, +) => { + let payloadToSend = payload; + let id: number | undefined; + + if (payload instanceof FormData) { + payload.delete('id'); + payloadToSend = payload; + } else { + const { id: _, ...rest } = payload; + payloadToSend = rest as any; + } + + const [error, data] = await safeFetchApi( + TrainingMutate, + '/training', + 'POST', + payloadToSend, + ); + + if (error) { + throw new Error(error.message || 'Error al crear el registro'); + } + + return data; +}; + +export const updateTrainingAction = async ( + payload: TrainingSchema | FormData, +) => { + let id: string | null = null; + let payloadToSend = payload; + + if (payload instanceof FormData) { + id = payload.get('id') as string; + payload.delete('id'); + payloadToSend = payload; + } else { + id = payload.id?.toString() || null; + const { id: _, ...rest } = payload; + payloadToSend = rest as any; + } + + if (!id) throw new Error('ID es requerido para actualizar'); + + const [error, data] = await safeFetchApi( + TrainingMutate, + `/training/${id}`, + 'PATCH', + payloadToSend, + ); + + if (error) { + throw new Error(error.message || 'Error al actualizar el registro'); + } + + return data; }; export const deleteTrainingAction = async (id: number) => { - const [error] = await safeFetchApi( - TrainingMutate, - `/training/${id}`, - 'DELETE' - ) + const [error] = await safeFetchApi( + TrainingMutate, + `/training/${id}`, + 'DELETE', + ); - if (error) throw new Error(error.message || 'Error al eliminar el registro'); + if (error) throw new Error(error.message || 'Error al eliminar el registro'); - return true; -} + return true; +}; + +export const getTrainingByIdAction = async (id: number) => { + const [error, response] = await safeFetchApi( + TrainingMutate, + `/training/${id}`, + 'GET', + ); + + if (error) throw new Error(error.message); + + return response?.data; +}; diff --git a/apps/web/feactures/training/components/form.tsx b/apps/web/feactures/training/components/form.tsx index 04ddc8e..2867647 100644 --- a/apps/web/feactures/training/components/form.tsx +++ b/apps/web/feactures/training/components/form.tsx @@ -3,556 +3,925 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@repo/shadcn/button'; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from '@repo/shadcn/form'; import { Input } from '@repo/shadcn/input'; -import { Textarea } from '@repo/shadcn/textarea'; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from '@repo/shadcn/select'; +import { Textarea } from '@repo/shadcn/textarea'; import { useForm } from 'react-hook-form'; -import { useCreateTraining } from "../hooks/use-training"; +import { useCreateTraining, useUpdateTraining } from '../hooks/use-training'; import { TrainingSchema, trainingSchema } from '../schemas/training'; -import { SelectSearchable } from '@repo/shadcn/select-searchable' +import { + useMunicipalityQuery, + useParishQuery, + useStateQuery, +} from '@/feactures/location/hooks/use-query-location'; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '@repo/shadcn/components/ui/card'; +import { SelectSearchable } from '@repo/shadcn/select-searchable'; import React from 'react'; -import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location'; const PRODUCTIVE_ACTIVITIES = [ - 'Agricola', - 'Textil', - 'Bloquera', - 'Carpinteria', - 'Unidad de suministro' + 'Agricola', + 'Textil', + 'Bloquera', + 'Carpinteria', + 'Unidad de suministro', ]; +const OSP_TYPES = ['COOPERATIVA', 'EPSIC', 'EPSDC', 'UPF', 'OTROS']; +const STATUS_OPTIONS = ['ACTIVA', 'INACTIVA']; +const CIVIL_STATE_OPTIONS = ['Soltero', 'Casado']; + interface CreateTrainingFormProps { - onSuccess?: () => void; - onCancel?: () => void; - defaultValues?: Partial; + onSuccess?: () => void; + onCancel?: () => void; + defaultValues?: Partial; } export function CreateTrainingForm({ - onSuccess, - onCancel, - defaultValues, + onSuccess, + onCancel, + defaultValues, }: CreateTrainingFormProps) { - const { - mutate: saveTraining, - isPending: isSaving, - } = useCreateTraining(); + const { mutate: createTraining, isPending: isCreating } = useCreateTraining(); + const { mutate: updateTraining, isPending: isUpdating } = useUpdateTraining(); - const [state, setState] = React.useState(0); - const [municipality, setMunicipality] = React.useState(0); - const [disabledMunicipality, setDisabledMunicipality] = React.useState(true); - const [disabledParish, setDisabledParish] = React.useState(true); + const isSaving = isCreating || isUpdating; - const { data: dataState } = useStateQuery() - const { data: dataMunicipality } = useMunicipalityQuery(state) - const { data: dataParish } = useParishQuery(municipality) + const [state, setState] = React.useState(0); + const [municipality, setMunicipality] = React.useState(0); + const [disabledMunicipality, setDisabledMunicipality] = React.useState(true); + const [disabledParish, setDisabledParish] = React.useState(true); + const [selectedFiles, setSelectedFiles] = React.useState([]); - const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }] + const { data: dataState } = useStateQuery(); + const { data: dataMunicipality } = useMunicipalityQuery(state); + const { data: dataParish } = useParishQuery(municipality); - const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0 - ? dataMunicipality.data - : [{ id: 0, stateId: 0, name: 'Sin Municipios' }] - // const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}] - const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0 - ? dataParish.data - : [{ id: 0, stateId: 0, name: 'Sin Parroquias' }] + const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }]; - const form = useForm({ - resolver: zodResolver(trainingSchema), - defaultValues: { - firstname: defaultValues?.firstname || '', - lastname: defaultValues?.lastname || '', - visitDate: defaultValues?.visitDate || new Date().toISOString().split('T')[0], - productiveActivity: defaultValues?.productiveActivity || '', - financialRequirementDescription: defaultValues?.financialRequirementDescription || '', - siturCodeCommune: defaultValues?.siturCodeCommune || '', - communalCouncil: defaultValues?.communalCouncil || '', - siturCodeCommunalCouncil: defaultValues?.siturCodeCommunalCouncil || '', - ospName: defaultValues?.ospName || '', - ospAddress: defaultValues?.ospAddress || '', - ospRif: defaultValues?.ospRif || '', - ospType: defaultValues?.ospType || '', - currentStatus: defaultValues?.currentStatus || '', - companyConstitutionYear: defaultValues?.companyConstitutionYear || new Date().getFullYear(), - producerCount: defaultValues?.producerCount || 0, - productDescription: defaultValues?.productDescription || '', - installedCapacity: defaultValues?.installedCapacity || '', - operationalCapacity: defaultValues?.operationalCapacity || '', - ospResponsibleFullname: defaultValues?.ospResponsibleFullname || '', - ospResponsibleCedula: defaultValues?.ospResponsibleCedula || '', - ospResponsibleRif: defaultValues?.ospResponsibleRif || '', - ospResponsiblePhone: defaultValues?.ospResponsiblePhone || '', - civilState: defaultValues?.civilState || '', - familyBurden: defaultValues?.familyBurden || 0, - numberOfChildren: defaultValues?.numberOfChildren || 0, - generalObservations: defaultValues?.generalObservations || '', - ospResponsibleEmail: defaultValues?.ospResponsibleEmail || '', - photo1: defaultValues?.photo1 || '', - photo2: defaultValues?.photo2 || '', - photo3: defaultValues?.photo3 || '', - paralysisReason: defaultValues?.paralysisReason || '', - state: defaultValues?.state || undefined, - municipality: defaultValues?.municipality || undefined, - parish: defaultValues?.parish || undefined, - }, - mode: 'onChange', + const municipalityOptions = + Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0 + ? dataMunicipality.data + : [{ id: 0, stateId: 0, name: 'Sin Municipios' }]; + // const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}] + const parishOptions = + Array.isArray(dataParish?.data) && dataParish.data.length > 0 + ? dataParish.data + : [{ id: 0, stateId: 0, name: 'Sin Parroquias' }]; + + // No local state needed for existing photos, we use form values + + React.useEffect(() => { + if (defaultValues) { + if (defaultValues.state) { + setState(Number(defaultValues.state)); + setDisabledMunicipality(false); + } + if (defaultValues.municipality) { + setMunicipality(Number(defaultValues.municipality)); + setDisabledParish(false); + } + } + }, [defaultValues]); + + const formatToLocalISO = (dateStr?: string | Date) => { + const date = dateStr ? new Date(dateStr) : new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}`; + }; + + const form = useForm({ + resolver: zodResolver(trainingSchema), + defaultValues: { + firstname: defaultValues?.firstname || '', + lastname: defaultValues?.lastname || '', + visitDate: formatToLocalISO(defaultValues?.visitDate), + productiveActivity: defaultValues?.productiveActivity || '', + financialRequirementDescription: + defaultValues?.financialRequirementDescription || '', + siturCodeCommune: defaultValues?.siturCodeCommune || '', + communalCouncil: defaultValues?.communalCouncil || '', + siturCodeCommunalCouncil: defaultValues?.siturCodeCommunalCouncil || '', + ospName: defaultValues?.ospName || '', + ospAddress: defaultValues?.ospAddress || '', + ospRif: defaultValues?.ospRif || '', + ospType: defaultValues?.ospType || '', + currentStatus: defaultValues?.currentStatus || 'ACTIVA', + companyConstitutionYear: + defaultValues?.companyConstitutionYear || new Date().getFullYear(), + producerCount: defaultValues?.producerCount || 0, + productCount: defaultValues?.productCount || 0, + productDescription: defaultValues?.productDescription || '', + installedCapacity: defaultValues?.installedCapacity || '', + operationalCapacity: defaultValues?.operationalCapacity || '', + ospResponsibleFullname: defaultValues?.ospResponsibleFullname || '', + ospResponsibleCedula: defaultValues?.ospResponsibleCedula || '', + ospResponsibleRif: defaultValues?.ospResponsibleRif || '', + ospResponsiblePhone: defaultValues?.ospResponsiblePhone || '', + civilState: defaultValues?.civilState || '', + familyBurden: defaultValues?.familyBurden || 0, + numberOfChildren: defaultValues?.numberOfChildren || 0, + generalObservations: defaultValues?.generalObservations || '', + ospResponsibleEmail: defaultValues?.ospResponsibleEmail || '', + photo1: defaultValues?.photo1 || '', + photo2: defaultValues?.photo2 || '', + photo3: defaultValues?.photo3 || '', + paralysisReason: defaultValues?.paralysisReason || '', + state: defaultValues?.state || undefined, + municipality: defaultValues?.municipality || undefined, + parish: defaultValues?.parish || undefined, + }, + mode: 'onChange', + }); + + const onSubmit = async (formData: TrainingSchema) => { + const data = new FormData(); + + Object.entries(formData).forEach(([key, value]) => { + if (key !== 'files' && value !== undefined && value !== null) { + data.append(key, value.toString()); + } }); - const onSubmit = async (formData: TrainingSchema) => { - saveTraining(formData, { - onSuccess: () => { - form.reset(); - onSuccess?.(); - }, - onError: (e) => { - console.error(e); - form.setError('root', { - type: 'manual', - message: 'Error al guardar el registro', - }); - }, + if (defaultValues?.id) { + data.append('id', defaultValues.id.toString()); + } + + selectedFiles.forEach((file) => { + data.append('files', file); + }); + + const mutation = defaultValues?.id ? updateTraining : createTraining; + + mutation(data as any, { + onSuccess: () => { + form.reset(); + setSelectedFiles([]); + onSuccess?.(); + }, + onError: (e) => { + console.error(e); + form.setError('root', { + type: 'manual', + message: 'Error al guardar el registro', }); - }; + }, + }); + }; - return ( - - - {form.formState.errors.root && ( -
- {form.formState.errors.root.message} -
+ return ( + <> +
+
+

+ Formulario de Registro de Organizaciones Socioproductivas +

+

+ Complete el Formulario para guardar la organización +

+
+
+ + + {form.formState.errors.root && ( +
+ {form.formState.errors.root.message} +
+ )} + + {/* 1. Datos de la visita */} + + + 1. Datos de la visita + + + ( + + Nombre del Coordinador Estadal + + + + + )} + /> -
- {/* Datos Personales */} -
-

Datos Básicos

-
+ ( + + Apellido del coordinador Estadal + + + + + + )} + /> - ( - - Nombre - - - - )} /> + ( + + Fecha y hora de la visita + + + + + + )} + /> + + - ( - - Apellido - - - - )} /> + {/* 2. Datos de la OSP */} + + + + 2. Datos de la Organización Socioproductiva (OSP) + + + + ( + + Tipos de Organización + + + + )} + /> - ( - - Fecha de la visita - - { - // Convert YYYY-MM-DD to ISO 8601 string - const dateValue = e.target.value; - if (dateValue) { - field.onChange(new Date(dateValue).toISOString()); - } else { - field.onChange(''); - } - }} - /> - - - - )} /> + ( + + RIF de la OSP + + + + + + )} + /> - {/* Ubicación */} -
-

Ubicación

-
+ ( + + Nombre de la Organización + + + + + + )} + /> - ( - - Estado + ( + + Actividad Productiva + + + + )} + /> - ({ - value: item.id.toString(), - label: item.name, - })) || [] - } - onValueChange={(value: any) => { field.onChange(Number(value)); setState(value); setDisabledMunicipality(false); setDisabledParish(true) } - } - placeholder="Selecciona un estado" - defaultValue={field.value?.toString()} - // disabled={readOnly} - /> - - - )} + ( + + Año de constitución + + + + + + )} + /> + + ( + + Cantidad de Productos + + + + + + )} + /> + + ( + + Estatus + + + + )} + /> + + ( + + + Breve descripción del producto o servicio + + +