Compare commits
29 Commits
inventory
...
export_exc
| Author | SHA1 | Date | |
|---|---|---|---|
| f1bdce317f | |||
| 63c39e399e | |||
| 26fb849fa3 | |||
| 2566e5e9a7 | |||
| 8efe595f73 | |||
| d2908f1e4c | |||
| 69843e9e68 | |||
| 5c080c6d32 | |||
| 08a5567d60 | |||
| 69b3aab02a | |||
| b8b11259cd | |||
| 6482e692b3 | |||
| c1d1626e9e | |||
| 824685723b | |||
| 127e3b0e7a | |||
| ee499abcf9 | |||
| 949d54e590 | |||
| 24bc0476e6 | |||
| 01c7bd149d | |||
| d3b3fa5e85 | |||
| efa1726223 | |||
| 28d51a9c00 | |||
| c1d4a40244 | |||
| 6f8a55b8fd | |||
| e2105ccbf5 | |||
| d71c25f0ff | |||
| 08fa179276 | |||
| 5cd663a653 | |||
| 5137c07c88 |
@@ -15,5 +15,5 @@ DATABASE_URL="postgresql://postgres:local**@localhost:5432/caja_ahorro" #url con
|
|||||||
|
|
||||||
#Mail Configuration
|
#Mail Configuration
|
||||||
MAIL_HOST=gmail
|
MAIL_HOST=gmail
|
||||||
MAIL_USERNAME=
|
MAIL_USERNAME="123"
|
||||||
MAIL_PASSWORD=
|
MAIL_PASSWORD="123"
|
||||||
|
|||||||
3
apps/api/.gitignore
vendored
3
apps/api/.gitignore
vendored
@@ -54,3 +54,6 @@ pids
|
|||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
/uploads/training/*
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true,
|
"deleteOutDir": true,
|
||||||
"builder": "swc",
|
"builder": "swc",
|
||||||
"typeCheck": true
|
"typeCheck": true,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"include": "features/training/export_template/*.xlsx",
|
||||||
|
"outDir": "dist",
|
||||||
|
"watchAssets": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"@nestjs/platform-express": "11.0.0",
|
"@nestjs/platform-express": "11.0.0",
|
||||||
"dotenv": "16.5.0",
|
"dotenv": "16.5.0",
|
||||||
"drizzle-orm": "0.40.0",
|
"drizzle-orm": "0.40.0",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
"joi": "17.13.3",
|
"joi": "17.13.3",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
@@ -49,7 +50,8 @@
|
|||||||
"pg": "8.13.3",
|
"pg": "8.13.3",
|
||||||
"pino-pretty": "13.0.0",
|
"pino-pretty": "13.0.0",
|
||||||
"reflect-metadata": "0.2.0",
|
"reflect-metadata": "0.2.0",
|
||||||
"rxjs": "7.8.1"
|
"rxjs": "7.8.1",
|
||||||
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs-modules/mailer": "^2.0.2",
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import { ThrottlerGuard } from '@nestjs/throttler';
|
|||||||
import { DrizzleModule } from './database/drizzle.module';
|
import { DrizzleModule } from './database/drizzle.module';
|
||||||
import { AuthModule } from './features/auth/auth.module';
|
import { AuthModule } from './features/auth/auth.module';
|
||||||
import { ConfigurationsModule } from './features/configurations/configurations.module';
|
import { ConfigurationsModule } from './features/configurations/configurations.module';
|
||||||
import { LocationModule} from './features/location/location.module'
|
import { LocationModule } from './features/location/location.module'
|
||||||
import { MailModule } from './features/mail/mail.module';
|
import { MailModule } from './features/mail/mail.module';
|
||||||
import { RolesModule } from './features/roles/roles.module';
|
import { RolesModule } from './features/roles/roles.module';
|
||||||
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
||||||
import { SurveysModule } from './features/surveys/surveys.module';
|
import { SurveysModule } from './features/surveys/surveys.module';
|
||||||
import {InventoryModule} from './features/inventory/inventory.module'
|
import { InventoryModule } from './features/inventory/inventory.module';
|
||||||
import { PicturesModule } from './features/pictures/pictures.module';
|
import { TrainingModule } from './features/training/training.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -61,7 +61,7 @@ import { PicturesModule } from './features/pictures/pictures.module';
|
|||||||
SurveysModule,
|
SurveysModule,
|
||||||
LocationModule,
|
LocationModule,
|
||||||
InventoryModule,
|
InventoryModule,
|
||||||
PicturesModule
|
TrainingModule
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule { }
|
||||||
|
|||||||
36
apps/api/src/common/pipes/image-processing.pipe.ts
Normal file
36
apps/api/src/common/pipes/image-processing.pipe.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable, PipeTransform } from '@nestjs/common';
|
||||||
|
import * as path from 'path';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImageProcessingPipe implements PipeTransform {
|
||||||
|
async transform(
|
||||||
|
files: Express.Multer.File[] | Express.Multer.File,
|
||||||
|
): Promise<Express.Multer.File[] | Express.Multer.File> {
|
||||||
|
if (!files) return files;
|
||||||
|
|
||||||
|
const processItem = async (
|
||||||
|
file: Express.Multer.File,
|
||||||
|
): Promise<Express.Multer.File> => {
|
||||||
|
const processedBuffer = await sharp(file.buffer)
|
||||||
|
.webp({ quality: 80 })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const originalName = path.parse(file.originalname).name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
buffer: processedBuffer,
|
||||||
|
originalname: `${originalName}.webp`,
|
||||||
|
mimetype: 'image/webp',
|
||||||
|
size: processedBuffer.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(files)) {
|
||||||
|
return await Promise.all(files.map((file) => processItem(file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await processItem(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
apps/api/src/database/migrations/0008_plain_scream.sql
Normal file
37
apps/api/src/database/migrations/0008_plain_scream.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
CREATE TABLE "training_surveys" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"firstname" text NOT NULL,
|
||||||
|
"lastname" text NOT NULL,
|
||||||
|
"visit_date" timestamp NOT NULL,
|
||||||
|
"productive_activity" text NOT NULL,
|
||||||
|
"financial_requirement_description" text NOT NULL,
|
||||||
|
"situr_code_commune" text NOT NULL,
|
||||||
|
"communal_council" text NOT NULL,
|
||||||
|
"situr_code_communal_council" text NOT NULL,
|
||||||
|
"osp_name" text NOT NULL,
|
||||||
|
"osp_address" text NOT NULL,
|
||||||
|
"osp_rif" text NOT NULL,
|
||||||
|
"osp_type" text NOT NULL,
|
||||||
|
"current_status" text NOT NULL,
|
||||||
|
"company_constitution_year" integer NOT NULL,
|
||||||
|
"producer_count" integer NOT NULL,
|
||||||
|
"product_description" text NOT NULL,
|
||||||
|
"installed_capacity" text NOT NULL,
|
||||||
|
"operational_capacity" text NOT NULL,
|
||||||
|
"osp_responsible_fullname" text NOT NULL,
|
||||||
|
"osp_responsible_cedula" text NOT NULL,
|
||||||
|
"osp_responsible_rif" text NOT NULL,
|
||||||
|
"osp_responsible_phone" text NOT NULL,
|
||||||
|
"civil_state" text NOT NULL,
|
||||||
|
"family_burden" integer NOT NULL,
|
||||||
|
"number_of_children" integer NOT NULL,
|
||||||
|
"general_observations" text NOT NULL,
|
||||||
|
"photo1" text NOT NULL,
|
||||||
|
"photo2" text NOT NULL,
|
||||||
|
"photo3" text NOT NULL,
|
||||||
|
"paralysis_reason" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp (3)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "training_surveys_index_00" ON "training_surveys" USING btree ("firstname");
|
||||||
7
apps/api/src/database/migrations/0009_eminent_ares.sql
Normal file
7
apps/api/src/database/migrations/0009_eminent_ares.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "state" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "municipality" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "parish" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "osp_responsible_email" text NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_state_states_id_fk" FOREIGN KEY ("state") REFERENCES "public"."states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_municipality_municipalities_id_fk" FOREIGN KEY ("municipality") REFERENCES "public"."municipalities"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_parish_parishes_id_fk" FOREIGN KEY ("parish") REFERENCES "public"."parishes"("id") ON DELETE set null ON UPDATE no action;
|
||||||
4
apps/api/src/database/migrations/0010_dashing_bishop.sql
Normal file
4
apps/api/src/database/migrations/0010_dashing_bishop.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "current_status" SET DEFAULT 'ACTIVA';--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "photo2" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "photo3" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_count" integer DEFAULT 0 NOT NULL;
|
||||||
14
apps/api/src/database/migrations/0011_magical_thundra.sql
Normal file
14
apps/api/src/database/migrations/0011_magical_thundra.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_name" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_rif" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_spokesperson_name" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_spokesperson_cedula" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_spokesperson_rif" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_spokesperson_phone" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "commune_email" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_rif" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_spokesperson_name" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_spokesperson_cedula" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_spokesperson_rif" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_spokesperson_phone" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "communal_council_email" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "osp_google_maps_link" text DEFAULT '' NOT NULL;
|
||||||
20
apps/api/src/database/migrations/0012_sudden_venus.sql
Normal file
20
apps/api/src/database/migrations/0012_sudden_venus.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_state" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_municipality" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_parish" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "coor_phone" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "eco_sector" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "productive_sector" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "central_productive_activity" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "main_productive_activity" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "types_of_equipment" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "equipment_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "equipment_description" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "raw_material" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "material_type" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "raw_material_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_count_daily" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_count_weekly" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_count_monthly" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_coor_state_states_id_fk" FOREIGN KEY ("coor_state") REFERENCES "public"."states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_coor_municipality_municipalities_id_fk" FOREIGN KEY ("coor_municipality") REFERENCES "public"."municipalities"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_coor_parish_parishes_id_fk" FOREIGN KEY ("coor_parish") REFERENCES "public"."parishes"("id") ON DELETE set null ON UPDATE no action;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "financial_requirement_description" SET DEFAULT '';--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "prod_description_internal" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "internal_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "prod_description_external" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "country" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "city" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "men_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "women_count" integer DEFAULT 0 NOT NULL;
|
||||||
9
apps/api/src/database/migrations/0014_deep_meteorite.sql
Normal file
9
apps/api/src/database/migrations/0014_deep_meteorite.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "infrastructure_mt2" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "has_transport" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "structure_type" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "is_open_space" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "equipment_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "production_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "product_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "internal_distribution_list" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "external_distribution_list" jsonb DEFAULT '[]'::jsonb NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "photo1" DROP NOT NULL;
|
||||||
36
apps/api/src/database/migrations/0016_silent_tag.sql
Normal file
36
apps/api/src/database/migrations/0016_silent_tag.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_state_states_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_municipality_municipalities_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP CONSTRAINT "training_surveys_coor_parish_parishes_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "general_observations" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ALTER COLUMN "paralysis_reason" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "financial_requirement_description";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "producer_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_description";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "installed_capacity";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "operational_capacity";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "coor_state";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "coor_municipality";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "coor_parish";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "types_of_equipment";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "equipment_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "equipment_description";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "raw_material";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "material_type";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "raw_material_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_count_daily";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_count_weekly";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "product_count_monthly";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "prod_description_internal";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "internal_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "external_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "prod_description_external";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "country";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "city";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "men_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "women_count";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "internal_distribution_list";--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" DROP COLUMN "external_distribution_list";
|
||||||
1778
apps/api/src/database/migrations/meta/0008_snapshot.json
Normal file
1778
apps/api/src/database/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1842
apps/api/src/database/migrations/meta/0009_snapshot.json
Normal file
1842
apps/api/src/database/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1850
apps/api/src/database/migrations/meta/0010_snapshot.json
Normal file
1850
apps/api/src/database/migrations/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1948
apps/api/src/database/migrations/meta/0011_snapshot.json
Normal file
1948
apps/api/src/database/migrations/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2102
apps/api/src/database/migrations/meta/0012_snapshot.json
Normal file
2102
apps/api/src/database/migrations/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2159
apps/api/src/database/migrations/meta/0013_snapshot.json
Normal file
2159
apps/api/src/database/migrations/meta/0013_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2222
apps/api/src/database/migrations/meta/0014_snapshot.json
Normal file
2222
apps/api/src/database/migrations/meta/0014_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
2222
apps/api/src/database/migrations/meta/0015_snapshot.json
Normal file
2222
apps/api/src/database/migrations/meta/0015_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1994
apps/api/src/database/migrations/meta/0016_snapshot.json
Normal file
1994
apps/api/src/database/migrations/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,69 @@
|
|||||||
"when": 1754420096323,
|
"when": 1754420096323,
|
||||||
"tag": "0007_curved_fantastic_four",
|
"tag": "0007_curved_fantastic_four",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764623430844,
|
||||||
|
"tag": "0008_plain_scream",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764883378610,
|
||||||
|
"tag": "0009_eminent_ares",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769097895095,
|
||||||
|
"tag": "0010_dashing_bishop",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769618795008,
|
||||||
|
"tag": "0011_magical_thundra",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769621656400,
|
||||||
|
"tag": "0012_sudden_venus",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 13,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769629815868,
|
||||||
|
"tag": "0013_cuddly_night_nurse",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 14,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769646908602,
|
||||||
|
"tag": "0014_deep_meteorite",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769648728698,
|
||||||
|
"tag": "0015_concerned_wild_pack",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769653021994,
|
||||||
|
"tag": "0016_silent_tag",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { sql } from 'drizzle-orm';
|
||||||
import * as t from 'drizzle-orm/pg-core';
|
import * as t from 'drizzle-orm/pg-core';
|
||||||
import { eq, lt, gte, ne, sql } from 'drizzle-orm';
|
|
||||||
import { timestamps } from '../timestamps';
|
import { timestamps } from '../timestamps';
|
||||||
import { users } from './auth';
|
import { users } from './auth';
|
||||||
|
import { municipalities, parishes, states } from './general';
|
||||||
|
|
||||||
// Tabla surveys
|
// Tabla surveys
|
||||||
export const surveys = t.pgTable(
|
export const surveys = t.pgTable(
|
||||||
@@ -18,9 +18,7 @@ export const surveys = t.pgTable(
|
|||||||
...timestamps,
|
...timestamps,
|
||||||
},
|
},
|
||||||
(surveys) => ({
|
(surveys) => ({
|
||||||
surveysIndex: t
|
surveysIndex: t.index('surveys_index_00').on(surveys.title),
|
||||||
.index('surveys_index_00')
|
|
||||||
.on(surveys.title),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -44,7 +42,118 @@ export const answersSurveys = t.pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tabla training_surveys
|
||||||
|
export const trainingSurveys = t.pgTable(
|
||||||
|
'training_surveys',
|
||||||
|
{
|
||||||
|
// === 1. IDENTIFICADORES Y DATOS DE VISITA ===
|
||||||
|
id: t.serial('id').primaryKey(),
|
||||||
|
firstname: t.text('firstname').notNull(),
|
||||||
|
lastname: t.text('lastname').notNull(),
|
||||||
|
visitDate: t.timestamp('visit_date').notNull(),
|
||||||
|
coorPhone: t.text('coor_phone'),
|
||||||
|
|
||||||
|
// === 2. UBICACIÓN (Claves Foráneas - Nullables) ===
|
||||||
|
state: t
|
||||||
|
.integer('state')
|
||||||
|
.references(() => states.id, { onDelete: 'set null' }),
|
||||||
|
municipality: t
|
||||||
|
.integer('municipality')
|
||||||
|
.references(() => municipalities.id, { onDelete: 'set null' }),
|
||||||
|
parish: t
|
||||||
|
.integer('parish')
|
||||||
|
.references(() => parishes.id, { onDelete: 'set null' }),
|
||||||
|
|
||||||
|
// === 3. DATOS DE LA OSP (Organización Socioproductiva) ===
|
||||||
|
ospType: t.text('osp_type').notNull(), // UPF, EPS, etc.
|
||||||
|
ecoSector: t.text('eco_sector').notNull().default(''),
|
||||||
|
productiveSector: t.text('productive_sector').notNull().default(''),
|
||||||
|
centralProductiveActivity: t
|
||||||
|
.text('central_productive_activity')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
mainProductiveActivity: t
|
||||||
|
.text('main_productive_activity')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
productiveActivity: t.text('productive_activity').notNull(),
|
||||||
|
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(''),
|
||||||
|
hasTransport: t.boolean('has_transport').notNull().default(false),
|
||||||
|
structureType: t.text('structure_type').notNull().default(''),
|
||||||
|
isOpenSpace: t.boolean('is_open_space').notNull().default(false),
|
||||||
|
paralysisReason: t.text('paralysis_reason'),
|
||||||
|
equipmentList: t.jsonb('equipment_list').notNull().default([]),
|
||||||
|
productionList: t.jsonb('production_list').notNull().default([]),
|
||||||
|
productList: t.jsonb('product_list').notNull().default([]),
|
||||||
|
ospAddress: t.text('osp_address').notNull(),
|
||||||
|
ospGoogleMapsLink: t.text('osp_google_maps_link').notNull().default(''),
|
||||||
|
communeName: t.text('commune_name').notNull().default(''),
|
||||||
|
siturCodeCommune: t.text('situr_code_commune').notNull(),
|
||||||
|
communeRif: t.text('commune_rif').notNull().default(''),
|
||||||
|
communeSpokespersonName: t
|
||||||
|
.text('commune_spokesperson_name')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
communeSpokespersonCedula: t
|
||||||
|
.text('commune_spokesperson_cedula')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
communeSpokespersonRif: t
|
||||||
|
.text('commune_spokesperson_rif')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
communeSpokespersonPhone: t
|
||||||
|
.text('commune_spokesperson_phone')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
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(''),
|
||||||
|
communalCouncilSpokespersonName: t
|
||||||
|
.text('communal_council_spokesperson_name')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
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()
|
||||||
|
.default(''),
|
||||||
|
communalCouncilEmail: t
|
||||||
|
.text('communal_council_email')
|
||||||
|
.notNull()
|
||||||
|
.default(''),
|
||||||
|
ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
|
||||||
|
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
|
||||||
|
ospResponsibleRif: t.text('osp_responsible_rif').notNull(),
|
||||||
|
civilState: t.text('civil_state').notNull(),
|
||||||
|
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
|
||||||
|
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'),
|
||||||
|
photo1: t.text('photo1'),
|
||||||
|
photo2: t.text('photo2'),
|
||||||
|
photo3: t.text('photo3'),
|
||||||
|
...timestamps,
|
||||||
|
},
|
||||||
|
(trainingSurveys) => ({
|
||||||
|
trainingSurveysIndex: t
|
||||||
|
.index('training_surveys_index_00')
|
||||||
|
.on(trainingSurveys.firstname),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const viewSurveys = t.pgView('v_surveys', {
|
export const viewSurveys = t.pgView('v_surveys', {
|
||||||
surverId: t.integer('survey_id'),
|
surverId: t.integer('survey_id'),
|
||||||
@@ -52,6 +161,7 @@ export const viewSurveys = t.pgView('v_surveys', {
|
|||||||
description: t.text('description'),
|
description: t.text('description'),
|
||||||
created_at: t.timestamp('created_at'),
|
created_at: t.timestamp('created_at'),
|
||||||
closingDate: t.date('closing_date'),
|
closingDate: t.date('closing_date'),
|
||||||
targetAudience: t.varchar('target_audience')
|
targetAudience: t.varchar('target_audience'),
|
||||||
}).as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
|
})
|
||||||
|
.as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
|
||||||
where published = true`);
|
where published = true`);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// api/src/feacture/auth/auth.controller.ts
|
||||||
import { Public } from '@/common/decorators';
|
import { Public } from '@/common/decorators';
|
||||||
import { JwtRefreshGuard } from '@/common/guards/jwt-refresh.guard';
|
import { JwtRefreshGuard } from '@/common/guards/jwt-refresh.guard';
|
||||||
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
||||||
@@ -51,24 +52,21 @@ export class AuthController {
|
|||||||
// return { message: 'Password reset link sent to your email' };
|
// return { message: 'Password reset link sent to your email' };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@UseGuards(JwtRefreshGuard)
|
// @UseGuards(JwtRefreshGuard)
|
||||||
@Public()
|
@Public()
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@Patch('refresh')
|
@Patch('refresh')
|
||||||
//@RequirePermissions('auth:refresh-token')
|
//@RequirePermissions('auth:refresh-token')
|
||||||
async refreshToken(@Req() req: Request,@Body() refreshTokenDto: RefreshTokenDto) {
|
async refreshToken(@Body() refreshTokenDto: any) {
|
||||||
|
|
||||||
// console.log("Pepeeeee");
|
console.log('refreshTokenDto', refreshTokenDto);
|
||||||
// console.log(req['user']);
|
|
||||||
// console.log("refreshTokenDto",refreshTokenDto);
|
|
||||||
// console.log(typeof refreshTokenDto);
|
|
||||||
|
|
||||||
const data = await this.authService.refreshToken(refreshTokenDto,req['user'].sub);
|
const data = await this.authService.refreshToken(refreshTokenDto);
|
||||||
// console.log("data",data);
|
|
||||||
|
|
||||||
if (!data) {
|
// console.log('data', data);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
return {tokens: data}
|
return {tokens: data}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
// auth.service
|
||||||
import { envs } from '@/common/config/envs';
|
import { envs } from '@/common/config/envs';
|
||||||
import { Env, validateString } from '@/common/utils';
|
import { Env, validateString } from '@/common/utils';
|
||||||
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
||||||
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
||||||
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
|
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
|
||||||
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
|
||||||
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
||||||
|
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
||||||
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
|
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
|
||||||
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
|
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
|
||||||
import {
|
import {
|
||||||
@@ -23,14 +24,14 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { and, eq, or } from 'drizzle-orm';
|
import { and, eq, or } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import * as schema from 'src/database/index';
|
import * as schema from 'src/database/index';
|
||||||
import { sessions, users, roles, usersRole } from 'src/database/index';
|
import { roles, sessions, users, usersRole } from 'src/database/index';
|
||||||
import { Session } from './interfaces/session.interface';
|
import { Session } from './interfaces/session.interface';
|
||||||
import * as bcrypt from 'bcryptjs';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -80,33 +81,43 @@ export class AuthService {
|
|||||||
|
|
||||||
//Generate Tokens
|
//Generate Tokens
|
||||||
async generateTokens(user: User): Promise<AuthTokensInterface> {
|
async generateTokens(user: User): Promise<AuthTokensInterface> {
|
||||||
|
const accessTokenSecret = envs.access_token_secret ?? '';
|
||||||
|
const accessTokenExp = envs.access_token_expiration ?? '';
|
||||||
|
const refreshTokenSecret = envs.refresh_token_secret ?? '';
|
||||||
|
const refreshTokenExp = envs.refresh_token_expiration ?? '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
!accessTokenSecret ||
|
||||||
|
!accessTokenExp ||
|
||||||
|
!refreshTokenSecret ||
|
||||||
|
!refreshTokenExp
|
||||||
|
) {
|
||||||
|
throw new Error('JWT environment variables are missing or invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JwtPayload {
|
||||||
|
sub: number;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
sub: Number(user?.id),
|
||||||
|
username: user.username ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
const [access_token, refresh_token] = await Promise.all([
|
const [access_token, refresh_token] = await Promise.all([
|
||||||
this.jwtService.signAsync(
|
this.jwtService.signAsync(payload, {
|
||||||
{
|
secret: accessTokenSecret,
|
||||||
sub: user.id,
|
expiresIn: accessTokenExp,
|
||||||
username: user.username,
|
} as JwtSignOptions),
|
||||||
},
|
|
||||||
{
|
this.jwtService.signAsync(payload, {
|
||||||
secret: envs.access_token_secret,
|
secret: refreshTokenSecret,
|
||||||
expiresIn: envs.access_token_expiration,
|
expiresIn: refreshTokenExp,
|
||||||
},
|
} as JwtSignOptions),
|
||||||
),
|
|
||||||
this.jwtService.signAsync(
|
|
||||||
{
|
|
||||||
sub: user.id,
|
|
||||||
username: user.username,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secret: envs.refresh_token_secret,
|
|
||||||
expiresIn: envs.refresh_token_expiration,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return { access_token, refresh_token };
|
||||||
access_token,
|
|
||||||
refresh_token,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Generate OTP Code For Email Confirmation
|
//Generate OTP Code For Email Confirmation
|
||||||
@@ -137,7 +148,8 @@ export class AuthService {
|
|||||||
userId: parseInt(userId),
|
userId: parseInt(userId),
|
||||||
expiresAt: sessionInput.expiresAt,
|
expiresAt: sessionInput.expiresAt,
|
||||||
});
|
});
|
||||||
if (session.rowCount === 0) throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);
|
if (session.rowCount === 0)
|
||||||
|
throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);
|
||||||
|
|
||||||
return 'Session created successfully';
|
return 'Session created successfully';
|
||||||
}
|
}
|
||||||
@@ -196,7 +208,6 @@ export class AuthService {
|
|||||||
|
|
||||||
//Sign In User Account
|
//Sign In User Account
|
||||||
async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
|
async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
|
||||||
|
|
||||||
const user = await this.validateUser(dto);
|
const user = await this.validateUser(dto);
|
||||||
const tokens = await this.generateTokens(user);
|
const tokens = await this.generateTokens(user);
|
||||||
const decodeAccess = this.decodeToken(tokens.access_token);
|
const decodeAccess = this.decodeToken(tokens.access_token);
|
||||||
@@ -261,18 +272,24 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Refresh User Access Token
|
//Refresh User Access Token
|
||||||
async refreshToken(dto: RefreshTokenDto,user_id:number): Promise<RefreshTokenInterface> {
|
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
|
||||||
// const { user_id } = dto;
|
const secret = envs.refresh_token_secret;
|
||||||
// const user_id = 1;
|
const { user_id, token } = dto;
|
||||||
|
|
||||||
|
console.log('secret', secret);
|
||||||
|
console.log('refresh_token', token);
|
||||||
|
|
||||||
|
const validation = await this.jwtService.verifyAsync(token, {
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!validation) throw new UnauthorizedException('Invalid refresh token');
|
||||||
|
|
||||||
const session = await this.drizzle
|
const session = await this.drizzle
|
||||||
.select()
|
.select()
|
||||||
.from(sessions)
|
.from(sessions)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(sessions.userId, user_id), eq(sessions.sessionToken, token)),
|
||||||
eq(sessions.userId, user_id) &&
|
|
||||||
eq(sessions.sessionToken, dto.refresh_token),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log(session.length);
|
// console.log(session.length);
|
||||||
@@ -301,75 +318,83 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async singUp(createUserDto: SingUpUserDto): Promise<User> {
|
async singUp(createUserDto: SingUpUserDto): Promise<User> {
|
||||||
// Check if username or email exists
|
// Check if username or email exists
|
||||||
const data = await this.drizzle
|
const data = await this.drizzle
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
username: users.username,
|
||||||
|
email: users.email,
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
eq(users.username, createUserDto.username),
|
||||||
|
eq(users.email, createUserDto.email),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
if (data[0].username === createUserDto.username) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Username already exists',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data[0].email === createUserDto.email) {
|
||||||
|
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
return await this.drizzle.transaction(async (tx) => {
|
||||||
|
// Hash the password
|
||||||
|
// Create the user
|
||||||
|
const [newUser] = await tx
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
username: createUserDto.username,
|
||||||
|
email: createUserDto.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
fullname: createUserDto.fullname,
|
||||||
|
isActive: true,
|
||||||
|
state: createUserDto.state,
|
||||||
|
municipality: createUserDto.municipality,
|
||||||
|
parish: createUserDto.parish,
|
||||||
|
phone: createUserDto.phone,
|
||||||
|
isEmailVerified: false,
|
||||||
|
isTwoFactorEnabled: false,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// check if user role is admin
|
||||||
|
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
|
||||||
|
// check if user role is admin
|
||||||
|
|
||||||
|
// Assign role to user
|
||||||
|
await tx.insert(usersRole).values({
|
||||||
|
userId: newUser.id,
|
||||||
|
roleId: role,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the created user with role
|
||||||
|
const [userWithRole] = await tx
|
||||||
.select({
|
.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
username: users.username,
|
username: users.username,
|
||||||
email: users.email
|
email: users.email,
|
||||||
|
fullname: users.fullname,
|
||||||
|
phone: users.phone,
|
||||||
|
isActive: users.isActive,
|
||||||
|
role: roles.name,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(or(eq(users.username, createUserDto.username), eq(users.email, createUserDto.email)));
|
.leftJoin(usersRole, eq(usersRole.userId, users.id))
|
||||||
|
.leftJoin(roles, eq(roles.id, usersRole.roleId))
|
||||||
if (data.length > 0) {
|
.where(eq(users.id, newUser.id));
|
||||||
if (data[0].username === createUserDto.username) {
|
|
||||||
throw new HttpException('Username already exists', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
if (data[0].email === createUserDto.email) {
|
|
||||||
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the password
|
|
||||||
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
|
||||||
|
|
||||||
// Start a transaction
|
|
||||||
return await this.drizzle.transaction(async (tx) => {
|
|
||||||
// Create the user
|
|
||||||
const [newUser] = await tx
|
|
||||||
.insert(users)
|
|
||||||
.values({
|
|
||||||
username: createUserDto.username,
|
|
||||||
email: createUserDto.email,
|
|
||||||
password: hashedPassword,
|
|
||||||
fullname: createUserDto.fullname,
|
|
||||||
isActive: true,
|
|
||||||
state: createUserDto.state,
|
|
||||||
municipality: createUserDto.municipality,
|
|
||||||
parish: createUserDto.parish,
|
|
||||||
phone: createUserDto.phone,
|
|
||||||
isEmailVerified: false,
|
|
||||||
isTwoFactorEnabled: false,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// check if user role is admin
|
|
||||||
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
|
|
||||||
|
|
||||||
// Assign role to user
|
|
||||||
await tx.insert(usersRole).values({
|
|
||||||
userId: newUser.id,
|
|
||||||
roleId: role,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the created user with role
|
|
||||||
const [userWithRole] = await tx
|
|
||||||
.select({
|
|
||||||
id: users.id,
|
|
||||||
username: users.username,
|
|
||||||
email: users.email,
|
|
||||||
fullname: users.fullname,
|
|
||||||
phone: users.phone,
|
|
||||||
isActive: users.isActive,
|
|
||||||
role: roles.name,
|
|
||||||
})
|
|
||||||
.from(users)
|
|
||||||
.leftJoin(usersRole, eq(usersRole.userId, users.id))
|
|
||||||
.leftJoin(roles, eq(roles.id, usersRole.roleId))
|
|
||||||
.where(eq(users.id, newUser.id));
|
|
||||||
|
|
||||||
return userWithRole;
|
|
||||||
})
|
|
||||||
|
|
||||||
|
return userWithRole;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// refresh-token
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsNumber, IsString } from 'class-validator';
|
import { IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
@@ -6,9 +7,9 @@ export class RefreshTokenDto {
|
|||||||
@IsString({
|
@IsString({
|
||||||
message: 'Refresh token must be a string',
|
message: 'Refresh token must be a string',
|
||||||
})
|
})
|
||||||
refresh_token: string;
|
token: string;
|
||||||
|
|
||||||
// @ApiProperty()
|
@ApiProperty()
|
||||||
// @IsNumber()
|
@IsNumber()
|
||||||
// user_id: number;
|
user_id: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Controller, Post, UploadedFiles, UseInterceptors, Body } from '@nestjs/common';
|
|
||||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
|
||||||
import { PicturesService } from './pictures.service';
|
|
||||||
|
|
||||||
@Controller('pictures')
|
|
||||||
export class PicturesController {
|
|
||||||
constructor(private readonly picturesService: PicturesService) {}
|
|
||||||
|
|
||||||
@Post('upload')
|
|
||||||
@UseInterceptors(FilesInterceptor('urlImg'))
|
|
||||||
async uploadFile(@UploadedFiles() files: Express.Multer.File[], @Body() body: any) {
|
|
||||||
// Aquí puedes acceder a los campos del formulario
|
|
||||||
// console.log('Archivos:', files);
|
|
||||||
// console.log('Otros campos del formulario:', body);
|
|
||||||
const result = await this.picturesService.saveImages(files);
|
|
||||||
|
|
||||||
return { data: result };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PicturesController } from './pictures.controller';
|
|
||||||
import { PicturesService } from './pictures.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [PicturesController],
|
|
||||||
providers: [PicturesService],
|
|
||||||
})
|
|
||||||
export class PicturesModule {}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { writeFile } from 'fs/promises';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PicturesService {
|
|
||||||
/**
|
|
||||||
* Guarda una imagen en el directorio de imágenes.
|
|
||||||
* @param file - El archivo de imagen a guardar.
|
|
||||||
* @returns La ruta de la imagen guardada.
|
|
||||||
*/
|
|
||||||
async saveImages(file: Express.Multer.File[]): Promise<string[]> {
|
|
||||||
// Construye la ruta al directorio de imágenes.
|
|
||||||
|
|
||||||
const picturesPath = join(__dirname, '..', '..', '..', '..', 'uploads','pict');
|
|
||||||
|
|
||||||
console.log(picturesPath);
|
|
||||||
|
|
||||||
let images : string[] = [];
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
file.forEach(async (file) => {
|
|
||||||
count++
|
|
||||||
// Crea un nombre de archivo único para la imagen.
|
|
||||||
const fileName = `${Date.now()}-${count}-${file.originalname}`;
|
|
||||||
images.push(fileName);
|
|
||||||
// console.log(fileName);
|
|
||||||
|
|
||||||
// Construye la ruta completa al archivo de imagen.
|
|
||||||
const filePath = join(picturesPath, fileName);
|
|
||||||
|
|
||||||
// Escribe el archivo de imagen en el disco.
|
|
||||||
await writeFile(filePath, file.buffer);
|
|
||||||
});
|
|
||||||
// Devuelve la ruta de la imagen guardada.
|
|
||||||
// return [file[0].originalname]
|
|
||||||
return images;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
280
apps/api/src/features/training/dto/create-training.dto.ts
Normal file
280
apps/api/src/features/training/dto/create-training.dto.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Transform, Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsDateString,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateTrainingDto {
|
||||||
|
// === 1. DATOS BÁSICOS ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsDateString()
|
||||||
|
visitDate: string; // Llega como string ISO "2024-11-11T10:00"
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
coorPhone?: string;
|
||||||
|
|
||||||
|
// === 2. DATOS OSP ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospType: string; // 'UPF', etc.
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
productiveActivity: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
currentStatus: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number) // Convierte "2017" -> 2017
|
||||||
|
companyConstitutionYear: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
ospAddress: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
ospGoogleMapsLink?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
infrastructureMt2?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
structureType?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value === 'true' || value === true) // Convierte "false" -> false
|
||||||
|
hasTransport?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value === 'true' || value === true)
|
||||||
|
isOpenSpace?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
paralysisReason?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
generalObservations?: string;
|
||||||
|
|
||||||
|
// === 3. SECTORES ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ecoSector: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
productiveSector: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
centralProductiveActivity: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
mainProductiveActivity: string;
|
||||||
|
|
||||||
|
// === 4. DATOS RESPONSABLE ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleFullname: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleCedula: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsiblePhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleEmail: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
civilState: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number) // Convierte "3" -> 3
|
||||||
|
familyBurden: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
numberOfChildren: number;
|
||||||
|
|
||||||
|
// === 5. COMUNA Y CONSEJO COMUNAL ===
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
siturCodeCommune: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeSpokespersonName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeSpokespersonCedula: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeSpokespersonRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeSpokespersonPhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communeEmail: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncil: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
siturCodeCommunalCouncil: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilSpokespersonName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilSpokespersonCedula: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilSpokespersonRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilSpokespersonPhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncilEmail: string;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// === 6. LISTAS (Arrays JSON) ===
|
||||||
|
// Reciben un string JSON '[{...}]' y lo convierten a Objeto JS real
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
equipmentList?: any[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
productionList?: any[];
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
productList?: any[];
|
||||||
|
|
||||||
|
|
||||||
|
//ubicacion
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
state: string;
|
||||||
|
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
municipality: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
parish: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber, IsDateString } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class TrainingStatisticsFilterDto {
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
startDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
endDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
stateId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
municipalityId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
parishId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
ospType?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateTrainingDto } from './create-training.dto';
|
||||||
|
|
||||||
|
export class UpdateTrainingDto extends PartialType(CreateTrainingDto) { }
|
||||||
BIN
apps/api/src/features/training/export_template/excel.osp.xlsx
Normal file
BIN
apps/api/src/features/training/export_template/excel.osp.xlsx
Normal file
Binary file not shown.
144
apps/api/src/features/training/training.controller.ts
Normal file
144
apps/api/src/features/training/training.controller.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
Res,
|
||||||
|
UploadedFiles,
|
||||||
|
UseInterceptors,
|
||||||
|
StreamableFile,
|
||||||
|
Header
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||||
|
import {
|
||||||
|
ApiConsumes,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiTags,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
|
import { ImageProcessingPipe } from '../../common/pipes/image-processing.pipe';
|
||||||
|
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||||
|
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||||
|
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||||
|
import { TrainingService } from './training.service';
|
||||||
|
import { Public } from '@/common/decorators';
|
||||||
|
|
||||||
|
@ApiTags('training')
|
||||||
|
@Controller('training')
|
||||||
|
export class TrainingController {
|
||||||
|
constructor(private readonly trainingService: TrainingService) { }
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Return paginated training records.',
|
||||||
|
})
|
||||||
|
async findAll(@Query() paginationDto: PaginationDto) {
|
||||||
|
const result = await this.trainingService.findAll(paginationDto);
|
||||||
|
return {
|
||||||
|
message: 'Training records fetched successfully',
|
||||||
|
data: result.data,
|
||||||
|
meta: result.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// get training statistics
|
||||||
|
@Get('statistics')
|
||||||
|
@ApiOperation({ summary: 'Get training statistics' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Return training statistics.' })
|
||||||
|
async getStatistics(@Query() filterDto: TrainingStatisticsFilterDto) {
|
||||||
|
const data = await this.trainingService.getStatistics(filterDto);
|
||||||
|
return { message: 'Training statistics fetched successfully', data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// get training record by id
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get a training record by ID' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Return the training record.' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||||
|
async findOne(@Param('id') id: string) {
|
||||||
|
const data = await this.trainingService.findOne(+id);
|
||||||
|
return { message: 'Training record fetched successfully', data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// create training record
|
||||||
|
@Post()
|
||||||
|
@UseInterceptors(FilesInterceptor('files', 3))
|
||||||
|
@ApiConsumes('multipart/form-data')
|
||||||
|
@ApiOperation({ summary: 'Create a new training record' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Training record created successfully.',
|
||||||
|
})
|
||||||
|
async create(
|
||||||
|
@Body() createTrainingDto: CreateTrainingDto,
|
||||||
|
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const data = await this.trainingService.create(createTrainingDto, files);
|
||||||
|
return { message: 'Training record created successfully', data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// update training record
|
||||||
|
@Patch(':id')
|
||||||
|
@UseInterceptors(FilesInterceptor('files', 3))
|
||||||
|
@ApiConsumes('multipart/form-data')
|
||||||
|
@ApiOperation({ summary: 'Update a training record' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Training record updated successfully.',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||||
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() updateTrainingDto: UpdateTrainingDto,
|
||||||
|
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const data = await this.trainingService.update(
|
||||||
|
+id,
|
||||||
|
updateTrainingDto,
|
||||||
|
files,
|
||||||
|
);
|
||||||
|
return { message: 'Training record updated successfully', data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete training record
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Delete a training record' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Training record deleted successfully.',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||||
|
async remove(@Param('id') id: string) {
|
||||||
|
return await this.trainingService.remove(+id);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/features/training/training.module.ts
Normal file
10
apps/api/src/features/training/training.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TrainingService } from './training.service';
|
||||||
|
import { TrainingController } from './training.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [TrainingController],
|
||||||
|
providers: [TrainingService],
|
||||||
|
exports: [TrainingService],
|
||||||
|
})
|
||||||
|
export class TrainingModule { }
|
||||||
539
apps/api/src/features/training/training.service.ts
Normal file
539
apps/api/src/features/training/training.service.ts
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
import { HttpException, HttpStatus, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { and, eq, getTableColumns, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm';
|
||||||
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||||
|
import * as schema from 'src/database/index';
|
||||||
|
import { municipalities, parishes, states, trainingSurveys } from 'src/database/index';
|
||||||
|
// import XlsxPopulate from 'xlsx-populate';
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
|
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||||
|
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||||
|
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TrainingService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async findAll(paginationDto?: PaginationDto) {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
search = '',
|
||||||
|
sortBy = 'id',
|
||||||
|
sortOrder = 'asc',
|
||||||
|
} = paginationDto || {};
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
let searchCondition: SQL<unknown> | undefined;
|
||||||
|
if (search) {
|
||||||
|
searchCondition = or(ilike(trainingSurveys.ospName, `%${search}%`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderBy =
|
||||||
|
sortOrder === 'asc'
|
||||||
|
? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc`
|
||||||
|
: sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`;
|
||||||
|
|
||||||
|
const totalCountResult = await this.drizzle
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(searchCondition);
|
||||||
|
|
||||||
|
const totalCount = Number(totalCountResult[0].count);
|
||||||
|
const totalPages = Math.ceil(totalCount / limit);
|
||||||
|
|
||||||
|
const data = await this.drizzle
|
||||||
|
.select()
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(searchCondition)
|
||||||
|
.orderBy(orderBy)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset);
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalCount,
|
||||||
|
totalPages,
|
||||||
|
hasNextPage: page < totalPages,
|
||||||
|
hasPreviousPage: page > 1,
|
||||||
|
nextPage: page < totalPages ? page + 1 : null,
|
||||||
|
previousPage: page > 1 ? page - 1 : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { data, meta };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
|
||||||
|
const { startDate, endDate, stateId, municipalityId, parishId, ospType } =
|
||||||
|
filterDto;
|
||||||
|
|
||||||
|
const filters: SQL[] = [];
|
||||||
|
|
||||||
|
if (startDate)
|
||||||
|
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||||
|
if (endDate)
|
||||||
|
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||||
|
if (stateId) filters.push(eq(trainingSurveys.state, stateId));
|
||||||
|
if (municipalityId)
|
||||||
|
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||||
|
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
|
||||||
|
if (ospType) filters.push(eq(trainingSurveys.ospType, ospType));
|
||||||
|
|
||||||
|
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
||||||
|
|
||||||
|
// Ejecutamos todas las consultas en paralelo con Promise.all para mayor velocidad
|
||||||
|
const [
|
||||||
|
totalOspsResult,
|
||||||
|
totalProducersResult,
|
||||||
|
totalProductsResult, // Nuevo: Calculado desde el JSON
|
||||||
|
statusDistribution,
|
||||||
|
activityDistribution,
|
||||||
|
typeDistribution,
|
||||||
|
stateDistribution,
|
||||||
|
yearDistribution,
|
||||||
|
] = await Promise.all([
|
||||||
|
// 1. Total OSPs
|
||||||
|
this.drizzle
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
|
// 2. Total Productores (Columna plana que mantuviste)
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
sum: sql<number>`
|
||||||
|
SUM(
|
||||||
|
(
|
||||||
|
SELECT SUM(
|
||||||
|
COALESCE((item->>'menCount')::int, 0) +
|
||||||
|
COALESCE((item->>'womenCount')::int, 0)
|
||||||
|
)
|
||||||
|
FROM jsonb_array_elements(${trainingSurveys.productList}) as item
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
|
// 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
sum: sql<number>`sum(jsonb_array_length(${trainingSurveys.productList}))`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
|
// 4. Distribución por Estatus
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.currentStatus,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.currentStatus),
|
||||||
|
|
||||||
|
// 5. Distribución por Actividad
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.productiveActivity,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.productiveActivity),
|
||||||
|
|
||||||
|
// 6. Distribución por Tipo
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.ospType,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.ospType),
|
||||||
|
|
||||||
|
// 7. Distribución por Estado (CORREGIDO con COALESCE)
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
// Si states.name es NULL, devuelve 'Sin Asignar'
|
||||||
|
name: sql<string>`COALESCE(${states.name}, 'Sin Asignar')`,
|
||||||
|
value: sql<number>`count(${trainingSurveys.id})`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
||||||
|
.where(whereCondition)
|
||||||
|
// Importante: Agrupar también por el resultado del COALESCE o por states.name
|
||||||
|
.groupBy(states.name),
|
||||||
|
|
||||||
|
// 8. Distribución por Año
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.companyConstitutionYear)
|
||||||
|
.orderBy(trainingSurveys.companyConstitutionYear),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalOsps: Number(totalOspsResult[0]?.count || 0),
|
||||||
|
totalProducers: Number(totalProducersResult[0]?.sum || 0),
|
||||||
|
totalProducts: Number(totalProductsResult[0]?.sum || 0), // Dato extraído del JSON
|
||||||
|
|
||||||
|
statusDistribution: statusDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
activityDistribution: activityDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
typeDistribution: typeDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
stateDistribution: stateDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
yearDistribution: yearDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: number) {
|
||||||
|
const find = await this.drizzle
|
||||||
|
.select({
|
||||||
|
...getTableColumns(trainingSurveys),
|
||||||
|
stateName: states.name,
|
||||||
|
municipalityName: municipalities.name,
|
||||||
|
parishName: parishes.name,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
||||||
|
.leftJoin(municipalities, eq(trainingSurveys.municipality, municipalities.id))
|
||||||
|
.leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
|
||||||
|
if (find.length === 0) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Training record not found',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return find[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveFiles(files: Express.Multer.File[]): Promise<string[]> {
|
||||||
|
if (!files || files.length === 0) return [];
|
||||||
|
|
||||||
|
const uploadDir = './uploads/training';
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedPaths: string[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const fileName = `${Date.now()}-${Math.round(Math.random() * 1e9)}.png`;
|
||||||
|
const filePath = path.join(uploadDir, fileName);
|
||||||
|
|
||||||
|
// Convertir a PNG usando sharp antes de guardar
|
||||||
|
await sharp(file.buffer)
|
||||||
|
.png()
|
||||||
|
.toFile(filePath);
|
||||||
|
|
||||||
|
savedPaths.push(`/assets/training/${fileName}`);
|
||||||
|
}
|
||||||
|
return savedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteFile(assetPath: string) {
|
||||||
|
if (!assetPath) return;
|
||||||
|
// Map /assets/training/filename.webp back to ./uploads/training/filename.webp
|
||||||
|
const relativePath = assetPath.replace('/assets/training/', '');
|
||||||
|
const fullPath = path.join('./uploads/training', relativePath);
|
||||||
|
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(fullPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error deleting file ${fullPath}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
createTrainingDto: CreateTrainingDto,
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
// 1. Guardar fotos
|
||||||
|
const photoPaths = await this.saveFiles(files);
|
||||||
|
|
||||||
|
// 2. Extraer solo visitDate para formatearlo.
|
||||||
|
const { visitDate, state, municipality, parish, ...rest } = createTrainingDto;
|
||||||
|
|
||||||
|
const [newRecord] = await this.drizzle
|
||||||
|
.insert(trainingSurveys)
|
||||||
|
.values({
|
||||||
|
// Insertamos el resto de datos planos y las listas (arrays)
|
||||||
|
...rest,
|
||||||
|
|
||||||
|
// Conversión de fecha
|
||||||
|
visitDate: new Date(visitDate),
|
||||||
|
|
||||||
|
// 3. Asignar fotos de forma segura
|
||||||
|
photo1: photoPaths[0] ?? null,
|
||||||
|
photo2: photoPaths[1] ?? null,
|
||||||
|
photo3: photoPaths[2] ?? null,
|
||||||
|
state: Number(state) ?? null,
|
||||||
|
municipality: Number(municipality) ?? null,
|
||||||
|
parish: Number(parish) ?? null,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: number,
|
||||||
|
updateTrainingDto: UpdateTrainingDto,
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const currentRecord = await this.findOne(id);
|
||||||
|
|
||||||
|
const photoPaths = await this.saveFiles(files);
|
||||||
|
|
||||||
|
const updateData: any = { ...updateTrainingDto };
|
||||||
|
|
||||||
|
// Handle photo updates/removals
|
||||||
|
const photoFields = ['photo1', 'photo2', 'photo3'] as const;
|
||||||
|
|
||||||
|
// 1. If we have NEW files, they replace any old files or occupy empty slots
|
||||||
|
if (photoPaths.length > 0) {
|
||||||
|
photoPaths.forEach((newPath, idx) => {
|
||||||
|
const fieldName = photoFields[idx];
|
||||||
|
const oldPath = currentRecord[fieldName];
|
||||||
|
if (oldPath && oldPath !== newPath) {
|
||||||
|
this.deleteFile(oldPath);
|
||||||
|
}
|
||||||
|
updateData[fieldName] = newPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If the user explicitly cleared a photo field (updateData.photoX === '')
|
||||||
|
photoFields.forEach((field) => {
|
||||||
|
if (updateData[field] === '') {
|
||||||
|
const oldPath = currentRecord[field];
|
||||||
|
if (oldPath) this.deleteFile(oldPath);
|
||||||
|
updateData[field] = null; // Set to null in DB
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updateTrainingDto.visitDate) {
|
||||||
|
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedRecord] = await this.drizzle
|
||||||
|
.update(trainingSurveys)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return updatedRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number) {
|
||||||
|
const record = await this.findOne(id);
|
||||||
|
|
||||||
|
// Delete associated files
|
||||||
|
if (record.photo1) this.deleteFile(record.photo1);
|
||||||
|
if (record.photo2) this.deleteFile(record.photo2);
|
||||||
|
if (record.photo3) this.deleteFile(record.photo3);
|
||||||
|
|
||||||
|
const [deletedRecord] = await this.drizzle
|
||||||
|
.delete(trainingSurveys)
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Training record deleted successfully',
|
||||||
|
data: deletedRecord,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportTemplate(id: number) {
|
||||||
|
// Validar que el registro exista
|
||||||
|
const record = await this.findOne(id);
|
||||||
|
if (!record) throw new NotFoundException(`No se encontró el registro`);
|
||||||
|
|
||||||
|
// Formatear fecha y hora
|
||||||
|
const dateObj = new Date(record.visitDate);
|
||||||
|
const fechaFormateada = dateObj.toLocaleDateString('es-ES');
|
||||||
|
const horaFormateada = dateObj.toLocaleTimeString('es-ES', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ruta de la plantilla
|
||||||
|
const templatePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'export_template',
|
||||||
|
'excel.osp.xlsx',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cargar la plantilla con ExcelJS
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
await workbook.xlsx.readFile(templatePath);
|
||||||
|
const worksheet = workbook.getWorksheet(1); // Usar la primera hoja
|
||||||
|
|
||||||
|
if (!worksheet) {
|
||||||
|
throw new Error('No se pudo encontrar la hoja de trabajo en la plantilla');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llenar los datos principales
|
||||||
|
worksheet.getCell('A6').value = record.productiveSector;
|
||||||
|
worksheet.getCell('B8').value = record.stateName;
|
||||||
|
worksheet.getCell('E8').value = record.municipalityName;
|
||||||
|
worksheet.getCell('B9').value = record.parishName;
|
||||||
|
worksheet.getCell('D6').value = record.ospName;
|
||||||
|
worksheet.getCell('L5').value = fechaFormateada;
|
||||||
|
worksheet.getCell('L6').value = horaFormateada;
|
||||||
|
worksheet.getCell('B10').value = record.ospAddress;
|
||||||
|
worksheet.getCell('C11').value = record.communeEmail;
|
||||||
|
worksheet.getCell('C12').value = record.communeSpokespersonName;
|
||||||
|
worksheet.getCell('G11').value = record.communeRif;
|
||||||
|
worksheet.getCell('G12').value = record.communeSpokespersonPhone;
|
||||||
|
worksheet.getCell('C13').value = record.siturCodeCommune;
|
||||||
|
worksheet.getCell('G13').value = record.siturCodeCommunalCouncil;
|
||||||
|
worksheet.getCell('G14').value = record.communalCouncilRif;
|
||||||
|
worksheet.getCell('C15').value = record.communalCouncilSpokespersonName;
|
||||||
|
worksheet.getCell('G15').value = record.communalCouncilSpokespersonPhone;
|
||||||
|
worksheet.getCell('C16').value = record.ospType;
|
||||||
|
worksheet.getCell('C17').value = record.ospName;
|
||||||
|
worksheet.getCell('C18').value = record.productiveActivity;
|
||||||
|
worksheet.getCell('C19').value = 'Proveedores';
|
||||||
|
worksheet.getCell('C20').value = record.companyConstitutionYear;
|
||||||
|
worksheet.getCell('C21').value = record.infrastructureMt2;
|
||||||
|
worksheet.getCell('G17').value = record.ospRif;
|
||||||
|
|
||||||
|
worksheet.getCell(record.hasTransport === true ? 'J19' : 'L19').value = 'X';
|
||||||
|
worksheet.getCell(record.structureType === 'CASA' ? 'J20' : 'L20').value =
|
||||||
|
'X';
|
||||||
|
worksheet.getCell(record.isOpenSpace === true ? 'J21' : 'L21').value = 'X';
|
||||||
|
|
||||||
|
worksheet.getCell('A24').value = record.ospResponsibleFullname;
|
||||||
|
worksheet.getCell('C24').value = record.ospResponsibleCedula;
|
||||||
|
worksheet.getCell('E24').value = record.ospResponsiblePhone;
|
||||||
|
|
||||||
|
worksheet.getCell('J24').value = 'N Femenino'; // Placeholder si no hay dato
|
||||||
|
worksheet.getCell('L24').value = 'N Masculino'; // Placeholder si no hay dato
|
||||||
|
|
||||||
|
// const photo1 = record.photo1;
|
||||||
|
// const photo2 = record.photo2;
|
||||||
|
// const photo3 = record.photo3;
|
||||||
|
|
||||||
|
if (record.photo1) {
|
||||||
|
const image = record.photo1.slice(17);
|
||||||
|
const extension = image.split('.')[1];
|
||||||
|
|
||||||
|
// Validar que sea una imagen png, gif o jpeg
|
||||||
|
if (extension === 'png' || extension === 'gif' || extension === 'jpeg') {
|
||||||
|
// Ruta de la imagen
|
||||||
|
const imagePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../../',
|
||||||
|
`uploads/training/${image}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add an image to the workbook from a file buffer
|
||||||
|
const logoId = workbook.addImage({
|
||||||
|
filename: imagePath,
|
||||||
|
extension: extension,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Anchor the image to a specific cell (e.g., A1)
|
||||||
|
worksheet.addImage(logoId, `I7:L17`); // Spans from A1 to C3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let i = 1;
|
||||||
|
// while (i <= 3) {
|
||||||
|
// const element = record[`photo${i}`];
|
||||||
|
// if (element) {
|
||||||
|
// const image = element.slice(17);
|
||||||
|
// const extension: extensionType = image.split('.')[1];
|
||||||
|
|
||||||
|
// // Validar que sea una imagen png, gif o jpeg
|
||||||
|
// if (extension === 'png' || extension === 'gif' || extension === 'jpeg') {
|
||||||
|
// // Ruta de la imagen
|
||||||
|
// const imagePath = path.join(
|
||||||
|
// __dirname,
|
||||||
|
// '../../../',
|
||||||
|
// `uploads/training/${image}`,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Add an image to the workbook from a file buffer
|
||||||
|
// const logoId = workbook.addImage({
|
||||||
|
// filename: imagePath,
|
||||||
|
// extension: extension,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Anchor the image to a specific cell (e.g., A1)
|
||||||
|
// worksheet.addImage(logoId, `I7:L17`); // Spans from A1 to C3
|
||||||
|
// i = 4;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// i++;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Listas (Equipos, Materia Prima, Productos)
|
||||||
|
const equipmentList = Array.isArray(record.equipmentList)
|
||||||
|
? record.equipmentList
|
||||||
|
: [];
|
||||||
|
const productionList = Array.isArray(record.productionList)
|
||||||
|
? record.productionList
|
||||||
|
: [];
|
||||||
|
const productList = Array.isArray(record.productList)
|
||||||
|
? record.productList
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Colocar listas empezando en la fila 28
|
||||||
|
equipmentList.forEach((item: any, i: number) => {
|
||||||
|
const row = 28 + i;
|
||||||
|
worksheet.getCell(`A${row}`).value = item.machine;
|
||||||
|
worksheet.getCell(`C${row}`).value = item.quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
productionList.forEach((item: any, i: number) => {
|
||||||
|
const row = 28 + i;
|
||||||
|
worksheet.getCell(`E${row}`).value = item.rawMaterial;
|
||||||
|
worksheet.getCell(`G${row}`).value = item.quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
productList.forEach((item: any, i: number) => {
|
||||||
|
const row = 28 + i;
|
||||||
|
worksheet.getCell(`I${row}`).value = item.productName;
|
||||||
|
worksheet.getCell(`J${row}`).value = item.dailyCount;
|
||||||
|
worksheet.getCell(`K${row}`).value = item.weeklyCount;
|
||||||
|
worksheet.getCell(`L${row}`).value = item.monthlyCount;
|
||||||
|
});
|
||||||
|
|
||||||
|
return await workbook.xlsx.writeBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
AUTH_URL = http://localhost:3000
|
AUTH_URL = http://localhost:3000
|
||||||
AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE=
|
AUTH_SECRET=wWgIwkHIGr28ydIUPsgVGNUNxXQ+brg1XXtA8PfjJjAEPJLDP2UTghWL8aE=
|
||||||
API_URL=http://localhost:8000
|
API_URL=http://localhost:8000
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import PageContainer from '@/components/layout/page-container';
|
|
||||||
|
|
||||||
const Page = () => {
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
|
||||||
En mantenimiento
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</PageContainer>
|
|
||||||
// <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
|
||||||
// <div className="flex w-full max-w-sm flex-col gap-6">
|
|
||||||
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
19
apps/web/app/dashboard/estadisticas/socioproductiva/page.tsx
Normal file
19
apps/web/app/dashboard/estadisticas/socioproductiva/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { TrainingStatistics } from '@/feactures/training/components/training-statistics';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Estadísticas Socioproductivas - Fondemi',
|
||||||
|
description: 'Análisis y estadísticas de las Organizaciones Socioproductivas',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SocioproductivaStatisticsPage() {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<div className="w-full">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Estadísticas Socioproductivas</h1>
|
||||||
|
<TrainingStatistics />
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
apps/web/app/dashboard/formulario/editar/[id]/page.tsx
Normal file
34
apps/web/app/dashboard/formulario/editar/[id]/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { CreateTrainingForm } from '@/feactures/training/components/form';
|
||||||
|
import { useTrainingByIdQuery } from '@/feactures/training/hooks/use-training';
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function EditTrainingPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const id = Number(params.id);
|
||||||
|
|
||||||
|
const { data: training, isLoading } = useTrainingByIdQuery(id);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<div>Cargando...</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <PageContainer scrollable>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<CreateTrainingForm
|
||||||
|
defaultValues={training}
|
||||||
|
onSuccess={() => router.push('/dashboard/formulario')}
|
||||||
|
onCancel={() => router.back()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
// </PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
apps/web/app/dashboard/formulario/nuevo/page.tsx
Normal file
18
apps/web/app/dashboard/formulario/nuevo/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
'use client';
|
||||||
|
import { CreateTrainingForm } from '@/feactures/training/components/form';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function NewTrainingPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <PageContainer scrollable>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<CreateTrainingForm
|
||||||
|
onSuccess={() => router.push('/dashboard/formulario')}
|
||||||
|
onCancel={() => router.back()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
// </PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
apps/web/app/dashboard/formulario/page.tsx
Normal file
40
apps/web/app/dashboard/formulario/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { TrainingHeader } from '@/feactures/training/components/training-header';
|
||||||
|
import TrainingList from '@/feactures/training/components/training-list';
|
||||||
|
import TrainingTableAction from '@/feactures/training/components/training-tables/training-table-action';
|
||||||
|
import { searchParamsCache } from '@repo/shadcn/lib/searchparams';
|
||||||
|
import { SearchParams } from 'nuqs';
|
||||||
|
|
||||||
|
import { env } from '@/lib/env';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Registro de OSP',
|
||||||
|
};
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
searchParams: Promise<SearchParams>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ searchParams }: PageProps) {
|
||||||
|
const {
|
||||||
|
page,
|
||||||
|
q: searchQuery,
|
||||||
|
limit,
|
||||||
|
} = searchParamsCache.parse(await searchParams);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <PageContainer>
|
||||||
|
// <div className="flex flex-1 flex-col space-y-6">
|
||||||
|
< div className="p-6 space-y-6" >
|
||||||
|
<TrainingHeader />
|
||||||
|
<TrainingTableAction />
|
||||||
|
<TrainingList
|
||||||
|
initialPage={page}
|
||||||
|
initialSearch={searchQuery}
|
||||||
|
initialLimit={limit || 10}
|
||||||
|
apiUrl={env.API_URL}
|
||||||
|
/>
|
||||||
|
</div >
|
||||||
|
// </PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { auth } from '@/lib/auth';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export default async function Dashboard() {
|
export default async function Dashboard() {
|
||||||
|
console.log('La sesion es llamada');
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const metadata = {
|
|||||||
openGraph: {
|
openGraph: {
|
||||||
type: 'website',
|
type: 'website',
|
||||||
title: 'fondemi',
|
title: 'fondemi',
|
||||||
description: 'Sistema integral para cajas de ahorro',
|
description: 'Sistema integral para fondemi',
|
||||||
url: 'https://turbo-npn.onrender.com',
|
url: 'https://turbo-npn.onrender.com',
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
@@ -44,6 +44,7 @@ const RootLayout = async ({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}>) => {
|
}>) => {
|
||||||
|
console.log('La sesion es llamada');
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { NavMain as GeneralMain, NavMain as AdministrationMain, NavMain as StatisticsMain, } from '@/components/nav-main';
|
import { NavMain as GeneralMain, NavMain as AdministrationMain, NavMain as StatisticsMain, } from '@/components/nav-main';
|
||||||
import { GeneralItems, AdministrationItems, StatisticsItems } from '@/constants/data';
|
import { GeneralItems, AdministrationItems, StatisticsItems } from '@/constants/routes';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -15,7 +15,7 @@ import { useSession } from 'next-auth/react';
|
|||||||
|
|
||||||
|
|
||||||
export const company = {
|
export const company = {
|
||||||
name: 'Sistema para Productores',
|
name: 'Sistema de Productores',
|
||||||
logo: GalleryVerticalEnd,
|
logo: GalleryVerticalEnd,
|
||||||
plan: 'FONDEMI',
|
plan: 'FONDEMI',
|
||||||
};
|
};
|
||||||
@@ -24,7 +24,7 @@ export const company = {
|
|||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol :'';
|
const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol : '';
|
||||||
// console.log(AdministrationItems[0]?.role);
|
// console.log(AdministrationItems[0]?.role);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,15 +42,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<GeneralMain titleGroup={'General'} items={GeneralItems} role={userRole}/>
|
{AdministrationItems[0]?.role?.includes(userRole) &&
|
||||||
|
<AdministrationMain titleGroup={'Administracion'} items={AdministrationItems} role={userRole} />
|
||||||
|
}
|
||||||
{StatisticsItems[0]?.role?.includes(userRole) &&
|
{StatisticsItems[0]?.role?.includes(userRole) &&
|
||||||
<StatisticsMain titleGroup={'Estadisticas'} items={StatisticsItems} role={userRole}/>
|
<StatisticsMain titleGroup={'Estadisticas'} items={StatisticsItems} role={userRole} />
|
||||||
}
|
}
|
||||||
|
|
||||||
{AdministrationItems[0]?.role?.includes(userRole) &&
|
<GeneralMain titleGroup={'General'} items={GeneralItems} role={userRole} />
|
||||||
<AdministrationMain titleGroup={'Administracion'} items={AdministrationItems} role={userRole}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* <NavProjects projects={data.projects} /> */}
|
{/* <NavProjects projects={data.projects} /> */}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
|
|||||||
196
apps/web/constants/countries.ts
Normal file
196
apps/web/constants/countries.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
export const COUNTRY_OPTIONS = [
|
||||||
|
'Afganistán',
|
||||||
|
'Albania',
|
||||||
|
'Alemania',
|
||||||
|
'Andorra',
|
||||||
|
'Angola',
|
||||||
|
'Antigua y Barbuda',
|
||||||
|
'Arabia Saudita',
|
||||||
|
'Argelia',
|
||||||
|
'Argentina',
|
||||||
|
'Armenia',
|
||||||
|
'Australia',
|
||||||
|
'Austria',
|
||||||
|
'Azerbaiyán',
|
||||||
|
'Bahamas',
|
||||||
|
'Bangladés',
|
||||||
|
'Barbados',
|
||||||
|
'Baréin',
|
||||||
|
'Bélgica',
|
||||||
|
'Belice',
|
||||||
|
'Benín',
|
||||||
|
'Bielorrusia',
|
||||||
|
'Birmania',
|
||||||
|
'Bolivia',
|
||||||
|
'Bosnia y Herzegovina',
|
||||||
|
'Botsuana',
|
||||||
|
'Brasil',
|
||||||
|
'Brunéi',
|
||||||
|
'Bulgaria',
|
||||||
|
'Burkina Faso',
|
||||||
|
'Burundi',
|
||||||
|
'Bután',
|
||||||
|
'Cabo Verde',
|
||||||
|
'Camboya',
|
||||||
|
'Camerún',
|
||||||
|
'Canadá',
|
||||||
|
'Catar',
|
||||||
|
'Chad',
|
||||||
|
'Chile',
|
||||||
|
'China',
|
||||||
|
'Chipre',
|
||||||
|
'Ciudad del Vaticano',
|
||||||
|
'Colombia',
|
||||||
|
'Comoras',
|
||||||
|
'Corea del Norte',
|
||||||
|
'Corea del Sur',
|
||||||
|
'Costa de Marfil',
|
||||||
|
'Costa Rica',
|
||||||
|
'Croacia',
|
||||||
|
'Cuba',
|
||||||
|
'Dinamarca',
|
||||||
|
'Dominica',
|
||||||
|
'Ecuador',
|
||||||
|
'Egipto',
|
||||||
|
'El Salvador',
|
||||||
|
'Emiratos Árabes Unidos',
|
||||||
|
'Eritrea',
|
||||||
|
'Eslovaquia',
|
||||||
|
'Eslovenia',
|
||||||
|
'España',
|
||||||
|
'Estados Unidos',
|
||||||
|
'Estonia',
|
||||||
|
'Etiopía',
|
||||||
|
'Filipinas',
|
||||||
|
'Finlandia',
|
||||||
|
'Fiyi',
|
||||||
|
'Francia',
|
||||||
|
'Gabón',
|
||||||
|
'Gambia',
|
||||||
|
'Georgia',
|
||||||
|
'Ghana',
|
||||||
|
'Granada',
|
||||||
|
'Grecia',
|
||||||
|
'Guatemala',
|
||||||
|
'Guyana',
|
||||||
|
'Guinea',
|
||||||
|
'Guinea Ecuatorial',
|
||||||
|
'Guinea-Bisáu',
|
||||||
|
'Haití',
|
||||||
|
'Honduras',
|
||||||
|
'Hungría',
|
||||||
|
'India',
|
||||||
|
'Indonesia',
|
||||||
|
'Irak',
|
||||||
|
'Irán',
|
||||||
|
'Irlanda',
|
||||||
|
'Islandia',
|
||||||
|
'Islas Marshall',
|
||||||
|
'Islas Salomón',
|
||||||
|
'Israel',
|
||||||
|
'Italia',
|
||||||
|
'Jamaica',
|
||||||
|
'Japón',
|
||||||
|
'Jordania',
|
||||||
|
'Kazajistán',
|
||||||
|
'Kenia',
|
||||||
|
'Kirguistán',
|
||||||
|
'Kiribati',
|
||||||
|
'Kuwait',
|
||||||
|
'Laos',
|
||||||
|
'Lesoto',
|
||||||
|
'Letonia',
|
||||||
|
'Líbano',
|
||||||
|
'Liberia',
|
||||||
|
'Libia',
|
||||||
|
'Liechtenstein',
|
||||||
|
'Lituania',
|
||||||
|
'Luxemburgo',
|
||||||
|
'Madagascar',
|
||||||
|
'Malasia',
|
||||||
|
'Malaui',
|
||||||
|
'Maldivas',
|
||||||
|
'Malí',
|
||||||
|
'Malta',
|
||||||
|
'Marruecos',
|
||||||
|
'Mauricio',
|
||||||
|
'Mauritania',
|
||||||
|
'México',
|
||||||
|
'Micronesia',
|
||||||
|
'Moldavia',
|
||||||
|
'Mónaco',
|
||||||
|
'Mongolia',
|
||||||
|
'Montenegro',
|
||||||
|
'Mozambique',
|
||||||
|
'Namibia',
|
||||||
|
'Nauru',
|
||||||
|
'Nepal',
|
||||||
|
'Nicaragua',
|
||||||
|
'Níger',
|
||||||
|
'Nigeria',
|
||||||
|
'Noruega',
|
||||||
|
'Nueva Zelanda',
|
||||||
|
'Omán',
|
||||||
|
'Países Bajos',
|
||||||
|
'Pakistán',
|
||||||
|
'Palaos',
|
||||||
|
'Panamá',
|
||||||
|
'Papúa Nueva Guinea',
|
||||||
|
'Paraguay',
|
||||||
|
'Perú',
|
||||||
|
'Polonia',
|
||||||
|
'Portugal',
|
||||||
|
'Reino Unido',
|
||||||
|
'República Centroafricana',
|
||||||
|
'República Checa',
|
||||||
|
'República de Macedonia',
|
||||||
|
'República del Congo',
|
||||||
|
'República Democrática del Congo',
|
||||||
|
'República Dominicana',
|
||||||
|
'República Sudafricana',
|
||||||
|
'Ruanda',
|
||||||
|
'Rumanía',
|
||||||
|
'Rusia',
|
||||||
|
'Samoa',
|
||||||
|
'San Cristóbal y Nieves',
|
||||||
|
'San Marino',
|
||||||
|
'San Vicente y las Granadinas',
|
||||||
|
'Santa Lucía',
|
||||||
|
'Santo Tomé y Príncipe',
|
||||||
|
'Senegal',
|
||||||
|
'Serbia',
|
||||||
|
'Seychelles',
|
||||||
|
'Sierra Leona',
|
||||||
|
'Singapur',
|
||||||
|
'Siria',
|
||||||
|
'Somalia',
|
||||||
|
'Sri Lanka',
|
||||||
|
'Suazilandia',
|
||||||
|
'Sudán',
|
||||||
|
'Sudán del Sur',
|
||||||
|
'Suecia',
|
||||||
|
'Suiza',
|
||||||
|
'Surinam',
|
||||||
|
'Tailandia',
|
||||||
|
'Tanzania',
|
||||||
|
'Tayikistán',
|
||||||
|
'Timor Oriental',
|
||||||
|
'Togo',
|
||||||
|
'Tonga',
|
||||||
|
'Trinidad y Tobago',
|
||||||
|
'Túnez',
|
||||||
|
'Turkmenistán',
|
||||||
|
'Turquía',
|
||||||
|
'Tuvalu',
|
||||||
|
'Ucrania',
|
||||||
|
'Uganda',
|
||||||
|
'Uruguay',
|
||||||
|
'Uzbekistán',
|
||||||
|
'Vanuatu',
|
||||||
|
'Venezuela',
|
||||||
|
'Vietnam',
|
||||||
|
'Yemen',
|
||||||
|
'Yibuti',
|
||||||
|
'Zambia',
|
||||||
|
'Zimbabue'
|
||||||
|
];
|
||||||
@@ -10,24 +10,23 @@ export const GeneralItems: NavItem[] = [
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
items: [], // No child items
|
items: [], // No child items
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'ProduTienda',
|
// title: 'ProduTienda',
|
||||||
url: '/dashboard/productos/',
|
// url: '/dashboard/productos/',
|
||||||
icon: 'blocks',
|
// icon: 'blocks',
|
||||||
shortcut: ['p', 'p'],
|
// shortcut: ['p', 'p'],
|
||||||
isActive: false,
|
// isActive: false,
|
||||||
items: [], // No child items
|
// items: [], // No child items
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export const AdministrationItems: NavItem[] = [
|
export const AdministrationItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Administracion',
|
title: 'Administracion',
|
||||||
url: '#', // Placeholder as there is no direct link for the parent
|
url: '#', // Placeholder as there is no direct link for the parent
|
||||||
icon: 'settings2',
|
icon: 'settings2',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
role:['admin','superadmin','manager','user'], // sumatoria de los roles que si tienen acceso
|
role: ['admin', 'superadmin', 'manager', 'autoridad', 'coordinators'], // sumatoria de los roles que si tienen acceso
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@@ -35,14 +34,21 @@ export const AdministrationItems: NavItem[] = [
|
|||||||
url: '/dashboard/administracion/usuario',
|
url: '/dashboard/administracion/usuario',
|
||||||
icon: 'userPen',
|
icon: 'userPen',
|
||||||
shortcut: ['m', 'm'],
|
shortcut: ['m', 'm'],
|
||||||
role:['admin','superadmin'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Encuestas',
|
title: 'Encuestas',
|
||||||
shortcut: ['l', 'l'],
|
shortcut: ['l', 'l'],
|
||||||
url: '/dashboard/administracion/encuestas',
|
url: '/dashboard/administracion/encuestas',
|
||||||
icon: 'login',
|
icon: 'login',
|
||||||
role:['admin','superadmin','manager','user'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Registro OSP',
|
||||||
|
shortcut: ['p', 'p'],
|
||||||
|
url: '/dashboard/formulario/',
|
||||||
|
icon: 'notepadText',
|
||||||
|
role: ['admin', 'superadmin', 'manager', 'autoridad', 'coordinators'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -54,7 +60,7 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
url: '#', // Placeholder as there is no direct link for the parent
|
url: '#', // Placeholder as there is no direct link for the parent
|
||||||
icon: 'chartColumn',
|
icon: 'chartColumn',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
role:['admin','superadmin','autoridad'],
|
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
// {
|
// {
|
||||||
@@ -69,13 +75,15 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
shortcut: ['l', 'l'],
|
shortcut: ['l', 'l'],
|
||||||
url: '/dashboard/estadisticas/encuestas',
|
url: '/dashboard/estadisticas/encuestas',
|
||||||
icon: 'notepadText',
|
icon: 'notepadText',
|
||||||
role:['admin','superadmin','autoridad'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Datos OSP',
|
||||||
|
shortcut: ['s', 's'],
|
||||||
|
url: '/dashboard/estadisticas/socioproductiva',
|
||||||
|
icon: 'blocks',
|
||||||
|
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import { safeFetchApi } from '@/lib';
|
import { safeFetchApi } from '@/lib/fetch.api';
|
||||||
import { loginResponseSchema, UserFormValue } from '../schemas/login';
|
import { loginResponseSchema, UserFormValue } from '../schemas/login';
|
||||||
|
|
||||||
type LoginActionSuccess = {
|
type LoginActionSuccess = {
|
||||||
@@ -20,9 +20,9 @@ type LoginActionSuccess = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LoginActionError = {
|
type LoginActionError = {
|
||||||
type: 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR'; // **Asegúrate de que el tipo de `type` sea este aquí**
|
type: 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR'; // **Asegúrate de que el tipo de `type` sea este aquí**
|
||||||
message: string;
|
message: string;
|
||||||
details?: any;
|
details?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Si SignInAction también puede devolver null, asegúralo en su tipo de retorno
|
// Si SignInAction también puede devolver null, asegúralo en su tipo de retorno
|
||||||
@@ -37,7 +37,7 @@ export const SignInAction = async (payload: UserFormValue): Promise<LoginActionR
|
|||||||
);
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
return {
|
return {
|
||||||
type: error.type as 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR',
|
type: error.type as 'API_ERROR' | 'VALIDATION_ERROR' | 'UNKNOWN_ERROR',
|
||||||
message: error.message,
|
message: error.message,
|
||||||
details: error.details
|
details: error.details
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// auth/actions/refresh-token-action.ts
|
||||||
'use server';
|
'use server';
|
||||||
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
|
import { refreshApi } from '@/lib/refreshApi'; // Importa la nueva instancia
|
||||||
import {
|
import {
|
||||||
@@ -7,7 +8,7 @@ import {
|
|||||||
|
|
||||||
export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => {
|
export const resfreshTokenAction = async (refreshToken: RefreshTokenValue) => {
|
||||||
try {
|
try {
|
||||||
const response = await refreshApi.patch('/auth/refresh', {refresh_token: refreshToken.token});
|
const response = await refreshApi.patch('/auth/refresh', refreshToken);
|
||||||
|
|
||||||
const parsed = RefreshTokenResponseSchema.safeParse(response.data);
|
const parsed = RefreshTokenResponseSchema.safeParse(response.data);
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export function LoginForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("", className)} {...props}>
|
<div className={cn("", className)} {...props}>
|
||||||
<Card className="overflow-hidden">
|
<Card className="">
|
||||||
<CardContent className="grid p-0 md:grid-cols-2">
|
<CardContent className="flex flex-col-reverse md:flex-row p-0">
|
||||||
<UserAuthForm />
|
<UserAuthForm />
|
||||||
<div className="relative hidden bg-muted md:block">
|
<div className="md:bg-muted">
|
||||||
<img
|
<img
|
||||||
src="logo.png"
|
src="logo.png"
|
||||||
alt="Image"
|
alt="Imagen del Logo"
|
||||||
className="absolute inset-0 p-10 h-full w-full"
|
className="pt-3 md:p-3 h-full w-1/3 md:w-full m-auto "
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ export function LoginForm({
|
|||||||
return (
|
return (
|
||||||
<div className={cn("", className)} {...props}>
|
<div className={cn("", className)} {...props}>
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
<CardContent className="flex p-0">
|
<CardContent className="flex flex-col-reverse md:flex-row p-0">
|
||||||
<UserAuthForm />
|
<UserAuthForm />
|
||||||
<div className="hidden bg-muted md:block m-auto">
|
<div className="md:m-auto">
|
||||||
<img
|
<img
|
||||||
src="logo.png"
|
src="logo.png"
|
||||||
alt="Image"
|
alt="Image"
|
||||||
className="inset-0 p-5"
|
className="pt-3 md:p-5 w-1/3 md:w-full m-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ export default function UserAuthForm() {
|
|||||||
<>
|
<>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
|
||||||
<form className="p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
<form className="flex-none p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-bold">Sistema para productores</h1>
|
<h1 className="text-2xl font-bold">Sistema Gestión de Productores</h1>
|
||||||
<p className="text-balance text-muted-foreground">
|
<p className="text-balance text-muted-foreground hidden md:block">
|
||||||
Ingresa tus datos
|
Ingresa tus datos
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function UserAuthForm() {
|
|||||||
<form className="flex-none p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
<form className="flex-none p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="items-center text-center">
|
<div className="items-center text-center">
|
||||||
<h1 className="text-2xl font-bold">Sistema para productores</h1>
|
<h1 className="text-2xl font-bold">Sistema Gestión de Productores</h1>
|
||||||
<p className="text-balance text-muted-foreground">
|
<p className="text-balance text-muted-foreground">
|
||||||
Ingresa tus datos
|
Ingresa tus datos
|
||||||
</p>
|
</p>
|
||||||
@@ -303,7 +303,7 @@ export default function UserAuthForm() {
|
|||||||
<FormMessage className="text-red-500">{error}</FormMessage>
|
<FormMessage className="text-red-500">{error}</FormMessage>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<Button type="submit" className="w-full">
|
<Button type="submit" className="w-full">
|
||||||
Registrarce
|
Registrarse
|
||||||
</Button>
|
</Button>
|
||||||
<div className="text-center text-sm">
|
<div className="text-center text-sm">
|
||||||
¿Ya tienes una cuenta?{" "}
|
¿Ya tienes una cuenta?{" "}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
// refreshtoken
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { tokensSchema } from './login';
|
import { tokensSchema } from './login';
|
||||||
|
|
||||||
// Esquema para el refresh token
|
// Esquema para el refresh token
|
||||||
export const refreshTokenSchema = z.object({
|
export const refreshTokenSchema = z.object({
|
||||||
|
user_id: z.number(),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { DataTable } from '@repo/shadcn/table/data-table';
|
import { DataTable } from '@repo/shadcn/table/data-table';
|
||||||
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
||||||
import { columns } from './product-tables/columns';
|
|
||||||
import { useProductQuery } from '../../hooks/use-query-products';
|
import { useProductQuery } from '../../hooks/use-query-products';
|
||||||
|
import { columns } from './product-tables/columns';
|
||||||
|
|
||||||
interface dataListProps {
|
interface dataListProps {
|
||||||
initialPage: number;
|
initialPage: number;
|
||||||
initialSearch?: string | null;
|
initialSearch?: string | null;
|
||||||
initialLimit: number;
|
initialLimit: number;
|
||||||
|
initialType?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UsersAdminList({
|
export default function UsersAdminList({
|
||||||
@@ -19,9 +20,9 @@ export default function UsersAdminList({
|
|||||||
page: initialPage,
|
page: initialPage,
|
||||||
limit: initialLimit,
|
limit: initialLimit,
|
||||||
...(initialSearch && { search: initialSearch }),
|
...(initialSearch && { search: initialSearch }),
|
||||||
}
|
};
|
||||||
|
|
||||||
const {data, isLoading} = useProductQuery(filters)
|
const { data, isLoading } = useProductQuery(filters);
|
||||||
|
|
||||||
// console.log(data?.data);
|
// console.log(data?.data);
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ export const columns: ColumnDef<InventoryTable>[] = [
|
|||||||
accessorKey: 'urlImg',
|
accessorKey: 'urlImg',
|
||||||
header: 'img',
|
header: 'img',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<img src={`/uploads/inventory/${row.original.userId}/${row.original.id}/${row.original.urlImg}`} alt="" width={64} height={64} className="rounded"/>
|
<img src={`/uploads/inventory/${row.original.userId}/${row.original.id}/${row.original.urlImg}`} alt="" width={64} height={64} className="rounded" />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@ export const columns: ColumnDef<InventoryTable>[] = [
|
|||||||
{
|
{
|
||||||
accessorKey: 'price',
|
accessorKey: 'price',
|
||||||
header: 'Precio',
|
header: 'Precio',
|
||||||
cell: ({ row }) => `${row.original.price}$`
|
cell: ({ row }) => `${row.original.price} Bs.`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'stock',
|
accessorKey: 'stock',
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { useUpdateProduct } from "@/feactures/inventory/hooks/use-mutation";
|
import { useUpdateProduct } from "@/feactures/inventory/hooks/use-mutation";
|
||||||
import { updateInventory, EditInventory, InventoryTable } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad
|
import { updateInventory, EditInventory, InventoryTable } from '@/feactures/inventory/schemas/inventory'; // Renombrado EditInventory para claridad
|
||||||
import { Textarea } from '@repo/shadcn/components/ui/textarea';
|
import { Textarea } from '@repo/shadcn/components/ui/textarea';
|
||||||
import {STATUS} from '@/constants/status'
|
import { STATUS } from '@/constants/status'
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {sizeFormate} from "@/feactures/inventory/utils/sizeFormate"
|
import { sizeFormate } from "@/feactures/inventory/utils/sizeFormate"
|
||||||
// import { z } from 'zod'; // Asegúrate de importar Zod
|
// import { z } from 'zod'; // Asegúrate de importar Zod
|
||||||
|
|
||||||
// --- MODIFICACIÓN CLAVE ---
|
// --- MODIFICACIÓN CLAVE ---
|
||||||
@@ -154,7 +154,7 @@ export function UpdateForm({
|
|||||||
<FormItem >
|
<FormItem >
|
||||||
<FormLabel>Precio</FormLabel>
|
<FormLabel>Precio</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input type="number" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -182,7 +182,7 @@ export function UpdateForm({
|
|||||||
<FormItem className='col-span-2'>
|
<FormItem className='col-span-2'>
|
||||||
<FormLabel>Descripción</FormLabel>
|
<FormLabel>Descripción</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea {...field} className="resize-none"/>
|
<Textarea {...field} className="resize-none" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -196,7 +196,7 @@ export function UpdateForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cantidad/Stock</FormLabel>
|
<FormLabel>Cantidad/Stock</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="number" onChange={(e) => field.onChange(Number(e.target.value))}/>
|
<Input {...field} type="number" onChange={(e) => field.onChange(Number(e.target.value))} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -248,8 +248,8 @@ export function UpdateForm({
|
|||||||
const newPreviewUrls: string[] = [];
|
const newPreviewUrls: string[] = [];
|
||||||
|
|
||||||
files.forEach(element => {
|
files.forEach(element => {
|
||||||
size += element.size;
|
size += element.size;
|
||||||
newPreviewUrls.push(URL.createObjectURL(element));
|
newPreviewUrls.push(URL.createObjectURL(element));
|
||||||
});
|
});
|
||||||
|
|
||||||
const tamañoFormateado = sizeFormate(size);
|
const tamañoFormateado = sizeFormate(size);
|
||||||
@@ -257,18 +257,18 @@ export function UpdateForm({
|
|||||||
setPreviewUrls(newPreviewUrls);
|
setPreviewUrls(newPreviewUrls);
|
||||||
onChange(e.target.files);
|
onChange(e.target.files);
|
||||||
} else {
|
} else {
|
||||||
setPreviewUrls([]);
|
setPreviewUrls([]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
{previewUrls.length > 0 && (
|
{previewUrls.length > 0 && (
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
{previewUrls.map((url, index) => (
|
{previewUrls.map((url, index) => (
|
||||||
<img key={index} src={url} alt={`Preview ${index}`} className="w-24 h-24 object-cover rounded-md" />
|
<img key={index} src={url} alt={`Preview ${index}`} className="w-24 h-24 object-cover rounded-md" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function ProductCard({ product, onClick }: cardProps) {
|
|||||||
{product.status === 'AGOTADO' ? (
|
{product.status === 'AGOTADO' ? (
|
||||||
<p className="font-semibold text-lg text-red-900">AGOTADO</p>
|
<p className="font-semibold text-lg text-red-900">AGOTADO</p>
|
||||||
) : ('')}
|
) : ('')}
|
||||||
<p className="font-semibold text-lg">$ {product.price}</p>
|
<p className="font-semibold text-lg">{product.price} Bs.</p>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { allProducts } from "../../schemas/inventory";
|
import { allProducts } from "../../schemas/inventory";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@repo/shadcn/card';
|
} from '@repo/shadcn/card';
|
||||||
|
|
||||||
export function ProductList({product}: {product: allProducts}) {
|
export function ProductList({ product }: { product: allProducts }) {
|
||||||
const [selectedImg, setSelectedImg] = useState(`/uploads/inventory/${product.userId}/${product.id}/${product.urlImg}`)
|
const [selectedImg, setSelectedImg] = useState(`/uploads/inventory/${product.userId}/${product.id}/${product.urlImg}`)
|
||||||
console.log(product);
|
console.log(product);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <PageContainer>
|
// <PageContainer>
|
||||||
<main className='px-4 lg:px-6 flex flex-col md:flex-row gap-3 lg:gap-4 md:relative'>
|
<main className='px-4 lg:px-6 flex flex-col md:flex-row gap-3 lg:gap-4 md:relative'>
|
||||||
<div className='w-full flex justify-between flex-col'>
|
<div className='w-full flex justify-between flex-col'>
|
||||||
@@ -31,15 +31,15 @@ return (
|
|||||||
</span>
|
</span>
|
||||||
</span> */}
|
</span> */}
|
||||||
{product.gallery?.map((img, index) => (
|
{product.gallery?.map((img, index) => (
|
||||||
<img
|
<img
|
||||||
key={index}
|
key={index}
|
||||||
className="cursor-pointer border-2 object-cover w-[64px] h-[64px] md:w-[96px] md:h-[96px] aspect-square rounded-2xl"
|
className="cursor-pointer border-2 object-cover w-[64px] h-[64px] md:w-[96px] md:h-[96px] aspect-square rounded-2xl"
|
||||||
src={`/uploads/inventory/${product.userId}/${product.id}/${img}`}
|
src={`/uploads/inventory/${product.userId}/${product.id}/${img}`}
|
||||||
alt=""
|
alt=""
|
||||||
onClick={() => setSelectedImg(`/uploads/inventory/${product.userId}/${product.id}/${img}`)}
|
onClick={() => setSelectedImg(`/uploads/inventory/${product.userId}/${product.id}/${img}`)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/* <div className="sticky right-0 flex items-center">
|
{/* <div className="sticky right-0 flex items-center">
|
||||||
<span className="text-xl p-3 cursor-pointer bg-neutral-800/50 rounded-full text-white">
|
<span className="text-xl p-3 cursor-pointer bg-neutral-800/50 rounded-full text-white">
|
||||||
{">"}
|
{">"}
|
||||||
</span>
|
</span>
|
||||||
@@ -53,7 +53,7 @@ return (
|
|||||||
<CardTitle className="font-bold text-2xl text-primary">
|
<CardTitle className="font-bold text-2xl text-primary">
|
||||||
{product.title.charAt(0).toUpperCase() + product.title.slice(1)}
|
{product.title.charAt(0).toUpperCase() + product.title.slice(1)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className='font-semibold'>{product.price}$
|
<p className='font-semibold'>{product.price} Bs.
|
||||||
{product.status === 'AGOTADO' ? (
|
{product.status === 'AGOTADO' ? (
|
||||||
<span className="font-semibold text-lg text-red-900"> AGOTADO</span>
|
<span className="font-semibold text-lg text-red-900"> AGOTADO</span>
|
||||||
) : ('')}
|
) : ('')}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function SurveysHeader() {
|
|||||||
description="Gestiona las encuestas disponibles en la plataforma"
|
description="Gestiona las encuestas disponibles en la plataforma"
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => router.push(`/dashboard/administracion/encuestas/crear`)} size="sm">
|
<Button onClick={() => router.push(`/dashboard/administracion/encuestas/crear`)} size="sm">
|
||||||
<Plus className="mr-2 h-4 w-4" /> Agregar Encuesta
|
<Plus className="h-4 w-4"/><span className='hidden sm:inline'>Agregar Encuesta</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
163
apps/web/feactures/training/actions/training-actions.ts
Normal file
163
apps/web/feactures/training/actions/training-actions.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
'use server';
|
||||||
|
import { safeFetchApi } from '@/lib/fetch.api';
|
||||||
|
import { trainingStatisticsResponseSchema } from '../schemas/statistics';
|
||||||
|
import {
|
||||||
|
TrainingMutate,
|
||||||
|
TrainingSchema,
|
||||||
|
trainingApiResponseSchema,
|
||||||
|
} from '../schemas/training';
|
||||||
|
|
||||||
|
export const getTrainingStatisticsAction = async (
|
||||||
|
params: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
stateId?: number;
|
||||||
|
municipalityId?: number;
|
||||||
|
parishId?: number;
|
||||||
|
ospType?: string;
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||||
|
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||||
|
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
|
||||||
|
if (params.municipalityId)
|
||||||
|
searchParams.append('municipalityId', params.municipalityId.toString());
|
||||||
|
if (params.parishId)
|
||||||
|
searchParams.append('parishId', params.parishId.toString());
|
||||||
|
if (params.ospType) searchParams.append('ospType', params.ospType);
|
||||||
|
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
trainingStatisticsResponseSchema,
|
||||||
|
`/training/statistics?${searchParams.toString()}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
|
return response?.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTrainingAction = async (params: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
}) => {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
page: (params.page || 1).toString(),
|
||||||
|
limit: (params.limit || 10).toString(),
|
||||||
|
...(params.search && { search: params.search }),
|
||||||
|
...(params.sortBy && { sortBy: params.sortBy }),
|
||||||
|
...(params.sortOrder && { sortOrder: params.sortOrder }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
trainingApiResponseSchema,
|
||||||
|
`/training?${searchParams}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response?.data || [],
|
||||||
|
meta: response?.meta || {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalCount: 0,
|
||||||
|
totalPages: 1,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
nextPage: null,
|
||||||
|
previousPage: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTrainingAction = async (
|
||||||
|
payload: TrainingSchema | FormData,
|
||||||
|
) => {
|
||||||
|
let payloadToSend = payload;
|
||||||
|
let id: number | undefined;
|
||||||
|
|
||||||
|
if (payload instanceof FormData) {
|
||||||
|
payload.delete('id');
|
||||||
|
payloadToSend = payload;
|
||||||
|
} else {
|
||||||
|
const { id: _, ...rest } = payload;
|
||||||
|
payloadToSend = rest as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(payloadToSend);
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
'/training',
|
||||||
|
'POST',
|
||||||
|
payloadToSend,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Error al crear el registro');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTrainingAction = async (
|
||||||
|
payload: TrainingSchema | FormData,
|
||||||
|
) => {
|
||||||
|
let id: string | null = null;
|
||||||
|
let payloadToSend = payload;
|
||||||
|
|
||||||
|
if (payload instanceof FormData) {
|
||||||
|
id = payload.get('id') as string;
|
||||||
|
payload.delete('id');
|
||||||
|
payloadToSend = payload;
|
||||||
|
} else {
|
||||||
|
id = payload.id?.toString() || null;
|
||||||
|
const { id: _, ...rest } = payload;
|
||||||
|
payloadToSend = rest as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) throw new Error('ID es requerido para actualizar');
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
`/training/${id}`,
|
||||||
|
'PATCH',
|
||||||
|
payloadToSend,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Error al actualizar el registro');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTrainingAction = async (id: number) => {
|
||||||
|
const [error] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
`/training/${id}`,
|
||||||
|
'DELETE',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message || 'Error al eliminar el registro');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTrainingByIdAction = async (id: number) => {
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
`/training/${id}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
|
return response?.data;
|
||||||
|
};
|
||||||
41
apps/web/feactures/training/columnas del excel.sql
Normal file
41
apps/web/feactures/training/columnas del excel.sql
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
-- datos basicos
|
||||||
|
nombre,
|
||||||
|
apellido,
|
||||||
|
fecha de la visita,
|
||||||
|
-->Falta
|
||||||
|
hora de la visita,
|
||||||
|
-- datos de la ubicacion
|
||||||
|
estado,
|
||||||
|
municipio,
|
||||||
|
parroquia,
|
||||||
|
nombre de la comuna,
|
||||||
|
CODIGO SITUR COMUNA,
|
||||||
|
CONSEJO COMUNAL,
|
||||||
|
CODIGO SITUR CONSEJO COMUNAL,
|
||||||
|
-- datos de la osp
|
||||||
|
actividad productiva (agricola,textil,bloquera,carpinteria,unidad de suministro),
|
||||||
|
realice una breve descripcion del requerimiento financiero,
|
||||||
|
NOMBRE DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
DIRECCIÓN DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
RIF DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
TIPO DE ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
ESTATUS ACTUAL,
|
||||||
|
AÑO DE CONSTITUCIÓN DE LA EMPRESA ,
|
||||||
|
CANTIDAD DE PRODUCTORES QUE LA CONFORMAN,
|
||||||
|
BREVE DESCRIPCIÓN DEL PRODUCTO O SERVICIO QUE OFRECE,
|
||||||
|
CAPACIDAD INSTALADA,
|
||||||
|
CAPACIDAD OPERATIVA,
|
||||||
|
¿EXPLIQUE LAS RAZONES GENERALES POR LAS CUALES LA UNIDAD DE PRODUCCIÓN TUVO QUE PARALIZARSE?
|
||||||
|
-- datos del responsable
|
||||||
|
NOMBRE Y APELLIDO DEL RESPONSABLE DE LA OSP,
|
||||||
|
CÉDULA DEL RESPONSABLE (SIN PUNTOS),
|
||||||
|
RIF DEL RESPONSABLE (SIN PUNTOS),
|
||||||
|
TELÉFONOS (COLOQUE 2 NUMEROS DE TELEFONOS),
|
||||||
|
CORREO ELECTRÓNICO,
|
||||||
|
ESTADO CIVIL DEL PRODUCTOR,
|
||||||
|
CARGA FAMILIAR,
|
||||||
|
NUMERO DE HIJOS,
|
||||||
|
-- datos adicionales
|
||||||
|
OBSERVACIONES GENERALES,
|
||||||
|
-- fotos
|
||||||
|
COLOCAR TRES (3) REGISTROS FOTOGRÁFICOS VISIBLES DEL ESPACIO Y MAQUINARIAS ACTUALMENTE (OBLIGATORIO),
|
||||||
171
apps/web/feactures/training/components/equipment-list.tsx
Normal file
171
apps/web/feactures/training/components/equipment-list.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@repo/shadcn/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@repo/shadcn/components/ui/table';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import { Label } from '@repo/shadcn/label';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
export function EquipmentList() {
|
||||||
|
const { control, register } = useFormContext();
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'equipmentList',
|
||||||
|
});
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [newItem, setNewItem] = useState({
|
||||||
|
machine: '',
|
||||||
|
specifications: '',
|
||||||
|
quantity: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (newItem.machine && newItem.quantity) {
|
||||||
|
append({ ...newItem, quantity: Number(newItem.quantity) });
|
||||||
|
setNewItem({ machine: '', specifications: '', quantity: '' });
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium">Datos del Equipamiento</h3>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Agregar Maquinaria</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Agregar Maquinaria/Equipo</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
Datos de equipamiento
|
||||||
|
</DialogDescription>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Maquinaria</Label>
|
||||||
|
<Input
|
||||||
|
value={newItem.machine}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, machine: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Nombre de la maquinaria"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<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
|
||||||
|
type="number"
|
||||||
|
value={newItem.quantity}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, quantity: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={handleAdd}>Guardar</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Maquinaria</TableHead>
|
||||||
|
<TableHead>Especificaciones</TableHead>
|
||||||
|
<TableHead>Cantidad</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<TableRow key={field.id}>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`equipmentList.${index}.machine`)}
|
||||||
|
/>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
{field.machine}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...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"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{fields.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No hay equipamiento registrado
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1526
apps/web/feactures/training/components/form.tsx
Normal file
1526
apps/web/feactures/training/components/form.tsx
Normal file
File diff suppressed because it is too large
Load Diff
504
apps/web/feactures/training/components/product-activity-list.tsx
Normal file
504
apps/web/feactures/training/components/product-activity-list.tsx
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
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,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@repo/shadcn/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@repo/shadcn/components/ui/table';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import { Label } from '@repo/shadcn/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
import { SelectSearchable } from '@repo/shadcn/select-searchable';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
// 1. Definimos la estructura de los datos para que TypeScript no se queje
|
||||||
|
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() {
|
||||||
|
// 2. Pasamos el tipo genérico a useFormContext
|
||||||
|
const { control, register } = useFormContext<ProductFormValues>();
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'productList',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
// 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.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">
|
||||||
|
<h3 className="text-lg font-medium">Datos de Actividad Productiva</h3>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Agregar Producto/Actividad</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[800px] max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Detalles de Actividad Productiva</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
Datos de actividad productiva
|
||||||
|
</DialogDescription>
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* Basic Info */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>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
|
||||||
|
value={newItem.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, description: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cant. Diario</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newItem.dailyCount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, dailyCount: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cant. Semanal</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newItem.weeklyCount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, weeklyCount: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cant. Mensual</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newItem.monthlyCount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, monthlyCount: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={handleAdd}>Guardar</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Producto</TableHead>
|
||||||
|
<TableHead>Descripción</TableHead>
|
||||||
|
<TableHead>Mensual</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<TableRow key={field.id}>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productList.${index}.productName`)}
|
||||||
|
// field.productName ahora es válido gracias a la interface
|
||||||
|
value={field.productName}
|
||||||
|
/>
|
||||||
|
{field.productName}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{field.description}</TableCell>
|
||||||
|
<TableCell>{field.monthlyCount}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{fields.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No hay productos registrados
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
171
apps/web/feactures/training/components/production-list.tsx
Normal file
171
apps/web/feactures/training/components/production-list.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@repo/shadcn/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@repo/shadcn/components/ui/table';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import { Label } from '@repo/shadcn/label';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
export function ProductionList() {
|
||||||
|
const { control, register } = useFormContext();
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'productionList',
|
||||||
|
});
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [newItem, setNewItem] = useState({
|
||||||
|
rawMaterial: '',
|
||||||
|
supplyType: '',
|
||||||
|
quantity: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (newItem.rawMaterial && newItem.quantity) {
|
||||||
|
append({ ...newItem, quantity: Number(newItem.quantity) });
|
||||||
|
setNewItem({ rawMaterial: '', supplyType: '', quantity: '' });
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium">Datos de Producción</h3>
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Agregar Producción</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<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
|
||||||
|
value={newItem.supplyType}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, supplyType: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Tipo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Cantidad Mensual (Kg, TON, UNID. LT)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={newItem.quantity}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewItem({ ...newItem, quantity: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={handleAdd}>Guardar</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Materia Prima</TableHead>
|
||||||
|
<TableHead>Tipo Insumo</TableHead>
|
||||||
|
<TableHead>Cantidad (Mensual)</TableHead>
|
||||||
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<TableRow key={field.id}>
|
||||||
|
<TableCell>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register(`productionList.${index}.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`)}
|
||||||
|
/>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
{field.quantity}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{fields.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No hay datos de producción registrados
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
apps/web/feactures/training/components/training-header.tsx
Normal file
13
apps/web/feactures/training/components/training-header.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
import { Heading } from '@repo/shadcn/heading';
|
||||||
|
|
||||||
|
export function TrainingHeader() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<Heading
|
||||||
|
title="Registro de Organizaciones Socioproductivas"
|
||||||
|
description="Gestiona los registros de las OSP"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
apps/web/feactures/training/components/training-list.tsx
Normal file
40
apps/web/feactures/training/components/training-list.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
import { DataTable } from '@repo/shadcn/table/data-table';
|
||||||
|
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
||||||
|
import { useTrainingQuery } from '../hooks/use-training';
|
||||||
|
import { columns } from './training-tables/columns';
|
||||||
|
|
||||||
|
interface TrainingListProps {
|
||||||
|
initialPage: number;
|
||||||
|
initialSearch?: string | null;
|
||||||
|
initialLimit: number;
|
||||||
|
apiUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TrainingList({
|
||||||
|
initialPage,
|
||||||
|
initialSearch,
|
||||||
|
initialLimit,
|
||||||
|
apiUrl,
|
||||||
|
}: TrainingListProps) {
|
||||||
|
const filters = {
|
||||||
|
page: initialPage,
|
||||||
|
limit: initialLimit,
|
||||||
|
...(initialSearch && { search: initialSearch }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isLoading } = useTrainingQuery(filters);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <DataTableSkeleton columnCount={5} rowCount={initialLimit} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
columns={columns({ apiUrl })}
|
||||||
|
data={data?.data || []}
|
||||||
|
totalItems={data?.meta.totalCount || 0}
|
||||||
|
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
373
apps/web/feactures/training/components/training-statistics.tsx
Normal file
373
apps/web/feactures/training/components/training-statistics.tsx
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useMunicipalityQuery,
|
||||||
|
useParishQuery,
|
||||||
|
useStateQuery,
|
||||||
|
} from '@/feactures/location/hooks/use-query-location';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@repo/shadcn/card';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
import { SelectSearchable } from '@repo/shadcn/select-searchable';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
Legend,
|
||||||
|
Pie,
|
||||||
|
PieChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
|
||||||
|
|
||||||
|
const OSP_TYPES = [
|
||||||
|
'EPSD',
|
||||||
|
'EPSI',
|
||||||
|
'UPF',
|
||||||
|
'Cooperativa',
|
||||||
|
'Grupo de Intercambio',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function TrainingStatistics() {
|
||||||
|
// Filter State
|
||||||
|
const [startDate, setStartDate] = useState<string>('');
|
||||||
|
const [endDate, setEndDate] = useState<string>('');
|
||||||
|
const [stateId, setStateId] = useState<number>(0);
|
||||||
|
const [municipalityId, setMunicipalityId] = useState<number>(0);
|
||||||
|
const [parishId, setParishId] = useState<number>(0);
|
||||||
|
const [ospType, setOspType] = useState<string>('');
|
||||||
|
|
||||||
|
// Location Data
|
||||||
|
const { data: dataState } = useStateQuery();
|
||||||
|
const { data: dataMunicipality } = useMunicipalityQuery(stateId);
|
||||||
|
const { data: dataParish } = useParishQuery(municipalityId);
|
||||||
|
|
||||||
|
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
|
||||||
|
const municipalityOptions =
|
||||||
|
Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
|
||||||
|
? dataMunicipality.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
|
||||||
|
const parishOptions =
|
||||||
|
Array.isArray(dataParish?.data) && dataParish.data.length > 0
|
||||||
|
? dataParish.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
|
||||||
|
|
||||||
|
// Query with Filters
|
||||||
|
const { data, isLoading, refetch } = useTrainingStatsQuery({
|
||||||
|
startDate: startDate || undefined,
|
||||||
|
endDate: endDate || undefined,
|
||||||
|
stateId: stateId || undefined,
|
||||||
|
municipalityId: municipalityId || undefined,
|
||||||
|
parishId: parishId || undefined,
|
||||||
|
ospType: ospType || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
setStartDate('');
|
||||||
|
setEndDate('');
|
||||||
|
setStateId(0);
|
||||||
|
setMunicipalityId(0);
|
||||||
|
setParishId(0);
|
||||||
|
setOspType('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center p-8">Cargando estadísticas...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center p-8">No hay datos disponibles.</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
totalOsps,
|
||||||
|
totalProducers,
|
||||||
|
statusDistribution,
|
||||||
|
activityDistribution,
|
||||||
|
typeDistribution,
|
||||||
|
stateDistribution,
|
||||||
|
yearDistribution,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'#0088FE',
|
||||||
|
'#00C49F',
|
||||||
|
'#FFBB28',
|
||||||
|
'#FF8042',
|
||||||
|
'#8884d8',
|
||||||
|
'#82ca9d',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filters Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filtros</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Fecha Inicio</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Fecha Fin</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Estado</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={stateOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setStateId(Number(value));
|
||||||
|
setMunicipalityId(0); // Reset municipality
|
||||||
|
setParishId(0); // Reset parish
|
||||||
|
}}
|
||||||
|
placeholder="Selecciona un estado"
|
||||||
|
defaultValue={stateId ? stateId.toString() : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Municipio</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={municipalityOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setMunicipalityId(Number(value));
|
||||||
|
setParishId(0);
|
||||||
|
}}
|
||||||
|
placeholder="Selecciona municipio"
|
||||||
|
defaultValue={municipalityId ? municipalityId.toString() : ''}
|
||||||
|
disabled={!stateId || stateId === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Parroquia</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={parishOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => setParishId(Number(value))}
|
||||||
|
placeholder="Selecciona parroquia"
|
||||||
|
defaultValue={parishId ? parishId.toString() : ''}
|
||||||
|
disabled={!municipalityId || municipalityId === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Tipo de OSP</label>
|
||||||
|
<Select value={ospType} onValueChange={setOspType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Todos" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
|
{OSP_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{type}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button variant="outline" onClick={handleClearFilters}>
|
||||||
|
Limpiar Filtros
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Total de OSP Registradas
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalOsps}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Organizaciones Socioproductivas
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Total de Productores
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalProducers}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Productores asociados
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Actividad Productiva</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Distribución por tipo de actividad
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={activityDistribution}
|
||||||
|
layout="vertical"
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={150} />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* State Distribution */}
|
||||||
|
{/* <Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribución por Estado</CardTitle>
|
||||||
|
<CardDescription>OSP registradas por estado</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={stateDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#00C49F" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card> */}
|
||||||
|
|
||||||
|
{/* Year Distribution */}
|
||||||
|
<Card className="col-span-full lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Año de Constitución</CardTitle>
|
||||||
|
<CardDescription>Año de registro de la empresa</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={yearDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Estatus Actual</CardTitle>
|
||||||
|
<CardDescription>Estado operativo de las OSP</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={statusDistribution}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{statusDistribution.map((entry, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={COLORS[index % COLORS.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Tipo de Organización</CardTitle>
|
||||||
|
<CardDescription>Clasificación de las OSP</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={typeDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
'use client';
|
||||||
|
import { AlertModal } from '@/components/modal/alert-modal';
|
||||||
|
import { useDeleteTraining } from '@/feactures/training/hooks/use-training';
|
||||||
|
import { TrainingSchema } from '@/feactures/training/schemas/training';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@repo/shadcn/tooltip';
|
||||||
|
import { Edit, Eye, Trash, FileDown } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { TrainingViewModal } from '../training-view-modal';
|
||||||
|
|
||||||
|
interface CellActionProps {
|
||||||
|
data: TrainingSchema;
|
||||||
|
apiUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CellAction: React.FC<CellActionProps> = ({ data, apiUrl }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [viewOpen, setViewOpen] = useState(false);
|
||||||
|
const { mutate: deleteTraining } = useDeleteTraining();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onConfirm = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
deleteTraining(data.id!);
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = (id?: number | undefined) => {
|
||||||
|
window.open(`${apiUrl}/training/export/${id}`, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AlertModal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
loading={loading}
|
||||||
|
title="¿Estás seguro que desea eliminar este registro?"
|
||||||
|
description="Esta acción no se puede deshacer."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TrainingViewModal
|
||||||
|
isOpen={viewOpen}
|
||||||
|
onClose={() => setViewOpen(false)}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setViewOpen(true)}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Ver detalle</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/dashboard/formulario/editar/${data.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Editar</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Eliminar</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
import { TrainingSchema } from '@/feactures/training/schemas/training';
|
||||||
|
import { Badge } from '@repo/shadcn/badge';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { CellAction } from './cell-action';
|
||||||
|
|
||||||
|
interface ColumnsProps {
|
||||||
|
apiUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function columns({ apiUrl }: ColumnsProps): ColumnDef<TrainingSchema>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: 'ospName',
|
||||||
|
header: 'Nombre OSP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'ospRif',
|
||||||
|
header: 'RIF',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'ospType',
|
||||||
|
header: 'Tipo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'currentStatus',
|
||||||
|
header: 'Estatus',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.getValue('currentStatus') as string;
|
||||||
|
return (
|
||||||
|
<Badge variant={status === 'ACTIVA' ? 'default' : 'secondary'}>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'visitDate',
|
||||||
|
header: 'Fecha Visita',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const date = row.getValue('visitDate') as string;
|
||||||
|
return date ? new Date(date).toLocaleString() : 'N/A';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Acciones',
|
||||||
|
cell: ({ row }) => <CellAction data={row.original} apiUrl={apiUrl} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@repo/shadcn/components/ui/button';
|
||||||
|
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
|
||||||
|
import { Plus } from 'lucide-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();
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between mt-4 ">
|
||||||
|
<div className="flex items-center gap-4 flex-grow">
|
||||||
|
<DataTableSearch
|
||||||
|
searchKey="nombre"
|
||||||
|
searchQuery={searchQuery || ''}
|
||||||
|
setSearchQuery={setSearchQuery}
|
||||||
|
setPage={setPage}
|
||||||
|
/>
|
||||||
|
</div>{' '}
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push(`/dashboard/formulario/nuevo`)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
<span className="hidden md:inline">Nuevo Registro</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { searchParams } from '@repo/shadcn/lib/searchparams';
|
||||||
|
import { useQueryState } from 'nuqs';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
export function useTrainingTableFilters() {
|
||||||
|
const [searchQuery, setSearchQuery] = useQueryState(
|
||||||
|
'q',
|
||||||
|
searchParams.q
|
||||||
|
.withOptions({
|
||||||
|
shallow: false,
|
||||||
|
throttleMs: 500,
|
||||||
|
})
|
||||||
|
.withDefault(''),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [page, setPage] = useQueryState(
|
||||||
|
'page',
|
||||||
|
searchParams.page.withDefault(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetFilters = useCallback(() => {
|
||||||
|
setSearchQuery(null);
|
||||||
|
setPage(1);
|
||||||
|
}, [setSearchQuery, setPage]);
|
||||||
|
|
||||||
|
const isAnyFilterActive = useMemo(() => {
|
||||||
|
return !!searchQuery;
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
resetFilters,
|
||||||
|
isAnyFilterActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
526
apps/web/feactures/training/components/training-view-modal.tsx
Normal file
526
apps/web/feactures/training/components/training-view-modal.tsx
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Badge } from '@repo/shadcn/badge';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@repo/shadcn/components/ui/card';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
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,
|
||||||
|
MapPin,
|
||||||
|
Package,
|
||||||
|
Wrench,
|
||||||
|
X,
|
||||||
|
} 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;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrainingViewModal({
|
||||||
|
data,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: TrainingViewModalProps) {
|
||||||
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { data: statesData } = useStateQuery();
|
||||||
|
const { data: municipalitiesData } = useMunicipalityQuery(data?.state || 0);
|
||||||
|
const { data: parishesData } = useParishQuery(data?.municipality || 0);
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const stateName = statesData?.data?.find((s: any) => s.id === data.state)?.name;
|
||||||
|
const municipalityName = municipalitiesData?.data?.find(
|
||||||
|
(m: any) => m.id === data.municipality,
|
||||||
|
)?.name;
|
||||||
|
const parishName = parishesData?.data?.find(
|
||||||
|
(p: any) => p.id === data.parish,
|
||||||
|
)?.name;
|
||||||
|
|
||||||
|
const DetailItem = ({ label, value }: { label: string; value: any }) => (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-semibold text-foreground break-words">
|
||||||
|
{value !== null && value !== undefined && value !== '' ? value : 'N/A'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Section = ({
|
||||||
|
title,
|
||||||
|
icon: Icon,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
icon?: React.ElementType;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => (
|
||||||
|
<Card className="overflow-hidden border-l-4 border-l-primary/20">
|
||||||
|
<CardHeader className="py-3 bg-muted/30">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
{Icon && <Icon className="h-5 w-5 text-primary" />}
|
||||||
|
{title}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-6 pt-4">
|
||||||
|
{children}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BooleanBadge = ({ value }: { value?: boolean }) => (
|
||||||
|
<Badge variant={value ? 'default' : 'secondary'}>
|
||||||
|
{value ? 'Sí' : 'No'}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-[1000px] max-h-[90vh] p-0 flex flex-col">
|
||||||
|
<DialogHeader className="px-6 py-4 border-b">
|
||||||
|
<DialogTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<Factory className="h-6 w-6" />
|
||||||
|
{data.ospName}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{data.ospType} • {data.ospRif} •{' '}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
data.currentStatus === 'ACTIVA'
|
||||||
|
? 'text-green-600 font-medium'
|
||||||
|
: 'text-red-600'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data.currentStatus}
|
||||||
|
</span>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1 px-6 py-6">
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* 1. Datos de la Visita */}
|
||||||
|
<Section title="Datos de la Visita">
|
||||||
|
<DetailItem
|
||||||
|
label="Coordinador"
|
||||||
|
value={`${data.firstname} ${data.lastname}`}
|
||||||
|
/>
|
||||||
|
<DetailItem label="Teléfono Coord." value={data.coorPhone} />
|
||||||
|
<DetailItem
|
||||||
|
label="Fecha Visita"
|
||||||
|
value={
|
||||||
|
data.visitDate
|
||||||
|
? new Date(data.visitDate).toLocaleString()
|
||||||
|
: 'N/A'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DetailItem label="Estado" value={stateName} />
|
||||||
|
<DetailItem label="Municipio" value={municipalityName} />
|
||||||
|
<DetailItem label="Parroquia" value={parishName} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 2. Sectores y Actividad */}
|
||||||
|
<Section title="Sectores Económicos">
|
||||||
|
<DetailItem label="Sector Económico" value={data.ecoSector} />
|
||||||
|
<DetailItem
|
||||||
|
label="Sector Productivo"
|
||||||
|
value={data.productiveSector}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Actividad Central"
|
||||||
|
value={data.centralProductiveActivity}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Actividad Principal"
|
||||||
|
value={data.mainProductiveActivity}
|
||||||
|
/>
|
||||||
|
<div className="col-span-full">
|
||||||
|
<DetailItem
|
||||||
|
label="Actividad Específica"
|
||||||
|
value={data.productiveActivity}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 3. Infraestructura y Ubicación */}
|
||||||
|
<Section title="Infraestructura y Ubicación" icon={MapPin}>
|
||||||
|
<DetailItem
|
||||||
|
label="Año Constitución"
|
||||||
|
value={data.companyConstitutionYear}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Infraestructura (m²)"
|
||||||
|
value={data.infrastructureMt2}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Tipo Estructura"
|
||||||
|
value={data.structureType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailItem
|
||||||
|
label="Posee Transporte"
|
||||||
|
value={<BooleanBadge value={data.hasTransport} />}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Espacio Abierto"
|
||||||
|
value={<BooleanBadge value={data.isOpenSpace} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="col-span-full space-y-4 mt-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground uppercase">
|
||||||
|
Dirección
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-medium">{data.ospAddress}</p>
|
||||||
|
</div>
|
||||||
|
{data.ospGoogleMapsLink && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={data.ospGoogleMapsLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
Ver en Google Maps
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 4. LISTAS DETALLADAS (Lo nuevo) */}
|
||||||
|
|
||||||
|
{/* PRODUCTOS */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Package className="h-5 w-5" />
|
||||||
|
Productos y Mano de Obra
|
||||||
|
<Badge variant="secondary" className="ml-2">
|
||||||
|
{data.productList?.length || 0}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4">
|
||||||
|
{data.productList?.map((prod: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-muted/40 p-4 rounded-lg border text-sm"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<h4 className="font-bold text-base text-primary">
|
||||||
|
{prod.productName}
|
||||||
|
</h4>
|
||||||
|
<Badge variant="outline">
|
||||||
|
Mano de obra:{' '}
|
||||||
|
{Number(prod.menCount || 0) +
|
||||||
|
Number(prod.womenCount || 0)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground mb-3">
|
||||||
|
{prod.description}
|
||||||
|
</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="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>
|
||||||
|
))}
|
||||||
|
{(!data.productList || data.productList.length === 0) && (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
No hay productos registrados.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* EQUIPAMIENTO Y PRODUCCIÓN */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Wrench className="h-5 w-5" />
|
||||||
|
Equipamiento
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{data.equipmentList?.map((eq: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex justify-between items-center p-2 rounded bg-muted/40 border"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{eq.machine}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{eq.specifications}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-sm font-bold h-8 w-8 flex items-center justify-center rounded-full"
|
||||||
|
>
|
||||||
|
{eq.quantity}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!data.equipmentList ||
|
||||||
|
data.equipmentList.length === 0) && (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
No hay equipamiento registrado.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Factory className="h-5 w-5" />
|
||||||
|
Materia Prima
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{data.productionList?.map((mat: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex justify-between items-center p-2 rounded bg-muted/40 border"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{mat.rawMaterial}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{mat.supplyType}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary">Cant: {mat.quantity}</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!data.productionList ||
|
||||||
|
data.productionList.length === 0) && (
|
||||||
|
<p className="text-sm text-muted-foreground italic">
|
||||||
|
No hay materia prima registrada.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 5. Comuna y Responsable */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<Section title="Datos de la Comuna">
|
||||||
|
<DetailItem label="Comuna" value={data.communeName} />
|
||||||
|
<DetailItem
|
||||||
|
label="Código SITUR"
|
||||||
|
value={data.siturCodeCommune}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Vocero"
|
||||||
|
value={data.communeSpokespersonName}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Teléfono"
|
||||||
|
value={data.communeSpokespersonPhone}
|
||||||
|
/>
|
||||||
|
<div className="col-span-full border-t pt-4 mt-2">
|
||||||
|
<DetailItem
|
||||||
|
label="Consejo Comunal"
|
||||||
|
value={data.communalCouncil}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Vocero C.C."
|
||||||
|
value={data.communalCouncilSpokespersonName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Responsable OSP">
|
||||||
|
<DetailItem
|
||||||
|
label="Nombre"
|
||||||
|
value={data.ospResponsibleFullname}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Cédula"
|
||||||
|
value={data.ospResponsibleCedula}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Teléfono"
|
||||||
|
value={data.ospResponsiblePhone}
|
||||||
|
/>
|
||||||
|
<DetailItem label="Email" value={data.ospResponsibleEmail} />
|
||||||
|
<DetailItem
|
||||||
|
label="Carga Familiar"
|
||||||
|
value={data.familyBurden}
|
||||||
|
/>
|
||||||
|
<DetailItem label="Hijos" value={data.numberOfChildren} />
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 6. Observaciones */}
|
||||||
|
{(data.generalObservations || data.paralysisReason) && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Observaciones</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{data.generalObservations && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold text-muted-foreground uppercase mb-1">
|
||||||
|
Generales
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">{data.generalObservations}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.paralysisReason && (
|
||||||
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 rounded-md border border-red-200 dark:border-red-900">
|
||||||
|
<p className="text-xs font-bold text-red-600 dark:text-red-400 uppercase mb-1">
|
||||||
|
Motivo de Paralización
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">{data.paralysisReason}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 7. Fotos */}
|
||||||
|
<Section title="Registro Fotográfico">
|
||||||
|
{[data.photo1, data.photo2, data.photo3].some(Boolean) ? (
|
||||||
|
<div className="col-span-full grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
{[data.photo1, data.photo2, data.photo3].map(
|
||||||
|
(photo, idx) =>
|
||||||
|
photo && (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="relative aspect-video rounded-lg overflow-hidden cursor-zoom-in border hover:shadow-lg transition-all"
|
||||||
|
onClick={() => setSelectedImage(photo)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
|
||||||
|
alt={`Evidencia ${idx + 1}`}
|
||||||
|
className="object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground col-span-full">
|
||||||
|
No hay imágenes cargadas.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<DialogFooter className="px-6 py-4 border-t bg-muted/20">
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
Cerrar
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Lightbox para imágenes */}
|
||||||
|
<Dialog
|
||||||
|
open={!!selectedImage}
|
||||||
|
onOpenChange={() => setSelectedImage(null)}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-[95vw] max-h-[95vh] p-0 overflow-hidden bg-black/95 border-none">
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>Imagen Ampliada</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogDescription></DialogDescription>
|
||||||
|
<div className="relative w-full h-full flex items-center justify-center p-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-4 right-4 text-white hover:bg-white/20 rounded-full z-50"
|
||||||
|
onClick={() => setSelectedImage(null)}
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
{selectedImage && (
|
||||||
|
<img
|
||||||
|
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
|
||||||
|
alt="Vista ampliada"
|
||||||
|
className="max-w-full max-h-[90vh] object-contain rounded-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
169
apps/web/feactures/training/constants/osp-data.ts
Normal file
169
apps/web/feactures/training/constants/osp-data.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
export const SECTOR_ECONOMICO = {
|
||||||
|
PRIMARIO: 'PRIMARIO',
|
||||||
|
SECUNDARIO: 'SECUNDARIO',
|
||||||
|
TERCIARIO: 'TERCIARIO',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const SECTOR_PRODUCTIVO = {
|
||||||
|
AGRICOLA: 'AGRÍCOLA',
|
||||||
|
MANUFACTURA: 'MANUFACTURA',
|
||||||
|
SERVICIOS: 'SERVICIOS',
|
||||||
|
TURISMO: 'TURISMO',
|
||||||
|
COMERCIO: 'COMERCIO',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ACTIVIDAD_CENTRAL = {
|
||||||
|
PRODUCCION_VEGETAL: 'PRODUCCIÓN VEGETAL',
|
||||||
|
PRODUCCION_ANIMAL: 'PRODUCCIÓN ANIMAL',
|
||||||
|
PRODUCCION_VEGETAL_ANIMAL: 'PRODUCCIÓN VEGETAL Y ANIMAL',
|
||||||
|
INDUSTRIAL: 'INDUSTRIAL',
|
||||||
|
SERVICIOS: 'SERVICIOS',
|
||||||
|
TURISMO: 'TURISMO',
|
||||||
|
COMERCIO: 'COMERCIO',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ACTIVIDAD_PRINCIPAL = {
|
||||||
|
AGRICULTURA: 'AGRICULTURA',
|
||||||
|
CRIA: 'CRIA',
|
||||||
|
PATIOS_PRODUCTIVOS: 'PATIOS PRODUCTIVOS O CONUCOS',
|
||||||
|
TRANSFORMACION_MATERIA: 'TRANSFORMACION DE LA MATERIA PRIMA',
|
||||||
|
TEXTIL: 'TALLER DE COFECCION TEXTIL',
|
||||||
|
CONSTRUCCION: 'CONSTRUCION',
|
||||||
|
BIENES_SERVICIOS: 'OFRECER PRODUCTOS DE BIENES Y SERVICIOS',
|
||||||
|
VISITAS_GUIADAS: 'VISITAS GUIADAS',
|
||||||
|
ALOJAMIENTO: 'ALOJAMIENTO',
|
||||||
|
TURISMO: 'TURISMO',
|
||||||
|
COMERCIO: 'COMERCIO',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const SECTOR_ECONOMICO_OPTIONS = [
|
||||||
|
SECTOR_ECONOMICO.PRIMARIO,
|
||||||
|
SECTOR_ECONOMICO.SECUNDARIO,
|
||||||
|
SECTOR_ECONOMICO.TERCIARIO,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Map: Sector Economico -> Productive Sectors
|
||||||
|
export const SECTOR_PRODUCTIVO_MAP: Record<string, string[]> = {
|
||||||
|
[SECTOR_ECONOMICO.PRIMARIO]: [SECTOR_PRODUCTIVO.AGRICOLA],
|
||||||
|
[SECTOR_ECONOMICO.SECUNDARIO]: [SECTOR_PRODUCTIVO.MANUFACTURA],
|
||||||
|
[SECTOR_ECONOMICO.TERCIARIO]: [
|
||||||
|
SECTOR_PRODUCTIVO.SERVICIOS,
|
||||||
|
SECTOR_PRODUCTIVO.TURISMO,
|
||||||
|
SECTOR_PRODUCTIVO.COMERCIO,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map: Productive Sector -> Central Productive Activity
|
||||||
|
export const ACTIVIDAD_CENTRAL_MAP: Record<string, string[]> = {
|
||||||
|
[SECTOR_PRODUCTIVO.AGRICOLA]: [
|
||||||
|
ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL,
|
||||||
|
ACTIVIDAD_CENTRAL.PRODUCCION_ANIMAL,
|
||||||
|
ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL_ANIMAL,
|
||||||
|
],
|
||||||
|
[SECTOR_PRODUCTIVO.MANUFACTURA]: [ACTIVIDAD_CENTRAL.INDUSTRIAL],
|
||||||
|
[SECTOR_PRODUCTIVO.SERVICIOS]: [ACTIVIDAD_CENTRAL.SERVICIOS],
|
||||||
|
[SECTOR_PRODUCTIVO.TURISMO]: [ACTIVIDAD_CENTRAL.TURISMO],
|
||||||
|
[SECTOR_PRODUCTIVO.COMERCIO]: [ACTIVIDAD_CENTRAL.COMERCIO],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map: Central Productive Activity -> Main Productive Activity
|
||||||
|
export const ACTIVIDAD_PRINCIPAL_MAP: Record<string, string[]> = {
|
||||||
|
[ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL]: [ACTIVIDAD_PRINCIPAL.AGRICULTURA],
|
||||||
|
[ACTIVIDAD_CENTRAL.PRODUCCION_ANIMAL]: [ACTIVIDAD_PRINCIPAL.CRIA],
|
||||||
|
[ACTIVIDAD_CENTRAL.PRODUCCION_VEGETAL_ANIMAL]: [
|
||||||
|
ACTIVIDAD_PRINCIPAL.PATIOS_PRODUCTIVOS,
|
||||||
|
],
|
||||||
|
[ACTIVIDAD_CENTRAL.INDUSTRIAL]: [
|
||||||
|
ACTIVIDAD_PRINCIPAL.TRANSFORMACION_MATERIA,
|
||||||
|
ACTIVIDAD_PRINCIPAL.TEXTIL,
|
||||||
|
ACTIVIDAD_PRINCIPAL.CONSTRUCCION,
|
||||||
|
],
|
||||||
|
[ACTIVIDAD_CENTRAL.SERVICIOS]: [ACTIVIDAD_PRINCIPAL.BIENES_SERVICIOS],
|
||||||
|
[ACTIVIDAD_CENTRAL.TURISMO]: [
|
||||||
|
ACTIVIDAD_PRINCIPAL.VISITAS_GUIADAS,
|
||||||
|
ACTIVIDAD_PRINCIPAL.ALOJAMIENTO,
|
||||||
|
ACTIVIDAD_PRINCIPAL.TURISMO,
|
||||||
|
],
|
||||||
|
[ACTIVIDAD_CENTRAL.COMERCIO]: [ACTIVIDAD_PRINCIPAL.COMERCIO],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map: Main Productive Activity -> Productive Activity (The long list)
|
||||||
|
export const ACTIVIDAD_PRODUCTIVA_MAP: Record<string, string[]> = {
|
||||||
|
[ACTIVIDAD_PRINCIPAL.AGRICULTURA]: [
|
||||||
|
'SIEMBRA DE MAIZ',
|
||||||
|
'SIEMBRA DE AJI',
|
||||||
|
'SIEMBRA DE CAFÉ',
|
||||||
|
'SIEMBRA DE PLATANO',
|
||||||
|
'SIEMBRA DE CAMBUR',
|
||||||
|
'SIEMBRA DE AGUACATE',
|
||||||
|
'SIEMBRA DE FRUTAS',
|
||||||
|
'SIEMBRA DE HORTALIZAS',
|
||||||
|
'SIEMBRA DE TOMATE',
|
||||||
|
'SIEMBRA DE CACAO',
|
||||||
|
'SIEMBRA DE PIMENTON',
|
||||||
|
'SIEMBRA DE YUCA',
|
||||||
|
'SIEMBRA DE CAÑA DE AZUCAR',
|
||||||
|
'SIEMBRA DE GRANOS (CARAOTAS, FRIJOLES)',
|
||||||
|
'SIEMBRA DE ARROZ',
|
||||||
|
'SIEMBRA DE CEREALES (CEBADA, LINAZA, SOYA)',
|
||||||
|
'ELABORACION DE BIO-INSUMO (ABONO ORGANICO)',
|
||||||
|
],
|
||||||
|
[ACTIVIDAD_PRINCIPAL.CRIA]: [
|
||||||
|
'BOVINO',
|
||||||
|
'PORCINO',
|
||||||
|
'CAPRINO',
|
||||||
|
'CUNICULTURA',
|
||||||
|
'AVICOLA',
|
||||||
|
'PISCICULA',
|
||||||
|
],
|
||||||
|
[ACTIVIDAD_PRINCIPAL.PATIOS_PRODUCTIVOS]: ['SIEMBRA Y CRIA'],
|
||||||
|
[ACTIVIDAD_PRINCIPAL.TRANSFORMACION_MATERIA]: [
|
||||||
|
'ELABORACION DE PRODUCTOS QUIMICOS (LIMPIEZA E HIGIENE PERSONAL)',
|
||||||
|
'PANADERIAS',
|
||||||
|
'RESPOSTERIA',
|
||||||
|
'ELABORACION DE HARINAS PRECOCIDA',
|
||||||
|
'PLANTA ABA (ELABORACION DE ALIMENTOS BALANCEADOS PARA ANIMALES)',
|
||||||
|
'ELABORACION DE PRODUCTOS DERIVADO DE LA LECHE (VACA, CABRA, BUFFALA)',
|
||||||
|
'EMPAQUETADORAS DE GRANOS Y POLVOS',
|
||||||
|
'ELABORACION DE ACEITE COMESTIBLE',
|
||||||
|
'FABRICA DE HIELO',
|
||||||
|
'ELABORACION DE PAPELON',
|
||||||
|
'ARTESANIAS',
|
||||||
|
],
|
||||||
|
[ACTIVIDAD_PRINCIPAL.TEXTIL]: [
|
||||||
|
'ELABORACION DE UNIFORME ESCOLARES Y PRENDA DE VESTIR',
|
||||||
|
'ELABORACION DE PRENDAS INTIMAS',
|
||||||
|
'ELABORACION DE LENCERIA',
|
||||||
|
'SUBLIMACION DE TEJIDOS',
|
||||||
|
'ELABORACION DE CALZADOS',
|
||||||
|
],
|
||||||
|
[ACTIVIDAD_PRINCIPAL.CONSTRUCCION]: [
|
||||||
|
'BLOQUERAS',
|
||||||
|
'PLANTA PREMEZCLADORA DE CEMENTO',
|
||||||
|
'CARPINTERIAS',
|
||||||
|
'HERRERIAS',
|
||||||
|
],
|
||||||
|
[ACTIVIDAD_PRINCIPAL.BIENES_SERVICIOS]: [
|
||||||
|
'MERCADOS COMUNALES',
|
||||||
|
'CENTROS DE ACOPIOS Y DISTRIBUCION',
|
||||||
|
'UNIDAD DE SUMINISTRO',
|
||||||
|
'MATADERO (SALA DE MATANZA DE ANIMALES)',
|
||||||
|
'PELUQUERIA',
|
||||||
|
'BARBERIA',
|
||||||
|
'AGENCIAS DE FESTEJOS',
|
||||||
|
'LAVANDERIAS',
|
||||||
|
'REPARACION DE CALZADOS',
|
||||||
|
'TALLER DE MECANICA',
|
||||||
|
'TRANSPORTES',
|
||||||
|
],
|
||||||
|
[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',
|
||||||
|
],
|
||||||
|
};
|
||||||
13
apps/web/feactures/training/hooks/use-training-statistics.ts
Normal file
13
apps/web/feactures/training/hooks/use-training-statistics.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useSafeQuery } from '@/hooks/use-safe-query';
|
||||||
|
import { getTrainingStatisticsAction } from '../actions/training-actions';
|
||||||
|
|
||||||
|
export function useTrainingStatsQuery(params: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
stateId?: number;
|
||||||
|
municipalityId?: number;
|
||||||
|
parishId?: number;
|
||||||
|
ospType?: string;
|
||||||
|
} = {}) {
|
||||||
|
return useSafeQuery(['training-statistics', JSON.stringify(params)], () => getTrainingStatisticsAction(params));
|
||||||
|
}
|
||||||
44
apps/web/feactures/training/hooks/use-training.ts
Normal file
44
apps/web/feactures/training/hooks/use-training.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useSafeQuery } from '@/hooks/use-safe-query';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
createTrainingAction,
|
||||||
|
deleteTrainingAction,
|
||||||
|
getTrainingAction,
|
||||||
|
getTrainingByIdAction,
|
||||||
|
updateTrainingAction,
|
||||||
|
} from '../actions/training-actions';
|
||||||
|
import { TrainingSchema } from '../schemas/training';
|
||||||
|
|
||||||
|
export function useTrainingQuery(params = {}) {
|
||||||
|
return useSafeQuery(['training', params], () => getTrainingAction(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTrainingByIdQuery(id: number) {
|
||||||
|
return useSafeQuery(['training', id], () => getTrainingByIdAction(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateTraining() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (data: TrainingSchema) => createTrainingAction(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
|
});
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateTraining() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (data: TrainingSchema) => updateTrainingAction(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
|
});
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteTraining() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => deleteTrainingAction(id),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
27
apps/web/feactures/training/schemas/statistics.ts
Normal file
27
apps/web/feactures/training/schemas/statistics.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const statisticsItemSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.transform((val) => val || 'Sin Información'),
|
||||||
|
value: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const trainingStatisticsSchema = z.object({
|
||||||
|
totalOsps: 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),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
|
||||||
|
|
||||||
|
export const trainingStatisticsResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: trainingStatisticsSchema,
|
||||||
|
});
|
||||||
186
apps/web/feactures/training/schemas/training.ts
Normal file
186
apps/web/feactures/training/schemas/training.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
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({
|
||||||
|
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({
|
||||||
|
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(),
|
||||||
|
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(),
|
||||||
|
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().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' }),
|
||||||
|
currentStatus: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Estatus actual es requerido' })
|
||||||
|
.default('ACTIVA'),
|
||||||
|
infrastructureMt2: z.string().optional().or(z.literal('')),
|
||||||
|
hasTransport: z
|
||||||
|
.preprocess((val) => val === 'true' || val === true, z.boolean())
|
||||||
|
.optional(),
|
||||||
|
structureType: z.string().optional().or(z.literal('')),
|
||||||
|
isOpenSpace: z
|
||||||
|
.preprocess((val) => val === 'true' || val === true, z.boolean())
|
||||||
|
.optional(),
|
||||||
|
paralysisReason: z.string().optional().default(''),
|
||||||
|
|
||||||
|
//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([]),
|
||||||
|
|
||||||
|
//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().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' })
|
||||||
|
.optional()
|
||||||
|
.or(z.literal('')),
|
||||||
|
communalCouncil: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Consejo Comunal es requerido' }),
|
||||||
|
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' })
|
||||||
|
.optional()
|
||||||
|
.or(z.literal('')),
|
||||||
|
|
||||||
|
//Datos del Responsable OSP
|
||||||
|
ospResponsibleCedula: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Cédula del responsable es requerida' }),
|
||||||
|
ospResponsibleFullname: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Nombre del responsable es requerido' }),
|
||||||
|
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' }),
|
||||||
|
ospResponsibleEmail: z
|
||||||
|
.string()
|
||||||
|
.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().default(''),
|
||||||
|
|
||||||
|
//IMAGENES
|
||||||
|
files: z.any().optional(),
|
||||||
|
|
||||||
|
//no se envia la backend al crear ni editar el formulario
|
||||||
|
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(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TrainingSchema = z.infer<typeof trainingSchema>;
|
||||||
|
|
||||||
|
export const trainingApiResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: z.array(trainingSchema),
|
||||||
|
meta: z.object({
|
||||||
|
page: z.number(),
|
||||||
|
limit: z.number(),
|
||||||
|
totalCount: z.number(),
|
||||||
|
totalPages: z.number(),
|
||||||
|
hasNextPage: z.boolean(),
|
||||||
|
hasPreviousPage: z.boolean(),
|
||||||
|
nextPage: z.number().nullable(),
|
||||||
|
previousPage: z.number().nullable(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TrainingMutate = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: trainingSchema,
|
||||||
|
});
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import { safeFetchApi } from '@/lib/fetch.api';
|
import { safeFetchApi } from '@/lib/fetch.api';
|
||||||
import {
|
import {
|
||||||
surveysApiResponseSchema,
|
|
||||||
CreateUser,
|
CreateUser,
|
||||||
|
surveysApiResponseSchema,
|
||||||
|
UpdateUser,
|
||||||
UsersMutate,
|
UsersMutate,
|
||||||
UpdateUser
|
|
||||||
} from '../schemas/users';
|
} from '../schemas/users';
|
||||||
|
|
||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
|
|
||||||
|
|
||||||
export const getProfileAction = async () => {
|
export const getProfileAction = async () => {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
const id = session?.user?.id
|
const id = session?.user?.id;
|
||||||
|
|
||||||
const [error, response] = await safeFetchApi(
|
const [error, response] = await safeFetchApi(
|
||||||
UsersMutate,
|
UsersMutate,
|
||||||
`/users/${id}`,
|
`/users/${id}`,
|
||||||
'GET'
|
'GET',
|
||||||
);
|
);
|
||||||
if (error) throw new Error(error.message);
|
if (error) throw new Error(error.message);
|
||||||
return response;
|
return response;
|
||||||
@@ -33,7 +32,6 @@ export const updateProfileAction = async (payload: UpdateUser) => {
|
|||||||
payloadWithoutId,
|
payloadWithoutId,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(payload);
|
|
||||||
if (error) {
|
if (error) {
|
||||||
if (error.message === 'Email already exists') {
|
if (error.message === 'Email already exists') {
|
||||||
throw new Error('Ese correo ya está en uso');
|
throw new Error('Ese correo ya está en uso');
|
||||||
@@ -51,7 +49,6 @@ export const getUsersAction = async (params: {
|
|||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortOrder?: 'asc' | 'desc';
|
sortOrder?: 'asc' | 'desc';
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
page: (params.page || 1).toString(),
|
page: (params.page || 1).toString(),
|
||||||
limit: (params.limit || 10).toString(),
|
limit: (params.limit || 10).toString(),
|
||||||
@@ -83,7 +80,7 @@ export const getUsersAction = async (params: {
|
|||||||
previousPage: null,
|
previousPage: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const createUserAction = async (payload: CreateUser) => {
|
export const createUserAction = async (payload: CreateUser) => {
|
||||||
const { id, confirmPassword, ...payloadWithoutId } = payload;
|
const { id, confirmPassword, ...payloadWithoutId } = payload;
|
||||||
@@ -130,19 +127,14 @@ export const updateUserAction = async (payload: UpdateUser) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const deleteUserAction = async (id: Number) => {
|
export const deleteUserAction = async (id: Number) => {
|
||||||
const [error] = await safeFetchApi(
|
const [error] = await safeFetchApi(UsersMutate, `/users/${id}`, 'DELETE');
|
||||||
UsersMutate,
|
|
||||||
`/users/${id}`,
|
|
||||||
'DELETE'
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
||||||
|
|
||||||
// if (error) throw new Error(error.message || 'Error al eliminar el usuario')
|
// if (error) throw new Error(error.message || 'Error al eliminar el usuario')
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@repo/shadcn/select';
|
} from '@repo/shadcn/select';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useCreateUser } from "../../hooks/use-mutation-users";
|
import { useCreateUser } from '../../hooks/use-mutation-users';
|
||||||
import { CreateUser, createUser } from '../../schemas/users';
|
import { CreateUser, createUser } from '../../schemas/users';
|
||||||
|
|
||||||
const ROLES = {
|
const ROLES = {
|
||||||
@@ -29,8 +29,9 @@ const ROLES = {
|
|||||||
4: 'Gerente',
|
4: 'Gerente',
|
||||||
5: 'Usuario',
|
5: 'Usuario',
|
||||||
6: 'Productor',
|
6: 'Productor',
|
||||||
7: 'Organización'
|
7: 'Organización',
|
||||||
}
|
8: 'Coordinadores',
|
||||||
|
};
|
||||||
|
|
||||||
interface CreateUserFormProps {
|
interface CreateUserFormProps {
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
@@ -60,7 +61,7 @@ export function CreateUserForm({
|
|||||||
id: defaultValues?.id,
|
id: defaultValues?.id,
|
||||||
phone: defaultValues?.phone || '',
|
phone: defaultValues?.phone || '',
|
||||||
role: undefined,
|
role: undefined,
|
||||||
}
|
};
|
||||||
|
|
||||||
const form = useForm<CreateUser>({
|
const form = useForm<CreateUser>({
|
||||||
resolver: zodResolver(createUser),
|
resolver: zodResolver(createUser),
|
||||||
@@ -69,8 +70,6 @@ export function CreateUserForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (formData: CreateUser) => {
|
const onSubmit = async (formData: CreateUser) => {
|
||||||
console.log(formData);
|
|
||||||
|
|
||||||
saveAccountingAccounts(formData, {
|
saveAccountingAccounts(formData, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -143,7 +142,7 @@ export function CreateUserForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Teléfono</FormLabel>
|
<FormLabel>Teléfono</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} value={field.value?.toString() ?? ''}/>
|
<Input {...field} value={field.value?.toString() ?? ''} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -157,7 +156,7 @@ export function CreateUserForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Contraseña</FormLabel>
|
<FormLabel>Contraseña</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" {...field}/>
|
<Input type="password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -166,12 +165,12 @@ export function CreateUserForm({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='confirmPassword'
|
name="confirmPassword"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Confirmar Contraseña</FormLabel>
|
<FormLabel>Confirmar Contraseña</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" {...field}/>
|
<Input type="password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -184,7 +183,9 @@ export function CreateUserForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormLabel>Rol</FormLabel>
|
<FormLabel>Rol</FormLabel>
|
||||||
<Select onValueChange={(value) => field.onChange(Number(value))}>
|
<Select
|
||||||
|
onValueChange={(value) => field.onChange(Number(value))}
|
||||||
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Selecciona un rol" />
|
<SelectValue placeholder="Selecciona un rol" />
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useUpdateUser } from '@/feactures/users/hooks/use-mutation-users';
|
||||||
|
import { UpdateUser, updateUser } from '@/feactures/users/schemas/users';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Button } from '@repo/shadcn/button';
|
import { Button } from '@repo/shadcn/button';
|
||||||
import {
|
import {
|
||||||
@@ -19,8 +21,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@repo/shadcn/select';
|
} from '@repo/shadcn/select';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useUpdateUser } from "@/feactures/users/hooks/use-mutation-users";
|
|
||||||
import { UpdateUser, updateUser } from '@/feactures/users/schemas/users';
|
|
||||||
|
|
||||||
const ROLES = {
|
const ROLES = {
|
||||||
// 1: 'Superadmin',
|
// 1: 'Superadmin',
|
||||||
@@ -29,8 +29,9 @@ const ROLES = {
|
|||||||
4: 'Gerente',
|
4: 'Gerente',
|
||||||
5: 'Usuario',
|
5: 'Usuario',
|
||||||
6: 'Productor',
|
6: 'Productor',
|
||||||
7: 'Organización'
|
7: 'Organización',
|
||||||
}
|
8: 'Coordinadores',
|
||||||
|
};
|
||||||
|
|
||||||
interface UserFormProps {
|
interface UserFormProps {
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
@@ -57,8 +58,8 @@ export function UpdateUserForm({
|
|||||||
id: defaultValues?.id,
|
id: defaultValues?.id,
|
||||||
phone: defaultValues?.phone || '',
|
phone: defaultValues?.phone || '',
|
||||||
role: undefined,
|
role: undefined,
|
||||||
isActive: defaultValues?.isActive
|
isActive: defaultValues?.isActive,
|
||||||
}
|
};
|
||||||
|
|
||||||
// console.log(defaultValues);
|
// console.log(defaultValues);
|
||||||
|
|
||||||
@@ -69,8 +70,7 @@ export function UpdateUserForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateUser) => {
|
const onSubmit = async (data: UpdateUser) => {
|
||||||
|
const formData = data;
|
||||||
const formData = data
|
|
||||||
|
|
||||||
saveAccountingAccounts(formData, {
|
saveAccountingAccounts(formData, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -144,7 +144,7 @@ export function UpdateUserForm({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Teléfono</FormLabel>
|
<FormLabel>Teléfono</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} value={field.value?.toString() ?? ''}/>
|
<Input {...field} value={field.value?.toString() ?? ''} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -153,12 +153,12 @@ export function UpdateUserForm({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='password'
|
name="password"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Nueva Contraseña</FormLabel>
|
<FormLabel>Nueva Contraseña</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" {...field}/>
|
<Input type="password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -171,7 +171,9 @@ export function UpdateUserForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormLabel>Rol</FormLabel>
|
<FormLabel>Rol</FormLabel>
|
||||||
<Select onValueChange={(value) => field.onChange(Number(value))}>
|
<Select
|
||||||
|
onValueChange={(value) => field.onChange(Number(value))}
|
||||||
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Selecciona un rol" />
|
<SelectValue placeholder="Selecciona un rol" />
|
||||||
@@ -196,7 +198,10 @@ export function UpdateUserForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormLabel>Estatus</FormLabel>
|
<FormLabel>Estatus</FormLabel>
|
||||||
<Select defaultValue={String(field.value)} onValueChange={(value) => field.onChange(Boolean(value))}>
|
<Select
|
||||||
|
defaultValue={String(field.value)}
|
||||||
|
onValueChange={(value) => field.onChange(Boolean(value))}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Seleccione un estatus" />
|
<SelectValue placeholder="Seleccione un estatus" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function UsersHeader() {
|
|||||||
description="Gestiona los usuarios registrados en la plataforma"
|
description="Gestiona los usuarios registrados en la plataforma"
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => setOpen(true)} size="sm">
|
<Button onClick={() => setOpen(true)} size="sm">
|
||||||
<Plus className="mr-2 h-4 w-4" /> Agregar Usuario
|
<Plus className="h-4 w-4"/><span className='hidden sm:inline'>Agregar Usuario</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -78,9 +78,7 @@ export function ModalForm({
|
|||||||
parish: undefined
|
parish: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log(defaultValues);
|
||||||
|
|
||||||
console.log(defaultValues);
|
|
||||||
|
|
||||||
const form = useForm<UpdateUser>({
|
const form = useForm<UpdateUser>({
|
||||||
resolver: zodResolver(updateUser),
|
resolver: zodResolver(updateUser),
|
||||||
|
|||||||
@@ -5,12 +5,24 @@ import { Edit2 } from 'lucide-react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AccountPlanModal } from './modal-profile';
|
import { AccountPlanModal } from './modal-profile';
|
||||||
|
|
||||||
|
const ROLE_TRANSLATIONS: Record<string, string> = {
|
||||||
|
superadmin: 'Superadmin',
|
||||||
|
admin: 'Administrador',
|
||||||
|
autoridad: 'Autoridad',
|
||||||
|
manager: 'Gerente',
|
||||||
|
user: 'Usuario',
|
||||||
|
producers: 'Productor',
|
||||||
|
organization: 'Organización',
|
||||||
|
coordinators: 'Coordinador',
|
||||||
|
};
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { data } = useUserByProfile();
|
const { data } = useUserByProfile();
|
||||||
|
|
||||||
// console.log("🎯 data:", data);
|
const userRole = data?.data.role as string;
|
||||||
|
const translatedRole = ROLE_TRANSLATIONS[userRole] || userRole || 'Sin Rol';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -18,58 +30,60 @@ export function Profile() {
|
|||||||
<Edit2 className="mr-2 h-4 w-4" /> Editar Perfil
|
<Edit2 className="mr-2 h-4 w-4" /> Editar Perfil
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<AccountPlanModal open={open} onOpenChange={setOpen} defaultValues={data?.data}/>
|
<AccountPlanModal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
defaultValues={data?.data}
|
||||||
|
/>
|
||||||
|
|
||||||
<h2 className='mt-3 mb-1'>Datos del usuario</h2>
|
<h2 className="mt-3 mb-1">Datos del usuario</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 ">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 ">
|
||||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||||
<p className='font-bold text-lg'>Usuario:</p>
|
<p className="font-bold text-lg">Usuario:</p>
|
||||||
<p>{data?.data.username || 'Sin Nombre de Usuario'}</p>
|
<p>{data?.data.username || 'Sin Nombre de Usuario'}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||||
<p className='font-bold text-lg'>Rol:</p>
|
<p className="font-bold text-lg">Rol:</p>
|
||||||
<p>{data?.data.role || 'Sin Rol'}</p>
|
<p>{translatedRole}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className='mt-3 mb-1'>Información personal</h2>
|
<h2 className="mt-3 mb-1">Información personal</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||||
<p className='font-bold text-lg'>Nombre completo:</p>
|
<p className="font-bold text-lg">Nombre completo:</p>
|
||||||
<p>{data?.data.fullname || 'Sin nombre y apellido'}</p>
|
<p>{data?.data.fullname || 'Sin nombre y apellido'}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||||
<p className='font-bold text-lg'>Correo:</p>
|
<p className="font-bold text-lg">Correo:</p>
|
||||||
<p>{data?.data.email || 'Sin correo'}</p>
|
<p>{data?.data.email || 'Sin correo'}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||||
<p className='font-bold text-lg'>Teléfono:</p>
|
<p className="font-bold text-lg">Teléfono:</p>
|
||||||
<p>{data?.data.phone || 'Sin teléfono'}</p>
|
<p>{data?.data.phone || 'Sin teléfono'}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className='mt-3 mb-1'>Información de ubicación</h2>
|
<h2 className="mt-3 mb-1">Información de ubicación</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||||
<p className='font-bold text-lg'>Estado:</p>
|
<p className="font-bold text-lg">Estado:</p>
|
||||||
<p>{data?.data.state || 'Sin Estado'}</p>
|
<p>{data?.data.state || 'Sin Estado'}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||||
<p className='font-bold text-lg'>Municipio:</p>
|
<p className="font-bold text-lg">Municipio:</p>
|
||||||
<p>{data?.data.municipality || 'Sin Municipio'}</p>
|
<p>{data?.data.municipality || 'Sin Municipio'}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||||
<p className='font-bold text-lg'>Parroquia:</p>
|
<p className="font-bold text-lg">Parroquia:</p>
|
||||||
<p>{data?.data.parish || 'Sin Parroquia'}</p>
|
<p>{data?.data.parish || 'Sin Parroquia'}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SignInAction } from '@/feactures/auth/actions/login-action';
|
import { SignInAction } from '@/feactures/auth/actions/login-action';
|
||||||
import { resfreshTokenAction } from '@/feactures/auth/actions/refresh-token-action';
|
import { resfreshTokenAction } from '@/feactures/auth/actions/refresh-token-action';
|
||||||
import { CredentialsSignin, NextAuthConfig, Session, User } from 'next-auth';
|
import { CredentialsSignin, NextAuthConfig, Session, User } from 'next-auth';
|
||||||
import { DefaultJWT } from 'next-auth/jwt';
|
// import { DefaultJWT } from 'next-auth/jwt';
|
||||||
import CredentialProvider from 'next-auth/providers/credentials';
|
import CredentialProvider from 'next-auth/providers/credentials';
|
||||||
|
|
||||||
|
|
||||||
@@ -91,8 +91,6 @@ const authConfig: NextAuthConfig = {
|
|||||||
refresh_token: response?.tokens.refresh_token ?? '',
|
refresh_token: response?.tokens.refresh_token ?? '',
|
||||||
refresh_expire_in: response?.tokens.refresh_expire_in ?? 0,
|
refresh_expire_in: response?.tokens.refresh_expire_in ?? 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -100,11 +98,7 @@ const authConfig: NextAuthConfig = {
|
|||||||
signIn: '/', //sigin page
|
signIn: '/', //sigin page
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user }:{
|
async jwt({ token, user }: { user: User, token: any }) {
|
||||||
user: User
|
|
||||||
token: any
|
|
||||||
|
|
||||||
}) {
|
|
||||||
// 1. Manejar el inicio de sesión inicial
|
// 1. Manejar el inicio de sesión inicial
|
||||||
// El `user` solo se proporciona en el primer inicio de sesión.
|
// El `user` solo se proporciona en el primer inicio de sesión.
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -119,7 +113,6 @@ const authConfig: NextAuthConfig = {
|
|||||||
refresh_token: user.refresh_token,
|
refresh_token: user.refresh_token,
|
||||||
refresh_expire_in: user.refresh_expire_in
|
refresh_expire_in: user.refresh_expire_in
|
||||||
}
|
}
|
||||||
// return token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Si no es un nuevo login, verificar la expiración del token
|
// 2. Si no es un nuevo login, verificar la expiración del token
|
||||||
@@ -130,43 +123,33 @@ const authConfig: NextAuthConfig = {
|
|||||||
return token; // Si no ha expirado, no hacer nada y devolver el token actual
|
return token; // Si no ha expirado, no hacer nada y devolver el token actual
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("Now Access Expire:",token.access_expire_in);
|
|
||||||
|
|
||||||
|
|
||||||
// 3. Si el token de acceso ha expirado, verificar el refresh token
|
// 3. Si el token de acceso ha expirado, verificar el refresh token
|
||||||
// console.log("Access token ha expirado. Verificando refresh token...");
|
|
||||||
if (now > (token.refresh_expire_in as number)) {
|
if (now > (token.refresh_expire_in as number)) {
|
||||||
// console.log("Refresh token ha expirado. Forzando logout.");
|
|
||||||
return null; // Forzar el logout al devolver null
|
return null; // Forzar el logout al devolver null
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("token:", token.refresh_token);
|
|
||||||
|
|
||||||
|
|
||||||
// 4. Si el token de acceso ha expirado pero el refresh token es válido, renovar
|
// 4. Si el token de acceso ha expirado pero el refresh token es válido, renovar
|
||||||
// console.log("Renovando token de acceso...");
|
console.log("Renovando token de acceso...");
|
||||||
try {
|
try {
|
||||||
const res = await resfreshTokenAction({ token: token.refresh_token as string });
|
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) {
|
if (!res || !res.tokens) {
|
||||||
throw new Error('Fallo en la respuesta de la API de refresco.');
|
throw new Error('Fallo en la respuesta de la API de refresco.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log("Old Access Expire:", token.access_expire_in);
|
|
||||||
// console.log("New Access Expire:", res.tokens.access_expire_in);
|
|
||||||
|
|
||||||
// console.log("token:", token.refresh_token);
|
|
||||||
|
|
||||||
|
|
||||||
// Actualizar el token directamente con los nuevos valores
|
// Actualizar el token directamente con los nuevos valores
|
||||||
token.access_token = res.tokens.access_token;
|
token.access_token = res.tokens.access_token;
|
||||||
token.access_expire_in = res.tokens.access_expire_in;
|
token.access_expire_in = res.tokens.access_expire_in;
|
||||||
token.refresh_token = res.tokens.refresh_token;
|
token.refresh_token = res.tokens.refresh_token;
|
||||||
token.refresh_expire_in = res.tokens.refresh_expire_in;
|
token.refresh_expire_in = res.tokens.refresh_expire_in;
|
||||||
|
|
||||||
|
console.log("Token renovado exitosamente.");
|
||||||
return token;
|
return token;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al renovar el token: ", error);
|
console.error(error);
|
||||||
return null; // Fallo al renovar, forzar logout
|
return null; // Fallo al renovar, forzar logout
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -182,9 +165,11 @@ const authConfig: NextAuthConfig = {
|
|||||||
email: token.email as string,
|
email: token.email as string,
|
||||||
role: token.role as Array<{ id: number; rol: string }>,
|
role: token.role as Array<{ id: number; rol: string }>,
|
||||||
};
|
};
|
||||||
|
console.log("Session: Habilitado");
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
} satisfies NextAuthConfig;
|
} satisfies NextAuthConfig;
|
||||||
|
|
||||||
export default authConfig;
|
export default authConfig;
|
||||||
|
|||||||
@@ -31,14 +31,14 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"next": "^15.1.6",
|
"next": "^15.5.9",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.25",
|
||||||
"next-safe-action": "^7.10.2",
|
"next-safe-action": "^7.10.2",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"nextjs-toploader": "^3.7.15",
|
"nextjs-toploader": "^3.7.15",
|
||||||
"nuqs": "^2.3.2",
|
"nuqs": "^2.3.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.3",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.3",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 133 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
@@ -7,6 +7,8 @@
|
|||||||
"add:api": "pnpm add --filter=api",
|
"add:api": "pnpm add --filter=api",
|
||||||
"add:web": "pnpm add --filter=web",
|
"add:web": "pnpm add --filter=web",
|
||||||
"build": "turbo build",
|
"build": "turbo build",
|
||||||
|
"build:api": "pnpm build --filter=api",
|
||||||
|
"build:web": "pnpm build --filter=web",
|
||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"clear:modules": "npx npkill",
|
"clear:modules": "npx npkill",
|
||||||
"commit": "cz",
|
"commit": "cz",
|
||||||
@@ -18,9 +20,7 @@
|
|||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"start": "turbo start",
|
"start": "turbo start",
|
||||||
"test": "turbo test",
|
"test": "turbo test"
|
||||||
"build:api": "pnpm build --filter=api",
|
|
||||||
"build:web": "pnpm build --filter=web"
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
|
|||||||
Reference in New Issue
Block a user