Compare commits
1 Commits
main
...
export_exc
| Author | SHA1 | Date | |
|---|---|---|---|
| f1bdce317f |
@@ -15,12 +15,5 @@ DATABASE_URL="postgresql://postgres:local**@localhost:5432/caja_ahorro" #url con
|
||||
|
||||
#Mail Configuration
|
||||
MAIL_HOST=gmail
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
|
||||
MINIO_ENDPOINT=
|
||||
MINIO_PORT=
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
MINIO_BUCKET=
|
||||
MINIO_USE_SSL=
|
||||
MAIL_USERNAME="123"
|
||||
MAIL_PASSWORD="123"
|
||||
|
||||
@@ -42,17 +42,16 @@
|
||||
"@nestjs/platform-express": "11.0.0",
|
||||
"dotenv": "16.5.0",
|
||||
"drizzle-orm": "0.40.0",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "5.1.0",
|
||||
"joi": "17.13.3",
|
||||
"minio": "^8.0.6",
|
||||
"moment": "2.30.1",
|
||||
"path-to-regexp": "8.2.0",
|
||||
"pg": "8.13.3",
|
||||
"pino-pretty": "13.0.0",
|
||||
"reflect-metadata": "0.2.0",
|
||||
"rxjs": "7.8.1",
|
||||
"sharp": "^0.34.5",
|
||||
"xlsx-populate": "^1.21.0"
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
|
||||
@@ -10,17 +10,16 @@ import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { MinioModule } from './common/minio/minio.module';
|
||||
import { DrizzleModule } from './database/drizzle.module';
|
||||
import { AuthModule } from './features/auth/auth.module';
|
||||
import { ConfigurationsModule } from './features/configurations/configurations.module';
|
||||
import { InventoryModule } from './features/inventory/inventory.module';
|
||||
import { LocationModule } from './features/location/location.module';
|
||||
import { LocationModule } from './features/location/location.module'
|
||||
import { MailModule } from './features/mail/mail.module';
|
||||
import { RolesModule } from './features/roles/roles.module';
|
||||
import { SurveysModule } from './features/surveys/surveys.module';
|
||||
import { TrainingModule } from './features/training/training.module';
|
||||
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
||||
import { SurveysModule } from './features/surveys/surveys.module';
|
||||
import { InventoryModule } from './features/inventory/inventory.module';
|
||||
import { TrainingModule } from './features/training/training.module';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
@@ -52,7 +51,6 @@ import { UserRolesModule } from './features/user-roles/user-roles.module';
|
||||
NodeMailerModule,
|
||||
LoggerModule,
|
||||
ThrottleModule,
|
||||
MinioModule,
|
||||
UsersModule,
|
||||
AuthModule,
|
||||
MailModule,
|
||||
@@ -63,7 +61,7 @@ import { UserRolesModule } from './features/user-roles/user-roles.module';
|
||||
SurveysModule,
|
||||
LocationModule,
|
||||
InventoryModule,
|
||||
TrainingModule,
|
||||
TrainingModule
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule { }
|
||||
|
||||
@@ -14,12 +14,6 @@ interface EnvVars {
|
||||
MAIL_HOST: string;
|
||||
MAIL_USERNAME: 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
|
||||
@@ -36,12 +30,6 @@ const envsSchema = joi
|
||||
MAIL_HOST: joi.string(),
|
||||
MAIL_USERNAME: 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);
|
||||
|
||||
@@ -66,10 +54,4 @@ export const envs = {
|
||||
mail_host: envVars.MAIL_HOST,
|
||||
mail_username: envVars.MAIL_USERNAME,
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { MinioService } from './minio.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [MinioService],
|
||||
exports: [MinioService],
|
||||
})
|
||||
export class MinioModule {}
|
||||
@@ -1,127 +0,0 @@
|
||||
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',
|
||||
ttl: 1000, // 1 sec
|
||||
limit: 10,
|
||||
limit: 2,
|
||||
},
|
||||
{
|
||||
name: 'medium',
|
||||
ttl: 10000, // 10 sec
|
||||
limit: 30,
|
||||
limit: 4,
|
||||
},
|
||||
{
|
||||
name: 'long',
|
||||
ttl: 60000, // 1 min
|
||||
limit: 100,
|
||||
limit: 10,
|
||||
},
|
||||
],
|
||||
errorMessage: 'Too many requests, please try again later.',
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE "auth"."sessions" ADD COLUMN "previous_session_token" varchar;--> statement-breakpoint
|
||||
ALTER TABLE "auth"."sessions" ADD COLUMN "last_rotated_at" timestamp;
|
||||
@@ -1,4 +0,0 @@
|
||||
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;
|
||||
@@ -1,10 +0,0 @@
|
||||
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;
|
||||
@@ -1,5 +0,0 @@
|
||||
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;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "osp_rif" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ALTER COLUMN "osp_name" DROP NOT NULL;
|
||||
@@ -1,9 +0,0 @@
|
||||
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;
|
||||
@@ -1,5 +0,0 @@
|
||||
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 +0,0 @@
|
||||
CREATE VIEW "public"."v_training_surveys" AS (select "id", "osp_name", "osp_rif", "osp_type", "current_status", "visit_date" from "training_surveys");
|
||||
@@ -1,2 +0,0 @@
|
||||
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");
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP VIEW "public"."v_training_surveys";--> statement-breakpoint
|
||||
ALTER TABLE "training_surveys" ADD COLUMN "productive_activity_other" text DEFAULT '';
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "training_surveys" ADD COLUMN "survey_status" text DEFAULT 'PUBLICADO' NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -120,83 +120,6 @@
|
||||
"when": 1769653021994,
|
||||
"tag": "0016_silent_tag",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "7",
|
||||
"when": 1770774052351,
|
||||
"tag": "0017_mute_mole_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "7",
|
||||
"when": 1771855467870,
|
||||
"tag": "0018_milky_prism",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "7",
|
||||
"when": 1771858973096,
|
||||
"tag": "0019_cuddly_cobalt_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "7",
|
||||
"when": 1771897944334,
|
||||
"tag": "0020_certain_bushwacker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 21,
|
||||
"version": "7",
|
||||
"when": 1771901546945,
|
||||
"tag": "0021_warm_machine_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "7",
|
||||
"when": 1772031518006,
|
||||
"tag": "0022_nervous_dragon_lord",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "7",
|
||||
"when": 1772032122473,
|
||||
"tag": "0023_sticky_slayback",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 24,
|
||||
"version": "7",
|
||||
"when": 1772642460042,
|
||||
"tag": "0024_petite_sabra",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 25,
|
||||
"version": "7",
|
||||
"when": 1772643066120,
|
||||
"tag": "0025_funny_makkari",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 26,
|
||||
"version": "7",
|
||||
"when": 1774379641691,
|
||||
"tag": "0026_last_vampiro",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 27,
|
||||
"version": "7",
|
||||
"when": 1775675160189,
|
||||
"tag": "0027_concerned_captain_flint",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import * as t from 'drizzle-orm/pg-core';
|
||||
import { timestamps } from '../timestamps';
|
||||
import { municipalities, parishes, states } from './general';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { authSchema } from './schemas';
|
||||
import { timestamps } from '../timestamps';
|
||||
import { states, municipalities, parishes } from './general';
|
||||
|
||||
|
||||
// Tabla de Usuarios sistema
|
||||
export const users = authSchema.table(
|
||||
@@ -14,15 +15,9 @@ export const users = authSchema.table(
|
||||
fullname: t.text('fullname').notNull(),
|
||||
phone: t.text('phone'),
|
||||
password: t.text('password').notNull(),
|
||||
state: t
|
||||
.integer('state')
|
||||
.references(() => states.id, { onDelete: 'set null' }),
|
||||
municipality: t
|
||||
.integer('municipality')
|
||||
.references(() => municipalities.id, { onDelete: 'set null' }),
|
||||
parish: t
|
||||
.integer('parish')
|
||||
.references(() => parishes.id, { onDelete: 'set null' }),
|
||||
state: t.integer('state').references(() => states.id, { onDelete: 'set null' }),
|
||||
municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }),
|
||||
parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }),
|
||||
isTwoFactorEnabled: t
|
||||
.boolean('is_two_factor_enabled')
|
||||
.notNull()
|
||||
@@ -37,6 +32,7 @@ export const users = authSchema.table(
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
// Tabla de Roles
|
||||
export const roles = authSchema.table(
|
||||
'roles',
|
||||
@@ -50,6 +46,8 @@ export const roles = authSchema.table(
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
|
||||
//tabla User_roles
|
||||
export const usersRole = authSchema.table(
|
||||
'user_role',
|
||||
@@ -90,6 +88,7 @@ LEFT JOIN
|
||||
LEFT JOIN
|
||||
auth.roles r ON ur.role_id = r.id`);
|
||||
|
||||
|
||||
// Tabla de Sesiones
|
||||
export const sessions = authSchema.table(
|
||||
'sessions',
|
||||
@@ -104,9 +103,6 @@ export const sessions = authSchema.table(
|
||||
.notNull(),
|
||||
sessionToken: t.text('session_token').notNull(),
|
||||
expiresAt: t.integer('expires_at').notNull(),
|
||||
previousSessionToken: t.varchar('previous_session_token'),
|
||||
lastRotatedAt: t.timestamp('last_rotated_at'),
|
||||
|
||||
...timestamps,
|
||||
},
|
||||
(sessions) => ({
|
||||
@@ -114,6 +110,8 @@ export const sessions = authSchema.table(
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
|
||||
//tabla de tokens de verificación
|
||||
export const verificationTokens = authSchema.table(
|
||||
'verificationToken',
|
||||
|
||||
@@ -48,7 +48,8 @@ export const trainingSurveys = t.pgTable(
|
||||
{
|
||||
// === 1. IDENTIFICADORES Y DATOS DE VISITA ===
|
||||
id: t.serial('id').primaryKey(),
|
||||
coorFullName: t.text('coor_full_name').notNull(),
|
||||
firstname: t.text('firstname').notNull(),
|
||||
lastname: t.text('lastname').notNull(),
|
||||
visitDate: t.timestamp('visit_date').notNull(),
|
||||
coorPhone: t.text('coor_phone'),
|
||||
|
||||
@@ -76,9 +77,8 @@ export const trainingSurveys = t.pgTable(
|
||||
.notNull()
|
||||
.default(''),
|
||||
productiveActivity: t.text('productive_activity').notNull(),
|
||||
productiveActivityOther: t.text('productive_activity_other').default(''),
|
||||
ospRif: t.text('osp_rif'),
|
||||
ospName: t.text('osp_name'),
|
||||
ospRif: t.text('osp_rif').notNull(),
|
||||
ospName: t.text('osp_name').notNull(),
|
||||
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
||||
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
|
||||
infrastructureMt2: t.text('infrastructure_mt2').notNull().default(''),
|
||||
@@ -98,13 +98,19 @@ export const trainingSurveys = t.pgTable(
|
||||
.text('commune_spokesperson_name')
|
||||
.notNull()
|
||||
.default(''),
|
||||
communeSpokespersonCedula: t.text('commune_spokesperson_cedula'),
|
||||
communeSpokespersonRif: t.text('commune_spokesperson_rif'),
|
||||
communeSpokespersonCedula: t
|
||||
.text('commune_spokesperson_cedula')
|
||||
.notNull()
|
||||
.default(''),
|
||||
communeSpokespersonRif: t
|
||||
.text('commune_spokesperson_rif')
|
||||
.notNull()
|
||||
.default(''),
|
||||
communeSpokespersonPhone: t
|
||||
.text('commune_spokesperson_phone')
|
||||
.notNull()
|
||||
.default(''),
|
||||
communeEmail: t.text('commune_email'),
|
||||
communeEmail: t.text('commune_email').notNull().default(''),
|
||||
communalCouncil: t.text('communal_council').notNull(),
|
||||
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
|
||||
communalCouncilRif: t.text('communal_council_rif').notNull().default(''),
|
||||
@@ -112,10 +118,14 @@ export const trainingSurveys = t.pgTable(
|
||||
.text('communal_council_spokesperson_name')
|
||||
.notNull()
|
||||
.default(''),
|
||||
communalCouncilSpokespersonCedula: t.text(
|
||||
'communal_council_spokesperson_cedula',
|
||||
),
|
||||
communalCouncilSpokespersonRif: t.text('communal_council_spokesperson_rif'),
|
||||
communalCouncilSpokespersonCedula: t
|
||||
.text('communal_council_spokesperson_cedula')
|
||||
.notNull()
|
||||
.default(''),
|
||||
communalCouncilSpokespersonRif: t
|
||||
.text('communal_council_spokesperson_rif')
|
||||
.notNull()
|
||||
.default(''),
|
||||
communalCouncilSpokespersonPhone: t
|
||||
.text('communal_council_spokesperson_phone')
|
||||
.notNull()
|
||||
@@ -126,45 +136,22 @@ export const trainingSurveys = t.pgTable(
|
||||
.default(''),
|
||||
ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
|
||||
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
|
||||
ospResponsibleRif: t.text('osp_responsible_rif'),
|
||||
civilState: t.text('civil_state'),
|
||||
ospResponsibleRif: t.text('osp_responsible_rif').notNull(),
|
||||
civilState: t.text('civil_state').notNull(),
|
||||
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
|
||||
ospResponsibleEmail: t.text('osp_responsible_email'),
|
||||
familyBurden: t.integer('family_burden'),
|
||||
numberOfChildren: t.integer('number_of_children'),
|
||||
ospResponsibleEmail: t.text('osp_responsible_email').notNull(),
|
||||
familyBurden: t.integer('family_burden').notNull(),
|
||||
numberOfChildren: t.integer('number_of_children').notNull(),
|
||||
generalObservations: t.text('general_observations'),
|
||||
|
||||
// === 4. DATOS DE DISTRIBUCIÓN Y EXPORTACIÓN ===
|
||||
internalDistributionZone: t.text('internal_distribution_zone'),
|
||||
isExporting: t.boolean('is_exporting').notNull().default(false),
|
||||
externalCountry: t.text('external_country'),
|
||||
externalCity: t.text('external_city'),
|
||||
externalDescription: t.text('external_description'),
|
||||
externalQuantity: t.text('external_quantity'),
|
||||
externalUnit: t.text('external_unit'),
|
||||
|
||||
// === 5. MANO DE OBRA ===
|
||||
womenCount: t.integer('women_count').notNull().default(0),
|
||||
menCount: t.integer('men_count').notNull().default(0),
|
||||
|
||||
// Fotos
|
||||
photo1: t.text('photo1'),
|
||||
photo2: t.text('photo2'),
|
||||
photo3: t.text('photo3'),
|
||||
// informacion del usuario que creo y actualizo el registro
|
||||
createdBy: t
|
||||
.integer('created_by')
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
updatedBy: t
|
||||
.integer('updated_by')
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
surveyStatus: t.text('survey_status').notNull().default('COMPLETADA'),
|
||||
...timestamps,
|
||||
},
|
||||
(trainingSurveys) => ({
|
||||
trainingSurveysIndex: t
|
||||
.index('training_surveys_index_00')
|
||||
.on(trainingSurveys.coorFullName),
|
||||
.on(trainingSurveys.firstname),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1673,5 +1673,415 @@ export const Municipalities = [
|
||||
id: 335,
|
||||
name: 'MP. VARGAS',
|
||||
stateId: 24,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 336,
|
||||
name: 'ALEMANIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 337,
|
||||
name: 'ANGOLA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 338,
|
||||
name: 'ANTIGUA Y BARBUDA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 339,
|
||||
name: 'ARABIA SAUDITA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 340,
|
||||
name: 'ARGELIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 341,
|
||||
name: 'ARGENTINA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 342,
|
||||
name: 'AUSTRALIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 343,
|
||||
name: 'AUSTRIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 344,
|
||||
name: 'BARBADOS',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 345,
|
||||
name: 'BELGICA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 346,
|
||||
name: 'BELICE',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 347,
|
||||
name: 'BENIN',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 348,
|
||||
name: 'BIELORRUSIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 349,
|
||||
name: 'BOLIVIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 350,
|
||||
name: 'BRASIL',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 351,
|
||||
name: 'CHILE',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 352,
|
||||
name: 'CHINA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 353,
|
||||
name: 'COLOMBIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 354,
|
||||
name: 'CONGO',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 355,
|
||||
name: 'COREA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 356,
|
||||
name: 'COSTA RICA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 357,
|
||||
name: 'CUBA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 358,
|
||||
name: 'DOMINICA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 359,
|
||||
name: 'ECUADOR',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 360,
|
||||
name: 'EGIPTO',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 361,
|
||||
name: 'EMIRATOS ARABES UNID',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 362,
|
||||
name: 'ESPAÑA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 363,
|
||||
name: 'ETIOPIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 364,
|
||||
name: 'FILIPINAS',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 365,
|
||||
name: 'FRANCIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 366,
|
||||
name: 'GRAN BRETAÑA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 367,
|
||||
name: 'GRECIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 368,
|
||||
name: 'GRENADA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 369,
|
||||
name: 'GUAYANA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 370,
|
||||
name: 'GUINEA ECUATORIAL',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 371,
|
||||
name: 'HAITI',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 372,
|
||||
name: 'HONDURAS',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 373,
|
||||
name: 'HUNGRIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 374,
|
||||
name: 'INDIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 375,
|
||||
name: 'INDONESIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 376,
|
||||
name: 'IRAK',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 377,
|
||||
name: 'IRAN',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 378,
|
||||
name: 'ITALIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 379,
|
||||
name: 'JAMAICA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 380,
|
||||
name: 'JAPON',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 381,
|
||||
name: 'JORDANIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 382,
|
||||
name: 'KENIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 383,
|
||||
name: 'KUWAIT',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 384,
|
||||
name: 'LIBANO',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 385,
|
||||
name: 'MALASIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 386,
|
||||
name: 'MALI',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 387,
|
||||
name: 'MARRUECOS',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 388,
|
||||
name: 'MEXICO',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 389,
|
||||
name: 'MOZAMBIQUE',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 390,
|
||||
name: 'NAMIBIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 391,
|
||||
name: 'NICARAGUA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 392,
|
||||
name: 'NIGERIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 393,
|
||||
name: 'NORUEGA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 394,
|
||||
name: 'PAISES BAJOS',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 395,
|
||||
name: 'PALESTINA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 396,
|
||||
name: 'PANAMA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 397,
|
||||
name: 'PERU',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 398,
|
||||
name: 'POLONIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 399,
|
||||
name: 'PORTUGAL',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 400,
|
||||
name: 'QATAR',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 401,
|
||||
name: 'REPUBLICA DOMINICANA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 402,
|
||||
name: 'RUMANIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 403,
|
||||
name: 'RUSIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 404,
|
||||
name: 'SAN KITTS Y NEVIS',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 405,
|
||||
name: 'SANTA LUCIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 406,
|
||||
name: 'SAN VICENTE Y LAS GR',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 407,
|
||||
name: 'SENEGAL',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 408,
|
||||
name: 'SERBIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 409,
|
||||
name: 'SINGAPUR',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 410,
|
||||
name: 'SIRIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 411,
|
||||
name: 'SUDAFRICA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 412,
|
||||
name: 'SUIZA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 413,
|
||||
name: 'SURINAME',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 414,
|
||||
name: 'TRINIDAD Y TOBAGO',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 415,
|
||||
name: 'TURQUIA',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 416,
|
||||
name: 'URUGUAY',
|
||||
stateId: 99,
|
||||
},
|
||||
{
|
||||
id: 417,
|
||||
name: 'VIETNAM',
|
||||
stateId: 99,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5703,5 +5703,515 @@ export const Parishes = [
|
||||
id: 1141,
|
||||
name: "PQ. URIMARE",
|
||||
municipalityId: 335,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 1142,
|
||||
name: "BERLIN",
|
||||
municipalityId: 336,
|
||||
},
|
||||
{
|
||||
id: 1143,
|
||||
name: "FRANKFURT",
|
||||
municipalityId: 336,
|
||||
},
|
||||
{
|
||||
id: 1144,
|
||||
name: "LUANDA",
|
||||
municipalityId: 337,
|
||||
},
|
||||
{
|
||||
id: 1145,
|
||||
name: "ST. JOHN'S",
|
||||
municipalityId: 338,
|
||||
},
|
||||
{
|
||||
id: 1146,
|
||||
name: "RIYADH",
|
||||
municipalityId: 339,
|
||||
},
|
||||
{
|
||||
id: 1147,
|
||||
name: "ARGEL",
|
||||
municipalityId: 340,
|
||||
},
|
||||
{
|
||||
id: 1148,
|
||||
name: "BUENOS AIRES",
|
||||
municipalityId: 341,
|
||||
},
|
||||
{
|
||||
id: 1149,
|
||||
name: "CABERRA",
|
||||
municipalityId: 342,
|
||||
},
|
||||
{
|
||||
id: 1150,
|
||||
name: "VIENA",
|
||||
municipalityId: 343,
|
||||
},
|
||||
{
|
||||
id: 1151,
|
||||
name: "BRIDGETOWN",
|
||||
municipalityId: 344,
|
||||
},
|
||||
{
|
||||
id: 1152,
|
||||
name: "BRUSELA",
|
||||
municipalityId: 345,
|
||||
},
|
||||
{
|
||||
id: 1153,
|
||||
name: "BELMONPAN",
|
||||
municipalityId: 346,
|
||||
},
|
||||
{
|
||||
id: 1154,
|
||||
name: "COTONOU",
|
||||
municipalityId: 347,
|
||||
},
|
||||
{
|
||||
id: 1155,
|
||||
name: "MINSK",
|
||||
municipalityId: 348,
|
||||
},
|
||||
{
|
||||
id: 1156,
|
||||
name: "LA PAZ",
|
||||
municipalityId: 349,
|
||||
},
|
||||
{
|
||||
id: 1157,
|
||||
name: "BRASILIA",
|
||||
municipalityId: 350,
|
||||
},
|
||||
{
|
||||
id: 1158,
|
||||
name: "SANTIAGO",
|
||||
municipalityId: 351,
|
||||
},
|
||||
{
|
||||
id: 1159,
|
||||
name: "BEIJING",
|
||||
municipalityId: 352,
|
||||
},
|
||||
{
|
||||
id: 1160,
|
||||
name: "HONG KONG",
|
||||
municipalityId: 352,
|
||||
},
|
||||
{
|
||||
id: 1161,
|
||||
name: "NEW",
|
||||
municipalityId: 352,
|
||||
},
|
||||
{
|
||||
id: 1162,
|
||||
name: "SHANGHAI",
|
||||
municipalityId: 352,
|
||||
},
|
||||
{
|
||||
id: 1163,
|
||||
name: "BARRANQUILLA",
|
||||
municipalityId: 353,
|
||||
},
|
||||
{
|
||||
id: 1164,
|
||||
name: "BOGOTA",
|
||||
municipalityId: 353,
|
||||
},
|
||||
{
|
||||
id: 1165,
|
||||
name: "CARTAGENA",
|
||||
municipalityId: 353,
|
||||
},
|
||||
{
|
||||
id: 1166,
|
||||
name: "CUCUTA",
|
||||
municipalityId: 353,
|
||||
},
|
||||
{
|
||||
id: 1167,
|
||||
name: "MEDELLIN",
|
||||
municipalityId: 353,
|
||||
},
|
||||
{
|
||||
id: 1168,
|
||||
name: "RIOHACHA",
|
||||
municipalityId: 353,
|
||||
},
|
||||
{
|
||||
id: 1169,
|
||||
name: "BRAZAVILLE",
|
||||
municipalityId: 354,
|
||||
},
|
||||
{
|
||||
id: 1170,
|
||||
name: "SEUL",
|
||||
municipalityId: 355,
|
||||
},
|
||||
{
|
||||
id: 1171,
|
||||
name: "SAN JOSE",
|
||||
municipalityId: 356,
|
||||
},
|
||||
{
|
||||
id: 1172,
|
||||
name: "LA HABANA",
|
||||
municipalityId: 357,
|
||||
},
|
||||
{
|
||||
id: 1173,
|
||||
name: "ROSEAU",
|
||||
municipalityId: 358,
|
||||
},
|
||||
{
|
||||
id: 1174,
|
||||
name: "GUAYAQUIL",
|
||||
municipalityId: 359,
|
||||
},
|
||||
{
|
||||
id: 1175,
|
||||
name: "QUITO",
|
||||
municipalityId: 359,
|
||||
},
|
||||
{
|
||||
id: 1176,
|
||||
name: "EL CAIRO",
|
||||
municipalityId: 360,
|
||||
},
|
||||
{
|
||||
id: 1177,
|
||||
name: "ABU DHABI",
|
||||
municipalityId: 361,
|
||||
},
|
||||
{
|
||||
id: 1178,
|
||||
name: "BARCELONA",
|
||||
municipalityId: 362,
|
||||
},
|
||||
{
|
||||
id: 1179,
|
||||
name: "BILBAO",
|
||||
municipalityId: 362,
|
||||
},
|
||||
{
|
||||
id: 1180,
|
||||
name: "MADRID",
|
||||
municipalityId: 362,
|
||||
},
|
||||
{
|
||||
id: 1181,
|
||||
name: "SANTA CRUZ DE TENERIFE",
|
||||
municipalityId: 362,
|
||||
},
|
||||
{
|
||||
id: 1182,
|
||||
name: "VIGO",
|
||||
municipalityId: 362,
|
||||
},
|
||||
{
|
||||
id: 1183,
|
||||
name: "ETIOPIA",
|
||||
municipalityId: 363,
|
||||
},
|
||||
{
|
||||
id: 1184,
|
||||
name: "MANILA",
|
||||
municipalityId: 364,
|
||||
},
|
||||
{
|
||||
id: 1185,
|
||||
name: "PARIS",
|
||||
municipalityId: 365,
|
||||
},
|
||||
{
|
||||
id: 1186,
|
||||
name: "LONDRES",
|
||||
municipalityId: 366,
|
||||
},
|
||||
{
|
||||
id: 1187,
|
||||
name: "ATENAS",
|
||||
municipalityId: 367,
|
||||
},
|
||||
{
|
||||
id: 1188,
|
||||
name: "ST. GEORGES",
|
||||
municipalityId: 368,
|
||||
},
|
||||
{
|
||||
id: 1189,
|
||||
name: "GEORGETOWN",
|
||||
municipalityId: 369,
|
||||
},
|
||||
{
|
||||
id: 1190,
|
||||
name: "MALABO",
|
||||
municipalityId: 370,
|
||||
},
|
||||
{
|
||||
id: 1191,
|
||||
name: "PUERTO PRINCIPE",
|
||||
municipalityId: 371,
|
||||
},
|
||||
{
|
||||
id: 1192,
|
||||
name: "TEGUCIGALPA",
|
||||
municipalityId: 372,
|
||||
},
|
||||
{
|
||||
id: 1193,
|
||||
name: "BUDAPEST",
|
||||
municipalityId: 373,
|
||||
},
|
||||
{
|
||||
id: 1194,
|
||||
name: "NUEVA DELHI",
|
||||
municipalityId: 374,
|
||||
},
|
||||
{
|
||||
id: 1195,
|
||||
name: "JAKARTA",
|
||||
municipalityId: 375,
|
||||
},
|
||||
{
|
||||
id: 1196,
|
||||
name: "BAGDAD",
|
||||
municipalityId: 376,
|
||||
},
|
||||
{
|
||||
id: 1197,
|
||||
name: "TEHERAN",
|
||||
municipalityId: 377,
|
||||
},
|
||||
{
|
||||
id: 1198,
|
||||
name: "MILAN",
|
||||
municipalityId: 378,
|
||||
},
|
||||
{
|
||||
id: 1199,
|
||||
name: "NAPOLES",
|
||||
municipalityId: 378,
|
||||
},
|
||||
{
|
||||
id: 1200,
|
||||
name: "ROMA",
|
||||
municipalityId: 378,
|
||||
},
|
||||
{
|
||||
id: 1201,
|
||||
name: "KINGSTON",
|
||||
municipalityId: 379,
|
||||
},
|
||||
{
|
||||
id: 1202,
|
||||
name: "TOKIO",
|
||||
municipalityId: 380,
|
||||
},
|
||||
{
|
||||
id: 1203,
|
||||
name: "AMMAN",
|
||||
municipalityId: 381,
|
||||
},
|
||||
{
|
||||
id: 1204,
|
||||
name: "NAIROBI",
|
||||
municipalityId: 382,
|
||||
},
|
||||
{
|
||||
id: 1205,
|
||||
name: "KUWAIT",
|
||||
municipalityId: 383,
|
||||
},
|
||||
{
|
||||
id: 1206,
|
||||
name: "BEIRUT",
|
||||
municipalityId: 384,
|
||||
},
|
||||
{
|
||||
id: 1207,
|
||||
name: "KUALA LUMPUR",
|
||||
municipalityId: 385,
|
||||
},
|
||||
{
|
||||
id: 1208,
|
||||
name: "MALI",
|
||||
municipalityId: 386,
|
||||
},
|
||||
{
|
||||
id: 1209,
|
||||
name: "RABAT",
|
||||
municipalityId: 387,
|
||||
},
|
||||
{
|
||||
id: 1210,
|
||||
name: "MEXICO",
|
||||
municipalityId: 388,
|
||||
},
|
||||
{
|
||||
id: 1211,
|
||||
name: "MAPUTO",
|
||||
municipalityId: 389,
|
||||
},
|
||||
{
|
||||
id: 1212,
|
||||
name: "WINDHOEK",
|
||||
municipalityId: 390,
|
||||
},
|
||||
{
|
||||
id: 1213,
|
||||
name: "MANAGUA",
|
||||
municipalityId: 391,
|
||||
},
|
||||
{
|
||||
id: 1214,
|
||||
name: "LAGOS",
|
||||
municipalityId: 392,
|
||||
},
|
||||
{
|
||||
id: 1215,
|
||||
name: "OSLO",
|
||||
municipalityId: 393,
|
||||
},
|
||||
{
|
||||
id: 1216,
|
||||
name: "ARUBA",
|
||||
municipalityId: 394,
|
||||
},
|
||||
{
|
||||
id: 1217,
|
||||
name: "CURAZAO",
|
||||
municipalityId: 394,
|
||||
},
|
||||
{
|
||||
id: 1218,
|
||||
name: "LA HAYA",
|
||||
municipalityId: 394,
|
||||
},
|
||||
{
|
||||
id: 1219,
|
||||
name: "PALESTINA",
|
||||
municipalityId: 395,
|
||||
},
|
||||
{
|
||||
id: 1220,
|
||||
name: "PANAMA",
|
||||
municipalityId: 396,
|
||||
},
|
||||
{
|
||||
id: 1221,
|
||||
name: "LIMA",
|
||||
municipalityId: 397,
|
||||
},
|
||||
{
|
||||
id: 1222,
|
||||
name: "VARSOVIA",
|
||||
municipalityId: 398,
|
||||
},
|
||||
{
|
||||
id: 1223,
|
||||
name: "FUNCHAL MADEIRA",
|
||||
municipalityId: 399,
|
||||
},
|
||||
{
|
||||
id: 1224,
|
||||
name: "LISBOA",
|
||||
municipalityId: 399,
|
||||
},
|
||||
{
|
||||
id: 1225,
|
||||
name: "DOHA",
|
||||
municipalityId: 400,
|
||||
},
|
||||
{
|
||||
id: 1226,
|
||||
name: "SANTO DOMINGO",
|
||||
municipalityId: 401,
|
||||
},
|
||||
{
|
||||
id: 1227,
|
||||
name: "BUCAREST",
|
||||
municipalityId: 402,
|
||||
},
|
||||
{
|
||||
id: 1228,
|
||||
name: "MOSCU",
|
||||
municipalityId: 403,
|
||||
},
|
||||
{
|
||||
id: 1229,
|
||||
name: "BASSETERRE",
|
||||
municipalityId: 404,
|
||||
},
|
||||
{
|
||||
id: 1230,
|
||||
name: "CASTRIES",
|
||||
municipalityId: 405,
|
||||
},
|
||||
{
|
||||
id: 1231,
|
||||
name: "KINGSTOWN",
|
||||
municipalityId: 406,
|
||||
},
|
||||
{
|
||||
id: 1232,
|
||||
name: "SENEGAL",
|
||||
municipalityId: 407,
|
||||
},
|
||||
{
|
||||
id: 1233,
|
||||
name: "BELGRADO",
|
||||
municipalityId: 408,
|
||||
},
|
||||
{
|
||||
id: 1234,
|
||||
name: "SINGAPUR",
|
||||
municipalityId: 409,
|
||||
},
|
||||
{
|
||||
id: 1235,
|
||||
name: "DAMASCO",
|
||||
municipalityId: 410,
|
||||
},
|
||||
{
|
||||
id: 1236,
|
||||
name: "PETRORIA",
|
||||
municipalityId: 411,
|
||||
},
|
||||
{
|
||||
id: 1237,
|
||||
name: "BERNA",
|
||||
municipalityId: 412,
|
||||
},
|
||||
{
|
||||
id: 1238,
|
||||
name: "PARAMARIBO",
|
||||
municipalityId: 413,
|
||||
},
|
||||
{
|
||||
id: 1239,
|
||||
name: "PUERTO ESPA A",
|
||||
municipalityId: 414,
|
||||
},
|
||||
{
|
||||
id: 1240,
|
||||
name: "ANKARA",
|
||||
municipalityId: 415,
|
||||
},
|
||||
{
|
||||
id: 1241,
|
||||
name: "NEW",
|
||||
municipalityId: 415,
|
||||
},
|
||||
{
|
||||
id: 1242,
|
||||
name: "MONTEVIDEO",
|
||||
municipalityId: 416,
|
||||
},
|
||||
{
|
||||
id: 1243,
|
||||
name: "VIETNAM",
|
||||
municipalityId: 417,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -94,5 +94,9 @@ export const States = [
|
||||
{
|
||||
id: 21,
|
||||
name: "EDO. ZULIA",
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 99,
|
||||
name: "EMBAJADA",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
// api/src/feacture/auth/auth.controller.ts
|
||||
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 { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
||||
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
||||
import { Body, Controller, HttpCode, Patch, Post } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
Patch,
|
||||
Post,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) { }
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Public()
|
||||
@HttpCode(200)
|
||||
@@ -28,8 +39,6 @@ export class AuthController {
|
||||
return await this.authService.signIn(signInUserDto);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@HttpCode(200)
|
||||
@Post('sign-out')
|
||||
//@RequirePermissions('auth:sign-out')
|
||||
async signOut(@Body() signOutUserDto: SignOutUserDto) {
|
||||
@@ -49,11 +58,17 @@ export class AuthController {
|
||||
@Patch('refresh')
|
||||
//@RequirePermissions('auth:refresh-token')
|
||||
async refreshToken(@Body() refreshTokenDto: any) {
|
||||
// console.log('REFRESCANDO');
|
||||
// console.log(refreshTokenDto);
|
||||
// console.log('-----------');
|
||||
|
||||
return await this.authService.refreshToken(refreshTokenDto);
|
||||
console.log('refreshTokenDto', refreshTokenDto);
|
||||
|
||||
const data = await this.authService.refreshToken(refreshTokenDto);
|
||||
|
||||
// console.log('data', data);
|
||||
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return {tokens: data}
|
||||
}
|
||||
|
||||
// @Public()
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
import { eq, or } from 'drizzle-orm';
|
||||
import { and, eq, or } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from 'src/database/index';
|
||||
import { roles, sessions, users, usersRole } from 'src/database/index';
|
||||
@@ -40,7 +40,7 @@ export class AuthService {
|
||||
private readonly config: ConfigService<Env>,
|
||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||
private readonly mailService: MailService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
//Decode Tokens
|
||||
// Método para decodificar el token y obtener los datos completos
|
||||
@@ -273,118 +273,50 @@ export class AuthService {
|
||||
|
||||
//Refresh User Access Token
|
||||
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
|
||||
const { refreshToken } = dto;
|
||||
const secret = envs.refresh_token_secret;
|
||||
const { user_id, token } = dto;
|
||||
|
||||
// 1. Validar firma del token (Crypto check)
|
||||
let payload: any;
|
||||
try {
|
||||
payload = await this.jwtService.verifyAsync(refreshToken, {
|
||||
secret: envs.refresh_token_secret,
|
||||
console.log('secret', secret);
|
||||
console.log('refresh_token', token);
|
||||
|
||||
const validation = await this.jwtService.verifyAsync(token, {
|
||||
secret,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new UnauthorizedException('Invalid Refresh Token Signature');
|
||||
}
|
||||
|
||||
const userId = Number(payload.sub); // Es más seguro usar el ID del token que el del DTO
|
||||
if (!validation) throw new UnauthorizedException('Invalid refresh token');
|
||||
|
||||
// 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
|
||||
const session = await this.drizzle
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(eq(sessions.userId, userId));
|
||||
.where(
|
||||
and(eq(sessions.userId, user_id), eq(sessions.sessionToken, token)),
|
||||
);
|
||||
|
||||
if (!currentSession) throw new NotFoundException('Session not found');
|
||||
// console.log(session.length);
|
||||
|
||||
// CONFIGURACIÓN: Tiempo de gracia en milisegundos (ej: 15 segundos)
|
||||
const GRACE_PERIOD_MS = 15000;
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ESCENARIO A: Rotación Normal (El token coincide con el actual)
|
||||
// -------------------------------------------------------------------
|
||||
if (currentSession.sessionToken === refreshToken) {
|
||||
const user = await this.findUserById(userId);
|
||||
if (session.length === 0) throw new NotFoundException('session not found');
|
||||
const user = await this.findUserById(user_id);
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
|
||||
// Generar nuevos tokens (A -> B)
|
||||
const tokensNew = await this.generateTokens(user);
|
||||
const decodeAccess = this.decodeToken(tokensNew.access_token);
|
||||
const decodeRefresh = this.decodeToken(tokensNew.refresh_token);
|
||||
// Genera token
|
||||
const tokens = await this.generateTokens(user);
|
||||
const decodeAccess = this.decodeToken(tokens.access_token);
|
||||
const decodeRefresh = this.decodeToken(tokens.refresh_token);
|
||||
|
||||
// Actualizamos DB guardando el token "viejo" como "previous"
|
||||
// Actualiza session
|
||||
await this.drizzle
|
||||
.update(sessions)
|
||||
.set({
|
||||
sessionToken: tokensNew.refresh_token, // Nuevo (B)
|
||||
previousSessionToken: refreshToken, // Viejo (A)
|
||||
lastRotatedAt: new Date(), // Marca de tiempo
|
||||
expiresAt: decodeRefresh.exp,
|
||||
})
|
||||
.where(eq(sessions.userId, userId));
|
||||
.set({ sessionToken: tokens.refresh_token, expiresAt: decodeRefresh.exp })
|
||||
.where(eq(sessions.userId, user_id));
|
||||
|
||||
return {
|
||||
access_token: tokensNew.access_token,
|
||||
access_token: tokens.access_token,
|
||||
access_expire_in: decodeAccess.exp,
|
||||
refresh_token: tokensNew.refresh_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
refresh_expire_in: decodeRefresh.exp,
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ESCENARIO B: Periodo de Gracia (Condición de Carrera)
|
||||
// -------------------------------------------------------------------
|
||||
// El token no coincide con el actual, ¿pero coincide con el anterior?
|
||||
const isPreviousToken =
|
||||
currentSession.previousSessionToken === refreshToken;
|
||||
|
||||
// Calculamos cuánto tiempo ha pasado desde la rotación
|
||||
const timeSinceRotation = currentSession.lastRotatedAt
|
||||
? Date.now() - new Date(currentSession.lastRotatedAt).getTime()
|
||||
: Infinity;
|
||||
|
||||
if (isPreviousToken && timeSinceRotation < GRACE_PERIOD_MS) {
|
||||
// ¡Es una condición de carrera! El usuario envió 'A' pero ya rotamos a 'B'.
|
||||
// Le devolvemos 'B' (el actual en DB) para que se sincronice.
|
||||
|
||||
const user = await this.findUserById(userId);
|
||||
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
|
||||
// Generamos un access token nuevo fresco (barato)
|
||||
const accessTokenPayload = { sub: user.id, username: user.username };
|
||||
const newAccessToken = await this.jwtService.signAsync(
|
||||
accessTokenPayload,
|
||||
{
|
||||
secret: envs.access_token_secret,
|
||||
expiresIn: envs.access_token_expiration,
|
||||
} as JwtSignOptions,
|
||||
);
|
||||
const decodeAccess = this.decodeToken(newAccessToken);
|
||||
|
||||
// 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> {
|
||||
// Check if username or email exists
|
||||
const data = await this.drizzle
|
||||
|
||||
@@ -7,9 +7,9 @@ export class RefreshTokenDto {
|
||||
@IsString({
|
||||
message: 'Refresh token must be a string',
|
||||
})
|
||||
refreshToken: string;
|
||||
token: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
userId: number;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
||||
import * as schema from '@/database/index';
|
||||
import { states } from '@/database/schema/general';
|
||||
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, eq, ne } from 'drizzle-orm';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { CreateStateDto } from './dto/create-state.dto';
|
||||
import { UpdateStateDto } from './dto/update-state.dto';
|
||||
@@ -15,17 +15,14 @@ export class StatesService {
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<State[]> {
|
||||
return await this.drizzle
|
||||
.select()
|
||||
.from(states)
|
||||
.where(ne(states.name, 'EMBAJADA'));
|
||||
return await this.drizzle.select().from(states);
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<State> {
|
||||
const state = await this.drizzle
|
||||
.select()
|
||||
.from(states)
|
||||
.where(and(eq(states.id, id), ne(states.name, 'EMBAJADA')));
|
||||
.where(eq(states.id, id));
|
||||
|
||||
if (state.length === 0) {
|
||||
throw new HttpException('State not found', HttpStatus.NOT_FOUND);
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||
// import { Env, validateString } from '@/common/utils';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { eq, ne } from 'drizzle-orm';
|
||||
import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from 'src/database/index';
|
||||
import { municipalities, parishes, states } from 'src/database/index';
|
||||
|
||||
import { Municipality, Parish, State } from './entities/user.entity';
|
||||
import { states, municipalities, parishes } from 'src/database/index';
|
||||
import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { State, Municipality, Parish } from './entities/user.entity';
|
||||
// import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async StateAll(): Promise<State[]> {
|
||||
async StateAll(): Promise< State[]> {
|
||||
const find = await this.drizzle
|
||||
.select()
|
||||
.from(states)
|
||||
.where(ne(states.name, 'EMBAJADA'));
|
||||
|
||||
return find;
|
||||
}
|
||||
|
||||
async MunicioalityAll(id: string): Promise<Municipality[]> {
|
||||
async MunicioalityAll(id: string): Promise< Municipality[]> {
|
||||
const find = await this.drizzle
|
||||
.select()
|
||||
.from(municipalities)
|
||||
@@ -33,7 +32,7 @@ export class UsersService {
|
||||
return find;
|
||||
}
|
||||
|
||||
async ParishAll(id: string): Promise<Parish[]> {
|
||||
async ParishAll(id: string): Promise< Parish[]> {
|
||||
const find = await this.drizzle
|
||||
.select()
|
||||
.from(parishes)
|
||||
@@ -42,3 +41,4 @@ export class UsersService {
|
||||
return find;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { Optional } from '@nestjs/common';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsEmail,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
isString,
|
||||
IsString,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateTrainingDto {
|
||||
// === 1. DATOS BÁSICOS ===
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
coorFullName: string;
|
||||
firstname: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
lastname: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDateString()
|
||||
@@ -29,11 +30,11 @@ export class CreateTrainingDto {
|
||||
|
||||
// === 2. DATOS OSP ===
|
||||
@ApiProperty()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ospName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ospRif: string;
|
||||
|
||||
@ApiProperty()
|
||||
@@ -44,11 +45,6 @@ export class CreateTrainingDto {
|
||||
@IsString()
|
||||
productiveActivity: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
productiveActivityOther: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currentStatus: string;
|
||||
@@ -79,14 +75,16 @@ export class CreateTrainingDto {
|
||||
structureType?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
hasTransport?: string;
|
||||
@Transform(({ value }) => value === 'true' || value === true) // Convierte "false" -> false
|
||||
hasTransport?: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isOpenSpace?: string;
|
||||
@Transform(({ value }) => value === 'true' || value === true)
|
||||
isOpenSpace?: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@@ -126,7 +124,6 @@ export class CreateTrainingDto {
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
ospResponsibleRif: string;
|
||||
|
||||
@ApiProperty()
|
||||
@@ -134,25 +131,20 @@ export class CreateTrainingDto {
|
||||
ospResponsiblePhone: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsOptional()
|
||||
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||
@IsEmail()
|
||||
ospResponsibleEmail?: string;
|
||||
@IsString()
|
||||
ospResponsibleEmail: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
civilState: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Type(() => Number) // Convierte "3" -> 3
|
||||
familyBurden: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
numberOfChildren: number;
|
||||
|
||||
@@ -173,15 +165,21 @@ export class CreateTrainingDto {
|
||||
@IsString()
|
||||
communeSpokespersonName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communeSpokespersonCedula: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communeSpokespersonRif: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communeSpokespersonPhone: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsOptional()
|
||||
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||
@IsEmail()
|
||||
communeEmail?: string;
|
||||
@IsString()
|
||||
communeEmail: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@@ -199,66 +197,25 @@ export class CreateTrainingDto {
|
||||
@IsString()
|
||||
communalCouncilSpokespersonName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communalCouncilSpokespersonCedula: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communalCouncilSpokespersonRif: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communalCouncilSpokespersonPhone: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsOptional()
|
||||
@ValidateIf((o, v) => v !== '' && v !== null && v !== undefined)
|
||||
@IsEmail()
|
||||
communalCouncilEmail?: string;
|
||||
|
||||
// === 6. DISTRIBUCIÓN Y EXPORTACIÓN ===
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
internalDistributionZone?: string;
|
||||
communalCouncilEmail: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
isExporting?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
externalCountry?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
externalCity?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
externalDescription?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
externalQuantity?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
externalUnit?: string;
|
||||
|
||||
// === 7. MANO DE OBRA ===
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
womenCount?: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
menCount?: number;
|
||||
|
||||
// === 8. LISTAS (Arrays JSON) ===
|
||||
// === 6. LISTAS (Arrays JSON) ===
|
||||
// Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real
|
||||
|
||||
@ApiProperty()
|
||||
@@ -306,11 +263,13 @@ export class CreateTrainingDto {
|
||||
})
|
||||
productList?: any[];
|
||||
|
||||
|
||||
//ubicacion
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
state: string;
|
||||
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
municipality: string;
|
||||
@@ -318,23 +277,4 @@ export class CreateTrainingDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
parish: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo1?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo2?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo3?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
surveyStatus: string
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Header,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
StreamableFile,
|
||||
Res,
|
||||
UploadedFiles,
|
||||
UseInterceptors,
|
||||
StreamableFile,
|
||||
Header
|
||||
} from '@nestjs/common';
|
||||
import { Readable } from 'stream';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import {
|
||||
ApiConsumes,
|
||||
@@ -33,25 +34,26 @@ import { Public } from '@/common/decorators';
|
||||
export class TrainingController {
|
||||
constructor(private readonly trainingService: TrainingService) { }
|
||||
|
||||
// @Public()
|
||||
// @Get('export/:id')
|
||||
// @ApiOperation({ summary: 'Export training template' })
|
||||
// @ApiResponse({
|
||||
// status: 200,
|
||||
// description: 'Return training template.',
|
||||
// content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
|
||||
// })
|
||||
// @Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
// @Header('Content-Disposition', 'attachment; filename=export_osp.xlsx')
|
||||
// async exportTemplate(@Param('id') id: string) {
|
||||
// if (!Number(id)) {
|
||||
// throw new Error('ID is required');
|
||||
// }
|
||||
// const data = await this.trainingService.exportTemplate(Number(id));
|
||||
// return new StreamableFile(data);
|
||||
// }
|
||||
// export training with excel
|
||||
@Public()
|
||||
@Get('export/:id')
|
||||
@ApiOperation({ summary: 'Export training with excel' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Return training with excel.',
|
||||
content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
|
||||
})
|
||||
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
@Header('Content-Disposition', 'attachment; filename=export_osp.xlsx')
|
||||
async exportTemplate(@Param('id') id: string) {
|
||||
if (!Number(id)) {
|
||||
throw new Error('ID is required');
|
||||
}
|
||||
const data = await this.trainingService.exportTemplate(Number(id));
|
||||
return new StreamableFile(Readable.from([data]));
|
||||
}
|
||||
|
||||
// ========== //
|
||||
// get all training records
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Get all training records with pagination and filters',
|
||||
@@ -60,10 +62,8 @@ export class TrainingController {
|
||||
status: 200,
|
||||
description: 'Return paginated training records.',
|
||||
})
|
||||
async findAll(@Req() req: Request, @Query() paginationDto: PaginationDto) {
|
||||
const user = (req as any).user;
|
||||
|
||||
const result = await this.trainingService.findAll(paginationDto, { role: user?.roles[0], id: user?.id });
|
||||
async findAll(@Query() paginationDto: PaginationDto) {
|
||||
const result = await this.trainingService.findAll(paginationDto);
|
||||
return {
|
||||
message: 'Training records fetched successfully',
|
||||
data: result.data,
|
||||
@@ -71,7 +71,7 @@ export class TrainingController {
|
||||
};
|
||||
}
|
||||
|
||||
// ========== //
|
||||
// get training statistics
|
||||
@Get('statistics')
|
||||
@ApiOperation({ summary: 'Get training statistics' })
|
||||
@ApiResponse({ status: 200, description: 'Return training statistics.' })
|
||||
@@ -79,40 +79,8 @@ export class TrainingController {
|
||||
const data = await this.trainingService.getStatistics(filterDto);
|
||||
return { message: 'Training statistics fetched successfully', data };
|
||||
}
|
||||
// ========== //
|
||||
// @Get('export/all')
|
||||
// @ApiOperation({ summary: 'Export all training records to Excel' })
|
||||
// @ApiResponse({
|
||||
// status: 200,
|
||||
// description: 'Return training records Excel.',
|
||||
// })
|
||||
// async exportAll(@Query() filterDto: TrainingStatisticsFilterDto) {
|
||||
// const data = await this.trainingService.exportAll(filterDto);
|
||||
// return new StreamableFile(data, {
|
||||
// type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
// disposition: 'attachment; filename=training_surveys.xlsx',
|
||||
// });
|
||||
// }
|
||||
|
||||
@Public()
|
||||
@Get('export/all')
|
||||
@ApiOperation({ summary: 'Export all training records to Excel' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Return training template.',
|
||||
content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
|
||||
})
|
||||
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
@Header('Content-Disposition', 'attachment; filename=export_osp.xlsx')
|
||||
async exportAll(@Query() filterDto: TrainingStatisticsFilterDto) {
|
||||
const data = await this.trainingService.exportAll(filterDto);
|
||||
return new StreamableFile(data, {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
disposition: 'attachment; filename=training_surveys.xlsx',
|
||||
});
|
||||
}
|
||||
|
||||
// ========== //
|
||||
// get training record by id
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a training record by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Return the training record.' })
|
||||
@@ -122,7 +90,7 @@ export class TrainingController {
|
||||
return { message: 'Training record fetched successfully', data };
|
||||
}
|
||||
|
||||
// ========== //
|
||||
// create training record
|
||||
@Post()
|
||||
@UseInterceptors(FilesInterceptor('files', 3))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@@ -132,20 +100,14 @@ export class TrainingController {
|
||||
description: 'Training record created successfully.',
|
||||
})
|
||||
async create(
|
||||
@Req() req: Request,
|
||||
@Body() createTrainingDto: CreateTrainingDto,
|
||||
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||
) {
|
||||
const userId = (req as any).user?.id;
|
||||
const data = await this.trainingService.create(
|
||||
createTrainingDto,
|
||||
files,
|
||||
userId,
|
||||
);
|
||||
const data = await this.trainingService.create(createTrainingDto, files);
|
||||
return { message: 'Training record created successfully', data };
|
||||
}
|
||||
|
||||
// ========== //
|
||||
// update training record
|
||||
@Patch(':id')
|
||||
@UseInterceptors(FilesInterceptor('files', 3))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@@ -156,22 +118,19 @@ export class TrainingController {
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||
async update(
|
||||
@Req() req: Request,
|
||||
@Param('id') id: string,
|
||||
@Body() updateTrainingDto: UpdateTrainingDto,
|
||||
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||
) {
|
||||
const userId = (req as any).user?.id;
|
||||
const data = await this.trainingService.update(
|
||||
+id,
|
||||
updateTrainingDto,
|
||||
files,
|
||||
userId,
|
||||
);
|
||||
return { message: 'Training record updated successfully', data };
|
||||
}
|
||||
|
||||
// ========== //
|
||||
// delete training record
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete a training record' })
|
||||
@ApiResponse({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,5 +2,4 @@ AUTH_URL = http://localhost:3000
|
||||
AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE=
|
||||
API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NODE_ENV='development' #development | production
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function SocioproductivaStatisticsPage() {
|
||||
return (
|
||||
// <PageContainer>
|
||||
<div className="w-full p-6">
|
||||
<PageContainer>
|
||||
<div className="w-full">
|
||||
<h1 className="text-2xl font-bold mb-6">Estadísticas Socioproductivas</h1>
|
||||
<TrainingStatistics />
|
||||
</div>
|
||||
// </PageContainer>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export default function EditTrainingPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
// <PageContainer scrollable>
|
||||
<div className="p-6 space-y-6">
|
||||
<CreateTrainingForm
|
||||
defaultValues={training}
|
||||
@@ -28,5 +29,6 @@ export default function EditTrainingPage() {
|
||||
onCancel={() => router.back()}
|
||||
/>
|
||||
</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 TrainingList from '@/feactures/training/components/training-list';
|
||||
import TrainingTableAction from '@/feactures/training/components/training-tables/training-table-action';
|
||||
@@ -24,7 +24,8 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
|
||||
return (
|
||||
// <PageContainer>
|
||||
<div className="flex flex-1 flex-col space-y-6 p-6">
|
||||
// <div className="flex flex-1 flex-col space-y-6">
|
||||
< div className="p-6 space-y-6" >
|
||||
<TrainingHeader />
|
||||
<TrainingTableAction />
|
||||
<TrainingList
|
||||
@@ -33,7 +34,7 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
initialLimit={limit || 10}
|
||||
apiUrl={env.API_URL}
|
||||
/>
|
||||
</div>
|
||||
</div >
|
||||
// </PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { auth } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Dashboard() {
|
||||
console.log('La sesion es llamada');
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
|
||||
@@ -44,6 +44,7 @@ const RootLayout = async ({
|
||||
}: Readonly<{
|
||||
children: ReactNode;
|
||||
}>) => {
|
||||
console.log('La sesion es llamada');
|
||||
const session = await auth();
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
|
||||
@@ -187,9 +187,10 @@ export const COUNTRY_OPTIONS = [
|
||||
'Uruguay',
|
||||
'Uzbekistán',
|
||||
'Vanuatu',
|
||||
'Venezuela',
|
||||
'Vietnam',
|
||||
'Yemen',
|
||||
'Yibuti',
|
||||
'Zambia',
|
||||
'Zimbabue',
|
||||
'Zimbabue'
|
||||
];
|
||||
@@ -34,7 +34,7 @@ export const AdministrationItems: NavItem[] = [
|
||||
url: '/dashboard/administracion/usuario',
|
||||
icon: 'userPen',
|
||||
shortcut: ['m', 'm'],
|
||||
role: ['admin', 'superadmin'],
|
||||
role: ['admin', 'superadmin', 'autoridad'],
|
||||
},
|
||||
{
|
||||
title: 'Encuestas',
|
||||
@@ -60,7 +60,7 @@ export const StatisticsItems: NavItem[] = [
|
||||
url: '#', // Placeholder as there is no direct link for the parent
|
||||
icon: 'chartColumn',
|
||||
isActive: true,
|
||||
role: ['admin', 'superadmin', 'autoridad'],
|
||||
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||
|
||||
items: [
|
||||
// {
|
||||
@@ -82,7 +82,7 @@ export const StatisticsItems: NavItem[] = [
|
||||
shortcut: ['s', 's'],
|
||||
url: '/dashboard/estadisticas/socioproductiva',
|
||||
icon: 'blocks',
|
||||
role: ['admin', 'superadmin', 'autoridad'],
|
||||
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use server';
|
||||
import { safeFetchApi } from '@/lib';
|
||||
import { cookies } from 'next/headers';
|
||||
import { safeFetchApi } from '@/lib/fetch.api';
|
||||
import { loginResponseSchema, UserFormValue } from '../schemas/login';
|
||||
|
||||
type LoginActionSuccess = {
|
||||
@@ -18,7 +17,7 @@ type LoginActionSuccess = {
|
||||
refresh_token: string;
|
||||
refresh_expire_in: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type LoginActionError = {
|
||||
type: 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR'; // **Asegúrate de que el tipo de `type` sea este aquí**
|
||||
@@ -29,7 +28,7 @@ type LoginActionError = {
|
||||
// Si SignInAction también puede devolver null, asegúralo en su tipo de retorno
|
||||
type LoginActionResult = LoginActionSuccess | LoginActionError | null;
|
||||
|
||||
export const SignInAction = async (payload: UserFormValue) => {
|
||||
export const SignInAction = async (payload: UserFormValue): Promise<LoginActionResult> => {
|
||||
const [error, data] = await safeFetchApi(
|
||||
loginResponseSchema,
|
||||
'/auth/sign-in',
|
||||
@@ -37,22 +36,12 @@ export const SignInAction = async (payload: UserFormValue) => {
|
||||
payload,
|
||||
);
|
||||
if (error) {
|
||||
return error;
|
||||
return {
|
||||
type: error.type as 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR',
|
||||
message: error.message,
|
||||
details: error.details
|
||||
};
|
||||
} 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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
'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,3 +1,4 @@
|
||||
// auth/actions/refresh-token-action.ts
|
||||
'use server';
|
||||
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
|
||||
import {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import z from 'zod';
|
||||
|
||||
export const logoutResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
@@ -4,19 +4,13 @@ import { tokensSchema } from './login';
|
||||
|
||||
// Esquema para el refresh token
|
||||
export const refreshTokenSchema = z.object({
|
||||
refreshToken: z.string(),
|
||||
user_id: z.number(),
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
export type RefreshTokenValue = z.infer<typeof refreshTokenSchema>;
|
||||
|
||||
// Esquema final para la respuesta del backend
|
||||
// export const RefreshTokenResponseSchema = z.object({
|
||||
// // tokens: tokensSchema,
|
||||
// access_token: z.string(),
|
||||
// access_expire_in: z.number(),
|
||||
// refresh_token: z.string(),
|
||||
// refresh_expire_in: z.number()
|
||||
// });
|
||||
|
||||
export const RefreshTokenResponseSchema = tokensSchema
|
||||
|
||||
export const RefreshTokenResponseSchema = z.object({
|
||||
tokens: tokensSchema,
|
||||
});
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
'use client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@repo/shadcn/button';
|
||||
import { Heading } from '@repo/shadcn/heading';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
|
||||
export function SurveysHeader() {
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const role = session?.user.role[0]?.rol;
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start justify-between">
|
||||
@@ -16,18 +14,11 @@ export function SurveysHeader() {
|
||||
title="Administración de Encuestas"
|
||||
description="Gestiona las encuestas disponibles en la plataforma"
|
||||
/>
|
||||
{['superadmin', 'admin'].includes(role ?? '') && (
|
||||
<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 onClick={() => router.push(`/dashboard/administracion/encuestas/crear`)} size="sm">
|
||||
<Plus className="h-4 w-4"/><span className='hidden sm:inline'>Agregar Encuesta</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
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 {
|
||||
Tooltip,
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@repo/shadcn/tooltip';
|
||||
import { Edit, Trash } from 'lucide-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { SurveyTable } from '@/feactures/surveys/schemas/survey';
|
||||
import { useDeleteSurvey } from '@/feactures/surveys/hooks/use-mutation-surveys';
|
||||
|
||||
|
||||
interface CellActionProps {
|
||||
data: SurveyTable;
|
||||
@@ -23,7 +23,6 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { mutate: deleteSurvey } = useDeleteSurvey();
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
@@ -37,8 +36,6 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const role = session?.user.role[0]?.rol;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
@@ -50,20 +47,15 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
||||
description="Esta acción no se puede deshacer."
|
||||
/>
|
||||
|
||||
|
||||
<div className="flex gap-1">
|
||||
{['superadmin', 'admin'].includes(role ?? '') && (
|
||||
<>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
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" />
|
||||
</Button>
|
||||
@@ -90,8 +82,6 @@ export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
TrainingSchema,
|
||||
trainingApiResponseSchema,
|
||||
} from '../schemas/training';
|
||||
import z from 'zod';
|
||||
|
||||
export const getTrainingStatisticsAction = async (
|
||||
params: {
|
||||
@@ -91,6 +90,8 @@ export const createTrainingAction = async (
|
||||
payloadToSend = rest as any;
|
||||
}
|
||||
|
||||
// console.log(payloadToSend);
|
||||
|
||||
const [error, data] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
'/training',
|
||||
@@ -160,39 +161,3 @@ export const getTrainingByIdAction = async (id: number) => {
|
||||
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
export const exportTrainingAction = async (
|
||||
params: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
stateId?: number;
|
||||
municipalityId?: number;
|
||||
parishId?: number;
|
||||
ospType?: string;
|
||||
} = {},
|
||||
) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
|
||||
if (params.municipalityId)
|
||||
searchParams.append('municipalityId', params.municipalityId.toString());
|
||||
if (params.parishId)
|
||||
searchParams.append('parishId', params.parishId.toString());
|
||||
if (params.ospType) searchParams.append('ospType', params.ospType);
|
||||
|
||||
|
||||
const [error, response] = await safeFetchApi(
|
||||
z.any(), //Schema
|
||||
`/training/export/all?${searchParams.toString()}`,
|
||||
'GET',
|
||||
undefined,
|
||||
{ responseType: 'arraybuffer' },
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Error al exportar los datos');
|
||||
}
|
||||
|
||||
return Array.from(new Uint8Array(response));
|
||||
};
|
||||
|
||||
@@ -20,34 +20,24 @@ import { Label } from '@repo/shadcn/label';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { TrainingSchema } from '../schemas/training';
|
||||
|
||||
interface EquipmentItem {
|
||||
machine: string;
|
||||
quantity: string | number;
|
||||
}
|
||||
|
||||
export function EquipmentList() {
|
||||
const { control, register } = useFormContext<TrainingSchema>();
|
||||
const { control, register } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'equipmentList',
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [newItem, setNewItem] = useState<EquipmentItem>({
|
||||
const [newItem, setNewItem] = useState({
|
||||
machine: '',
|
||||
specifications: '',
|
||||
quantity: '',
|
||||
});
|
||||
|
||||
const handleAdd = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (newItem.machine.trim()) {
|
||||
append({
|
||||
machine: newItem.machine,
|
||||
quantity: newItem.quantity ? Number(newItem.quantity) : 0,
|
||||
});
|
||||
setNewItem({ machine: '', quantity: '' });
|
||||
const handleAdd = () => {
|
||||
if (newItem.machine && newItem.quantity) {
|
||||
append({ ...newItem, quantity: Number(newItem.quantity) });
|
||||
setNewItem({ machine: '', specifications: '', quantity: '' });
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -58,11 +48,9 @@ export function EquipmentList() {
|
||||
<h3 className="text-lg font-medium">Datos del Equipamiento</h3>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" type="button">
|
||||
Agregar Maquinaria
|
||||
</Button>
|
||||
<Button variant="outline">Agregar Maquinaria</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Agregar Maquinaria/Equipo</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -71,9 +59,8 @@ export function EquipmentList() {
|
||||
</DialogDescription>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="modal-machine">Maquinaria</Label>
|
||||
<Label>Maquinaria</Label>
|
||||
<Input
|
||||
id="modal-machine"
|
||||
value={newItem.machine}
|
||||
onChange={(e) =>
|
||||
setNewItem({ ...newItem, machine: e.target.value })
|
||||
@@ -82,9 +69,18 @@ export function EquipmentList() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="modal-quantity">Cantidad</Label>
|
||||
<Label>Especificaciones</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
|
||||
id="modal-quantity"
|
||||
type="number"
|
||||
value={newItem.quantity}
|
||||
onChange={(e) =>
|
||||
@@ -97,17 +93,12 @@ export function EquipmentList() {
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
<Button type="button" onClick={handleAdd}>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button onClick={handleAdd}>Guardar</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -119,6 +110,7 @@ export function EquipmentList() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Maquinaria</TableHead>
|
||||
<TableHead>Especificaciones</TableHead>
|
||||
<TableHead>Cantidad</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
</TableRow>
|
||||
@@ -130,27 +122,31 @@ export function EquipmentList() {
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`equipmentList.${index}.machine`)}
|
||||
defaultValue={field.machine ?? ''}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
{field.machine}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`equipmentList.${index}.quantity`)}
|
||||
defaultValue={field.quantity ?? ''}
|
||||
{...register(`equipmentList.${index}.specifications`)}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
{field.specifications}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`equipmentList.${index}.quantity`)}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
{field.quantity}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
remove(index);
|
||||
}}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
@@ -160,7 +156,7 @@ export function EquipmentList() {
|
||||
{fields.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={3}
|
||||
colSpan={4}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
No hay equipamiento registrado
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,9 @@
|
||||
import { COUNTRY_OPTIONS } from '@/constants/countries';
|
||||
import {
|
||||
useMunicipalityQuery,
|
||||
useParishQuery,
|
||||
useStateQuery,
|
||||
} from '@/feactures/location/hooks/use-query-location';
|
||||
import { Button } from '@repo/shadcn/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -17,18 +23,36 @@ import {
|
||||
} from '@repo/shadcn/components/ui/table';
|
||||
import { Input } from '@repo/shadcn/input';
|
||||
import { Label } from '@repo/shadcn/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@repo/shadcn/select';
|
||||
import { SelectSearchable } from '@repo/shadcn/select-searchable';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { TrainingSchema } from '../schemas/training';
|
||||
|
||||
const UNIT_OPTIONS = ['KG', 'TON', 'UNID', 'LT', 'MTS', 'QQ', 'HM2', 'SACOS'];
|
||||
|
||||
// 1. Definimos la estructura de los datos para que TypeScript no se queje
|
||||
// ProductItem y ProductFormValues locales eliminados en favor de TrainingSchema
|
||||
interface ProductItem {
|
||||
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() {
|
||||
const { control, register } = useFormContext<TrainingSchema>();
|
||||
// 2. Pasamos el tipo genérico a useFormContext
|
||||
const { control, register } = useFormContext<ProductFormValues>();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
@@ -39,25 +63,91 @@ export function ProductActivityList() {
|
||||
|
||||
// Modal Form State
|
||||
const [newItem, setNewItem] = useState<any>({
|
||||
productName: '',
|
||||
description: '',
|
||||
dailyCount: '',
|
||||
weeklyCount: '',
|
||||
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 = () => {
|
||||
if (newItem.description) {
|
||||
if (newItem.productName) {
|
||||
append(newItem);
|
||||
setNewItem({
|
||||
productName: '',
|
||||
description: '',
|
||||
dailyCount: '',
|
||||
weeklyCount: '',
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const stateOptions = statesData?.data || [];
|
||||
const internalMuniOptions = internalMuniData?.data || [];
|
||||
const internalParishOptions = internalParishData?.data || [];
|
||||
const externalMuniOptions = externalMuniData?.data || [];
|
||||
const externalParishOptions = externalParishData?.data || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -68,7 +158,7 @@ export function ProductActivityList() {
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Producto Terminado</DialogTitle>
|
||||
<DialogTitle>Detalles de Actividad Productiva</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription className="sr-only">
|
||||
Datos de actividad productiva
|
||||
@@ -76,6 +166,15 @@ export function ProductActivityList() {
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Basic Info */}
|
||||
<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">
|
||||
<Label>Descripción</Label>
|
||||
<Input
|
||||
@@ -120,6 +219,222 @@ export function ProductActivityList() {
|
||||
</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">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -140,10 +455,9 @@ export function ProductActivityList() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Producto/Descripción</TableHead>
|
||||
<TableHead>Producción Diario</TableHead>
|
||||
<TableHead>Producción Semanal</TableHead>
|
||||
<TableHead>Producción Mensual</TableHead>
|
||||
<TableHead>Producto</TableHead>
|
||||
<TableHead>Descripción</TableHead>
|
||||
<TableHead>Mensual</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -153,28 +467,13 @@ export function ProductActivityList() {
|
||||
<TableCell>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`productList.${index}.description`)}
|
||||
defaultValue={field.description ?? ''}
|
||||
{...register(`productList.${index}.productName`)}
|
||||
// field.productName ahora es válido gracias a la interface
|
||||
value={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}
|
||||
{field.productName}
|
||||
</TableCell>
|
||||
<TableCell>{field.dailyCount}</TableCell>
|
||||
<TableCell>{field.weeklyCount}</TableCell>
|
||||
<TableCell>{field.description}</TableCell>
|
||||
<TableCell>{field.monthlyCount}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
|
||||
@@ -17,39 +17,27 @@ import {
|
||||
} from '@repo/shadcn/components/ui/table';
|
||||
import { Input } from '@repo/shadcn/input';
|
||||
import { Label } from '@repo/shadcn/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@repo/shadcn/select';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { TrainingSchema } from '../schemas/training';
|
||||
|
||||
const UNIT_OPTIONS = ['KG', 'TON', 'UNID', 'LT', 'MTS', 'QQ', 'HM2', 'SACOS'];
|
||||
|
||||
export function ProductionList() {
|
||||
const { control, register } = useFormContext<TrainingSchema>();
|
||||
const { control, register } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'productionList',
|
||||
});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [newItem, setNewItem] = useState({
|
||||
rawMaterial: '',
|
||||
supplyType: '',
|
||||
quantity: '',
|
||||
unit: '',
|
||||
});
|
||||
|
||||
const handleAdd = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (newItem.supplyType && newItem.quantity && newItem.unit) {
|
||||
const handleAdd = () => {
|
||||
if (newItem.rawMaterial && newItem.quantity) {
|
||||
append({ ...newItem, quantity: Number(newItem.quantity) });
|
||||
setNewItem({ supplyType: '', quantity: '', unit: '' });
|
||||
setNewItem({ rawMaterial: '', supplyType: '', quantity: '' });
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -62,14 +50,24 @@ export function ProductionList() {
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Agregar Producción</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Materia prima requerida (mensual)</DialogTitle>
|
||||
<DialogTitle>Agregar Datos de Producción</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription className="sr-only">
|
||||
Datos de producción
|
||||
</DialogDescription>
|
||||
<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">
|
||||
<Label>Tipo de Insumo/Rubro</Label>
|
||||
<Input
|
||||
@@ -81,57 +79,26 @@ export function ProductionList() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Cantidad Mensual</Label>
|
||||
<div className="flex gap-2">
|
||||
<Label>Cantidad Mensual (Kg, TON, UNID. LT)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="flex-1"
|
||||
value={newItem.quantity}
|
||||
onChange={(e) =>
|
||||
setNewItem({ ...newItem, quantity: e.target.value })
|
||||
}
|
||||
placeholder="0"
|
||||
/>
|
||||
<Select
|
||||
value={newItem.unit}
|
||||
onValueChange={(val) =>
|
||||
setNewItem({ ...newItem, unit: val })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Unidad" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{UNIT_OPTIONS.map((unit) => (
|
||||
<SelectItem key={unit} value={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAdd}
|
||||
disabled={
|
||||
!newItem.supplyType || !newItem.quantity || !newItem.unit
|
||||
}
|
||||
>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button onClick={handleAdd}>Guardar</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -142,6 +109,7 @@ export function ProductionList() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Materia Prima</TableHead>
|
||||
<TableHead>Tipo Insumo</TableHead>
|
||||
<TableHead>Cantidad (Mensual)</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
@@ -153,33 +121,32 @@ export function ProductionList() {
|
||||
<TableCell>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`productionList.${index}.supplyType`)}
|
||||
defaultValue={field.supplyType ?? ''}
|
||||
{...register(`productionList.${index}.rawMaterial`)}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
{field.rawMaterial}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`productionList.${index}.supplyType`)}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
{field.supplyType}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`productionList.${index}.quantity`)}
|
||||
defaultValue={field.quantity ?? ''}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register(`productionList.${index}.unit`)}
|
||||
defaultValue={field.unit ?? ''}
|
||||
/>
|
||||
{field.quantity} {field.unit}
|
||||
{/* @ts-ignore */}
|
||||
{field.quantity}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
remove(index);
|
||||
}}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
|
||||
@@ -29,18 +29,10 @@ export default function TrainingList({
|
||||
return <DataTableSkeleton columnCount={5} rowCount={initialLimit} />;
|
||||
}
|
||||
|
||||
const transformedData =
|
||||
data?.data?.map((item) => ({
|
||||
...item,
|
||||
communeRif: item.communeRif || '',
|
||||
communeSpokespersonName: item.communeSpokespersonName || '',
|
||||
communalCouncilRif: item.communalCouncilRif || '',
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns({ apiUrl })}
|
||||
data={transformedData}
|
||||
data={data?.data || []}
|
||||
totalItems={data?.meta.totalCount || 0}
|
||||
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||
/>
|
||||
|
||||
@@ -37,8 +37,6 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
|
||||
import { exportTrainingAction } from '../actions/training-actions';
|
||||
import { Download } from 'lucide-react';
|
||||
|
||||
const OSP_TYPES = [
|
||||
'EPSD',
|
||||
@@ -91,38 +89,6 @@ export function TrainingStatistics() {
|
||||
setOspType('');
|
||||
};
|
||||
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
const bytes = await exportTrainingAction({
|
||||
startDate: startDate || undefined,
|
||||
endDate: endDate || undefined,
|
||||
stateId: stateId || undefined,
|
||||
municipalityId: municipalityId || undefined,
|
||||
parishId: parishId || undefined,
|
||||
ospType: ospType || undefined,
|
||||
});
|
||||
|
||||
const blob = new Blob([new Uint8Array(bytes)], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `entrenamientos_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error('Error exporting:', error);
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center p-8">Cargando estadísticas...</div>
|
||||
@@ -137,22 +103,12 @@ export function TrainingStatistics() {
|
||||
|
||||
const {
|
||||
totalOsps,
|
||||
// totalProducers,
|
||||
totalProducers,
|
||||
statusDistribution,
|
||||
activityDistribution,
|
||||
typeDistribution,
|
||||
stateDistribution,
|
||||
yearDistribution,
|
||||
ecoSectorDistribution,
|
||||
productiveSectorDistribution,
|
||||
centralActivityDistribution,
|
||||
mainActivityDistribution,
|
||||
structureTypeDistribution,
|
||||
isOpenSpaceDistribution,
|
||||
hasTransportDistribution,
|
||||
genderDistribution,
|
||||
municipalityDistribution,
|
||||
parishDistribution,
|
||||
} = data;
|
||||
|
||||
const COLORS = [
|
||||
@@ -250,19 +206,10 @@ export function TrainingStatistics() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex items-end">
|
||||
<Button variant="outline" onClick={handleClearFilters}>
|
||||
Limpiar Filtros
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleExport}
|
||||
disabled={isExporting}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
{isExporting ? 'Exportando...' : 'Exportar Excel'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -283,8 +230,7 @@ export function TrainingStatistics() {
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* <Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total de Productores
|
||||
@@ -296,8 +242,7 @@ export function TrainingStatistics() {
|
||||
Productores asociados
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card> */}
|
||||
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
@@ -324,8 +269,8 @@ export function TrainingStatistics() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Location Distribution (Dynamic) */}
|
||||
<Card className="col-span-full">
|
||||
{/* State Distribution */}
|
||||
{/* <Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Distribución por Estado</CardTitle>
|
||||
<CardDescription>OSP registradas por estado</CardDescription>
|
||||
@@ -345,69 +290,7 @@ export function TrainingStatistics() {
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{stateId > 0 ? (
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Distribución por Municipio</CardTitle>
|
||||
<CardDescription>OSP registradas en este estado</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={municipalityDistribution}
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#0088FE" name="Cantidad" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Distribución por Municipio</CardTitle>
|
||||
<CardDescription>Seleccione un estado</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{municipalityId > 0 ? (
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Distribución por Parroquia</CardTitle>
|
||||
<CardDescription>OSP registradas en este municipio</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={parishDistribution}
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Distribución por Parroquia</CardTitle>
|
||||
<CardDescription>Seleccione un municipio</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
)}
|
||||
</Card> */}
|
||||
|
||||
{/* Year Distribution */}
|
||||
<Card className="col-span-full lg:col-span-1">
|
||||
@@ -484,223 +367,6 @@ export function TrainingStatistics() {
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ECO SECTOR DISTRIBUTION */}
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Sector Económico</CardTitle>
|
||||
<CardDescription>
|
||||
Distribución por sector económico
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={ecoSectorDistribution}
|
||||
layout="vertical"
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis type="number" />
|
||||
<YAxis dataKey="name" type="category" width={150} />
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#0088FE" name="Cantidad" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* PRODUCTIVE SECTOR DISTRIBUTION */}
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Sector Productivo</CardTitle>
|
||||
<CardDescription>
|
||||
Distribución por sector productivo
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={productiveSectorDistribution}
|
||||
layout="vertical"
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis type="number" />
|
||||
<YAxis dataKey="name" type="category" width={150} />
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#00C49F" name="Cantidad" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CENTRAL PRODUCTIVE ACTIVITY DISTRIBUTION */}
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Actividad Central Productiva</CardTitle>
|
||||
<CardDescription>
|
||||
Distribución por actividad central productiva
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={centralActivityDistribution}
|
||||
layout="vertical"
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis type="number" />
|
||||
<YAxis dataKey="name" type="category" width={150} />
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#FF8042" name="Cantidad" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* MAIN PRODUCTIVE ACTIVITY DISTRIBUTION */}
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Actividad Productiva Principal</CardTitle>
|
||||
<CardDescription>
|
||||
Distribución por actividad productiva principal
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={mainActivityDistribution}
|
||||
layout="vertical"
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis type="number" />
|
||||
<YAxis dataKey="name" type="category" width={150} />
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* STRUCTURE TYPE DISTRIBUTION */}
|
||||
<Card className="col-span-full lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Tipo de Estructura</CardTitle>
|
||||
<CardDescription>Distribución física</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={structureTypeDistribution}
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* GENDER DISTRIBUTION */}
|
||||
<Card className="col-span-full lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Distribución de Género</CardTitle>
|
||||
<CardDescription>Conteo total por género</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[400px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={genderDistribution}
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#8884d8" name="Personas" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* OPEN SPACE AND TRANSPORT (PIE CHARTS) */}
|
||||
<div className="col-span-full grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Espacio Abierto</CardTitle>
|
||||
<CardDescription>¿Poseen áreas libres?</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={isOpenSpaceDistribution}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
>
|
||||
{isOpenSpaceDistribution.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={COLORS[(index + 2) % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transporte</CardTitle>
|
||||
<CardDescription>¿Tienen vehículo propio?</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={hasTransportDistribution}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
>
|
||||
{hasTransportDistribution.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={COLORS[(index + 4) % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||
<Legend />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@repo/shadcn/tooltip';
|
||||
import { Edit, Eye, Trash } from 'lucide-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { Edit, Eye, Trash, FileDown } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { TrainingViewModal } from '../training-view-modal';
|
||||
@@ -26,7 +25,6 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
const [viewOpen, setViewOpen] = useState(false);
|
||||
const { mutate: deleteTraining } = useDeleteTraining();
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
@@ -40,29 +38,9 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Mapear roles a minúsculas para comparación segura
|
||||
const userRoles = session?.user?.role?.map((r) => r.rol.toLowerCase()) || [];
|
||||
|
||||
const isAdminOrSuper = userRoles.some((r) =>
|
||||
['superadmin', 'admin'].includes(r),
|
||||
);
|
||||
|
||||
// Soporta tanto 'coordinator' como 'coordinador'
|
||||
const isCoordinator = userRoles.some(r =>
|
||||
r.includes('coordinator') || r.includes('coordinador')
|
||||
);
|
||||
|
||||
const isOtherAuthorized = userRoles.some((r) =>
|
||||
['autoridad', 'manager'].includes(r),
|
||||
);
|
||||
|
||||
// El creador del registro: intentamos createdBy o created_by por si acaso
|
||||
const createdBy = data.createdBy ?? (data as any).created_by;
|
||||
|
||||
// Comparación robusta de IDs
|
||||
const isOwner = createdBy !== undefined &&
|
||||
createdBy !== null &&
|
||||
Number(createdBy) === Number(session?.user?.id);
|
||||
const handleExport = (id?: number | undefined) => {
|
||||
window.open(`${apiUrl}/training/export/${id}`, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -82,8 +60,6 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
/>
|
||||
|
||||
<div className="flex gap-1">
|
||||
{/* VER DETALLE: superadmin, admin, autoridad, manager, or owner coordinator */}
|
||||
{(isAdminOrSuper || isOtherAuthorized || (isCoordinator && isOwner)) && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -100,10 +76,24 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* EDITAR: Superadmin, admin OR (coordinator if owner) */}
|
||||
{(isAdminOrSuper || (isCoordinator && isOwner)) && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<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>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -122,10 +112,7 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* ELIMINAR: Solo superadmin y admin */}
|
||||
{isAdminOrSuper && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -142,7 +129,6 @@ export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ export function columns({ apiUrl }: ColumnsProps): ColumnDef<TrainingSchema>[] {
|
||||
},
|
||||
{
|
||||
accessorKey: 'currentStatus',
|
||||
header: 'Estatus OSP',
|
||||
header: 'Estatus',
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('currentStatus') as string;
|
||||
return (
|
||||
@@ -35,26 +35,13 @@ export function columns({ apiUrl }: ColumnsProps): ColumnDef<TrainingSchema>[] {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Fecha de creación',
|
||||
accessorKey: 'visitDate',
|
||||
header: 'Fecha Visita',
|
||||
cell: ({ row }) => {
|
||||
// console.log(row.getValue('created_at'));
|
||||
const date = row.getValue('created_at') as string;
|
||||
const date = row.getValue('visitDate') as string;
|
||||
return date ? new Date(date).toLocaleString() : 'N/A';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'surveyStatus',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('surveyStatus') as string;
|
||||
return (
|
||||
<Badge variant={status === 'COMPLETADA' ? 'success' : 'secondary'}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
|
||||
@@ -3,15 +3,12 @@
|
||||
import { Button } from '@repo/shadcn/components/ui/button';
|
||||
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTrainingTableFilters } from './use-training-table-filters';
|
||||
|
||||
export default function TrainingTableAction() {
|
||||
const { searchQuery, setPage, setSearchQuery } = useTrainingTableFilters();
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const role = session?.user.role[0]?.rol;
|
||||
return (
|
||||
<div className="flex items-center justify-between mt-4 ">
|
||||
<div className="flex items-center gap-4 flex-grow">
|
||||
@@ -22,9 +19,6 @@ export default function TrainingTableAction() {
|
||||
setPage={setPage}
|
||||
/>
|
||||
</div>{' '}
|
||||
{['superadmin', 'autoridad', 'admin', 'manager', 'coordinators'].includes(
|
||||
role ?? '',
|
||||
) && (
|
||||
<Button
|
||||
onClick={() => router.push(`/dashboard/formulario/nuevo`)}
|
||||
size="sm"
|
||||
@@ -32,7 +26,6 @@ export default function TrainingTableAction() {
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden md:inline">Nuevo Registro</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
useMunicipalityQuery,
|
||||
useParishQuery,
|
||||
useStateQuery,
|
||||
} from '@/feactures/location/hooks/use-query-location';
|
||||
import { Badge } from '@repo/shadcn/badge';
|
||||
import { Button } from '@repo/shadcn/button';
|
||||
import {
|
||||
@@ -22,6 +17,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@repo/shadcn/components/ui/dialog';
|
||||
import { ScrollArea } from '@repo/shadcn/components/ui/scroll-area';
|
||||
import { Separator } from '@repo/shadcn/components/ui/separator';
|
||||
import {
|
||||
ExternalLink,
|
||||
Factory,
|
||||
@@ -32,6 +28,11 @@ import {
|
||||
} from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import { TrainingSchema } from '../schemas/training';
|
||||
import {
|
||||
useMunicipalityQuery,
|
||||
useParishQuery,
|
||||
useStateQuery,
|
||||
} from '@/feactures/location/hooks/use-query-location';
|
||||
|
||||
interface TrainingViewModalProps {
|
||||
data: TrainingSchema | null;
|
||||
@@ -52,9 +53,7 @@ export function TrainingViewModal({
|
||||
|
||||
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(
|
||||
(m: any) => m.id === data.municipality,
|
||||
)?.name;
|
||||
@@ -95,15 +94,12 @@ export function TrainingViewModal({
|
||||
</Card>
|
||||
);
|
||||
|
||||
const BooleanBadge = ({ value }: { value?: boolean | null }) => (
|
||||
const BooleanBadge = ({ value }: { value?: boolean }) => (
|
||||
<Badge variant={value ? 'default' : 'secondary'}>
|
||||
{value ? 'Sí' : 'No'}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
// console.log(data);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
@@ -131,7 +127,10 @@ export function TrainingViewModal({
|
||||
<div className="space-y-8">
|
||||
{/* 1. Datos de la Visita */}
|
||||
<Section title="Datos de la Visita">
|
||||
<DetailItem label="Coordinador" value={data.coorFullName} />
|
||||
<DetailItem
|
||||
label="Coordinador"
|
||||
value={`${data.firstname} ${data.lastname}`}
|
||||
/>
|
||||
<DetailItem label="Teléfono Coord." value={data.coorPhone} />
|
||||
<DetailItem
|
||||
label="Fecha Visita"
|
||||
@@ -161,17 +160,12 @@ export function TrainingViewModal({
|
||||
label="Actividad Principal"
|
||||
value={data.mainProductiveActivity}
|
||||
/>
|
||||
{/* <div className="sm-col-span-full"> */}
|
||||
<div className="col-span-full">
|
||||
<DetailItem
|
||||
label="Actividad Específica"
|
||||
value={data.productiveActivity}
|
||||
/>
|
||||
|
||||
{data.productiveActivity == 'OTRO' && (<DetailItem
|
||||
label="Otra Actividad Específica"
|
||||
value={data.productiveActivityOther}
|
||||
/>)}
|
||||
{/* </div> */}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 3. Infraestructura y Ubicación */}
|
||||
@@ -213,11 +207,7 @@ export function TrainingViewModal({
|
||||
className="gap-2"
|
||||
>
|
||||
<a
|
||||
href={
|
||||
data.ospGoogleMapsLink.startsWith('http')
|
||||
? data.ospGoogleMapsLink
|
||||
: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(data.ospGoogleMapsLink)}`
|
||||
}
|
||||
href={data.ospGoogleMapsLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@@ -237,36 +227,74 @@ export function TrainingViewModal({
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Package className="h-5 w-5" />
|
||||
Productos Registrados
|
||||
Productos y Mano de Obra
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{data.productList?.length || 0}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{data.productList?.map((prod: any, idx: number) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-muted/40 p-4 rounded-lg border text-sm"
|
||||
>
|
||||
<h4 className="font-bold text-base text-primary mb-2">
|
||||
{prod.description}
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h4 className="font-bold text-base text-primary">
|
||||
{prod.productName}
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<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}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
||||
<DetailItem label="Diario" value={prod.dailyCount} />
|
||||
<DetailItem label="Semanal" value={prod.weeklyCount} />
|
||||
<DetailItem label="Mensual" value={prod.monthlyCount} />
|
||||
<DetailItem
|
||||
label="Semanal"
|
||||
value={prod.weeklyCount}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Mensual"
|
||||
value={prod.monthlyCount}
|
||||
label="Hombres / Mujeres"
|
||||
value={`${prod.menCount || 0} / ${prod.womenCount || 0}`}
|
||||
/>
|
||||
</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>
|
||||
{(!data.productList || data.productList.length === 0) && (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No hay productos registrados.
|
||||
@@ -275,64 +303,6 @@ export function TrainingViewModal({
|
||||
</CardContent>
|
||||
</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 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
@@ -390,9 +360,7 @@ export function TrainingViewModal({
|
||||
{mat.supplyType}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
Cant: {mat.quantity} {mat.unit}
|
||||
</Badge>
|
||||
<Badge variant="secondary">Cant: {mat.quantity}</Badge>
|
||||
</div>
|
||||
))}
|
||||
{(!data.productionList ||
|
||||
@@ -446,12 +414,12 @@ export function TrainingViewModal({
|
||||
label="Teléfono"
|
||||
value={data.ospResponsiblePhone}
|
||||
/>
|
||||
{/* <DetailItem label="Email" value={data.ospResponsibleEmail} />
|
||||
<DetailItem label="Email" value={data.ospResponsibleEmail} />
|
||||
<DetailItem
|
||||
label="Carga Familiar"
|
||||
value={data.familyBurden}
|
||||
/>
|
||||
<DetailItem label="Hijos" value={data.numberOfChildren} /> */}
|
||||
<DetailItem label="Hijos" value={data.numberOfChildren} />
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -495,7 +463,7 @@ export function TrainingViewModal({
|
||||
onClick={() => setSelectedImage(photo)}
|
||||
>
|
||||
<img
|
||||
src={`${photo}`}
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
|
||||
alt={`Evidencia ${idx + 1}`}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
@@ -545,7 +513,7 @@ export function TrainingViewModal({
|
||||
</Button>
|
||||
{selectedImage && (
|
||||
<img
|
||||
src={`${selectedImage}`}
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
|
||||
alt="Vista ampliada"
|
||||
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',
|
||||
TRANSFORMACION_MATERIA: 'TRANSFORMACION DE LA MATERIA PRIMA',
|
||||
TEXTIL: 'TALLER DE COFECCION TEXTIL',
|
||||
CONSTRUCCION: 'CONSTRUCCION',
|
||||
CONSTRUCCION: 'CONSTRUCION',
|
||||
BIENES_SERVICIOS: 'OFRECER PRODUCTOS DE BIENES Y SERVICIOS',
|
||||
VISITAS_GUIADAS: 'VISITAS GUIADAS',
|
||||
ALOJAMIENTO: 'ALOJAMIENTO',
|
||||
@@ -107,7 +107,6 @@ export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
|
||||
'SIEMBRA DE ARROZ',
|
||||
'SIEMBRA DE CEREALES (CEBADA, LINAZA, SOYA)',
|
||||
'ELABORACION DE BIO-INSUMO (ABONO ORGANICO)',
|
||||
'OTRO'
|
||||
],
|
||||
[ACTIVIDAD_PRINCIPAL.CRIA]: [
|
||||
'BOVINO',
|
||||
@@ -116,9 +115,8 @@ export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
|
||||
'CUNICULTURA',
|
||||
'AVICOLA',
|
||||
'PISCICULA',
|
||||
'OTRO'
|
||||
],
|
||||
[ACTIVIDAD_PRINCIPAL.PATIOS_PRODUCTIVOS]: ['SIEMBRA Y CRIA', 'OTRO'],
|
||||
[ACTIVIDAD_PRINCIPAL.PATIOS_PRODUCTIVOS]: ['SIEMBRA Y CRIA'],
|
||||
[ACTIVIDAD_PRINCIPAL.TRANSFORMACION_MATERIA]: [
|
||||
'ELABORACION DE PRODUCTOS QUIMICOS (LIMPIEZA E HIGIENE PERSONAL)',
|
||||
'PANADERIAS',
|
||||
@@ -130,10 +128,7 @@ export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
|
||||
'ELABORACION DE ACEITE COMESTIBLE',
|
||||
'FABRICA DE HIELO',
|
||||
'ELABORACION DE PAPELON',
|
||||
'TORREFACTORA DE CÁFE',
|
||||
'ESPULPADORA DE TOMATES Y FRUTAS',
|
||||
'ARTESANIAS',
|
||||
'OTRO'
|
||||
],
|
||||
[ACTIVIDAD_PRINCIPAL.TEXTIL]: [
|
||||
'ELABORACION DE UNIFORME ESCOLARES Y PRENDA DE VESTIR',
|
||||
@@ -141,14 +136,12 @@ export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
|
||||
'ELABORACION DE LENCERIA',
|
||||
'SUBLIMACION DE TEJIDOS',
|
||||
'ELABORACION DE CALZADOS',
|
||||
'OTRO'
|
||||
],
|
||||
[ACTIVIDAD_PRINCIPAL.CONSTRUCCION]: [
|
||||
'BLOQUERAS',
|
||||
'PLANTA PREMEZCLADORA DE CEMENTO',
|
||||
'CARPINTERIAS',
|
||||
'HERRERIAS',
|
||||
'OTRO'
|
||||
],
|
||||
[ACTIVIDAD_PRINCIPAL.BIENES_SERVICIOS]: [
|
||||
'MERCADOS COMUNALES',
|
||||
@@ -162,17 +155,15 @@ export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
|
||||
'REPARACION DE CALZADOS',
|
||||
'TALLER DE MECANICA',
|
||||
'TRANSPORTES',
|
||||
'OTRO'
|
||||
],
|
||||
[ACTIVIDAD_PRINCIPAL.VISITAS_GUIADAS]: ['RUTAS TURISTICAS', 'OTRO'],
|
||||
[ACTIVIDAD_PRINCIPAL.ALOJAMIENTO]: ['POSADAS', 'HOTELES', 'OTRO'],
|
||||
[ACTIVIDAD_PRINCIPAL.TURISMO]: ['AGENCIAS DE VIAJES', 'OTRO'],
|
||||
[ACTIVIDAD_PRINCIPAL.VISITAS_GUIADAS]: ['RUTAS TURISTICAS'],
|
||||
[ACTIVIDAD_PRINCIPAL.ALOJAMIENTO]: ['POSADAS', 'HOTELES'],
|
||||
[ACTIVIDAD_PRINCIPAL.TURISMO]: ['AGENCIAS DE VIAJES'],
|
||||
[ACTIVIDAD_PRINCIPAL.COMERCIO]: [
|
||||
'VENTA DE VIVERES',
|
||||
'VENTAS DE PRENDAS DE VESTIR',
|
||||
'VENTA DE PRODUCTOS QUIMICOS Y DERIVADOS',
|
||||
'BODEGAS COMUNALES',
|
||||
'FRIGORIFICOS Y CARNICOS',
|
||||
'OTRO'
|
||||
],
|
||||
};
|
||||
|
||||
@@ -10,23 +10,13 @@ export const statisticsItemSchema = z.object({
|
||||
|
||||
export const trainingStatisticsSchema = z.object({
|
||||
totalOsps: z.number(),
|
||||
// totalProducers: z.number(),
|
||||
totalProducers: z.number(),
|
||||
totalProducts: z.number(),
|
||||
statusDistribution: z.array(statisticsItemSchema),
|
||||
activityDistribution: z.array(statisticsItemSchema),
|
||||
typeDistribution: z.array(statisticsItemSchema),
|
||||
stateDistribution: z.array(statisticsItemSchema),
|
||||
yearDistribution: z.array(statisticsItemSchema),
|
||||
ecoSectorDistribution: z.array(statisticsItemSchema),
|
||||
productiveSectorDistribution: z.array(statisticsItemSchema),
|
||||
centralActivityDistribution: z.array(statisticsItemSchema),
|
||||
mainActivityDistribution: z.array(statisticsItemSchema),
|
||||
structureTypeDistribution: z.array(statisticsItemSchema),
|
||||
isOpenSpaceDistribution: z.array(statisticsItemSchema),
|
||||
hasTransportDistribution: z.array(statisticsItemSchema),
|
||||
genderDistribution: z.array(statisticsItemSchema),
|
||||
municipalityDistribution: z.array(statisticsItemSchema),
|
||||
parishDistribution: z.array(statisticsItemSchema),
|
||||
});
|
||||
|
||||
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
|
||||
|
||||
@@ -3,52 +3,66 @@ import { z } from 'zod';
|
||||
// 1. Definimos el esquema de un item individual de la lista de productos
|
||||
// Basado en los campos que usaste en ProductActivityList
|
||||
const productItemSchema = z.object({
|
||||
description: z.string().optional().nullable(),
|
||||
dailyCount: z.coerce.string().or(z.number()).optional().nullable(),
|
||||
weeklyCount: z.coerce.string().or(z.number()).optional().nullable(),
|
||||
monthlyCount: z.coerce.string().or(z.number()).optional().nullable(),
|
||||
productName: z.string(),
|
||||
description: z.string().optional(),
|
||||
dailyCount: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
|
||||
weeklyCount: z.coerce.string().or(z.number()).optional(),
|
||||
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({
|
||||
supplyType: z.string().optional().nullable(),
|
||||
quantity: z.coerce.string().or(z.number()).optional().nullable(),
|
||||
unit: z.string().min(1, { message: 'Unidad es requerida' }).nullable(),
|
||||
rawMaterial: z.string(),
|
||||
supplyType: z.string().optional(),
|
||||
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
|
||||
});
|
||||
|
||||
const equipmentItemSchema = z.object({
|
||||
machine: z.string().nullable(),
|
||||
quantity: z.coerce.string().or(z.number()).optional().nullable(),
|
||||
machine: z.string(),
|
||||
specifications: z.string().optional(),
|
||||
quantity: z.coerce.string().or(z.number()).optional(), // Aceptamos string o number por los inputs
|
||||
});
|
||||
|
||||
export const trainingSchema = z.object({
|
||||
//Datos de la visita
|
||||
id: z.number().optional(),
|
||||
coorFullName: z
|
||||
.string()
|
||||
.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',
|
||||
}),
|
||||
firstname: z.string().min(1, { message: 'Nombre es requerido' }),
|
||||
lastname: z.string().min(1, { message: 'Apellido es requerido' }),
|
||||
coorPhone: z.string().optional().nullable(),
|
||||
visitDate: z
|
||||
.string()
|
||||
.min(1, { message: 'Fecha y hora de visita es requerida' }),
|
||||
|
||||
//Datos de la organización socioproductiva (OSP)
|
||||
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
|
||||
ecoSector: z.string({ message: 'Sector Económico es requerido' }),
|
||||
productiveSector: z.string({ message: 'Sector Productivo es requerido' }),
|
||||
centralProductiveActivity: z.string({
|
||||
message: 'Actividad Central Productiva es requerido',
|
||||
}),
|
||||
mainProductiveActivity: z.string({
|
||||
message: 'Actividad Productiva Principal es requerida',
|
||||
}),
|
||||
productiveActivity: z.string({
|
||||
message: 'Actividad Productiva es requerida',
|
||||
}),
|
||||
productiveActivityOther: z.string().min(1, { message: 'Otra actividad productiva es requerida' }).optional(),
|
||||
ospRif: z.string().optional(),
|
||||
ospName: z.string().optional(),
|
||||
ecoSector: z.string().optional().or(z.literal('')),
|
||||
productiveSector: z.string().optional().or(z.literal('')),
|
||||
centralProductiveActivity: z.string().optional().or(z.literal('')),
|
||||
mainProductiveActivity: z.string().optional().or(z.literal('')),
|
||||
productiveActivity: z
|
||||
.string()
|
||||
.min(1, { message: 'Actividad productiva es requerida' }),
|
||||
ospRif: z.string().optional().or(z.literal('')),
|
||||
ospName: z.string().min(1, { message: 'Nombre de la OSP es requerido' }),
|
||||
companyConstitutionYear: z.coerce
|
||||
.number()
|
||||
.min(1900, { message: 'Año inválido' }),
|
||||
@@ -56,25 +70,15 @@ export const trainingSchema = z.object({
|
||||
.string()
|
||||
.min(1, { message: 'Estatus actual es requerido' })
|
||||
.default('ACTIVA'),
|
||||
infrastructureMt2: z.string({ message: 'Infraestructura es requerida' }).optional(),
|
||||
infrastructureMt2: z.string().optional().or(z.literal('')),
|
||||
hasTransport: z
|
||||
.preprocess(
|
||||
(val) => val === 'true' || val === true || val === 1 || val === '1',
|
||||
z.boolean(),
|
||||
)
|
||||
.optional()
|
||||
.nullable()
|
||||
.default(false),
|
||||
structureType: z.string({ message: 'Tipo de estructura es requerido' }),
|
||||
.preprocess((val) => val === 'true' || val === true, z.boolean())
|
||||
.optional(),
|
||||
structureType: z.string().optional().or(z.literal('')),
|
||||
isOpenSpace: z
|
||||
.preprocess(
|
||||
(val) => val === 'true' || val === true || val === 1 || val === '1',
|
||||
z.boolean(),
|
||||
)
|
||||
.optional()
|
||||
.nullable()
|
||||
.default(false),
|
||||
paralysisReason: z.string().optional(),
|
||||
.preprocess((val) => val === 'true' || val === true, z.boolean())
|
||||
.optional(),
|
||||
paralysisReason: z.string().optional().default(''),
|
||||
|
||||
//Datos del Equipamiento
|
||||
equipmentList: z.array(equipmentItemSchema).optional().default([]),
|
||||
@@ -85,52 +89,18 @@ export const trainingSchema = z.object({
|
||||
// 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(),
|
||||
externalCity: z.string().optional(),
|
||||
externalDescription: z.string().optional(),
|
||||
externalQuantity: z.coerce.string().or(z.number()).optional(),
|
||||
externalUnit: z.string().optional(),
|
||||
|
||||
// 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
|
||||
ospAddress: z
|
||||
.string()
|
||||
.min(1, { message: 'Dirección de la OSP es requerida' }),
|
||||
ospGoogleMapsLink: z.string().optional().or(z.literal('')),
|
||||
communeName: z
|
||||
.string()
|
||||
.min(1, { message: 'Nombre de la comuna es requerida' }),
|
||||
siturCodeCommune: z
|
||||
.string()
|
||||
.min(1, { message: 'Código SITUR de la comuna es requerida' }),
|
||||
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',
|
||||
}),
|
||||
communeName: z.string().optional().or(z.literal('')),
|
||||
siturCodeCommune: z.string().optional().or(z.literal('')),
|
||||
communeRif: z.string().optional().or(z.literal('')),
|
||||
communeSpokespersonName: z.string().optional().or(z.literal('')),
|
||||
communeSpokespersonCedula: z.string().optional().or(z.literal('')),
|
||||
communeSpokespersonRif: z.string().optional().or(z.literal('')),
|
||||
communeSpokespersonPhone: z.string().optional().or(z.literal('')),
|
||||
communeEmail: z
|
||||
.string()
|
||||
.email({ message: 'Correo electrónico de la Comuna inválido' })
|
||||
@@ -139,22 +109,12 @@ export const trainingSchema = z.object({
|
||||
communalCouncil: z
|
||||
.string()
|
||||
.min(1, { message: 'Consejo Comunal es requerido' }),
|
||||
siturCodeCommunalCouncil: z
|
||||
.string()
|
||||
.min(1, { message: 'Código SITUR del Consejo Comunal es requerido' }),
|
||||
communalCouncilRif: z
|
||||
.string()
|
||||
.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',
|
||||
}),
|
||||
siturCodeCommunalCouncil: z.string().optional().or(z.literal('')),
|
||||
communalCouncilRif: z.string().optional().or(z.literal('')),
|
||||
communalCouncilSpokespersonName: z.string().optional().or(z.literal('')),
|
||||
communalCouncilSpokespersonCedula: z.string().optional().or(z.literal('')),
|
||||
communalCouncilSpokespersonRif: z.string().optional().or(z.literal('')),
|
||||
communalCouncilSpokespersonPhone: z.string().optional().or(z.literal('')),
|
||||
communalCouncilEmail: z
|
||||
.string()
|
||||
.email({ message: 'Correo electrónico del Consejo Comunal inválido' })
|
||||
@@ -168,156 +128,46 @@ export const trainingSchema = z.object({
|
||||
ospResponsibleFullname: z
|
||||
.string()
|
||||
.min(1, { message: 'Nombre del responsable es requerido' }),
|
||||
ospResponsibleRif: z.string().optional().nullable(),
|
||||
civilState: z.string().optional().nullable(),
|
||||
ospResponsibleRif: z
|
||||
.string()
|
||||
.min(1, { message: 'RIF del responsable es requerido' }),
|
||||
civilState: z.string().min(1, { message: 'Estado civil es requerido' }),
|
||||
ospResponsiblePhone: z
|
||||
.string()
|
||||
.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',
|
||||
}),
|
||||
.min(1, { message: 'Teléfono del responsable es requerido' }),
|
||||
ospResponsibleEmail: z
|
||||
.string()
|
||||
.email({ message: 'Correo electrónico inválido' })
|
||||
.optional()
|
||||
.or(z.literal(''))
|
||||
.nullable(),
|
||||
|
||||
familyBurden: z.coerce.number().optional(),
|
||||
numberOfChildren: z.coerce.number().optional(),
|
||||
.email({ message: 'Correo electrónico inválido' }),
|
||||
familyBurden: z.coerce
|
||||
.number()
|
||||
.min(0, { message: 'Carga familiar requerida' }),
|
||||
numberOfChildren: z.coerce
|
||||
.number()
|
||||
.min(0, { message: 'Número de hijos requerido' }),
|
||||
|
||||
//Datos adicionales
|
||||
generalObservations: z.string().optional(),
|
||||
generalObservations: z.string().optional().default(''),
|
||||
|
||||
//IMAGENES
|
||||
files: z.any().optional(),
|
||||
|
||||
//no se envia la backend al crear ni editar el formulario
|
||||
state: z.number({ message: 'El estado es requerido' }).nullable(),
|
||||
municipality: z.number({ message: 'Municipio es requerido' }).nullable(),
|
||||
parish: z.number({ message: 'Parroquia es requerido' }).nullable(),
|
||||
state: z.number().optional().nullable(),
|
||||
municipality: z.number().optional().nullable(),
|
||||
parish: z.number().optional().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(),
|
||||
surveyStatus: z.string()
|
||||
});
|
||||
|
||||
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(),
|
||||
productiveActivityOther: z.string(),
|
||||
ospRif: z.string().optional().or(z.literal('')),
|
||||
ospName: z.string().optional().or(z.literal('')),
|
||||
companyConstitutionYear: z.coerce.number(),
|
||||
currentStatus: z.string(),
|
||||
infrastructureMt2: z.string().optional(),
|
||||
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(),
|
||||
//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(),
|
||||
externalCity: z.string().optional(),
|
||||
externalDescription: z.string().optional(),
|
||||
externalQuantity: z.coerce.string().or(z.number()).optional(),
|
||||
externalUnit: z.string().optional(),
|
||||
// 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('')),
|
||||
communeName: z.string(),
|
||||
siturCodeCommune: z.string(),
|
||||
communeRif: z.string().or(z.literal('')),
|
||||
communeSpokespersonName: z.string().or(z.literal('')),
|
||||
communeSpokespersonPhone: z.string(),
|
||||
communeEmail: z.string().optional().or(z.literal('')),
|
||||
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(),
|
||||
civilState: z.string().optional(),
|
||||
ospResponsiblePhone: z.string(),
|
||||
ospResponsibleEmail: z.string(),
|
||||
familyBurden: z.coerce.number().optional(),
|
||||
numberOfChildren: z.coerce.number().optional(),
|
||||
//Datos adicionales
|
||||
generalObservations: z.string().optional(),
|
||||
//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(),
|
||||
surveyStatus: z.string()
|
||||
});
|
||||
|
||||
// Para mostrar datos
|
||||
export const trainingApiResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
data: z.array(getTrainingSchema),
|
||||
data: z.array(trainingSchema),
|
||||
meta: z.object({
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
@@ -330,8 +180,7 @@ export const trainingApiResponseSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
//
|
||||
export const TrainingMutate = z.object({
|
||||
message: z.string(),
|
||||
data: getTrainingSchema,
|
||||
data: trainingSchema,
|
||||
});
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
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 { logoutAction } from '@/feactures/auth/actions/logout-action';
|
||||
import { resfreshTokenAction } from '@/feactures/auth/actions/refresh-token-action';
|
||||
import { CredentialsSignin, NextAuthConfig, Session, User } from 'next-auth';
|
||||
import { DefaultJWT, JWT } from 'next-auth/jwt';
|
||||
// import { DefaultJWT } from 'next-auth/jwt';
|
||||
import CredentialProvider from 'next-auth/providers/credentials';
|
||||
|
||||
|
||||
// Define los tipos para tus respuestas de SignInAction
|
||||
interface SignInSuccessResponse {
|
||||
message: string;
|
||||
@@ -57,10 +57,8 @@ const authConfig: NextAuthConfig = {
|
||||
|
||||
// **NUEVO: Manejar el caso `null` primero**
|
||||
if (response === null) {
|
||||
console.error(
|
||||
'SignInAction returned null, indicating a potential issue before API call or generic error.',
|
||||
);
|
||||
throw new CredentialsSignin('Error de inicio de sesión inesperado.');
|
||||
console.error("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
|
||||
@@ -71,19 +69,15 @@ const authConfig: NextAuthConfig = {
|
||||
response.type === 'UNKNOWN_ERROR') // Incluye todos los tipos de error posibles
|
||||
) {
|
||||
// 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)) {
|
||||
// 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'.
|
||||
// Es un caso de respuesta inesperada del API.
|
||||
console.error(
|
||||
"Respuesta de SignInAction con formato inesperado: falta la propiedad 'user'.",
|
||||
);
|
||||
throw new CredentialsSignin(
|
||||
'Error en el formato de la respuesta del servidor.',
|
||||
);
|
||||
console.error("Respuesta de SignInAction con formato inesperado: falta la propiedad 'user'.");
|
||||
throw new CredentialsSignin("Error en el formato de la respuesta del servidor.");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -94,6 +88,8 @@ const authConfig: NextAuthConfig = {
|
||||
role: response?.user.rol ?? [], // Add role array
|
||||
access_token: response?.tokens.access_token ?? '',
|
||||
access_expire_in: response?.tokens.access_expire_in ?? 0,
|
||||
refresh_token: response?.tokens.refresh_token ?? '',
|
||||
refresh_expire_in: response?.tokens.refresh_expire_in ?? 0,
|
||||
};
|
||||
},
|
||||
}),
|
||||
@@ -102,7 +98,7 @@ const authConfig: NextAuthConfig = {
|
||||
signIn: '/', //sigin page
|
||||
},
|
||||
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
|
||||
// El `user` solo se proporciona en el primer inicio de sesión.
|
||||
if (user) {
|
||||
@@ -114,14 +110,54 @@ const authConfig: NextAuthConfig = {
|
||||
role: user.role,
|
||||
access_token: user.access_token,
|
||||
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;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null; // Fallo al renovar, forzar logout
|
||||
}
|
||||
},
|
||||
async session({ session, token }: { session: Session; token: DefaultJWT }) {
|
||||
async session({ session, token }: { session: Session; token: any }) {
|
||||
session.access_token = token.access_token as string;
|
||||
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 = {
|
||||
id: token.id as number,
|
||||
username: token.username as string,
|
||||
@@ -129,21 +165,11 @@ const authConfig: NextAuthConfig = {
|
||||
email: token.email as string,
|
||||
role: token.role as Array<{ id: number; rol: string }>,
|
||||
};
|
||||
console.log("Session: Habilitado");
|
||||
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;
|
||||
|
||||
export default authConfig;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server';
|
||||
import { env } from '@/lib/env';
|
||||
import axios, { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Crear instancia de Axios con la URL base validada
|
||||
@@ -10,21 +10,33 @@ const fetchApi = axios.create({
|
||||
|
||||
// Interceptor para incluir el token automáticamente en las peticiones
|
||||
// ESTE INTERCEPTOR ESTÁ BIEN PARA EL RESTO DE LAS PETICIONES AUTENTICADAS
|
||||
fetchApi.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
fetchApi.interceptors.request.use(async (config: any) => {
|
||||
try {
|
||||
const { getValidAccessToken } = await import('@/lib/auth-token');
|
||||
const token = await getValidAccessToken();
|
||||
// console.log("Solicitando autenticación...");
|
||||
|
||||
const { auth } = await import('@/lib/auth'); // Importación dinámica
|
||||
const session = await auth();
|
||||
const token = session?.access_token;
|
||||
|
||||
if (token) {
|
||||
config.headers.set('Authorization', `Bearer ${token}`);
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error getting auth token:', err);
|
||||
|
||||
// **Importante:** Si el body es FormData, elimina el Content-Type para que Axios lo configure automáticamente.
|
||||
if (config.data instanceof FormData) {
|
||||
delete config.headers['Content-Type'];
|
||||
} else {
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
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
|
||||
export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
||||
@@ -32,7 +44,6 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
||||
url: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig,
|
||||
): Promise<
|
||||
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
|
||||
> => {
|
||||
@@ -41,7 +52,6 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
...config,
|
||||
});
|
||||
|
||||
const parsed = schema.safeParse(response.data);
|
||||
|
||||
99
apps/web/lib/fetch.api2.ts
Normal file
99
apps/web/lib/fetch.api2.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
'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,6 +4,8 @@ declare module 'next-auth' {
|
||||
interface Session extends DefaultSession {
|
||||
access_token: string;
|
||||
access_expire_in: number;
|
||||
refresh_token: string;
|
||||
refresh_expire_in: number;
|
||||
user: {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -27,6 +29,8 @@ declare module 'next-auth' {
|
||||
}>;
|
||||
access_token: string;
|
||||
access_expire_in: number;
|
||||
refresh_token: string;
|
||||
refresh_expire_in: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,5 +46,7 @@ declare module 'next-auth/jwt' {
|
||||
}>;
|
||||
access_token: string;
|
||||
access_expire_in: number;
|
||||
refresh_token: string;
|
||||
refresh_expire_in: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from '@repo/shadcn/lib/utils';
|
||||
import { cn } from "@repo/shadcn/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70',
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70",
|
||||
outline:
|
||||
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
success:
|
||||
'border-transparent bg-green-500 text-black [a&]:hover:bg-green-300',
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'span'> &
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'span';
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -42,7 +40,7 @@ function Badge({
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export { Badge, badgeVariants }
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: hsl(51, 76%, 97%);
|
||||
--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(17, 86%, 45%);
|
||||
--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, 2%, 31%);
|
||||
--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%);
|
||||
@@ -33,13 +33,13 @@
|
||||
--chart-3: hsl(197 37% 24%);
|
||||
--chart-4: hsl(43 74% 66%);
|
||||
--chart-5: hsl(27 87% 67%);
|
||||
--sidebar-background: hsl(27, 92%, 90%);
|
||||
--sidebar-foreground: hsl(0, 0%, 1%);
|
||||
--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(24, 82%, 67%);
|
||||
--sidebar-accent: hsl(240 4.8% 95.9%);
|
||||
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
||||
--sidebar-border: hsl(20, 13%, 91%);
|
||||
--sidebar-border: hsl(220 13% 91%);
|
||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||
--sidebar: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
@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