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)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Uploads
|
||||
/uploads/training/*
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
"pg": "8.13.3",
|
||||
"pino-pretty": "13.0.0",
|
||||
"reflect-metadata": "0.2.0",
|
||||
"rxjs": "7.8.1"
|
||||
"rxjs": "7.8.1",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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,
|
||||
"tag": "0009_eminent_ares",
|
||||
"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 { eq, lt, gte, ne, sql } from 'drizzle-orm';
|
||||
import { timestamps } from '../timestamps';
|
||||
import { users } from './auth';
|
||||
import { states, municipalities, parishes } from './general';
|
||||
|
||||
import { municipalities, parishes, states } from './general';
|
||||
|
||||
// Tabla surveys
|
||||
export const surveys = t.pgTable(
|
||||
@@ -19,9 +18,7 @@ export const surveys = t.pgTable(
|
||||
...timestamps,
|
||||
},
|
||||
(surveys) => ({
|
||||
surveysIndex: t
|
||||
.index('surveys_index_00')
|
||||
.on(surveys.title),
|
||||
surveysIndex: t.index('surveys_index_00').on(surveys.title),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -55,9 +52,15 @@ export const trainingSurveys = t.pgTable(
|
||||
lastname: t.text('lastname').notNull(),
|
||||
visitDate: t.timestamp('visit_date').notNull(),
|
||||
// ubicacion
|
||||
state: t.integer('state').references(() => states.id, { onDelete: 'set null' }),
|
||||
municipality: t.integer('municipality').references(() => municipalities.id, { onDelete: 'set null' }),
|
||||
parish: t.integer('parish').references(() => parishes.id, { onDelete: 'set null' }),
|
||||
state: t
|
||||
.integer('state')
|
||||
.references(() => states.id, { onDelete: 'set null' }),
|
||||
municipality: t
|
||||
.integer('municipality')
|
||||
.references(() => municipalities.id, { onDelete: 'set null' }),
|
||||
parish: t
|
||||
.integer('parish')
|
||||
.references(() => parishes.id, { onDelete: 'set null' }),
|
||||
siturCodeCommune: t.text('situr_code_commune').notNull(),
|
||||
communalCouncil: t.text('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(),
|
||||
ospType: t.text('osp_type').notNull(),
|
||||
productiveActivity: t.text('productive_activity').notNull(),
|
||||
financialRequirementDescription: t.text('financial_requirement_description').notNull(),
|
||||
currentStatus: t.text('current_status').notNull(),
|
||||
financialRequirementDescription: t
|
||||
.text('financial_requirement_description')
|
||||
.notNull(),
|
||||
currentStatus: t.text('current_status').notNull().default('ACTIVA'),
|
||||
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
||||
producerCount: t.integer('producer_count').notNull(),
|
||||
productCount: t.integer('product_count').notNull().default(0),
|
||||
productDescription: t.text('product_description').notNull(),
|
||||
installedCapacity: t.text('installed_capacity').notNull(),
|
||||
operationalCapacity: t.text('operational_capacity').notNull(),
|
||||
@@ -88,13 +94,15 @@ export const trainingSurveys = t.pgTable(
|
||||
paralysisReason: t.text('paralysis_reason').notNull(),
|
||||
// fotos
|
||||
photo1: t.text('photo1').notNull(),
|
||||
photo2: t.text('photo2').notNull(),
|
||||
photo3: t.text('photo3').notNull(),
|
||||
photo2: t.text('photo2'),
|
||||
photo3: t.text('photo3'),
|
||||
...timestamps,
|
||||
},
|
||||
(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', {
|
||||
@@ -103,6 +111,7 @@ export const viewSurveys = t.pgView('v_surveys', {
|
||||
description: t.text('description'),
|
||||
created_at: t.timestamp('created_at'),
|
||||
closingDate: t.date('closing_date'),
|
||||
targetAudience: t.varchar('target_audience')
|
||||
}).as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
|
||||
where published = true`);
|
||||
targetAudience: t.varchar('target_audience'),
|
||||
})
|
||||
.as(sql`select id as survey_id, title, description, created_at, closing_date, target_audience from surveys
|
||||
where published = true`);
|
||||
|
||||
@@ -1,140 +1,149 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsInt, IsString, IsDateString, IsOptional } from 'class-validator';
|
||||
import { IsDateString, IsInt, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateTrainingDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
firstname: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
firstname: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
lastname: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
lastname: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDateString()
|
||||
visitDate: string;
|
||||
@ApiProperty()
|
||||
@IsDateString()
|
||||
visitDate: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
productiveActivity: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
productiveActivity: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
financialRequirementDescription: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
financialRequirementDescription: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
state: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
state: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
municipality: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
municipality: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
parish: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
parish: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
siturCodeCommune: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
siturCodeCommune: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communalCouncil: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
communalCouncil: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
siturCodeCommunalCouncil: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
siturCodeCommunalCouncil: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospName: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospAddress: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospAddress: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospRif: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospRif: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospType: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospType: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currentStatus: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
currentStatus: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
companyConstitutionYear: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
companyConstitutionYear: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
producerCount: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
producerCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
productDescription: string;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
productCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
installedCapacity: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
productDescription: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
operationalCapacity: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
installedCapacity: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleFullname: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
operationalCapacity: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleCedula: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleFullname: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleRif: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleCedula: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsiblePhone: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleRif: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleEmail: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsiblePhone: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
civilState: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ospResponsibleEmail: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
familyBurden: number;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
civilState: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
numberOfChildren: number;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
familyBurden: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
generalObservations: string;
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
numberOfChildren: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
photo1: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
generalObservations: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
photo2: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo1?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
photo3: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
photo2?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
paralysisReason: string;
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@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 { TrainingService } from './training.service';
|
||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
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 { ImageProcessingPipe } from '../../common/pipes/image-processing.pipe';
|
||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||
import { TrainingService } from './training.service';
|
||||
|
||||
@ApiTags('training')
|
||||
@Controller('training')
|
||||
export class TrainingController {
|
||||
constructor(private readonly trainingService: TrainingService) { }
|
||||
constructor(private readonly trainingService: TrainingService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all training records with pagination and filters' })
|
||||
@ApiResponse({ status: 200, description: 'Return paginated training records.' })
|
||||
async findAll(@Query() paginationDto: PaginationDto) {
|
||||
const result = await this.trainingService.findAll(paginationDto);
|
||||
return {
|
||||
message: 'Training records fetched successfully',
|
||||
data: result.data,
|
||||
meta: result.meta
|
||||
};
|
||||
}
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Get all training records with pagination and filters',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Return paginated training records.',
|
||||
})
|
||||
async findAll(@Query() paginationDto: PaginationDto) {
|
||||
const result = await this.trainingService.findAll(paginationDto);
|
||||
return {
|
||||
message: 'Training records fetched successfully',
|
||||
data: result.data,
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('statistics')
|
||||
@ApiOperation({ summary: 'Get training statistics' })
|
||||
@ApiResponse({ status: 200, description: 'Return training statistics.' })
|
||||
async getStatistics(@Query() filterDto: TrainingStatisticsFilterDto) {
|
||||
const data = await this.trainingService.getStatistics(filterDto);
|
||||
return { message: 'Training statistics fetched successfully', data };
|
||||
}
|
||||
@Get('statistics')
|
||||
@ApiOperation({ summary: 'Get training statistics' })
|
||||
@ApiResponse({ status: 200, description: 'Return training statistics.' })
|
||||
async getStatistics(@Query() filterDto: TrainingStatisticsFilterDto) {
|
||||
const data = await this.trainingService.getStatistics(filterDto);
|
||||
return { message: 'Training statistics fetched successfully', data };
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a training record by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Return the training record.' })
|
||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
const data = await this.trainingService.findOne(+id);
|
||||
return { message: 'Training record fetched successfully', data };
|
||||
}
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a training record by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Return the training record.' })
|
||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
const data = await this.trainingService.findOne(+id);
|
||||
return { message: 'Training record fetched successfully', data };
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new training record' })
|
||||
@ApiResponse({ status: 201, description: 'Training record created successfully.' })
|
||||
async create(@Body() createTrainingDto: CreateTrainingDto) {
|
||||
const data = await this.trainingService.create(createTrainingDto);
|
||||
return { message: 'Training record created successfully', data };
|
||||
}
|
||||
@Post()
|
||||
@UseInterceptors(FilesInterceptor('files', 3))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Create a new training record' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Training record created successfully.',
|
||||
})
|
||||
async create(
|
||||
@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')
|
||||
@ApiOperation({ summary: 'Update a training record' })
|
||||
@ApiResponse({ status: 200, description: 'Training record updated successfully.' })
|
||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||
async update(@Param('id') id: string, @Body() updateTrainingDto: UpdateTrainingDto) {
|
||||
const data = await this.trainingService.update(+id, updateTrainingDto);
|
||||
return { message: 'Training record updated successfully', data };
|
||||
}
|
||||
@Patch(':id')
|
||||
@UseInterceptors(FilesInterceptor('files', 3))
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Update a training record' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Training record updated successfully.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||
async update(
|
||||
@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')
|
||||
@ApiOperation({ summary: 'Delete a training record' })
|
||||
@ApiResponse({ status: 200, description: 'Training record deleted successfully.' })
|
||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||
async remove(@Param('id') id: string) {
|
||||
return await this.trainingService.remove(+id);
|
||||
}
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete a training record' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Training record deleted successfully.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||
async remove(@Param('id') id: string) {
|
||||
return await this.trainingService.remove(+id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,223 +1,324 @@
|
||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||
import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, eq, gte, ilike, lte, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import * 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 { 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 { states, trainingSurveys } from 'src/database/index';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TrainingService {
|
||||
constructor(
|
||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||
) { }
|
||||
constructor(
|
||||
@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 { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {};
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
let searchCondition: SQL<unknown> | undefined;
|
||||
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 };
|
||||
let searchCondition: SQL<unknown> | undefined;
|
||||
if (search) {
|
||||
searchCondition = or(ilike(trainingSurveys.ospName, `%${search}%`));
|
||||
}
|
||||
|
||||
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
|
||||
const { startDate, endDate, stateId, municipalityId, parishId, ospType } = filterDto;
|
||||
const orderBy =
|
||||
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) {
|
||||
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||
}
|
||||
const totalCount = Number(totalCountResult[0].count);
|
||||
const totalPages = Math.ceil(totalCount / limit);
|
||||
|
||||
if (endDate) {
|
||||
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||
}
|
||||
const data = await this.drizzle
|
||||
.select()
|
||||
.from(trainingSurveys)
|
||||
.where(searchCondition)
|
||||
.orderBy(orderBy)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
if (stateId) {
|
||||
filters.push(eq(trainingSurveys.state, stateId));
|
||||
}
|
||||
const meta = {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPreviousPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
previousPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
|
||||
if (municipalityId) {
|
||||
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||
}
|
||||
return { data, meta };
|
||||
}
|
||||
|
||||
if (parishId) {
|
||||
filters.push(eq(trainingSurveys.parish, parishId));
|
||||
}
|
||||
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
|
||||
const { startDate, endDate, stateId, municipalityId, parishId, ospType } =
|
||||
filterDto;
|
||||
|
||||
if (ospType) {
|
||||
filters.push(eq(trainingSurveys.ospType, ospType));
|
||||
}
|
||||
const filters: SQL[] = [];
|
||||
|
||||
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) })),
|
||||
};
|
||||
if (startDate) {
|
||||
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const find = await this.drizzle
|
||||
.select()
|
||||
.from(trainingSurveys)
|
||||
.where(eq(trainingSurveys.id, id));
|
||||
if (endDate) {
|
||||
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||
}
|
||||
|
||||
if (find.length === 0) {
|
||||
throw new HttpException('Training record not found', HttpStatus.NOT_FOUND);
|
||||
if (stateId) {
|
||||
filters.push(eq(trainingSurveys.state, stateId));
|
||||
}
|
||||
|
||||
if (municipalityId) {
|
||||
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||
}
|
||||
|
||||
if (parishId) {
|
||||
filters.push(eq(trainingSurveys.parish, parishId));
|
||||
}
|
||||
|
||||
if (ospType) {
|
||||
filters.push(eq(trainingSurveys.ospType, ospType));
|
||||
}
|
||||
|
||||
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return find[0];
|
||||
updateData[fieldName] = newPath;
|
||||
});
|
||||
}
|
||||
|
||||
async create(createTrainingDto: CreateTrainingDto) {
|
||||
const [newRecord] = await this.drizzle
|
||||
.insert(trainingSurveys)
|
||||
.values({
|
||||
...createTrainingDto,
|
||||
visitDate: new Date(createTrainingDto.visitDate),
|
||||
})
|
||||
.returning();
|
||||
// 2. If the user explicitly cleared a photo field (updateData.photoX === '')
|
||||
photoFields.forEach((field) => {
|
||||
if (updateData[field] === '') {
|
||||
const oldPath = currentRecord[field];
|
||||
if (oldPath) this.deleteFile(oldPath);
|
||||
updateData[field] = null; // Set to null in DB
|
||||
}
|
||||
});
|
||||
|
||||
return newRecord;
|
||||
if (updateTrainingDto.visitDate) {
|
||||
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||
}
|
||||
|
||||
async update(id: number, updateTrainingDto: UpdateTrainingDto) {
|
||||
await this.findOne(id);
|
||||
const [updatedRecord] = await this.drizzle
|
||||
.update(trainingSurveys)
|
||||
.set(updateData)
|
||||
.where(eq(trainingSurveys.id, id))
|
||||
.returning();
|
||||
|
||||
const updateData: any = { ...updateTrainingDto };
|
||||
if (updateTrainingDto.visitDate) {
|
||||
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||
}
|
||||
return updatedRecord;
|
||||
}
|
||||
|
||||
const [updatedRecord] = await this.drizzle
|
||||
.update(trainingSurveys)
|
||||
.set(updateData)
|
||||
.where(eq(trainingSurveys.id, id))
|
||||
.returning();
|
||||
async remove(id: number) {
|
||||
const record = await this.findOne(id);
|
||||
|
||||
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) {
|
||||
await this.findOne(id);
|
||||
const [deletedRecord] = await this.drizzle
|
||||
.delete(trainingSurveys)
|
||||
.where(eq(trainingSurveys.id, id))
|
||||
.returning();
|
||||
|
||||
const [deletedRecord] = await this.drizzle
|
||||
.delete(trainingSurveys)
|
||||
.where(eq(trainingSurveys.id, id))
|
||||
.returning();
|
||||
|
||||
return { message: 'Training record deleted successfully', data: deletedRecord };
|
||||
}
|
||||
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 { 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 = () => {
|
||||
return (
|
||||
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<CreateTrainingForm />
|
||||
</div>
|
||||
);
|
||||
export const metadata = {
|
||||
title: 'Registro de OSP',
|
||||
};
|
||||
|
||||
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 = {
|
||||
name: 'Sistema para Productores',
|
||||
name: 'Sistema de Productores',
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: 'FONDEMI',
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ export const AdministrationItems: NavItem[] = [
|
||||
url: '#', // Placeholder as there is no direct link for the parent
|
||||
icon: 'settings2',
|
||||
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: [
|
||||
{
|
||||
@@ -41,14 +41,14 @@ export const AdministrationItems: NavItem[] = [
|
||||
shortcut: ['l', 'l'],
|
||||
url: '/dashboard/administracion/encuestas',
|
||||
icon: 'login',
|
||||
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||
role: ['admin', 'superadmin', 'autoridad'],
|
||||
},
|
||||
{
|
||||
title: 'Registro OSP',
|
||||
shortcut: ['p', 'p'],
|
||||
url: '/dashboard/formulario/',
|
||||
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
|
||||
icon: 'chartColumn',
|
||||
isActive: true,
|
||||
role: ['admin', 'superadmin', 'autoridad'],
|
||||
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||
|
||||
items: [
|
||||
// {
|
||||
@@ -82,7 +82,7 @@ export const StatisticsItems: NavItem[] = [
|
||||
shortcut: ['s', 's'],
|
||||
url: '/dashboard/estadisticas/socioproductiva',
|
||||
icon: 'blocks',
|
||||
role: ['admin', 'superadmin', 'autoridad'],
|
||||
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function UserAuthForm() {
|
||||
<form className="flex-none p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<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">
|
||||
Ingresa tus datos
|
||||
</p>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function UserAuthForm() {
|
||||
<form className="flex-none p-6 md:p-8" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<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">
|
||||
Ingresa tus datos
|
||||
</p>
|
||||
@@ -303,7 +303,7 @@ export default function UserAuthForm() {
|
||||
<FormMessage className="text-red-500">{error}</FormMessage>
|
||||
)}{' '}
|
||||
<Button type="submit" className="w-full">
|
||||
Registrarce
|
||||
Registrarse
|
||||
</Button>
|
||||
<div className="text-center text-sm">
|
||||
¿Ya tienes una cuenta?{" "}
|
||||
|
||||
@@ -1,123 +1,161 @@
|
||||
'use server';
|
||||
import { safeFetchApi } from '@/lib/fetch.api';
|
||||
import {
|
||||
TrainingSchema,
|
||||
TrainingMutate,
|
||||
trainingApiResponseSchema
|
||||
} from '../schemas/training';
|
||||
import { trainingStatisticsResponseSchema } from '../schemas/statistics';
|
||||
import {
|
||||
TrainingMutate,
|
||||
TrainingSchema,
|
||||
trainingApiResponseSchema,
|
||||
} from '../schemas/training';
|
||||
|
||||
export const getTrainingStatisticsAction = async (params: {
|
||||
export const getTrainingStatisticsAction = async (
|
||||
params: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
stateId?: number;
|
||||
municipalityId?: number;
|
||||
parishId?: number;
|
||||
ospType?: string;
|
||||
} = {}) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
|
||||
if (params.municipalityId) searchParams.append('municipalityId', params.municipalityId.toString());
|
||||
if (params.parishId) searchParams.append('parishId', params.parishId.toString());
|
||||
if (params.ospType) searchParams.append('ospType', params.ospType);
|
||||
} = {},
|
||||
) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
|
||||
if (params.municipalityId)
|
||||
searchParams.append('municipalityId', params.municipalityId.toString());
|
||||
if (params.parishId)
|
||||
searchParams.append('parishId', params.parishId.toString());
|
||||
if (params.ospType) searchParams.append('ospType', params.ospType);
|
||||
|
||||
const [error, response] = await safeFetchApi(
|
||||
trainingStatisticsResponseSchema,
|
||||
`/training/statistics?${searchParams.toString()}`,
|
||||
'GET',
|
||||
);
|
||||
const [error, response] = await safeFetchApi(
|
||||
trainingStatisticsResponseSchema,
|
||||
`/training/statistics?${searchParams.toString()}`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
if (error) throw new Error(error.message);
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
return response?.data;
|
||||
}
|
||||
|
||||
|
||||
export const getTrainingAction = async (params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}) => {
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
page: (params.page || 1).toString(),
|
||||
limit: (params.limit || 10).toString(),
|
||||
...(params.search && { search: params.search }),
|
||||
...(params.sortBy && { sortBy: params.sortBy }),
|
||||
...(params.sortOrder && { sortOrder: params.sortOrder }),
|
||||
});
|
||||
|
||||
const [error, response] = await safeFetchApi(
|
||||
trainingApiResponseSchema,
|
||||
`/training?${searchParams}`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
return {
|
||||
data: response?.data || [],
|
||||
meta: response?.meta || {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
totalCount: 0,
|
||||
totalPages: 1,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
nextPage: null,
|
||||
previousPage: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const createTrainingAction = async (payload: TrainingSchema) => {
|
||||
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;
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
export const updateTrainingAction = async (payload: TrainingSchema) => {
|
||||
const { id, ...payloadWithoutId } = payload;
|
||||
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 }),
|
||||
});
|
||||
|
||||
if (!id) throw new Error('ID es requerido para actualizar');
|
||||
const [error, response] = await safeFetchApi(
|
||||
trainingApiResponseSchema,
|
||||
`/training?${searchParams}`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
const [error, data] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
`/training/${id}`,
|
||||
'PATCH',
|
||||
payloadWithoutId,
|
||||
);
|
||||
if (error) throw new Error(error.message);
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message || 'Error al actualizar el registro');
|
||||
}
|
||||
return {
|
||||
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) => {
|
||||
const [error] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
`/training/${id}`,
|
||||
'DELETE'
|
||||
)
|
||||
const [error] = await safeFetchApi(
|
||||
TrainingMutate,
|
||||
`/training/${id}`,
|
||||
'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 { TrainingSchema } from "../schemas/training";
|
||||
import { createTrainingAction, updateTrainingAction, deleteTrainingAction } from "../actions/training-actions";
|
||||
import { useSafeQuery } from '@/hooks/use-safe-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
createTrainingAction,
|
||||
deleteTrainingAction,
|
||||
getTrainingAction,
|
||||
getTrainingByIdAction,
|
||||
updateTrainingAction,
|
||||
} from '../actions/training-actions';
|
||||
import { TrainingSchema } from '../schemas/training';
|
||||
|
||||
export function useTrainingQuery(params = {}) {
|
||||
return useSafeQuery(['training', params], () => getTrainingAction(params));
|
||||
}
|
||||
|
||||
export function useTrainingByIdQuery(id: number) {
|
||||
return useSafeQuery(['training', id], () => getTrainingByIdAction(id));
|
||||
}
|
||||
|
||||
export function useCreateTraining() {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: TrainingSchema) => createTrainingAction(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||
})
|
||||
return mutation
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: TrainingSchema) => createTrainingAction(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||
});
|
||||
return mutation;
|
||||
}
|
||||
|
||||
export function useUpdateTraining() {
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: TrainingSchema) => updateTrainingAction(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||
})
|
||||
return mutation;
|
||||
const queryClient = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: TrainingSchema) => updateTrainingAction(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||
});
|
||||
return mutation;
|
||||
}
|
||||
|
||||
export function useDeleteTraining() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteTrainingAction(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||
})
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => deleteTrainingAction(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,61 +1,107 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const trainingSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
firstname: z.string().min(1, { message: "Nombre es requerido" }),
|
||||
lastname: z.string().min(1, { message: "Apellido es requerido" }),
|
||||
visitDate: z.string().or(z.date()).transform((val) => new Date(val).toISOString()),
|
||||
productiveActivity: z.string().min(1, { message: "Actividad productiva es requerida" }),
|
||||
financialRequirementDescription: z.string().min(1, { message: "Descripción es requerida" }),
|
||||
siturCodeCommune: z.string().min(1, { message: "Código SITUR Comuna es requerido" }),
|
||||
communalCouncil: z.string().min(1, { message: "Consejo Comunal es requerido" }),
|
||||
siturCodeCommunalCouncil: z.string().min(1, { message: "Código SITUR Consejo Comunal es requerido" }),
|
||||
ospName: z.string().min(1, { message: "Nombre de la OSP es requerido" }),
|
||||
ospAddress: z.string().min(1, { message: "Dirección de la OSP es requerida" }),
|
||||
ospRif: z.string().min(1, { message: "RIF de la OSP es requerido" }),
|
||||
ospType: z.string().min(1, { message: "Tipo de OSP es requerido" }),
|
||||
currentStatus: z.string().min(1, { message: "Estatus actual es requerido" }),
|
||||
companyConstitutionYear: z.coerce.number().min(1900, { message: "Año inválido" }),
|
||||
producerCount: z.coerce.number().min(1, { message: "Cantidad de productores requerida" }),
|
||||
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().default(''),
|
||||
photo2: z.string().optional().default(''),
|
||||
photo3: z.string().optional().default(''),
|
||||
paralysisReason: z.string().optional().default(''),
|
||||
state: z.number().optional().nullable(),
|
||||
municipality: z.number().optional().nullable(),
|
||||
parish: z.number().optional().nullable(),
|
||||
id: z.number().optional(),
|
||||
firstname: z.string().min(1, { message: 'Nombre es requerido' }),
|
||||
lastname: z.string().min(1, { message: 'Apellido es requerido' }),
|
||||
visitDate: z
|
||||
.string()
|
||||
.min(1, { message: 'Fecha y hora de visita es requerida' }),
|
||||
productiveActivity: z
|
||||
.string()
|
||||
.min(1, { message: 'Actividad productiva es requerida' }),
|
||||
financialRequirementDescription: z
|
||||
.string()
|
||||
.min(1, { message: 'Descripción es requerida' }),
|
||||
siturCodeCommune: z
|
||||
.string()
|
||||
.min(1, { message: 'Código SITUR Comuna es requerido' }),
|
||||
communalCouncil: z
|
||||
.string()
|
||||
.min(1, { message: 'Consejo Comunal es requerido' }),
|
||||
siturCodeCommunalCouncil: z
|
||||
.string()
|
||||
.min(1, { message: 'Código SITUR Consejo Comunal es requerido' }),
|
||||
ospName: z.string().min(1, { message: 'Nombre de la OSP es requerido' }),
|
||||
ospAddress: z
|
||||
.string()
|
||||
.min(1, { message: 'Dirección de la OSP es requerida' }),
|
||||
ospRif: z.string().min(1, { message: 'RIF de la OSP es requerido' }),
|
||||
ospType: z.string().min(1, { message: 'Tipo de OSP es requerido' }),
|
||||
currentStatus: z
|
||||
.string()
|
||||
.min(1, { message: 'Estatus actual es requerido' })
|
||||
.default('ACTIVA'),
|
||||
companyConstitutionYear: z.coerce
|
||||
.number()
|
||||
.min(1900, { message: 'Año inválido' }),
|
||||
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 const trainingApiResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
data: z.array(trainingSchema),
|
||||
meta: z.object({
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
totalCount: z.number(),
|
||||
totalPages: z.number(),
|
||||
hasNextPage: z.boolean(),
|
||||
hasPreviousPage: z.boolean(),
|
||||
nextPage: z.number().nullable(),
|
||||
previousPage: z.number().nullable(),
|
||||
}),
|
||||
message: z.string(),
|
||||
data: z.array(trainingSchema),
|
||||
meta: z.object({
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
totalCount: z.number(),
|
||||
totalPages: z.number(),
|
||||
hasNextPage: z.boolean(),
|
||||
hasPreviousPage: z.boolean(),
|
||||
nextPage: z.number().nullable(),
|
||||
previousPage: z.number().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const TrainingMutate = z.object({
|
||||
message: z.string(),
|
||||
data: trainingSchema,
|
||||
message: z.string(),
|
||||
data: trainingSchema,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@repo/shadcn/select';
|
||||
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';
|
||||
|
||||
const ROLES = {
|
||||
@@ -29,8 +29,9 @@ const ROLES = {
|
||||
4: 'Gerente',
|
||||
5: 'Usuario',
|
||||
6: 'Productor',
|
||||
7: 'Organización'
|
||||
}
|
||||
7: 'Organización',
|
||||
8: 'Coordinadores',
|
||||
};
|
||||
|
||||
interface CreateUserFormProps {
|
||||
onSuccess?: () => void;
|
||||
@@ -60,7 +61,7 @@ export function CreateUserForm({
|
||||
id: defaultValues?.id,
|
||||
phone: defaultValues?.phone || '',
|
||||
role: undefined,
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm<CreateUser>({
|
||||
resolver: zodResolver(createUser),
|
||||
@@ -69,8 +70,6 @@ export function CreateUserForm({
|
||||
});
|
||||
|
||||
const onSubmit = async (formData: CreateUser) => {
|
||||
console.log(formData);
|
||||
|
||||
saveAccountingAccounts(formData, {
|
||||
onSuccess: () => {
|
||||
form.reset();
|
||||
@@ -143,7 +142,7 @@ export function CreateUserForm({
|
||||
<FormItem>
|
||||
<FormLabel>Teléfono</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value?.toString() ?? ''}/>
|
||||
<Input {...field} value={field.value?.toString() ?? ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -157,7 +156,7 @@ export function CreateUserForm({
|
||||
<FormItem>
|
||||
<FormLabel>Contraseña</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field}/>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -166,12 +165,12 @@ export function CreateUserForm({
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='confirmPassword'
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirmar Contraseña</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field}/>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -184,7 +183,9 @@ export function CreateUserForm({
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Rol</FormLabel>
|
||||
<Select onValueChange={(value) => field.onChange(Number(value))}>
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(Number(value))}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Selecciona un rol" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'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 { Button } from '@repo/shadcn/button';
|
||||
import {
|
||||
@@ -19,8 +21,6 @@ import {
|
||||
SelectValue,
|
||||
} from '@repo/shadcn/select';
|
||||
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 = {
|
||||
// 1: 'Superadmin',
|
||||
@@ -29,8 +29,9 @@ const ROLES = {
|
||||
4: 'Gerente',
|
||||
5: 'Usuario',
|
||||
6: 'Productor',
|
||||
7: 'Organización'
|
||||
}
|
||||
7: 'Organización',
|
||||
8: 'Coordinadores',
|
||||
};
|
||||
|
||||
interface UserFormProps {
|
||||
onSuccess?: () => void;
|
||||
@@ -57,8 +58,8 @@ export function UpdateUserForm({
|
||||
id: defaultValues?.id,
|
||||
phone: defaultValues?.phone || '',
|
||||
role: undefined,
|
||||
isActive: defaultValues?.isActive
|
||||
}
|
||||
isActive: defaultValues?.isActive,
|
||||
};
|
||||
|
||||
// console.log(defaultValues);
|
||||
|
||||
@@ -69,8 +70,7 @@ export function UpdateUserForm({
|
||||
});
|
||||
|
||||
const onSubmit = async (data: UpdateUser) => {
|
||||
|
||||
const formData = data
|
||||
const formData = data;
|
||||
|
||||
saveAccountingAccounts(formData, {
|
||||
onSuccess: () => {
|
||||
@@ -144,7 +144,7 @@ export function UpdateUserForm({
|
||||
<FormItem>
|
||||
<FormLabel>Teléfono</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value?.toString() ?? ''}/>
|
||||
<Input {...field} value={field.value?.toString() ?? ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -153,12 +153,12 @@ export function UpdateUserForm({
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nueva Contraseña</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field}/>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -171,7 +171,9 @@ export function UpdateUserForm({
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Rol</FormLabel>
|
||||
<Select onValueChange={(value) => field.onChange(Number(value))}>
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(Number(value))}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Selecciona un rol" />
|
||||
@@ -196,7 +198,10 @@ export function UpdateUserForm({
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<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">
|
||||
<SelectValue placeholder="Seleccione un estatus" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -5,12 +5,24 @@ import { Edit2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
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() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data } = useUserByProfile();
|
||||
|
||||
// console.log("🎯 data:", data);
|
||||
const userRole = data?.data.role as string;
|
||||
const translatedRole = ROLE_TRANSLATIONS[userRole] || userRole || 'Sin Rol';
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -18,58 +30,60 @@ export function Profile() {
|
||||
<Edit2 className="mr-2 h-4 w-4" /> Editar Perfil
|
||||
</Button>
|
||||
|
||||
<AccountPlanModal open={open} onOpenChange={setOpen} defaultValues={data?.data}/>
|
||||
|
||||
<h2 className='mt-3 mb-1'>Datos del usuario</h2>
|
||||
<AccountPlanModal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
defaultValues={data?.data}
|
||||
/>
|
||||
|
||||
<h2 className="mt-3 mb-1">Datos del usuario</h2>
|
||||
<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'>
|
||||
<p className='font-bold text-lg'>Usuario:</p>
|
||||
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||
<p className="font-bold text-lg">Usuario:</p>
|
||||
<p>{data?.data.username || 'Sin Nombre de Usuario'}</p>
|
||||
</section>
|
||||
|
||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
||||
<p className='font-bold text-lg'>Rol:</p>
|
||||
<p>{data?.data.role || 'Sin Rol'}</p>
|
||||
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||
<p className="font-bold text-lg">Rol:</p>
|
||||
<p>{translatedRole}</p>
|
||||
</section>
|
||||
</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">
|
||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
||||
<p className='font-bold text-lg'>Nombre completo:</p>
|
||||
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||
<p className="font-bold text-lg">Nombre completo:</p>
|
||||
<p>{data?.data.fullname || 'Sin nombre y apellido'}</p>
|
||||
</section>
|
||||
|
||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
||||
<p className='font-bold text-lg'>Correo:</p>
|
||||
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||
<p className="font-bold text-lg">Correo:</p>
|
||||
<p>{data?.data.email || 'Sin correo'}</p>
|
||||
</section>
|
||||
|
||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
||||
<p className='font-bold text-lg'>Teléfono:</p>
|
||||
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||
<p className="font-bold text-lg">Teléfono:</p>
|
||||
<p>{data?.data.phone || 'Sin teléfono'}</p>
|
||||
</section>
|
||||
</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">
|
||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
||||
<p className='font-bold text-lg'>Estado:</p>
|
||||
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||
<p className="font-bold text-lg">Estado:</p>
|
||||
<p>{data?.data.state || 'Sin Estado'}</p>
|
||||
</section>
|
||||
|
||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
||||
<p className='font-bold text-lg'>Municipio:</p>
|
||||
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||
<p className="font-bold text-lg">Municipio:</p>
|
||||
<p>{data?.data.municipality || 'Sin Municipio'}</p>
|
||||
</section>
|
||||
|
||||
<section className='border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md'>
|
||||
<p className='font-bold text-lg'>Parroquia:</p>
|
||||
<section className="border bg-neutral-200 dark:bg-neutral-800 p-2 rounded-md">
|
||||
<p className="font-bold text-lg">Parroquia:</p>
|
||||
<p>{data?.data.parish || 'Sin Parroquia'}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"next": "^15.1.6",
|
||||
"next": "^15.5.9",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"next-safe-action": "^7.10.2",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^3.7.15",
|
||||
"nuqs": "^2.3.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react": "^19.0.3",
|
||||
"react-dom": "^19.0.3",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"recharts": "^2.15.3",
|
||||
"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