mejoras al formulario de registro organizaciones productivas
This commit is contained in:
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/*
|
||||||
|
|||||||
@@ -49,7 +49,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",
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
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
@@ -71,6 +71,13 @@
|
|||||||
"when": 1764883378610,
|
"when": 1764883378610,
|
||||||
"tag": "0009_eminent_ares",
|
"tag": "0009_eminent_ares",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769097895095,
|
||||||
|
"tag": "0010_dashing_bishop",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,9 +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 { states, municipalities, parishes } from './general';
|
import { municipalities, parishes, states } from './general';
|
||||||
|
|
||||||
|
|
||||||
// Tabla surveys
|
// Tabla surveys
|
||||||
export const surveys = t.pgTable(
|
export const surveys = t.pgTable(
|
||||||
@@ -19,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),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,9 +52,15 @@ export const trainingSurveys = t.pgTable(
|
|||||||
lastname: t.text('lastname').notNull(),
|
lastname: t.text('lastname').notNull(),
|
||||||
visitDate: t.timestamp('visit_date').notNull(),
|
visitDate: t.timestamp('visit_date').notNull(),
|
||||||
// ubicacion
|
// ubicacion
|
||||||
state: t.integer('state').references(() => states.id, { onDelete: 'set null' }),
|
state: t
|
||||||
municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }),
|
.integer('state')
|
||||||
parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }),
|
.references(() => states.id, { onDelete: 'set null' }),
|
||||||
|
municipality: t
|
||||||
|
.integer('municipality')
|
||||||
|
.references(() => municipalities.id, { onDelete: 'set null' }),
|
||||||
|
parish: t
|
||||||
|
.integer('parish')
|
||||||
|
.references(() => parishes.id, { onDelete: 'set null' }),
|
||||||
siturCodeCommune: t.text('situr_code_commune').notNull(),
|
siturCodeCommune: t.text('situr_code_commune').notNull(),
|
||||||
communalCouncil: t.text('communal_council').notNull(),
|
communalCouncil: t.text('communal_council').notNull(),
|
||||||
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
|
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
|
||||||
@@ -67,10 +70,13 @@ export const trainingSurveys = t.pgTable(
|
|||||||
ospRif: t.text('osp_rif').notNull(),
|
ospRif: t.text('osp_rif').notNull(),
|
||||||
ospType: t.text('osp_type').notNull(),
|
ospType: t.text('osp_type').notNull(),
|
||||||
productiveActivity: t.text('productive_activity').notNull(),
|
productiveActivity: t.text('productive_activity').notNull(),
|
||||||
financialRequirementDescription: t.text('financial_requirement_description').notNull(),
|
financialRequirementDescription: t
|
||||||
currentStatus: t.text('current_status').notNull(),
|
.text('financial_requirement_description')
|
||||||
|
.notNull(),
|
||||||
|
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
|
||||||
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
||||||
producerCount: t.integer('producer_count').notNull(),
|
producerCount: t.integer('producer_count').notNull(),
|
||||||
|
productCount: t.integer('product_count').notNull().default(0),
|
||||||
productDescription: t.text('product_description').notNull(),
|
productDescription: t.text('product_description').notNull(),
|
||||||
installedCapacity: t.text('installed_capacity').notNull(),
|
installedCapacity: t.text('installed_capacity').notNull(),
|
||||||
operationalCapacity: t.text('operational_capacity').notNull(),
|
operationalCapacity: t.text('operational_capacity').notNull(),
|
||||||
@@ -88,13 +94,15 @@ export const trainingSurveys = t.pgTable(
|
|||||||
paralysisReason: t.text('paralysis_reason').notNull(),
|
paralysisReason: t.text('paralysis_reason').notNull(),
|
||||||
// fotos
|
// fotos
|
||||||
photo1: t.text('photo1').notNull(),
|
photo1: t.text('photo1').notNull(),
|
||||||
photo2: t.text('photo2').notNull(),
|
photo2: t.text('photo2'),
|
||||||
photo3: t.text('photo3').notNull(),
|
photo3: t.text('photo3'),
|
||||||
...timestamps,
|
...timestamps,
|
||||||
},
|
},
|
||||||
(trainingSurveys) => ({
|
(trainingSurveys) => ({
|
||||||
trainingSurveysIndex: t.index('training_surveys_index_00').on(trainingSurveys.firstname),
|
trainingSurveysIndex: t
|
||||||
})
|
.index('training_surveys_index_00')
|
||||||
|
.on(trainingSurveys.firstname),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const viewSurveys = t.pgView('v_surveys', {
|
export const viewSurveys = t.pgView('v_surveys', {
|
||||||
@@ -103,6 +111,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,140 +1,149 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsInt, IsString, IsDateString, IsOptional } from 'class-validator';
|
import { IsDateString, IsInt, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class CreateTrainingDto {
|
export class CreateTrainingDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
firstname: string;
|
firstname: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
lastname: string;
|
lastname: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
visitDate: string;
|
visitDate: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
productiveActivity: string;
|
productiveActivity: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
financialRequirementDescription: string;
|
financialRequirementDescription: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
state: number;
|
state: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
municipality: number;
|
municipality: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
parish: number;
|
parish: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
siturCodeCommune: string;
|
siturCodeCommune: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
communalCouncil: string;
|
communalCouncil: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
siturCodeCommunalCouncil: string;
|
siturCodeCommunalCouncil: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospName: string;
|
ospName: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospAddress: string;
|
ospAddress: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospRif: string;
|
ospRif: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospType: string;
|
ospType: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
currentStatus: string;
|
currentStatus: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
companyConstitutionYear: number;
|
companyConstitutionYear: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
producerCount: number;
|
producerCount: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsInt()
|
||||||
productDescription: string;
|
@IsOptional()
|
||||||
|
productCount: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
installedCapacity: string;
|
productDescription: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
operationalCapacity: string;
|
installedCapacity: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospResponsibleFullname: string;
|
operationalCapacity: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospResponsibleCedula: string;
|
ospResponsibleFullname: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospResponsibleRif: string;
|
ospResponsibleCedula: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospResponsiblePhone: string;
|
ospResponsibleRif: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ospResponsibleEmail: string;
|
ospResponsiblePhone: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
civilState: string;
|
ospResponsibleEmail: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsString()
|
||||||
familyBurden: number;
|
civilState: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
numberOfChildren: number;
|
familyBurden: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsInt()
|
||||||
generalObservations: string;
|
numberOfChildren: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
photo1: string;
|
generalObservations: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
photo2: string;
|
@IsOptional()
|
||||||
|
photo1?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
photo3: string;
|
@IsOptional()
|
||||||
|
photo2?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
paralysisReason: string;
|
@IsOptional()
|
||||||
|
photo3?: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
paralysisReason: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,114 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
|
import {
|
||||||
import { TrainingService } from './training.service';
|
Body,
|
||||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
Controller,
|
||||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
Delete,
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
UploadedFiles,
|
||||||
|
UseInterceptors,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||||
|
import {
|
||||||
|
ApiConsumes,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiTags,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
import { 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 { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||||
|
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||||
|
import { TrainingService } from './training.service';
|
||||||
|
|
||||||
@ApiTags('training')
|
@ApiTags('training')
|
||||||
@Controller('training')
|
@Controller('training')
|
||||||
export class TrainingController {
|
export class TrainingController {
|
||||||
constructor(private readonly trainingService: TrainingService) { }
|
constructor(private readonly trainingService: TrainingService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Get all training records with pagination and filters' })
|
@ApiOperation({
|
||||||
@ApiResponse({ status: 200, description: 'Return paginated training records.' })
|
summary: 'Get all training records with pagination and filters',
|
||||||
async findAll(@Query() paginationDto: PaginationDto) {
|
})
|
||||||
const result = await this.trainingService.findAll(paginationDto);
|
@ApiResponse({
|
||||||
return {
|
status: 200,
|
||||||
message: 'Training records fetched successfully',
|
description: 'Return paginated training records.',
|
||||||
data: result.data,
|
})
|
||||||
meta: result.meta
|
async findAll(@Query() paginationDto: PaginationDto) {
|
||||||
};
|
const result = await this.trainingService.findAll(paginationDto);
|
||||||
}
|
return {
|
||||||
|
message: 'Training records fetched successfully',
|
||||||
|
data: result.data,
|
||||||
|
meta: result.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Get('statistics')
|
@Get('statistics')
|
||||||
@ApiOperation({ summary: 'Get training statistics' })
|
@ApiOperation({ summary: 'Get training statistics' })
|
||||||
@ApiResponse({ status: 200, description: 'Return training statistics.' })
|
@ApiResponse({ status: 200, description: 'Return training statistics.' })
|
||||||
async getStatistics(@Query() filterDto: TrainingStatisticsFilterDto) {
|
async getStatistics(@Query() filterDto: TrainingStatisticsFilterDto) {
|
||||||
const data = await this.trainingService.getStatistics(filterDto);
|
const data = await this.trainingService.getStatistics(filterDto);
|
||||||
return { message: 'Training statistics fetched successfully', data };
|
return { message: 'Training statistics fetched successfully', data };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'Get a training record by ID' })
|
@ApiOperation({ summary: 'Get a training record by ID' })
|
||||||
@ApiResponse({ status: 200, description: 'Return the training record.' })
|
@ApiResponse({ status: 200, description: 'Return the training record.' })
|
||||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||||
async findOne(@Param('id') id: string) {
|
async findOne(@Param('id') id: string) {
|
||||||
const data = await this.trainingService.findOne(+id);
|
const data = await this.trainingService.findOne(+id);
|
||||||
return { message: 'Training record fetched successfully', data };
|
return { message: 'Training record fetched successfully', data };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new training record' })
|
@UseInterceptors(FilesInterceptor('files', 3))
|
||||||
@ApiResponse({ status: 201, description: 'Training record created successfully.' })
|
@ApiConsumes('multipart/form-data')
|
||||||
async create(@Body() createTrainingDto: CreateTrainingDto) {
|
@ApiOperation({ summary: 'Create a new training record' })
|
||||||
const data = await this.trainingService.create(createTrainingDto);
|
@ApiResponse({
|
||||||
return { message: 'Training record created successfully', data };
|
status: 201,
|
||||||
}
|
description: 'Training record created successfully.',
|
||||||
|
})
|
||||||
|
async create(
|
||||||
|
@Body() createTrainingDto: CreateTrainingDto,
|
||||||
|
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const data = await this.trainingService.create(createTrainingDto, files);
|
||||||
|
return { message: 'Training record created successfully', data };
|
||||||
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@ApiOperation({ summary: 'Update a training record' })
|
@UseInterceptors(FilesInterceptor('files', 3))
|
||||||
@ApiResponse({ status: 200, description: 'Training record updated successfully.' })
|
@ApiConsumes('multipart/form-data')
|
||||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
@ApiOperation({ summary: 'Update a training record' })
|
||||||
async update(@Param('id') id: string, @Body() updateTrainingDto: UpdateTrainingDto) {
|
@ApiResponse({
|
||||||
const data = await this.trainingService.update(+id, updateTrainingDto);
|
status: 200,
|
||||||
return { message: 'Training record updated successfully', data };
|
description: 'Training record updated successfully.',
|
||||||
}
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||||
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() updateTrainingDto: UpdateTrainingDto,
|
||||||
|
@UploadedFiles(ImageProcessingPipe) files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const data = await this.trainingService.update(
|
||||||
|
+id,
|
||||||
|
updateTrainingDto,
|
||||||
|
files,
|
||||||
|
);
|
||||||
|
return { message: 'Training record updated successfully', data };
|
||||||
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ApiOperation({ summary: 'Delete a training record' })
|
@ApiOperation({ summary: 'Delete a training record' })
|
||||||
@ApiResponse({ status: 200, description: 'Training record deleted successfully.' })
|
@ApiResponse({
|
||||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
status: 200,
|
||||||
async remove(@Param('id') id: string) {
|
description: 'Training record deleted successfully.',
|
||||||
return await this.trainingService.remove(+id);
|
})
|
||||||
}
|
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||||
|
async remove(@Param('id') id: string) {
|
||||||
|
return await this.trainingService.remove(+id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,223 +1,324 @@
|
|||||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||||
import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
import { and, eq, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||||
import * as schema from 'src/database/index';
|
import * as schema from 'src/database/index';
|
||||||
import { trainingSurveys } from 'src/database/index';
|
import { states, trainingSurveys } from 'src/database/index';
|
||||||
import { eq, like, or, and, gte, lte, SQL, sql } from 'drizzle-orm';
|
|
||||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
|
||||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
|
||||||
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
|
||||||
import { states } from 'src/database/index';
|
|
||||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
|
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||||
|
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||||
|
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrainingService {
|
export class TrainingService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
|
async findAll(paginationDto?: PaginationDto) {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
search = '',
|
||||||
|
sortBy = 'id',
|
||||||
|
sortOrder = 'asc',
|
||||||
|
} = paginationDto || {};
|
||||||
|
|
||||||
async findAll(paginationDto?: PaginationDto) {
|
const offset = (page - 1) * limit;
|
||||||
const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {};
|
|
||||||
|
|
||||||
const offset = (page - 1) * limit;
|
let searchCondition: SQL<unknown> | undefined;
|
||||||
|
if (search) {
|
||||||
let searchCondition: SQL<unknown> | undefined;
|
searchCondition = or(ilike(trainingSurveys.ospName, `%${search}%`));
|
||||||
if (search) {
|
|
||||||
searchCondition = or(
|
|
||||||
like(trainingSurveys.firstname, `%${search}%`),
|
|
||||||
like(trainingSurveys.lastname, `%${search}%`),
|
|
||||||
like(trainingSurveys.ospName, `%${search}%`),
|
|
||||||
like(trainingSurveys.ospRif, `%${search}%`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderBy = sortOrder === 'asc'
|
|
||||||
? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc`
|
|
||||||
: sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`;
|
|
||||||
|
|
||||||
const totalCountResult = await this.drizzle
|
|
||||||
.select({ count: sql<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 orderBy =
|
||||||
const { startDate, endDate, stateId, municipalityId, parishId, ospType } = filterDto;
|
sortOrder === 'asc'
|
||||||
|
? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc`
|
||||||
|
: sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`;
|
||||||
|
|
||||||
const filters: SQL[] = [];
|
const totalCountResult = await this.drizzle
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(searchCondition);
|
||||||
|
|
||||||
if (startDate) {
|
const totalCount = Number(totalCountResult[0].count);
|
||||||
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
const totalPages = Math.ceil(totalCount / limit);
|
||||||
}
|
|
||||||
|
|
||||||
if (endDate) {
|
const data = await this.drizzle
|
||||||
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
.select()
|
||||||
}
|
.from(trainingSurveys)
|
||||||
|
.where(searchCondition)
|
||||||
|
.orderBy(orderBy)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset);
|
||||||
|
|
||||||
if (stateId) {
|
const meta = {
|
||||||
filters.push(eq(trainingSurveys.state, stateId));
|
page,
|
||||||
}
|
limit,
|
||||||
|
totalCount,
|
||||||
|
totalPages,
|
||||||
|
hasNextPage: page < totalPages,
|
||||||
|
hasPreviousPage: page > 1,
|
||||||
|
nextPage: page < totalPages ? page + 1 : null,
|
||||||
|
previousPage: page > 1 ? page - 1 : null,
|
||||||
|
};
|
||||||
|
|
||||||
if (municipalityId) {
|
return { data, meta };
|
||||||
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (parishId) {
|
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
|
||||||
filters.push(eq(trainingSurveys.parish, parishId));
|
const { startDate, endDate, stateId, municipalityId, parishId, ospType } =
|
||||||
}
|
filterDto;
|
||||||
|
|
||||||
if (ospType) {
|
const filters: SQL[] = [];
|
||||||
filters.push(eq(trainingSurveys.ospType, ospType));
|
|
||||||
}
|
|
||||||
|
|
||||||
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
if (startDate) {
|
||||||
|
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||||
const totalOspsResult = await this.drizzle
|
|
||||||
.select({ count: sql<number>`count(*)` })
|
|
||||||
.from(trainingSurveys)
|
|
||||||
.where(whereCondition);
|
|
||||||
const totalOsps = Number(totalOspsResult[0].count);
|
|
||||||
|
|
||||||
const totalProducersResult = await this.drizzle
|
|
||||||
.select({ sum: sql<number>`sum(${trainingSurveys.producerCount})` })
|
|
||||||
.from(trainingSurveys)
|
|
||||||
.where(whereCondition);
|
|
||||||
const totalProducers = Number(totalProducersResult[0].sum || 0);
|
|
||||||
|
|
||||||
const statusDistribution = await this.drizzle
|
|
||||||
.select({
|
|
||||||
name: trainingSurveys.currentStatus,
|
|
||||||
value: sql<number>`count(*)`
|
|
||||||
})
|
|
||||||
.from(trainingSurveys)
|
|
||||||
.where(whereCondition)
|
|
||||||
.groupBy(trainingSurveys.currentStatus);
|
|
||||||
|
|
||||||
const activityDistribution = await this.drizzle
|
|
||||||
.select({
|
|
||||||
name: trainingSurveys.productiveActivity,
|
|
||||||
value: sql<number>`count(*)`
|
|
||||||
})
|
|
||||||
.from(trainingSurveys)
|
|
||||||
.where(whereCondition)
|
|
||||||
.groupBy(trainingSurveys.productiveActivity);
|
|
||||||
|
|
||||||
const typeDistribution = await this.drizzle
|
|
||||||
.select({
|
|
||||||
name: trainingSurveys.ospType,
|
|
||||||
value: sql<number>`count(*)`
|
|
||||||
})
|
|
||||||
.from(trainingSurveys)
|
|
||||||
.where(whereCondition)
|
|
||||||
.groupBy(trainingSurveys.ospType);
|
|
||||||
|
|
||||||
// New Aggregations
|
|
||||||
const stateDistribution = await this.drizzle
|
|
||||||
.select({
|
|
||||||
name: states.name,
|
|
||||||
value: sql<number>`count(${trainingSurveys.id})`
|
|
||||||
})
|
|
||||||
.from(trainingSurveys)
|
|
||||||
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
|
||||||
.where(whereCondition)
|
|
||||||
.groupBy(states.name);
|
|
||||||
|
|
||||||
const yearDistribution = await this.drizzle
|
|
||||||
.select({
|
|
||||||
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
|
|
||||||
value: sql<number>`count(*)`
|
|
||||||
})
|
|
||||||
.from(trainingSurveys)
|
|
||||||
.where(whereCondition)
|
|
||||||
.groupBy(trainingSurveys.companyConstitutionYear)
|
|
||||||
.orderBy(trainingSurveys.companyConstitutionYear);
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalOsps,
|
|
||||||
totalProducers,
|
|
||||||
statusDistribution: statusDistribution.map(item => ({ ...item, value: Number(item.value) })),
|
|
||||||
activityDistribution: activityDistribution.map(item => ({ ...item, value: Number(item.value) })),
|
|
||||||
typeDistribution: typeDistribution.map(item => ({ ...item, value: Number(item.value) })),
|
|
||||||
stateDistribution: stateDistribution.map(item => ({ ...item, value: Number(item.value) })),
|
|
||||||
yearDistribution: yearDistribution.map(item => ({ ...item, value: Number(item.value) })),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number) {
|
if (endDate) {
|
||||||
const find = await this.drizzle
|
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||||
.select()
|
}
|
||||||
.from(trainingSurveys)
|
|
||||||
.where(eq(trainingSurveys.id, id));
|
|
||||||
|
|
||||||
if (find.length === 0) {
|
if (stateId) {
|
||||||
throw new HttpException('Training record not found', HttpStatus.NOT_FOUND);
|
filters.push(eq(trainingSurveys.state, stateId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (municipalityId) {
|
||||||
|
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parishId) {
|
||||||
|
filters.push(eq(trainingSurveys.parish, parishId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ospType) {
|
||||||
|
filters.push(eq(trainingSurveys.ospType, ospType));
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
||||||
|
|
||||||
|
const totalOspsResult = await this.drizzle
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition);
|
||||||
|
const totalOsps = Number(totalOspsResult[0].count);
|
||||||
|
|
||||||
|
const totalProducersResult = await this.drizzle
|
||||||
|
.select({ sum: sql<number>`sum(${trainingSurveys.producerCount})` })
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition);
|
||||||
|
const totalProducers = Number(totalProducersResult[0].sum || 0);
|
||||||
|
|
||||||
|
const statusDistribution = await this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.currentStatus,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.currentStatus);
|
||||||
|
|
||||||
|
const activityDistribution = await this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.productiveActivity,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.productiveActivity);
|
||||||
|
|
||||||
|
const typeDistribution = await this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.ospType,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.ospType);
|
||||||
|
|
||||||
|
// New Aggregations
|
||||||
|
const stateDistribution = await this.drizzle
|
||||||
|
.select({
|
||||||
|
name: states.name,
|
||||||
|
value: sql<number>`count(${trainingSurveys.id})`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(states.name);
|
||||||
|
|
||||||
|
const yearDistribution = await this.drizzle
|
||||||
|
.select({
|
||||||
|
name: sql<string>`cast(${trainingSurveys.companyConstitutionYear} as text)`,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.companyConstitutionYear)
|
||||||
|
.orderBy(trainingSurveys.companyConstitutionYear);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalOsps,
|
||||||
|
totalProducers,
|
||||||
|
statusDistribution: statusDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
activityDistribution: activityDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
typeDistribution: typeDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
stateDistribution: stateDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
yearDistribution: yearDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: number) {
|
||||||
|
const find = await this.drizzle
|
||||||
|
.select()
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(eq(trainingSurveys.id, id));
|
||||||
|
|
||||||
|
if (find.length === 0) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Training record not found',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return find[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveFiles(files: Express.Multer.File[]): Promise<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)}${path.extname(file.originalname)}`;
|
||||||
|
const filePath = path.join(uploadDir, fileName);
|
||||||
|
fs.writeFileSync(filePath, file.buffer);
|
||||||
|
savedPaths.push(`/assets/training/${fileName}`);
|
||||||
|
}
|
||||||
|
return savedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteFile(assetPath: string) {
|
||||||
|
if (!assetPath) return;
|
||||||
|
// Map /assets/training/filename.webp back to ./uploads/training/filename.webp
|
||||||
|
const relativePath = assetPath.replace('/assets/training/', '');
|
||||||
|
const fullPath = path.join('./uploads/training', relativePath);
|
||||||
|
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(fullPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error deleting file ${fullPath}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
createTrainingDto: CreateTrainingDto,
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const photoPaths = await this.saveFiles(files);
|
||||||
|
|
||||||
|
const [newRecord] = await this.drizzle
|
||||||
|
.insert(trainingSurveys)
|
||||||
|
.values({
|
||||||
|
...createTrainingDto,
|
||||||
|
visitDate: new Date(createTrainingDto.visitDate),
|
||||||
|
photo1: photoPaths[0] || '',
|
||||||
|
photo2: photoPaths[1] || null,
|
||||||
|
photo3: photoPaths[2] || null,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: number,
|
||||||
|
updateTrainingDto: UpdateTrainingDto,
|
||||||
|
files: Express.Multer.File[],
|
||||||
|
) {
|
||||||
|
const currentRecord = await this.findOne(id);
|
||||||
|
|
||||||
|
const photoPaths = await this.saveFiles(files);
|
||||||
|
|
||||||
|
const updateData: any = { ...updateTrainingDto };
|
||||||
|
|
||||||
|
// Handle photo updates/removals
|
||||||
|
const photoFields = ['photo1', 'photo2', 'photo3'] as const;
|
||||||
|
|
||||||
|
// 1. If we have NEW files, they replace any old files or occupy empty slots
|
||||||
|
if (photoPaths.length > 0) {
|
||||||
|
photoPaths.forEach((newPath, idx) => {
|
||||||
|
const fieldName = photoFields[idx];
|
||||||
|
const oldPath = currentRecord[fieldName];
|
||||||
|
if (oldPath && oldPath !== newPath) {
|
||||||
|
this.deleteFile(oldPath);
|
||||||
}
|
}
|
||||||
|
updateData[fieldName] = newPath;
|
||||||
return find[0];
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(createTrainingDto: CreateTrainingDto) {
|
// 2. If the user explicitly cleared a photo field (updateData.photoX === '')
|
||||||
const [newRecord] = await this.drizzle
|
photoFields.forEach((field) => {
|
||||||
.insert(trainingSurveys)
|
if (updateData[field] === '') {
|
||||||
.values({
|
const oldPath = currentRecord[field];
|
||||||
...createTrainingDto,
|
if (oldPath) this.deleteFile(oldPath);
|
||||||
visitDate: new Date(createTrainingDto.visitDate),
|
updateData[field] = null; // Set to null in DB
|
||||||
})
|
}
|
||||||
.returning();
|
});
|
||||||
|
|
||||||
return newRecord;
|
if (updateTrainingDto.visitDate) {
|
||||||
|
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: number, updateTrainingDto: UpdateTrainingDto) {
|
const [updatedRecord] = await this.drizzle
|
||||||
await this.findOne(id);
|
.update(trainingSurveys)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
const updateData: any = { ...updateTrainingDto };
|
return updatedRecord;
|
||||||
if (updateTrainingDto.visitDate) {
|
}
|
||||||
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [updatedRecord] = await this.drizzle
|
async remove(id: number) {
|
||||||
.update(trainingSurveys)
|
const record = await this.findOne(id);
|
||||||
.set(updateData)
|
|
||||||
.where(eq(trainingSurveys.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return updatedRecord;
|
// Delete associated files
|
||||||
}
|
if (record.photo1) this.deleteFile(record.photo1);
|
||||||
|
if (record.photo2) this.deleteFile(record.photo2);
|
||||||
|
if (record.photo3) this.deleteFile(record.photo3);
|
||||||
|
|
||||||
async remove(id: number) {
|
const [deletedRecord] = await this.drizzle
|
||||||
await this.findOne(id);
|
.delete(trainingSurveys)
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
const [deletedRecord] = await this.drizzle
|
return {
|
||||||
.delete(trainingSurveys)
|
message: 'Training record deleted successfully',
|
||||||
.where(eq(trainingSurveys.id, id))
|
data: deletedRecord,
|
||||||
.returning();
|
};
|
||||||
|
}
|
||||||
return { message: 'Training record deleted successfully', data: deletedRecord };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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="flex-1 space-y-4">
|
||||||
|
<CreateTrainingForm
|
||||||
|
defaultValues={training}
|
||||||
|
onSuccess={() => router.push('/dashboard/formulario')}
|
||||||
|
onCancel={() => router.back()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
apps/web/app/dashboard/formulario/nuevo/page.tsx
Normal file
20
apps/web/app/dashboard/formulario/nuevo/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { CreateTrainingForm } from '@/feactures/training/components/form';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function NewTrainingPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer scrollable>
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
<CreateTrainingForm
|
||||||
|
onSuccess={() => router.push('/dashboard/formulario')}
|
||||||
|
onCancel={() => router.back()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,36 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import PageContainer from '@/components/layout/page-container';
|
import PageContainer from '@/components/layout/page-container';
|
||||||
import { CreateTrainingForm } from '@/feactures/training/components/form';
|
import { TrainingHeader } from '@/feactures/training/components/training-header';
|
||||||
|
import TrainingList from '@/feactures/training/components/training-list';
|
||||||
|
import TrainingTableAction from '@/feactures/training/components/training-tables/training-table-action';
|
||||||
|
import { searchParamsCache } from '@repo/shadcn/lib/searchparams';
|
||||||
|
import { SearchParams } from 'nuqs';
|
||||||
|
|
||||||
const Page = () => {
|
export const metadata = {
|
||||||
return (
|
title: 'Registro de OSP',
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
|
||||||
<CreateTrainingForm />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Page;
|
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">
|
||||||
|
<TrainingHeader />
|
||||||
|
<TrainingTableAction />
|
||||||
|
<TrainingList
|
||||||
|
initialPage={page}
|
||||||
|
initialSearch={searchQuery}
|
||||||
|
initialLimit={limit || 10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const AdministrationItems: NavItem[] = [
|
|||||||
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', 'autoridad'], // sumatoria de los roles que si tienen acceso
|
role: ['admin', 'superadmin', 'manager', 'autoridad', 'coordinators'], // sumatoria de los roles que si tienen acceso
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@@ -41,14 +41,14 @@ export const AdministrationItems: NavItem[] = [
|
|||||||
shortcut: ['l', 'l'],
|
shortcut: ['l', 'l'],
|
||||||
url: '/dashboard/administracion/encuestas',
|
url: '/dashboard/administracion/encuestas',
|
||||||
icon: 'login',
|
icon: 'login',
|
||||||
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Registro OSP',
|
title: 'Registro OSP',
|
||||||
shortcut: ['p', 'p'],
|
shortcut: ['p', 'p'],
|
||||||
url: '/dashboard/formulario/',
|
url: '/dashboard/formulario/',
|
||||||
icon: 'notepadText',
|
icon: 'notepadText',
|
||||||
role: ['admin', 'superadmin', 'manager', 'autoridad'],
|
role: ['admin', 'superadmin', 'manager', 'autoridad', 'coordinators'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
url: '#', // Placeholder as there is no direct link for the parent
|
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: [
|
||||||
// {
|
// {
|
||||||
@@ -82,7 +82,7 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
shortcut: ['s', 's'],
|
shortcut: ['s', 's'],
|
||||||
url: '/dashboard/estadisticas/socioproductiva',
|
url: '/dashboard/estadisticas/socioproductiva',
|
||||||
icon: 'blocks',
|
icon: 'blocks',
|
||||||
role: ['admin', 'superadmin', 'autoridad'],
|
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,7 +72,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="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 hidden md:block">
|
<p className="text-balance text-muted-foreground hidden md:block">
|
||||||
Ingresa tus datos
|
Ingresa tus datos
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -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,123 +1,161 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import { safeFetchApi } from '@/lib/fetch.api';
|
import { safeFetchApi } from '@/lib/fetch.api';
|
||||||
import {
|
|
||||||
TrainingSchema,
|
|
||||||
TrainingMutate,
|
|
||||||
trainingApiResponseSchema
|
|
||||||
} from '../schemas/training';
|
|
||||||
import { trainingStatisticsResponseSchema } from '../schemas/statistics';
|
import { trainingStatisticsResponseSchema } from '../schemas/statistics';
|
||||||
|
import {
|
||||||
|
TrainingMutate,
|
||||||
|
TrainingSchema,
|
||||||
|
trainingApiResponseSchema,
|
||||||
|
} from '../schemas/training';
|
||||||
|
|
||||||
export const getTrainingStatisticsAction = async (params: {
|
export const getTrainingStatisticsAction = async (
|
||||||
|
params: {
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
stateId?: number;
|
stateId?: number;
|
||||||
municipalityId?: number;
|
municipalityId?: number;
|
||||||
parishId?: number;
|
parishId?: number;
|
||||||
ospType?: string;
|
ospType?: string;
|
||||||
} = {}) => {
|
} = {},
|
||||||
const searchParams = new URLSearchParams();
|
) => {
|
||||||
if (params.startDate) searchParams.append('startDate', params.startDate);
|
const searchParams = new URLSearchParams();
|
||||||
if (params.endDate) searchParams.append('endDate', params.endDate);
|
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||||
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
|
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||||
if (params.municipalityId) searchParams.append('municipalityId', params.municipalityId.toString());
|
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
|
||||||
if (params.parishId) searchParams.append('parishId', params.parishId.toString());
|
if (params.municipalityId)
|
||||||
if (params.ospType) searchParams.append('ospType', params.ospType);
|
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(
|
const [error, response] = await safeFetchApi(
|
||||||
trainingStatisticsResponseSchema,
|
trainingStatisticsResponseSchema,
|
||||||
`/training/statistics?${searchParams.toString()}`,
|
`/training/statistics?${searchParams.toString()}`,
|
||||||
'GET',
|
'GET',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) throw new Error(error.message);
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const getTrainingAction = async (params: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
search?: string;
|
|
||||||
sortBy?: string;
|
|
||||||
sortOrder?: 'asc' | 'desc';
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
page: (params.page || 1).toString(),
|
|
||||||
limit: (params.limit || 10).toString(),
|
|
||||||
...(params.search && { search: params.search }),
|
|
||||||
...(params.sortBy && { sortBy: params.sortBy }),
|
|
||||||
...(params.sortOrder && { sortOrder: params.sortOrder }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [error, response] = await safeFetchApi(
|
|
||||||
trainingApiResponseSchema,
|
|
||||||
`/training?${searchParams}`,
|
|
||||||
'GET',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) throw new Error(error.message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: response?.data || [],
|
|
||||||
meta: response?.meta || {
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
totalCount: 0,
|
|
||||||
totalPages: 1,
|
|
||||||
hasNextPage: false,
|
|
||||||
hasPreviousPage: false,
|
|
||||||
nextPage: null,
|
|
||||||
previousPage: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createTrainingAction = async (payload: TrainingSchema) => {
|
|
||||||
const { id, ...payloadWithoutId } = payload;
|
|
||||||
|
|
||||||
const [error, data] = await safeFetchApi(
|
|
||||||
TrainingMutate,
|
|
||||||
'/training',
|
|
||||||
'POST',
|
|
||||||
payloadWithoutId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error.message || 'Error al crear el registro');
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTrainingAction = async (payload: TrainingSchema) => {
|
export const getTrainingAction = async (params: {
|
||||||
const { id, ...payloadWithoutId } = payload;
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
}) => {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
page: (params.page || 1).toString(),
|
||||||
|
limit: (params.limit || 10).toString(),
|
||||||
|
...(params.search && { search: params.search }),
|
||||||
|
...(params.sortBy && { sortBy: params.sortBy }),
|
||||||
|
...(params.sortOrder && { sortOrder: params.sortOrder }),
|
||||||
|
});
|
||||||
|
|
||||||
if (!id) throw new Error('ID es requerido para actualizar');
|
const [error, response] = await safeFetchApi(
|
||||||
|
trainingApiResponseSchema,
|
||||||
|
`/training?${searchParams}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
const [error, data] = await safeFetchApi(
|
if (error) throw new Error(error.message);
|
||||||
TrainingMutate,
|
|
||||||
`/training/${id}`,
|
|
||||||
'PATCH',
|
|
||||||
payloadWithoutId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
return {
|
||||||
throw new Error(error.message || 'Error al actualizar el registro');
|
data: response?.data || [],
|
||||||
}
|
meta: response?.meta || {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalCount: 0,
|
||||||
|
totalPages: 1,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
nextPage: null,
|
||||||
|
previousPage: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return data;
|
export const createTrainingAction = async (
|
||||||
|
payload: TrainingSchema | FormData,
|
||||||
|
) => {
|
||||||
|
let payloadToSend = payload;
|
||||||
|
let id: number | undefined;
|
||||||
|
|
||||||
|
if (payload instanceof FormData) {
|
||||||
|
payload.delete('id');
|
||||||
|
payloadToSend = payload;
|
||||||
|
} else {
|
||||||
|
const { id: _, ...rest } = payload;
|
||||||
|
payloadToSend = rest as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
'/training',
|
||||||
|
'POST',
|
||||||
|
payloadToSend,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Error al crear el registro');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTrainingAction = async (
|
||||||
|
payload: TrainingSchema | FormData,
|
||||||
|
) => {
|
||||||
|
let id: string | null = null;
|
||||||
|
let payloadToSend = payload;
|
||||||
|
|
||||||
|
if (payload instanceof FormData) {
|
||||||
|
id = payload.get('id') as string;
|
||||||
|
payload.delete('id');
|
||||||
|
payloadToSend = payload;
|
||||||
|
} else {
|
||||||
|
id = payload.id?.toString() || null;
|
||||||
|
const { id: _, ...rest } = payload;
|
||||||
|
payloadToSend = rest as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) throw new Error('ID es requerido para actualizar');
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
`/training/${id}`,
|
||||||
|
'PATCH',
|
||||||
|
payloadToSend,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Error al actualizar el registro');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTrainingAction = async (id: number) => {
|
export const deleteTrainingAction = async (id: number) => {
|
||||||
const [error] = await safeFetchApi(
|
const [error] = await safeFetchApi(
|
||||||
TrainingMutate,
|
TrainingMutate,
|
||||||
`/training/${id}`,
|
`/training/${id}`,
|
||||||
'DELETE'
|
'DELETE',
|
||||||
)
|
);
|
||||||
|
|
||||||
if (error) throw new Error(error.message || 'Error al eliminar el registro');
|
if (error) throw new Error(error.message || 'Error al eliminar el registro');
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const getTrainingByIdAction = async (id: number) => {
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
`/training/${id}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
|
return response?.data;
|
||||||
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
apps/web/feactures/training/components/training-list.tsx
Normal file
39
apps/web/feactures/training/components/training-list.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
'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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TrainingList({
|
||||||
|
initialPage,
|
||||||
|
initialSearch,
|
||||||
|
initialLimit,
|
||||||
|
}: 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}
|
||||||
|
data={data?.data || []}
|
||||||
|
totalItems={data?.meta.totalCount || 0}
|
||||||
|
pageSizeOptions={[10, 20, 30, 40, 50]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
'use client';
|
||||||
|
import { AlertModal } from '@/components/modal/alert-modal';
|
||||||
|
import { useDeleteTraining } from '@/feactures/training/hooks/use-training';
|
||||||
|
import { TrainingSchema } from '@/feactures/training/schemas/training';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@repo/shadcn/tooltip';
|
||||||
|
import { Edit, Eye, Trash } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { TrainingViewModal } from '../training-view-modal';
|
||||||
|
|
||||||
|
interface CellActionProps {
|
||||||
|
data: TrainingSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CellAction: React.FC<CellActionProps> = ({ data }) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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={() =>
|
||||||
|
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,45 @@
|
|||||||
|
'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';
|
||||||
|
|
||||||
|
export const columns: ColumnDef<TrainingSchema>[] = [
|
||||||
|
{
|
||||||
|
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} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -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" />
|
||||||
|
Nuevo Registro
|
||||||
|
</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,
|
||||||
|
};
|
||||||
|
}
|
||||||
285
apps/web/feactures/training/components/training-view-modal.tsx
Normal file
285
apps/web/feactures/training/components/training-view-modal.tsx
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
'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 { X } from 'lucide-react';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { TrainingSchema } from '../schemas/training';
|
||||||
|
|
||||||
|
interface TrainingViewModalProps {
|
||||||
|
data: TrainingSchema | null;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TrainingViewModal({
|
||||||
|
data,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: TrainingViewModalProps) {
|
||||||
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const DetailItem = ({ label, value }: { label: string; value: any }) => (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">{label}</p>
|
||||||
|
<p className="text-sm font-semibold">{value || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Section = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => (
|
||||||
|
<Card className="mb-4">
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="text-lg">{title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{children}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="sm:max-w-[800px] overflow-y-auto [&>button:last-child]:hidden">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl font-bold">
|
||||||
|
Detalle de la Organización Socioproductiva
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
Resumen detallado de la información de la organización
|
||||||
|
socioproductiva incluyendo ubicación, responsable y registro
|
||||||
|
fotográfico.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-6">
|
||||||
|
{/* 1. Datos de la visita */}
|
||||||
|
<Section title="1. Datos de la visita">
|
||||||
|
<DetailItem
|
||||||
|
label="Nombre del Coordinador"
|
||||||
|
value={data.firstname}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Apellido del Coordinador"
|
||||||
|
value={data.lastname}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Fecha y hora de la visita"
|
||||||
|
value={
|
||||||
|
data.visitDate
|
||||||
|
? new Date(data.visitDate).toLocaleString()
|
||||||
|
: 'N/A'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 2. Datos de la OSP */}
|
||||||
|
<Section title="2. Datos de la OSP">
|
||||||
|
<DetailItem label="Nombre" value={data.ospName} />
|
||||||
|
<DetailItem label="RIF" value={data.ospRif} />
|
||||||
|
<DetailItem label="Tipo" value={data.ospType} />
|
||||||
|
<DetailItem
|
||||||
|
label="Actividad Productiva"
|
||||||
|
value={data.productiveActivity}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Estatus"
|
||||||
|
value={
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
data.currentStatus === 'ACTIVA' ? 'default' : 'secondary'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data.currentStatus}
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Año de Constitución"
|
||||||
|
value={data.companyConstitutionYear}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Cant. Productores"
|
||||||
|
value={data.producerCount}
|
||||||
|
/>
|
||||||
|
<DetailItem label="Cant. Productos" value={data.productCount} />
|
||||||
|
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
||||||
|
<DetailItem
|
||||||
|
label="Descripción del Producto"
|
||||||
|
value={data.productDescription}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DetailItem
|
||||||
|
label="Capacidad Instalada"
|
||||||
|
value={data.installedCapacity}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Capacidad Operativa"
|
||||||
|
value={data.operationalCapacity}
|
||||||
|
/>
|
||||||
|
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
||||||
|
<DetailItem
|
||||||
|
label="Requerimiento Financiero"
|
||||||
|
value={data.financialRequirementDescription}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data.paralysisReason && (
|
||||||
|
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
||||||
|
<DetailItem
|
||||||
|
label="Razones de paralización"
|
||||||
|
value={data.paralysisReason}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 3. Ubicación */}
|
||||||
|
<Section title="3. Detalles de la ubicación">
|
||||||
|
<DetailItem
|
||||||
|
label="Código SITUR Comuna"
|
||||||
|
value={data.siturCodeCommune}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Consejo Comunal"
|
||||||
|
value={data.communalCouncil}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
label="Código SITUR Consejo Comunal"
|
||||||
|
value={data.siturCodeCommunalCouncil}
|
||||||
|
/>
|
||||||
|
<div className="col-span-1 md:col-span-2 lg:col-span-3">
|
||||||
|
<DetailItem label="Dirección OSP" value={data.ospAddress} />
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 4. Responsable */}
|
||||||
|
<Section title="4. Datos del Responsable">
|
||||||
|
<DetailItem
|
||||||
|
label="Nombre Completo"
|
||||||
|
value={data.ospResponsibleFullname}
|
||||||
|
/>
|
||||||
|
<DetailItem label="Cédula" value={data.ospResponsibleCedula} />
|
||||||
|
<DetailItem label="RIF" value={data.ospResponsibleRif} />
|
||||||
|
<DetailItem label="Estado Civil" value={data.civilState} />
|
||||||
|
<DetailItem label="Teléfono" value={data.ospResponsiblePhone} />
|
||||||
|
<DetailItem label="Email" value={data.ospResponsibleEmail} />
|
||||||
|
<DetailItem label="Carga Familiar" value={data.familyBurden} />
|
||||||
|
<DetailItem
|
||||||
|
label="Número de Hijos"
|
||||||
|
value={data.numberOfChildren}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* 5. Observaciones */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="text-lg">
|
||||||
|
5. Observaciones Generales
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm whitespace-pre-wrap">
|
||||||
|
{data.generalObservations || 'Sin observaciones'}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 6. Registro fotográfico */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<CardTitle className="text-lg">
|
||||||
|
6. Registro fotográfico
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{[data.photo1, data.photo2, data.photo3].map(
|
||||||
|
(photo, idx) =>
|
||||||
|
photo && (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="relative aspect-video rounded-md overflow-hidden cursor-pointer hover:opacity-90 transition-opacity bg-muted"
|
||||||
|
onClick={() => setSelectedImage(photo)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`${process.env.NEXT_PUBLIC_API_URL}${photo}`}
|
||||||
|
alt={`Registro ${idx + 1}`}
|
||||||
|
className="object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{![data.photo1, data.photo2, data.photo3].some(Boolean) && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No hay registro fotográfico
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="mt-6">
|
||||||
|
<Button onClick={onClose} variant="outline" className="w-32">
|
||||||
|
Cerrar
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Lightbox */}
|
||||||
|
<Dialog
|
||||||
|
open={!!selectedImage}
|
||||||
|
onOpenChange={() => setSelectedImage(null)}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-[95vw] max-h-[95vh] p-0 overflow-hidden bg-black/90 border-none [&>button:last-child]:hidden">
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>Vista ampliada de la imagen</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Imagen ampliada del registro fotográfico de la organización.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="relative w-full h-full flex items-center justify-center p-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-white/20 z-10"
|
||||||
|
onClick={() => setSelectedImage(null)}
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
{selectedImage && (
|
||||||
|
<img
|
||||||
|
src={`${process.env.NEXT_PUBLIC_API_URL}${selectedImage}`}
|
||||||
|
alt="Expanded view"
|
||||||
|
className="max-w-full max-h-[90vh] object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,29 +1,44 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useSafeQuery } from '@/hooks/use-safe-query';
|
||||||
import { TrainingSchema } from "../schemas/training";
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { createTrainingAction, updateTrainingAction, deleteTrainingAction } from "../actions/training-actions";
|
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() {
|
export function useCreateTraining() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (data: TrainingSchema) => createTrainingAction(data),
|
mutationFn: (data: TrainingSchema) => createTrainingAction(data),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
})
|
});
|
||||||
return mutation
|
return mutation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateTraining() {
|
export function useUpdateTraining() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (data: TrainingSchema) => updateTrainingAction(data),
|
mutationFn: (data: TrainingSchema) => updateTrainingAction(data),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
})
|
});
|
||||||
return mutation;
|
return mutation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDeleteTraining() {
|
export function useDeleteTraining() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: number) => deleteTrainingAction(id),
|
mutationFn: (id: number) => deleteTrainingAction(id),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,107 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const trainingSchema = z.object({
|
export const trainingSchema = z.object({
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
firstname: z.string().min(1, { message: "Nombre es requerido" }),
|
firstname: z.string().min(1, { message: 'Nombre es requerido' }),
|
||||||
lastname: z.string().min(1, { message: "Apellido es requerido" }),
|
lastname: z.string().min(1, { message: 'Apellido es requerido' }),
|
||||||
visitDate: z.string().or(z.date()).transform((val) => new Date(val).toISOString()),
|
visitDate: z
|
||||||
productiveActivity: z.string().min(1, { message: "Actividad productiva es requerida" }),
|
.string()
|
||||||
financialRequirementDescription: z.string().min(1, { message: "Descripción es requerida" }),
|
.min(1, { message: 'Fecha y hora de visita es requerida' }),
|
||||||
siturCodeCommune: z.string().min(1, { message: "Código SITUR Comuna es requerido" }),
|
productiveActivity: z
|
||||||
communalCouncil: z.string().min(1, { message: "Consejo Comunal es requerido" }),
|
.string()
|
||||||
siturCodeCommunalCouncil: z.string().min(1, { message: "Código SITUR Consejo Comunal es requerido" }),
|
.min(1, { message: 'Actividad productiva es requerida' }),
|
||||||
ospName: z.string().min(1, { message: "Nombre de la OSP es requerido" }),
|
financialRequirementDescription: z
|
||||||
ospAddress: z.string().min(1, { message: "Dirección de la OSP es requerida" }),
|
.string()
|
||||||
ospRif: z.string().min(1, { message: "RIF de la OSP es requerido" }),
|
.min(1, { message: 'Descripción es requerida' }),
|
||||||
ospType: z.string().min(1, { message: "Tipo de OSP es requerido" }),
|
siturCodeCommune: z
|
||||||
currentStatus: z.string().min(1, { message: "Estatus actual es requerido" }),
|
.string()
|
||||||
companyConstitutionYear: z.coerce.number().min(1900, { message: "Año inválido" }),
|
.min(1, { message: 'Código SITUR Comuna es requerido' }),
|
||||||
producerCount: z.coerce.number().min(1, { message: "Cantidad de productores requerida" }),
|
communalCouncil: z
|
||||||
productDescription: z.string().min(1, { message: "Descripción del producto es requerida" }),
|
.string()
|
||||||
installedCapacity: z.string().min(1, { message: "Capacidad instalada es requerida" }),
|
.min(1, { message: 'Consejo Comunal es requerido' }),
|
||||||
operationalCapacity: z.string().min(1, { message: "Capacidad operativa es requerida" }),
|
siturCodeCommunalCouncil: z
|
||||||
ospResponsibleFullname: z.string().min(1, { message: "Nombre del responsable es requerido" }),
|
.string()
|
||||||
ospResponsibleCedula: z.string().min(1, { message: "Cédula del responsable es requerida" }),
|
.min(1, { message: 'Código SITUR Consejo Comunal es requerido' }),
|
||||||
ospResponsibleRif: z.string().min(1, { message: "RIF del responsable es requerido" }),
|
ospName: z.string().min(1, { message: 'Nombre de la OSP es requerido' }),
|
||||||
ospResponsiblePhone: z.string().min(1, { message: "Teléfono del responsable es requerido" }),
|
ospAddress: z
|
||||||
civilState: z.string().min(1, { message: "Estado civil es requerido" }),
|
.string()
|
||||||
familyBurden: z.coerce.number().min(0, { message: "Carga familiar requerida" }),
|
.min(1, { message: 'Dirección de la OSP es requerida' }),
|
||||||
numberOfChildren: z.coerce.number().min(0, { message: "Número de hijos requerido" }),
|
ospRif: z.string().min(1, { message: 'RIF de la OSP es requerido' }),
|
||||||
ospResponsibleEmail: z.string().email({ message: "Correo electrónico inválido" }),
|
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
|
||||||
generalObservations: z.string().optional().default(''),
|
currentStatus: z
|
||||||
photo1: z.string().optional().default(''),
|
.string()
|
||||||
photo2: z.string().optional().default(''),
|
.min(1, { message: 'Estatus actual es requerido' })
|
||||||
photo3: z.string().optional().default(''),
|
.default('ACTIVA'),
|
||||||
paralysisReason: z.string().optional().default(''),
|
companyConstitutionYear: z.coerce
|
||||||
state: z.number().optional().nullable(),
|
.number()
|
||||||
municipality: z.number().optional().nullable(),
|
.min(1900, { message: 'Año inválido' }),
|
||||||
parish: z.number().optional().nullable(),
|
producerCount: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'Cantidad de productores requerida' }),
|
||||||
|
productCount: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'Cantidad de productos requerida' })
|
||||||
|
.optional(),
|
||||||
|
productDescription: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Descripción del producto es requerida' }),
|
||||||
|
installedCapacity: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Capacidad instalada es requerida' }),
|
||||||
|
operationalCapacity: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Capacidad operativa es requerida' }),
|
||||||
|
ospResponsibleFullname: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Nombre del responsable es requerido' }),
|
||||||
|
ospResponsibleCedula: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Cédula del responsable es requerida' }),
|
||||||
|
ospResponsibleRif: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'RIF del responsable es requerido' }),
|
||||||
|
ospResponsiblePhone: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Teléfono del responsable es requerido' }),
|
||||||
|
civilState: z.string().min(1, { message: 'Estado civil es requerido' }),
|
||||||
|
familyBurden: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'Carga familiar requerida' }),
|
||||||
|
numberOfChildren: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, { message: 'Número de hijos requerido' }),
|
||||||
|
ospResponsibleEmail: z
|
||||||
|
.string()
|
||||||
|
.email({ message: 'Correo electrónico inválido' }),
|
||||||
|
generalObservations: z.string().optional().default(''),
|
||||||
|
photo1: z.string().optional().nullable(),
|
||||||
|
photo2: z.string().optional().nullable(),
|
||||||
|
photo3: z.string().optional().nullable(),
|
||||||
|
files: z.any().optional(),
|
||||||
|
paralysisReason: z.string().optional().default(''),
|
||||||
|
state: z.number().optional().nullable(),
|
||||||
|
municipality: z.number().optional().nullable(),
|
||||||
|
parish: z.number().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TrainingSchema = z.infer<typeof trainingSchema>;
|
export type TrainingSchema = z.infer<typeof trainingSchema>;
|
||||||
|
|
||||||
export const trainingApiResponseSchema = z.object({
|
export const trainingApiResponseSchema = z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
data: z.array(trainingSchema),
|
data: z.array(trainingSchema),
|
||||||
meta: z.object({
|
meta: z.object({
|
||||||
page: z.number(),
|
page: z.number(),
|
||||||
limit: z.number(),
|
limit: z.number(),
|
||||||
totalCount: z.number(),
|
totalCount: z.number(),
|
||||||
totalPages: z.number(),
|
totalPages: z.number(),
|
||||||
hasNextPage: z.boolean(),
|
hasNextPage: z.boolean(),
|
||||||
hasPreviousPage: z.boolean(),
|
hasPreviousPage: z.boolean(),
|
||||||
nextPage: z.number().nullable(),
|
nextPage: z.number().nullable(),
|
||||||
previousPage: z.number().nullable(),
|
previousPage: z.number().nullable(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TrainingMutate = z.object({
|
export const TrainingMutate = z.object({
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
data: trainingSchema,
|
data: trainingSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
Reference in New Issue
Block a user