Compare commits
18 Commits
export_exc
...
00ab65aee3
| Author | SHA1 | Date | |
|---|---|---|---|
| 00ab65aee3 | |||
| 524869b1f9 | |||
| f88ab2a971 | |||
| 0666877811 | |||
| ff46776e4a | |||
| d6de7527e4 | |||
| f910aea3cc | |||
| a88cf94adb | |||
| 70e5200549 | |||
| c70e146ce2 | |||
| fed90d9ff1 | |||
| 0efd5a11bd | |||
| e149500735 | |||
| d71ad98e85 | |||
| 590f62fad9 | |||
| 510327de58 | |||
| a0c363dd1b | |||
| 42e802f8a7 |
@@ -15,5 +15,12 @@ DATABASE_URL="postgresql://postgres:local**@localhost:5432/caja_ahorro" #url con
|
|||||||
|
|
||||||
#Mail Configuration
|
#Mail Configuration
|
||||||
MAIL_HOST=gmail
|
MAIL_HOST=gmail
|
||||||
MAIL_USERNAME="123"
|
MAIL_USERNAME=
|
||||||
MAIL_PASSWORD="123"
|
MAIL_PASSWORD=
|
||||||
|
|
||||||
|
MINIO_ENDPOINT=
|
||||||
|
MINIO_PORT=
|
||||||
|
MINIO_ACCESS_KEY=
|
||||||
|
MINIO_SECRET_KEY=
|
||||||
|
MINIO_BUCKET=
|
||||||
|
MINIO_USE_SSL=
|
||||||
|
|||||||
@@ -42,16 +42,17 @@
|
|||||||
"@nestjs/platform-express": "11.0.0",
|
"@nestjs/platform-express": "11.0.0",
|
||||||
"dotenv": "16.5.0",
|
"dotenv": "16.5.0",
|
||||||
"drizzle-orm": "0.40.0",
|
"drizzle-orm": "0.40.0",
|
||||||
"exceljs": "^4.4.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"
|
"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 { TrainingModule } from './features/training/training.module';
|
||||||
|
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -51,6 +52,7 @@ import { TrainingModule } from './features/training/training.module';
|
|||||||
NodeMailerModule,
|
NodeMailerModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
ThrottleModule,
|
ThrottleModule,
|
||||||
|
MinioModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
MailModule,
|
MailModule,
|
||||||
@@ -61,7 +63,7 @@ import { TrainingModule } from './features/training/training.module';
|
|||||||
SurveysModule,
|
SurveysModule,
|
||||||
LocationModule,
|
LocationModule,
|
||||||
InventoryModule,
|
InventoryModule,
|
||||||
TrainingModule
|
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.',
|
||||||
|
|||||||
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");
|
||||||
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
@@ -120,6 +120,69 @@
|
|||||||
"when": 1769653021994,
|
"when": 1769653021994,
|
||||||
"tag": "0016_silent_tag",
|
"tag": "0016_silent_tag",
|
||||||
"breakpoints": true
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ export const trainingSurveys = t.pgTable(
|
|||||||
{
|
{
|
||||||
// === 1. IDENTIFICADORES Y DATOS DE VISITA ===
|
// === 1. IDENTIFICADORES Y DATOS DE VISITA ===
|
||||||
id: t.serial('id').primaryKey(),
|
id: t.serial('id').primaryKey(),
|
||||||
firstname: t.text('firstname').notNull(),
|
coorFullName: t.text('coor_full_name').notNull(),
|
||||||
lastname: t.text('lastname').notNull(),
|
|
||||||
visitDate: t.timestamp('visit_date').notNull(),
|
visitDate: t.timestamp('visit_date').notNull(),
|
||||||
coorPhone: t.text('coor_phone'),
|
coorPhone: t.text('coor_phone'),
|
||||||
|
|
||||||
@@ -77,8 +76,8 @@ export const trainingSurveys = t.pgTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.default(''),
|
.default(''),
|
||||||
productiveActivity: t.text('productive_activity').notNull(),
|
productiveActivity: t.text('productive_activity').notNull(),
|
||||||
ospRif: t.text('osp_rif').notNull(),
|
ospRif: t.text('osp_rif'),
|
||||||
ospName: t.text('osp_name').notNull(),
|
ospName: t.text('osp_name'),
|
||||||
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
||||||
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
|
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
|
||||||
infrastructureMt2: t.text('infrastructure_mt2').notNull().default(''),
|
infrastructureMt2: t.text('infrastructure_mt2').notNull().default(''),
|
||||||
@@ -98,19 +97,13 @@ export const trainingSurveys = t.pgTable(
|
|||||||
.text('commune_spokesperson_name')
|
.text('commune_spokesperson_name')
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(''),
|
.default(''),
|
||||||
communeSpokespersonCedula: t
|
communeSpokespersonCedula: t.text('commune_spokesperson_cedula'),
|
||||||
.text('commune_spokesperson_cedula')
|
communeSpokespersonRif: t.text('commune_spokesperson_rif'),
|
||||||
.notNull()
|
|
||||||
.default(''),
|
|
||||||
communeSpokespersonRif: t
|
|
||||||
.text('commune_spokesperson_rif')
|
|
||||||
.notNull()
|
|
||||||
.default(''),
|
|
||||||
communeSpokespersonPhone: t
|
communeSpokespersonPhone: t
|
||||||
.text('commune_spokesperson_phone')
|
.text('commune_spokesperson_phone')
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(''),
|
.default(''),
|
||||||
communeEmail: t.text('commune_email').notNull().default(''),
|
communeEmail: t.text('commune_email'),
|
||||||
communalCouncil: t.text('communal_council').notNull(),
|
communalCouncil: t.text('communal_council').notNull(),
|
||||||
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
|
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
|
||||||
communalCouncilRif: t.text('communal_council_rif').notNull().default(''),
|
communalCouncilRif: t.text('communal_council_rif').notNull().default(''),
|
||||||
@@ -118,14 +111,10 @@ export const trainingSurveys = t.pgTable(
|
|||||||
.text('communal_council_spokesperson_name')
|
.text('communal_council_spokesperson_name')
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(''),
|
.default(''),
|
||||||
communalCouncilSpokespersonCedula: t
|
communalCouncilSpokespersonCedula: t.text(
|
||||||
.text('communal_council_spokesperson_cedula')
|
'communal_council_spokesperson_cedula',
|
||||||
.notNull()
|
),
|
||||||
.default(''),
|
communalCouncilSpokespersonRif: t.text('communal_council_spokesperson_rif'),
|
||||||
communalCouncilSpokespersonRif: t
|
|
||||||
.text('communal_council_spokesperson_rif')
|
|
||||||
.notNull()
|
|
||||||
.default(''),
|
|
||||||
communalCouncilSpokespersonPhone: t
|
communalCouncilSpokespersonPhone: t
|
||||||
.text('communal_council_spokesperson_phone')
|
.text('communal_council_spokesperson_phone')
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -136,22 +125,44 @@ export const trainingSurveys = t.pgTable(
|
|||||||
.default(''),
|
.default(''),
|
||||||
ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
|
ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
|
||||||
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
|
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
|
||||||
ospResponsibleRif: t.text('osp_responsible_rif').notNull(),
|
ospResponsibleRif: t.text('osp_responsible_rif'),
|
||||||
civilState: t.text('civil_state').notNull(),
|
civilState: t.text('civil_state'),
|
||||||
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
|
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
|
||||||
ospResponsibleEmail: t.text('osp_responsible_email').notNull(),
|
ospResponsibleEmail: t.text('osp_responsible_email'),
|
||||||
familyBurden: t.integer('family_burden').notNull(),
|
familyBurden: t.integer('family_burden'),
|
||||||
numberOfChildren: t.integer('number_of_children').notNull(),
|
numberOfChildren: t.integer('number_of_children'),
|
||||||
generalObservations: t.text('general_observations'),
|
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'),
|
photo1: t.text('photo1'),
|
||||||
photo2: t.text('photo2'),
|
photo2: t.text('photo2'),
|
||||||
photo3: t.text('photo3'),
|
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,
|
...timestamps,
|
||||||
},
|
},
|
||||||
(trainingSurveys) => ({
|
(trainingSurveys) => ({
|
||||||
trainingSurveysIndex: t
|
trainingSurveysIndex: t
|
||||||
.index('training_surveys_index_00')
|
.index('training_surveys_index_00')
|
||||||
.on(trainingSurveys.firstname),
|
.on(trainingSurveys.coorFullName),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
// api/src/feacture/auth/auth.controller.ts
|
// 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')
|
||||||
@@ -39,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) {
|
||||||
@@ -58,17 +49,11 @@ export class AuthController {
|
|||||||
@Patch('refresh')
|
@Patch('refresh')
|
||||||
//@RequirePermissions('auth:refresh-token')
|
//@RequirePermissions('auth:refresh-token')
|
||||||
async refreshToken(@Body() refreshTokenDto: any) {
|
async refreshToken(@Body() refreshTokenDto: any) {
|
||||||
|
// console.log('REFRESCANDO');
|
||||||
|
// console.log(refreshTokenDto);
|
||||||
|
// console.log('-----------');
|
||||||
|
|
||||||
console.log('refreshTokenDto', refreshTokenDto);
|
return await this.authService.refreshToken(refreshTokenDto);
|
||||||
|
|
||||||
const data = await this.authService.refreshToken(refreshTokenDto);
|
|
||||||
|
|
||||||
// console.log('data', data);
|
|
||||||
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
return {tokens: data}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Public()
|
// @Public()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||||
import * as bcrypt from 'bcryptjs';
|
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 { roles, sessions, users, usersRole } from 'src/database/index';
|
import { roles, sessions, users, usersRole } from 'src/database/index';
|
||||||
@@ -273,50 +273,118 @@ export class AuthService {
|
|||||||
|
|
||||||
//Refresh User Access Token
|
//Refresh User Access Token
|
||||||
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
|
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
|
||||||
const secret = envs.refresh_token_secret;
|
const { refreshToken } = dto;
|
||||||
const { user_id, token } = dto;
|
|
||||||
|
|
||||||
console.log('secret', secret);
|
// 1. Validar firma del token (Crypto check)
|
||||||
console.log('refresh_token', token);
|
let payload: any;
|
||||||
|
try {
|
||||||
const validation = await this.jwtService.verifyAsync(token, {
|
payload = await this.jwtService.verifyAsync(refreshToken, {
|
||||||
secret,
|
secret: envs.refresh_token_secret,
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new UnauthorizedException('Invalid Refresh Token Signature');
|
||||||
|
}
|
||||||
|
|
||||||
if (!validation) throw new UnauthorizedException('Invalid refresh token');
|
const userId = Number(payload.sub); // Es más seguro usar el ID del token que el del DTO
|
||||||
|
|
||||||
const session = await this.drizzle
|
// 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), eq(sessions.sessionToken, token)),
|
|
||||||
);
|
|
||||||
|
|
||||||
// console.log(session.length);
|
if (!currentSession) throw new NotFoundException('Session not found');
|
||||||
|
|
||||||
if (session.length === 0) throw new NotFoundException('session not found');
|
// CONFIGURACIÓN: Tiempo de gracia en milisegundos (ej: 15 segundos)
|
||||||
const user = await this.findUserById(user_id);
|
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');
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
|
||||||
// Genera token
|
// Generar nuevos tokens (A -> B)
|
||||||
const tokens = await this.generateTokens(user);
|
const tokensNew = await this.generateTokens(user);
|
||||||
const decodeAccess = this.decodeToken(tokens.access_token);
|
const decodeAccess = this.decodeToken(tokensNew.access_token);
|
||||||
const decodeRefresh = this.decodeToken(tokens.refresh_token);
|
const decodeRefresh = this.decodeToken(tokensNew.refresh_token);
|
||||||
|
|
||||||
// Actualiza session
|
// Actualizamos DB guardando el token "viejo" como "previous"
|
||||||
await this.drizzle
|
await this.drizzle
|
||||||
.update(sessions)
|
.update(sessions)
|
||||||
.set({ sessionToken: tokens.refresh_token, expiresAt: decodeRefresh.exp })
|
.set({
|
||||||
.where(eq(sessions.userId, user_id));
|
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 {
|
return {
|
||||||
access_token: tokens.access_token,
|
access_token: tokensNew.access_token,
|
||||||
access_expire_in: decodeAccess.exp,
|
access_expire_in: decodeAccess.exp,
|
||||||
refresh_token: tokens.refresh_token,
|
refresh_token: tokensNew.refresh_token,
|
||||||
refresh_expire_in: decodeRefresh.exp,
|
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);
|
||||||
|
|
||||||
|
// 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ó.
|
||||||
|
return {
|
||||||
|
access_token: newAccessToken,
|
||||||
|
access_expire_in: decodeAccess.exp,
|
||||||
|
refresh_token: currentSession.sessionToken!, // Devolvemos el token 'B'
|
||||||
|
refresh_expire_in: currentSession.expiresAt as number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// ESCENARIO C: Robo de Token (Reuse Detection)
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Si el token no es el actual, ni el anterior válido... ALGUIEN LO ROBÓ.
|
||||||
|
// 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
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export class RefreshTokenDto {
|
|||||||
@IsString({
|
@IsString({
|
||||||
message: 'Refresh token must be a string',
|
message: 'Refresh token must be a string',
|
||||||
})
|
})
|
||||||
token: string;
|
refreshToken: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
user_id: number;
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,19 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||||||
import { Transform, Type } from 'class-transformer';
|
import { Transform, Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
IsBoolean,
|
|
||||||
IsDateString,
|
IsDateString,
|
||||||
|
IsEmail,
|
||||||
IsInt,
|
IsInt,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class CreateTrainingDto {
|
export class CreateTrainingDto {
|
||||||
// === 1. DATOS BÁSICOS ===
|
// === 1. DATOS BÁSICOS ===
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
firstname: string;
|
coorFullName: string;
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
lastname: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
@@ -30,11 +27,11 @@ export class CreateTrainingDto {
|
|||||||
|
|
||||||
// === 2. DATOS OSP ===
|
// === 2. DATOS OSP ===
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsOptional()
|
||||||
ospName: string;
|
ospName: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsOptional()
|
||||||
ospRif: string;
|
ospRif: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -75,16 +72,14 @@ export class CreateTrainingDto {
|
|||||||
structureType?: string;
|
structureType?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsBoolean()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => value === 'true' || value === true) // Convierte "false" -> false
|
hasTransport?: string;
|
||||||
hasTransport?: boolean;
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsBoolean()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => value === 'true' || value === true)
|
isOpenSpace?: string;
|
||||||
isOpenSpace?: boolean;
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -124,6 +119,7 @@ export class CreateTrainingDto {
|
|||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
ospResponsibleRif: string;
|
ospResponsibleRif: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -131,20 +127,25 @@ export class CreateTrainingDto {
|
|||||||
ospResponsiblePhone: string;
|
ospResponsiblePhone: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsOptional()
|
||||||
ospResponsibleEmail: string;
|
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||||
|
@IsEmail()
|
||||||
|
ospResponsibleEmail?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
civilState: string;
|
civilState: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
@Type(() => Number) // Convierte "3" -> 3
|
@Type(() => Number) // Convierte "3" -> 3
|
||||||
familyBurden: number;
|
familyBurden: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
numberOfChildren: number;
|
numberOfChildren: number;
|
||||||
|
|
||||||
@@ -165,21 +166,15 @@ export class CreateTrainingDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
communeSpokespersonName: string;
|
communeSpokespersonName: string;
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
communeSpokespersonCedula: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
communeSpokespersonRif: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
communeSpokespersonPhone: string;
|
communeSpokespersonPhone: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsOptional()
|
||||||
communeEmail: string;
|
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||||
|
@IsEmail()
|
||||||
|
communeEmail?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -197,25 +192,66 @@ export class CreateTrainingDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
communalCouncilSpokespersonName: string;
|
communalCouncilSpokespersonName: string;
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
communalCouncilSpokespersonCedula: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsString()
|
|
||||||
communalCouncilSpokespersonRif: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
communalCouncilSpokespersonPhone: string;
|
communalCouncilSpokespersonPhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||||
|
@IsEmail()
|
||||||
|
communalCouncilEmail?: string;
|
||||||
|
|
||||||
|
// === 6. DISTRIBUCIÓN Y EXPORTACIÓN ===
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
communalCouncilEmail: string;
|
@IsOptional()
|
||||||
|
internalDistributionZone?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
isExporting?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
externalCountry?: string;
|
||||||
|
|
||||||
// === 6. LISTAS (Arrays JSON) ===
|
@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
|
// Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -263,13 +299,11 @@ export class CreateTrainingDto {
|
|||||||
})
|
})
|
||||||
productList?: any[];
|
productList?: any[];
|
||||||
|
|
||||||
|
|
||||||
//ubicacion
|
//ubicacion
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
state: string;
|
state: string;
|
||||||
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
municipality: string;
|
municipality: string;
|
||||||
@@ -277,4 +311,19 @@ export class CreateTrainingDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
parish: string;
|
parish: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
photo1?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
photo2?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
photo3?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,10 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Res,
|
Req,
|
||||||
UploadedFiles,
|
UploadedFiles,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
StreamableFile,
|
|
||||||
Header
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Readable } from 'stream';
|
|
||||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||||
import {
|
import {
|
||||||
ApiConsumes,
|
ApiConsumes,
|
||||||
@@ -27,33 +24,30 @@ import { CreateTrainingDto } from './dto/create-training.dto';
|
|||||||
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||||
import { TrainingService } from './training.service';
|
import { TrainingService } from './training.service';
|
||||||
import { Public } from '@/common/decorators';
|
|
||||||
|
|
||||||
@ApiTags('training')
|
@ApiTags('training')
|
||||||
@Controller('training')
|
@Controller('training')
|
||||||
export class TrainingController {
|
export class TrainingController {
|
||||||
constructor(private readonly trainingService: TrainingService) {}
|
constructor(private readonly trainingService: TrainingService) {}
|
||||||
|
|
||||||
// export training with excel
|
// @Public()
|
||||||
@Public()
|
// @Get('export/:id')
|
||||||
@Get('export/:id')
|
// @ApiOperation({ summary: 'Export training template' })
|
||||||
@ApiOperation({ summary: 'Export training with excel' })
|
// @ApiResponse({
|
||||||
@ApiResponse({
|
// status: 200,
|
||||||
status: 200,
|
// description: 'Return training template.',
|
||||||
description: 'Return training with excel.',
|
// content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
|
||||||
content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
|
// })
|
||||||
})
|
// @Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
// @Header('Content-Disposition', 'attachment; filename=export_osp.xlsx')
|
||||||
@Header('Content-Disposition', 'attachment; filename=export_osp.xlsx')
|
// async exportTemplate(@Param('id') id: string) {
|
||||||
async exportTemplate(@Param('id') id: string) {
|
// if (!Number(id)) {
|
||||||
if (!Number(id)) {
|
// throw new Error('ID is required');
|
||||||
throw new Error('ID is required');
|
// }
|
||||||
}
|
// const data = await this.trainingService.exportTemplate(Number(id));
|
||||||
const data = await this.trainingService.exportTemplate(Number(id));
|
// return new StreamableFile(data);
|
||||||
return new StreamableFile(Readable.from([data]));
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
// get all training records
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get all training records with pagination and filters',
|
summary: 'Get all training records with pagination and filters',
|
||||||
@@ -71,7 +65,6 @@ export class TrainingController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// get training statistics
|
|
||||||
@Get('statistics')
|
@Get('statistics')
|
||||||
@ApiOperation({ summary: 'Get training statistics' })
|
@ApiOperation({ summary: 'Get training statistics' })
|
||||||
@ApiResponse({ status: 200, description: 'Return training statistics.' })
|
@ApiResponse({ status: 200, description: 'Return training statistics.' })
|
||||||
@@ -80,7 +73,6 @@ export class TrainingController {
|
|||||||
return { message: 'Training statistics fetched successfully', data };
|
return { message: 'Training statistics fetched successfully', data };
|
||||||
}
|
}
|
||||||
|
|
||||||
// get training record by id
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'Get a training record by ID' })
|
@ApiOperation({ summary: 'Get a training record by ID' })
|
||||||
@ApiResponse({ status: 200, description: 'Return the training record.' })
|
@ApiResponse({ status: 200, description: 'Return the training record.' })
|
||||||
@@ -90,7 +82,6 @@ export class TrainingController {
|
|||||||
return { message: 'Training record fetched successfully', data };
|
return { message: 'Training record fetched successfully', data };
|
||||||
}
|
}
|
||||||
|
|
||||||
// create training record
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseInterceptors(FilesInterceptor('files', 3))
|
@UseInterceptors(FilesInterceptor('files', 3))
|
||||||
@ApiConsumes('multipart/form-data')
|
@ApiConsumes('multipart/form-data')
|
||||||
@@ -100,14 +91,19 @@ export class TrainingController {
|
|||||||
description: 'Training record created successfully.',
|
description: 'Training record created successfully.',
|
||||||
})
|
})
|
||||||
async create(
|
async create(
|
||||||
|
@Req() req: Request,
|
||||||
@Body() createTrainingDto: CreateTrainingDto,
|
@Body() createTrainingDto: CreateTrainingDto,
|
||||||
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||||
) {
|
) {
|
||||||
const data = await this.trainingService.create(createTrainingDto, files);
|
const userId = (req as any).user?.id;
|
||||||
|
const data = await this.trainingService.create(
|
||||||
|
createTrainingDto,
|
||||||
|
files,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
return { message: 'Training record created successfully', data };
|
return { message: 'Training record created successfully', data };
|
||||||
}
|
}
|
||||||
|
|
||||||
// update training record
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@UseInterceptors(FilesInterceptor('files', 3))
|
@UseInterceptors(FilesInterceptor('files', 3))
|
||||||
@ApiConsumes('multipart/form-data')
|
@ApiConsumes('multipart/form-data')
|
||||||
@@ -118,19 +114,21 @@ export class TrainingController {
|
|||||||
})
|
})
|
||||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||||
async update(
|
async update(
|
||||||
|
@Req() req: Request,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() updateTrainingDto: UpdateTrainingDto,
|
@Body() updateTrainingDto: UpdateTrainingDto,
|
||||||
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||||
) {
|
) {
|
||||||
|
const userId = (req as any).user?.id;
|
||||||
const data = await this.trainingService.update(
|
const data = await this.trainingService.update(
|
||||||
+id,
|
+id,
|
||||||
updateTrainingDto,
|
updateTrainingDto,
|
||||||
files,
|
files,
|
||||||
|
userId,
|
||||||
);
|
);
|
||||||
return { message: 'Training record updated successfully', data };
|
return { message: 'Training record updated successfully', data };
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete training record
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ApiOperation({ summary: 'Delete a training record' })
|
@ApiOperation({ summary: 'Delete a training record' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { HttpException, HttpStatus, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
import { MinioService } from '@/common/minio/minio.service';
|
||||||
import { and, eq, getTableColumns, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm';
|
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 { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||||
import * as schema from 'src/database/index';
|
import * as schema from 'src/database/index';
|
||||||
import { municipalities, parishes, states, trainingSurveys } from 'src/database/index';
|
import { states, trainingSurveys } from 'src/database/index';
|
||||||
// import XlsxPopulate from 'xlsx-populate';
|
|
||||||
import ExcelJS from 'exceljs';
|
|
||||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||||
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrainingService {
|
export class TrainingService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||||
|
private readonly minioService: MinioService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findAll(paginationDto?: PaginationDto) {
|
async findAll(paginationDto?: PaginationDto) {
|
||||||
@@ -110,17 +107,7 @@ export class TrainingService {
|
|||||||
// 2. Total Productores (Columna plana que mantuviste)
|
// 2. Total Productores (Columna plana que mantuviste)
|
||||||
this.drizzle
|
this.drizzle
|
||||||
.select({
|
.select({
|
||||||
sum: sql<number>`
|
sum: sql<number>`SUM(${trainingSurveys.womenCount} + ${trainingSurveys.menCount})`,
|
||||||
SUM(
|
|
||||||
(
|
|
||||||
SELECT SUM(
|
|
||||||
COALESCE((item->>'menCount')::int, 0) +
|
|
||||||
COALESCE((item->>'womenCount')::int, 0)
|
|
||||||
)
|
|
||||||
FROM jsonb_array_elements(${trainingSurveys.productList}) as item
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
})
|
})
|
||||||
.from(trainingSurveys)
|
.from(trainingSurveys)
|
||||||
.where(whereCondition),
|
.where(whereCondition),
|
||||||
@@ -218,17 +205,9 @@ export class TrainingService {
|
|||||||
|
|
||||||
async findOne(id: number) {
|
async findOne(id: number) {
|
||||||
const find = await this.drizzle
|
const find = await this.drizzle
|
||||||
.select({
|
.select()
|
||||||
...getTableColumns(trainingSurveys),
|
|
||||||
stateName: states.name,
|
|
||||||
municipalityName: municipalities.name,
|
|
||||||
parishName: parishes.name,
|
|
||||||
})
|
|
||||||
.from(trainingSurveys)
|
.from(trainingSurveys)
|
||||||
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
.where(eq(trainingSurveys.id, id));
|
||||||
.leftJoin(municipalities, eq(trainingSurveys.municipality, municipalities.id))
|
|
||||||
.leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
|
|
||||||
.where(eq(trainingSurveys.id, id))
|
|
||||||
|
|
||||||
if (find.length === 0) {
|
if (find.length === 0) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@@ -243,50 +222,54 @@ export class TrainingService {
|
|||||||
private async saveFiles(files: Express.Multer.File[]): Promise<string[]> {
|
private async saveFiles(files: Express.Multer.File[]): Promise<string[]> {
|
||||||
if (!files || files.length === 0) return [];
|
if (!files || files.length === 0) return [];
|
||||||
|
|
||||||
const uploadDir = './uploads/training';
|
|
||||||
if (!fs.existsSync(uploadDir)) {
|
|
||||||
fs.mkdirSync(uploadDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedPaths: string[] = [];
|
const savedPaths: string[] = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const fileName = `${Date.now()}-${Math.round(Math.random() * 1e9)}.png`;
|
const objectName = await this.minioService.upload(file, 'training');
|
||||||
const filePath = path.join(uploadDir, fileName);
|
const fileUrl = this.minioService.getPublicUrl(objectName);
|
||||||
|
savedPaths.push(fileUrl);
|
||||||
// Convertir a PNG usando sharp antes de guardar
|
|
||||||
await sharp(file.buffer)
|
|
||||||
.png()
|
|
||||||
.toFile(filePath);
|
|
||||||
|
|
||||||
savedPaths.push(`/assets/training/${fileName}`);
|
|
||||||
}
|
}
|
||||||
return savedPaths;
|
return savedPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteFile(assetPath: string) {
|
private async deleteFile(fileUrl: string) {
|
||||||
if (!assetPath) return;
|
if (!fileUrl) return;
|
||||||
// Map /assets/training/filename.webp back to ./uploads/training/filename.webp
|
|
||||||
const relativePath = assetPath.replace('/assets/training/', '');
|
|
||||||
const fullPath = path.join('./uploads/training', relativePath);
|
|
||||||
|
|
||||||
if (fs.existsSync(fullPath)) {
|
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(fullPath);
|
// If it's a full URL, we need to extract the part after the bucket name
|
||||||
} catch (err) {
|
if (fileUrl.startsWith('http')) {
|
||||||
console.error(`Error deleting file ${fullPath}:`, err);
|
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(
|
async create(
|
||||||
createTrainingDto: CreateTrainingDto,
|
createTrainingDto: CreateTrainingDto,
|
||||||
files: Express.Multer.File[],
|
files: Express.Multer.File[],
|
||||||
|
userId: number,
|
||||||
) {
|
) {
|
||||||
// 1. Guardar fotos
|
// 1. Guardar fotos
|
||||||
|
|
||||||
const photoPaths = await this.saveFiles(files);
|
const photoPaths = await this.saveFiles(files);
|
||||||
|
|
||||||
// 2. Extraer solo visitDate para formatearlo.
|
// 2. Extraer solo visitDate para formatearlo.
|
||||||
const { visitDate, state, municipality, parish, ...rest } = createTrainingDto;
|
// Ya NO extraemos state, municipality, etc. porque no vienen en el DTO.
|
||||||
|
const { visitDate, state, municipality, parish, ...rest } =
|
||||||
|
createTrainingDto;
|
||||||
|
|
||||||
const [newRecord] = await this.drizzle
|
const [newRecord] = await this.drizzle
|
||||||
.insert(trainingSurveys)
|
.insert(trainingSurveys)
|
||||||
@@ -304,6 +287,11 @@ export class TrainingService {
|
|||||||
state: Number(state) ?? null,
|
state: Number(state) ?? null,
|
||||||
municipality: Number(municipality) ?? null,
|
municipality: Number(municipality) ?? null,
|
||||||
parish: Number(parish) ?? 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();
|
.returning();
|
||||||
|
|
||||||
@@ -314,41 +302,68 @@ export class TrainingService {
|
|||||||
id: number,
|
id: number,
|
||||||
updateTrainingDto: UpdateTrainingDto,
|
updateTrainingDto: UpdateTrainingDto,
|
||||||
files: Express.Multer.File[],
|
files: Express.Multer.File[],
|
||||||
|
userId: number,
|
||||||
) {
|
) {
|
||||||
const currentRecord = await this.findOne(id);
|
const currentRecord = await this.findOne(id);
|
||||||
|
const photoFields = ['photo1', 'photo2', 'photo3'] as const;
|
||||||
|
|
||||||
const photoPaths = await this.saveFiles(files);
|
// 1. Guardar fotos nuevas en MinIO
|
||||||
|
const newFilePaths = await this.saveFiles(files);
|
||||||
|
|
||||||
const updateData: any = { ...updateTrainingDto };
|
const updateData: any = { ...updateTrainingDto };
|
||||||
|
|
||||||
// Handle photo updates/removals
|
// 2. Determinar el estado final de las fotos (diff)
|
||||||
const photoFields = ['photo1', 'photo2', 'photo3'] as const;
|
// - Si el DTO tiene un valor (URL existente o ''), lo usamos.
|
||||||
|
// - Si el DTO no tiene el campo (undefined), mantenemos el de la DB.
|
||||||
// 1. If we have NEW files, they replace any old files or occupy empty slots
|
const finalPhotos: (string | null)[] = photoFields.map((field) => {
|
||||||
if (photoPaths.length > 0) {
|
const dtoValue = updateData[field];
|
||||||
photoPaths.forEach((newPath, idx) => {
|
if (dtoValue !== undefined) {
|
||||||
const fieldName = photoFields[idx];
|
return dtoValue === '' ? null : dtoValue;
|
||||||
const oldPath = currentRecord[fieldName];
|
|
||||||
if (oldPath && oldPath !== newPath) {
|
|
||||||
this.deleteFile(oldPath);
|
|
||||||
}
|
}
|
||||||
updateData[fieldName] = newPath;
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. If the user explicitly cleared a photo field (updateData.photoX === '')
|
// 4. LIMPIEZA: Borrar de MinIO los archivos que ya no están en ningún slot
|
||||||
photoFields.forEach((field) => {
|
const oldPhotos = photoFields
|
||||||
if (updateData[field] === '') {
|
.map((f) => currentRecord[f])
|
||||||
const oldPath = currentRecord[field];
|
.filter((p): p is string => Boolean(p));
|
||||||
if (oldPath) this.deleteFile(oldPath);
|
const newPhotosSet = new Set(finalPhotos.filter(Boolean));
|
||||||
updateData[field] = null; // Set to null in DB
|
|
||||||
|
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) {
|
if (updateTrainingDto.visitDate) {
|
||||||
updateData.visitDate = new Date(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
|
const [updatedRecord] = await this.drizzle
|
||||||
.update(trainingSurveys)
|
.update(trainingSurveys)
|
||||||
.set(updateData)
|
.set(updateData)
|
||||||
@@ -362,9 +377,9 @@ export class TrainingService {
|
|||||||
const record = await this.findOne(id);
|
const record = await this.findOne(id);
|
||||||
|
|
||||||
// Delete associated files
|
// Delete associated files
|
||||||
if (record.photo1) this.deleteFile(record.photo1);
|
if (record.photo1) await this.deleteFile(record.photo1);
|
||||||
if (record.photo2) this.deleteFile(record.photo2);
|
if (record.photo2) await this.deleteFile(record.photo2);
|
||||||
if (record.photo3) this.deleteFile(record.photo3);
|
if (record.photo3) await this.deleteFile(record.photo3);
|
||||||
|
|
||||||
const [deletedRecord] = await this.drizzle
|
const [deletedRecord] = await this.drizzle
|
||||||
.delete(trainingSurveys)
|
.delete(trainingSurveys)
|
||||||
@@ -377,163 +392,296 @@ export class TrainingService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportTemplate(id: number) {
|
// async exportTemplate() {
|
||||||
// Validar que el registro exista
|
|
||||||
const record = await this.findOne(id);
|
|
||||||
if (!record) throw new NotFoundException(`No se encontró el registro`);
|
|
||||||
|
|
||||||
// Formatear fecha y hora
|
// const templatePath = path.join(
|
||||||
const dateObj = new Date(record.visitDate);
|
|
||||||
const fechaFormateada = dateObj.toLocaleDateString('es-ES');
|
|
||||||
const horaFormateada = dateObj.toLocaleTimeString('es-ES', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ruta de la plantilla
|
|
||||||
const templatePath = path.join(
|
|
||||||
__dirname,
|
|
||||||
'export_template',
|
|
||||||
'excel.osp.xlsx',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cargar la plantilla con ExcelJS
|
|
||||||
const workbook = new ExcelJS.Workbook();
|
|
||||||
await workbook.xlsx.readFile(templatePath);
|
|
||||||
const worksheet = workbook.getWorksheet(1); // Usar la primera hoja
|
|
||||||
|
|
||||||
if (!worksheet) {
|
|
||||||
throw new Error('No se pudo encontrar la hoja de trabajo en la plantilla');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Llenar los datos principales
|
|
||||||
worksheet.getCell('A6').value = record.productiveSector;
|
|
||||||
worksheet.getCell('B8').value = record.stateName;
|
|
||||||
worksheet.getCell('E8').value = record.municipalityName;
|
|
||||||
worksheet.getCell('B9').value = record.parishName;
|
|
||||||
worksheet.getCell('D6').value = record.ospName;
|
|
||||||
worksheet.getCell('L5').value = fechaFormateada;
|
|
||||||
worksheet.getCell('L6').value = horaFormateada;
|
|
||||||
worksheet.getCell('B10').value = record.ospAddress;
|
|
||||||
worksheet.getCell('C11').value = record.communeEmail;
|
|
||||||
worksheet.getCell('C12').value = record.communeSpokespersonName;
|
|
||||||
worksheet.getCell('G11').value = record.communeRif;
|
|
||||||
worksheet.getCell('G12').value = record.communeSpokespersonPhone;
|
|
||||||
worksheet.getCell('C13').value = record.siturCodeCommune;
|
|
||||||
worksheet.getCell('G13').value = record.siturCodeCommunalCouncil;
|
|
||||||
worksheet.getCell('G14').value = record.communalCouncilRif;
|
|
||||||
worksheet.getCell('C15').value = record.communalCouncilSpokespersonName;
|
|
||||||
worksheet.getCell('G15').value = record.communalCouncilSpokespersonPhone;
|
|
||||||
worksheet.getCell('C16').value = record.ospType;
|
|
||||||
worksheet.getCell('C17').value = record.ospName;
|
|
||||||
worksheet.getCell('C18').value = record.productiveActivity;
|
|
||||||
worksheet.getCell('C19').value = 'Proveedores';
|
|
||||||
worksheet.getCell('C20').value = record.companyConstitutionYear;
|
|
||||||
worksheet.getCell('C21').value = record.infrastructureMt2;
|
|
||||||
worksheet.getCell('G17').value = record.ospRif;
|
|
||||||
|
|
||||||
worksheet.getCell(record.hasTransport === true ? 'J19' : 'L19').value = 'X';
|
|
||||||
worksheet.getCell(record.structureType === 'CASA' ? 'J20' : 'L20').value =
|
|
||||||
'X';
|
|
||||||
worksheet.getCell(record.isOpenSpace === true ? 'J21' : 'L21').value = 'X';
|
|
||||||
|
|
||||||
worksheet.getCell('A24').value = record.ospResponsibleFullname;
|
|
||||||
worksheet.getCell('C24').value = record.ospResponsibleCedula;
|
|
||||||
worksheet.getCell('E24').value = record.ospResponsiblePhone;
|
|
||||||
|
|
||||||
worksheet.getCell('J24').value = 'N Femenino'; // Placeholder si no hay dato
|
|
||||||
worksheet.getCell('L24').value = 'N Masculino'; // Placeholder si no hay dato
|
|
||||||
|
|
||||||
// const photo1 = record.photo1;
|
|
||||||
// const photo2 = record.photo2;
|
|
||||||
// const photo3 = record.photo3;
|
|
||||||
|
|
||||||
if (record.photo1) {
|
|
||||||
const image = record.photo1.slice(17);
|
|
||||||
const extension = image.split('.')[1];
|
|
||||||
|
|
||||||
// Validar que sea una imagen png, gif o jpeg
|
|
||||||
if (extension === 'png' || extension === 'gif' || extension === 'jpeg') {
|
|
||||||
// Ruta de la imagen
|
|
||||||
const imagePath = path.join(
|
|
||||||
__dirname,
|
|
||||||
'../../../',
|
|
||||||
`uploads/training/${image}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add an image to the workbook from a file buffer
|
|
||||||
const logoId = workbook.addImage({
|
|
||||||
filename: imagePath,
|
|
||||||
extension: extension,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Anchor the image to a specific cell (e.g., A1)
|
|
||||||
worksheet.addImage(logoId, `I7:L17`); // Spans from A1 to C3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// let i = 1;
|
|
||||||
// while (i <= 3) {
|
|
||||||
// const element = record[`photo${i}`];
|
|
||||||
// if (element) {
|
|
||||||
// const image = element.slice(17);
|
|
||||||
// const extension: extensionType = image.split('.')[1];
|
|
||||||
|
|
||||||
// // Validar que sea una imagen png, gif o jpeg
|
|
||||||
// if (extension === 'png' || extension === 'gif' || extension === 'jpeg') {
|
|
||||||
// // Ruta de la imagen
|
|
||||||
// const imagePath = path.join(
|
|
||||||
// __dirname,
|
// __dirname,
|
||||||
// '../../../',
|
// 'export_template',
|
||||||
// `uploads/training/${image}`,
|
// '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',
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// // Add an image to the workbook from a file buffer
|
// // Cargar la plantilla
|
||||||
// const logoId = workbook.addImage({
|
// const book = await XlsxPopulate.fromFileAsync(templatePath);
|
||||||
// filename: imagePath,
|
|
||||||
// extension: extension,
|
// 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',
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// // Anchor the image to a specific cell (e.g., A1)
|
// // Llenar los datos
|
||||||
// worksheet.addImage(logoId, `I7:L17`); // Spans from A1 to C3
|
// book.sheet(0).cell('A6').value(records[0].productiveSector);
|
||||||
// i = 4;
|
// 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();
|
||||||
// }
|
// }
|
||||||
// }
|
|
||||||
// i++;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Listas (Equipos, Materia Prima, Productos)
|
|
||||||
const equipmentList = Array.isArray(record.equipmentList)
|
|
||||||
? record.equipmentList
|
|
||||||
: [];
|
|
||||||
const productionList = Array.isArray(record.productionList)
|
|
||||||
? record.productionList
|
|
||||||
: [];
|
|
||||||
const productList = Array.isArray(record.productList)
|
|
||||||
? record.productList
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Colocar listas empezando en la fila 28
|
|
||||||
equipmentList.forEach((item: any, i: number) => {
|
|
||||||
const row = 28 + i;
|
|
||||||
worksheet.getCell(`A${row}`).value = item.machine;
|
|
||||||
worksheet.getCell(`C${row}`).value = item.quantity;
|
|
||||||
});
|
|
||||||
|
|
||||||
productionList.forEach((item: any, i: number) => {
|
|
||||||
const row = 28 + i;
|
|
||||||
worksheet.getCell(`E${row}`).value = item.rawMaterial;
|
|
||||||
worksheet.getCell(`G${row}`).value = item.quantity;
|
|
||||||
});
|
|
||||||
|
|
||||||
productList.forEach((item: any, i: number) => {
|
|
||||||
const row = 28 + i;
|
|
||||||
worksheet.getCell(`I${row}`).value = item.productName;
|
|
||||||
worksheet.getCell(`J${row}`).value = item.dailyCount;
|
|
||||||
worksheet.getCell(`K${row}`).value = item.weeklyCount;
|
|
||||||
worksheet.getCell(`L${row}`).value = item.monthlyCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
return await workbook.xlsx.writeBuffer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ 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
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
NODE_ENV='development' #development | production
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export default function EditTrainingPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <PageContainer scrollable>
|
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<CreateTrainingForm
|
<CreateTrainingForm
|
||||||
defaultValues={training}
|
defaultValues={training}
|
||||||
@@ -29,6 +28,5 @@ export default function EditTrainingPage() {
|
|||||||
onCancel={() => router.back()}
|
onCancel={() => router.back()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
// </PageContainer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import PageContainer from '@/components/layout/page-container';
|
// import PageContainer from '@/components/layout/page-container';
|
||||||
import { TrainingHeader } from '@/feactures/training/components/training-header';
|
import { TrainingHeader } from '@/feactures/training/components/training-header';
|
||||||
import TrainingList from '@/feactures/training/components/training-list';
|
import TrainingList from '@/feactures/training/components/training-list';
|
||||||
import TrainingTableAction from '@/feactures/training/components/training-tables/training-table-action';
|
import TrainingTableAction from '@/feactures/training/components/training-tables/training-table-action';
|
||||||
@@ -24,8 +24,7 @@ export default async function Page({ searchParams }: PageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// <PageContainer>
|
// <PageContainer>
|
||||||
// <div className="flex flex-1 flex-col space-y-6">
|
<div className="flex flex-1 flex-col space-y-6 p-6">
|
||||||
< div className="p-6 space-y-6" >
|
|
||||||
<TrainingHeader />
|
<TrainingHeader />
|
||||||
<TrainingTableAction />
|
<TrainingTableAction />
|
||||||
<TrainingList
|
<TrainingList
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { auth } from '@/lib/auth';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export default async function Dashboard() {
|
export default async function Dashboard() {
|
||||||
console.log('La sesion es llamada');
|
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ const RootLayout = async ({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}>) => {
|
}>) => {
|
||||||
console.log('La sesion es llamada');
|
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
|||||||
@@ -187,10 +187,9 @@ export const COUNTRY_OPTIONS = [
|
|||||||
'Uruguay',
|
'Uruguay',
|
||||||
'Uzbekistán',
|
'Uzbekistán',
|
||||||
'Vanuatu',
|
'Vanuatu',
|
||||||
'Venezuela',
|
|
||||||
'Vietnam',
|
'Vietnam',
|
||||||
'Yemen',
|
'Yemen',
|
||||||
'Yibuti',
|
'Yibuti',
|
||||||
'Zambia',
|
'Zambia',
|
||||||
'Zimbabue'
|
'Zimbabue',
|
||||||
];
|
];
|
||||||
@@ -34,7 +34,7 @@ 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', 'autoridad'],
|
role: ['admin', 'superadmin'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Encuestas',
|
title: 'Encuestas',
|
||||||
@@ -60,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', 'manager'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
// {
|
// {
|
||||||
@@ -82,7 +82,7 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
shortcut: ['s', 's'],
|
shortcut: ['s', 's'],
|
||||||
url: '/dashboard/estadisticas/socioproductiva',
|
url: '/dashboard/estadisticas/socioproductiva',
|
||||||
icon: 'blocks',
|
icon: 'blocks',
|
||||||
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import { safeFetchApi } from '@/lib/fetch.api';
|
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,7 +18,7 @@ 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í**
|
||||||
@@ -28,7 +29,7 @@ type LoginActionError = {
|
|||||||
// 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');
|
||||||
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// auth/actions/refresh-token-action.ts
|
|
||||||
'use server';
|
'use server';
|
||||||
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
|
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
|
||||||
import {
|
import {
|
||||||
|
|||||||
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(),
|
||||||
|
});
|
||||||
@@ -4,13 +4,19 @@ import { tokensSchema } from './login';
|
|||||||
|
|
||||||
// Esquema para el refresh token
|
// Esquema para el refresh token
|
||||||
export const refreshTokenSchema = z.object({
|
export const refreshTokenSchema = z.object({
|
||||||
user_id: z.number(),
|
refreshToken: z.string(),
|
||||||
token: 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,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="h-4 w-4"/><span className='hidden sm:inline'>Agregar Encuesta</span>
|
<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>
|
</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,15 +50,20 @@ 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">
|
||||||
|
{['superadmin', 'admin'].includes(role ?? '') && (
|
||||||
|
<>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => router.push(`/dashboard/administracion/encuestas/editar/${data.id!}`)}
|
onClick={() =>
|
||||||
|
router.push(
|
||||||
|
`/dashboard/administracion/encuestas/editar/${data.id!}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -82,6 +90,8 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,8 +90,6 @@ export const createTrainingAction = async (
|
|||||||
payloadToSend = rest as any;
|
payloadToSend = rest as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(payloadToSend);
|
|
||||||
|
|
||||||
const [error, data] = await safeFetchApi(
|
const [error, data] = await safeFetchApi(
|
||||||
TrainingMutate,
|
TrainingMutate,
|
||||||
'/training',
|
'/training',
|
||||||
|
|||||||
@@ -20,24 +20,34 @@ import { Label } from '@repo/shadcn/label';
|
|||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { TrainingSchema } from '../schemas/training';
|
||||||
|
|
||||||
|
interface EquipmentItem {
|
||||||
|
machine: string;
|
||||||
|
quantity: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
export function EquipmentList() {
|
export function EquipmentList() {
|
||||||
const { control, register } = useFormContext();
|
const { control, register } = useFormContext<TrainingSchema>();
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: 'equipmentList',
|
name: 'equipmentList',
|
||||||
});
|
});
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [newItem, setNewItem] = useState({
|
const [newItem, setNewItem] = useState<EquipmentItem>({
|
||||||
machine: '',
|
machine: '',
|
||||||
specifications: '',
|
|
||||||
quantity: '',
|
quantity: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = (e: React.MouseEvent) => {
|
||||||
if (newItem.machine && newItem.quantity) {
|
e.preventDefault();
|
||||||
append({ ...newItem, quantity: Number(newItem.quantity) });
|
e.stopPropagation();
|
||||||
setNewItem({ machine: '', specifications: '', quantity: '' });
|
if (newItem.machine.trim()) {
|
||||||
|
append({
|
||||||
|
machine: newItem.machine,
|
||||||
|
quantity: newItem.quantity ? Number(newItem.quantity) : 0,
|
||||||
|
});
|
||||||
|
setNewItem({ machine: '', quantity: '' });
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -48,9 +58,11 @@ export function EquipmentList() {
|
|||||||
<h3 className="text-lg font-medium">Datos del Equipamiento</h3>
|
<h3 className="text-lg font-medium">Datos del Equipamiento</h3>
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">Agregar Maquinaria</Button>
|
<Button variant="outline" type="button">
|
||||||
|
Agregar Maquinaria
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Agregar Maquinaria/Equipo</DialogTitle>
|
<DialogTitle>Agregar Maquinaria/Equipo</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -59,8 +71,9 @@ export function EquipmentList() {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Maquinaria</Label>
|
<Label htmlFor="modal-machine">Maquinaria</Label>
|
||||||
<Input
|
<Input
|
||||||
|
id="modal-machine"
|
||||||
value={newItem.machine}
|
value={newItem.machine}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewItem({ ...newItem, machine: e.target.value })
|
setNewItem({ ...newItem, machine: e.target.value })
|
||||||
@@ -69,18 +82,9 @@ export function EquipmentList() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Especificaciones</Label>
|
<Label htmlFor="modal-quantity">Cantidad</Label>
|
||||||
<Input
|
|
||||||
value={newItem.specifications}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({ ...newItem, specifications: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Especificaciones técnicas"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Cantidad</Label>
|
|
||||||
<Input
|
<Input
|
||||||
|
id="modal-quantity"
|
||||||
type="number"
|
type="number"
|
||||||
value={newItem.quantity}
|
value={newItem.quantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -93,12 +97,17 @@ export function EquipmentList() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={handleAdd}>Guardar</Button>
|
<Button type="button" onClick={handleAdd}>
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -110,7 +119,6 @@ export function EquipmentList() {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Maquinaria</TableHead>
|
<TableHead>Maquinaria</TableHead>
|
||||||
<TableHead>Especificaciones</TableHead>
|
|
||||||
<TableHead>Cantidad</TableHead>
|
<TableHead>Cantidad</TableHead>
|
||||||
<TableHead className="w-[50px]"></TableHead>
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -122,31 +130,27 @@ export function EquipmentList() {
|
|||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
{...register(`equipmentList.${index}.machine`)}
|
{...register(`equipmentList.${index}.machine`)}
|
||||||
|
defaultValue={field.machine ?? ''}
|
||||||
/>
|
/>
|
||||||
{/* @ts-ignore */}
|
|
||||||
{field.machine}
|
{field.machine}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
{...register(`equipmentList.${index}.specifications`)}
|
|
||||||
/>
|
|
||||||
{/* @ts-ignore */}
|
|
||||||
{field.specifications}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
{...register(`equipmentList.${index}.quantity`)}
|
{...register(`equipmentList.${index}.quantity`)}
|
||||||
|
defaultValue={field.quantity ?? ''}
|
||||||
/>
|
/>
|
||||||
{/* @ts-ignore */}
|
|
||||||
{field.quantity}
|
{field.quantity}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => remove(index)}
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -156,7 +160,7 @@ export function EquipmentList() {
|
|||||||
{fields.length === 0 && (
|
{fields.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={4}
|
colSpan={3}
|
||||||
className="text-center text-muted-foreground"
|
className="text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
No hay equipamiento registrado
|
No hay equipamiento registrado
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,3 @@
|
|||||||
import { COUNTRY_OPTIONS } from '@/constants/countries';
|
|
||||||
import {
|
|
||||||
useMunicipalityQuery,
|
|
||||||
useParishQuery,
|
|
||||||
useStateQuery,
|
|
||||||
} from '@/feactures/location/hooks/use-query-location';
|
|
||||||
import { Button } from '@repo/shadcn/button';
|
import { Button } from '@repo/shadcn/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -23,36 +17,18 @@ import {
|
|||||||
} from '@repo/shadcn/components/ui/table';
|
} from '@repo/shadcn/components/ui/table';
|
||||||
import { Input } from '@repo/shadcn/input';
|
import { Input } from '@repo/shadcn/input';
|
||||||
import { Label } from '@repo/shadcn/label';
|
import { Label } from '@repo/shadcn/label';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@repo/shadcn/select';
|
|
||||||
import { SelectSearchable } from '@repo/shadcn/select-searchable';
|
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
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
|
// 1. Definimos la estructura de los datos para que TypeScript no se queje
|
||||||
interface ProductItem {
|
// ProductItem y ProductFormValues locales eliminados en favor de TrainingSchema
|
||||||
productName: string;
|
|
||||||
description: string;
|
|
||||||
dailyCount: string;
|
|
||||||
weeklyCount: string;
|
|
||||||
monthlyCount: string;
|
|
||||||
// ... resto de propiedades opcionales si las necesitas tipar estrictamente
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProductFormValues {
|
|
||||||
productList: ProductItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProductActivityList() {
|
export function ProductActivityList() {
|
||||||
// 2. Pasamos el tipo genérico a useFormContext
|
const { control, register } = useFormContext<TrainingSchema>();
|
||||||
const { control, register } = useFormContext<ProductFormValues>();
|
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@@ -63,91 +39,25 @@ export function ProductActivityList() {
|
|||||||
|
|
||||||
// Modal Form State
|
// Modal Form State
|
||||||
const [newItem, setNewItem] = useState<any>({
|
const [newItem, setNewItem] = useState<any>({
|
||||||
productName: '',
|
|
||||||
description: '',
|
description: '',
|
||||||
dailyCount: '',
|
dailyCount: '',
|
||||||
weeklyCount: '',
|
weeklyCount: '',
|
||||||
monthlyCount: '',
|
monthlyCount: '',
|
||||||
|
|
||||||
// Internal dist
|
|
||||||
internalState: undefined,
|
|
||||||
internalMunicipality: undefined,
|
|
||||||
internalParish: undefined,
|
|
||||||
internalDescription: '',
|
|
||||||
internalQuantity: '',
|
|
||||||
internalUnit: '',
|
|
||||||
|
|
||||||
// External dist
|
|
||||||
externalCountry: '',
|
|
||||||
externalState: undefined,
|
|
||||||
externalMunicipality: undefined,
|
|
||||||
externalParish: undefined,
|
|
||||||
externalCity: '',
|
|
||||||
externalDescription: '',
|
|
||||||
externalQuantity: '',
|
|
||||||
externalUnit: '',
|
|
||||||
|
|
||||||
// Workforce
|
|
||||||
womenCount: '',
|
|
||||||
menCount: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Location logic for Internal Validation
|
|
||||||
const [internalStateId, setInternalStateId] = useState(0);
|
|
||||||
const [internalMuniId, setInternalMuniId] = useState(0);
|
|
||||||
|
|
||||||
const { data: statesData } = useStateQuery();
|
|
||||||
const { data: internalMuniData } = useMunicipalityQuery(internalStateId);
|
|
||||||
const { data: internalParishData } = useParishQuery(internalMuniId);
|
|
||||||
|
|
||||||
// Location logic for External Validation
|
|
||||||
const [externalStateId, setExternalStateId] = useState(0);
|
|
||||||
const [externalMuniId, setExternalMuniId] = useState(0);
|
|
||||||
const { data: externalMuniData } = useMunicipalityQuery(externalStateId);
|
|
||||||
const { data: externalParishData } = useParishQuery(externalMuniId);
|
|
||||||
|
|
||||||
const isVenezuela = newItem.externalCountry === 'Venezuela';
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (newItem.productName) {
|
if (newItem.description) {
|
||||||
append(newItem);
|
append(newItem);
|
||||||
setNewItem({
|
setNewItem({
|
||||||
productName: '',
|
|
||||||
description: '',
|
description: '',
|
||||||
dailyCount: '',
|
dailyCount: '',
|
||||||
weeklyCount: '',
|
weeklyCount: '',
|
||||||
monthlyCount: '',
|
monthlyCount: '',
|
||||||
internalState: undefined,
|
|
||||||
internalMunicipality: undefined,
|
|
||||||
internalParish: undefined,
|
|
||||||
internalDescription: '',
|
|
||||||
internalQuantity: '',
|
|
||||||
internalUnit: '',
|
|
||||||
externalCountry: '',
|
|
||||||
externalState: undefined,
|
|
||||||
externalMunicipality: undefined,
|
|
||||||
externalParish: undefined,
|
|
||||||
externalCity: '',
|
|
||||||
externalDescription: '',
|
|
||||||
externalQuantity: '',
|
|
||||||
externalUnit: '',
|
|
||||||
womenCount: '',
|
|
||||||
menCount: '',
|
|
||||||
});
|
});
|
||||||
setInternalStateId(0);
|
|
||||||
setInternalMuniId(0);
|
|
||||||
setExternalStateId(0);
|
|
||||||
setExternalMuniId(0);
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateOptions = statesData?.data || [];
|
|
||||||
const internalMuniOptions = internalMuniData?.data || [];
|
|
||||||
const internalParishOptions = internalParishData?.data || [];
|
|
||||||
const externalMuniOptions = externalMuniData?.data || [];
|
|
||||||
const externalParishOptions = externalParishData?.data || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@@ -158,7 +68,7 @@ export function ProductActivityList() {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Detalles de Actividad Productiva</DialogTitle>
|
<DialogTitle>Producto Terminado</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
Datos de actividad productiva
|
Datos de actividad productiva
|
||||||
@@ -166,15 +76,6 @@ export function ProductActivityList() {
|
|||||||
<div className="space-y-6 py-4">
|
<div className="space-y-6 py-4">
|
||||||
{/* Basic Info */}
|
{/* Basic Info */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Producto Terminado</Label>
|
|
||||||
<Input
|
|
||||||
value={newItem.productName}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({ ...newItem, productName: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Descripción</Label>
|
<Label>Descripción</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -219,222 +120,6 @@ export function ProductActivityList() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
|
||||||
<h4 className="font-semibold">Distribución Interna</h4>
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Estado</Label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={stateOptions.map((s) => ({
|
|
||||||
value: String(s.id),
|
|
||||||
label: s.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
const id = Number(val);
|
|
||||||
setInternalStateId(id);
|
|
||||||
setNewItem({ ...newItem, internalState: id });
|
|
||||||
}}
|
|
||||||
placeholder="Estado"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Municipio</Label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={internalMuniOptions.map((s) => ({
|
|
||||||
value: String(s.id),
|
|
||||||
label: s.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
const id = Number(val);
|
|
||||||
setInternalMuniId(id);
|
|
||||||
setNewItem({ ...newItem, internalMunicipality: id });
|
|
||||||
}}
|
|
||||||
placeholder="Municipio"
|
|
||||||
disabled={!internalStateId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Parroquia</Label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={internalParishOptions.map((s) => ({
|
|
||||||
value: String(s.id),
|
|
||||||
label: s.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
setNewItem({ ...newItem, internalParish: Number(val) })
|
|
||||||
}
|
|
||||||
placeholder="Parroquia"
|
|
||||||
disabled={!internalMuniId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Breve Descripción</Label>
|
|
||||||
<Input
|
|
||||||
value={newItem.internalDescription}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({
|
|
||||||
...newItem,
|
|
||||||
internalDescription: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Cantidad Numérica (Kg, TON, UNID. LT)</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={newItem.internalQuantity}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({
|
|
||||||
...newItem,
|
|
||||||
internalQuantity: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
<h4 className="font-semibold">Distribución Externa</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>País</Label>
|
|
||||||
<Select
|
|
||||||
value={newItem.externalCountry}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
setNewItem({ ...newItem, externalCountry: val })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Seleccione País" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{/* 3. CORRECCIÓN DEL MAPEO DE PAÍSES Y KEYS */}
|
|
||||||
{COUNTRY_OPTIONS.map((country: string) => (
|
|
||||||
<SelectItem key={country} value={country}>
|
|
||||||
{country}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{!isVenezuela && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Ciudad</Label>
|
|
||||||
<Input
|
|
||||||
value={newItem.externalCity}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({ ...newItem, externalCity: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isVenezuela && (
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Estado</Label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={stateOptions.map((s) => ({
|
|
||||||
value: String(s.id),
|
|
||||||
label: s.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
const id = Number(val);
|
|
||||||
setExternalStateId(id);
|
|
||||||
setNewItem({ ...newItem, externalState: id });
|
|
||||||
}}
|
|
||||||
placeholder="Estado"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Municipio</Label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={externalMuniOptions.map((s) => ({
|
|
||||||
value: String(s.id),
|
|
||||||
label: s.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
const id = Number(val);
|
|
||||||
setExternalMuniId(id);
|
|
||||||
setNewItem({ ...newItem, externalMunicipality: id });
|
|
||||||
}}
|
|
||||||
placeholder="Municipio"
|
|
||||||
disabled={!externalStateId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Parroquia</Label>
|
|
||||||
<SelectSearchable
|
|
||||||
options={externalParishOptions.map((s) => ({
|
|
||||||
value: String(s.id),
|
|
||||||
label: s.name,
|
|
||||||
}))}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
setNewItem({ ...newItem, externalParish: Number(val) })
|
|
||||||
}
|
|
||||||
placeholder="Parroquia"
|
|
||||||
disabled={!externalMuniId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Breve Descripción</Label>
|
|
||||||
<Input
|
|
||||||
value={newItem.externalDescription}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({
|
|
||||||
...newItem,
|
|
||||||
externalDescription: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Cantidad Numérica (Kg, TON, UNID. LT)</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={newItem.externalQuantity}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({
|
|
||||||
...newItem,
|
|
||||||
externalQuantity: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
<h4 className="font-semibold">Mano de Obra</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Mujer (cantidad)</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={newItem.womenCount}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({ ...newItem, womenCount: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Hombre (cantidad)</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={newItem.menCount}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({ ...newItem, menCount: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -455,9 +140,10 @@ export function ProductActivityList() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Producto</TableHead>
|
<TableHead>Producto/Descripción</TableHead>
|
||||||
<TableHead>Descripción</TableHead>
|
<TableHead>Producción Diario</TableHead>
|
||||||
<TableHead>Mensual</TableHead>
|
<TableHead>Producción Semanal</TableHead>
|
||||||
|
<TableHead>Producción Mensual</TableHead>
|
||||||
<TableHead className="w-[50px]"></TableHead>
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -467,13 +153,28 @@ export function ProductActivityList() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
{...register(`productList.${index}.productName`)}
|
{...register(`productList.${index}.description`)}
|
||||||
// field.productName ahora es válido gracias a la interface
|
defaultValue={field.description ?? ''}
|
||||||
value={field.productName}
|
|
||||||
/>
|
/>
|
||||||
{field.productName}
|
<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>
|
||||||
<TableCell>{field.description}</TableCell>
|
<TableCell>{field.dailyCount}</TableCell>
|
||||||
|
<TableCell>{field.weeklyCount}</TableCell>
|
||||||
<TableCell>{field.monthlyCount}</TableCell>
|
<TableCell>{field.monthlyCount}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -17,27 +17,39 @@ import {
|
|||||||
} from '@repo/shadcn/components/ui/table';
|
} from '@repo/shadcn/components/ui/table';
|
||||||
import { Input } from '@repo/shadcn/input';
|
import { Input } from '@repo/shadcn/input';
|
||||||
import { Label } from '@repo/shadcn/label';
|
import { Label } from '@repo/shadcn/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
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() {
|
export function ProductionList() {
|
||||||
const { control, register } = useFormContext();
|
const { control, register } = useFormContext<TrainingSchema>();
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: 'productionList',
|
name: 'productionList',
|
||||||
});
|
});
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [newItem, setNewItem] = useState({
|
const [newItem, setNewItem] = useState({
|
||||||
rawMaterial: '',
|
|
||||||
supplyType: '',
|
supplyType: '',
|
||||||
quantity: '',
|
quantity: '',
|
||||||
|
unit: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = (e: React.MouseEvent) => {
|
||||||
if (newItem.rawMaterial && newItem.quantity) {
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (newItem.supplyType && newItem.quantity && newItem.unit) {
|
||||||
append({ ...newItem, quantity: Number(newItem.quantity) });
|
append({ ...newItem, quantity: Number(newItem.quantity) });
|
||||||
setNewItem({ rawMaterial: '', supplyType: '', quantity: '' });
|
setNewItem({ supplyType: '', quantity: '', unit: '' });
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -50,24 +62,14 @@ export function ProductionList() {
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">Agregar Producción</Button>
|
<Button variant="outline">Agregar Producción</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Agregar Datos de Producción</DialogTitle>
|
<DialogTitle>Materia prima requerida (mensual)</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
Datos de producción
|
Datos de producción
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Materia prima requerida (mensual)</Label>
|
|
||||||
<Input
|
|
||||||
value={newItem.rawMaterial}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNewItem({ ...newItem, rawMaterial: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Descripción de materia prima"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Tipo de Insumo/Rubro</Label>
|
<Label>Tipo de Insumo/Rubro</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -79,26 +81,57 @@ export function ProductionList() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Cantidad Mensual (Kg, TON, UNID. LT)</Label>
|
<Label>Cantidad Mensual</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
|
className="flex-1"
|
||||||
value={newItem.quantity}
|
value={newItem.quantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewItem({ ...newItem, quantity: e.target.value })
|
setNewItem({ ...newItem, quantity: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="0"
|
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>
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={handleAdd}>Guardar</Button>
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAdd}
|
||||||
|
disabled={
|
||||||
|
!newItem.supplyType || !newItem.quantity || !newItem.unit
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -109,7 +142,6 @@ export function ProductionList() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Materia Prima</TableHead>
|
|
||||||
<TableHead>Tipo Insumo</TableHead>
|
<TableHead>Tipo Insumo</TableHead>
|
||||||
<TableHead>Cantidad (Mensual)</TableHead>
|
<TableHead>Cantidad (Mensual)</TableHead>
|
||||||
<TableHead className="w-[50px]"></TableHead>
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
@@ -118,35 +150,36 @@ export function ProductionList() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<TableRow key={field.id}>
|
<TableRow key={field.id}>
|
||||||
<TableCell>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
{...register(`productionList.${index}.rawMaterial`)}
|
|
||||||
/>
|
|
||||||
{/* @ts-ignore */}
|
|
||||||
{field.rawMaterial}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
{...register(`productionList.${index}.supplyType`)}
|
{...register(`productionList.${index}.supplyType`)}
|
||||||
|
defaultValue={field.supplyType ?? ''}
|
||||||
/>
|
/>
|
||||||
{/* @ts-ignore */}
|
|
||||||
{field.supplyType}
|
{field.supplyType}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
{...register(`productionList.${index}.quantity`)}
|
{...register(`productionList.${index}.quantity`)}
|
||||||
|
defaultValue={field.quantity ?? ''}
|
||||||
/>
|
/>
|
||||||
{/* @ts-ignore */}
|
<input
|
||||||
{field.quantity}
|
type="hidden"
|
||||||
|
{...register(`productionList.${index}.unit`)}
|
||||||
|
defaultValue={field.unit ?? ''}
|
||||||
|
/>
|
||||||
|
{field.quantity} {field.unit}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => remove(index)}
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -29,10 +29,18 @@ export default function TrainingList({
|
|||||||
return <DataTableSkeleton columnCount={5} rowCount={initialLimit} />;
|
return <DataTableSkeleton columnCount={5} rowCount={initialLimit} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transformedData =
|
||||||
|
data?.data?.map((item) => ({
|
||||||
|
...item,
|
||||||
|
communeRif: item.communeRif || '',
|
||||||
|
communeSpokespersonName: item.communeSpokespersonName || '',
|
||||||
|
communalCouncilRif: item.communalCouncilRif || '',
|
||||||
|
})) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns({ apiUrl })}
|
columns={columns({ apiUrl })}
|
||||||
data={data?.data || []}
|
data={transformedData}
|
||||||
totalItems={data?.meta.totalCount || 0}
|
totalItems={data?.meta.totalCount || 0}
|
||||||
pageSizeOptions={[10, 20, 30, 40, 50]}
|
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@repo/shadcn/tooltip';
|
} from '@repo/shadcn/tooltip';
|
||||||
import { Edit, Eye, Trash, FileDown } from 'lucide-react';
|
import { Edit, Eye, Trash } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TrainingViewModal } from '../training-view-modal';
|
import { TrainingViewModal } from '../training-view-modal';
|
||||||
@@ -25,6 +26,7 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
|||||||
const [viewOpen, setViewOpen] = useState(false);
|
const [viewOpen, setViewOpen] = useState(false);
|
||||||
const { mutate: deleteTraining } = useDeleteTraining();
|
const { mutate: deleteTraining } = useDeleteTraining();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -38,9 +40,29 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = (id?: number | undefined) => {
|
// Mapear roles a minúsculas para comparación segura
|
||||||
window.open(`${apiUrl}/training/export/${id}`, '_blank');
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -60,6 +82,8 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
|
{/* VER DETALLE: superadmin, admin, autoridad, manager, or owner coordinator */}
|
||||||
|
{(isAdminOrSuper || isOtherAuthorized || (isCoordinator && isOwner)) && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -76,24 +100,10 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
<TooltipProvider>
|
{/* EDITAR: Superadmin, admin OR (coordinator if owner) */}
|
||||||
<Tooltip>
|
{(isAdminOrSuper || (isCoordinator && isOwner)) && (
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleExport(data.id)}
|
|
||||||
>
|
|
||||||
<FileDown className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Exportar Excel</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -112,7 +122,10 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ELIMINAR: Solo superadmin y admin */}
|
||||||
|
{isAdminOrSuper && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -129,6 +142,7 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ export function columns({ apiUrl }: ColumnsProps): ColumnDef<TrainingSchema>[] {
|
|||||||
accessorKey: 'ospType',
|
accessorKey: 'ospType',
|
||||||
header: 'Tipo',
|
header: 'Tipo',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: 'Fecha de creación',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
// console.log(row.getValue('created_at'));
|
||||||
|
const date = row.getValue('created_at') as string;
|
||||||
|
return date ? new Date(date).toLocaleString() : 'N/A';
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'currentStatus',
|
accessorKey: 'currentStatus',
|
||||||
header: 'Estatus',
|
header: 'Estatus',
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
import { Button } from '@repo/shadcn/components/ui/button';
|
import { Button } from '@repo/shadcn/components/ui/button';
|
||||||
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
|
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useTrainingTableFilters } from './use-training-table-filters';
|
import { useTrainingTableFilters } from './use-training-table-filters';
|
||||||
|
|
||||||
export default function TrainingTableAction() {
|
export default function TrainingTableAction() {
|
||||||
const { searchQuery, setPage, setSearchQuery } = useTrainingTableFilters();
|
const { searchQuery, setPage, setSearchQuery } = useTrainingTableFilters();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const role = session?.user.role[0]?.rol;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between mt-4 ">
|
<div className="flex items-center justify-between mt-4 ">
|
||||||
<div className="flex items-center gap-4 flex-grow">
|
<div className="flex items-center gap-4 flex-grow">
|
||||||
@@ -19,6 +22,9 @@ export default function TrainingTableAction() {
|
|||||||
setPage={setPage}
|
setPage={setPage}
|
||||||
/>
|
/>
|
||||||
</div>{' '}
|
</div>{' '}
|
||||||
|
{['superadmin', 'autoridad', 'admin', 'manager', 'coordinators'].includes(
|
||||||
|
role ?? '',
|
||||||
|
) && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => router.push(`/dashboard/formulario/nuevo`)}
|
onClick={() => router.push(`/dashboard/formulario/nuevo`)}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -26,6 +32,7 @@ export default function TrainingTableAction() {
|
|||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span className="hidden md:inline">Nuevo Registro</span>
|
<span className="hidden md:inline">Nuevo Registro</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useMunicipalityQuery,
|
||||||
|
useParishQuery,
|
||||||
|
useStateQuery,
|
||||||
|
} from '@/feactures/location/hooks/use-query-location';
|
||||||
import { Badge } from '@repo/shadcn/badge';
|
import { Badge } from '@repo/shadcn/badge';
|
||||||
import { Button } from '@repo/shadcn/button';
|
import { Button } from '@repo/shadcn/button';
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +22,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@repo/shadcn/components/ui/dialog';
|
} from '@repo/shadcn/components/ui/dialog';
|
||||||
import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area';
|
import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area';
|
||||||
import { Separator } from '@repo/shadcn/components/ui/separator';
|
|
||||||
import {
|
import {
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Factory,
|
Factory,
|
||||||
@@ -28,11 +32,6 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { TrainingSchema } from '../schemas/training';
|
import { TrainingSchema } from '../schemas/training';
|
||||||
import {
|
|
||||||
useMunicipalityQuery,
|
|
||||||
useParishQuery,
|
|
||||||
useStateQuery,
|
|
||||||
} from '@/feactures/location/hooks/use-query-location';
|
|
||||||
|
|
||||||
interface TrainingViewModalProps {
|
interface TrainingViewModalProps {
|
||||||
data: TrainingSchema | null;
|
data: TrainingSchema | null;
|
||||||
@@ -53,7 +52,9 @@ export function TrainingViewModal({
|
|||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const stateName = statesData?.data?.find((s: any) => s.id === data.state)?.name;
|
const stateName = statesData?.data?.find(
|
||||||
|
(s: any) => s.id === data.state,
|
||||||
|
)?.name;
|
||||||
const municipalityName = municipalitiesData?.data?.find(
|
const municipalityName = municipalitiesData?.data?.find(
|
||||||
(m: any) => m.id === data.municipality,
|
(m: any) => m.id === data.municipality,
|
||||||
)?.name;
|
)?.name;
|
||||||
@@ -94,7 +95,7 @@ export function TrainingViewModal({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const BooleanBadge = ({ value }: { value?: boolean }) => (
|
const BooleanBadge = ({ value }: { value?: boolean | null }) => (
|
||||||
<Badge variant={value ? 'default' : 'secondary'}>
|
<Badge variant={value ? 'default' : 'secondary'}>
|
||||||
{value ? 'Sí' : 'No'}
|
{value ? 'Sí' : 'No'}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -127,10 +128,7 @@ export function TrainingViewModal({
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* 1. Datos de la Visita */}
|
{/* 1. Datos de la Visita */}
|
||||||
<Section title="Datos de la Visita">
|
<Section title="Datos de la Visita">
|
||||||
<DetailItem
|
<DetailItem label="Coordinador" value={data.coorFullName} />
|
||||||
label="Coordinador"
|
|
||||||
value={`${data.firstname} ${data.lastname}`}
|
|
||||||
/>
|
|
||||||
<DetailItem label="Teléfono Coord." value={data.coorPhone} />
|
<DetailItem label="Teléfono Coord." value={data.coorPhone} />
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Fecha Visita"
|
label="Fecha Visita"
|
||||||
@@ -207,7 +205,11 @@ export function TrainingViewModal({
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={data.ospGoogleMapsLink}
|
href={
|
||||||
|
data.ospGoogleMapsLink.startsWith('http')
|
||||||
|
? data.ospGoogleMapsLink
|
||||||
|
: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(data.ospGoogleMapsLink)}`
|
||||||
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
@@ -227,74 +229,36 @@ export function TrainingViewModal({
|
|||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-lg flex items-center gap-2">
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
<Package className="h-5 w-5" />
|
<Package className="h-5 w-5" />
|
||||||
Productos y Mano de Obra
|
Productos Registrados
|
||||||
<Badge variant="secondary" className="ml-2">
|
<Badge variant="secondary" className="ml-2">
|
||||||
{data.productList?.length || 0}
|
{data.productList?.length || 0}
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-4">
|
<CardContent className="grid gap-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{data.productList?.map((prod: any, idx: number) => (
|
{data.productList?.map((prod: any, idx: number) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className="bg-muted/40 p-4 rounded-lg border text-sm"
|
className="bg-muted/40 p-4 rounded-lg border text-sm"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start mb-2">
|
<h4 className="font-bold text-base text-primary mb-2">
|
||||||
<h4 className="font-bold text-base text-primary">
|
|
||||||
{prod.productName}
|
|
||||||
</h4>
|
|
||||||
<Badge variant="outline">
|
|
||||||
Mano de obra:{' '}
|
|
||||||
{Number(prod.menCount || 0) +
|
|
||||||
Number(prod.womenCount || 0)}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground mb-3">
|
|
||||||
{prod.description}
|
{prod.description}
|
||||||
</p>
|
</h4>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
|
||||||
<DetailItem label="Diario" value={prod.dailyCount} />
|
<DetailItem label="Diario" value={prod.dailyCount} />
|
||||||
<DetailItem label="Semanal" value={prod.weeklyCount} />
|
|
||||||
<DetailItem label="Mensual" value={prod.monthlyCount} />
|
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Hombres / Mujeres"
|
label="Semanal"
|
||||||
value={`${prod.menCount || 0} / ${prod.womenCount || 0}`}
|
value={prod.weeklyCount}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Mensual"
|
||||||
|
value={prod.monthlyCount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Detalles de distribución si existen */}
|
|
||||||
{(prod.internalQuantity || prod.externalQuantity) && (
|
|
||||||
<>
|
|
||||||
<Separator className="my-2" />
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{prod.internalQuantity && (
|
|
||||||
<div>
|
|
||||||
<span className="text-xs font-bold text-muted-foreground block mb-1">
|
|
||||||
DISTRIBUCIÓN INTERNA
|
|
||||||
</span>
|
|
||||||
<p>Cant: {prod.internalQuantity}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{prod.internalDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{prod.externalQuantity && (
|
|
||||||
<div>
|
|
||||||
<span className="text-xs font-bold text-muted-foreground block mb-1">
|
|
||||||
EXPORTACIÓN ({prod.externalCountry})
|
|
||||||
</span>
|
|
||||||
<p>Cant: {prod.externalQuantity}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{prod.externalDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
{(!data.productList || data.productList.length === 0) && (
|
{(!data.productList || data.productList.length === 0) && (
|
||||||
<p className="text-sm text-muted-foreground italic">
|
<p className="text-sm text-muted-foreground italic">
|
||||||
No hay productos registrados.
|
No hay productos registrados.
|
||||||
@@ -303,6 +267,64 @@ export function TrainingViewModal({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* DISTRIBUCIÓN, EXPORTACIÓN Y MANO DE OBRA */}
|
||||||
|
<Section title="Distribución, Exportación y Mano de Obra">
|
||||||
|
<div className="col-span-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-bold border-b pb-1">
|
||||||
|
Distribución Interna
|
||||||
|
</h4>
|
||||||
|
<DetailItem
|
||||||
|
label="Zona de Distribución"
|
||||||
|
value={data.internalDistributionZone}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-bold border-b pb-1">
|
||||||
|
Mano de Obra
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<DetailItem label="Mujeres" value={data.womenCount} />
|
||||||
|
<DetailItem label="Hombres" value={data.menCount} />
|
||||||
|
<DetailItem
|
||||||
|
label="Total"
|
||||||
|
value={
|
||||||
|
Number(data.womenCount || 0) +
|
||||||
|
Number(data.menCount || 0)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-bold border-b pb-1 flex items-center gap-2">
|
||||||
|
Exportación <BooleanBadge value={data.isExporting} />
|
||||||
|
</h4>
|
||||||
|
{data.isExporting && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<DetailItem label="País" value={data.externalCountry} />
|
||||||
|
<DetailItem label="Ciudad" value={data.externalCity} />
|
||||||
|
<DetailItem
|
||||||
|
label="Descripción"
|
||||||
|
value={data.externalDescription}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<DetailItem
|
||||||
|
label="Cantidad"
|
||||||
|
value={data.externalQuantity}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Unidad"
|
||||||
|
value={data.externalUnit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
{/* EQUIPAMIENTO Y PRODUCCIÓN */}
|
{/* EQUIPAMIENTO Y PRODUCCIÓN */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -360,7 +382,9 @@ export function TrainingViewModal({
|
|||||||
{mat.supplyType}
|
{mat.supplyType}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary">Cant: {mat.quantity}</Badge>
|
<Badge variant="secondary">
|
||||||
|
Cant: {mat.quantity} {mat.unit}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(!data.productionList ||
|
{(!data.productionList ||
|
||||||
@@ -414,12 +438,12 @@ export function TrainingViewModal({
|
|||||||
label="Teléfono"
|
label="Teléfono"
|
||||||
value={data.ospResponsiblePhone}
|
value={data.ospResponsiblePhone}
|
||||||
/>
|
/>
|
||||||
<DetailItem label="Email" value={data.ospResponsibleEmail} />
|
{/* <DetailItem label="Email" value={data.ospResponsibleEmail} />
|
||||||
<DetailItem
|
<DetailItem
|
||||||
label="Carga Familiar"
|
label="Carga Familiar"
|
||||||
value={data.familyBurden}
|
value={data.familyBurden}
|
||||||
/>
|
/>
|
||||||
<DetailItem label="Hijos" value={data.numberOfChildren} />
|
<DetailItem label="Hijos" value={data.numberOfChildren} /> */}
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -463,7 +487,7 @@ export function TrainingViewModal({
|
|||||||
onClick={() => setSelectedImage(photo)}
|
onClick={() => setSelectedImage(photo)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
|
src={`${photo}`}
|
||||||
alt={`Evidencia ${idx + 1}`}
|
alt={`Evidencia ${idx + 1}`}
|
||||||
className="object-cover w-full h-full"
|
className="object-cover w-full h-full"
|
||||||
/>
|
/>
|
||||||
@@ -513,7 +537,7 @@ export function TrainingViewModal({
|
|||||||
</Button>
|
</Button>
|
||||||
{selectedImage && (
|
{selectedImage && (
|
||||||
<img
|
<img
|
||||||
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
|
src={`${selectedImage}`}
|
||||||
alt="Vista ampliada"
|
alt="Vista ampliada"
|
||||||
className="max-w-full max-h-[90vh] object-contain rounded-md"
|
className="max-w-full max-h-[90vh] object-contain rounded-md"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const ACTIVIDAD_PRINCIPAL = {
|
|||||||
PATIOS_PRODUCTIVOS: 'PATIOS PRODUCTIVOS O CONUCOS',
|
PATIOS_PRODUCTIVOS: 'PATIOS PRODUCTIVOS O CONUCOS',
|
||||||
TRANSFORMACION_MATERIA: 'TRANSFORMACION DE LA MATERIA PRIMA',
|
TRANSFORMACION_MATERIA: 'TRANSFORMACION DE LA MATERIA PRIMA',
|
||||||
TEXTIL: 'TALLER DE COFECCION TEXTIL',
|
TEXTIL: 'TALLER DE COFECCION TEXTIL',
|
||||||
CONSTRUCCION: 'CONSTRUCION',
|
CONSTRUCCION: 'CONSTRUCCION',
|
||||||
BIENES_SERVICIOS: 'OFRECER PRODUCTOS DE BIENES Y SERVICIOS',
|
BIENES_SERVICIOS: 'OFRECER PRODUCTOS DE BIENES Y SERVICIOS',
|
||||||
VISITAS_GUIADAS: 'VISITAS GUIADAS',
|
VISITAS_GUIADAS: 'VISITAS GUIADAS',
|
||||||
ALOJAMIENTO: 'ALOJAMIENTO',
|
ALOJAMIENTO: 'ALOJAMIENTO',
|
||||||
@@ -128,6 +128,8 @@ export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
|
|||||||
'ELABORACION DE ACEITE COMESTIBLE',
|
'ELABORACION DE ACEITE COMESTIBLE',
|
||||||
'FABRICA DE HIELO',
|
'FABRICA DE HIELO',
|
||||||
'ELABORACION DE PAPELON',
|
'ELABORACION DE PAPELON',
|
||||||
|
'TORREFACTORA DE CÁFE',
|
||||||
|
'ESPULPADORA DE TOMATES Y FRUTAS',
|
||||||
'ARTESANIAS',
|
'ARTESANIAS',
|
||||||
],
|
],
|
||||||
[ACTIVIDAD_PRINCIPAL.TEXTIL]: [
|
[ACTIVIDAD_PRINCIPAL.TEXTIL]: [
|
||||||
|
|||||||
@@ -3,82 +3,78 @@ import { z } from 'zod';
|
|||||||
// 1. Definimos el esquema de un item individual de la lista de productos
|
// 1. Definimos el esquema de un item individual de la lista de productos
|
||||||
// Basado en los campos que usaste en ProductActivityList
|
// Basado en los campos que usaste en ProductActivityList
|
||||||
const productItemSchema = z.object({
|
const productItemSchema = z.object({
|
||||||
productName: z.string(),
|
description: z.string().optional().nullable(),
|
||||||
description: z.string().optional(),
|
dailyCount: z.coerce.string().or(z.number()).optional().nullable(),
|
||||||
dailyCount: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
|
weeklyCount: z.coerce.string().or(z.number()).optional().nullable(),
|
||||||
weeklyCount: z.coerce.string().or(z.number()).optional(),
|
monthlyCount: z.coerce.string().or(z.number()).optional().nullable(),
|
||||||
monthlyCount: z.coerce.string().or(z.number()).optional(),
|
|
||||||
|
|
||||||
// Distribución Interna
|
|
||||||
internalState: z.number().optional(),
|
|
||||||
internalMunicipality: z.number().optional(),
|
|
||||||
internalParish: z.number().optional(),
|
|
||||||
internalDescription: z.string().optional(),
|
|
||||||
internalQuantity: z.coerce.string().or(z.number()).optional(),
|
|
||||||
|
|
||||||
// Distribución Externa
|
|
||||||
externalCountry: z.string().optional(),
|
|
||||||
externalState: z.number().optional(),
|
|
||||||
externalMunicipality: z.number().optional(),
|
|
||||||
externalParish: z.number().optional(),
|
|
||||||
externalCity: z.string().optional(),
|
|
||||||
externalDescription: z.string().optional(),
|
|
||||||
externalQuantity: z.coerce.string().or(z.number()).optional(),
|
|
||||||
|
|
||||||
// Mano de obra
|
|
||||||
womenCount: z.coerce.string().or(z.number()).optional(),
|
|
||||||
menCount: z.coerce.string().or(z.number()).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const productionItemSchema = z.object({
|
const productionItemSchema = z.object({
|
||||||
rawMaterial: z.string(),
|
supplyType: z.string().optional().nullable(),
|
||||||
supplyType: z.string().optional(),
|
quantity: z.coerce.string().or(z.number()).optional().nullable(),
|
||||||
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
|
unit: z.string().min(1, { message: 'Unidad es requerida' }).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const equipmentItemSchema = z.object({
|
const equipmentItemSchema = z.object({
|
||||||
machine: z.string(),
|
machine: z.string().nullable(),
|
||||||
specifications: z.string().optional(),
|
quantity: z.coerce.string().or(z.number()).optional().nullable(),
|
||||||
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const trainingSchema = z.object({
|
export const trainingSchema = z.object({
|
||||||
//Datos de la visita
|
//Datos de la visita
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
firstname: z.string().min(1, { message: 'Nombre es requerido' }),
|
coorFullName: z
|
||||||
lastname: z.string().min(1, { message: 'Apellido es requerido' }),
|
.string()
|
||||||
coorPhone: z.string().optional().nullable(),
|
.min(1, { message: 'Nombre del coordinador es requerido' }),
|
||||||
|
coorPhone: z.string().refine((val) => /^(04|02)\d{9}$/.test(val), {
|
||||||
|
message: 'El teléfono debe tener 11 dígitos y comenzar con 04 o 02',
|
||||||
|
}),
|
||||||
visitDate: z
|
visitDate: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Fecha y hora de visita es requerida' }),
|
.min(1, { message: 'Fecha y hora de visita es requerida' }),
|
||||||
|
|
||||||
//Datos de la organización socioproductiva (OSP)
|
//Datos de la organización socioproductiva (OSP)
|
||||||
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
|
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
|
||||||
ecoSector: z.string().optional().or(z.literal('')),
|
ecoSector: z.string({ message: 'Sector Económico es requerido' }),
|
||||||
productiveSector: z.string().optional().or(z.literal('')),
|
productiveSector: z.string({ message: 'Sector Productivo es requerido' }),
|
||||||
centralProductiveActivity: z.string().optional().or(z.literal('')),
|
centralProductiveActivity: z.string({
|
||||||
mainProductiveActivity: z.string().optional().or(z.literal('')),
|
message: 'Actividad Central Productiva es requerido',
|
||||||
productiveActivity: z
|
}),
|
||||||
.string()
|
mainProductiveActivity: z.string({
|
||||||
.min(1, { message: 'Actividad productiva es requerida' }),
|
message: 'Actividad Productiva Principal es requerida',
|
||||||
ospRif: z.string().optional().or(z.literal('')),
|
}),
|
||||||
ospName: z.string().min(1, { message: 'Nombre de la OSP es requerido' }),
|
productiveActivity: z.string({
|
||||||
|
message: 'Actividad Productiva es requerida',
|
||||||
|
}),
|
||||||
|
ospRif: z.string().optional().or(z.literal('')).nullable(),
|
||||||
|
ospName: z.string().optional().or(z.literal('')).nullable(),
|
||||||
companyConstitutionYear: z.coerce
|
companyConstitutionYear: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(1900, { message: 'Año inválido' }),
|
.min(1900, { message: 'Año inválido' })
|
||||||
|
.nullable(),
|
||||||
currentStatus: z
|
currentStatus: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Estatus actual es requerido' })
|
.min(1, { message: 'Estatus actual es requerido' })
|
||||||
.default('ACTIVA'),
|
.default('ACTIVA'),
|
||||||
infrastructureMt2: z.string().optional().or(z.literal('')),
|
infrastructureMt2: z.string({ message: 'Infraestructura es requerida' }),
|
||||||
hasTransport: z
|
hasTransport: z
|
||||||
.preprocess((val) => val === 'true' || val === true, z.boolean())
|
.preprocess(
|
||||||
.optional(),
|
(val) => val === 'true' || val === true || val === 1 || val === '1',
|
||||||
structureType: z.string().optional().or(z.literal('')),
|
z.boolean(),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.default(false),
|
||||||
|
structureType: z.string({ message: 'Tipo de estructura es requerido' }),
|
||||||
isOpenSpace: z
|
isOpenSpace: z
|
||||||
.preprocess((val) => val === 'true' || val === true, z.boolean())
|
.preprocess(
|
||||||
.optional(),
|
(val) => val === 'true' || val === true || val === 1 || val === '1',
|
||||||
paralysisReason: z.string().optional().default(''),
|
z.boolean(),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.default(false),
|
||||||
|
paralysisReason: z.string().optional().nullable(),
|
||||||
|
|
||||||
//Datos del Equipamiento
|
//Datos del Equipamiento
|
||||||
equipmentList: z.array(equipmentItemSchema).optional().default([]),
|
equipmentList: z.array(equipmentItemSchema).optional().default([]),
|
||||||
@@ -89,37 +85,84 @@ export const trainingSchema = z.object({
|
|||||||
// Datos de Actividad Productiva
|
// Datos de Actividad Productiva
|
||||||
productList: z.array(productItemSchema).optional().default([]),
|
productList: z.array(productItemSchema).optional().default([]),
|
||||||
|
|
||||||
|
// Distribución y Exportación
|
||||||
|
internalDistributionZone: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Zona de distribución es requerida' }),
|
||||||
|
isExporting: z
|
||||||
|
.preprocess(
|
||||||
|
(val) => val === 'true' || val === true || val === 1 || val === '1',
|
||||||
|
z.boolean(),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.default(false),
|
||||||
|
externalCountry: z.string().optional().nullable(),
|
||||||
|
externalCity: z.string().optional().nullable(),
|
||||||
|
externalDescription: z.string().optional().nullable(),
|
||||||
|
externalQuantity: z.coerce.string().or(z.number()).optional().nullable(),
|
||||||
|
externalUnit: z.string().optional().nullable(),
|
||||||
|
|
||||||
|
// Mano de obra
|
||||||
|
womenCount: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'Cantidad de mujeres es requerida' }),
|
||||||
|
menCount: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'Cantidad de hombres es requerida' }),
|
||||||
|
|
||||||
//Detalles de la ubicación
|
//Detalles de la ubicación
|
||||||
ospAddress: z
|
ospAddress: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Dirección de la OSP es requerida' }),
|
.min(1, { message: 'Dirección de la OSP es requerida' }),
|
||||||
ospGoogleMapsLink: z.string().optional().or(z.literal('')),
|
ospGoogleMapsLink: z.string().optional().or(z.literal('')).nullable(),
|
||||||
communeName: z.string().optional().or(z.literal('')),
|
communeName: z
|
||||||
siturCodeCommune: z.string().optional().or(z.literal('')),
|
.string()
|
||||||
communeRif: z.string().optional().or(z.literal('')),
|
.min(1, { message: 'Nombre de la comuna es requerida' }),
|
||||||
communeSpokespersonName: z.string().optional().or(z.literal('')),
|
siturCodeCommune: z
|
||||||
communeSpokespersonCedula: z.string().optional().or(z.literal('')),
|
.string()
|
||||||
communeSpokespersonRif: z.string().optional().or(z.literal('')),
|
.min(1, { message: 'Código SITUR de la comuna es requerida' }),
|
||||||
communeSpokespersonPhone: z.string().optional().or(z.literal('')),
|
communeRif: z.string().min(1, { message: 'Rif de la comuna es requerida' }),
|
||||||
|
communeSpokespersonName: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Nombre del vocero de la comuna es requerido' }),
|
||||||
|
communeSpokespersonPhone: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.or(z.literal(''))
|
||||||
|
.refine((val) => !val || /^(04|02)\d{9}$/.test(val), {
|
||||||
|
message: 'El teléfono debe tener 11 dígitos y comenzar con 04 o 02',
|
||||||
|
}),
|
||||||
communeEmail: z
|
communeEmail: z
|
||||||
.string()
|
.string()
|
||||||
.email({ message: 'Correo electrónico de la Comuna inválido' })
|
.email({ message: 'Correo electrónico de la Comuna inválido' })
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal('')),
|
.or(z.literal(''))
|
||||||
|
.nullable(),
|
||||||
communalCouncil: z
|
communalCouncil: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Consejo Comunal es requerido' }),
|
.min(1, { message: 'Consejo Comunal es requerido' }),
|
||||||
siturCodeCommunalCouncil: z.string().optional().or(z.literal('')),
|
siturCodeCommunalCouncil: z
|
||||||
communalCouncilRif: z.string().optional().or(z.literal('')),
|
.string()
|
||||||
communalCouncilSpokespersonName: z.string().optional().or(z.literal('')),
|
.min(1, { message: 'Código SITUR del Consejo Comunal es requerido' }),
|
||||||
communalCouncilSpokespersonCedula: z.string().optional().or(z.literal('')),
|
communalCouncilRif: z
|
||||||
communalCouncilSpokespersonRif: z.string().optional().or(z.literal('')),
|
.string()
|
||||||
communalCouncilSpokespersonPhone: z.string().optional().or(z.literal('')),
|
.min(1, { message: 'Rif del Consejo Comunal es requerido' }),
|
||||||
|
communalCouncilSpokespersonName: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Nombre del vocero es requerido' }),
|
||||||
|
communalCouncilSpokespersonPhone: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.or(z.literal(''))
|
||||||
|
.refine((val) => !val || /^(04|02)\d{9}$/.test(val), {
|
||||||
|
message: 'El teléfono debe tener 11 dígitos y comenzar con 04 o 02',
|
||||||
|
}),
|
||||||
communalCouncilEmail: z
|
communalCouncilEmail: z
|
||||||
.string()
|
.string()
|
||||||
.email({ message: 'Correo electrónico del Consejo Comunal inválido' })
|
.email({ message: 'Correo electrónico del Consejo Comunal inválido' })
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal('')),
|
.or(z.literal(''))
|
||||||
|
.nullable(),
|
||||||
|
|
||||||
//Datos del Responsable OSP
|
//Datos del Responsable OSP
|
||||||
ospResponsibleCedula: z
|
ospResponsibleCedula: z
|
||||||
@@ -128,46 +171,152 @@ export const trainingSchema = z.object({
|
|||||||
ospResponsibleFullname: z
|
ospResponsibleFullname: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Nombre del responsable es requerido' }),
|
.min(1, { message: 'Nombre del responsable es requerido' }),
|
||||||
ospResponsibleRif: z
|
ospResponsibleRif: z.string().optional().nullable(),
|
||||||
.string()
|
civilState: z.string().optional().nullable(),
|
||||||
.min(1, { message: 'RIF del responsable es requerido' }),
|
|
||||||
civilState: z.string().min(1, { message: 'Estado civil es requerido' }),
|
|
||||||
ospResponsiblePhone: z
|
ospResponsiblePhone: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Teléfono del responsable es requerido' }),
|
.min(1, { message: 'Teléfono del responsable es requerido' })
|
||||||
|
.regex(/^(04|02)\d{9}$/, {
|
||||||
|
message: 'El teléfono debe tener 11 dígitos y comenzar con 04 o 02',
|
||||||
|
}),
|
||||||
ospResponsibleEmail: z
|
ospResponsibleEmail: z
|
||||||
.string()
|
.string()
|
||||||
.email({ message: 'Correo electrónico inválido' }),
|
.email({ message: 'Correo electrónico inválido' })
|
||||||
familyBurden: z.coerce
|
.optional()
|
||||||
.number()
|
.or(z.literal(''))
|
||||||
.min(0, { message: 'Carga familiar requerida' }),
|
.nullable(),
|
||||||
numberOfChildren: z.coerce
|
|
||||||
.number()
|
familyBurden: z.coerce.number().optional(),
|
||||||
.min(0, { message: 'Número de hijos requerido' }),
|
numberOfChildren: z.coerce.number().optional(),
|
||||||
|
|
||||||
//Datos adicionales
|
//Datos adicionales
|
||||||
generalObservations: z.string().optional().default(''),
|
generalObservations: z.string().optional().nullable(),
|
||||||
|
|
||||||
//IMAGENES
|
//IMAGENES
|
||||||
files: z.any().optional(),
|
files: z.any().optional(),
|
||||||
|
|
||||||
//no se envia la backend al crear ni editar el formulario
|
//no se envia la backend al crear ni editar el formulario
|
||||||
state: z.number().optional().nullable(),
|
state: z.number({ message: 'El estado es requerido' }).nullable(),
|
||||||
municipality: z.number().optional().nullable(),
|
municipality: z.number({ message: 'Municipio es requerido' }).nullable(),
|
||||||
parish: z.number().optional().nullable(),
|
parish: z.number({ message: 'Parroquia es requerido' }).nullable(),
|
||||||
coorState: z.number().optional().nullable(),
|
coorState: z.number().optional().nullable(),
|
||||||
coorMunicipality: z.number().optional().nullable(),
|
coorMunicipality: z.number().optional().nullable(),
|
||||||
coorParish: z.number().optional().nullable(),
|
coorParish: z.number().optional().nullable(),
|
||||||
photo1: z.string().optional().nullable(),
|
photo1: z.string().optional().nullable(),
|
||||||
photo2: z.string().optional().nullable(),
|
photo2: z.string().optional().nullable(),
|
||||||
photo3: z.string().optional().nullable(),
|
photo3: z.string().optional().nullable(),
|
||||||
|
createdBy: z.number().optional().nullable(),
|
||||||
|
updatedBy: z.number().optional().nullable(),
|
||||||
|
created_at: z.string().optional().nullable(),
|
||||||
|
updated_at: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TrainingSchema = z.infer<typeof trainingSchema>;
|
export type TrainingSchema = z.infer<typeof trainingSchema>;
|
||||||
|
|
||||||
|
export const getTrainingSchema = z.object({
|
||||||
|
//Datos de la visita
|
||||||
|
id: z.number().optional(),
|
||||||
|
coorFullName: z.string(),
|
||||||
|
coorPhone: z.string(),
|
||||||
|
visitDate: z.string(),
|
||||||
|
//Datos de la organización socioproductiva (OSP)
|
||||||
|
ospType: z.string(),
|
||||||
|
ecoSector: z.string(),
|
||||||
|
productiveSector: z.string(),
|
||||||
|
centralProductiveActivity: z.string(),
|
||||||
|
mainProductiveActivity: z.string(),
|
||||||
|
productiveActivity: z.string(),
|
||||||
|
ospRif: z.string().optional().or(z.literal('')).nullable(),
|
||||||
|
ospName: z.string().optional().or(z.literal('')).nullable(),
|
||||||
|
companyConstitutionYear: z.coerce.number(),
|
||||||
|
currentStatus: z.string(),
|
||||||
|
infrastructureMt2: z.string(),
|
||||||
|
hasTransport: z
|
||||||
|
.preprocess(
|
||||||
|
(val) => val === 'true' || val === true || val === 1 || val === '1',
|
||||||
|
z.boolean(),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.default(false),
|
||||||
|
structureType: z.string(),
|
||||||
|
isOpenSpace: z
|
||||||
|
.preprocess(
|
||||||
|
(val) => val === 'true' || val === true || val === 1 || val === '1',
|
||||||
|
z.boolean(),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.default(false),
|
||||||
|
paralysisReason: z.string().optional().nullable(),
|
||||||
|
//Datos del Equipamiento
|
||||||
|
equipmentList: z.array(equipmentItemSchema).optional().default([]),
|
||||||
|
//Datos de Producción
|
||||||
|
productionList: z.array(productionItemSchema).optional().default([]),
|
||||||
|
// Datos de Actividad Productiva
|
||||||
|
productList: z.array(productItemSchema).optional().default([]),
|
||||||
|
// Distribución y Exportación
|
||||||
|
internalDistributionZone: z.string(),
|
||||||
|
isExporting: z
|
||||||
|
.preprocess(
|
||||||
|
(val) => val === 'true' || val === true || val === 1 || val === '1',
|
||||||
|
z.boolean(),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.default(false),
|
||||||
|
externalCountry: z.string().optional().nullable(),
|
||||||
|
externalCity: z.string().optional().nullable(),
|
||||||
|
externalDescription: z.string().optional().nullable(),
|
||||||
|
externalQuantity: z.coerce.string().or(z.number()).optional().nullable(),
|
||||||
|
externalUnit: z.string().optional().nullable(),
|
||||||
|
// Mano de obra
|
||||||
|
womenCount: z.coerce.number(),
|
||||||
|
menCount: z.coerce.number(),
|
||||||
|
//Detalles de la ubicación
|
||||||
|
ospAddress: z.string(),
|
||||||
|
ospGoogleMapsLink: z.string().optional().or(z.literal('')).nullable(),
|
||||||
|
communeName: z.string(),
|
||||||
|
siturCodeCommune: z.string(),
|
||||||
|
communeRif: z.string().or(z.literal('')).nullable(),
|
||||||
|
communeSpokespersonName: z.string().or(z.literal('')).nullable(),
|
||||||
|
communeSpokespersonPhone: z.string(),
|
||||||
|
communeEmail: z.string().optional().or(z.literal('')).nullable(),
|
||||||
|
communalCouncil: z.string(),
|
||||||
|
siturCodeCommunalCouncil: z.string(),
|
||||||
|
communalCouncilRif: z.string().optional(),
|
||||||
|
communalCouncilSpokespersonName: z.string(),
|
||||||
|
communalCouncilSpokespersonPhone: z.string(),
|
||||||
|
communalCouncilEmail: z.string(),
|
||||||
|
//Datos del Responsable OSP
|
||||||
|
ospResponsibleCedula: z.string(),
|
||||||
|
ospResponsibleFullname: z.string(),
|
||||||
|
ospResponsibleRif: z.string().optional().nullable(),
|
||||||
|
civilState: z.string().optional().nullable(),
|
||||||
|
ospResponsiblePhone: z.string(),
|
||||||
|
ospResponsibleEmail: z.string(),
|
||||||
|
familyBurden: z.coerce.number().optional(),
|
||||||
|
numberOfChildren: z.coerce.number().optional(),
|
||||||
|
//Datos adicionales
|
||||||
|
generalObservations: z.string().optional().nullable(),
|
||||||
|
//no se envia la backend al crear ni editar el formulario
|
||||||
|
state: z.number().nullable(),
|
||||||
|
municipality: z.number().nullable(),
|
||||||
|
parish: z.number().nullable(),
|
||||||
|
coorState: z.number().optional().nullable(),
|
||||||
|
coorMunicipality: z.number().optional().nullable(),
|
||||||
|
coorParish: z.number().optional().nullable(),
|
||||||
|
photo1: z.string().optional().nullable(),
|
||||||
|
photo2: z.string().optional().nullable(),
|
||||||
|
photo3: z.string().optional().nullable(),
|
||||||
|
createdBy: z.number().optional().nullable(),
|
||||||
|
updatedBy: z.number().optional().nullable(),
|
||||||
|
created_at: z.string().optional().nullable(),
|
||||||
|
updated_at: z.string().optional().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
export const trainingApiResponseSchema = z.object({
|
export const trainingApiResponseSchema = z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
data: z.array(trainingSchema),
|
data: z.array(getTrainingSchema),
|
||||||
meta: z.object({
|
meta: z.object({
|
||||||
page: z.number(),
|
page: z.number(),
|
||||||
limit: z.number(),
|
limit: z.number(),
|
||||||
|
|||||||
74
apps/web/lib/auth-token.ts
Normal file
74
apps/web/lib/auth-token.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { resfreshTokenAction } from '@/feactures/auth/actions/refresh-token-action';
|
||||||
|
import { auth } from '@/lib/auth';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { cache } from 'react';
|
||||||
|
|
||||||
|
export const getValidAccessToken = cache(async () => {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.access_token) {
|
||||||
|
// console.log('No hay Access Token');
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// console.log('Si hay Access Token');
|
||||||
|
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
// Restamos 10s para tener margen de seguridad
|
||||||
|
const isValid = (session.access_expire_in as number) - 10 > now;
|
||||||
|
|
||||||
|
// A. Si es válido, lo retornamos directo
|
||||||
|
if (isValid) return session.access_token;
|
||||||
|
// console.log('Access Token Expiró');
|
||||||
|
|
||||||
|
// B. Si expiró, buscamos la cookie
|
||||||
|
const cookieStore = cookies();
|
||||||
|
const cookie = await cookieStore
|
||||||
|
const refreshToken = cookie.get('refresh_token')?.value;
|
||||||
|
const teaToken = cookie.get('tea_token')?.value;
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
// console.log('No hay Refresh Token');
|
||||||
|
// Si no hay refres pero si access token pero ya expiro borrar la cookie para forzar cierre de session
|
||||||
|
(await cookieStore).delete('authjs.session-token');// comentar si por algun motivo da error
|
||||||
|
return null
|
||||||
|
} // No hay refresh token, fin del juego
|
||||||
|
// console.log('Si hay Refresh Token');
|
||||||
|
|
||||||
|
if (teaToken) {
|
||||||
|
return teaToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// C. Intentamos refrescar
|
||||||
|
const newTokens = await resfreshTokenAction({ refreshToken });
|
||||||
|
|
||||||
|
if (!newTokens) {
|
||||||
|
// console.log('No hay token nuevo');
|
||||||
|
// Si falla el refresh (token revocado o expirado), borramos cookies
|
||||||
|
(await cookieStore).delete('refresh_token');
|
||||||
|
(await cookieStore).delete('authjs.session-token');// comentar si por algun motivo da error
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// console.log('Si hay token nuevo');
|
||||||
|
|
||||||
|
// console.log('Guardamos refresh');
|
||||||
|
// D. Guardamos el nuevo refresh token en cookie y retornamos el access token
|
||||||
|
(await cookieStore).set('refresh_token', newTokens.refresh_token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
maxAge: 7 * 24 * 60 * 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log('guardamo tea');
|
||||||
|
(await cookieStore).set('tea_token', newTokens.access_token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
maxAge: 7 * 24 * 60 * 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newTokens.access_token;
|
||||||
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
|
// lib/auth.config.ts
|
||||||
import { SignInAction } from '@/feactures/auth/actions/login-action';
|
import { SignInAction } from '@/feactures/auth/actions/login-action';
|
||||||
import { resfreshTokenAction } from '@/feactures/auth/actions/refresh-token-action';
|
import { logoutAction } from '@/feactures/auth/actions/logout-action';
|
||||||
import { CredentialsSignin, NextAuthConfig, Session, User } from 'next-auth';
|
import { CredentialsSignin, NextAuthConfig, Session, User } from 'next-auth';
|
||||||
// import { DefaultJWT } from 'next-auth/jwt';
|
import { DefaultJWT, JWT } from 'next-auth/jwt';
|
||||||
import CredentialProvider from 'next-auth/providers/credentials';
|
import CredentialProvider from 'next-auth/providers/credentials';
|
||||||
|
|
||||||
|
|
||||||
// Define los tipos para tus respuestas de SignInAction
|
// Define los tipos para tus respuestas de SignInAction
|
||||||
interface SignInSuccessResponse {
|
interface SignInSuccessResponse {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -57,8 +57,10 @@ const authConfig: NextAuthConfig = {
|
|||||||
|
|
||||||
// **NUEVO: Manejar el caso `null` primero**
|
// **NUEVO: Manejar el caso `null` primero**
|
||||||
if (response === null) {
|
if (response === null) {
|
||||||
console.error("SignInAction returned null, indicating a potential issue before API call or generic error.");
|
console.error(
|
||||||
throw new CredentialsSignin("Error de inicio de sesión inesperado.");
|
'SignInAction returned null, indicating a potential issue before API call or generic error.',
|
||||||
|
);
|
||||||
|
throw new CredentialsSignin('Error de inicio de sesión inesperado.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tipo Guarda: Verificar la respuesta de error
|
// Tipo Guarda: Verificar la respuesta de error
|
||||||
@@ -69,15 +71,19 @@ const authConfig: NextAuthConfig = {
|
|||||||
response.type === 'UNKNOWN_ERROR') // Incluye todos los tipos de error posibles
|
response.type === 'UNKNOWN_ERROR') // Incluye todos los tipos de error posibles
|
||||||
) {
|
) {
|
||||||
// Si es un error, lánzalo. Este camino termina aquí.
|
// Si es un error, lánzalo. Este camino termina aquí.
|
||||||
throw new CredentialsSignin("Error en la API:" + response.message);
|
throw new CredentialsSignin('Error en la API:' + response.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!('user' in response)) {
|
if (!('user' in response)) {
|
||||||
// Esto solo ocurriría si SignInAction devolvió un objeto que no es null,
|
// Esto solo ocurriría si SignInAction devolvió un objeto que no es null,
|
||||||
// no es un error conocido por 'type', PERO tampoco tiene la propiedad 'user'.
|
// no es un error conocido por 'type', PERO tampoco tiene la propiedad 'user'.
|
||||||
// Es un caso de respuesta inesperada del API.
|
// Es un caso de respuesta inesperada del API.
|
||||||
console.error("Respuesta de SignInAction con formato inesperado: falta la propiedad 'user'.");
|
console.error(
|
||||||
throw new CredentialsSignin("Error en el formato de la respuesta del servidor.");
|
"Respuesta de SignInAction con formato inesperado: falta la propiedad 'user'.",
|
||||||
|
);
|
||||||
|
throw new CredentialsSignin(
|
||||||
|
'Error en el formato de la respuesta del servidor.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -88,8 +94,6 @@ const authConfig: NextAuthConfig = {
|
|||||||
role: response?.user.rol ?? [], // Add role array
|
role: response?.user.rol ?? [], // Add role array
|
||||||
access_token: response?.tokens.access_token ?? '',
|
access_token: response?.tokens.access_token ?? '',
|
||||||
access_expire_in: response?.tokens.access_expire_in ?? 0,
|
access_expire_in: response?.tokens.access_expire_in ?? 0,
|
||||||
refresh_token: response?.tokens.refresh_token ?? '',
|
|
||||||
refresh_expire_in: response?.tokens.refresh_expire_in ?? 0,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -98,7 +102,7 @@ const authConfig: NextAuthConfig = {
|
|||||||
signIn: '/', //sigin page
|
signIn: '/', //sigin page
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user }: { user: User, token: any }) {
|
async jwt({ token, user }: { user: User; token: any }) {
|
||||||
// 1. Manejar el inicio de sesión inicial
|
// 1. Manejar el inicio de sesión inicial
|
||||||
// El `user` solo se proporciona en el primer inicio de sesión.
|
// El `user` solo se proporciona en el primer inicio de sesión.
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -110,54 +114,14 @@ const authConfig: NextAuthConfig = {
|
|||||||
role: user.role,
|
role: user.role,
|
||||||
access_token: user.access_token,
|
access_token: user.access_token,
|
||||||
access_expire_in: user.access_expire_in,
|
access_expire_in: user.access_expire_in,
|
||||||
refresh_token: user.refresh_token,
|
};
|
||||||
refresh_expire_in: user.refresh_expire_in
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Si no es un nuevo login, verificar la expiración del token
|
|
||||||
const now = Math.floor(Date.now() / 1000); // Usar Math.floor para un número entero
|
|
||||||
|
|
||||||
// Verificar si el token de acceso aún es válido
|
|
||||||
if (now < (token.access_expire_in as number)) {
|
|
||||||
return token; // Si no ha expirado, no hacer nada y devolver el token actual
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Si el token de acceso ha expirado, verificar el refresh token
|
|
||||||
if (now > (token.refresh_expire_in as number)) {
|
|
||||||
return null; // Forzar el logout al devolver null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Si el token de acceso ha expirado pero el refresh token es válido, renovar
|
|
||||||
console.log("Renovando token de acceso...");
|
|
||||||
try {
|
|
||||||
const refresh_token = { token: token.refresh_token as string, user_id: Number(token.id) as number }
|
|
||||||
|
|
||||||
const res = await resfreshTokenAction(refresh_token);
|
|
||||||
|
|
||||||
if (!res || !res.tokens) {
|
|
||||||
throw new Error('Fallo en la respuesta de la API de refresco.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualizar el token directamente con los nuevos valores
|
|
||||||
token.access_token = res.tokens.access_token;
|
|
||||||
token.access_expire_in = res.tokens.access_expire_in;
|
|
||||||
token.refresh_token = res.tokens.refresh_token;
|
|
||||||
token.refresh_expire_in = res.tokens.refresh_expire_in;
|
|
||||||
|
|
||||||
console.log("Token renovado exitosamente.");
|
|
||||||
return token;
|
return token;
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return null; // Fallo al renovar, forzar logout
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async session({ session, token }: { session: Session; token: any }) {
|
async session({ session, token }: { session: Session; token: DefaultJWT }) {
|
||||||
session.access_token = token.access_token as string;
|
session.access_token = token.access_token as string;
|
||||||
session.access_expire_in = token.access_expire_in as number;
|
session.access_expire_in = token.access_expire_in as number;
|
||||||
session.refresh_token = token.refresh_token as string;
|
|
||||||
session.refresh_expire_in = token.refresh_expire_in as number;
|
|
||||||
session.user = {
|
session.user = {
|
||||||
id: token.id as number,
|
id: token.id as number,
|
||||||
username: token.username as string,
|
username: token.username as string,
|
||||||
@@ -165,11 +129,21 @@ const authConfig: NextAuthConfig = {
|
|||||||
email: token.email as string,
|
email: token.email as string,
|
||||||
role: token.role as Array<{ id: number; rol: string }>,
|
role: token.role as Array<{ id: number; rol: string }>,
|
||||||
};
|
};
|
||||||
console.log("Session: Habilitado");
|
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
events: {
|
||||||
|
async signOut(message) {
|
||||||
|
// 1. verificamos que venga token (puede no venir con algunos providers)
|
||||||
|
const token = (message as { token?: JWT }).token;
|
||||||
|
if (!token?.access_token) return;
|
||||||
|
try {
|
||||||
|
await logoutAction(String(token?.id));
|
||||||
|
} catch {
|
||||||
|
/* silencioso para que next-auth siempre cierre */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
} satisfies NextAuthConfig;
|
} satisfies NextAuthConfig;
|
||||||
|
|
||||||
export default authConfig;
|
export default authConfig;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import { env } from '@/lib/env';
|
import { env } from '@/lib/env';
|
||||||
import axios from 'axios';
|
import axios, { InternalAxiosRequestConfig } from 'axios';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Crear instancia de Axios con la URL base validada
|
// Crear instancia de Axios con la URL base validada
|
||||||
@@ -10,33 +10,21 @@ const fetchApi = axios.create({
|
|||||||
|
|
||||||
// Interceptor para incluir el token automáticamente en las peticiones
|
// Interceptor para incluir el token automáticamente en las peticiones
|
||||||
// ESTE INTERCEPTOR ESTÁ BIEN PARA EL RESTO DE LAS PETICIONES AUTENTICADAS
|
// ESTE INTERCEPTOR ESTÁ BIEN PARA EL RESTO DE LAS PETICIONES AUTENTICADAS
|
||||||
fetchApi.interceptors.request.use(async (config: any) => {
|
fetchApi.interceptors.request.use(
|
||||||
|
async (config: InternalAxiosRequestConfig) => {
|
||||||
try {
|
try {
|
||||||
// console.log("Solicitando autenticación...");
|
const { getValidAccessToken } = await import('@/lib/auth-token');
|
||||||
|
const token = await getValidAccessToken();
|
||||||
const { auth } = await import('@/lib/auth'); // Importación dinámica
|
|
||||||
const session = await auth();
|
|
||||||
const token = session?.access_token;
|
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.set('Authorization', `Bearer ${token}`);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
// **Importante:** Si el body es FormData, elimina el Content-Type para que Axios lo configure automáticamente.
|
console.error('Error getting auth token:', err);
|
||||||
if (config.data instanceof FormData) {
|
|
||||||
delete config.headers['Content-Type'];
|
|
||||||
} else {
|
|
||||||
config.headers['Content-Type'] = 'application/json';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
},
|
||||||
console.error('Error al obtener el token de autenticación para el interceptor:', error);
|
);
|
||||||
// IMPORTANTE: Si ocurre un error aquí, es mejor rechazar la promesa
|
|
||||||
// para que la solicitud no se envíe sin autorización.
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// safeFetchApi sigue siendo útil para el resto de las llamadas que requieren autenticación
|
// safeFetchApi sigue siendo útil para el resto de las llamadas que requieren autenticación
|
||||||
export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
'use server';
|
|
||||||
import { env } from '@/lib/env'; // Importamos la configuración de entorno validada
|
|
||||||
import axios from 'axios';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
// Crear instancia de Axios con la URL base validada
|
|
||||||
const fetchApi = axios.create({
|
|
||||||
baseURL: env.API_URL, // Aquí usamos env.API_URL en vez de process.env.BACKEND_URL
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Interceptor para incluir el token automáticamente en las peticiones
|
|
||||||
fetchApi.interceptors.request.use(async (config: any) => {
|
|
||||||
try {
|
|
||||||
// Importación dinámica para evitar la referencia circular
|
|
||||||
const { auth } = await import('@/lib/auth');
|
|
||||||
const session = await auth();
|
|
||||||
const token = session?.access_token;
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting auth token:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Función para hacer peticiones con validación de respuesta
|
|
||||||
* @param schema - Esquema de Zod para validar la respuesta
|
|
||||||
* @param url - Endpoint a consultar
|
|
||||||
* @param config - Configuración opcional de Axios
|
|
||||||
* @returns [error, data] -> Devuelve el error como objeto estructurado si hay fallo, o los datos validados
|
|
||||||
*/
|
|
||||||
export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
|
||||||
schema: T,
|
|
||||||
url: string,
|
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
|
|
||||||
body?: any,
|
|
||||||
): Promise<
|
|
||||||
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
|
|
||||||
> => {
|
|
||||||
try {
|
|
||||||
const response = await fetchApi({
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
data: body,
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsed = schema.safeParse(response.data);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
console.error('Validation Error Details:', {
|
|
||||||
errors: parsed.error.errors,
|
|
||||||
receivedData: response.data,
|
|
||||||
expectedSchema: schema,
|
|
||||||
data: response.data.data,
|
|
||||||
});
|
|
||||||
// console.error(parsed.error.errors)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'VALIDATION_ERROR',
|
|
||||||
message: 'Validation error',
|
|
||||||
details: parsed.error.errors,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [null, parsed.data];
|
|
||||||
} catch (error: any) {
|
|
||||||
const errorDetails = {
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
message: error.message,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method,
|
|
||||||
requestData: error.config?.data,
|
|
||||||
responseData: error.response?.data,
|
|
||||||
headers: error.config?.headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
// console.log(error)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'API_ERROR',
|
|
||||||
message: error.response?.data?.message || 'Unknown API error',
|
|
||||||
details: errorDetails,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export { fetchApi };
|
|
||||||
6
apps/web/types/next-auth.d.ts
vendored
6
apps/web/types/next-auth.d.ts
vendored
@@ -4,8 +4,6 @@ declare module 'next-auth' {
|
|||||||
interface Session extends DefaultSession {
|
interface Session extends DefaultSession {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
access_expire_in: number;
|
access_expire_in: number;
|
||||||
refresh_token: string;
|
|
||||||
refresh_expire_in: number;
|
|
||||||
user: {
|
user: {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -29,8 +27,6 @@ declare module 'next-auth' {
|
|||||||
}>;
|
}>;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
access_expire_in: number;
|
access_expire_in: number;
|
||||||
refresh_token: string;
|
|
||||||
refresh_expire_in: number;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +42,5 @@ declare module 'next-auth/jwt' {
|
|||||||
}>;
|
}>;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
access_expire_in: number;
|
access_expire_in: number;
|
||||||
refresh_token: string;
|
|
||||||
refresh_expire_in: number;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,18 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: hsl(0 0% 100%);
|
--background: hsl(51, 76%, 97%);
|
||||||
--foreground: hsl(240 10% 3.9%);
|
--foreground: hsl(240 10% 3.9%);
|
||||||
--card: hsl(0 0% 100%);
|
--card: hsl(0 0% 100%);
|
||||||
--card-foreground: hsl(240 10% 3.9%);
|
--card-foreground: hsl(240 10% 3.9%);
|
||||||
--popover: hsl(0 0% 100%);
|
--popover: hsl(0 0% 100%);
|
||||||
--popover-foreground: hsl(240 10% 3.9%);
|
--popover-foreground: hsl(240 10% 3.9%);
|
||||||
--primary: hsl(3, 85%, 32%);
|
--primary: hsl(17, 86%, 45%);
|
||||||
--primary-foreground: hsl(355.7 100% 97.3%);
|
--primary-foreground: hsl(355.7 100% 97.3%);
|
||||||
--secondary: hsl(240 4.8% 95.9%);
|
--secondary: hsl(240 4.8% 95.9%);
|
||||||
--secondary-foreground: hsl(240 5.9% 10%);
|
--secondary-foreground: hsl(240 5.9% 10%);
|
||||||
--muted: hsl(240 4.8% 95.9%);
|
--muted: hsl(240 4.8% 95.9%);
|
||||||
--muted-foreground: hsl(240 3.8% 46.1%);
|
--muted-foreground: hsl(240, 2%, 31%);
|
||||||
--accent: hsl(240 4.8% 95.9%);
|
--accent: hsl(240 4.8% 95.9%);
|
||||||
--accent-foreground: hsl(240 5.9% 10%);
|
--accent-foreground: hsl(240 5.9% 10%);
|
||||||
--destructive: hsl(0 84.2% 60.2%);
|
--destructive: hsl(0 84.2% 60.2%);
|
||||||
@@ -33,13 +33,13 @@
|
|||||||
--chart-3: hsl(197 37% 24%);
|
--chart-3: hsl(197 37% 24%);
|
||||||
--chart-4: hsl(43 74% 66%);
|
--chart-4: hsl(43 74% 66%);
|
||||||
--chart-5: hsl(27 87% 67%);
|
--chart-5: hsl(27 87% 67%);
|
||||||
--sidebar-background: hsl(0 0% 98%);
|
--sidebar-background: hsl(27, 92%, 90%);
|
||||||
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
--sidebar-foreground: hsl(0, 0%, 1%);
|
||||||
--sidebar-primary: hsl(240 5.9% 10%);
|
--sidebar-primary: hsl(240 5.9% 10%);
|
||||||
--sidebar-primary-foreground: hsl(0 0% 98%);
|
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||||
--sidebar-accent: hsl(240 4.8% 95.9%);
|
--sidebar-accent: hsl(24, 82%, 67%);
|
||||||
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
||||||
--sidebar-border: hsl(220 13% 91%);
|
--sidebar-border: hsl(20, 13%, 91%);
|
||||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
--sidebar: hsl(0 0% 98%);
|
--sidebar: hsl(0 0% 98%);
|
||||||
}
|
}
|
||||||
|
|||||||
184
packages/shadcn/src/shadcn.origin.css
Normal file
184
packages/shadcn/src/shadcn.origin.css
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@source './components/ui';
|
||||||
|
|
||||||
|
@plugin "tailwindcss-animate";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: hsl(0 0% 100%);
|
||||||
|
--foreground: hsl(240 10% 3.9%);
|
||||||
|
--card: hsl(0 0% 100%);
|
||||||
|
--card-foreground: hsl(240 10% 3.9%);
|
||||||
|
--popover: hsl(0 0% 100%);
|
||||||
|
--popover-foreground: hsl(240 10% 3.9%);
|
||||||
|
--primary: hsl(3, 85%, 32%);
|
||||||
|
--primary-foreground: hsl(355.7 100% 97.3%);
|
||||||
|
--secondary: hsl(240 4.8% 95.9%);
|
||||||
|
--secondary-foreground: hsl(240 5.9% 10%);
|
||||||
|
--muted: hsl(240 4.8% 95.9%);
|
||||||
|
--muted-foreground: hsl(240 3.8% 46.1%);
|
||||||
|
--accent: hsl(240 4.8% 95.9%);
|
||||||
|
--accent-foreground: hsl(240 5.9% 10%);
|
||||||
|
--destructive: hsl(0 84.2% 60.2%);
|
||||||
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--border: hsl(240 5.9% 90%);
|
||||||
|
--input: hsl(240 5.9% 90%);
|
||||||
|
--ring: hsl(3, 85%, 32%);
|
||||||
|
--radius: 0.7rem;
|
||||||
|
--chart-1: hsl(12 76% 61%);
|
||||||
|
--chart-2: hsl(173 58% 39%);
|
||||||
|
--chart-3: hsl(197 37% 24%);
|
||||||
|
--chart-4: hsl(43 74% 66%);
|
||||||
|
--chart-5: hsl(27 87% 67%);
|
||||||
|
--sidebar-background: hsl(0 0% 98%);
|
||||||
|
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
||||||
|
--sidebar-primary: hsl(240 5.9% 10%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||||
|
--sidebar-accent: hsl(240 4.8% 95.9%);
|
||||||
|
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
||||||
|
--sidebar-border: hsl(220 13% 91%);
|
||||||
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
|
--sidebar: hsl(0 0% 98%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: hsl(20 14.3% 4.1%);
|
||||||
|
--foreground: hsl(0 0% 95%);
|
||||||
|
--card: hsl(240 5.9% 10%);
|
||||||
|
--card-foreground: hsl(0 0% 95%);
|
||||||
|
--popover: hsl(0 0% 9%);
|
||||||
|
--popover-foreground: hsl(0 0% 95%);
|
||||||
|
--primary: hsl(3, 85%, 32%);
|
||||||
|
--primary-foreground: hsl(180, 33%, 99%);
|
||||||
|
--secondary: hsl(240 3.7% 15.9%);
|
||||||
|
--secondary-foreground: hsl(0 0% 98%);
|
||||||
|
--muted: hsl(0 0% 15%);
|
||||||
|
--muted-foreground: hsl(240 5% 64.9%);
|
||||||
|
--accent: hsl(12 6.5% 15.1%);
|
||||||
|
--accent-foreground: hsl(0 0% 98%);
|
||||||
|
--destructive: hsl(0 62.8% 30.6%);
|
||||||
|
--destructive-foreground: hsl(0 85.7% 97.3%);
|
||||||
|
--border: hsl(240 3.7% 15.9%);
|
||||||
|
--input: hsl(240 3.7% 15.9%);
|
||||||
|
--ring: hsl(3, 85%, 32%);
|
||||||
|
/* --chart-1: hsl(220 70% 50%); */
|
||||||
|
--chart-1: hsl(22 70% 50%);
|
||||||
|
--chart-2: hsl(160 60% 45%);
|
||||||
|
--chart-3: hsl(30 80% 55%);
|
||||||
|
--chart-4: hsl(280 65% 60%);
|
||||||
|
--chart-5: hsl(340 75% 55%);
|
||||||
|
--sidebar-background: hsl(240 5.9% 10%);
|
||||||
|
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
||||||
|
--sidebar-primary: hsl(224.3 76.3% 48%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||||
|
--sidebar-accent: hsl(240 3.7% 15.9%);
|
||||||
|
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||||
|
--sidebar-border: hsl(240 3.7% 15.9%);
|
||||||
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
|
--sidebar: hsl(240 5.9% 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar-background);
|
||||||
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
|
@keyframes accordion-down {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes accordion-up {
|
||||||
|
from {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground overscroll-none;
|
||||||
|
/* font-feature-settings: "rlig" 1, "calt" 1; */
|
||||||
|
font-synthesis-weight: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (font: -apple-system-body) and (-webkit-appearance: none) {
|
||||||
|
[data-wrapper] {
|
||||||
|
@apply min-[1800px]:border-t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar styling. Thanks @pranathiperii. */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 5px;
|
||||||
|
background: hsl(var(--border));
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
scrollbar-color: hsl(var(--border)) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user