Compare commits
4 Commits
ce1727525b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ed110bafa | |||
| f9eef8ebd6 | |||
| 548bb0cdb2 | |||
| 8f207e675c |
@@ -158,7 +158,7 @@ export const trainingSurveys = t.pgTable(
|
|||||||
updatedBy: t
|
updatedBy: t
|
||||||
.integer('updated_by')
|
.integer('updated_by')
|
||||||
.references(() => users.id, { onDelete: 'cascade' }),
|
.references(() => users.id, { onDelete: 'cascade' }),
|
||||||
surveyStatus: t.text('survey_status').notNull().default('PUBLICADO'),
|
surveyStatus: t.text('survey_status').notNull().default('COMPLETADA'),
|
||||||
...timestamps,
|
...timestamps,
|
||||||
},
|
},
|
||||||
(trainingSurveys) => ({
|
(trainingSurveys) => ({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
|||||||
import * as schema from '@/database/index';
|
import * as schema from '@/database/index';
|
||||||
import { states } from '@/database/schema/general';
|
import { states } from '@/database/schema/general';
|
||||||
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||||
import { eq } from 'drizzle-orm';
|
import { and, eq, ne } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import { CreateStateDto } from './dto/create-state.dto';
|
import { CreateStateDto } from './dto/create-state.dto';
|
||||||
import { UpdateStateDto } from './dto/update-state.dto';
|
import { UpdateStateDto } from './dto/update-state.dto';
|
||||||
@@ -15,14 +15,17 @@ export class StatesService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findAll(): Promise<State[]> {
|
async findAll(): Promise<State[]> {
|
||||||
return await this.drizzle.select().from(states);
|
return await this.drizzle
|
||||||
|
.select()
|
||||||
|
.from(states)
|
||||||
|
.where(ne(states.name, 'EMBAJADA'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number): Promise<State> {
|
async findOne(id: number): Promise<State> {
|
||||||
const state = await this.drizzle
|
const state = await this.drizzle
|
||||||
.select()
|
.select()
|
||||||
.from(states)
|
.from(states)
|
||||||
.where(eq(states.id, id));
|
.where(and(eq(states.id, id), ne(states.name, 'EMBAJADA')));
|
||||||
|
|
||||||
if (state.length === 0) {
|
if (state.length === 0) {
|
||||||
throw new HttpException('State not found', HttpStatus.NOT_FOUND);
|
throw new HttpException('State not found', HttpStatus.NOT_FOUND);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||||
// import { Env, validateString } from '@/common/utils';
|
// import { Env, validateString } from '@/common/utils';
|
||||||
import { Inject, Injectable, HttpException, HttpStatus, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { eq, ne } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import * as schema from 'src/database/index';
|
import * as schema from 'src/database/index';
|
||||||
import { states, municipalities, parishes } from 'src/database/index';
|
import { municipalities, parishes, states } from 'src/database/index';
|
||||||
import { eq, like, or, SQL, sql, and, not } from 'drizzle-orm';
|
|
||||||
import * as bcrypt from 'bcryptjs';
|
import { Municipality, Parish, State } from './entities/user.entity';
|
||||||
import { State, Municipality, Parish } from './entities/user.entity';
|
|
||||||
// import { PaginationDto } from '../../common/dto/pagination.dto';
|
// import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -19,6 +19,7 @@ export class UsersService {
|
|||||||
const find = await this.drizzle
|
const find = await this.drizzle
|
||||||
.select()
|
.select()
|
||||||
.from(states)
|
.from(states)
|
||||||
|
.where(ne(states.name, 'EMBAJADA'));
|
||||||
|
|
||||||
return find;
|
return find;
|
||||||
}
|
}
|
||||||
@@ -41,4 +42,3 @@ export class UsersService {
|
|||||||
return find;
|
return find;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
|
Header,
|
||||||
Param,
|
Param,
|
||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
|
StreamableFile,
|
||||||
UploadedFiles,
|
UploadedFiles,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@@ -24,6 +26,7 @@ import { CreateTrainingDto } from './dto/create-training.dto';
|
|||||||
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||||
import { UpdateTrainingDto } from './dto/update-training.dto';
|
import { UpdateTrainingDto } from './dto/update-training.dto';
|
||||||
import { TrainingService } from './training.service';
|
import { TrainingService } from './training.service';
|
||||||
|
import { Public } from '@/common/decorators';
|
||||||
|
|
||||||
@ApiTags('training')
|
@ApiTags('training')
|
||||||
@Controller('training')
|
@Controller('training')
|
||||||
@@ -76,6 +79,38 @@ export class TrainingController {
|
|||||||
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('export/all')
|
||||||
|
// @ApiOperation({ summary: 'Export all training records to Excel' })
|
||||||
|
// @ApiResponse({
|
||||||
|
// status: 200,
|
||||||
|
// description: 'Return training records Excel.',
|
||||||
|
// })
|
||||||
|
// async exportAll(@Query() filterDto: TrainingStatisticsFilterDto) {
|
||||||
|
// const data = await this.trainingService.exportAll(filterDto);
|
||||||
|
// return new StreamableFile(data, {
|
||||||
|
// type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
// disposition: 'attachment; filename=training_surveys.xlsx',
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('export/all')
|
||||||
|
@ApiOperation({ summary: 'Export all training records to Excel' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Return training template.',
|
||||||
|
content: { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: { type: 'string', format: 'binary' } } }
|
||||||
|
})
|
||||||
|
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
@Header('Content-Disposition', 'attachment; filename=export_osp.xlsx')
|
||||||
|
async exportAll(@Query() filterDto: TrainingStatisticsFilterDto) {
|
||||||
|
const data = await this.trainingService.exportAll(filterDto);
|
||||||
|
return new StreamableFile(data, {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
disposition: 'attachment; filename=training_surveys.xlsx',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========== //
|
// ========== //
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ 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 { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||||
import * as schema from 'src/database/index';
|
import * as schema from 'src/database/index';
|
||||||
import { states, trainingSurveys } from 'src/database/index';
|
import { municipalities, parishes, states, trainingSurveys } from 'src/database/index';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
// @ts-ignore
|
||||||
|
import XlsxPopulate from 'xlsx-populate';
|
||||||
|
|
||||||
|
|
||||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
import { CreateTrainingDto } from './dto/create-training.dto';
|
import { CreateTrainingDto } from './dto/create-training.dto';
|
||||||
@@ -98,20 +103,30 @@ export class TrainingService {
|
|||||||
if (municipalityId)
|
if (municipalityId)
|
||||||
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||||
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
|
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
|
||||||
if (ospType) filters.push(eq(trainingSurveys.ospType, ospType));
|
if (ospType && ospType !== 'all') filters.push(eq(trainingSurveys.ospType, ospType));
|
||||||
|
|
||||||
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
||||||
|
|
||||||
// Ejecutamos todas las consultas en paralelo con Promise.all para mayor velocidad
|
// Ejecutamos todas las consultas en paralelo con Promise.all para mayor velocidad
|
||||||
const [
|
const [
|
||||||
totalOspsResult,
|
totalOspsResult,
|
||||||
totalProducersResult,
|
// totalProducersResult,
|
||||||
totalProductsResult, // Nuevo: Calculado desde el JSON
|
totalProductsResult,
|
||||||
statusDistribution,
|
statusDistribution,
|
||||||
activityDistribution,
|
activityDistribution,
|
||||||
typeDistribution,
|
typeDistribution,
|
||||||
stateDistribution,
|
stateDistribution,
|
||||||
yearDistribution,
|
yearDistribution,
|
||||||
|
ecoSectorDistribution,
|
||||||
|
productiveSectorDistribution,
|
||||||
|
centralActivityDistribution,
|
||||||
|
mainActivityDistribution,
|
||||||
|
structureTypeDistribution,
|
||||||
|
isOpenSpaceDistribution,
|
||||||
|
hasTransportDistribution,
|
||||||
|
genderResult,
|
||||||
|
municipalityDistribution,
|
||||||
|
parishDistribution,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// 1. Total OSPs
|
// 1. Total OSPs
|
||||||
this.drizzle
|
this.drizzle
|
||||||
@@ -120,12 +135,12 @@ export class TrainingService {
|
|||||||
.where(whereCondition),
|
.where(whereCondition),
|
||||||
|
|
||||||
// 2. Total Productores (Columna plana que mantuviste)
|
// 2. Total Productores (Columna plana que mantuviste)
|
||||||
this.drizzle
|
// this.drizzle
|
||||||
.select({
|
// .select({
|
||||||
sum: sql<number>`SUM(${trainingSurveys.womenCount} + ${trainingSurveys.menCount})`,
|
// sum: sql<number>`SUM(${trainingSurveys.womenCount} + ${trainingSurveys.menCount})`,
|
||||||
})
|
// })
|
||||||
.from(trainingSurveys)
|
// .from(trainingSurveys)
|
||||||
.where(whereCondition),
|
// .where(whereCondition),
|
||||||
|
|
||||||
// 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
|
// 3. NUEVO: Total Productos (Contamos el largo del array JSON productList)
|
||||||
this.drizzle
|
this.drizzle
|
||||||
@@ -145,7 +160,7 @@ export class TrainingService {
|
|||||||
.where(whereCondition)
|
.where(whereCondition)
|
||||||
.groupBy(trainingSurveys.currentStatus),
|
.groupBy(trainingSurveys.currentStatus),
|
||||||
|
|
||||||
// 5. Distribución por Actividad
|
// 5. Distribución por Actividad (General)
|
||||||
this.drizzle
|
this.drizzle
|
||||||
.select({
|
.select({
|
||||||
name: trainingSurveys.productiveActivity,
|
name: trainingSurveys.productiveActivity,
|
||||||
@@ -188,11 +203,115 @@ export class TrainingService {
|
|||||||
.where(whereCondition)
|
.where(whereCondition)
|
||||||
.groupBy(trainingSurveys.companyConstitutionYear)
|
.groupBy(trainingSurveys.companyConstitutionYear)
|
||||||
.orderBy(trainingSurveys.companyConstitutionYear),
|
.orderBy(trainingSurveys.companyConstitutionYear),
|
||||||
|
|
||||||
|
// 9. Distribución por Sector Económico
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.ecoSector,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.ecoSector),
|
||||||
|
|
||||||
|
// 10. Distribución por Sector Productivo
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.productiveSector,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.productiveSector),
|
||||||
|
|
||||||
|
// 11. Distribución por Actividad Central Productiva
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.centralProductiveActivity,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.centralProductiveActivity),
|
||||||
|
|
||||||
|
// 12. Distribución por Actividad Productiva Principal
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.mainProductiveActivity,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.mainProductiveActivity),
|
||||||
|
|
||||||
|
// 13. Distribución por Tipo de Estructura
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: trainingSurveys.structureType,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.structureType),
|
||||||
|
|
||||||
|
// 14. Distribución por Espacio Abierto
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: sql<string>`case when ${trainingSurveys.isOpenSpace} then 'Sí' else 'No' end`,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.isOpenSpace),
|
||||||
|
|
||||||
|
// 15. Distribución por Transporte
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: sql<string>`case when ${trainingSurveys.hasTransport} then 'Sí' else 'No' end`,
|
||||||
|
value: sql<number>`count(*)`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(trainingSurveys.hasTransport),
|
||||||
|
|
||||||
|
// 16. Distribución por Género
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
women: sql<number>`sum(${trainingSurveys.womenCount})`,
|
||||||
|
men: sql<number>`sum(${trainingSurveys.menCount})`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(whereCondition),
|
||||||
|
|
||||||
|
// 17. Distribución por Municipio
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: sql<string>`COALESCE(${municipalities.name}, 'Sin Asignar')`,
|
||||||
|
value: sql<number>`count(${trainingSurveys.id})`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.leftJoin(
|
||||||
|
municipalities,
|
||||||
|
eq(trainingSurveys.municipality, municipalities.id),
|
||||||
|
)
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(municipalities.name),
|
||||||
|
|
||||||
|
// 18. Distribución por Parroquia
|
||||||
|
this.drizzle
|
||||||
|
.select({
|
||||||
|
name: sql<string>`COALESCE(${parishes.name}, 'Sin Asignar')`,
|
||||||
|
value: sql<number>`count(${trainingSurveys.id})`,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
|
||||||
|
.where(whereCondition)
|
||||||
|
.groupBy(parishes.name),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalOsps: Number(totalOspsResult[0]?.count || 0),
|
totalOsps: Number(totalOspsResult[0]?.count || 0),
|
||||||
totalProducers: Number(totalProducersResult[0]?.sum || 0),
|
// totalProducers: Number(totalProducersResult[0]?.sum || 0),
|
||||||
totalProducts: Number(totalProductsResult[0]?.sum || 0), // Dato extraído del JSON
|
totalProducts: Number(totalProductsResult[0]?.sum || 0), // Dato extraído del JSON
|
||||||
|
|
||||||
statusDistribution: statusDistribution.map((item) => ({
|
statusDistribution: statusDistribution.map((item) => ({
|
||||||
@@ -215,6 +334,46 @@ export class TrainingService {
|
|||||||
...item,
|
...item,
|
||||||
value: Number(item.value),
|
value: Number(item.value),
|
||||||
})),
|
})),
|
||||||
|
ecoSectorDistribution: ecoSectorDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
productiveSectorDistribution: productiveSectorDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
centralActivityDistribution: centralActivityDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
mainActivityDistribution: mainActivityDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
structureTypeDistribution: structureTypeDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
isOpenSpaceDistribution: isOpenSpaceDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
hasTransportDistribution: hasTransportDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
genderDistribution: [
|
||||||
|
{ name: 'Mujeres', value: Number(genderResult[0]?.women || 0) },
|
||||||
|
{ name: 'Hombres', value: Number(genderResult[0]?.men || 0) },
|
||||||
|
],
|
||||||
|
municipalityDistribution: municipalityDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
|
parishDistribution: parishDistribution.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: Number(item.value),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,4 +869,178 @@ export class TrainingService {
|
|||||||
|
|
||||||
// return book.outputAsync();
|
// return book.outputAsync();
|
||||||
// }
|
// }
|
||||||
|
async exportAll(filterDto: TrainingStatisticsFilterDto) {
|
||||||
|
try {
|
||||||
|
const { startDate, endDate, stateId, municipalityId, parishId, ospType } =
|
||||||
|
filterDto;
|
||||||
|
|
||||||
|
const filters: SQL[] = [];
|
||||||
|
|
||||||
|
if (startDate)
|
||||||
|
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||||
|
if (endDate)
|
||||||
|
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||||
|
if (stateId) filters.push(eq(trainingSurveys.state, stateId));
|
||||||
|
if (municipalityId)
|
||||||
|
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||||
|
if (parishId) filters.push(eq(trainingSurveys.parish, parishId));
|
||||||
|
if (ospType && ospType !== 'all')
|
||||||
|
filters.push(eq(trainingSurveys.ospType, ospType));
|
||||||
|
|
||||||
|
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
||||||
|
|
||||||
|
const records = await this.drizzle
|
||||||
|
.select({
|
||||||
|
coorFullName: trainingSurveys.coorFullName,
|
||||||
|
visitDate: trainingSurveys.visitDate,
|
||||||
|
stateName: states.name,
|
||||||
|
municipalityName: municipalities.name,
|
||||||
|
parishName: parishes.name,
|
||||||
|
communeName: trainingSurveys.communeName,
|
||||||
|
siturCodeCommune: trainingSurveys.siturCodeCommune,
|
||||||
|
communalCouncil: trainingSurveys.communalCouncil,
|
||||||
|
siturCodeCommunalCouncil: trainingSurveys.siturCodeCommunalCouncil,
|
||||||
|
productiveActivity: trainingSurveys.productiveActivity,
|
||||||
|
ospName: trainingSurveys.ospName,
|
||||||
|
ospAddress: trainingSurveys.ospAddress,
|
||||||
|
ospRif: trainingSurveys.ospRif,
|
||||||
|
ospType: trainingSurveys.ospType,
|
||||||
|
currentStatus: trainingSurveys.currentStatus,
|
||||||
|
companyConstitutionYear: trainingSurveys.companyConstitutionYear,
|
||||||
|
ospResponsibleFullname: trainingSurveys.ospResponsibleFullname,
|
||||||
|
ospResponsibleCedula: trainingSurveys.ospResponsibleCedula,
|
||||||
|
ospResponsibleRif: trainingSurveys.ospResponsibleRif,
|
||||||
|
ospResponsiblePhone: trainingSurveys.ospResponsiblePhone,
|
||||||
|
ospResponsibleEmail: trainingSurveys.ospResponsibleEmail,
|
||||||
|
civilState: trainingSurveys.civilState,
|
||||||
|
familyBurden: trainingSurveys.familyBurden,
|
||||||
|
numberOfChildren: trainingSurveys.numberOfChildren,
|
||||||
|
generalObservations: trainingSurveys.generalObservations,
|
||||||
|
paralysisReason: trainingSurveys.paralysisReason,
|
||||||
|
productList: trainingSurveys.productList,
|
||||||
|
infrastructureMt2: trainingSurveys.infrastructureMt2,
|
||||||
|
photo1: trainingSurveys.photo1,
|
||||||
|
photo2: trainingSurveys.photo2,
|
||||||
|
photo3: trainingSurveys.photo3,
|
||||||
|
})
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.leftJoin(states, eq(trainingSurveys.state, states.id))
|
||||||
|
.leftJoin(
|
||||||
|
municipalities,
|
||||||
|
eq(trainingSurveys.municipality, municipalities.id),
|
||||||
|
)
|
||||||
|
.leftJoin(parishes, eq(trainingSurveys.parish, parishes.id))
|
||||||
|
.where(whereCondition)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const workbook: any = await XlsxPopulate.fromBlankAsync();
|
||||||
|
const sheet = workbook.sheet(0);
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
'Coordinador',
|
||||||
|
'Fecha',
|
||||||
|
'Hora',
|
||||||
|
'Estado',
|
||||||
|
'Municipio',
|
||||||
|
'Parroquia',
|
||||||
|
'Comuna',
|
||||||
|
'Código SITUR Comuna',
|
||||||
|
'Consejo Comunal',
|
||||||
|
'Código SITUR C.C.',
|
||||||
|
'Actividad Productiva',
|
||||||
|
'Nombre OSP',
|
||||||
|
'Dirección OSP',
|
||||||
|
'RIF OSP',
|
||||||
|
'Tipo OSP',
|
||||||
|
'Estatus Actual',
|
||||||
|
'Año Constitución',
|
||||||
|
'Total Productores',
|
||||||
|
'Productos',
|
||||||
|
'Infraestructura (mt2)',
|
||||||
|
'Motivo Paralización',
|
||||||
|
'Responsable',
|
||||||
|
'Cédula',
|
||||||
|
'RIF Responsable',
|
||||||
|
'Teléfono',
|
||||||
|
'Email',
|
||||||
|
'Estado Civil',
|
||||||
|
'Carga Familiar',
|
||||||
|
'Nro Hijos',
|
||||||
|
'Observaciones',
|
||||||
|
// 'Foto 1',
|
||||||
|
// 'Foto 2',
|
||||||
|
// 'Foto 3',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Configurar encabezados
|
||||||
|
sheet.range('A1:AG1').value([headers]).style({
|
||||||
|
bold: true,
|
||||||
|
fill: 'BFBFBF',
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentRow = 2;
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
const date = new Date(record.visitDate);
|
||||||
|
const dateStr = date.toLocaleDateString('es-VE');
|
||||||
|
const timeStr = date.toLocaleTimeString('es-VE');
|
||||||
|
|
||||||
|
const products = (record.productList as any[]) || [];
|
||||||
|
const totalProducers = products.reduce(
|
||||||
|
(sum, p) =>
|
||||||
|
sum + (Number(p.menCount) || 0) + (Number(p.womenCount) || 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const productsDesc = products.map((p) => p.name).join(', ');
|
||||||
|
|
||||||
|
const rowData = [
|
||||||
|
record.coorFullName,
|
||||||
|
dateStr,
|
||||||
|
timeStr,
|
||||||
|
record.stateName || '',
|
||||||
|
record.municipalityName || '',
|
||||||
|
record.parishName || '',
|
||||||
|
record.communeName,
|
||||||
|
record.siturCodeCommune,
|
||||||
|
record.communalCouncil,
|
||||||
|
record.siturCodeCommunalCouncil,
|
||||||
|
record.productiveActivity,
|
||||||
|
record.ospName,
|
||||||
|
record.ospAddress,
|
||||||
|
record.ospRif,
|
||||||
|
record.ospType,
|
||||||
|
record.currentStatus,
|
||||||
|
record.companyConstitutionYear,
|
||||||
|
totalProducers,
|
||||||
|
productsDesc,
|
||||||
|
record.infrastructureMt2,
|
||||||
|
record.paralysisReason || '',
|
||||||
|
record.ospResponsibleFullname,
|
||||||
|
record.ospResponsibleCedula,
|
||||||
|
record.ospResponsibleRif,
|
||||||
|
record.ospResponsiblePhone,
|
||||||
|
record.ospResponsibleEmail,
|
||||||
|
record.civilState,
|
||||||
|
record.familyBurden,
|
||||||
|
record.numberOfChildren,
|
||||||
|
record.generalObservations || '',
|
||||||
|
// record.photo1 || '',
|
||||||
|
// record.photo2 || '',
|
||||||
|
// record.photo3 || '',
|
||||||
|
];
|
||||||
|
|
||||||
|
sheet.cell(`A${currentRow}`).value([rowData]);
|
||||||
|
currentRow++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await workbook.outputAsync();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Export Error:', error);
|
||||||
|
throw new HttpException(
|
||||||
|
'Error al generar el archivo Excel: ' + error.message,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function SocioproductivaStatisticsPage() {
|
export default function SocioproductivaStatisticsPage() {
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
// <PageContainer>
|
||||||
<div className="w-full">
|
<div className="w-full p-6">
|
||||||
<h1 className="text-2xl font-bold mb-6">Estadísticas Socioproductivas</h1>
|
<h1 className="text-2xl font-bold mb-6">Estadísticas Socioproductivas</h1>
|
||||||
<TrainingStatistics />
|
<TrainingStatistics />
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
// </PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
TrainingSchema,
|
TrainingSchema,
|
||||||
trainingApiResponseSchema,
|
trainingApiResponseSchema,
|
||||||
} from '../schemas/training';
|
} from '../schemas/training';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
export const getTrainingStatisticsAction = async (
|
export const getTrainingStatisticsAction = async (
|
||||||
params: {
|
params: {
|
||||||
@@ -159,3 +160,39 @@ export const getTrainingByIdAction = async (id: number) => {
|
|||||||
|
|
||||||
return response?.data;
|
return response?.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const exportTrainingAction = async (
|
||||||
|
params: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
stateId?: number;
|
||||||
|
municipalityId?: number;
|
||||||
|
parishId?: number;
|
||||||
|
ospType?: string;
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||||
|
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||||
|
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
|
||||||
|
if (params.municipalityId)
|
||||||
|
searchParams.append('municipalityId', params.municipalityId.toString());
|
||||||
|
if (params.parishId)
|
||||||
|
searchParams.append('parishId', params.parishId.toString());
|
||||||
|
if (params.ospType) searchParams.append('ospType', params.ospType);
|
||||||
|
|
||||||
|
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
z.any(), //Schema
|
||||||
|
`/training/export/all?${searchParams.toString()}`,
|
||||||
|
'GET',
|
||||||
|
undefined,
|
||||||
|
{ responseType: 'arraybuffer' },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Error al exportar los datos');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(new Uint8Array(response));
|
||||||
|
};
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ export function CreateTrainingForm({
|
|||||||
coorPhone: defaultValues?.coorPhone || '',
|
coorPhone: defaultValues?.coorPhone || '',
|
||||||
visitDate: formatToLocalISO(defaultValues?.visitDate),
|
visitDate: formatToLocalISO(defaultValues?.visitDate),
|
||||||
productiveActivity: defaultValues?.productiveActivity || undefined,
|
productiveActivity: defaultValues?.productiveActivity || undefined,
|
||||||
productiveActivityOther: defaultValues?.productiveActivityOther || undefined,
|
productiveActivityOther:
|
||||||
|
defaultValues?.productiveActivityOther || undefined,
|
||||||
ecoSector: defaultValues?.ecoSector || undefined,
|
ecoSector: defaultValues?.ecoSector || undefined,
|
||||||
productiveSector: defaultValues?.productiveSector || undefined,
|
productiveSector: defaultValues?.productiveSector || undefined,
|
||||||
centralProductiveActivity:
|
centralProductiveActivity:
|
||||||
@@ -172,7 +173,7 @@ export function CreateTrainingForm({
|
|||||||
|
|
||||||
womenCount: defaultValues?.womenCount || 0,
|
womenCount: defaultValues?.womenCount || 0,
|
||||||
menCount: defaultValues?.menCount || 0,
|
menCount: defaultValues?.menCount || 0,
|
||||||
surveyStatus: defaultValues?.surveyStatus || 'BORRADOR'
|
surveyStatus: defaultValues?.surveyStatus || 'BORRADOR',
|
||||||
},
|
},
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
});
|
});
|
||||||
@@ -240,7 +241,7 @@ export function CreateTrainingForm({
|
|||||||
productiveSector,
|
productiveSector,
|
||||||
centralProductiveActivity,
|
centralProductiveActivity,
|
||||||
mainProductiveActivity,
|
mainProductiveActivity,
|
||||||
productiveActivity
|
productiveActivity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
|
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
|
||||||
@@ -690,8 +691,8 @@ export function CreateTrainingForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{other && (
|
||||||
{other && (<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="productiveActivityOther"
|
name="productiveActivityOther"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -700,16 +701,13 @@ export function CreateTrainingForm({
|
|||||||
¿Cuál otra Actividad Productiva?
|
¿Cuál otra Actividad Productiva?
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} value={field.value} />
|
||||||
{...field}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>)}
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -756,11 +754,7 @@ export function CreateTrainingForm({
|
|||||||
Año de constitución
|
Año de constitución
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input type="number" {...field} value={field.value} />
|
||||||
type="number"
|
|
||||||
{...field}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -1291,11 +1285,7 @@ export function CreateTrainingForm({
|
|||||||
Correo Electrónico de la Comuna (Opcional)
|
Correo Electrónico de la Comuna (Opcional)
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input type="email" {...field} value={field.value} />
|
||||||
type="email"
|
|
||||||
{...field}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -1405,11 +1395,7 @@ export function CreateTrainingForm({
|
|||||||
Correo Electrónico del Consejo Comunal (Opcional)
|
Correo Electrónico del Consejo Comunal (Opcional)
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input type="email" {...field} value={field.value} />
|
||||||
type="email"
|
|
||||||
{...field}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -1555,9 +1541,7 @@ export function CreateTrainingForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<FormLabel>
|
<FormLabel>Subir imágenes (Máximo 3 y opcional)</FormLabel>
|
||||||
Subir imágenes (Máximo 3 y opcional)
|
|
||||||
</FormLabel>
|
|
||||||
<Input
|
<Input
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
@@ -1643,13 +1627,8 @@ export function CreateTrainingForm({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 justify-items-end gap-3 mt-8">
|
<div className="flex justify-end gap-4">
|
||||||
<Button
|
<Button variant="outline" type="button" onClick={onCancel}>
|
||||||
variant="outline"
|
|
||||||
type="button"
|
|
||||||
onClick={onCancel}
|
|
||||||
className="w-32 col-span-2 md:col-span-1"
|
|
||||||
>
|
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -1668,8 +1647,10 @@ export function CreateTrainingForm({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem defaultChecked value="BORRADOR">BORRADOR</SelectItem>
|
<SelectItem defaultChecked value="BORRADOR">
|
||||||
<SelectItem value="PUBLICADO">PUBLICADO</SelectItem>
|
BORRADOR
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="COMPLETADA">COMPLETADA</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
|
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
|
||||||
|
import { exportTrainingAction } from '../actions/training-actions';
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
|
|
||||||
const OSP_TYPES = [
|
const OSP_TYPES = [
|
||||||
'EPSD',
|
'EPSD',
|
||||||
@@ -89,6 +91,38 @@ export function TrainingStatistics() {
|
|||||||
setOspType('');
|
setOspType('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
setIsExporting(true);
|
||||||
|
const bytes = await exportTrainingAction({
|
||||||
|
startDate: startDate || undefined,
|
||||||
|
endDate: endDate || undefined,
|
||||||
|
stateId: stateId || undefined,
|
||||||
|
municipalityId: municipalityId || undefined,
|
||||||
|
parishId: parishId || undefined,
|
||||||
|
ospType: ospType || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([new Uint8Array(bytes)], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `entrenamientos_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting:', error);
|
||||||
|
} finally {
|
||||||
|
setIsExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center p-8">Cargando estadísticas...</div>
|
<div className="flex justify-center p-8">Cargando estadísticas...</div>
|
||||||
@@ -103,12 +137,22 @@ export function TrainingStatistics() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
totalOsps,
|
totalOsps,
|
||||||
totalProducers,
|
// totalProducers,
|
||||||
statusDistribution,
|
statusDistribution,
|
||||||
activityDistribution,
|
activityDistribution,
|
||||||
typeDistribution,
|
typeDistribution,
|
||||||
stateDistribution,
|
stateDistribution,
|
||||||
yearDistribution,
|
yearDistribution,
|
||||||
|
ecoSectorDistribution,
|
||||||
|
productiveSectorDistribution,
|
||||||
|
centralActivityDistribution,
|
||||||
|
mainActivityDistribution,
|
||||||
|
structureTypeDistribution,
|
||||||
|
isOpenSpaceDistribution,
|
||||||
|
hasTransportDistribution,
|
||||||
|
genderDistribution,
|
||||||
|
municipalityDistribution,
|
||||||
|
parishDistribution,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
@@ -206,10 +250,19 @@ export function TrainingStatistics() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end gap-2">
|
||||||
<Button variant="outline" onClick={handleClearFilters}>
|
<Button variant="outline" onClick={handleClearFilters}>
|
||||||
Limpiar Filtros
|
Limpiar Filtros
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={isExporting}
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
>
|
||||||
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
{isExporting ? 'Exportando...' : 'Exportar Excel'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -230,7 +283,8 @@ export function TrainingStatistics() {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
|
||||||
|
{/* <Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">
|
<CardTitle className="text-sm font-medium">
|
||||||
Total de Productores
|
Total de Productores
|
||||||
@@ -242,7 +296,8 @@ export function TrainingStatistics() {
|
|||||||
Productores asociados
|
Productores asociados
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card> */}
|
||||||
|
|
||||||
|
|
||||||
<Card className="col-span-full">
|
<Card className="col-span-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -269,8 +324,8 @@ export function TrainingStatistics() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* State Distribution */}
|
{/* Location Distribution (Dynamic) */}
|
||||||
{/* <Card className="col-span-full">
|
<Card className="col-span-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Distribución por Estado</CardTitle>
|
<CardTitle>Distribución por Estado</CardTitle>
|
||||||
<CardDescription>OSP registradas por estado</CardDescription>
|
<CardDescription>OSP registradas por estado</CardDescription>
|
||||||
@@ -290,7 +345,69 @@ export function TrainingStatistics() {
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card> */}
|
</Card>
|
||||||
|
|
||||||
|
{stateId > 0 ? (
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribución por Municipio</CardTitle>
|
||||||
|
<CardDescription>OSP registradas en este estado</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={municipalityDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#0088FE" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribución por Municipio</CardTitle>
|
||||||
|
<CardDescription>Seleccione un estado</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{municipalityId > 0 ? (
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribución por Parroquia</CardTitle>
|
||||||
|
<CardDescription>OSP registradas en este municipio</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={parishDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribución por Parroquia</CardTitle>
|
||||||
|
<CardDescription>Seleccione un municipio</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Year Distribution */}
|
{/* Year Distribution */}
|
||||||
<Card className="col-span-full lg:col-span-1">
|
<Card className="col-span-full lg:col-span-1">
|
||||||
@@ -367,6 +484,223 @@ export function TrainingStatistics() {
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* ECO SECTOR DISTRIBUTION */}
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Sector Económico</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Distribución por sector económico
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={ecoSectorDistribution}
|
||||||
|
layout="vertical"
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={150} />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#0088FE" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* PRODUCTIVE SECTOR DISTRIBUTION */}
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Sector Productivo</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Distribución por sector productivo
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={productiveSectorDistribution}
|
||||||
|
layout="vertical"
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={150} />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#00C49F" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* CENTRAL PRODUCTIVE ACTIVITY DISTRIBUTION */}
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Actividad Central Productiva</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Distribución por actividad central productiva
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={centralActivityDistribution}
|
||||||
|
layout="vertical"
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={150} />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#FF8042" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* MAIN PRODUCTIVE ACTIVITY DISTRIBUTION */}
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Actividad Productiva Principal</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Distribución por actividad productiva principal
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={mainActivityDistribution}
|
||||||
|
layout="vertical"
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={150} />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* STRUCTURE TYPE DISTRIBUTION */}
|
||||||
|
<Card className="col-span-full lg:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Tipo de Estructura</CardTitle>
|
||||||
|
<CardDescription>Distribución física</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={structureTypeDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* GENDER DISTRIBUTION */}
|
||||||
|
<Card className="col-span-full lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribución de Género</CardTitle>
|
||||||
|
<CardDescription>Conteo total por género</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={genderDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#8884d8" name="Personas" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* OPEN SPACE AND TRANSPORT (PIE CHARTS) */}
|
||||||
|
<div className="col-span-full grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Espacio Abierto</CardTitle>
|
||||||
|
<CardDescription>¿Poseen áreas libres?</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={isOpenSpaceDistribution}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{isOpenSpaceDistribution.map((entry, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={COLORS[(index + 2) % COLORS.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Transporte</CardTitle>
|
||||||
|
<CardDescription>¿Tienen vehículo propio?</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={hasTransportDistribution}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{hasTransportDistribution.map((entry, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={COLORS[(index + 4) % COLORS.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,22 +43,13 @@ export function columns({ apiUrl }: ColumnsProps): ColumnDef<TrainingSchema>[] {
|
|||||||
return date ? new Date(date).toLocaleString() : 'N/A';
|
return date ? new Date(date).toLocaleString() : 'N/A';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
accessorKey: 'visitDate',
|
|
||||||
header: 'Fecha Visita',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const date = row.getValue('visitDate') as string;
|
|
||||||
return date ? new Date(date).toLocaleString() : 'N/A';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'surveyStatus',
|
accessorKey: 'surveyStatus',
|
||||||
header: '',
|
header: '',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.getValue('surveyStatus') as string;
|
const status = row.getValue('surveyStatus') as string;
|
||||||
return (
|
return (
|
||||||
<Badge variant={status === 'PUBLICADO' ? 'default' : 'secondary'}>
|
<Badge variant={status === 'COMPLETADA' ? 'success' : 'secondary'}>
|
||||||
{status}
|
{status}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,13 +10,23 @@ export const statisticsItemSchema = z.object({
|
|||||||
|
|
||||||
export const trainingStatisticsSchema = z.object({
|
export const trainingStatisticsSchema = z.object({
|
||||||
totalOsps: z.number(),
|
totalOsps: z.number(),
|
||||||
totalProducers: z.number(),
|
// totalProducers: z.number(),
|
||||||
totalProducts: z.number(),
|
totalProducts: z.number(),
|
||||||
statusDistribution: z.array(statisticsItemSchema),
|
statusDistribution: z.array(statisticsItemSchema),
|
||||||
activityDistribution: z.array(statisticsItemSchema),
|
activityDistribution: z.array(statisticsItemSchema),
|
||||||
typeDistribution: z.array(statisticsItemSchema),
|
typeDistribution: z.array(statisticsItemSchema),
|
||||||
stateDistribution: z.array(statisticsItemSchema),
|
stateDistribution: z.array(statisticsItemSchema),
|
||||||
yearDistribution: z.array(statisticsItemSchema),
|
yearDistribution: z.array(statisticsItemSchema),
|
||||||
|
ecoSectorDistribution: z.array(statisticsItemSchema),
|
||||||
|
productiveSectorDistribution: z.array(statisticsItemSchema),
|
||||||
|
centralActivityDistribution: z.array(statisticsItemSchema),
|
||||||
|
mainActivityDistribution: z.array(statisticsItemSchema),
|
||||||
|
structureTypeDistribution: z.array(statisticsItemSchema),
|
||||||
|
isOpenSpaceDistribution: z.array(statisticsItemSchema),
|
||||||
|
hasTransportDistribution: z.array(statisticsItemSchema),
|
||||||
|
genderDistribution: z.array(statisticsItemSchema),
|
||||||
|
municipalityDistribution: z.array(statisticsItemSchema),
|
||||||
|
parishDistribution: z.array(statisticsItemSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
|
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
import { env } from '@/lib/env';
|
import { env } from '@/lib/env';
|
||||||
import axios, { InternalAxiosRequestConfig } from 'axios';
|
import axios, { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Crear instancia de Axios con la URL base validada
|
// Crear instancia de Axios con la URL base validada
|
||||||
@@ -32,6 +32,7 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
|||||||
url: string,
|
url: string,
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
|
||||||
data?: any,
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
): Promise<
|
): Promise<
|
||||||
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
|
[{ type: string; message: string; details?: any } | null, z.infer<T> | null]
|
||||||
> => {
|
> => {
|
||||||
@@ -40,6 +41,7 @@ export const safeFetchApi = async <T extends z.ZodSchema<any>>(
|
|||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
data,
|
data,
|
||||||
|
...config,
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = schema.safeParse(response.data);
|
const parsed = schema.safeParse(response.data);
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
import * as React from "react"
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from "@repo/shadcn/lib/utils"
|
import { cn } from '@repo/shadcn/lib/utils';
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||||
secondary:
|
secondary:
|
||||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70",
|
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70',
|
||||||
outline:
|
outline:
|
||||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||||
|
success:
|
||||||
|
'border-transparent bg-green-500 text-black [a&]:hover:bg-green-300',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
function Badge({
|
function Badge({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"span"> &
|
}: React.ComponentProps<'span'> &
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
const Comp = asChild ? Slot : "span"
|
const Comp = asChild ? Slot : 'span';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
@@ -40,7 +42,7 @@ function Badge({
|
|||||||
className={cn(badgeVariants({ variant }), className)}
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge, badgeVariants };
|
||||||
|
|||||||
Reference in New Issue
Block a user