Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d29edf1f4e | |||
| 200df205c9 | |||
| 00ab65aee3 | |||
| 524869b1f9 | |||
| f88ab2a971 | |||
| 0666877811 | |||
| ff46776e4a | |||
| d6de7527e4 | |||
| f910aea3cc | |||
| a88cf94adb | |||
| 70e5200549 | |||
| c70e146ce2 | |||
| fed90d9ff1 | |||
| 0efd5a11bd | |||
| e149500735 | |||
| d71ad98e85 | |||
| 590f62fad9 | |||
| 510327de58 | |||
| a0c363dd1b | |||
| 42e802f8a7 | |||
| 63c39e399e | |||
| 26fb849fa3 | |||
| 2566e5e9a7 | |||
| 8efe595f73 | |||
| d2908f1e4c | |||
| 69843e9e68 | |||
| 5c080c6d32 | |||
| 08a5567d60 | |||
| 69b3aab02a | |||
| b8b11259cd | |||
| 6482e692b3 | |||
| c1d1626e9e | |||
| 824685723b | |||
| 127e3b0e7a | |||
| ee499abcf9 | |||
| 949d54e590 | |||
| 24bc0476e6 | |||
| 01c7bd149d | |||
| d3b3fa5e85 | |||
| efa1726223 | |||
| 28d51a9c00 | |||
| c1d4a40244 | |||
| 6f8a55b8fd | |||
| e2105ccbf5 | |||
| d71c25f0ff | |||
| 08fa179276 | |||
| 5cd663a653 | |||
| 5137c07c88 |
@@ -17,3 +17,10 @@ DATABASE_URL="postgresql://postgres:local**@localhost:5432/caja_ahorro" #url con
|
|||||||
MAIL_HOST=gmail
|
MAIL_HOST=gmail
|
||||||
MAIL_USERNAME=
|
MAIL_USERNAME=
|
||||||
MAIL_PASSWORD=
|
MAIL_PASSWORD=
|
||||||
|
|
||||||
|
MINIO_ENDPOINT=
|
||||||
|
MINIO_PORT=
|
||||||
|
MINIO_ACCESS_KEY=
|
||||||
|
MINIO_SECRET_KEY=
|
||||||
|
MINIO_BUCKET=
|
||||||
|
MINIO_USE_SSL=
|
||||||
|
|||||||
3
apps/api/.gitignore
vendored
3
apps/api/.gitignore
vendored
@@ -54,3 +54,6 @@ pids
|
|||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
/uploads/training/*
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true,
|
"deleteOutDir": true,
|
||||||
"builder": "swc",
|
"builder": "swc",
|
||||||
"typeCheck": true
|
"typeCheck": true,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"include": "features/training/export_template/*.xlsx",
|
||||||
|
"outDir": "dist",
|
||||||
|
"watchAssets": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,12 +44,15 @@
|
|||||||
"drizzle-orm": "0.40.0",
|
"drizzle-orm": "0.40.0",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
"joi": "17.13.3",
|
"joi": "17.13.3",
|
||||||
|
"minio": "^8.0.6",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"path-to-regexp": "8.2.0",
|
"path-to-regexp": "8.2.0",
|
||||||
"pg": "8.13.3",
|
"pg": "8.13.3",
|
||||||
"pino-pretty": "13.0.0",
|
"pino-pretty": "13.0.0",
|
||||||
"reflect-metadata": "0.2.0",
|
"reflect-metadata": "0.2.0",
|
||||||
"rxjs": "7.8.1"
|
"rxjs": "7.8.1",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
|
"xlsx-populate": "^1.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs-modules/mailer": "^2.0.2",
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
|
|||||||
@@ -10,16 +10,17 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { APP_GUARD } from '@nestjs/core';
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||||
|
import { MinioModule } from './common/minio/minio.module';
|
||||||
import { DrizzleModule } from './database/drizzle.module';
|
import { DrizzleModule } from './database/drizzle.module';
|
||||||
import { AuthModule } from './features/auth/auth.module';
|
import { AuthModule } from './features/auth/auth.module';
|
||||||
import { ConfigurationsModule } from './features/configurations/configurations.module';
|
import { ConfigurationsModule } from './features/configurations/configurations.module';
|
||||||
import { LocationModule} from './features/location/location.module'
|
import { InventoryModule } from './features/inventory/inventory.module';
|
||||||
|
import { LocationModule } from './features/location/location.module';
|
||||||
import { MailModule } from './features/mail/mail.module';
|
import { MailModule } from './features/mail/mail.module';
|
||||||
import { RolesModule } from './features/roles/roles.module';
|
import { RolesModule } from './features/roles/roles.module';
|
||||||
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
|
||||||
import { SurveysModule } from './features/surveys/surveys.module';
|
import { SurveysModule } from './features/surveys/surveys.module';
|
||||||
import {InventoryModule} from './features/inventory/inventory.module'
|
import { TrainingModule } from './features/training/training.module';
|
||||||
import { PicturesModule } from './features/pictures/pictures.module';
|
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -51,6 +52,7 @@ import { PicturesModule } from './features/pictures/pictures.module';
|
|||||||
NodeMailerModule,
|
NodeMailerModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
ThrottleModule,
|
ThrottleModule,
|
||||||
|
MinioModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
MailModule,
|
MailModule,
|
||||||
@@ -61,7 +63,7 @@ import { PicturesModule } from './features/pictures/pictures.module';
|
|||||||
SurveysModule,
|
SurveysModule,
|
||||||
LocationModule,
|
LocationModule,
|
||||||
InventoryModule,
|
InventoryModule,
|
||||||
PicturesModule
|
TrainingModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ interface EnvVars {
|
|||||||
MAIL_HOST: string;
|
MAIL_HOST: string;
|
||||||
MAIL_USERNAME: string;
|
MAIL_USERNAME: string;
|
||||||
MAIL_PASSWORD: string;
|
MAIL_PASSWORD: string;
|
||||||
|
MINIO_ENDPOINT: string;
|
||||||
|
MINIO_PORT: number;
|
||||||
|
MINIO_ACCESS_KEY: string;
|
||||||
|
MINIO_SECRET_KEY: string;
|
||||||
|
MINIO_BUCKET: string;
|
||||||
|
MINIO_USE_SSL: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envsSchema = joi
|
const envsSchema = joi
|
||||||
@@ -30,6 +36,12 @@ const envsSchema = joi
|
|||||||
MAIL_HOST: joi.string(),
|
MAIL_HOST: joi.string(),
|
||||||
MAIL_USERNAME: joi.string(),
|
MAIL_USERNAME: joi.string(),
|
||||||
MAIL_PASSWORD: joi.string(),
|
MAIL_PASSWORD: joi.string(),
|
||||||
|
MINIO_ENDPOINT: joi.string().required(),
|
||||||
|
MINIO_PORT: joi.number().required(),
|
||||||
|
MINIO_ACCESS_KEY: joi.string().required(),
|
||||||
|
MINIO_SECRET_KEY: joi.string().required(),
|
||||||
|
MINIO_BUCKET: joi.string().required(),
|
||||||
|
MINIO_USE_SSL: joi.boolean().default(false),
|
||||||
})
|
})
|
||||||
.unknown(true);
|
.unknown(true);
|
||||||
|
|
||||||
@@ -54,4 +66,10 @@ export const envs = {
|
|||||||
mail_host: envVars.MAIL_HOST,
|
mail_host: envVars.MAIL_HOST,
|
||||||
mail_username: envVars.MAIL_USERNAME,
|
mail_username: envVars.MAIL_USERNAME,
|
||||||
mail_password: envVars.MAIL_PASSWORD,
|
mail_password: envVars.MAIL_PASSWORD,
|
||||||
|
minio_endpoint: envVars.MINIO_ENDPOINT,
|
||||||
|
minio_port: envVars.MINIO_PORT,
|
||||||
|
minio_access_key: envVars.MINIO_ACCESS_KEY,
|
||||||
|
minio_secret_key: envVars.MINIO_SECRET_KEY,
|
||||||
|
minio_bucket: envVars.MINIO_BUCKET,
|
||||||
|
minio_use_ssl: envVars.MINIO_USE_SSL,
|
||||||
};
|
};
|
||||||
|
|||||||
9
apps/api/src/common/minio/minio.module.ts
Normal file
9
apps/api/src/common/minio/minio.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { MinioService } from './minio.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [MinioService],
|
||||||
|
exports: [MinioService],
|
||||||
|
})
|
||||||
|
export class MinioModule {}
|
||||||
127
apps/api/src/common/minio/minio.service.ts
Normal file
127
apps/api/src/common/minio/minio.service.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
import * as Minio from 'minio';
|
||||||
|
import { envs } from '../config/envs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MinioService implements OnModuleInit {
|
||||||
|
private readonly minioClient: Minio.Client;
|
||||||
|
private readonly logger = new Logger(MinioService.name);
|
||||||
|
private readonly bucketName = envs.minio_bucket;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.minioClient = new Minio.Client({
|
||||||
|
endPoint: envs.minio_endpoint,
|
||||||
|
port: envs.minio_port,
|
||||||
|
useSSL: envs.minio_use_ssl,
|
||||||
|
accessKey: envs.minio_access_key,
|
||||||
|
secretKey: envs.minio_secret_key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.ensureBucketExists();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureBucketExists() {
|
||||||
|
// Ejecuta esto siempre al menos una vez para asegurar que sea público
|
||||||
|
const policy = {
|
||||||
|
Version: '2012-10-17',
|
||||||
|
Statement: [
|
||||||
|
{
|
||||||
|
Effect: 'Allow',
|
||||||
|
Principal: { AWS: ['*'] },
|
||||||
|
Action: ['s3:GetObject'],
|
||||||
|
Resource: [`arn:aws:s3:::${this.bucketName}/*`],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
// const bucketExists = await this.minioClient.bucketExists(this.bucketName);
|
||||||
|
// if (!bucketExists) {
|
||||||
|
// await this.minioClient.makeBucket(this.bucketName);
|
||||||
|
// }
|
||||||
|
|
||||||
|
await this.minioClient.setBucketPolicy(
|
||||||
|
this.bucketName,
|
||||||
|
JSON.stringify(policy),
|
||||||
|
);
|
||||||
|
this.logger.log(`Public policy ensured for bucket "${this.bucketName}"`);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Error checking/creating bucket: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload(
|
||||||
|
file: Express.Multer.File,
|
||||||
|
folder: string = 'general',
|
||||||
|
): Promise<string> {
|
||||||
|
const fileName = `${Date.now()}-${Math.round(Math.random() * 1e9)}-${file.originalname.replace(/\s/g, '_')}`;
|
||||||
|
const objectName = `${folder}/${fileName}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.minioClient.putObject(
|
||||||
|
this.bucketName,
|
||||||
|
objectName,
|
||||||
|
file.buffer,
|
||||||
|
file.size,
|
||||||
|
{
|
||||||
|
'Content-Type': file.mimetype,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the URL or the object path.
|
||||||
|
// Usually, we store the object path and generate a signed URL or use a proxy.
|
||||||
|
// The user asked for the URL to be stored in the database.
|
||||||
|
return objectName;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Error uploading file: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileUrl(objectName: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
// If the bucket is public, we can just return the URL.
|
||||||
|
// If private, we need a signed URL.
|
||||||
|
// For simplicity and common use cases in these projects, I'll generate a signed URL with a long expiration
|
||||||
|
// or assume there is some way to access it.
|
||||||
|
// But let's use signed URL for 1 week (maximum is 7 days) if needed,
|
||||||
|
// or just return the object name if the backend handles the serving.
|
||||||
|
// The user wants the URL stored in the DB.
|
||||||
|
|
||||||
|
return await this.minioClient.presignedUrl(
|
||||||
|
'GET',
|
||||||
|
this.bucketName,
|
||||||
|
objectName,
|
||||||
|
604800,
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Error getting file URL: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPublicUrl(objectName: string): string {
|
||||||
|
const protocol = envs.minio_use_ssl ? 'https' : 'http';
|
||||||
|
return `${protocol}://${envs.minio_endpoint}:${envs.minio_port}/${this.bucketName}/${objectName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(objectName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Ensure we don't have a leading slash which can cause issues with removeObject
|
||||||
|
const cleanedName = objectName.startsWith('/')
|
||||||
|
? objectName.slice(1)
|
||||||
|
: objectName;
|
||||||
|
|
||||||
|
await this.minioClient.removeObject(this.bucketName, cleanedName);
|
||||||
|
this.logger.log(
|
||||||
|
`Object "${cleanedName}" deleted successfully from bucket "${this.bucketName}".`,
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error deleting file "${objectName}": ${error.message}`,
|
||||||
|
);
|
||||||
|
// We don't necessarily want to throw if the file is already gone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,17 +8,17 @@ import { ThrottlerModule } from '@nestjs/throttler';
|
|||||||
{
|
{
|
||||||
name: 'short',
|
name: 'short',
|
||||||
ttl: 1000, // 1 sec
|
ttl: 1000, // 1 sec
|
||||||
limit: 2,
|
limit: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'medium',
|
name: 'medium',
|
||||||
ttl: 10000, // 10 sec
|
ttl: 10000, // 10 sec
|
||||||
limit: 4,
|
limit: 30,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'long',
|
name: 'long',
|
||||||
ttl: 60000, // 1 min
|
ttl: 60000, // 1 min
|
||||||
limit: 10,
|
limit: 100,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
errorMessage: 'Too many requests, please try again later.',
|
errorMessage: 'Too many requests, please try again later.',
|
||||||
|
|||||||
36
apps/api/src/common/pipes/image-processing.pipe.ts
Normal file
36
apps/api/src/common/pipes/image-processing.pipe.ts
Normal file
@@ -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<Express.Multer.File[] | Express.Multer.File> {
|
||||||
|
if (!files) return files;
|
||||||
|
|
||||||
|
const processItem = async (
|
||||||
|
file: Express.Multer.File,
|
||||||
|
): Promise<Express.Multer.File> => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
apps/api/src/database/migrations/0008_plain_scream.sql
Normal file
37
apps/api/src/database/migrations/0008_plain_scream.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
CREATE TABLE "training_surveys" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"firstname" text NOT NULL,
|
||||||
|
"lastname" text NOT NULL,
|
||||||
|
"visit_date" timestamp NOT NULL,
|
||||||
|
"productive_activity" text NOT NULL,
|
||||||
|
"financial_requirement_description" text NOT NULL,
|
||||||
|
"situr_code_commune" text NOT NULL,
|
||||||
|
"communal_council" text NOT NULL,
|
||||||
|
"situr_code_communal_council" text NOT NULL,
|
||||||
|
"osp_name" text NOT NULL,
|
||||||
|
"osp_address" text NOT NULL,
|
||||||
|
"osp_rif" text NOT NULL,
|
||||||
|
"osp_type" text NOT NULL,
|
||||||
|
"current_status" text NOT NULL,
|
||||||
|
"company_constitution_year" integer NOT NULL,
|
||||||
|
"producer_count" integer NOT NULL,
|
||||||
|
"product_description" text NOT NULL,
|
||||||
|
"installed_capacity" text NOT NULL,
|
||||||
|
"operational_capacity" text NOT NULL,
|
||||||
|
"osp_responsible_fullname" text NOT NULL,
|
||||||
|
"osp_responsible_cedula" text NOT NULL,
|
||||||
|
"osp_responsible_rif" text NOT NULL,
|
||||||
|
"osp_responsible_phone" text NOT NULL,
|
||||||
|
"civil_state" text NOT NULL,
|
||||||
|
"family_burden" integer NOT NULL,
|
||||||
|
"number_of_children" integer NOT NULL,
|
||||||
|
"general_observations" text NOT NULL,
|
||||||
|
"photo1" text NOT NULL,
|
||||||
|
"photo2" text NOT NULL,
|
||||||
|
"photo3" text NOT NULL,
|
||||||
|
"paralysis_reason" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp (3)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "training_surveys_index_00" ON "training_surveys" USING btree ("firstname");
|
||||||
7
apps/api/src/database/migrations/0009_eminent_ares.sql
Normal file
7
apps/api/src/database/migrations/0009_eminent_ares.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "state" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "municipality" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "parish" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "osp_responsible_email" text NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_state_states_id_fk" FOREIGN KEY ("state") REFERENCES "public"."states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_municipality_municipalities_id_fk" FOREIGN KEY ("municipality") REFERENCES "public"."municipalities"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_parish_parishes_id_fk" FOREIGN KEY ("parish") REFERENCES "public"."parishes"("id") ON DELETE set null ON UPDATE no action;
|
||||||
4
apps/api/src/database/migrations/0010_dashing_bishop.sql
Normal file
4
apps/api/src/database/migrations/0010_dashing_bishop.sql
Normal file
@@ -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;
|
||||||
14
apps/api/src/database/migrations/0011_magical_thundra.sql
Normal file
14
apps/api/src/database/migrations/0011_magical_thundra.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_name" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_rif" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_spokesperson_name" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_spokesperson_cedula" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_spokesperson_rif" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_spokesperson_phone" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_email" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_rif" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_spokesperson_name" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_spokesperson_cedula" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_spokesperson_rif" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_spokesperson_phone" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_email" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "osp_google_maps_link" text DEFAULT '' NOT NULL;
|
||||||
20
apps/api/src/database/migrations/0012_sudden_venus.sql
Normal file
20
apps/api/src/database/migrations/0012_sudden_venus.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_state" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_municipality" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_parish" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_phone" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "eco_sector" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "productive_sector" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "central_productive_activity" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "main_productive_activity" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "types_of_equipment" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "equipment_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "equipment_description" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "raw_material" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "material_type" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "raw_material_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_count_daily" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_count_weekly" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_count_monthly" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_coor_state_states_id_fk" FOREIGN KEY ("coor_state") REFERENCES "public"."states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_coor_municipality_municipalities_id_fk" FOREIGN KEY ("coor_municipality") REFERENCES "public"."municipalities"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_coor_parish_parishes_id_fk" FOREIGN KEY ("coor_parish") REFERENCES "public"."parishes"("id") ON DELETE set null ON UPDATE no action;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "financial_requirement_description" SET DEFAULT '';--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "prod_description_internal" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "internal_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "prod_description_external" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "country" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "city" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "men_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "women_count" integer DEFAULT 0 NOT NULL;
|
||||||
9
apps/api/src/database/migrations/0014_deep_meteorite.sql
Normal file
9
apps/api/src/database/migrations/0014_deep_meteorite.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "infrastructure_mt2" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "has_transport" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "structure_type" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "is_open_space" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "equipment_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "production_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "internal_distribution_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_distribution_list" jsonb DEFAULT '[]'::jsonb NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "photo1" DROP NOT NULL;
|
||||||
36
apps/api/src/database/migrations/0016_silent_tag.sql
Normal file
36
apps/api/src/database/migrations/0016_silent_tag.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_state_states_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_municipality_municipalities_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_parish_parishes_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "general_observations" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "paralysis_reason" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "financial_requirement_description";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "producer_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_description";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "installed_capacity";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "operational_capacity";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "coor_state";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "coor_municipality";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "coor_parish";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "types_of_equipment";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "equipment_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "equipment_description";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "raw_material";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "material_type";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "raw_material_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_count_daily";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_count_weekly";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_count_monthly";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "prod_description_internal";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "internal_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "external_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "prod_description_external";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "country";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "city";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "men_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "women_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "internal_distribution_list";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "external_distribution_list";
|
||||||
2
apps/api/src/database/migrations/0017_mute_mole_man.sql
Normal file
2
apps/api/src/database/migrations/0017_mute_mole_man.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "auth"."sessions" ADD COLUMN "previous_session_token" varchar;--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth"."sessions" ADD COLUMN "last_rotated_at" timestamp;
|
||||||
4
apps/api/src/database/migrations/0018_milky_prism.sql
Normal file
4
apps/api/src/database/migrations/0018_milky_prism.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "created_by" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "updated_by" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_updated_by_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
10
apps/api/src/database/migrations/0019_cuddly_cobalt_man.sql
Normal file
10
apps/api/src/database/migrations/0019_cuddly_cobalt_man.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "commune_spokesperson_cedula" DROP DEFAULT;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "commune_spokesperson_cedula" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "commune_spokesperson_rif" DROP DEFAULT;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "commune_spokesperson_rif" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "commune_email" DROP DEFAULT;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "commune_email" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "communal_council_spokesperson_cedula" DROP DEFAULT;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "communal_council_spokesperson_cedula" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "communal_council_spokesperson_rif" DROP DEFAULT;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "communal_council_spokesperson_rif" DROP NOT NULL;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "osp_responsible_rif" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "civil_state" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "osp_responsible_email" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "family_burden" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "number_of_children" DROP NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "osp_rif" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "osp_name" DROP NOT NULL;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "internal_distribution_zone" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "is_exporting" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_country" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_city" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_description" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_quantity" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_unit" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "women_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "men_count" integer DEFAULT 0 NOT NULL;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
DROP INDEX "training_surveys_index_00";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_full_name" text NOT NULL;--> statement-breakpoint
|
||||||
|
CREATE INDEX "training_surveys_index_00" ON "training_surveys" USING btree ("coor_full_name");--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "firstname";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "lastname";
|
||||||
1
apps/api/src/database/migrations/0024_petite_sabra.sql
Normal file
1
apps/api/src/database/migrations/0024_petite_sabra.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE VIEW "public"."v_training_surveys" AS (select "id", "osp_name", "osp_rif", "osp_type", "current_status", "visit_date" from "training_surveys");
|
||||||
2
apps/api/src/database/migrations/0025_funny_makkari.sql
Normal file
2
apps/api/src/database/migrations/0025_funny_makkari.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DROP VIEW "public"."v_training_surveys";--> statement-breakpoint
|
||||||
|
CREATE VIEW "public"."v_training_surveys" AS (select "id", "coor_full_name", "visit_date", "coor_phone", "state", "municipality", "parish", "osp_type", "eco_sector", "productive_sector", "central_productive_activity", "main_productive_activity", "productive_activity", "osp_rif", "osp_name", "company_constitution_year", "current_status", "infrastructure_mt2", "has_transport", "structure_type", "is_open_space", "paralysis_reason", "equipment_list", "production_list", "product_list", "osp_address", "osp_google_maps_link", "commune_name", "situr_code_commune", "commune_rif", "commune_spokesperson_name", "commune_spokesperson_cedula", "commune_spokesperson_rif", "commune_spokesperson_phone", "commune_email", "communal_council", "situr_code_communal_council", "communal_council_rif", "communal_council_spokesperson_name", "communal_council_spokesperson_cedula", "communal_council_spokesperson_rif", "communal_council_spokesperson_phone", "communal_council_email", "osp_responsible_fullname", "osp_responsible_cedula", "osp_responsible_rif", "civil_state", "osp_responsible_phone", "osp_responsible_email", "family_burden", "number_of_children", "general_observations", "internal_distribution_zone", "is_exporting", "external_country", "external_city", "external_description", "external_quantity", "external_unit", "women_count", "men_count", "photo1", "photo2", "photo3", "created_by", "updated_by", "created_at", "updated_at" from "training_surveys");
|
||||||
2
apps/api/src/database/migrations/0026_last_vampiro.sql
Normal file
2
apps/api/src/database/migrations/0026_last_vampiro.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DROP VIEW "public"."v_training_surveys";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "productive_activity_other" text DEFAULT '';
|
||||||
1778
apps/api/src/database/migrations/meta/0008_snapshot.json
Normal file
1778
apps/api/src/database/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1842
apps/api/src/database/migrations/meta/0009_snapshot.json
Normal file
1842
apps/api/src/database/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1850
apps/api/src/database/migrations/meta/0010_snapshot.json
Normal file
1850
apps/api/src/database/migrations/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1948
apps/api/src/database/migrations/meta/0011_snapshot.json
Normal file
1948
apps/api/src/database/migrations/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2102
apps/api/src/database/migrations/meta/0012_snapshot.json
Normal file
2102
apps/api/src/database/migrations/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2159
apps/api/src/database/migrations/meta/0013_snapshot.json
Normal file
2159
apps/api/src/database/migrations/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2222
apps/api/src/database/migrations/meta/0014_snapshot.json
Normal file
2222
apps/api/src/database/migrations/meta/0014_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2222
apps/api/src/database/migrations/meta/0015_snapshot.json
Normal file
2222
apps/api/src/database/migrations/meta/0015_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1994
apps/api/src/database/migrations/meta/0016_snapshot.json
Normal file
1994
apps/api/src/database/migrations/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2006
apps/api/src/database/migrations/meta/0017_snapshot.json
Normal file
2006
apps/api/src/database/migrations/meta/0017_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2046
apps/api/src/database/migrations/meta/0018_snapshot.json
Normal file
2046
apps/api/src/database/migrations/meta/0018_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2041
apps/api/src/database/migrations/meta/0019_snapshot.json
Normal file
2041
apps/api/src/database/migrations/meta/0019_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2041
apps/api/src/database/migrations/meta/0020_snapshot.json
Normal file
2041
apps/api/src/database/migrations/meta/0020_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2041
apps/api/src/database/migrations/meta/0021_snapshot.json
Normal file
2041
apps/api/src/database/migrations/meta/0021_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2098
apps/api/src/database/migrations/meta/0022_snapshot.json
Normal file
2098
apps/api/src/database/migrations/meta/0022_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2092
apps/api/src/database/migrations/meta/0023_snapshot.json
Normal file
2092
apps/api/src/database/migrations/meta/0023_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2138
apps/api/src/database/migrations/meta/0024_snapshot.json
Normal file
2138
apps/api/src/database/migrations/meta/0024_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2534
apps/api/src/database/migrations/meta/0025_snapshot.json
Normal file
2534
apps/api/src/database/migrations/meta/0025_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2099
apps/api/src/database/migrations/meta/0026_snapshot.json
Normal file
2099
apps/api/src/database/migrations/meta/0026_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,139 @@
|
|||||||
"when": 1754420096323,
|
"when": 1754420096323,
|
||||||
"tag": "0007_curved_fantastic_four",
|
"tag": "0007_curved_fantastic_four",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764623430844,
|
||||||
|
"tag": "0008_plain_scream",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764883378610,
|
||||||
|
"tag": "0009_eminent_ares",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769097895095,
|
||||||
|
"tag": "0010_dashing_bishop",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769618795008,
|
||||||
|
"tag": "0011_magical_thundra",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769621656400,
|
||||||
|
"tag": "0012_sudden_venus",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 13,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769629815868,
|
||||||
|
"tag": "0013_cuddly_night_nurse",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 14,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769646908602,
|
||||||
|
"tag": "0014_deep_meteorite",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769648728698,
|
||||||
|
"tag": "0015_concerned_wild_pack",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769653021994,
|
||||||
|
"tag": "0016_silent_tag",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1770774052351,
|
||||||
|
"tag": "0017_mute_mole_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1771855467870,
|
||||||
|
"tag": "0018_milky_prism",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 19,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1771858973096,
|
||||||
|
"tag": "0019_cuddly_cobalt_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1771897944334,
|
||||||
|
"tag": "0020_certain_bushwacker",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1771901546945,
|
||||||
|
"tag": "0021_warm_machine_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 22,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1772031518006,
|
||||||
|
"tag": "0022_nervous_dragon_lord",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 23,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1772032122473,
|
||||||
|
"tag": "0023_sticky_slayback",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 24,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1772642460042,
|
||||||
|
"tag": "0024_petite_sabra",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 25,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1772643066120,
|
||||||
|
"tag": "0025_funny_makkari",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 26,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774379641691,
|
||||||
|
"tag": "0026_last_vampiro",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as t from 'drizzle-orm/pg-core';
|
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { authSchema } from './schemas';
|
import * as t from 'drizzle-orm/pg-core';
|
||||||
import { timestamps } from '../timestamps';
|
import { timestamps } from '../timestamps';
|
||||||
import { states, municipalities, parishes } from './general';
|
import { municipalities, parishes, states } from './general';
|
||||||
|
import { authSchema } from './schemas';
|
||||||
|
|
||||||
// Tabla de Usuarios sistema
|
// Tabla de Usuarios sistema
|
||||||
export const users = authSchema.table(
|
export const users = authSchema.table(
|
||||||
@@ -15,9 +14,15 @@ export const users = authSchema.table(
|
|||||||
fullname: t.text('fullname').notNull(),
|
fullname: t.text('fullname').notNull(),
|
||||||
phone: t.text('phone'),
|
phone: t.text('phone'),
|
||||||
password: t.text('password').notNull(),
|
password: t.text('password').notNull(),
|
||||||
state: t.integer('state').references(() => states.id, { onDelete: 'set null' }),
|
state: t
|
||||||
municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }),
|
.integer('state')
|
||||||
parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }),
|
.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' }),
|
||||||
isTwoFactorEnabled: t
|
isTwoFactorEnabled: t
|
||||||
.boolean('is_two_factor_enabled')
|
.boolean('is_two_factor_enabled')
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -32,7 +37,6 @@ export const users = authSchema.table(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// Tabla de Roles
|
// Tabla de Roles
|
||||||
export const roles = authSchema.table(
|
export const roles = authSchema.table(
|
||||||
'roles',
|
'roles',
|
||||||
@@ -46,8 +50,6 @@ export const roles = authSchema.table(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//tabla User_roles
|
//tabla User_roles
|
||||||
export const usersRole = authSchema.table(
|
export const usersRole = authSchema.table(
|
||||||
'user_role',
|
'user_role',
|
||||||
@@ -88,7 +90,6 @@ LEFT JOIN
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
auth.roles r ON ur.role_id = r.id`);
|
auth.roles r ON ur.role_id = r.id`);
|
||||||
|
|
||||||
|
|
||||||
// Tabla de Sesiones
|
// Tabla de Sesiones
|
||||||
export const sessions = authSchema.table(
|
export const sessions = authSchema.table(
|
||||||
'sessions',
|
'sessions',
|
||||||
@@ -103,6 +104,9 @@ export const sessions = authSchema.table(
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
sessionToken: t.text('session_token').notNull(),
|
sessionToken: t.text('session_token').notNull(),
|
||||||
expiresAt: t.integer('expires_at').notNull(),
|
expiresAt: t.integer('expires_at').notNull(),
|
||||||
|
previousSessionToken: t.varchar('previous_session_token'),
|
||||||
|
lastRotatedAt: t.timestamp('last_rotated_at'),
|
||||||
|
|
||||||
...timestamps,
|
...timestamps,
|
||||||
},
|
},
|
||||||
(sessions) => ({
|
(sessions) => ({
|
||||||
@@ -110,8 +114,6 @@ export const sessions = authSchema.table(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//tabla de tokens de verificación
|
//tabla de tokens de verificación
|
||||||
export const verificationTokens = authSchema.table(
|
export const verificationTokens = authSchema.table(
|
||||||
'verificationToken',
|
'verificationToken',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { sql } from 'drizzle-orm';
|
||||||
import * as t from 'drizzle-orm/pg-core';
|
import * as t from 'drizzle-orm/pg-core';
|
||||||
import { eq, lt, gte, ne, sql } from 'drizzle-orm';
|
|
||||||
import { timestamps } from '../timestamps';
|
import { timestamps } from '../timestamps';
|
||||||
import { users } from './auth';
|
import { users } from './auth';
|
||||||
|
import { municipalities, parishes, states } from './general';
|
||||||
|
|
||||||
// Tabla surveys
|
// Tabla surveys
|
||||||
export const surveys = t.pgTable(
|
export const surveys = t.pgTable(
|
||||||
@@ -18,9 +18,7 @@ export const surveys = t.pgTable(
|
|||||||
...timestamps,
|
...timestamps,
|
||||||
},
|
},
|
||||||
(surveys) => ({
|
(surveys) => ({
|
||||||
surveysIndex: t
|
surveysIndex: t.index('surveys_index_00').on(surveys.title),
|
||||||
.index('surveys_index_00')
|
|
||||||
.on(surveys.title),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -44,7 +42,130 @@ export const answersSurveys = t.pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tabla training_surveys
|
||||||
|
export const trainingSurveys = t.pgTable(
|
||||||
|
'training_surveys',
|
||||||
|
{
|
||||||
|
// === 1. IDENTIFICADORES Y DATOS DE VISITA ===
|
||||||
|
id: t.serial('id').primaryKey(),
|
||||||
|
coorFullName: t.text('coor_full_name').notNull(),
|
||||||
|
visitDate: t.timestamp('visit_date').notNull(),
|
||||||
|
coorPhone: t.text('coor_phone'),
|
||||||
|
|
||||||
|
// === 2. UBICACIÓN (Claves Foráneas - Nullables) ===
|
||||||
|
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' }),
|
||||||
|
|
||||||
|
// === 3. DATOS DE LA OSP (Organización Socioproductiva) ===
|
||||||
|
ospType: t.text('osp_type').notNull(), // UPF, EPS, etc.
|
||||||
|
ecoSector: t.text('eco_sector').notNull().default(''),
|
||||||
|
productiveSector: t.text('productive_sector').notNull().default(''),
|
||||||
|
centralProductiveActivity: t
|
||||||
|
.text('central_productive_activity')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
mainProductiveActivity: t
|
||||||
|
.text('main_productive_activity')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
productiveActivity: t.text('productive_activity').notNull(),
|
||||||
|
productiveActivityOther: t.text('productive_activity_other').default(''),
|
||||||
|
ospRif: t.text('osp_rif'),
|
||||||
|
ospName: t.text('osp_name'),
|
||||||
|
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
||||||
|
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
|
||||||
|
infrastructureMt2: t.text('infrastructure_mt2').notNull().default(''),
|
||||||
|
hasTransport: t.boolean('has_transport').notNull().default(false),
|
||||||
|
structureType: t.text('structure_type').notNull().default(''),
|
||||||
|
isOpenSpace: t.boolean('is_open_space').notNull().default(false),
|
||||||
|
paralysisReason: t.text('paralysis_reason'),
|
||||||
|
equipmentList: t.jsonb('equipment_list').notNull().default([]),
|
||||||
|
productionList: t.jsonb('production_list').notNull().default([]),
|
||||||
|
productList: t.jsonb('product_list').notNull().default([]),
|
||||||
|
ospAddress: t.text('osp_address').notNull(),
|
||||||
|
ospGoogleMapsLink: t.text('osp_google_maps_link').notNull().default(''),
|
||||||
|
communeName: t.text('commune_name').notNull().default(''),
|
||||||
|
siturCodeCommune: t.text('situr_code_commune').notNull(),
|
||||||
|
communeRif: t.text('commune_rif').notNull().default(''),
|
||||||
|
communeSpokespersonName: t
|
||||||
|
.text('commune_spokesperson_name')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
communeSpokespersonCedula: t.text('commune_spokesperson_cedula'),
|
||||||
|
communeSpokespersonRif: t.text('commune_spokesperson_rif'),
|
||||||
|
communeSpokespersonPhone: t
|
||||||
|
.text('commune_spokesperson_phone')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
communeEmail: t.text('commune_email'),
|
||||||
|
communalCouncil: t.text('communal_council').notNull(),
|
||||||
|
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
|
||||||
|
communalCouncilRif: t.text('communal_council_rif').notNull().default(''),
|
||||||
|
communalCouncilSpokespersonName: t
|
||||||
|
.text('communal_council_spokesperson_name')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
communalCouncilSpokespersonCedula: t.text(
|
||||||
|
'communal_council_spokesperson_cedula',
|
||||||
|
),
|
||||||
|
communalCouncilSpokespersonRif: t.text('communal_council_spokesperson_rif'),
|
||||||
|
communalCouncilSpokespersonPhone: t
|
||||||
|
.text('communal_council_spokesperson_phone')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
communalCouncilEmail: t
|
||||||
|
.text('communal_council_email')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
|
||||||
|
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
|
||||||
|
ospResponsibleRif: t.text('osp_responsible_rif'),
|
||||||
|
civilState: t.text('civil_state'),
|
||||||
|
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
|
||||||
|
ospResponsibleEmail: t.text('osp_responsible_email'),
|
||||||
|
familyBurden: t.integer('family_burden'),
|
||||||
|
numberOfChildren: t.integer('number_of_children'),
|
||||||
|
generalObservations: t.text('general_observations'),
|
||||||
|
|
||||||
|
// === 4. DATOS DE DISTRIBUCIÓN Y EXPORTACIÓN ===
|
||||||
|
internalDistributionZone: t.text('internal_distribution_zone'),
|
||||||
|
isExporting: t.boolean('is_exporting').notNull().default(false),
|
||||||
|
externalCountry: t.text('external_country'),
|
||||||
|
externalCity: t.text('external_city'),
|
||||||
|
externalDescription: t.text('external_description'),
|
||||||
|
externalQuantity: t.text('external_quantity'),
|
||||||
|
externalUnit: t.text('external_unit'),
|
||||||
|
|
||||||
|
// === 5. MANO DE OBRA ===
|
||||||
|
womenCount: t.integer('women_count').notNull().default(0),
|
||||||
|
menCount: t.integer('men_count').notNull().default(0),
|
||||||
|
|
||||||
|
// Fotos
|
||||||
|
photo1: t.text('photo1'),
|
||||||
|
photo2: t.text('photo2'),
|
||||||
|
photo3: t.text('photo3'),
|
||||||
|
// informacion del usuario que creo y actualizo el registro
|
||||||
|
createdBy: t
|
||||||
|
.integer('created_by')
|
||||||
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
updatedBy: t
|
||||||
|
.integer('updated_by')
|
||||||
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
...timestamps,
|
||||||
|
},
|
||||||
|
(trainingSurveys) => ({
|
||||||
|
trainingSurveysIndex: t
|
||||||
|
.index('training_surveys_index_00')
|
||||||
|
.on(trainingSurveys.coorFullName),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const viewSurveys = t.pgView('v_surveys', {
|
export const viewSurveys = t.pgView('v_surveys', {
|
||||||
surverId: t.integer('survey_id'),
|
surverId: t.integer('survey_id'),
|
||||||
@@ -52,6 +173,7 @@ export const viewSurveys = t.pgView('v_surveys', {
|
|||||||
description: t.text('description'),
|
description: t.text('description'),
|
||||||
created_at: t.timestamp('created_at'),
|
created_at: t.timestamp('created_at'),
|
||||||
closingDate: t.date('closing_date'),
|
closingDate: t.date('closing_date'),
|
||||||
targetAudience: t.varchar('target_audience')
|
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`);
|
.as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
|
||||||
|
where published = true`);
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
|
// api/src/feacture/auth/auth.controller.ts
|
||||||
import { Public } from '@/common/decorators';
|
import { Public } from '@/common/decorators';
|
||||||
import { JwtRefreshGuard } from '@/common/guards/jwt-refresh.guard';
|
|
||||||
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
|
||||||
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
|
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
|
||||||
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
||||||
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
||||||
import {
|
import { Body, Controller, HttpCode, Patch, Post } from '@nestjs/common';
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
HttpCode,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
Req,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) { }
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@@ -38,6 +28,8 @@ export class AuthController {
|
|||||||
return await this.authService.signIn(signInUserDto);
|
return await this.authService.signIn(signInUserDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@HttpCode(200)
|
||||||
@Post('sign-out')
|
@Post('sign-out')
|
||||||
//@RequirePermissions('auth:sign-out')
|
//@RequirePermissions('auth:sign-out')
|
||||||
async signOut(@Body() signOutUserDto: SignOutUserDto) {
|
async signOut(@Body() signOutUserDto: SignOutUserDto) {
|
||||||
@@ -51,26 +43,17 @@ export class AuthController {
|
|||||||
// return { message: 'Password reset link sent to your email' };
|
// return { message: 'Password reset link sent to your email' };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@UseGuards(JwtRefreshGuard)
|
// @UseGuards(JwtRefreshGuard)
|
||||||
@Public()
|
@Public()
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@Patch('refresh')
|
@Patch('refresh')
|
||||||
//@RequirePermissions('auth:refresh-token')
|
//@RequirePermissions('auth:refresh-token')
|
||||||
async refreshToken(@Req() req: Request,@Body() refreshTokenDto: RefreshTokenDto) {
|
async refreshToken(@Body() refreshTokenDto: any) {
|
||||||
|
// console.log('REFRESCANDO');
|
||||||
// console.log("Pepeeeee");
|
// console.log(refreshTokenDto);
|
||||||
// console.log(req['user']);
|
// console.log('-----------');
|
||||||
// console.log("refreshTokenDto",refreshTokenDto);
|
|
||||||
// console.log(typeof refreshTokenDto);
|
|
||||||
|
|
||||||
const data = await this.authService.refreshToken(refreshTokenDto,req['user'].sub);
|
return await this.authService.refreshToken(refreshTokenDto);
|
||||||
// console.log("data",data);
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {tokens: data}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Public()
|
// @Public()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
// auth.service
|
||||||
import { envs } from '@/common/config/envs';
|
import { envs } from '@/common/config/envs';
|
||||||
import { Env, validateString } from '@/common/utils';
|
import { Env, validateString } from '@/common/utils';
|
||||||
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
||||||
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
||||||
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
|
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
|
||||||
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
|
||||||
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
||||||
|
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
||||||
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
|
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
|
||||||
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
|
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
|
||||||
import {
|
import {
|
||||||
@@ -23,14 +24,14 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { and, eq, or } from 'drizzle-orm';
|
import { eq, or } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import * as schema from 'src/database/index';
|
import * as schema from 'src/database/index';
|
||||||
import { sessions, users, roles, usersRole } from 'src/database/index';
|
import { roles, sessions, users, usersRole } from 'src/database/index';
|
||||||
import { Session } from './interfaces/session.interface';
|
import { Session } from './interfaces/session.interface';
|
||||||
import * as bcrypt from 'bcryptjs';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -39,7 +40,7 @@ export class AuthService {
|
|||||||
private readonly config: ConfigService<Env>,
|
private readonly config: ConfigService<Env>,
|
||||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
//Decode Tokens
|
//Decode Tokens
|
||||||
// Método para decodificar el token y obtener los datos completos
|
// Método para decodificar el token y obtener los datos completos
|
||||||
@@ -80,33 +81,43 @@ export class AuthService {
|
|||||||
|
|
||||||
//Generate Tokens
|
//Generate Tokens
|
||||||
async generateTokens(user: User): Promise<AuthTokensInterface> {
|
async generateTokens(user: User): Promise<AuthTokensInterface> {
|
||||||
|
const accessTokenSecret = envs.access_token_secret ?? '';
|
||||||
|
const accessTokenExp = envs.access_token_expiration ?? '';
|
||||||
|
const refreshTokenSecret = envs.refresh_token_secret ?? '';
|
||||||
|
const refreshTokenExp = envs.refresh_token_expiration ?? '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
!accessTokenSecret ||
|
||||||
|
!accessTokenExp ||
|
||||||
|
!refreshTokenSecret ||
|
||||||
|
!refreshTokenExp
|
||||||
|
) {
|
||||||
|
throw new Error('JWT environment variables are missing or invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JwtPayload {
|
||||||
|
sub: number;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
sub: Number(user?.id),
|
||||||
|
username: user.username ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
const [access_token, refresh_token] = await Promise.all([
|
const [access_token, refresh_token] = await Promise.all([
|
||||||
this.jwtService.signAsync(
|
this.jwtService.signAsync(payload, {
|
||||||
{
|
secret: accessTokenSecret,
|
||||||
sub: user.id,
|
expiresIn: accessTokenExp,
|
||||||
username: user.username,
|
} as JwtSignOptions),
|
||||||
},
|
|
||||||
{
|
this.jwtService.signAsync(payload, {
|
||||||
secret: envs.access_token_secret,
|
secret: refreshTokenSecret,
|
||||||
expiresIn: envs.access_token_expiration,
|
expiresIn: refreshTokenExp,
|
||||||
},
|
} as JwtSignOptions),
|
||||||
),
|
|
||||||
this.jwtService.signAsync(
|
|
||||||
{
|
|
||||||
sub: user.id,
|
|
||||||
username: user.username,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secret: envs.refresh_token_secret,
|
|
||||||
expiresIn: envs.refresh_token_expiration,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return { access_token, refresh_token };
|
||||||
access_token,
|
|
||||||
refresh_token,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Generate OTP Code For Email Confirmation
|
//Generate OTP Code For Email Confirmation
|
||||||
@@ -137,7 +148,8 @@ export class AuthService {
|
|||||||
userId: parseInt(userId),
|
userId: parseInt(userId),
|
||||||
expiresAt: sessionInput.expiresAt,
|
expiresAt: sessionInput.expiresAt,
|
||||||
});
|
});
|
||||||
if (session.rowCount === 0) throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);
|
if (session.rowCount === 0)
|
||||||
|
throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);
|
||||||
|
|
||||||
return 'Session created successfully';
|
return 'Session created successfully';
|
||||||
}
|
}
|
||||||
@@ -196,7 +208,6 @@ export class AuthService {
|
|||||||
|
|
||||||
//Sign In User Account
|
//Sign In User Account
|
||||||
async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
|
async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
|
||||||
|
|
||||||
const user = await this.validateUser(dto);
|
const user = await this.validateUser(dto);
|
||||||
const tokens = await this.generateTokens(user);
|
const tokens = await this.generateTokens(user);
|
||||||
const decodeAccess = this.decodeToken(tokens.access_token);
|
const decodeAccess = this.decodeToken(tokens.access_token);
|
||||||
@@ -261,115 +272,197 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Refresh User Access Token
|
//Refresh User Access Token
|
||||||
async refreshToken(dto: RefreshTokenDto,user_id:number): Promise<RefreshTokenInterface> {
|
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
|
||||||
// const { user_id } = dto;
|
const { refreshToken } = dto;
|
||||||
// const user_id = 1;
|
|
||||||
|
|
||||||
const session = await this.drizzle
|
// 1. Validar firma del token (Crypto check)
|
||||||
|
let payload: any;
|
||||||
|
try {
|
||||||
|
payload = await this.jwtService.verifyAsync(refreshToken, {
|
||||||
|
secret: envs.refresh_token_secret,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new UnauthorizedException('Invalid Refresh Token Signature');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = Number(payload.sub); // Es más seguro usar el ID del token que el del DTO
|
||||||
|
|
||||||
|
// 2. Buscar la sesión por UserID (SIN filtrar por token todavía)
|
||||||
|
// Esto es clave: traemos la sesión para ver qué está pasando
|
||||||
|
const [currentSession] = await this.drizzle
|
||||||
.select()
|
.select()
|
||||||
.from(sessions)
|
.from(sessions)
|
||||||
.where(
|
.where(eq(sessions.userId, userId));
|
||||||
and(
|
|
||||||
eq(sessions.userId, user_id) &&
|
if (!currentSession) throw new NotFoundException('Session not found');
|
||||||
eq(sessions.sessionToken, dto.refresh_token),
|
|
||||||
),
|
// CONFIGURACIÓN: Tiempo de gracia en milisegundos (ej: 15 segundos)
|
||||||
|
const GRACE_PERIOD_MS = 15000;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// ESCENARIO A: Rotación Normal (El token coincide con el actual)
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
if (currentSession.sessionToken === refreshToken) {
|
||||||
|
const user = await this.findUserById(userId);
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
|
||||||
|
// Generar nuevos tokens (A -> B)
|
||||||
|
const tokensNew = await this.generateTokens(user);
|
||||||
|
const decodeAccess = this.decodeToken(tokensNew.access_token);
|
||||||
|
const decodeRefresh = this.decodeToken(tokensNew.refresh_token);
|
||||||
|
|
||||||
|
// Actualizamos DB guardando el token "viejo" como "previous"
|
||||||
|
await this.drizzle
|
||||||
|
.update(sessions)
|
||||||
|
.set({
|
||||||
|
sessionToken: tokensNew.refresh_token, // Nuevo (B)
|
||||||
|
previousSessionToken: refreshToken, // Viejo (A)
|
||||||
|
lastRotatedAt: new Date(), // Marca de tiempo
|
||||||
|
expiresAt: decodeRefresh.exp,
|
||||||
|
})
|
||||||
|
.where(eq(sessions.userId, userId));
|
||||||
|
|
||||||
|
return {
|
||||||
|
access_token: tokensNew.access_token,
|
||||||
|
access_expire_in: decodeAccess.exp,
|
||||||
|
refresh_token: tokensNew.refresh_token,
|
||||||
|
refresh_expire_in: decodeRefresh.exp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// ESCENARIO B: Periodo de Gracia (Condición de Carrera)
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// El token no coincide con el actual, ¿pero coincide con el anterior?
|
||||||
|
const isPreviousToken =
|
||||||
|
currentSession.previousSessionToken === refreshToken;
|
||||||
|
|
||||||
|
// Calculamos cuánto tiempo ha pasado desde la rotación
|
||||||
|
const timeSinceRotation = currentSession.lastRotatedAt
|
||||||
|
? Date.now() - new Date(currentSession.lastRotatedAt).getTime()
|
||||||
|
: Infinity;
|
||||||
|
|
||||||
|
if (isPreviousToken && timeSinceRotation < GRACE_PERIOD_MS) {
|
||||||
|
// ¡Es una condición de carrera! El usuario envió 'A' pero ya rotamos a 'B'.
|
||||||
|
// Le devolvemos 'B' (el actual en DB) para que se sincronice.
|
||||||
|
|
||||||
|
const user = await this.findUserById(userId);
|
||||||
|
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
|
||||||
|
// Generamos un access token nuevo fresco (barato)
|
||||||
|
const accessTokenPayload = { sub: user.id, username: user.username };
|
||||||
|
const newAccessToken = await this.jwtService.signAsync(
|
||||||
|
accessTokenPayload,
|
||||||
|
{
|
||||||
|
secret: envs.access_token_secret,
|
||||||
|
expiresIn: envs.access_token_expiration,
|
||||||
|
} as JwtSignOptions,
|
||||||
);
|
);
|
||||||
|
const decodeAccess = this.decodeToken(newAccessToken);
|
||||||
|
|
||||||
// console.log(session.length);
|
// IMPORTANTE: Devolvemos el refresh token QUE YA ESTÁ EN LA BASE DE DATOS
|
||||||
|
// No generamos uno nuevo para no romper la cadena de la otra petición que ganó.
|
||||||
if (session.length === 0) throw new NotFoundException('session not found');
|
return {
|
||||||
const user = await this.findUserById(user_id);
|
access_token: newAccessToken,
|
||||||
if (!user) throw new NotFoundException('User not found');
|
access_expire_in: decodeAccess.exp,
|
||||||
|
refresh_token: currentSession.sessionToken!, // Devolvemos el token 'B'
|
||||||
// Genera token
|
refresh_expire_in: currentSession.expiresAt as number,
|
||||||
const tokens = await this.generateTokens(user);
|
};
|
||||||
const decodeAccess = this.decodeToken(tokens.access_token);
|
}
|
||||||
const decodeRefresh = this.decodeToken(tokens.refresh_token);
|
|
||||||
|
|
||||||
// Actualiza session
|
|
||||||
await this.drizzle
|
|
||||||
.update(sessions)
|
|
||||||
.set({ sessionToken: tokens.refresh_token, expiresAt: decodeRefresh.exp })
|
|
||||||
.where(eq(sessions.userId, user_id));
|
|
||||||
|
|
||||||
return {
|
// -------------------------------------------------------------------
|
||||||
access_token: tokens.access_token,
|
// ESCENARIO C: Robo de Token (Reuse Detection)
|
||||||
access_expire_in: decodeAccess.exp,
|
// -------------------------------------------------------------------
|
||||||
refresh_token: tokens.refresh_token,
|
// Si el token no es el actual, ni el anterior válido... ALGUIEN LO ROBÓ.
|
||||||
refresh_expire_in: decodeRefresh.exp,
|
// O el usuario está intentando reusar un token muy viejo.
|
||||||
};
|
|
||||||
|
// Medida de seguridad: Borrar todas las sesiones del usuario
|
||||||
|
await this.drizzle.delete(sessions).where(eq(sessions.userId, userId));
|
||||||
|
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
'Refresh token reuse detected. Access revoked.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async singUp(createUserDto: SingUpUserDto): Promise<User> {
|
async singUp(createUserDto: SingUpUserDto): Promise<User> {
|
||||||
// Check if username or email exists
|
// Check if username or email exists
|
||||||
const data = await this.drizzle
|
const data = await this.drizzle
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
username: users.username,
|
||||||
|
email: users.email,
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
eq(users.username, createUserDto.username),
|
||||||
|
eq(users.email, createUserDto.email),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
if (data[0].username === createUserDto.username) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Username already exists',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data[0].email === createUserDto.email) {
|
||||||
|
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
return await this.drizzle.transaction(async (tx) => {
|
||||||
|
// Hash the password
|
||||||
|
// Create the user
|
||||||
|
const [newUser] = await tx
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
username: createUserDto.username,
|
||||||
|
email: createUserDto.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
fullname: createUserDto.fullname,
|
||||||
|
isActive: true,
|
||||||
|
state: createUserDto.state,
|
||||||
|
municipality: createUserDto.municipality,
|
||||||
|
parish: createUserDto.parish,
|
||||||
|
phone: createUserDto.phone,
|
||||||
|
isEmailVerified: false,
|
||||||
|
isTwoFactorEnabled: false,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// check if user role is admin
|
||||||
|
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
|
||||||
|
// check if user role is admin
|
||||||
|
|
||||||
|
// Assign role to user
|
||||||
|
await tx.insert(usersRole).values({
|
||||||
|
userId: newUser.id,
|
||||||
|
roleId: role,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the created user with role
|
||||||
|
const [userWithRole] = await tx
|
||||||
.select({
|
.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
username: users.username,
|
username: users.username,
|
||||||
email: users.email
|
email: users.email,
|
||||||
|
fullname: users.fullname,
|
||||||
|
phone: users.phone,
|
||||||
|
isActive: users.isActive,
|
||||||
|
role: roles.name,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(or(eq(users.username, createUserDto.username), eq(users.email, createUserDto.email)));
|
.leftJoin(usersRole, eq(usersRole.userId, users.id))
|
||||||
|
.leftJoin(roles, eq(roles.id, usersRole.roleId))
|
||||||
if (data.length > 0) {
|
.where(eq(users.id, newUser.id));
|
||||||
if (data[0].username === createUserDto.username) {
|
|
||||||
throw new HttpException('Username already exists', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
if (data[0].email === createUserDto.email) {
|
|
||||||
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the password
|
return userWithRole;
|
||||||
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
});
|
||||||
|
|
||||||
// Start a transaction
|
|
||||||
return await this.drizzle.transaction(async (tx) => {
|
|
||||||
// Create the user
|
|
||||||
const [newUser] = await tx
|
|
||||||
.insert(users)
|
|
||||||
.values({
|
|
||||||
username: createUserDto.username,
|
|
||||||
email: createUserDto.email,
|
|
||||||
password: hashedPassword,
|
|
||||||
fullname: createUserDto.fullname,
|
|
||||||
isActive: true,
|
|
||||||
state: createUserDto.state,
|
|
||||||
municipality: createUserDto.municipality,
|
|
||||||
parish: createUserDto.parish,
|
|
||||||
phone: createUserDto.phone,
|
|
||||||
isEmailVerified: false,
|
|
||||||
isTwoFactorEnabled: false,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// check if user role is admin
|
|
||||||
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
|
|
||||||
|
|
||||||
// Assign role to user
|
|
||||||
await tx.insert(usersRole).values({
|
|
||||||
userId: newUser.id,
|
|
||||||
roleId: role,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the created user with role
|
|
||||||
const [userWithRole] = await tx
|
|
||||||
.select({
|
|
||||||
id: users.id,
|
|
||||||
username: users.username,
|
|
||||||
email: users.email,
|
|
||||||
fullname: users.fullname,
|
|
||||||
phone: users.phone,
|
|
||||||
isActive: users.isActive,
|
|
||||||
role: roles.name,
|
|
||||||
})
|
|
||||||
.from(users)
|
|
||||||
.leftJoin(usersRole, eq(usersRole.userId, users.id))
|
|
||||||
.leftJoin(roles, eq(roles.id, usersRole.roleId))
|
|
||||||
.where(eq(users.id, newUser.id));
|
|
||||||
|
|
||||||
return userWithRole;
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// refresh-token
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNumber, IsString } from 'class-validator';
|
import { IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
@@ -6,9 +7,9 @@ export class RefreshTokenDto {
|
|||||||
@IsString({
|
@IsString({
|
||||||
message: 'Refresh token must be a string',
|
message: 'Refresh token must be a string',
|
||||||
})
|
})
|
||||||
refresh_token: string;
|
refreshToken: string;
|
||||||
|
|
||||||
// @ApiProperty()
|
@ApiProperty()
|
||||||
// @IsNumber()
|
@IsNumber()
|
||||||
// user_id: number;
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Controller, Post, UploadedFiles, UseInterceptors, Body } from '@nestjs/common';
|
|
||||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
|
||||||
import { PicturesService } from './pictures.service';
|
|
||||||
|
|
||||||
@Controller('pictures')
|
|
||||||
export class PicturesController {
|
|
||||||
constructor(private readonly picturesService: PicturesService) {}
|
|
||||||
|
|
||||||
@Post('upload')
|
|
||||||
@UseInterceptors(FilesInterceptor('urlImg'))
|
|
||||||
async uploadFile(@UploadedFiles() files: Express.Multer.File[], @Body() body: any) {
|
|
||||||
// Aquí puedes acceder a los campos del formulario
|
|
||||||
// console.log('Archivos:', files);
|
|
||||||
// console.log('Otros campos del formulario:', body);
|
|
||||||
const result = await this.picturesService.saveImages(files);
|
|
||||||
|
|
||||||
return { data: result };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PicturesController } from './pictures.controller';
|
|
||||||
import { PicturesService } from './pictures.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [PicturesController],
|
|
||||||
providers: [PicturesService],
|
|
||||||
})
|
|
||||||
export class PicturesModule {}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { writeFile } from 'fs/promises';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PicturesService {
|
|
||||||
/**
|
|
||||||
* Guarda una imagen en el directorio de imágenes.
|
|
||||||
* @param file - El archivo de imagen a guardar.
|
|
||||||
* @returns La ruta de la imagen guardada.
|
|
||||||
*/
|
|
||||||
async saveImages(file: Express.Multer.File[]): Promise<string[]> {
|
|
||||||
// Construye la ruta al directorio de imágenes.
|
|
||||||
|
|
||||||
const picturesPath = join(__dirname, '..', '..', '..', '..', 'uploads','pict');
|
|
||||||
|
|
||||||
console.log(picturesPath);
|
|
||||||
|
|
||||||
let images : string[] = [];
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
file.forEach(async (file) => {
|
|
||||||
count++
|
|
||||||
// Crea un nombre de archivo único para la imagen.
|
|
||||||
const fileName = `${Date.now()}-${count}-${file.originalname}`;
|
|
||||||
images.push(fileName);
|
|
||||||
// console.log(fileName);
|
|
||||||
|
|
||||||
// Construye la ruta completa al archivo de imagen.
|
|
||||||
const filePath = join(picturesPath, fileName);
|
|
||||||
|
|
||||||
// Escribe el archivo de imagen en el disco.
|
|
||||||
await writeFile(filePath, file.buffer);
|
|
||||||
});
|
|
||||||
// Devuelve la ruta de la imagen guardada.
|
|
||||||
// return [file[0].originalname]
|
|
||||||
return images;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
335
apps/api/src/features/training/dto/create-training.dto.ts
Normal file
335
apps/api/src/features/training/dto/create-training.dto.ts
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import { Optional } from '@nestjs/common';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Transform, Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsDateString,
|
||||||
|
IsEmail,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateIf,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateTrainingDto {
|
||||||
|
// === 1. DATOS BÁSICOS ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
coorFullName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsDateString()
|
||||||
|
visitDate: string; // Llega como string ISO "2024-11-11T10:00"
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
coorPhone?: string;
|
||||||
|
|
||||||
|
// === 2. DATOS OSP ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
ospName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
ospRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospType: string; // 'UPF', etc.
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
productiveActivity: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
productiveActivityOther: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
currentStatus: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number) // Convierte "2017" -> 2017
|
||||||
|
companyConstitutionYear: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
ospAddress: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
ospGoogleMapsLink?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
infrastructureMt2?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
structureType?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
hasTransport?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
isOpenSpace?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
paralysisReason?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
generalObservations?: string;
|
||||||
|
|
||||||
|
// === 3. SECTORES ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ecoSector: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
productiveSector: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
centralProductiveActivity: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
mainProductiveActivity: string;
|
||||||
|
|
||||||
|
// === 4. DATOS RESPONSABLE ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleFullname: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleCedula: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
ospResponsibleRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsiblePhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||||
|
@IsEmail()
|
||||||
|
ospResponsibleEmail?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
civilState: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number) // Convierte "3" -> 3
|
||||||
|
familyBurden: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
numberOfChildren: number;
|
||||||
|
|
||||||
|
// === 5. COMUNA Y CONSEJO COMUNAL ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
siturCodeCommune: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeSpokespersonName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeSpokespersonPhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||||
|
@IsEmail()
|
||||||
|
communeEmail?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncil: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
siturCodeCommunalCouncil: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilSpokespersonName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilSpokespersonPhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||||
|
@IsEmail()
|
||||||
|
communalCouncilEmail?: string;
|
||||||
|
|
||||||
|
// === 6. DISTRIBUCIÓN Y EXPORTACIÓN ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
internalDistributionZone?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
isExporting?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
externalCountry?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
externalCity?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
externalDescription?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
externalQuantity?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
externalUnit?: string;
|
||||||
|
|
||||||
|
// === 7. MANO DE OBRA ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
womenCount?: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
menCount?: number;
|
||||||
|
|
||||||
|
// === 8. LISTAS (Arrays JSON) ===
|
||||||
|
// Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
equipmentList?: any[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
productionList?: any[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
productList?: any[];
|
||||||
|
|
||||||
|
//ubicacion
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
state: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
municipality: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
parish: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
photo1?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
photo2?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
photo3?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber, IsDateString } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class TrainingStatisticsFilterDto {
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
startDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
endDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
stateId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
municipalityId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
parishId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
ospType?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateTrainingDto } from './create-training.dto';
|
||||||
|
|
||||||
|
export class UpdateTrainingDto extends PartialType(CreateTrainingDto) { }
|
||||||
BIN
apps/api/src/features/training/export_template/excel.osp.xlsx
Normal file
BIN
apps/api/src/features/training/export_template/excel.osp.xlsx
Normal file
Binary file not shown.
142
apps/api/src/features/training/training.controller.ts
Normal file
142
apps/api/src/features/training/training.controller.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
Req,
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
// @Public()
|
||||||
|
// @Get('export/:id')
|
||||||
|
// @ApiOperation({ summary: 'Export training template' })
|
||||||
|
// @ApiResponse({
|
||||||
|
// status: 200,
|
||||||
|
// description: 'Return training template.',
|
||||||
|
// content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
|
||||||
|
// })
|
||||||
|
// @Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
// @Header('Content-Disposition', 'attachment; filename=export_osp.xlsx')
|
||||||
|
// async exportTemplate(@Param('id') id: string) {
|
||||||
|
// if (!Number(id)) {
|
||||||
|
// throw new Error('ID is required');
|
||||||
|
// }
|
||||||
|
// const data = await this.trainingService.exportTemplate(Number(id));
|
||||||
|
// return new StreamableFile(data);
|
||||||
|
// }
|
||||||
|
|
||||||
|
@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(':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()
|
||||||
|
@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(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Body() createTrainingDto: CreateTrainingDto,
|
||||||
|
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const userId = (req as any).user?.id;
|
||||||
|
const data = await this.trainingService.create(
|
||||||
|
createTrainingDto,
|
||||||
|
files,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
return { message: 'Training record created 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(
|
||||||
|
@Req() req: Request,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() updateTrainingDto: UpdateTrainingDto,
|
||||||
|
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const userId = (req as any).user?.id;
|
||||||
|
const data = await this.trainingService.update(
|
||||||
|
+id,
|
||||||
|
updateTrainingDto,
|
||||||
|
files,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/features/training/training.module.ts
Normal file
10
apps/api/src/features/training/training.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TrainingService } from './training.service';
|
||||||
|
import { TrainingController } from './training.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [TrainingController],
|
||||||
|
providers: [TrainingService],
|
||||||
|
exports: [TrainingService],
|
||||||
|
})
|
||||||
|
export class TrainingModule { }
|
||||||
690
apps/api/src/features/training/training.service.ts
Normal file
690
apps/api/src/features/training/training.service.ts
Normal file
@@ -0,0 +1,690 @@
|
|||||||
|
import { MinioService } from '@/common/minio/minio.service';
|
||||||
|
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 { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||||
|
import * as schema 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<typeof schema>,
|
||||||
|
private readonly minioService: MinioService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async findAll(paginationDto?: PaginationDto) {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
search = '',
|
||||||
|
sortBy = 'id',
|
||||||
|
sortOrder = 'asc',
|
||||||
|
} = paginationDto || {};
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
let searchCondition: SQL<unknown> | undefined;
|
||||||
|
if (search) {
|
||||||
|
searchCondition = or(ilike(trainingSurveys.ospName, `%${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<number>`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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
|
||||||
|
const { startDate, endDate, stateId, municipalityId, parishId, ospType } =
|
||||||
|
filterDto;
|
||||||
|
|
||||||
|
const filters: SQL[] = [];
|
||||||
|
|
||||||
|
if (startDate)
|
||||||
|
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||||
|
if (endDate)
|
||||||
|
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Ejecutamos todas las consultas en paralelo con Promise.all para mayor velocidad
|
||||||
|
const [
|
||||||
|
totalOspsResult,
|
||||||
|
totalProducersResult,
|
||||||
|
totalProductsResult, // Nuevo: Calculado desde el JSON
|
||||||
|
statusDistribution,
|
||||||
|
activityDistribution,
|
||||||
|
typeDistribution,
|
||||||
|
stateDistribution,
|
||||||
|
yearDistribution,
|
||||||
|
] = await Promise.all([
|
||||||
|
// 1. Total OSPs
|
||||||
|
this.drizzle
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
|
// 2. Total Productores (Columna plana que mantuviste)
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
sum: sql<number>`SUM(${trainingSurveys.womenCount} + ${trainingSurveys.menCount})`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
|
// 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
sum: sql<number>`sum(jsonb_array_length(${trainingSurveys.productList}))`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
|
// 4. Distribución por Estatus
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.currentStatus,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.currentStatus),
|
||||||
|
|
||||||
|
// 5. Distribución por Actividad
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.productiveActivity,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.productiveActivity),
|
||||||
|
|
||||||
|
// 6. Distribución por Tipo
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.ospType,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.ospType),
|
||||||
|
|
||||||
|
// 7. Distribución por Estado (CORREGIDO con COALESCE)
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
// Si states.name es NULL, devuelve 'Sin Asignar'
|
||||||
|
name: sql<string>`COALESCE(${states.name}, 'Sin Asignar')`,
|
||||||
|
value: sql<number>`count(${trainingSurveys.id})`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
||||||
|
.where(whereCondition)
|
||||||
|
// Importante: Agrupar también por el resultado del COALESCE o por states.name
|
||||||
|
.groupBy(states.name),
|
||||||
|
|
||||||
|
// 8. Distribución por Año
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.companyConstitutionYear)
|
||||||
|
.orderBy(trainingSurveys.companyConstitutionYear),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalOsps: Number(totalOspsResult[0]?.count || 0),
|
||||||
|
totalProducers: Number(totalProducersResult[0]?.sum || 0),
|
||||||
|
totalProducts: Number(totalProductsResult[0]?.sum || 0), // Dato extraído del JSON
|
||||||
|
|
||||||
|
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<string[]> {
|
||||||
|
if (!files || files.length === 0) return [];
|
||||||
|
|
||||||
|
const savedPaths: string[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const objectName = await this.minioService.upload(file, 'training');
|
||||||
|
const fileUrl = this.minioService.getPublicUrl(objectName);
|
||||||
|
savedPaths.push(fileUrl);
|
||||||
|
}
|
||||||
|
return savedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteFile(fileUrl: string) {
|
||||||
|
if (!fileUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If it's a full URL, we need to extract the part after the bucket name
|
||||||
|
if (fileUrl.startsWith('http')) {
|
||||||
|
const url = new URL(fileUrl);
|
||||||
|
const pathname = url.pathname; // /bucket/folder/filename
|
||||||
|
const parts = pathname.split('/').filter(Boolean); // ['bucket', 'folder', 'filename']
|
||||||
|
|
||||||
|
// The first part is the bucket name, the rest is the object name
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const objectName = parts.slice(1).join('/');
|
||||||
|
await this.minioService.delete(objectName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a URL or doesn't match the expected format, pass it as is
|
||||||
|
await this.minioService.delete(fileUrl);
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback if URL parsing fails
|
||||||
|
await this.minioService.delete(fileUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
createTrainingDto: CreateTrainingDto,
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
userId: number,
|
||||||
|
) {
|
||||||
|
// 1. Guardar fotos
|
||||||
|
|
||||||
|
const photoPaths = await this.saveFiles(files);
|
||||||
|
|
||||||
|
// 2. Extraer solo visitDate para formatearlo.
|
||||||
|
// Ya NO extraemos state, municipality, etc. porque no vienen en el DTO.
|
||||||
|
const { visitDate, state, municipality, parish, productiveActivityOther, ...rest } =
|
||||||
|
createTrainingDto;
|
||||||
|
|
||||||
|
const [newRecord] = await this.drizzle
|
||||||
|
.insert(trainingSurveys)
|
||||||
|
.values({
|
||||||
|
// Insertamos el resto de datos planos y las listas (arrays)
|
||||||
|
...rest,
|
||||||
|
|
||||||
|
// Conversión de fecha
|
||||||
|
visitDate: new Date(visitDate),
|
||||||
|
|
||||||
|
// Borra las tildes y cambia el texto a mayusculas
|
||||||
|
productiveActivityOther: productiveActivityOther.toUpperCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
|
||||||
|
|
||||||
|
// 3. Asignar fotos de forma segura
|
||||||
|
photo1: photoPaths[0] ?? null,
|
||||||
|
photo2: photoPaths[1] ?? null,
|
||||||
|
photo3: photoPaths[2] ?? null,
|
||||||
|
state: Number(state) ?? null,
|
||||||
|
municipality: Number(municipality) ?? null,
|
||||||
|
parish: Number(parish) ?? null,
|
||||||
|
hasTransport: rest.hasTransport === 'true' ? true : false,
|
||||||
|
isOpenSpace: rest.isOpenSpace === 'true' ? true : false,
|
||||||
|
isExporting: rest.isExporting === 'true' ? true : false,
|
||||||
|
createdBy: userId,
|
||||||
|
updatedBy: userId,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: number,
|
||||||
|
updateTrainingDto: UpdateTrainingDto,
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
userId: number,
|
||||||
|
) {
|
||||||
|
const currentRecord = await this.findOne(id);
|
||||||
|
const photoFields = ['photo1', 'photo2', 'photo3'] as const;
|
||||||
|
|
||||||
|
// 1. Guardar fotos nuevas en MinIO
|
||||||
|
const newFilePaths = await this.saveFiles(files);
|
||||||
|
|
||||||
|
const updateData: any = { ...updateTrainingDto };
|
||||||
|
|
||||||
|
// 2. Determinar el estado final de las fotos (diff)
|
||||||
|
// - Si el DTO tiene un valor (URL existente o ''), lo usamos.
|
||||||
|
// - Si el DTO no tiene el campo (undefined), mantenemos el de la DB.
|
||||||
|
const finalPhotos: (string | null)[] = photoFields.map((field) => {
|
||||||
|
const dtoValue = updateData[field];
|
||||||
|
if (dtoValue !== undefined) {
|
||||||
|
return dtoValue === '' ? null : dtoValue;
|
||||||
|
}
|
||||||
|
return currentRecord[field];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Asignar los nuevos paths subidos a los slots que quedaron vacíos
|
||||||
|
if (newFilePaths.length > 0) {
|
||||||
|
let newIdx = 0;
|
||||||
|
for (let i = 0; i < 3 && newIdx < newFilePaths.length; i++) {
|
||||||
|
if (!finalPhotos[i]) {
|
||||||
|
finalPhotos[i] = newFilePaths[newIdx];
|
||||||
|
newIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. LIMPIEZA: Borrar de MinIO los archivos que ya no están en ningún slot
|
||||||
|
const oldPhotos = photoFields
|
||||||
|
.map((f) => currentRecord[f])
|
||||||
|
.filter((p): p is string => Boolean(p));
|
||||||
|
const newPhotosSet = new Set(finalPhotos.filter(Boolean));
|
||||||
|
|
||||||
|
for (const oldPath of oldPhotos) {
|
||||||
|
if (!newPhotosSet.has(oldPath)) {
|
||||||
|
await this.deleteFile(oldPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Preparar datos finales para la DB
|
||||||
|
updateData.photo1 = finalPhotos[0];
|
||||||
|
updateData.photo2 = finalPhotos[1];
|
||||||
|
updateData.photo3 = finalPhotos[2];
|
||||||
|
|
||||||
|
if (updateTrainingDto.visitDate) {
|
||||||
|
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// actualizamos el id del usuario que actualizo el registro
|
||||||
|
updateData.updatedBy = userId;
|
||||||
|
updateData.hasTransport =
|
||||||
|
updateTrainingDto.hasTransport === 'true' ? true : false;
|
||||||
|
updateData.isOpenSpace =
|
||||||
|
updateTrainingDto.isOpenSpace === 'true' ? true : false;
|
||||||
|
updateData.isExporting =
|
||||||
|
updateTrainingDto.isExporting === 'true' ? true : false;
|
||||||
|
|
||||||
|
const [updatedRecord] = await this.drizzle
|
||||||
|
.update(trainingSurveys)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return updatedRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number) {
|
||||||
|
const record = await this.findOne(id);
|
||||||
|
|
||||||
|
// Delete associated files
|
||||||
|
if (record.photo1) await this.deleteFile(record.photo1);
|
||||||
|
if (record.photo2) await this.deleteFile(record.photo2);
|
||||||
|
if (record.photo3) await this.deleteFile(record.photo3);
|
||||||
|
|
||||||
|
const [deletedRecord] = await this.drizzle
|
||||||
|
.delete(trainingSurveys)
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Training record deleted successfully',
|
||||||
|
data: deletedRecord,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// async exportTemplate() {
|
||||||
|
|
||||||
|
// const templatePath = path.join(
|
||||||
|
// __dirname,
|
||||||
|
// 'export_template',
|
||||||
|
// 'excel.osp.xlsx',
|
||||||
|
// );
|
||||||
|
// const templateBuffer = fs.readFileSync(templatePath);
|
||||||
|
|
||||||
|
// const workbook: any = await XlsxPopulate.fromDataAsync(templateBuffer);
|
||||||
|
// const sheet = workbook.sheet(0);
|
||||||
|
|
||||||
|
// const records = await this.drizzle
|
||||||
|
// .select({
|
||||||
|
// coorFullName: trainingSurveys.coorFullName,
|
||||||
|
// visitDate: trainingSurveys.visitDate,
|
||||||
|
// stateName: states.name,
|
||||||
|
// municipalityName: municipalities.name,
|
||||||
|
// parishName: parishes.name,
|
||||||
|
// communeName: trainingSurveys.communeName,
|
||||||
|
// siturCodeCommune: trainingSurveys.siturCodeCommune,
|
||||||
|
// communalCouncil: trainingSurveys.communalCouncil,
|
||||||
|
// siturCodeCommunalCouncil: trainingSurveys.siturCodeCommunalCouncil,
|
||||||
|
// productiveActivity: trainingSurveys.productiveActivity,
|
||||||
|
// ospName: trainingSurveys.ospName,
|
||||||
|
// ospAddress: trainingSurveys.ospAddress,
|
||||||
|
// ospRif: trainingSurveys.ospRif,
|
||||||
|
// ospType: trainingSurveys.ospType,
|
||||||
|
// currentStatus: trainingSurveys.currentStatus,
|
||||||
|
// companyConstitutionYear: trainingSurveys.companyConstitutionYear,
|
||||||
|
// ospResponsibleFullname: trainingSurveys.ospResponsibleFullname,
|
||||||
|
// ospResponsibleCedula: trainingSurveys.ospResponsibleCedula,
|
||||||
|
// ospResponsibleRif: trainingSurveys.ospResponsibleRif,
|
||||||
|
// ospResponsiblePhone: trainingSurveys.ospResponsiblePhone,
|
||||||
|
// ospResponsibleEmail: trainingSurveys.ospResponsibleEmail,
|
||||||
|
// civilState: trainingSurveys.civilState,
|
||||||
|
// familyBurden: trainingSurveys.familyBurden,
|
||||||
|
// numberOfChildren: trainingSurveys.numberOfChildren,
|
||||||
|
// generalObservations: trainingSurveys.generalObservations,
|
||||||
|
// paralysisReason: trainingSurveys.paralysisReason,
|
||||||
|
// productList: trainingSurveys.productList,
|
||||||
|
// infrastructureMt2: trainingSurveys.infrastructureMt2,
|
||||||
|
// photo1: trainingSurveys.photo1,
|
||||||
|
// photo2: trainingSurveys.photo2,
|
||||||
|
// photo3: trainingSurveys.photo3,
|
||||||
|
// })
|
||||||
|
// .from(trainingSurveys)
|
||||||
|
// .leftJoin(states, eq(trainingSurveys.state, states.id))
|
||||||
|
// .leftJoin(
|
||||||
|
// municipalities,
|
||||||
|
// eq(trainingSurveys.municipality, municipalities.id),
|
||||||
|
// )
|
||||||
|
// .leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
|
||||||
|
// .execute();
|
||||||
|
|
||||||
|
// let currentRow = 2;
|
||||||
|
|
||||||
|
// for (const record of records) {
|
||||||
|
// const date = new Date(record.visitDate);
|
||||||
|
// const dateStr = date.toLocaleDateString('es-VE');
|
||||||
|
// const timeStr = date.toLocaleTimeString('es-VE');
|
||||||
|
|
||||||
|
// sheet.cell(`A${currentRow}`).value(record.coorFullName);
|
||||||
|
// sheet.cell(`C${currentRow}`).value(dateStr);
|
||||||
|
// sheet.cell(`D${currentRow}`).value(timeStr);
|
||||||
|
// sheet.cell(`E${currentRow}`).value(record.stateName || '');
|
||||||
|
// sheet.cell(`F${currentRow}`).value(record.municipalityName || '');
|
||||||
|
// sheet.cell(`G${currentRow}`).value(record.parishName || '');
|
||||||
|
// sheet.cell(`H${currentRow}`).value(record.communeName);
|
||||||
|
// sheet.cell(`I${currentRow}`).value(record.siturCodeCommune);
|
||||||
|
// sheet.cell(`J${currentRow}`).value(record.communalCouncil);
|
||||||
|
// sheet.cell(`K${currentRow}`).value(record.siturCodeCommunalCouncil);
|
||||||
|
// sheet.cell(`L${currentRow}`).value(record.productiveActivity);
|
||||||
|
// sheet.cell(`M${currentRow}`).value(''); // requerimiento financiero description
|
||||||
|
// sheet.cell(`N${currentRow}`).value(record.ospName);
|
||||||
|
// sheet.cell(`O${currentRow}`).value(record.ospAddress);
|
||||||
|
// sheet.cell(`P${currentRow}`).value(record.ospRif);
|
||||||
|
// sheet.cell(`Q${currentRow}`).value(record.ospType);
|
||||||
|
// sheet.cell(`R${currentRow}`).value(record.currentStatus);
|
||||||
|
// sheet.cell(`S${currentRow}`).value(record.companyConstitutionYear);
|
||||||
|
|
||||||
|
// const products = (record.productList as any[]) || [];
|
||||||
|
// const totalProducers = products.reduce(
|
||||||
|
// (sum, p) =>
|
||||||
|
// sum + (Number(p.menCount) || 0) + (Number(p.womenCount) || 0),
|
||||||
|
// 0,
|
||||||
|
// );
|
||||||
|
// const productsDesc = products.map((p) => p.name).join(', ');
|
||||||
|
|
||||||
|
// sheet.cell(`T${currentRow}`).value(totalProducers);
|
||||||
|
// sheet.cell(`U${currentRow}`).value(productsDesc);
|
||||||
|
// sheet.cell(`V${currentRow}`).value(record.infrastructureMt2);
|
||||||
|
// sheet.cell(`W${currentRow}`).value('');
|
||||||
|
// sheet.cell(`X${currentRow}`).value(record.paralysisReason || '');
|
||||||
|
// sheet.cell(`Y${currentRow}`).value(record.ospResponsibleFullname);
|
||||||
|
// sheet.cell(`Z${currentRow}`).value(record.ospResponsibleCedula);
|
||||||
|
// sheet.cell(`AA${currentRow}`).value(record.ospResponsibleRif);
|
||||||
|
// sheet.cell(`AB${currentRow}`).value(record.ospResponsiblePhone);
|
||||||
|
// sheet.cell(`AC${currentRow}`).value(record.ospResponsibleEmail);
|
||||||
|
// sheet.cell(`AD${currentRow}`).value(record.civilState);
|
||||||
|
// sheet.cell(`AE${currentRow}`).value(record.familyBurden);
|
||||||
|
// sheet.cell(`AF${currentRow}`).value(record.numberOfChildren);
|
||||||
|
// sheet.cell(`AG${currentRow}`).value(record.generalObservations || '');
|
||||||
|
|
||||||
|
// sheet.cell(`AH${currentRow}`).value(record.photo1 || '');
|
||||||
|
// sheet.cell(`AI${currentRow}`).value(record.photo2 || '');
|
||||||
|
// sheet.cell(`AJ${currentRow}`).value(record.photo3 || '');
|
||||||
|
|
||||||
|
// currentRow++;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return await workbook.outputAsync();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async exportTemplate(id: number) {
|
||||||
|
// // Validar que el registro exista
|
||||||
|
// const exist = await this.findOne(id);
|
||||||
|
// if (!exist) throw new NotFoundException(`No se encontro el registro`);
|
||||||
|
|
||||||
|
// // Obtener los datos del registro
|
||||||
|
// const records = await this.drizzle
|
||||||
|
// .select({
|
||||||
|
// // id: trainingSurveys.id,
|
||||||
|
// visitDate: trainingSurveys.visitDate,
|
||||||
|
// ospName: trainingSurveys.ospName,
|
||||||
|
// productiveSector: trainingSurveys.productiveSector,
|
||||||
|
// ospAddress: trainingSurveys.ospAddress,
|
||||||
|
// ospRif: trainingSurveys.ospRif,
|
||||||
|
|
||||||
|
// siturCodeCommune: trainingSurveys.siturCodeCommune,
|
||||||
|
// communeEmail: trainingSurveys.communeEmail,
|
||||||
|
// communeRif: trainingSurveys.communeRif,
|
||||||
|
// communeSpokespersonName: trainingSurveys.communeSpokespersonName,
|
||||||
|
// communeSpokespersonPhone: trainingSurveys.communeSpokespersonPhone,
|
||||||
|
|
||||||
|
// siturCodeCommunalCouncil: trainingSurveys.siturCodeCommunalCouncil,
|
||||||
|
// communalCouncilRif: trainingSurveys.communalCouncilRif,
|
||||||
|
// communalCouncilSpokespersonName:
|
||||||
|
// trainingSurveys.communalCouncilSpokespersonName,
|
||||||
|
// communalCouncilSpokespersonPhone:
|
||||||
|
// trainingSurveys.communalCouncilSpokespersonPhone,
|
||||||
|
|
||||||
|
// ospType: trainingSurveys.ospType,
|
||||||
|
// productiveActivity: trainingSurveys.productiveActivity, // Sector Productivo
|
||||||
|
// companyConstitutionYear: trainingSurveys.companyConstitutionYear,
|
||||||
|
// infrastructureMt2: trainingSurveys.infrastructureMt2,
|
||||||
|
|
||||||
|
// hasTransport: trainingSurveys.hasTransport,
|
||||||
|
// structureType: trainingSurveys.structureType,
|
||||||
|
// isOpenSpace: trainingSurveys.isOpenSpace,
|
||||||
|
|
||||||
|
// ospResponsibleFullname: trainingSurveys.ospResponsibleFullname,
|
||||||
|
// ospResponsibleCedula: trainingSurveys.ospResponsibleCedula,
|
||||||
|
// ospResponsiblePhone: trainingSurveys.ospResponsiblePhone,
|
||||||
|
|
||||||
|
// productList: trainingSurveys.productList,
|
||||||
|
// equipmentList: trainingSurveys.equipmentList,
|
||||||
|
// productionList: trainingSurveys.productionList,
|
||||||
|
|
||||||
|
// // photo1: trainingSurveys.photo1
|
||||||
|
// })
|
||||||
|
// .from(trainingSurveys)
|
||||||
|
// .where(eq(trainingSurveys.id, id));
|
||||||
|
// // .leftJoin(states, eq(trainingSurveys.state, states.id))
|
||||||
|
// // .leftJoin(municipalities,eq(trainingSurveys.municipality, municipalities.id))
|
||||||
|
// // .leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
|
||||||
|
|
||||||
|
// let equipmentList: any[] = Array.isArray(records[0].equipmentList)
|
||||||
|
// ? records[0].equipmentList
|
||||||
|
// : [];
|
||||||
|
// let productList: any[] = Array.isArray(records[0].productList)
|
||||||
|
// ? records[0].productList
|
||||||
|
// : [];
|
||||||
|
// let productionList: any[] = Array.isArray(records[0].productionList)
|
||||||
|
// ? records[0].productionList
|
||||||
|
// : [];
|
||||||
|
|
||||||
|
// console.log('equipmentList', equipmentList);
|
||||||
|
// console.log('productList', productList);
|
||||||
|
// console.log('productionList', productionList);
|
||||||
|
|
||||||
|
// let equipmentListArray: any[] = [];
|
||||||
|
// let productListArray: any[] = [];
|
||||||
|
// let productionListArray: any[] = [];
|
||||||
|
|
||||||
|
// const equipmentListCount = equipmentList.length;
|
||||||
|
// for (let i = 0; i < equipmentListCount; i++) {
|
||||||
|
// equipmentListArray.push([
|
||||||
|
// equipmentList[i].machine,
|
||||||
|
// '',
|
||||||
|
// equipmentList[i].quantity,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const productListCount = productList.length;
|
||||||
|
// for (let i = 0; i < productListCount; i++) {
|
||||||
|
// productListArray.push([
|
||||||
|
// productList[i].productName,
|
||||||
|
// productList[i].dailyCount,
|
||||||
|
// productList[i].weeklyCount,
|
||||||
|
// productList[i].monthlyCount,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const productionListCount = productionList.length;
|
||||||
|
// for (let i = 0; i < productionListCount; i++) {
|
||||||
|
// productionListArray.push([
|
||||||
|
// productionList[i].rawMaterial,
|
||||||
|
// '',
|
||||||
|
// productionList[i].quantity,
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Ruta de la plantilla
|
||||||
|
// const templatePath = path.join(
|
||||||
|
// __dirname,
|
||||||
|
// 'export_template',
|
||||||
|
// 'excel.osp.xlsx',
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Cargar la plantilla
|
||||||
|
// const book = await XlsxPopulate.fromFileAsync(templatePath);
|
||||||
|
|
||||||
|
// const isoString = records[0].visitDate;
|
||||||
|
// const dateObj = new Date(isoString);
|
||||||
|
// const fechaFormateada = dateObj.toLocaleDateString('es-ES');
|
||||||
|
// const horaFormateada = dateObj.toLocaleTimeString('es-ES', {
|
||||||
|
// hour: '2-digit',
|
||||||
|
// minute: '2-digit',
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Llenar los datos
|
||||||
|
// book.sheet(0).cell('A6').value(records[0].productiveSector);
|
||||||
|
// book.sheet(0).cell('D6').value(records[0].ospName);
|
||||||
|
// book.sheet(0).cell('L5').value(fechaFormateada);
|
||||||
|
// book.sheet(0).cell('L6').value(horaFormateada);
|
||||||
|
// book.sheet(0).cell('B10').value(records[0].ospAddress);
|
||||||
|
// book.sheet(0).cell('C11').value(records[0].communeEmail);
|
||||||
|
// book.sheet(0).cell('C12').value(records[0].communeSpokespersonName);
|
||||||
|
// book.sheet(0).cell('G11').value(records[0].communeRif);
|
||||||
|
// book.sheet(0).cell('G12').value(records[0].communeSpokespersonPhone);
|
||||||
|
// book.sheet(0).cell('C13').value(records[0].siturCodeCommune);
|
||||||
|
// book.sheet(0).cell('G13').value(records[0].siturCodeCommunalCouncil);
|
||||||
|
// book.sheet(0).cell('G14').value(records[0].communalCouncilRif);
|
||||||
|
// book.sheet(0).cell('C15').value(records[0].communalCouncilSpokespersonName);
|
||||||
|
// book
|
||||||
|
// .sheet(0)
|
||||||
|
// .cell('G15')
|
||||||
|
// .value(records[0].communalCouncilSpokespersonPhone);
|
||||||
|
// book.sheet(0).cell('C16').value(records[0].ospType);
|
||||||
|
// book.sheet(0).cell('C17').value(records[0].ospName);
|
||||||
|
// book.sheet(0).cell('C18').value(records[0].productiveActivity);
|
||||||
|
// book.sheet(0).cell('C19').value('Proveedores');
|
||||||
|
// book.sheet(0).cell('C20').value(records[0].companyConstitutionYear);
|
||||||
|
// book.sheet(0).cell('C21').value(records[0].infrastructureMt2);
|
||||||
|
// book.sheet(0).cell('G17').value(records[0].ospRif);
|
||||||
|
|
||||||
|
// book
|
||||||
|
// .sheet(0)
|
||||||
|
// .cell(records[0].hasTransport === true ? 'J19' : 'L19')
|
||||||
|
// .value('X');
|
||||||
|
// book
|
||||||
|
// .sheet(0)
|
||||||
|
// .cell(records[0].structureType === 'CASA' ? 'J20' : 'L20')
|
||||||
|
// .value('X');
|
||||||
|
// book
|
||||||
|
// .sheet(0)
|
||||||
|
// .cell(records[0].isOpenSpace === true ? 'J21' : 'L21')
|
||||||
|
// .value('X');
|
||||||
|
|
||||||
|
// book.sheet(0).cell('A24').value(records[0].ospResponsibleFullname);
|
||||||
|
// book.sheet(0).cell('C24').value(records[0].ospResponsibleCedula);
|
||||||
|
// book.sheet(0).cell('E24').value(records[0].ospResponsiblePhone);
|
||||||
|
|
||||||
|
// book.sheet(0).cell('J24').value('N Femenino');
|
||||||
|
// book.sheet(0).cell('L24').value('N Masculino');
|
||||||
|
|
||||||
|
// book
|
||||||
|
// .sheet(0)
|
||||||
|
// .range(`A28:C${equipmentListCount + 28}`)
|
||||||
|
// .value(equipmentListArray);
|
||||||
|
// book
|
||||||
|
// .sheet(0)
|
||||||
|
// .range(`E28:G${productionListCount + 28}`)
|
||||||
|
// .value(productionListArray);
|
||||||
|
// book
|
||||||
|
// .sheet(0)
|
||||||
|
// .range(`I28:L${productListCount + 28}`)
|
||||||
|
// .value(productListArray);
|
||||||
|
|
||||||
|
// return book.outputAsync();
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
AUTH_URL = http://localhost:3000
|
AUTH_URL = http://localhost:3000
|
||||||
AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE=
|
AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE=
|
||||||
API_URL=http://localhost:8000
|
API_URL=http://localhost:8000
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
NODE_ENV='development' #development | production
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import PageContainer from '@/components/layout/page-container';
|
|
||||||
|
|
||||||
const Page = () => {
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
|
||||||
En mantenimiento
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</PageContainer>
|
|
||||||
// <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
|
||||||
// <div className="flex w-full max-w-sm flex-col gap-6">
|
|
||||||
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
19
apps/web/app/dashboard/estadisticas/socioproductiva/page.tsx
Normal file
19
apps/web/app/dashboard/estadisticas/socioproductiva/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { TrainingStatistics } from '@/feactures/training/components/training-statistics';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Estadísticas Socioproductivas - Fondemi',
|
||||||
|
description: 'Análisis y estadísticas de las Organizaciones Socioproductivas',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SocioproductivaStatisticsPage() {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<div className="w-full">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Estadísticas Socioproductivas</h1>
|
||||||
|
<TrainingStatistics />
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
apps/web/app/dashboard/formulario/editar/[id]/page.tsx
Normal file
32
apps/web/app/dashboard/formulario/editar/[id]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
'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 (
|
||||||
|
<PageContainer>
|
||||||
|
<div>Cargando...</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<CreateTrainingForm
|
||||||
|
defaultValues={training}
|
||||||
|
onSuccess={() => router.push('/dashboard/formulario')}
|
||||||
|
onCancel={() => router.back()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
apps/web/app/dashboard/formulario/nuevo/page.tsx
Normal file
18
apps/web/app/dashboard/formulario/nuevo/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
'use client';
|
||||||
|
import { CreateTrainingForm } from '@/feactures/training/components/form';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function NewTrainingPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <PageContainer scrollable>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<CreateTrainingForm
|
||||||
|
onSuccess={() => router.push('/dashboard/formulario')}
|
||||||
|
onCancel={() => router.back()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
// </PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
apps/web/app/dashboard/formulario/page.tsx
Normal file
39
apps/web/app/dashboard/formulario/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// import PageContainer from '@/components/layout/page-container';
|
||||||
|
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';
|
||||||
|
|
||||||
|
import { env } from '@/lib/env';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Registro de OSP',
|
||||||
|
};
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
searchParams: Promise<SearchParams>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
const {
|
||||||
|
page,
|
||||||
|
q: searchQuery,
|
||||||
|
limit,
|
||||||
|
} = searchParamsCache.parse(await searchParams);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <PageContainer>
|
||||||
|
<div className="flex flex-1 flex-col space-y-6 p-6">
|
||||||
|
<TrainingHeader />
|
||||||
|
<TrainingTableAction />
|
||||||
|
<TrainingList
|
||||||
|
initialPage={page}
|
||||||
|
initialSearch={searchQuery}
|
||||||
|
initialLimit={limit || 10}
|
||||||
|
apiUrl={env.API_URL}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
// </PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ export const metadata = {
|
|||||||
openGraph: {
|
openGraph: {
|
||||||
type: 'website',
|
type: 'website',
|
||||||
title: 'fondemi',
|
title: 'fondemi',
|
||||||
description: 'Sistema integral para cajas de ahorro',
|
description: 'Sistema integral para fondemi',
|
||||||
url: 'https://turbo-npn.onrender.com',
|
url: 'https://turbo-npn.onrender.com',
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { NavMain as GeneralMain, NavMain as AdministrationMain, NavMain as StatisticsMain, } from '@/components/nav-main';
|
import { NavMain as GeneralMain, NavMain as AdministrationMain, NavMain as StatisticsMain, } from '@/components/nav-main';
|
||||||
import { GeneralItems, AdministrationItems, StatisticsItems } from '@/constants/data';
|
import { GeneralItems, AdministrationItems, StatisticsItems } from '@/constants/routes';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -15,7 +15,7 @@ import { useSession } from 'next-auth/react';
|
|||||||
|
|
||||||
|
|
||||||
export const company = {
|
export const company = {
|
||||||
name: 'Sistema para Productores',
|
name: 'Sistema de Productores',
|
||||||
logo: GalleryVerticalEnd,
|
logo: GalleryVerticalEnd,
|
||||||
plan: 'FONDEMI',
|
plan: 'FONDEMI',
|
||||||
};
|
};
|
||||||
@@ -24,9 +24,9 @@ export const company = {
|
|||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol :'';
|
const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol : '';
|
||||||
// console.log(AdministrationItems[0]?.role);
|
// console.log(AdministrationItems[0]?.role);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
@@ -42,15 +42,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<GeneralMain titleGroup={'General'} items={GeneralItems} role={userRole}/>
|
{AdministrationItems[0]?.role?.includes(userRole) &&
|
||||||
|
<AdministrationMain titleGroup={'Administracion'} items={AdministrationItems} role={userRole} />
|
||||||
|
}
|
||||||
{StatisticsItems[0]?.role?.includes(userRole) &&
|
{StatisticsItems[0]?.role?.includes(userRole) &&
|
||||||
<StatisticsMain titleGroup={'Estadisticas'} items={StatisticsItems} role={userRole}/>
|
<StatisticsMain titleGroup={'Estadisticas'} items={StatisticsItems} role={userRole} />
|
||||||
}
|
|
||||||
|
|
||||||
{AdministrationItems[0]?.role?.includes(userRole) &&
|
|
||||||
<AdministrationMain titleGroup={'Administracion'} items={AdministrationItems} role={userRole}/>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<GeneralMain titleGroup={'General'} items={GeneralItems} role={userRole} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* <NavProjects projects={data.projects} /> */}
|
{/* <NavProjects projects={data.projects} /> */}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
|
|||||||
195
apps/web/constants/countries.ts
Normal file
195
apps/web/constants/countries.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
export const COUNTRY_OPTIONS = [
|
||||||
|
'Afganistán',
|
||||||
|
'Albania',
|
||||||
|
'Alemania',
|
||||||
|
'Andorra',
|
||||||
|
'Angola',
|
||||||
|
'Antigua y Barbuda',
|
||||||
|
'Arabia Saudita',
|
||||||
|
'Argelia',
|
||||||
|
'Argentina',
|
||||||
|
'Armenia',
|
||||||
|
'Australia',
|
||||||
|
'Austria',
|
||||||
|
'Azerbaiyán',
|
||||||
|
'Bahamas',
|
||||||
|
'Bangladés',
|
||||||
|
'Barbados',
|
||||||
|
'Baréin',
|
||||||
|
'Bélgica',
|
||||||
|
'Belice',
|
||||||
|
'Benín',
|
||||||
|
'Bielorrusia',
|
||||||
|
'Birmania',
|
||||||
|
'Bolivia',
|
||||||
|
'Bosnia y Herzegovina',
|
||||||
|
'Botsuana',
|
||||||
|
'Brasil',
|
||||||
|
'Brunéi',
|
||||||
|
'Bulgaria',
|
||||||
|
'Burkina Faso',
|
||||||
|
'Burundi',
|
||||||
|
'Bután',
|
||||||
|
'Cabo Verde',
|
||||||
|
'Camboya',
|
||||||
|
'Camerún',
|
||||||
|
'Canadá',
|
||||||
|
'Catar',
|
||||||
|
'Chad',
|
||||||
|
'Chile',
|
||||||
|
'China',
|
||||||
|
'Chipre',
|
||||||
|
'Ciudad del Vaticano',
|
||||||
|
'Colombia',
|
||||||
|
'Comoras',
|
||||||
|
'Corea del Norte',
|
||||||
|
'Corea del Sur',
|
||||||
|
'Costa de Marfil',
|
||||||
|
'Costa Rica',
|
||||||
|
'Croacia',
|
||||||
|
'Cuba',
|
||||||
|
'Dinamarca',
|
||||||
|
'Dominica',
|
||||||
|
'Ecuador',
|
||||||
|
'Egipto',
|
||||||
|
'El Salvador',
|
||||||
|
'Emiratos Árabes Unidos',
|
||||||
|
'Eritrea',
|
||||||
|
'Eslovaquia',
|
||||||
|
'Eslovenia',
|
||||||
|
'España',
|
||||||
|
'Estados Unidos',
|
||||||
|
'Estonia',
|
||||||
|
'Etiopía',
|
||||||
|
'Filipinas',
|
||||||
|
'Finlandia',
|
||||||
|
'Fiyi',
|
||||||
|
'Francia',
|
||||||
|
'Gabón',
|
||||||
|
'Gambia',
|
||||||
|
'Georgia',
|
||||||
|
'Ghana',
|
||||||
|
'Granada',
|
||||||
|
'Grecia',
|
||||||
|
'Guatemala',
|
||||||
|
'Guyana',
|
||||||
|
'Guinea',
|
||||||
|
'Guinea Ecuatorial',
|
||||||
|
'Guinea-Bisáu',
|
||||||
|
'Haití',
|
||||||
|
'Honduras',
|
||||||
|
'Hungría',
|
||||||
|
'India',
|
||||||
|
'Indonesia',
|
||||||
|
'Irak',
|
||||||
|
'Irán',
|
||||||
|
'Irlanda',
|
||||||
|
'Islandia',
|
||||||
|
'Islas Marshall',
|
||||||
|
'Islas Salomón',
|
||||||
|
'Israel',
|
||||||
|
'Italia',
|
||||||
|
'Jamaica',
|
||||||
|
'Japón',
|
||||||
|
'Jordania',
|
||||||
|
'Kazajistán',
|
||||||
|
'Kenia',
|
||||||
|
'Kirguistán',
|
||||||
|
'Kiribati',
|
||||||
|
'Kuwait',
|
||||||
|
'Laos',
|
||||||
|
'Lesoto',
|
||||||
|
'Letonia',
|
||||||
|
'Líbano',
|
||||||
|
'Liberia',
|
||||||
|
'Libia',
|
||||||
|
'Liechtenstein',
|
||||||
|
'Lituania',
|
||||||
|
'Luxemburgo',
|
||||||
|
'Madagascar',
|
||||||
|
'Malasia',
|
||||||
|
'Malaui',
|
||||||
|
'Maldivas',
|
||||||
|
'Malí',
|
||||||
|
'Malta',
|
||||||
|
'Marruecos',
|
||||||
|
'Mauricio',
|
||||||
|
'Mauritania',
|
||||||
|
'México',
|
||||||
|
'Micronesia',
|
||||||
|
'Moldavia',
|
||||||
|
'Mónaco',
|
||||||
|
'Mongolia',
|
||||||
|
'Montenegro',
|
||||||
|
'Mozambique',
|
||||||
|
'Namibia',
|
||||||
|
'Nauru',
|
||||||
|
'Nepal',
|
||||||
|
'Nicaragua',
|
||||||
|
'Níger',
|
||||||
|
'Nigeria',
|
||||||
|
'Noruega',
|
||||||
|
'Nueva Zelanda',
|
||||||
|
'Omán',
|
||||||
|
'Países Bajos',
|
||||||
|
'Pakistán',
|
||||||
|
'Palaos',
|
||||||
|
'Panamá',
|
||||||
|
'Papúa Nueva Guinea',
|
||||||
|
'Paraguay',
|
||||||
|
'Perú',
|
||||||
|
'Polonia',
|
||||||
|
'Portugal',
|
||||||
|
'Reino Unido',
|
||||||
|
'República Centroafricana',
|
||||||
|
'República Checa',
|
||||||
|
'República de Macedonia',
|
||||||
|
'República del Congo',
|
||||||
|
'República Democrática del Congo',
|
||||||
|
'República Dominicana',
|
||||||
|
'República Sudafricana',
|
||||||
|
'Ruanda',
|
||||||
|
'Rumanía',
|
||||||
|
'Rusia',
|
||||||
|
'Samoa',
|
||||||
|
'San Cristóbal y Nieves',
|
||||||
|
'San Marino',
|
||||||
|
'San Vicente y las Granadinas',
|
||||||
|
'Santa Lucía',
|
||||||
|
'Santo Tomé y Príncipe',
|
||||||
|
'Senegal',
|
||||||
|
'Serbia',
|
||||||
|
'Seychelles',
|
||||||
|
'Sierra Leona',
|
||||||
|
'Singapur',
|
||||||
|
'Siria',
|
||||||
|
'Somalia',
|
||||||
|
'Sri Lanka',
|
||||||
|
'Suazilandia',
|
||||||
|
'Sudán',
|
||||||
|
'Sudán del Sur',
|
||||||
|
'Suecia',
|
||||||
|
'Suiza',
|
||||||
|
'Surinam',
|
||||||
|
'Tailandia',
|
||||||
|
'Tanzania',
|
||||||
|
'Tayikistán',
|
||||||
|
'Timor Oriental',
|
||||||
|
'Togo',
|
||||||
|
'Tonga',
|
||||||
|
'Trinidad y Tobago',
|
||||||
|
'Túnez',
|
||||||
|
'Turkmenistán',
|
||||||
|
'Turquía',
|
||||||
|
'Tuvalu',
|
||||||
|
'Ucrania',
|
||||||
|
'Uganda',
|
||||||
|
'Uruguay',
|
||||||
|
'Uzbekistán',
|
||||||
|
'Vanuatu',
|
||||||
|
'Vietnam',
|
||||||
|
'Yemen',
|
||||||
|
'Yibuti',
|
||||||
|
'Zambia',
|
||||||
|
'Zimbabue',
|
||||||
|
];
|
||||||
@@ -10,24 +10,23 @@ export const GeneralItems: NavItem[] = [
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
items: [], // No child items
|
items: [], // No child items
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'ProduTienda',
|
// title: 'ProduTienda',
|
||||||
url: '/dashboard/productos/',
|
// url: '/dashboard/productos/',
|
||||||
icon: 'blocks',
|
// icon: 'blocks',
|
||||||
shortcut: ['p', 'p'],
|
// shortcut: ['p', 'p'],
|
||||||
isActive: false,
|
// isActive: false,
|
||||||
items: [], // No child items
|
// items: [], // No child items
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export const AdministrationItems: NavItem[] = [
|
export const AdministrationItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Administracion',
|
title: 'Administracion',
|
||||||
url: '#', // Placeholder as there is no direct link for the parent
|
url: '#', // Placeholder as there is no direct link for the parent
|
||||||
icon: 'settings2',
|
icon: 'settings2',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
role:['admin','superadmin','manager','user'], // sumatoria de los roles que si tienen acceso
|
role: ['admin', 'superadmin', 'manager', 'autoridad', 'coordinators'], // sumatoria de los roles que si tienen acceso
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@@ -35,14 +34,21 @@ export const AdministrationItems: NavItem[] = [
|
|||||||
url: '/dashboard/administracion/usuario',
|
url: '/dashboard/administracion/usuario',
|
||||||
icon: 'userPen',
|
icon: 'userPen',
|
||||||
shortcut: ['m', 'm'],
|
shortcut: ['m', 'm'],
|
||||||
role:['admin','superadmin'],
|
role: ['admin', 'superadmin'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Encuestas',
|
title: 'Encuestas',
|
||||||
shortcut: ['l', 'l'],
|
shortcut: ['l', 'l'],
|
||||||
url: '/dashboard/administracion/encuestas',
|
url: '/dashboard/administracion/encuestas',
|
||||||
icon: 'login',
|
icon: 'login',
|
||||||
role:['admin','superadmin','manager','user'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Registro OSP',
|
||||||
|
shortcut: ['p', 'p'],
|
||||||
|
url: '/dashboard/formulario/',
|
||||||
|
icon: 'notepadText',
|
||||||
|
role: ['admin', 'superadmin', 'manager', 'autoridad', 'coordinators'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -54,7 +60,7 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
url: '#', // Placeholder as there is no direct link for the parent
|
url: '#', // Placeholder as there is no direct link for the parent
|
||||||
icon: 'chartColumn',
|
icon: 'chartColumn',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
role:['admin','superadmin','autoridad'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
// {
|
// {
|
||||||
@@ -69,13 +75,15 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
shortcut: ['l', 'l'],
|
shortcut: ['l', 'l'],
|
||||||
url: '/dashboard/estadisticas/encuestas',
|
url: '/dashboard/estadisticas/encuestas',
|
||||||
icon: 'notepadText',
|
icon: 'notepadText',
|
||||||
role:['admin','superadmin','autoridad'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Datos OSP',
|
||||||
|
shortcut: ['s', 's'],
|
||||||
|
url: '/dashboard/estadisticas/socioproductiva',
|
||||||
|
icon: 'blocks',
|
||||||
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import { safeFetchApi } from '@/lib';
|
import { safeFetchApi } from '@/lib';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import { loginResponseSchema, UserFormValue } from '../schemas/login';
|
import { loginResponseSchema, UserFormValue } from '../schemas/login';
|
||||||
|
|
||||||
type LoginActionSuccess = {
|
type LoginActionSuccess = {
|
||||||
@@ -17,18 +18,18 @@ type LoginActionSuccess = {
|
|||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
refresh_expire_in: number;
|
refresh_expire_in: number;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type LoginActionError = {
|
type LoginActionError = {
|
||||||
type: 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR'; // **Asegúrate de que el tipo de `type` sea este aquí**
|
type: 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR'; // **Asegúrate de que el tipo de `type` sea este aquí**
|
||||||
message: string;
|
message: string;
|
||||||
details?: any;
|
details?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Si SignInAction también puede devolver null, asegúralo en su tipo de retorno
|
// Si SignInAction también puede devolver null, asegúralo en su tipo de retorno
|
||||||
type LoginActionResult = LoginActionSuccess | LoginActionError | null;
|
type LoginActionResult = LoginActionSuccess | LoginActionError | null;
|
||||||
|
|
||||||
export const SignInAction = async (payload: UserFormValue): Promise<LoginActionResult> => {
|
export const SignInAction = async (payload: UserFormValue) => {
|
||||||
const [error, data] = await safeFetchApi(
|
const [error, data] = await safeFetchApi(
|
||||||
loginResponseSchema,
|
loginResponseSchema,
|
||||||
'/auth/sign-in',
|
'/auth/sign-in',
|
||||||
@@ -36,12 +37,22 @@ export const SignInAction = async (payload: UserFormValue): Promise<LoginActionR
|
|||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
return {
|
return error;
|
||||||
type: error.type as 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR',
|
|
||||||
message: error.message,
|
|
||||||
details: error.details
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
|
// 2. GUARDAR REFRESH TOKEN EN COOKIE (La clave del cambio)
|
||||||
|
|
||||||
|
(await cookies()).set(
|
||||||
|
'refresh_token',
|
||||||
|
String(data?.tokens?.refresh_token),
|
||||||
|
{
|
||||||
|
httpOnly: true, // JavaScript no puede leerla
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
maxAge: 7 * 24 * 60 * 60, // Ej: 7 días (debe coincidir con tu backend)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
47
apps/web/feactures/auth/actions/logout-action.ts
Normal file
47
apps/web/feactures/auth/actions/logout-action.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
// import { safeFetchApi } from '@/lib';
|
||||||
|
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { logoutResponseSchema } from '../schemas/logout';
|
||||||
|
|
||||||
|
export const logoutAction = async (user_id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await refreshApi.post('/auth/sign-out', { user_id });
|
||||||
|
|
||||||
|
const parsed = logoutResponseSchema.safeParse(response.data);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error('Error de validación en la respuesta de refresh token:', {
|
||||||
|
errors: parsed.error.errors,
|
||||||
|
receivedData: response.data,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.data;
|
||||||
|
} catch (error: any) { // Captura el error para acceso a error.response
|
||||||
|
console.error('Error al cerrar sesion:', error.response?.data || error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// const payload = { user_id };
|
||||||
|
// const [error, data] = await safeFetchApi(
|
||||||
|
// logoutResponseSchema,
|
||||||
|
// '/auth/sign-out',
|
||||||
|
// 'POST',
|
||||||
|
// payload,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (error) {
|
||||||
|
// console.error('Error:', error);
|
||||||
|
// // Devuelve un objeto con la propiedad 'type' para que el callback de NextAuth lo reconozca como un error
|
||||||
|
// return {
|
||||||
|
// type: 'API_ERROR',
|
||||||
|
// message: error.message,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
(await cookies()).delete('refresh_token');
|
||||||
|
};
|
||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
|
|
||||||
export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => {
|
export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => {
|
||||||
try {
|
try {
|
||||||
const response = await refreshApi.patch('/auth/refresh', {refresh_token: refreshToken.token});
|
const response = await refreshApi.patch('/auth/refresh', refreshToken);
|
||||||
|
|
||||||
const parsed = RefreshTokenResponseSchema.safeParse(response.data);
|
const parsed = RefreshTokenResponseSchema.safeParse(response.data);
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
console.error('Error de validación en la respuesta de refresh token:', {
|
console.error('Error de validación en la respuesta de refresh token:', {
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export function LoginForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("", className)} {...props}>
|
<div className={cn("", className)} {...props}>
|
||||||
<Card className="overflow-hidden">
|
<Card className="">
|
||||||
<CardContent className="grid p-0 md:grid-cols-2">
|
<CardContent className="flex flex-col-reverse md:flex-row p-0">
|
||||||
<UserAuthForm />
|
<UserAuthForm />
|
||||||
<div className="relative hidden bg-muted md:block">
|
<div className="md:bg-muted">
|
||||||
<img
|
<img
|
||||||
src="logo.png"
|
src="logo.png"
|
||||||
alt="Image"
|
alt="Imagen del Logo"
|
||||||
className="absolute inset-0 p-10 h-full w-full"
|
className="pt-3 md:p-3 h-full w-1/3 md:w-full m-auto "
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ export function LoginForm({
|
|||||||
return (
|
return (
|
||||||
<div className={cn("", className)} {...props}>
|
<div className={cn("", className)} {...props}>
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
<CardContent className="flex p-0">
|
<CardContent className="flex flex-col-reverse md:flex-row p-0">
|
||||||
<UserAuthForm />
|
<UserAuthForm />
|
||||||
<div className="hidden bg-muted md:block m-auto">
|
<div className="md:m-auto">
|
||||||
<img
|
<img
|
||||||
src="logo.png"
|
src="logo.png"
|
||||||
alt="Image"
|
alt="Image"
|
||||||
className="inset-0 p-5"
|
className="pt-3 md:p-5 w-1/3 md:w-full m-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ export default function UserAuthForm() {
|
|||||||
<>
|
<>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
|
||||||
<form className="p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
<form className="flex-none p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold">Sistema para productores</h1>
|
<h1 className="text-2xl font-bold">Sistema Gestión de Productores</h1>
|
||||||
<p className="text-balance text-muted-foreground">
|
<p className="text-balance text-muted-foreground hidden md:block">
|
||||||
Ingresa tus datos
|
Ingresa tus datos
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function UserAuthForm() {
|
|||||||
<form className="flex-none p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
<form className="flex-none p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="items-center text-center">
|
<div className="items-center text-center">
|
||||||
<h1 className="text-2xl font-bold">Sistema para productores</h1>
|
<h1 className="text-2xl font-bold">Sistema Gestión de Productores</h1>
|
||||||
<p className="text-balance text-muted-foreground">
|
<p className="text-balance text-muted-foreground">
|
||||||
Ingresa tus datos
|
Ingresa tus datos
|
||||||
</p>
|
</p>
|
||||||
@@ -303,7 +303,7 @@ export default function UserAuthForm() {
|
|||||||
<FormMessage className="text-red-500">{error}</FormMessage>
|
<FormMessage className="text-red-500">{error}</FormMessage>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<Button type="submit" className="w-full">
|
<Button type="submit" className="w-full">
|
||||||
Registrarce
|
Registrarse
|
||||||
</Button>
|
</Button>
|
||||||
<div className="text-center text-sm">
|
<div className="text-center text-sm">
|
||||||
¿Ya tienes una cuenta?{" "}
|
¿Ya tienes una cuenta?{" "}
|
||||||
|
|||||||
5
apps/web/feactures/auth/schemas/logout.ts
Normal file
5
apps/web/feactures/auth/schemas/logout.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
export const logoutResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
});
|
||||||
@@ -1,14 +1,22 @@
|
|||||||
|
// refreshtoken
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { tokensSchema } from './login';
|
import { tokensSchema } from './login';
|
||||||
|
|
||||||
// Esquema para el refresh token
|
// Esquema para el refresh token
|
||||||
export const refreshTokenSchema = z.object({
|
export const refreshTokenSchema = z.object({
|
||||||
token: z.string(),
|
refreshToken: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RefreshTokenValue = z.infer<typeof refreshTokenSchema>;
|
export type RefreshTokenValue = z.infer<typeof refreshTokenSchema>;
|
||||||
|
|
||||||
// Esquema final para la respuesta del backend
|
// Esquema final para la respuesta del backend
|
||||||
export const RefreshTokenResponseSchema = z.object({
|
// export const RefreshTokenResponseSchema = z.object({
|
||||||
tokens: tokensSchema,
|
// // tokens: tokensSchema,
|
||||||
});
|
// access_token: z.string(),
|
||||||
|
// access_expire_in: z.number(),
|
||||||
|
// refresh_token: z.string(),
|
||||||
|
// refresh_expire_in: z.number()
|
||||||
|
// });
|
||||||
|
|
||||||
|
export const RefreshTokenResponseSchema = tokensSchema
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { DataTable } from '@repo/shadcn/table/data-table';
|
import { DataTable } from '@repo/shadcn/table/data-table';
|
||||||
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
||||||
import { columns } from './product-tables/columns';
|
|
||||||
import { useProductQuery } from '../../hooks/use-query-products';
|
import { useProductQuery } from '../../hooks/use-query-products';
|
||||||
|
import { columns } from './product-tables/columns';
|
||||||
|
|
||||||
interface dataListProps {
|
interface dataListProps {
|
||||||
initialPage: number;
|
initialPage: number;
|
||||||
initialSearch?: string | null;
|
initialSearch?: string | null;
|
||||||
initialLimit: number;
|
initialLimit: number;
|
||||||
|
initialType?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UsersAdminList({
|
export default function UsersAdminList({
|
||||||
@@ -19,9 +20,9 @@ export default function UsersAdminList({
|
|||||||
page: initialPage,
|
page: initialPage,
|
||||||
limit: initialLimit,
|
limit: initialLimit,
|
||||||
...(initialSearch && { search: initialSearch }),
|
...(initialSearch && { search: initialSearch }),
|
||||||
}
|
};
|
||||||
|
|
||||||
const {data, isLoading} = useProductQuery(filters)
|
const { data, isLoading } = useProductQuery(filters);
|
||||||
|
|
||||||
// console.log(data?.data);
|
// console.log(data?.data);
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ export const columns: ColumnDef<InventoryTable>[] = [
|
|||||||
accessorKey: 'urlImg',
|
accessorKey: 'urlImg',
|
||||||
header: 'img',
|
header: 'img',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<img src={`/uploads/inventory/${row.original.userId}/${row.original.id}/${row.original.urlImg}`} alt="" width={64} height={64} className="rounded"/>
|
<img src={`/uploads/inventory/${row.original.userId}/${row.original.id}/${row.original.urlImg}`} alt="" width={64} height={64} className="rounded" />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@ export const columns: ColumnDef<InventoryTable>[] = [
|
|||||||
{
|
{
|
||||||
accessorKey: 'price',
|
accessorKey: 'price',
|
||||||
header: 'Precio',
|
header: 'Precio',
|
||||||
cell: ({ row }) => `${row.original.price}$`
|
cell: ({ row }) => `${row.original.price} Bs.`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'stock',
|
accessorKey: 'stock',
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { useUpdateProduct } from "@/feactures/inventory/hooks/use-mutation";
|
import { useUpdateProduct } from "@/feactures/inventory/hooks/use-mutation";
|
||||||
import { updateInventory, EditInventory, InventoryTable } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad
|
import { updateInventory, EditInventory, InventoryTable } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad
|
||||||
import { Textarea } from '@repo/shadcn/components/ui/textarea';
|
import { Textarea } from '@repo/shadcn/components/ui/textarea';
|
||||||
import {STATUS} from '@/constants/status'
|
import { STATUS } from '@/constants/status'
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {sizeFormate} from "@/feactures/inventory/utils/sizeFormate"
|
import { sizeFormate } from "@/feactures/inventory/utils/sizeFormate"
|
||||||
// import { z } from 'zod'; // Asegúrate de importar Zod
|
// import { z } from 'zod'; // Asegúrate de importar Zod
|
||||||
|
|
||||||
// --- MODIFICACIÓN CLAVE ---
|
// --- MODIFICACIÓN CLAVE ---
|
||||||
@@ -57,17 +57,17 @@ export function UpdateForm({
|
|||||||
isError,
|
isError,
|
||||||
} = useUpdateProduct();
|
} = useUpdateProduct();
|
||||||
|
|
||||||
const [sizeFile, setSizeFile] = useState('0 bytes');
|
const [sizeFile, setSizeFile] = useState('0 bytes');
|
||||||
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
|
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
previewUrls.forEach(url => URL.revokeObjectURL(url));
|
previewUrls.forEach(url => URL.revokeObjectURL(url));
|
||||||
};
|
};
|
||||||
}, [previewUrls]);
|
}, [previewUrls]);
|
||||||
|
|
||||||
const defaultformValues: EditInventory = { // Usamos el nuevo tipo aquí
|
const defaultformValues: EditInventory = { // Usamos el nuevo tipo aquí
|
||||||
id: defaultValues?.id,
|
id: defaultValues?.id,
|
||||||
title: defaultValues?.title || '',
|
title: defaultValues?.title || '',
|
||||||
description: defaultValues?.description || '',
|
description: defaultValues?.description || '',
|
||||||
price: defaultValues?.price || '',
|
price: defaultValues?.price || '',
|
||||||
@@ -154,7 +154,7 @@ export function UpdateForm({
|
|||||||
<FormItem >
|
<FormItem >
|
||||||
<FormLabel>Precio</FormLabel>
|
<FormLabel>Precio</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input type="number" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -182,7 +182,7 @@ export function UpdateForm({
|
|||||||
<FormItem className='col-span-2'>
|
<FormItem className='col-span-2'>
|
||||||
<FormLabel>Descripción</FormLabel>
|
<FormLabel>Descripción</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea {...field} className="resize-none"/>
|
<Textarea {...field} className="resize-none" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -196,7 +196,7 @@ export function UpdateForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cantidad/Stock</FormLabel>
|
<FormLabel>Cantidad/Stock</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="number" onChange={(e) => field.onChange(Number(e.target.value))}/>
|
<Input {...field} type="number" onChange={(e) => field.onChange(Number(e.target.value))} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -248,8 +248,8 @@ export function UpdateForm({
|
|||||||
const newPreviewUrls: string[] = [];
|
const newPreviewUrls: string[] = [];
|
||||||
|
|
||||||
files.forEach(element => {
|
files.forEach(element => {
|
||||||
size += element.size;
|
size += element.size;
|
||||||
newPreviewUrls.push(URL.createObjectURL(element));
|
newPreviewUrls.push(URL.createObjectURL(element));
|
||||||
});
|
});
|
||||||
|
|
||||||
const tamañoFormateado = sizeFormate(size);
|
const tamañoFormateado = sizeFormate(size);
|
||||||
@@ -257,18 +257,18 @@ export function UpdateForm({
|
|||||||
setPreviewUrls(newPreviewUrls);
|
setPreviewUrls(newPreviewUrls);
|
||||||
onChange(e.target.files);
|
onChange(e.target.files);
|
||||||
} else {
|
} else {
|
||||||
setPreviewUrls([]);
|
setPreviewUrls([]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
{previewUrls.length > 0 && (
|
{previewUrls.length > 0 && (
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
{previewUrls.map((url, index) => (
|
{previewUrls.map((url, index) => (
|
||||||
<img key={index} src={url} alt={`Preview ${index}`} className="w-24 h-24 object-cover rounded-md" />
|
<img key={index} src={url} alt={`Preview ${index}`} className="w-24 h-24 object-cover rounded-md" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function ProductCard({ product, onClick }: cardProps) {
|
|||||||
{product.status === 'AGOTADO' ? (
|
{product.status === 'AGOTADO' ? (
|
||||||
<p className="font-semibold text-lg text-red-900">AGOTADO</p>
|
<p className="font-semibold text-lg text-red-900">AGOTADO</p>
|
||||||
) : ('')}
|
) : ('')}
|
||||||
<p className="font-semibold text-lg">$ {product.price}</p>
|
<p className="font-semibold text-lg">{product.price} Bs.</p>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { allProducts } from "../../schemas/inventory";
|
import { allProducts } from "../../schemas/inventory";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@repo/shadcn/card';
|
} from '@repo/shadcn/card';
|
||||||
|
|
||||||
export function ProductList({product}: {product: allProducts}) {
|
export function ProductList({ product }: { product: allProducts }) {
|
||||||
const [selectedImg, setSelectedImg] = useState(`/uploads/inventory/${product.userId}/${product.id}/${product.urlImg}`)
|
const [selectedImg, setSelectedImg] = useState(`/uploads/inventory/${product.userId}/${product.id}/${product.urlImg}`)
|
||||||
console.log(product);
|
console.log(product);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <PageContainer>
|
// <PageContainer>
|
||||||
<main className='px-4 lg:px-6 flex flex-col md:flex-row gap-3 lg:gap-4 md:relative'>
|
<main className='px-4 lg:px-6 flex flex-col md:flex-row gap-3 lg:gap-4 md:relative'>
|
||||||
<div className='w-full flex justify-between flex-col'>
|
<div className='w-full flex justify-between flex-col'>
|
||||||
@@ -31,21 +31,21 @@ return (
|
|||||||
</span>
|
</span>
|
||||||
</span> */}
|
</span> */}
|
||||||
{product.gallery?.map((img, index) => (
|
{product.gallery?.map((img, index) => (
|
||||||
<img
|
<img
|
||||||
key={index}
|
key={index}
|
||||||
className="cursor-pointer border-2 object-cover w-[64px] h-[64px] md:w-[96px] md:h-[96px] aspect-square rounded-2xl"
|
className="cursor-pointer border-2 object-cover w-[64px] h-[64px] md:w-[96px] md:h-[96px] aspect-square rounded-2xl"
|
||||||
src={`/uploads/inventory/${product.userId}/${product.id}/${img}`}
|
src={`/uploads/inventory/${product.userId}/${product.id}/${img}`}
|
||||||
alt=""
|
alt=""
|
||||||
onClick={() => setSelectedImg(`/uploads/inventory/${product.userId}/${product.id}/${img}`)}
|
onClick={() => setSelectedImg(`/uploads/inventory/${product.userId}/${product.id}/${img}`)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/* <div className="sticky right-0 flex items-center">
|
{/* <div className="sticky right-0 flex items-center">
|
||||||
<span className="text-xl p-3 cursor-pointer bg-neutral-800/50 rounded-full text-white">
|
<span className="text-xl p-3 cursor-pointer bg-neutral-800/50 rounded-full text-white">
|
||||||
{">"}
|
{">"}
|
||||||
</span>
|
</span>
|
||||||
</div> */}
|
</div> */}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Card className="flex flex-col md:w-[400px] lg:w-[500px] min-h-[400px] md:h-[85vh] md:overflow-auto md:sticky top-0 right-0">
|
<Card className="flex flex-col md:w-[400px] lg:w-[500px] min-h-[400px] md:h-[85vh] md:overflow-auto md:sticky top-0 right-0">
|
||||||
@@ -53,7 +53,7 @@ return (
|
|||||||
<CardTitle className="font-bold text-2xl text-primary">
|
<CardTitle className="font-bold text-2xl text-primary">
|
||||||
{product.title.charAt(0).toUpperCase() + product.title.slice(1)}
|
{product.title.charAt(0).toUpperCase() + product.title.slice(1)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className='font-semibold'>{product.price}$
|
<p className='font-semibold'>{product.price} Bs.
|
||||||
{product.status === 'AGOTADO' ? (
|
{product.status === 'AGOTADO' ? (
|
||||||
<span className="font-semibold text-lg text-red-900"> AGOTADO</span>
|
<span className="font-semibold text-lg text-red-900"> AGOTADO</span>
|
||||||
) : ('')}
|
) : ('')}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { Button } from '@repo/shadcn/button';
|
import { Button } from '@repo/shadcn/button';
|
||||||
import { Heading } from '@repo/shadcn/heading';
|
import { Heading } from '@repo/shadcn/heading';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export function SurveysHeader() {
|
export function SurveysHeader() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const role = session?.user.role[0]?.rol;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
@@ -14,11 +16,18 @@ export function SurveysHeader() {
|
|||||||
title="Administración de Encuestas"
|
title="Administración de Encuestas"
|
||||||
description="Gestiona las encuestas disponibles en la plataforma"
|
description="Gestiona las encuestas disponibles en la plataforma"
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => router.push(`/dashboard/administracion/encuestas/crear`)} size="sm">
|
{['superadmin', 'admin'].includes(role ?? '') && (
|
||||||
<Plus className="mr-2 h-4 w-4" /> Agregar Encuesta
|
<Button
|
||||||
</Button>
|
onClick={() =>
|
||||||
|
router.push(`/dashboard/administracion/encuestas/crear`)
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Agregar Encuesta</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { AlertModal } from '@/components/modal/alert-modal';
|
import { AlertModal } from '@/components/modal/alert-modal';
|
||||||
|
import { useDeleteSurvey } from '@/feactures/surveys/hooks/use-mutation-surveys';
|
||||||
|
import { SurveyTable } from '@/feactures/surveys/schemas/survey';
|
||||||
import { Button } from '@repo/shadcn/button';
|
import { Button } from '@repo/shadcn/button';
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@repo/shadcn/tooltip';
|
} from '@repo/shadcn/tooltip';
|
||||||
import { Edit, Trash } from 'lucide-react';
|
import { Edit, Trash } from 'lucide-react';
|
||||||
import { SurveyTable } from '@/feactures/surveys/schemas/survey';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useDeleteSurvey } from '@/feactures/surveys/hooks/use-mutation-surveys';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
interface CellActionProps {
|
interface CellActionProps {
|
||||||
data: SurveyTable;
|
data: SurveyTable;
|
||||||
@@ -23,6 +23,7 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { mutate: deleteSurvey } = useDeleteSurvey();
|
const { mutate: deleteSurvey } = useDeleteSurvey();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -36,6 +37,8 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const role = session?.user.role[0]?.rol;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AlertModal
|
<AlertModal
|
||||||
@@ -47,41 +50,48 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
|||||||
description="Esta acción no se puede deshacer."
|
description="Esta acción no se puede deshacer."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<TooltipProvider>
|
{['superadmin', 'admin'].includes(role ?? '') && (
|
||||||
<Tooltip>
|
<>
|
||||||
<TooltipTrigger asChild>
|
<TooltipProvider>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="outline"
|
<TooltipTrigger asChild>
|
||||||
size="icon"
|
<Button
|
||||||
onClick={() => router.push(`/dashboard/administracion/encuestas/editar/${data.id!}`)}
|
variant="outline"
|
||||||
>
|
size="icon"
|
||||||
<Edit className="h-4 w-4" />
|
onClick={() =>
|
||||||
</Button>
|
router.push(
|
||||||
</TooltipTrigger>
|
`/dashboard/administracion/encuestas/editar/${data.id!}`,
|
||||||
<TooltipContent>
|
)
|
||||||
<p>Editar</p>
|
}
|
||||||
</TooltipContent>
|
>
|
||||||
</Tooltip>
|
<Edit className="h-4 w-4" />
|
||||||
</TooltipProvider>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Editar</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
<Trash className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Eliminar</p>
|
<p>Eliminar</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
161
apps/web/feactures/training/actions/training-actions.ts
Normal file
161
apps/web/feactures/training/actions/training-actions.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
'use server';
|
||||||
|
import { safeFetchApi } from '@/lib/fetch.api';
|
||||||
|
import { trainingStatisticsResponseSchema } from '../schemas/statistics';
|
||||||
|
import {
|
||||||
|
TrainingMutate,
|
||||||
|
TrainingSchema,
|
||||||
|
trainingApiResponseSchema,
|
||||||
|
} from '../schemas/training';
|
||||||
|
|
||||||
|
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 [error, response] = await safeFetchApi(
|
||||||
|
trainingStatisticsResponseSchema,
|
||||||
|
`/training/statistics?${searchParams.toString()}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
|
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 | 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',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message || 'Error al eliminar el registro');
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
41
apps/web/feactures/training/columnas del excel.sql
Normal file
41
apps/web/feactures/training/columnas del excel.sql
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
-- datos basicos
|
||||||
|
nombre,
|
||||||
|
apellido,
|
||||||
|
fecha de la visita,
|
||||||
|
-->Falta
|
||||||
|
hora de la visita,
|
||||||
|
-- datos de la ubicacion
|
||||||
|
estado,
|
||||||
|
municipio,
|
||||||
|
parroquia,
|
||||||
|
nombre de la comuna,
|
||||||
|
CODIGO SITUR COMUNA,
|
||||||
|
CONSEJO COMUNAL,
|
||||||
|
CODIGO SITUR CONSEJO COMUNAL,
|
||||||
|
-- datos de la osp
|
||||||
|
actividad productiva (agricola,textil,bloquera,carpinteria,unidad de suministro),
|
||||||
|
realice una breve descripcion del requerimiento financiero,
|
||||||
|
NOMBRE DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
DIRECCIÓN DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
RIF DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
TIPO DE ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
ESTATUS ACTUAL,
|
||||||
|
AÑO DE CONSTITUCIÓN DE LA EMPRESA ,
|
||||||
|
CANTIDAD DE PRODUCTORES QUE LA CONFORMAN,
|
||||||
|
BREVE DESCRIPCIÓN DEL PRODUCTO O SERVICIO QUE OFRECE,
|
||||||
|
CAPACIDAD INSTALADA,
|
||||||
|
CAPACIDAD OPERATIVA,
|
||||||
|
¿EXPLIQUE LAS RAZONES GENERALES POR LAS CUALES LA UNIDAD DE PRODUCCIÓN TUVO QUE PARALIZARSE?
|
||||||
|
-- datos del responsable
|
||||||
|
NOMBRE Y APELLIDO DEL RESPONSABLE DE LA OSP,
|
||||||
|
CÉDULA DEL RESPONSABLE (SIN PUNTOS),
|
||||||
|
RIF DEL RESPONSABLE (SIN PUNTOS),
|
||||||
|
TELÉFONOS (COLOQUE 2 NUMEROS DE TELEFONOS),
|
||||||
|
CORREO ELECTRÓNICO,
|
||||||
|
ESTADO CIVIL DEL PRODUCTOR,
|
||||||
|
CARGA FAMILIAR,
|
||||||
|
NUMERO DE HIJOS,
|
||||||
|
-- datos adicionales
|
||||||
|
OBSERVACIONES GENERALES,
|
||||||
|
-- fotos
|
||||||
|
COLOCAR TRES (3) REGISTROS FOTOGRÁFICOS VISIBLES DEL ESPACIO Y MAQUINARIAS ACTUALMENTE (OBLIGATORIO),
|
||||||
175
apps/web/feactures/training/components/equipment-list.tsx
Normal file
175
apps/web/feactures/training/components/equipment-list.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@repo/shadcn/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@repo/shadcn/components/ui/table';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import { Label } from '@repo/shadcn/label';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { TrainingSchema } from '../schemas/training';
|
||||||
|
|
||||||
|
interface EquipmentItem {
|
||||||
|
machine: string;
|
||||||
|
quantity: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EquipmentList() {
|
||||||
|
const { control, register } = useFormContext<TrainingSchema>();
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'equipmentList',
|
||||||
|
});
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [newItem, setNewItem] = useState<EquipmentItem>({
|
||||||
|
machine: '',
|
||||||
|
quantity: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (newItem.machine.trim()) {
|
||||||
|
append({
|
||||||
|
machine: newItem.machine,
|
||||||
|
quantity: newItem.quantity ? Number(newItem.quantity) : 0,
|
||||||
|
});
|
||||||
|
setNewItem({ machine: '', quantity: '' });
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium">Datos del Equipamiento</h3>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" type="button">
|
||||||
|
Agregar Maquinaria
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Agregar Maquinaria/Equipo</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
Datos de equipamiento
|
||||||
|
</DialogDescription>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="modal-machine">Maquinaria</Label>
|
||||||
|
<Input
|
||||||
|
id="modal-machine"
|
||||||
|
value={newItem.machine}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, machine: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Nombre de la maquinaria"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="modal-quantity">Cantidad</Label>
|
||||||
|
<Input
|
||||||
|
id="modal-quantity"
|
||||||
|
type="number"
|
||||||
|
value={newItem.quantity}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, quantity: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="button" onClick={handleAdd}>
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Maquinaria</TableHead>
|
||||||
|
<TableHead>Cantidad</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<TableRow key={field.id}>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`equipmentList.${index}.machine`)}
|
||||||
|
defaultValue={field.machine ?? ''}
|
||||||
|
/>
|
||||||
|
{field.machine}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`equipmentList.${index}.quantity`)}
|
||||||
|
defaultValue={field.quantity ?? ''}
|
||||||
|
/>
|
||||||
|
{field.quantity}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{fields.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={3}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No hay equipamiento registrado
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1762
apps/web/feactures/training/components/form.tsx
Normal file
1762
apps/web/feactures/training/components/form.tsx
Normal file
File diff suppressed because it is too large
Load Diff
205
apps/web/feactures/training/components/product-activity-list.tsx
Normal file
205
apps/web/feactures/training/components/product-activity-list.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@repo/shadcn/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@repo/shadcn/components/ui/table';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import { Label } from '@repo/shadcn/label';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { TrainingSchema } from '../schemas/training';
|
||||||
|
|
||||||
|
const UNIT_OPTIONS = ['KG', 'TON', 'UNID', 'LT', 'MTS', 'QQ', 'HM2', 'SACOS'];
|
||||||
|
|
||||||
|
// 1. Definimos la estructura de los datos para que TypeScript no se queje
|
||||||
|
// ProductItem y ProductFormValues locales eliminados en favor de TrainingSchema
|
||||||
|
|
||||||
|
export function ProductActivityList() {
|
||||||
|
const { control, register } = useFormContext<TrainingSchema>();
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'productList',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
// Modal Form State
|
||||||
|
const [newItem, setNewItem] = useState<any>({
|
||||||
|
description: '',
|
||||||
|
dailyCount: '',
|
||||||
|
weeklyCount: '',
|
||||||
|
monthlyCount: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (newItem.description) {
|
||||||
|
append(newItem);
|
||||||
|
setNewItem({
|
||||||
|
description: '',
|
||||||
|
dailyCount: '',
|
||||||
|
weeklyCount: '',
|
||||||
|
monthlyCount: '',
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium">Datos de Actividad Productiva</h3>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Agregar Producto/Actividad</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Producto Terminado</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
Datos de actividad productiva
|
||||||
|
</DialogDescription>
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Descripción</Label>
|
||||||
|
<Input
|
||||||
|
value={newItem.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, description: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cant. Diario</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newItem.dailyCount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, dailyCount: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cant. Semanal</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newItem.weeklyCount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, weeklyCount: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cant. Mensual</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newItem.monthlyCount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, monthlyCount: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={handleAdd}>Guardar</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Producto/Descripción</TableHead>
|
||||||
|
<TableHead>Producción Diario</TableHead>
|
||||||
|
<TableHead>Producción Semanal</TableHead>
|
||||||
|
<TableHead>Producción Mensual</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<TableRow key={field.id}>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productList.${index}.description`)}
|
||||||
|
defaultValue={field.description ?? ''}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productList.${index}.dailyCount`)}
|
||||||
|
defaultValue={field.dailyCount ?? ''}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productList.${index}.weeklyCount`)}
|
||||||
|
defaultValue={field.weeklyCount ?? ''}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productList.${index}.monthlyCount`)}
|
||||||
|
defaultValue={field.monthlyCount ?? ''}
|
||||||
|
/>
|
||||||
|
{field.description}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{field.dailyCount}</TableCell>
|
||||||
|
<TableCell>{field.weeklyCount}</TableCell>
|
||||||
|
<TableCell>{field.monthlyCount}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{fields.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No hay productos registrados
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
204
apps/web/feactures/training/components/production-list.tsx
Normal file
204
apps/web/feactures/training/components/production-list.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@repo/shadcn/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@repo/shadcn/components/ui/table';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import { Label } from '@repo/shadcn/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { TrainingSchema } from '../schemas/training';
|
||||||
|
|
||||||
|
const UNIT_OPTIONS = ['KG', 'TON', 'UNID', 'LT', 'MTS', 'QQ', 'HM2', 'SACOS'];
|
||||||
|
|
||||||
|
export function ProductionList() {
|
||||||
|
const { control, register } = useFormContext<TrainingSchema>();
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'productionList',
|
||||||
|
});
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [newItem, setNewItem] = useState({
|
||||||
|
supplyType: '',
|
||||||
|
quantity: '',
|
||||||
|
unit: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (newItem.supplyType && newItem.quantity && newItem.unit) {
|
||||||
|
append({ ...newItem, quantity: Number(newItem.quantity) });
|
||||||
|
setNewItem({ supplyType: '', quantity: '', unit: '' });
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium">Datos de Producción</h3>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Agregar Producción</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Materia prima requerida (mensual)</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
Datos de producción
|
||||||
|
</DialogDescription>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Tipo de Insumo/Rubro</Label>
|
||||||
|
<Input
|
||||||
|
value={newItem.supplyType}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, supplyType: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Tipo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cantidad Mensual</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
className="flex-1"
|
||||||
|
value={newItem.quantity}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, quantity: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={newItem.unit}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
setNewItem({ ...newItem, unit: val })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[120px]">
|
||||||
|
<SelectValue placeholder="Unidad" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{UNIT_OPTIONS.map((unit) => (
|
||||||
|
<SelectItem key={unit} value={unit}>
|
||||||
|
{unit}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={
|
||||||
|
!newItem.supplyType || !newItem.quantity || !newItem.unit
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Tipo Insumo</TableHead>
|
||||||
|
<TableHead>Cantidad (Mensual)</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<TableRow key={field.id}>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productionList.${index}.supplyType`)}
|
||||||
|
defaultValue={field.supplyType ?? ''}
|
||||||
|
/>
|
||||||
|
{field.supplyType}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productionList.${index}.quantity`)}
|
||||||
|
defaultValue={field.quantity ?? ''}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productionList.${index}.unit`)}
|
||||||
|
defaultValue={field.unit ?? ''}
|
||||||
|
/>
|
||||||
|
{field.quantity} {field.unit}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{fields.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No hay datos de producción registrados
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
apps/web/feactures/training/components/training-header.tsx
Normal file
13
apps/web/feactures/training/components/training-header.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
import { Heading } from '@repo/shadcn/heading';
|
||||||
|
|
||||||
|
export function TrainingHeader() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<Heading
|
||||||
|
title="Registro de Organizaciones Socioproductivas"
|
||||||
|
description="Gestiona los registros de las OSP"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
apps/web/feactures/training/components/training-list.tsx
Normal file
48
apps/web/feactures/training/components/training-list.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
import { DataTable } from '@repo/shadcn/table/data-table';
|
||||||
|
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
||||||
|
import { useTrainingQuery } from '../hooks/use-training';
|
||||||
|
import { columns } from './training-tables/columns';
|
||||||
|
|
||||||
|
interface TrainingListProps {
|
||||||
|
initialPage: number;
|
||||||
|
initialSearch?: string | null;
|
||||||
|
initialLimit: number;
|
||||||
|
apiUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TrainingList({
|
||||||
|
initialPage,
|
||||||
|
initialSearch,
|
||||||
|
initialLimit,
|
||||||
|
apiUrl,
|
||||||
|
}: TrainingListProps) {
|
||||||
|
const filters = {
|
||||||
|
page: initialPage,
|
||||||
|
limit: initialLimit,
|
||||||
|
...(initialSearch && { search: initialSearch }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isLoading } = useTrainingQuery(filters);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <DataTableSkeleton columnCount={5} rowCount={initialLimit} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformedData =
|
||||||
|
data?.data?.map((item) => ({
|
||||||
|
...item,
|
||||||
|
communeRif: item.communeRif || '',
|
||||||
|
communeSpokespersonName: item.communeSpokespersonName || '',
|
||||||
|
communalCouncilRif: item.communalCouncilRif || '',
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
columns={columns({ apiUrl })}
|
||||||
|
data={transformedData}
|
||||||
|
totalItems={data?.meta.totalCount || 0}
|
||||||
|
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
373
apps/web/feactures/training/components/training-statistics.tsx
Normal file
373
apps/web/feactures/training/components/training-statistics.tsx
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useMunicipalityQuery,
|
||||||
|
useParishQuery,
|
||||||
|
useStateQuery,
|
||||||
|
} from '@/feactures/location/hooks/use-query-location';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@repo/shadcn/card';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
import { SelectSearchable } from '@repo/shadcn/select-searchable';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
Legend,
|
||||||
|
Pie,
|
||||||
|
PieChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
|
||||||
|
|
||||||
|
const OSP_TYPES = [
|
||||||
|
'EPSD',
|
||||||
|
'EPSI',
|
||||||
|
'UPF',
|
||||||
|
'Cooperativa',
|
||||||
|
'Grupo de Intercambio',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function TrainingStatistics() {
|
||||||
|
// Filter State
|
||||||
|
const [startDate, setStartDate] = useState<string>('');
|
||||||
|
const [endDate, setEndDate] = useState<string>('');
|
||||||
|
const [stateId, setStateId] = useState<number>(0);
|
||||||
|
const [municipalityId, setMunicipalityId] = useState<number>(0);
|
||||||
|
const [parishId, setParishId] = useState<number>(0);
|
||||||
|
const [ospType, setOspType] = useState<string>('');
|
||||||
|
|
||||||
|
// Location Data
|
||||||
|
const { data: dataState } = useStateQuery();
|
||||||
|
const { data: dataMunicipality } = useMunicipalityQuery(stateId);
|
||||||
|
const { data: dataParish } = useParishQuery(municipalityId);
|
||||||
|
|
||||||
|
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
|
||||||
|
const municipalityOptions =
|
||||||
|
Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
|
||||||
|
? dataMunicipality.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
|
||||||
|
const parishOptions =
|
||||||
|
Array.isArray(dataParish?.data) && dataParish.data.length > 0
|
||||||
|
? dataParish.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
|
||||||
|
|
||||||
|
// Query with Filters
|
||||||
|
const { data, isLoading, refetch } = useTrainingStatsQuery({
|
||||||
|
startDate: startDate || undefined,
|
||||||
|
endDate: endDate || undefined,
|
||||||
|
stateId: stateId || undefined,
|
||||||
|
municipalityId: municipalityId || undefined,
|
||||||
|
parishId: parishId || undefined,
|
||||||
|
ospType: ospType || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
setStartDate('');
|
||||||
|
setEndDate('');
|
||||||
|
setStateId(0);
|
||||||
|
setMunicipalityId(0);
|
||||||
|
setParishId(0);
|
||||||
|
setOspType('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center p-8">Cargando estadísticas...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center p-8">No hay datos disponibles.</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
totalOsps,
|
||||||
|
totalProducers,
|
||||||
|
statusDistribution,
|
||||||
|
activityDistribution,
|
||||||
|
typeDistribution,
|
||||||
|
stateDistribution,
|
||||||
|
yearDistribution,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'#0088FE',
|
||||||
|
'#00C49F',
|
||||||
|
'#FFBB28',
|
||||||
|
'#FF8042',
|
||||||
|
'#8884d8',
|
||||||
|
'#82ca9d',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filters Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filtros</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Fecha Inicio</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Fecha Fin</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Estado</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={stateOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setStateId(Number(value));
|
||||||
|
setMunicipalityId(0); // Reset municipality
|
||||||
|
setParishId(0); // Reset parish
|
||||||
|
}}
|
||||||
|
placeholder="Selecciona un estado"
|
||||||
|
defaultValue={stateId ? stateId.toString() : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Municipio</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={municipalityOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setMunicipalityId(Number(value));
|
||||||
|
setParishId(0);
|
||||||
|
}}
|
||||||
|
placeholder="Selecciona municipio"
|
||||||
|
defaultValue={municipalityId ? municipalityId.toString() : ''}
|
||||||
|
disabled={!stateId || stateId === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Parroquia</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={parishOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => setParishId(Number(value))}
|
||||||
|
placeholder="Selecciona parroquia"
|
||||||
|
defaultValue={parishId ? parishId.toString() : ''}
|
||||||
|
disabled={!municipalityId || municipalityId === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Tipo de OSP</label>
|
||||||
|
<Select value={ospType} onValueChange={setOspType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Todos" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
|
{OSP_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{type}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button variant="outline" onClick={handleClearFilters}>
|
||||||
|
Limpiar Filtros
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Total de OSP Registradas
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalOsps}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Organizaciones Socioproductivas
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Total de Productores
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalProducers}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Productores asociados
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Actividad Productiva</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Distribución por tipo de actividad
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={activityDistribution}
|
||||||
|
layout="vertical"
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={150} />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* State Distribution */}
|
||||||
|
{/* <Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribución por Estado</CardTitle>
|
||||||
|
<CardDescription>OSP registradas por estado</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={stateDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#00C49F" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card> */}
|
||||||
|
|
||||||
|
{/* Year Distribution */}
|
||||||
|
<Card className="col-span-full lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Año de Constitución</CardTitle>
|
||||||
|
<CardDescription>Año de registro de la empresa</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={yearDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Estatus Actual</CardTitle>
|
||||||
|
<CardDescription>Estado operativo de las OSP</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={statusDistribution}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{statusDistribution.map((entry, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={COLORS[index % COLORS.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Tipo de Organización</CardTitle>
|
||||||
|
<CardDescription>Clasificación de las OSP</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={typeDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
'use client';
|
||||||
|
import { AlertModal } from '@/components/modal/alert-modal';
|
||||||
|
import { useDeleteTraining } from '@/feactures/training/hooks/use-training';
|
||||||
|
import { TrainingSchema } from '@/feactures/training/schemas/training';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@repo/shadcn/tooltip';
|
||||||
|
import { Edit, Eye, Trash } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { TrainingViewModal } from '../training-view-modal';
|
||||||
|
|
||||||
|
interface CellActionProps {
|
||||||
|
data: TrainingSchema;
|
||||||
|
apiUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [viewOpen, setViewOpen] = useState(false);
|
||||||
|
const { mutate: deleteTraining } = useDeleteTraining();
|
||||||
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
deleteTraining(data.id!);
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapear roles a minúsculas para comparación segura
|
||||||
|
const userRoles = session?.user?.role?.map((r) => r.rol.toLowerCase()) || [];
|
||||||
|
|
||||||
|
const isAdminOrSuper = userRoles.some((r) =>
|
||||||
|
['superadmin', 'admin'].includes(r),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Soporta tanto 'coordinator' como 'coordinador'
|
||||||
|
const isCoordinator = userRoles.some(r =>
|
||||||
|
r.includes('coordinator') || r.includes('coordinador')
|
||||||
|
);
|
||||||
|
|
||||||
|
const isOtherAuthorized = userRoles.some((r) =>
|
||||||
|
['autoridad', 'manager'].includes(r),
|
||||||
|
);
|
||||||
|
|
||||||
|
// El creador del registro: intentamos createdBy o created_by por si acaso
|
||||||
|
const createdBy = data.createdBy ?? (data as any).created_by;
|
||||||
|
|
||||||
|
// Comparación robusta de IDs
|
||||||
|
const isOwner = createdBy !== undefined &&
|
||||||
|
createdBy !== null &&
|
||||||
|
Number(createdBy) === Number(session?.user?.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
loading={loading}
|
||||||
|
title="¿Estás seguro que desea eliminar este registro?"
|
||||||
|
description="Esta acción no se puede deshacer."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TrainingViewModal
|
||||||
|
isOpen={viewOpen}
|
||||||
|
onClose={() => setViewOpen(false)}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{/* VER DETALLE: superadmin, admin, autoridad, manager, or owner coordinator */}
|
||||||
|
{(isAdminOrSuper || isOtherAuthorized || (isCoordinator && isOwner)) && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setViewOpen(true)}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Ver detalle</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* EDITAR: Superadmin, admin OR (coordinator if owner) */}
|
||||||
|
{(isAdminOrSuper || (isCoordinator && isOwner)) && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/dashboard/formulario/editar/${data.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Editar</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ELIMINAR: Solo superadmin y admin */}
|
||||||
|
{isAdminOrSuper && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Eliminar</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user