Merge branch 'main' of ssh://git.fondemi.gob.ve:222/Fondemi/sistema_base
This commit is contained in:
@@ -13,13 +13,13 @@ import { ThrottlerGuard } from '@nestjs/throttler';
|
|||||||
import { DrizzleModule } from './database/drizzle.module';
|
import { DrizzleModule } from './database/drizzle.module';
|
||||||
import { AuthModule } from './features/auth/auth.module';
|
import { AuthModule } from './features/auth/auth.module';
|
||||||
import { ConfigurationsModule } from './features/configurations/configurations.module';
|
import { ConfigurationsModule } from './features/configurations/configurations.module';
|
||||||
import { LocationModule} from './features/location/location.module'
|
import { LocationModule } from './features/location/location.module'
|
||||||
import { MailModule } from './features/mail/mail.module';
|
import { MailModule } from './features/mail/mail.module';
|
||||||
import { RolesModule } from './features/roles/roles.module';
|
import { RolesModule } from './features/roles/roles.module';
|
||||||
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
import { UserRolesModule } from './features/user-roles/user-roles.module';
|
||||||
import { SurveysModule } from './features/surveys/surveys.module';
|
import { SurveysModule } from './features/surveys/surveys.module';
|
||||||
import {InventoryModule} from './features/inventory/inventory.module'
|
import { InventoryModule } from './features/inventory/inventory.module';
|
||||||
import { PicturesModule } from './features/pictures/pictures.module';
|
import { TrainingModule } from './features/training/training.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -61,7 +61,7 @@ import { PicturesModule } from './features/pictures/pictures.module';
|
|||||||
SurveysModule,
|
SurveysModule,
|
||||||
LocationModule,
|
LocationModule,
|
||||||
InventoryModule,
|
InventoryModule,
|
||||||
PicturesModule
|
TrainingModule
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule { }
|
||||||
|
|||||||
37
apps/api/src/database/migrations/0008_plain_scream.sql
Normal file
37
apps/api/src/database/migrations/0008_plain_scream.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
CREATE TABLE "training_surveys" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"firstname" text NOT NULL,
|
||||||
|
"lastname" text NOT NULL,
|
||||||
|
"visit_date" timestamp NOT NULL,
|
||||||
|
"productive_activity" text NOT NULL,
|
||||||
|
"financial_requirement_description" text NOT NULL,
|
||||||
|
"situr_code_commune" text NOT NULL,
|
||||||
|
"communal_council" text NOT NULL,
|
||||||
|
"situr_code_communal_council" text NOT NULL,
|
||||||
|
"osp_name" text NOT NULL,
|
||||||
|
"osp_address" text NOT NULL,
|
||||||
|
"osp_rif" text NOT NULL,
|
||||||
|
"osp_type" text NOT NULL,
|
||||||
|
"current_status" text NOT NULL,
|
||||||
|
"company_constitution_year" integer NOT NULL,
|
||||||
|
"producer_count" integer NOT NULL,
|
||||||
|
"product_description" text NOT NULL,
|
||||||
|
"installed_capacity" text NOT NULL,
|
||||||
|
"operational_capacity" text NOT NULL,
|
||||||
|
"osp_responsible_fullname" text NOT NULL,
|
||||||
|
"osp_responsible_cedula" text NOT NULL,
|
||||||
|
"osp_responsible_rif" text NOT NULL,
|
||||||
|
"osp_responsible_phone" text NOT NULL,
|
||||||
|
"civil_state" text NOT NULL,
|
||||||
|
"family_burden" integer NOT NULL,
|
||||||
|
"number_of_children" integer NOT NULL,
|
||||||
|
"general_observations" text NOT NULL,
|
||||||
|
"photo1" text NOT NULL,
|
||||||
|
"photo2" text NOT NULL,
|
||||||
|
"photo3" text NOT NULL,
|
||||||
|
"paralysis_reason" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp (3)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "training_surveys_index_00" ON "training_surveys" USING btree ("firstname");
|
||||||
7
apps/api/src/database/migrations/0009_eminent_ares.sql
Normal file
7
apps/api/src/database/migrations/0009_eminent_ares.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "state" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "municipality" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "parish" integer;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD COLUMN "osp_responsible_email" text NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_state_states_id_fk" FOREIGN KEY ("state") REFERENCES "public"."states"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_municipality_municipalities_id_fk" FOREIGN KEY ("municipality") REFERENCES "public"."municipalities"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "training_surveys" ADD CONSTRAINT "training_surveys_parish_parishes_id_fk" FOREIGN KEY ("parish") REFERENCES "public"."parishes"("id") ON DELETE set null ON UPDATE no action;
|
||||||
1778
apps/api/src/database/migrations/meta/0008_snapshot.json
Normal file
1778
apps/api/src/database/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1842
apps/api/src/database/migrations/meta/0009_snapshot.json
Normal file
1842
apps/api/src/database/migrations/meta/0009_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,20 @@
|
|||||||
"when": 1754420096323,
|
"when": 1754420096323,
|
||||||
"tag": "0007_curved_fantastic_four",
|
"tag": "0007_curved_fantastic_four",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764623430844,
|
||||||
|
"tag": "0008_plain_scream",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764883378610,
|
||||||
|
"tag": "0009_eminent_ares",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import * as t from 'drizzle-orm/pg-core';
|
|||||||
import { eq, lt, gte, ne, sql } from 'drizzle-orm';
|
import { eq, lt, gte, ne, sql } from 'drizzle-orm';
|
||||||
import { timestamps } from '../timestamps';
|
import { timestamps } from '../timestamps';
|
||||||
import { users } from './auth';
|
import { users } from './auth';
|
||||||
|
import { states, municipalities, parishes } from './general';
|
||||||
|
|
||||||
|
|
||||||
// Tabla surveys
|
// Tabla surveys
|
||||||
@@ -44,7 +45,57 @@ export const answersSurveys = t.pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tabla training_surveys
|
||||||
|
export const trainingSurveys = t.pgTable(
|
||||||
|
'training_surveys',
|
||||||
|
{
|
||||||
|
// Datos basicos
|
||||||
|
id: t.serial('id').primaryKey(),
|
||||||
|
firstname: t.text('firstname').notNull(),
|
||||||
|
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' }),
|
||||||
|
siturCodeCommune: t.text('situr_code_commune').notNull(),
|
||||||
|
communalCouncil: t.text('communal_council').notNull(),
|
||||||
|
siturCodeCommunalCouncil: t.text('situr_code_communal_council').notNull(),
|
||||||
|
// datos del OSP (ORGANIZACIÓN SOCIOPRODUCTIVA)
|
||||||
|
ospName: t.text('osp_name').notNull(),
|
||||||
|
ospAddress: t.text('osp_address').notNull(),
|
||||||
|
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(),
|
||||||
|
companyConstitutionYear: t.integer('company_constitution_year').notNull(),
|
||||||
|
producerCount: t.integer('producer_count').notNull(),
|
||||||
|
productDescription: t.text('product_description').notNull(),
|
||||||
|
installedCapacity: t.text('installed_capacity').notNull(),
|
||||||
|
operationalCapacity: t.text('operational_capacity').notNull(),
|
||||||
|
// datos del responsable
|
||||||
|
ospResponsibleFullname: t.text('osp_responsible_fullname').notNull(),
|
||||||
|
ospResponsibleCedula: t.text('osp_responsible_cedula').notNull(),
|
||||||
|
ospResponsibleRif: t.text('osp_responsible_rif').notNull(),
|
||||||
|
ospResponsiblePhone: t.text('osp_responsible_phone').notNull(),
|
||||||
|
ospResponsibleEmail: t.text('osp_responsible_email').notNull(),
|
||||||
|
civilState: t.text('civil_state').notNull(),
|
||||||
|
familyBurden: t.integer('family_burden').notNull(),
|
||||||
|
numberOfChildren: t.integer('number_of_children').notNull(),
|
||||||
|
// datos adicionales
|
||||||
|
generalObservations: t.text('general_observations').notNull(),
|
||||||
|
paralysisReason: t.text('paralysis_reason').notNull(),
|
||||||
|
// fotos
|
||||||
|
photo1: t.text('photo1').notNull(),
|
||||||
|
photo2: t.text('photo2').notNull(),
|
||||||
|
photo3: t.text('photo3').notNull(),
|
||||||
|
...timestamps,
|
||||||
|
},
|
||||||
|
(trainingSurveys) => ({
|
||||||
|
trainingSurveysIndex: t.index('training_surveys_index_00').on(trainingSurveys.firstname),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const viewSurveys = t.pgView('v_surveys', {
|
export const viewSurveys = t.pgView('v_surveys', {
|
||||||
surverId: t.integer('survey_id'),
|
surverId: t.integer('survey_id'),
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { Env, validateString } from '@/common/utils';
|
|||||||
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
||||||
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
import { RefreshTokenDto } from '@/features/auth/dto/refresh-token.dto';
|
||||||
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
|
import { SignInUserDto } from '@/features/auth/dto/signIn-user.dto';
|
||||||
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
|
||||||
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
import { SignOutUserDto } from '@/features/auth/dto/signOut-user.dto';
|
||||||
|
import { SingUpUserDto } from '@/features/auth/dto/signUp-user.dto';
|
||||||
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
|
import { ValidateUserDto } from '@/features/auth/dto/validate-user.dto';
|
||||||
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
|
import AuthTokensInterface from '@/features/auth/interfaces/auth-tokens.interface';
|
||||||
import {
|
import {
|
||||||
@@ -24,14 +24,14 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { and, eq, or } from 'drizzle-orm';
|
import { and, eq, or } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import * as schema from 'src/database/index';
|
import * as schema from 'src/database/index';
|
||||||
import { sessions, users, roles, usersRole } from 'src/database/index';
|
import { roles, sessions, users, usersRole } from 'src/database/index';
|
||||||
import { Session } from './interfaces/session.interface';
|
import { Session } from './interfaces/session.interface';
|
||||||
import * as bcrypt from 'bcryptjs';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -81,33 +81,43 @@ export class AuthService {
|
|||||||
|
|
||||||
//Generate Tokens
|
//Generate Tokens
|
||||||
async generateTokens(user: User): Promise<AuthTokensInterface> {
|
async generateTokens(user: User): Promise<AuthTokensInterface> {
|
||||||
|
const accessTokenSecret = envs.access_token_secret ?? '';
|
||||||
|
const accessTokenExp = envs.access_token_expiration ?? '';
|
||||||
|
const refreshTokenSecret = envs.refresh_token_secret ?? '';
|
||||||
|
const refreshTokenExp = envs.refresh_token_expiration ?? '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
!accessTokenSecret ||
|
||||||
|
!accessTokenExp ||
|
||||||
|
!refreshTokenSecret ||
|
||||||
|
!refreshTokenExp
|
||||||
|
) {
|
||||||
|
throw new Error('JWT environment variables are missing or invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JwtPayload {
|
||||||
|
sub: number;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
sub: Number(user?.id),
|
||||||
|
username: user.username ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
const [access_token, refresh_token] = await Promise.all([
|
const [access_token, refresh_token] = await Promise.all([
|
||||||
this.jwtService.signAsync(
|
this.jwtService.signAsync(payload, {
|
||||||
{
|
secret: accessTokenSecret,
|
||||||
sub: user.id,
|
expiresIn: accessTokenExp,
|
||||||
username: user.username,
|
} as JwtSignOptions),
|
||||||
},
|
|
||||||
{
|
this.jwtService.signAsync(payload, {
|
||||||
secret: envs.access_token_secret,
|
secret: refreshTokenSecret,
|
||||||
expiresIn: envs.access_token_expiration,
|
expiresIn: refreshTokenExp,
|
||||||
},
|
} as JwtSignOptions),
|
||||||
),
|
|
||||||
this.jwtService.signAsync(
|
|
||||||
{
|
|
||||||
sub: user.id,
|
|
||||||
username: user.username,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
secret: envs.refresh_token_secret,
|
|
||||||
expiresIn: envs.refresh_token_expiration,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return { access_token, refresh_token };
|
||||||
access_token,
|
|
||||||
refresh_token,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Generate OTP Code For Email Confirmation
|
//Generate OTP Code For Email Confirmation
|
||||||
@@ -138,7 +148,8 @@ export class AuthService {
|
|||||||
userId: parseInt(userId),
|
userId: parseInt(userId),
|
||||||
expiresAt: sessionInput.expiresAt,
|
expiresAt: sessionInput.expiresAt,
|
||||||
});
|
});
|
||||||
if (session.rowCount === 0) throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);
|
if (session.rowCount === 0)
|
||||||
|
throw new HttpException('Failed to create session', HttpStatus.NOT_FOUND);
|
||||||
|
|
||||||
return 'Session created successfully';
|
return 'Session created successfully';
|
||||||
}
|
}
|
||||||
@@ -197,7 +208,6 @@ export class AuthService {
|
|||||||
|
|
||||||
//Sign In User Account
|
//Sign In User Account
|
||||||
async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
|
async signIn(dto: SignInUserDto): Promise<LoginUserInterface> {
|
||||||
|
|
||||||
const user = await this.validateUser(dto);
|
const user = await this.validateUser(dto);
|
||||||
const tokens = await this.generateTokens(user);
|
const tokens = await this.generateTokens(user);
|
||||||
const decodeAccess = this.decodeToken(tokens.access_token);
|
const decodeAccess = this.decodeToken(tokens.access_token);
|
||||||
@@ -265,12 +275,12 @@ export class AuthService {
|
|||||||
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
|
async refreshToken(dto: RefreshTokenDto): Promise<RefreshTokenInterface> {
|
||||||
const secret = envs.refresh_token_secret;
|
const secret = envs.refresh_token_secret;
|
||||||
const { user_id, token } = dto;
|
const { user_id, token } = dto;
|
||||||
|
|
||||||
console.log('secret', secret);
|
console.log('secret', secret);
|
||||||
console.log('refresh_token', token);
|
console.log('refresh_token', token);
|
||||||
|
|
||||||
const validation = await this.jwtService.verifyAsync(token, {
|
const validation = await this.jwtService.verifyAsync(token, {
|
||||||
secret
|
secret,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!validation) throw new UnauthorizedException('Invalid refresh token');
|
if (!validation) throw new UnauthorizedException('Invalid refresh token');
|
||||||
@@ -279,23 +289,20 @@ export class AuthService {
|
|||||||
.select()
|
.select()
|
||||||
.from(sessions)
|
.from(sessions)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(eq(sessions.userId, user_id), eq(sessions.sessionToken, token)),
|
||||||
eq(sessions.userId, user_id),
|
|
||||||
eq(sessions.sessionToken, token)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log(session.length);
|
// console.log(session.length);
|
||||||
|
|
||||||
if (session.length === 0) throw new NotFoundException('session not found');
|
if (session.length === 0) throw new NotFoundException('session not found');
|
||||||
const user = await this.findUserById(user_id);
|
const user = await this.findUserById(user_id);
|
||||||
if (!user) throw new NotFoundException('User not found');
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
|
||||||
// Genera token
|
// Genera token
|
||||||
const tokens = await this.generateTokens(user);
|
const tokens = await this.generateTokens(user);
|
||||||
const decodeAccess = this.decodeToken(tokens.access_token);
|
const decodeAccess = this.decodeToken(tokens.access_token);
|
||||||
const decodeRefresh = this.decodeToken(tokens.refresh_token);
|
const decodeRefresh = this.decodeToken(tokens.refresh_token);
|
||||||
|
|
||||||
// Actualiza session
|
// Actualiza session
|
||||||
await this.drizzle
|
await this.drizzle
|
||||||
.update(sessions)
|
.update(sessions)
|
||||||
@@ -311,75 +318,83 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async singUp(createUserDto: SingUpUserDto): Promise<User> {
|
async singUp(createUserDto: SingUpUserDto): Promise<User> {
|
||||||
// Check if username or email exists
|
// Check if username or email exists
|
||||||
const data = await this.drizzle
|
const data = await this.drizzle
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
username: users.username,
|
||||||
|
email: users.email,
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
eq(users.username, createUserDto.username),
|
||||||
|
eq(users.email, createUserDto.email),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
if (data[0].username === createUserDto.username) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Username already exists',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data[0].email === createUserDto.email) {
|
||||||
|
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
return await this.drizzle.transaction(async (tx) => {
|
||||||
|
// Hash the password
|
||||||
|
// Create the user
|
||||||
|
const [newUser] = await tx
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
username: createUserDto.username,
|
||||||
|
email: createUserDto.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
fullname: createUserDto.fullname,
|
||||||
|
isActive: true,
|
||||||
|
state: createUserDto.state,
|
||||||
|
municipality: createUserDto.municipality,
|
||||||
|
parish: createUserDto.parish,
|
||||||
|
phone: createUserDto.phone,
|
||||||
|
isEmailVerified: false,
|
||||||
|
isTwoFactorEnabled: false,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// check if user role is admin
|
||||||
|
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
|
||||||
|
// check if user role is admin
|
||||||
|
|
||||||
|
// Assign role to user
|
||||||
|
await tx.insert(usersRole).values({
|
||||||
|
userId: newUser.id,
|
||||||
|
roleId: role,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the created user with role
|
||||||
|
const [userWithRole] = await tx
|
||||||
.select({
|
.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
username: users.username,
|
username: users.username,
|
||||||
email: users.email
|
email: users.email,
|
||||||
|
fullname: users.fullname,
|
||||||
|
phone: users.phone,
|
||||||
|
isActive: users.isActive,
|
||||||
|
role: roles.name,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(or(eq(users.username, createUserDto.username), eq(users.email, createUserDto.email)));
|
.leftJoin(usersRole, eq(usersRole.userId, users.id))
|
||||||
|
.leftJoin(roles, eq(roles.id, usersRole.roleId))
|
||||||
if (data.length > 0) {
|
.where(eq(users.id, newUser.id));
|
||||||
if (data[0].username === createUserDto.username) {
|
|
||||||
throw new HttpException('Username already exists', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
if (data[0].email === createUserDto.email) {
|
|
||||||
throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the password
|
return userWithRole;
|
||||||
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
});
|
||||||
|
|
||||||
// Start a transaction
|
|
||||||
return await this.drizzle.transaction(async (tx) => {
|
|
||||||
// Create the user
|
|
||||||
const [newUser] = await tx
|
|
||||||
.insert(users)
|
|
||||||
.values({
|
|
||||||
username: createUserDto.username,
|
|
||||||
email: createUserDto.email,
|
|
||||||
password: hashedPassword,
|
|
||||||
fullname: createUserDto.fullname,
|
|
||||||
isActive: true,
|
|
||||||
state: createUserDto.state,
|
|
||||||
municipality: createUserDto.municipality,
|
|
||||||
parish: createUserDto.parish,
|
|
||||||
phone: createUserDto.phone,
|
|
||||||
isEmailVerified: false,
|
|
||||||
isTwoFactorEnabled: false,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// check if user role is admin
|
|
||||||
const role = createUserDto.role <= 2 ? 5 : createUserDto.role;
|
|
||||||
|
|
||||||
// Assign role to user
|
|
||||||
await tx.insert(usersRole).values({
|
|
||||||
userId: newUser.id,
|
|
||||||
roleId: role,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the created user with role
|
|
||||||
const [userWithRole] = await tx
|
|
||||||
.select({
|
|
||||||
id: users.id,
|
|
||||||
username: users.username,
|
|
||||||
email: users.email,
|
|
||||||
fullname: users.fullname,
|
|
||||||
phone: users.phone,
|
|
||||||
isActive: users.isActive,
|
|
||||||
role: roles.name,
|
|
||||||
})
|
|
||||||
.from(users)
|
|
||||||
.leftJoin(usersRole, eq(usersRole.userId, users.id))
|
|
||||||
.leftJoin(roles, eq(roles.id, usersRole.roleId))
|
|
||||||
.where(eq(users.id, newUser.id));
|
|
||||||
|
|
||||||
return userWithRole;
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Controller, Post, UploadedFiles, UseInterceptors, Body } from '@nestjs/common';
|
|
||||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
|
||||||
import { PicturesService } from './pictures.service';
|
|
||||||
|
|
||||||
@Controller('pictures')
|
|
||||||
export class PicturesController {
|
|
||||||
constructor(private readonly picturesService: PicturesService) {}
|
|
||||||
|
|
||||||
@Post('upload')
|
|
||||||
@UseInterceptors(FilesInterceptor('urlImg'))
|
|
||||||
async uploadFile(@UploadedFiles() files: Express.Multer.File[], @Body() body: any) {
|
|
||||||
// Aquí puedes acceder a los campos del formulario
|
|
||||||
// console.log('Archivos:', files);
|
|
||||||
// console.log('Otros campos del formulario:', body);
|
|
||||||
const result = await this.picturesService.saveImages(files);
|
|
||||||
|
|
||||||
return { data: result };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PicturesController } from './pictures.controller';
|
|
||||||
import { PicturesService } from './pictures.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
controllers: [PicturesController],
|
|
||||||
providers: [PicturesService],
|
|
||||||
})
|
|
||||||
export class PicturesModule {}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { writeFile } from 'fs/promises';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PicturesService {
|
|
||||||
/**
|
|
||||||
* Guarda una imagen en el directorio de imágenes.
|
|
||||||
* @param file - El archivo de imagen a guardar.
|
|
||||||
* @returns La ruta de la imagen guardada.
|
|
||||||
*/
|
|
||||||
async saveImages(file: Express.Multer.File[]): Promise<string[]> {
|
|
||||||
// Construye la ruta al directorio de imágenes.
|
|
||||||
|
|
||||||
const picturesPath = join(__dirname, '..', '..', '..', '..', 'uploads','pict');
|
|
||||||
|
|
||||||
console.log(picturesPath);
|
|
||||||
|
|
||||||
let images : string[] = [];
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
file.forEach(async (file) => {
|
|
||||||
count++
|
|
||||||
// Crea un nombre de archivo único para la imagen.
|
|
||||||
const fileName = `${Date.now()}-${count}-${file.originalname}`;
|
|
||||||
images.push(fileName);
|
|
||||||
// console.log(fileName);
|
|
||||||
|
|
||||||
// Construye la ruta completa al archivo de imagen.
|
|
||||||
const filePath = join(picturesPath, fileName);
|
|
||||||
|
|
||||||
// Escribe el archivo de imagen en el disco.
|
|
||||||
await writeFile(filePath, file.buffer);
|
|
||||||
});
|
|
||||||
// Devuelve la ruta de la imagen guardada.
|
|
||||||
// return [file[0].originalname]
|
|
||||||
return images;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
140
apps/api/src/features/training/dto/create-training.dto.ts
Normal file
140
apps/api/src/features/training/dto/create-training.dto.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsInt, IsString, IsDateString, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateTrainingDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
firstname: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
lastname: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsDateString()
|
||||||
|
visitDate: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
productiveActivity: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
financialRequirementDescription: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
state: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
municipality: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
parish: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
siturCodeCommune: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
communalCouncil: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
siturCodeCommunalCouncil: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospName: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospAddress: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospType: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
currentStatus: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
companyConstitutionYear: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
producerCount: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
productDescription: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
installedCapacity: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
operationalCapacity: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleFullname: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleCedula: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleRif: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsiblePhone: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
ospResponsibleEmail: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
civilState: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
familyBurden: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsInt()
|
||||||
|
numberOfChildren: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
generalObservations: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
photo1: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
photo2: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
photo3: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
paralysisReason: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber, IsDateString } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class TrainingStatisticsFilterDto {
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
startDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
endDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
stateId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
municipalityId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNumber()
|
||||||
|
parishId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
ospType?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateTrainingDto } from './create-training.dto';
|
||||||
|
|
||||||
|
export class UpdateTrainingDto extends PartialType(CreateTrainingDto) { }
|
||||||
68
apps/api/src/features/training/training.controller.ts
Normal file
68
apps/api/src/features/training/training.controller.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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 { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
|
|
||||||
|
import { TrainingStatisticsFilterDto } from './dto/training-statistics-filter.dto';
|
||||||
|
|
||||||
|
@ApiTags('training')
|
||||||
|
@Controller('training')
|
||||||
|
export class TrainingController {
|
||||||
|
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('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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Delete a training record' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Training record deleted successfully.' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Training record not found.' })
|
||||||
|
async remove(@Param('id') id: string) {
|
||||||
|
return await this.trainingService.remove(+id);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/features/training/training.module.ts
Normal file
10
apps/api/src/features/training/training.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TrainingService } from './training.service';
|
||||||
|
import { TrainingController } from './training.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [TrainingController],
|
||||||
|
providers: [TrainingService],
|
||||||
|
exports: [TrainingService],
|
||||||
|
})
|
||||||
|
export class TrainingModule { }
|
||||||
223
apps/api/src/features/training/training.service.ts
Normal file
223
apps/api/src/features/training/training.service.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import { DRIZZLE_PROVIDER } from 'src/database/drizzle-provider';
|
||||||
|
import { Inject, Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
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 { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TrainingService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
|
||||||
|
async findAll(paginationDto?: PaginationDto) {
|
||||||
|
const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {};
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
let searchCondition: SQL<unknown> | undefined;
|
||||||
|
if (search) {
|
||||||
|
searchCondition = or(
|
||||||
|
like(trainingSurveys.firstname, `%${search}%`),
|
||||||
|
like(trainingSurveys.lastname, `%${search}%`),
|
||||||
|
like(trainingSurveys.ospName, `%${search}%`),
|
||||||
|
like(trainingSurveys.ospRif, `%${search}%`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderBy = sortOrder === 'asc'
|
||||||
|
? sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} asc`
|
||||||
|
: sql`${trainingSurveys[sortBy as keyof typeof trainingSurveys]} desc`;
|
||||||
|
|
||||||
|
const totalCountResult = await this.drizzle
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(searchCondition);
|
||||||
|
|
||||||
|
const totalCount = Number(totalCountResult[0].count);
|
||||||
|
const totalPages = Math.ceil(totalCount / limit);
|
||||||
|
|
||||||
|
const data = await this.drizzle
|
||||||
|
.select()
|
||||||
|
.from(trainingSurveys)
|
||||||
|
.where(searchCondition)
|
||||||
|
.orderBy(orderBy)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset);
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalCount,
|
||||||
|
totalPages,
|
||||||
|
hasNextPage: page < totalPages,
|
||||||
|
hasPreviousPage: page > 1,
|
||||||
|
nextPage: page < totalPages ? page + 1 : null,
|
||||||
|
previousPage: page > 1 ? page - 1 : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { data, meta };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatistics(filterDto: TrainingStatisticsFilterDto) {
|
||||||
|
const { startDate, endDate, stateId, municipalityId, parishId, ospType } = filterDto;
|
||||||
|
|
||||||
|
const filters: SQL[] = [];
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
filters.push(gte(trainingSurveys.visitDate, new Date(startDate)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
filters.push(lte(trainingSurveys.visitDate, new Date(endDate)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateId) {
|
||||||
|
filters.push(eq(trainingSurveys.state, stateId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (municipalityId) {
|
||||||
|
filters.push(eq(trainingSurveys.municipality, municipalityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parishId) {
|
||||||
|
filters.push(eq(trainingSurveys.parish, parishId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ospType) {
|
||||||
|
filters.push(eq(trainingSurveys.ospType, ospType));
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereCondition = filters.length > 0 ? and(...filters) : undefined;
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(createTrainingDto: CreateTrainingDto) {
|
||||||
|
const [newRecord] = await this.drizzle
|
||||||
|
.insert(trainingSurveys)
|
||||||
|
.values({
|
||||||
|
...createTrainingDto,
|
||||||
|
visitDate: new Date(createTrainingDto.visitDate),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, updateTrainingDto: UpdateTrainingDto) {
|
||||||
|
await this.findOne(id);
|
||||||
|
|
||||||
|
const updateData: any = { ...updateTrainingDto };
|
||||||
|
if (updateTrainingDto.visitDate) {
|
||||||
|
updateData.visitDate = new Date(updateTrainingDto.visitDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedRecord] = await this.drizzle
|
||||||
|
.update(trainingSurveys)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return updatedRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number) {
|
||||||
|
await this.findOne(id);
|
||||||
|
|
||||||
|
const [deletedRecord] = await this.drizzle
|
||||||
|
.delete(trainingSurveys)
|
||||||
|
.where(eq(trainingSurveys.id, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return { message: 'Training record deleted successfully', data: deletedRecord };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import PageContainer from '@/components/layout/page-container';
|
|
||||||
|
|
||||||
const Page = () => {
|
|
||||||
return (
|
|
||||||
<PageContainer>
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
|
||||||
En mantenimiento
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</PageContainer>
|
|
||||||
// <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
|
||||||
// <div className="flex w-full max-w-sm flex-col gap-6">
|
|
||||||
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
19
apps/web/app/dashboard/estadisticas/socioproductiva/page.tsx
Normal file
19
apps/web/app/dashboard/estadisticas/socioproductiva/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { TrainingStatistics } from '@/feactures/training/components/training-statistics';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Estadísticas Socioproductivas - Fondemi',
|
||||||
|
description: 'Análisis y estadísticas de las Organizaciones Socioproductivas',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SocioproductivaStatisticsPage() {
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<div className="w-full">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Estadísticas Socioproductivas</h1>
|
||||||
|
<TrainingStatistics />
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
apps/web/app/dashboard/formulario/page.tsx
Normal file
15
apps/web/app/dashboard/formulario/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import PageContainer from '@/components/layout/page-container';
|
||||||
|
import { CreateTrainingForm } from '@/feactures/training/components/form';
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||||
|
<CreateTrainingForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { NavMain as GeneralMain, NavMain as AdministrationMain, NavMain as StatisticsMain, } from '@/components/nav-main';
|
import { NavMain as GeneralMain, NavMain as AdministrationMain, NavMain as StatisticsMain, } from '@/components/nav-main';
|
||||||
import { GeneralItems, AdministrationItems, StatisticsItems } from '@/constants/data';
|
import { GeneralItems, AdministrationItems, StatisticsItems } from '@/constants/routes';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -24,9 +24,9 @@ export const company = {
|
|||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol :'';
|
const userRole = session?.user.role[0]?.rol ? session.user.role[0].rol : '';
|
||||||
// console.log(AdministrationItems[0]?.role);
|
// console.log(AdministrationItems[0]?.role);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
@@ -42,14 +42,14 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<GeneralMain titleGroup={'General'} items={GeneralItems} role={userRole}/>
|
<GeneralMain titleGroup={'General'} items={GeneralItems} role={userRole} />
|
||||||
|
|
||||||
{StatisticsItems[0]?.role?.includes(userRole) &&
|
{StatisticsItems[0]?.role?.includes(userRole) &&
|
||||||
<StatisticsMain titleGroup={'Estadisticas'} items={StatisticsItems} role={userRole}/>
|
<StatisticsMain titleGroup={'Estadisticas'} items={StatisticsItems} role={userRole} />
|
||||||
}
|
}
|
||||||
|
|
||||||
{AdministrationItems[0]?.role?.includes(userRole) &&
|
{AdministrationItems[0]?.role?.includes(userRole) &&
|
||||||
<AdministrationMain titleGroup={'Administracion'} items={AdministrationItems} role={userRole}/>
|
<AdministrationMain titleGroup={'Administracion'} items={AdministrationItems} role={userRole} />
|
||||||
}
|
}
|
||||||
{/* <NavProjects projects={data.projects} /> */}
|
{/* <NavProjects projects={data.projects} /> */}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|||||||
@@ -20,14 +20,13 @@ export const GeneralItems: NavItem[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export const AdministrationItems: NavItem[] = [
|
export const AdministrationItems: NavItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Administracion',
|
title: 'Administracion',
|
||||||
url: '#', // Placeholder as there is no direct link for the parent
|
url: '#', // Placeholder as there is no direct link for the parent
|
||||||
icon: 'settings2',
|
icon: 'settings2',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
role:['admin','superadmin','manager','user'], // sumatoria de los roles que si tienen acceso
|
role: ['admin', 'superadmin', 'manager', 'autoridad'], // sumatoria de los roles que si tienen acceso
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@@ -35,14 +34,21 @@ export const AdministrationItems: NavItem[] = [
|
|||||||
url: '/dashboard/administracion/usuario',
|
url: '/dashboard/administracion/usuario',
|
||||||
icon: 'userPen',
|
icon: 'userPen',
|
||||||
shortcut: ['m', 'm'],
|
shortcut: ['m', 'm'],
|
||||||
role:['admin','superadmin'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Encuestas',
|
title: 'Encuestas',
|
||||||
shortcut: ['l', 'l'],
|
shortcut: ['l', 'l'],
|
||||||
url: '/dashboard/administracion/encuestas',
|
url: '/dashboard/administracion/encuestas',
|
||||||
icon: 'login',
|
icon: 'login',
|
||||||
role:['admin','superadmin','manager','user'],
|
role: ['admin', 'superadmin', 'autoridad', 'manager'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Registro OSP',
|
||||||
|
shortcut: ['p', 'p'],
|
||||||
|
url: '/dashboard/formulario/',
|
||||||
|
icon: 'notepadText',
|
||||||
|
role: ['admin', 'superadmin', 'manager', 'autoridad'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -54,7 +60,7 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
url: '#', // Placeholder as there is no direct link for the parent
|
url: '#', // Placeholder as there is no direct link for the parent
|
||||||
icon: 'chartColumn',
|
icon: 'chartColumn',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
role:['admin','superadmin','autoridad'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
// {
|
// {
|
||||||
@@ -69,13 +75,15 @@ export const StatisticsItems: NavItem[] = [
|
|||||||
shortcut: ['l', 'l'],
|
shortcut: ['l', 'l'],
|
||||||
url: '/dashboard/estadisticas/encuestas',
|
url: '/dashboard/estadisticas/encuestas',
|
||||||
icon: 'notepadText',
|
icon: 'notepadText',
|
||||||
role:['admin','superadmin','autoridad'],
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'OSP',
|
||||||
|
shortcut: ['s', 's'],
|
||||||
|
url: '/dashboard/estadisticas/socioproductiva',
|
||||||
|
icon: 'blocks',
|
||||||
|
role: ['admin', 'superadmin', 'autoridad'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { DataTable } from '@repo/shadcn/table/data-table';
|
import { DataTable } from '@repo/shadcn/table/data-table';
|
||||||
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
|
||||||
import { columns } from './product-tables/columns';
|
|
||||||
import { useProductQuery } from '../../hooks/use-query-products';
|
import { useProductQuery } from '../../hooks/use-query-products';
|
||||||
|
import { columns } from './product-tables/columns';
|
||||||
|
|
||||||
interface dataListProps {
|
interface dataListProps {
|
||||||
initialPage: number;
|
initialPage: number;
|
||||||
initialSearch?: string | null;
|
initialSearch?: string | null;
|
||||||
initialLimit: number;
|
initialLimit: number;
|
||||||
|
initialType?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UsersAdminList({
|
export default function UsersAdminList({
|
||||||
@@ -19,9 +20,9 @@ export default function UsersAdminList({
|
|||||||
page: initialPage,
|
page: initialPage,
|
||||||
limit: initialLimit,
|
limit: initialLimit,
|
||||||
...(initialSearch && { search: initialSearch }),
|
...(initialSearch && { search: initialSearch }),
|
||||||
}
|
};
|
||||||
|
|
||||||
const {data, isLoading} = useProductQuery(filters)
|
const { data, isLoading } = useProductQuery(filters);
|
||||||
|
|
||||||
// console.log(data?.data);
|
// console.log(data?.data);
|
||||||
|
|
||||||
|
|||||||
123
apps/web/feactures/training/actions/training-actions.ts
Normal file
123
apps/web/feactures/training/actions/training-actions.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
'use server';
|
||||||
|
import { safeFetchApi } from '@/lib/fetch.api';
|
||||||
|
import {
|
||||||
|
TrainingSchema,
|
||||||
|
TrainingMutate,
|
||||||
|
trainingApiResponseSchema
|
||||||
|
} from '../schemas/training';
|
||||||
|
import { trainingStatisticsResponseSchema } from '../schemas/statistics';
|
||||||
|
|
||||||
|
export const getTrainingStatisticsAction = async (params: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
stateId?: number;
|
||||||
|
municipalityId?: number;
|
||||||
|
parishId?: number;
|
||||||
|
ospType?: string;
|
||||||
|
} = {}) => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (params.startDate) searchParams.append('startDate', params.startDate);
|
||||||
|
if (params.endDate) searchParams.append('endDate', params.endDate);
|
||||||
|
if (params.stateId) searchParams.append('stateId', params.stateId.toString());
|
||||||
|
if (params.municipalityId) searchParams.append('municipalityId', params.municipalityId.toString());
|
||||||
|
if (params.parishId) searchParams.append('parishId', params.parishId.toString());
|
||||||
|
if (params.ospType) searchParams.append('ospType', params.ospType);
|
||||||
|
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
trainingStatisticsResponseSchema,
|
||||||
|
`/training/statistics?${searchParams.toString()}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
|
return response?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getTrainingAction = async (params: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
page: (params.page || 1).toString(),
|
||||||
|
limit: (params.limit || 10).toString(),
|
||||||
|
...(params.search && { search: params.search }),
|
||||||
|
...(params.sortBy && { sortBy: params.sortBy }),
|
||||||
|
...(params.sortOrder && { sortOrder: params.sortOrder }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, response] = await safeFetchApi(
|
||||||
|
trainingApiResponseSchema,
|
||||||
|
`/training?${searchParams}`,
|
||||||
|
'GET',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response?.data || [],
|
||||||
|
meta: response?.meta || {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalCount: 0,
|
||||||
|
totalPages: 1,
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
nextPage: null,
|
||||||
|
previousPage: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTrainingAction = async (payload: TrainingSchema) => {
|
||||||
|
const { id, ...payloadWithoutId } = payload;
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
'/training',
|
||||||
|
'POST',
|
||||||
|
payloadWithoutId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Error al crear el registro');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTrainingAction = async (payload: TrainingSchema) => {
|
||||||
|
const { id, ...payloadWithoutId } = payload;
|
||||||
|
|
||||||
|
if (!id) throw new Error('ID es requerido para actualizar');
|
||||||
|
|
||||||
|
const [error, data] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
`/training/${id}`,
|
||||||
|
'PATCH',
|
||||||
|
payloadWithoutId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message || 'Error al actualizar el registro');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTrainingAction = async (id: number) => {
|
||||||
|
const [error] = await safeFetchApi(
|
||||||
|
TrainingMutate,
|
||||||
|
`/training/${id}`,
|
||||||
|
'DELETE'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message || 'Error al eliminar el registro');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
41
apps/web/feactures/training/columnas del excel.sql
Normal file
41
apps/web/feactures/training/columnas del excel.sql
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
-- datos basicos
|
||||||
|
nombre,
|
||||||
|
apellido,
|
||||||
|
fecha de la visita,
|
||||||
|
-->Falta
|
||||||
|
hora de la visita,
|
||||||
|
-- datos de la ubicacion
|
||||||
|
estado,
|
||||||
|
municipio,
|
||||||
|
parroquia,
|
||||||
|
nombre de la comuna,
|
||||||
|
CODIGO SITUR COMUNA,
|
||||||
|
CONSEJO COMUNAL,
|
||||||
|
CODIGO SITUR CONSEJO COMUNAL,
|
||||||
|
-- datos de la osp
|
||||||
|
actividad productiva (agricola,textil,bloquera,carpinteria,unidad de suministro),
|
||||||
|
realice una breve descripcion del requerimiento financiero,
|
||||||
|
NOMBRE DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
DIRECCIÓN DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
RIF DE LA ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
TIPO DE ORGANIZACIÓN SOCIOPRODUCTIVA,
|
||||||
|
ESTATUS ACTUAL,
|
||||||
|
AÑO DE CONSTITUCIÓN DE LA EMPRESA ,
|
||||||
|
CANTIDAD DE PRODUCTORES QUE LA CONFORMAN,
|
||||||
|
BREVE DESCRIPCIÓN DEL PRODUCTO O SERVICIO QUE OFRECE,
|
||||||
|
CAPACIDAD INSTALADA,
|
||||||
|
CAPACIDAD OPERATIVA,
|
||||||
|
¿EXPLIQUE LAS RAZONES GENERALES POR LAS CUALES LA UNIDAD DE PRODUCCIÓN TUVO QUE PARALIZARSE?
|
||||||
|
-- datos del responsable
|
||||||
|
NOMBRE Y APELLIDO DEL RESPONSABLE DE LA OSP,
|
||||||
|
CÉDULA DEL RESPONSABLE (SIN PUNTOS),
|
||||||
|
RIF DEL RESPONSABLE (SIN PUNTOS),
|
||||||
|
TELÉFONOS (COLOQUE 2 NUMEROS DE TELEFONOS),
|
||||||
|
CORREO ELECTRÓNICO,
|
||||||
|
ESTADO CIVIL DEL PRODUCTOR,
|
||||||
|
CARGA FAMILIAR,
|
||||||
|
NUMERO DE HIJOS,
|
||||||
|
-- datos adicionales
|
||||||
|
OBSERVACIONES GENERALES,
|
||||||
|
-- fotos
|
||||||
|
COLOCAR TRES (3) REGISTROS FOTOGRÁFICOS VISIBLES DEL ESPACIO Y MAQUINARIAS ACTUALMENTE (OBLIGATORIO),
|
||||||
558
apps/web/feactures/training/components/form.tsx
Normal file
558
apps/web/feactures/training/components/form.tsx
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@repo/shadcn/form';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import { Textarea } from '@repo/shadcn/textarea';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useCreateTraining } from "../hooks/use-training";
|
||||||
|
import { TrainingSchema, trainingSchema } from '../schemas/training';
|
||||||
|
|
||||||
|
import { SelectSearchable } from '@repo/shadcn/select-searchable'
|
||||||
|
import React from 'react';
|
||||||
|
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
|
||||||
|
|
||||||
|
const PRODUCTIVE_ACTIVITIES = [
|
||||||
|
'Agricola',
|
||||||
|
'Textil',
|
||||||
|
'Bloquera',
|
||||||
|
'Carpinteria',
|
||||||
|
'Unidad de suministro'
|
||||||
|
];
|
||||||
|
|
||||||
|
interface CreateTrainingFormProps {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
defaultValues?: Partial<TrainingSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateTrainingForm({
|
||||||
|
onSuccess,
|
||||||
|
onCancel,
|
||||||
|
defaultValues,
|
||||||
|
}: CreateTrainingFormProps) {
|
||||||
|
const {
|
||||||
|
mutate: saveTraining,
|
||||||
|
isPending: isSaving,
|
||||||
|
} = useCreateTraining();
|
||||||
|
|
||||||
|
const [state, setState] = React.useState(0);
|
||||||
|
const [municipality, setMunicipality] = React.useState(0);
|
||||||
|
const [disabledMunicipality, setDisabledMunicipality] = React.useState(true);
|
||||||
|
const [disabledParish, setDisabledParish] = React.useState(true);
|
||||||
|
|
||||||
|
const { data: dataState } = useStateQuery()
|
||||||
|
const { data: dataMunicipality } = useMunicipalityQuery(state)
|
||||||
|
const { data: dataParish } = useParishQuery(municipality)
|
||||||
|
|
||||||
|
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }]
|
||||||
|
|
||||||
|
const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
|
||||||
|
? dataMunicipality.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }]
|
||||||
|
// const parishOptions = dataParish?.data || [{id:0,municipalityId:0,name:'Sin Parroquias'}]
|
||||||
|
const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0
|
||||||
|
? dataParish.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }]
|
||||||
|
|
||||||
|
const form = useForm<TrainingSchema>({
|
||||||
|
resolver: zodResolver(trainingSchema),
|
||||||
|
defaultValues: {
|
||||||
|
firstname: defaultValues?.firstname || '',
|
||||||
|
lastname: defaultValues?.lastname || '',
|
||||||
|
visitDate: defaultValues?.visitDate || new Date().toISOString().split('T')[0],
|
||||||
|
productiveActivity: defaultValues?.productiveActivity || '',
|
||||||
|
financialRequirementDescription: defaultValues?.financialRequirementDescription || '',
|
||||||
|
siturCodeCommune: defaultValues?.siturCodeCommune || '',
|
||||||
|
communalCouncil: defaultValues?.communalCouncil || '',
|
||||||
|
siturCodeCommunalCouncil: defaultValues?.siturCodeCommunalCouncil || '',
|
||||||
|
ospName: defaultValues?.ospName || '',
|
||||||
|
ospAddress: defaultValues?.ospAddress || '',
|
||||||
|
ospRif: defaultValues?.ospRif || '',
|
||||||
|
ospType: defaultValues?.ospType || '',
|
||||||
|
currentStatus: defaultValues?.currentStatus || '',
|
||||||
|
companyConstitutionYear: defaultValues?.companyConstitutionYear || new Date().getFullYear(),
|
||||||
|
producerCount: defaultValues?.producerCount || 0,
|
||||||
|
productDescription: defaultValues?.productDescription || '',
|
||||||
|
installedCapacity: defaultValues?.installedCapacity || '',
|
||||||
|
operationalCapacity: defaultValues?.operationalCapacity || '',
|
||||||
|
ospResponsibleFullname: defaultValues?.ospResponsibleFullname || '',
|
||||||
|
ospResponsibleCedula: defaultValues?.ospResponsibleCedula || '',
|
||||||
|
ospResponsibleRif: defaultValues?.ospResponsibleRif || '',
|
||||||
|
ospResponsiblePhone: defaultValues?.ospResponsiblePhone || '',
|
||||||
|
civilState: defaultValues?.civilState || '',
|
||||||
|
familyBurden: defaultValues?.familyBurden || 0,
|
||||||
|
numberOfChildren: defaultValues?.numberOfChildren || 0,
|
||||||
|
generalObservations: defaultValues?.generalObservations || '',
|
||||||
|
ospResponsibleEmail: defaultValues?.ospResponsibleEmail || '',
|
||||||
|
photo1: defaultValues?.photo1 || '',
|
||||||
|
photo2: defaultValues?.photo2 || '',
|
||||||
|
photo3: defaultValues?.photo3 || '',
|
||||||
|
paralysisReason: defaultValues?.paralysisReason || '',
|
||||||
|
state: defaultValues?.state || undefined,
|
||||||
|
municipality: defaultValues?.municipality || undefined,
|
||||||
|
parish: defaultValues?.parish || undefined,
|
||||||
|
},
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (formData: TrainingSchema) => {
|
||||||
|
saveTraining(formData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
form.reset();
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
console.error(e);
|
||||||
|
form.setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: 'Error al guardar el registro',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{form.formState.errors.root && (
|
||||||
|
<div className="text-destructive text-sm">
|
||||||
|
{form.formState.errors.root.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Datos Personales */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<h3 className="text-lg font-medium mb-2">Datos Básicos</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField control={form.control} name="firstname" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="lastname" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Apellido</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="visitDate" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Fecha de la visita</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={field.value ? new Date(field.value).toISOString().split('T')[0] : ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
// Convert YYYY-MM-DD to ISO 8601 string
|
||||||
|
const dateValue = e.target.value;
|
||||||
|
if (dateValue) {
|
||||||
|
field.onChange(new Date(dateValue).toISOString());
|
||||||
|
} else {
|
||||||
|
field.onChange('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
{/* Ubicación */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<h3 className="text-lg font-medium mb-2 mt-4">Ubicación</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="state"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Estado</FormLabel>
|
||||||
|
|
||||||
|
<SelectSearchable
|
||||||
|
options={
|
||||||
|
stateOptions?.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
onValueChange={(value: any) => { field.onChange(Number(value)); setState(value); setDisabledMunicipality(false); setDisabledParish(true) }
|
||||||
|
}
|
||||||
|
placeholder="Selecciona un estado"
|
||||||
|
defaultValue={field.value?.toString()}
|
||||||
|
// disabled={readOnly}
|
||||||
|
/>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="municipality"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Municipio</FormLabel>
|
||||||
|
|
||||||
|
<SelectSearchable
|
||||||
|
options={
|
||||||
|
municipalityOptions?.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
onValueChange={(value: any) => { field.onChange(Number(value)); setMunicipality(value); setDisabledParish(false) }
|
||||||
|
}
|
||||||
|
placeholder="Selecciona un Municipio"
|
||||||
|
defaultValue={field.value?.toString()}
|
||||||
|
disabled={disabledMunicipality}
|
||||||
|
/>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="parish"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Parroquia</FormLabel>
|
||||||
|
|
||||||
|
<SelectSearchable
|
||||||
|
options={
|
||||||
|
parishOptions?.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
onValueChange={(value: any) =>
|
||||||
|
field.onChange(Number(value))
|
||||||
|
}
|
||||||
|
placeholder="Selecciona una Parroquia"
|
||||||
|
defaultValue={field.value?.toString()}
|
||||||
|
disabled={disabledParish}
|
||||||
|
/>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <FormField control={form.control} name="state" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Estado</FormLabel>
|
||||||
|
<FormControl><Input {...field} value={field.value || ''} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="municipality" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Municipio</FormLabel>
|
||||||
|
<FormControl><Input {...field} value={field.value || ''} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="parish" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Parroquia</FormLabel>
|
||||||
|
<FormControl><Input {...field} value={field.value || ''} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} /> */}
|
||||||
|
|
||||||
|
<FormField control={form.control} name="siturCodeCommune" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Código SITUR Comuna</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="communalCouncil" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Consejo Comunal</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="siturCodeCommunalCouncil" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Código SITUR Consejo Comunal</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
{/* Datos de la OSP */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<h3 className="text-lg font-medium mb-2 mt-4">Datos de la Organización Socioproductiva (OSP)</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospName" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre de la Organización</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospAddress" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Dirección</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospRif" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>RIF</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospType" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Tipo de Organización</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="productiveActivity" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Actividad Productiva</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Seleccione actividad" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{PRODUCTIVE_ACTIVITIES.map((activity) => (
|
||||||
|
<SelectItem key={activity} value={activity}>
|
||||||
|
{activity}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="currentStatus" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Estatus Actual</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="companyConstitutionYear" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Año de Constitución</FormLabel>
|
||||||
|
<FormControl><Input type="number" {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="producerCount" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Cantidad de Productores</FormLabel>
|
||||||
|
<FormControl><Input type="number" {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="productDescription" render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Breve descripción del producto o servicio</FormLabel>
|
||||||
|
<FormControl><Textarea {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="installedCapacity" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Capacidad Instalada</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="operationalCapacity" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Capacidad Operativa</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="financialRequirementDescription" render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Descripción del Requerimiento Financiero</FormLabel>
|
||||||
|
<FormControl><Textarea {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="paralysisReason" render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Razones de paralización (si aplica)</FormLabel>
|
||||||
|
<FormControl><Textarea {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
{/* Responsable */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<h3 className="text-lg font-medium mb-2 mt-4">Datos del Responsable</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospResponsibleFullname" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre y Apellido</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospResponsibleCedula" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Cédula (sin puntos)</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospResponsibleRif" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>RIF (sin puntos)</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospResponsiblePhone" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Teléfonos (2 números)</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="civilState" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Estado Civil</FormLabel>
|
||||||
|
<FormControl><Input {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="ospResponsibleEmail" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Correo Electrónico</FormLabel>
|
||||||
|
<FormControl><Input type="email" {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="familyBurden" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Carga Familiar</FormLabel>
|
||||||
|
<FormControl><Input type="number" {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="numberOfChildren" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Número de Hijos</FormLabel>
|
||||||
|
<FormControl><Input type="number" {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
{/* datos adicionales */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<h3 className="text-lg font-medium mb-2 mt-4">Datos Adicionales</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField control={form.control} name="generalObservations" render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Observaciones Generales</FormLabel>
|
||||||
|
<FormControl><Textarea {...field} /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
{/* Fotos */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<h3 className="text-lg font-medium mb-2 mt-4">Registro Fotográfico (URLs)</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField control={form.control} name="photo1" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Foto 1</FormLabel>
|
||||||
|
<FormControl><Input {...field} placeholder="URL de la imagen" /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="photo2" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Foto 2</FormLabel>
|
||||||
|
<FormControl><Input {...field} placeholder="URL de la imagen" /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
<FormField control={form.control} name="photo3" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Foto 3</FormLabel>
|
||||||
|
<FormControl><Input {...field} placeholder="URL de la imagen" /></FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4 mt-6">
|
||||||
|
<Button variant="outline" type="button" onClick={onCancel}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSaving}>
|
||||||
|
{isSaving ? 'Guardando...' : 'Guardar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
319
apps/web/feactures/training/components/training-statistics.tsx
Normal file
319
apps/web/feactures/training/components/training-statistics.tsx
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/shadcn/card';
|
||||||
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
|
||||||
|
import { useTrainingStatsQuery } from '../hooks/use-training-statistics';
|
||||||
|
import { Input } from '@repo/shadcn/input';
|
||||||
|
import { Button } from '@repo/shadcn/button';
|
||||||
|
import { SelectSearchable } from '@repo/shadcn/select-searchable';
|
||||||
|
import { useStateQuery, useMunicipalityQuery, useParishQuery } from '@/feactures/location/hooks/use-query-location';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@repo/shadcn/select';
|
||||||
|
|
||||||
|
const OSP_TYPES = [
|
||||||
|
'EPSD',
|
||||||
|
'EPSI',
|
||||||
|
'UPF',
|
||||||
|
'Cooperativa',
|
||||||
|
'Grupo de Intercambio',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function TrainingStatistics() {
|
||||||
|
// Filter State
|
||||||
|
const [startDate, setStartDate] = useState<string>('');
|
||||||
|
const [endDate, setEndDate] = useState<string>('');
|
||||||
|
const [stateId, setStateId] = useState<number>(0);
|
||||||
|
const [municipalityId, setMunicipalityId] = useState<number>(0);
|
||||||
|
const [parishId, setParishId] = useState<number>(0);
|
||||||
|
const [ospType, setOspType] = useState<string>('');
|
||||||
|
|
||||||
|
// Location Data
|
||||||
|
const { data: dataState } = useStateQuery();
|
||||||
|
const { data: dataMunicipality } = useMunicipalityQuery(stateId);
|
||||||
|
const { data: dataParish } = useParishQuery(municipalityId);
|
||||||
|
|
||||||
|
const stateOptions = dataState?.data || [{ id: 0, name: 'Sin estados' }];
|
||||||
|
const municipalityOptions = Array.isArray(dataMunicipality?.data) && dataMunicipality.data.length > 0
|
||||||
|
? dataMunicipality.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Municipios' }];
|
||||||
|
const parishOptions = Array.isArray(dataParish?.data) && dataParish.data.length > 0
|
||||||
|
? dataParish.data
|
||||||
|
: [{ id: 0, stateId: 0, name: 'Sin Parroquias' }];
|
||||||
|
|
||||||
|
// Query with Filters
|
||||||
|
const { data, isLoading, refetch } = useTrainingStatsQuery({
|
||||||
|
startDate: startDate || undefined,
|
||||||
|
endDate: endDate || undefined,
|
||||||
|
stateId: stateId || undefined,
|
||||||
|
municipalityId: municipalityId || undefined,
|
||||||
|
parishId: parishId || undefined,
|
||||||
|
ospType: ospType || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
setStartDate('');
|
||||||
|
setEndDate('');
|
||||||
|
setStateId(0);
|
||||||
|
setMunicipalityId(0);
|
||||||
|
setParishId(0);
|
||||||
|
setOspType('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="flex justify-center p-8">Cargando estadísticas...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <div className="flex justify-center p-8">No hay datos disponibles.</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { totalOsps, totalProducers, statusDistribution, activityDistribution, typeDistribution, stateDistribution, yearDistribution } = data;
|
||||||
|
|
||||||
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filters Section */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filtros</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Fecha Inicio</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Fecha Fin</label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Estado</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={stateOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setStateId(Number(value));
|
||||||
|
setMunicipalityId(0); // Reset municipality
|
||||||
|
setParishId(0); // Reset parish
|
||||||
|
}}
|
||||||
|
placeholder="Selecciona un estado"
|
||||||
|
defaultValue={stateId ? stateId.toString() : ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Municipio</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={municipalityOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => {
|
||||||
|
setMunicipalityId(Number(value));
|
||||||
|
setParishId(0);
|
||||||
|
}}
|
||||||
|
placeholder="Selecciona municipio"
|
||||||
|
defaultValue={municipalityId ? municipalityId.toString() : ""}
|
||||||
|
disabled={!stateId || stateId === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Parroquia</label>
|
||||||
|
<SelectSearchable
|
||||||
|
options={parishOptions.map((item) => ({
|
||||||
|
value: item.id.toString(),
|
||||||
|
label: item.name,
|
||||||
|
}))}
|
||||||
|
onValueChange={(value: any) => setParishId(Number(value))}
|
||||||
|
placeholder="Selecciona parroquia"
|
||||||
|
defaultValue={parishId ? parishId.toString() : ""}
|
||||||
|
disabled={!municipalityId || municipalityId === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Tipo de OSP</label>
|
||||||
|
<Select value={ospType} onValueChange={setOspType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Todos" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
|
{OSP_TYPES.map(type => (
|
||||||
|
<SelectItem key={type} value={type}>{type}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button variant="outline" onClick={handleClearFilters}>
|
||||||
|
Limpiar Filtros
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total de OSP Registradas</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalOsps}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Organizaciones Socioproductivas
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total de Productores</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{totalProducers}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Productores asociados
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Actividad Productiva</CardTitle>
|
||||||
|
<CardDescription>Distribución por tipo de actividad</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={activityDistribution}
|
||||||
|
layout="vertical"
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" />
|
||||||
|
<YAxis dataKey="name" type="category" width={150} />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#8884d8" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* State Distribution */}
|
||||||
|
<Card className="col-span-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Distribución por Estado</CardTitle>
|
||||||
|
<CardDescription>OSP registradas por estado</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={stateDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#00C49F" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Year Distribution */}
|
||||||
|
<Card className="col-span-full lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Año de Constitución</CardTitle>
|
||||||
|
<CardDescription>Año de registro de la empresa</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={yearDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#FFBB28" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Estatus Actual</CardTitle>
|
||||||
|
<CardDescription>Estado operativo de las OSP</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={statusDistribution}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{statusDistribution.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Tipo de Organización</CardTitle>
|
||||||
|
<CardDescription>Clasificación de las OSP</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="h-[300px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={typeDistribution}
|
||||||
|
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip wrapperStyle={{ color: '#000' }} />
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="value" fill="#82ca9d" name="Cantidad" />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
apps/web/feactures/training/hooks/use-training-statistics.ts
Normal file
13
apps/web/feactures/training/hooks/use-training-statistics.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useSafeQuery } from '@/hooks/use-safe-query';
|
||||||
|
import { getTrainingStatisticsAction } from '../actions/training-actions';
|
||||||
|
|
||||||
|
export function useTrainingStatsQuery(params: {
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
stateId?: number;
|
||||||
|
municipalityId?: number;
|
||||||
|
parishId?: number;
|
||||||
|
ospType?: string;
|
||||||
|
} = {}) {
|
||||||
|
return useSafeQuery(['training-statistics', JSON.stringify(params)], () => getTrainingStatisticsAction(params));
|
||||||
|
}
|
||||||
29
apps/web/feactures/training/hooks/use-training.ts
Normal file
29
apps/web/feactures/training/hooks/use-training.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { TrainingSchema } from "../schemas/training";
|
||||||
|
import { createTrainingAction, updateTrainingAction, deleteTrainingAction } from "../actions/training-actions";
|
||||||
|
|
||||||
|
export function useCreateTraining() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (data: TrainingSchema) => createTrainingAction(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
|
})
|
||||||
|
return mutation
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateTraining() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (data: TrainingSchema) => updateTrainingAction(data),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
|
})
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteTraining() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => deleteTrainingAction(id),
|
||||||
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['training'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
23
apps/web/feactures/training/schemas/statistics.ts
Normal file
23
apps/web/feactures/training/schemas/statistics.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const statisticsItemSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
value: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const trainingStatisticsSchema = z.object({
|
||||||
|
totalOsps: z.number(),
|
||||||
|
totalProducers: z.number(),
|
||||||
|
statusDistribution: z.array(statisticsItemSchema),
|
||||||
|
activityDistribution: z.array(statisticsItemSchema),
|
||||||
|
typeDistribution: z.array(statisticsItemSchema),
|
||||||
|
stateDistribution: z.array(statisticsItemSchema),
|
||||||
|
yearDistribution: z.array(statisticsItemSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TrainingStatisticsData = z.infer<typeof trainingStatisticsSchema>;
|
||||||
|
|
||||||
|
export const trainingStatisticsResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: trainingStatisticsSchema,
|
||||||
|
});
|
||||||
61
apps/web/feactures/training/schemas/training.ts
Normal file
61
apps/web/feactures/training/schemas/training.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TrainingSchema = z.infer<typeof trainingSchema>;
|
||||||
|
|
||||||
|
export const trainingApiResponseSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: z.array(trainingSchema),
|
||||||
|
meta: z.object({
|
||||||
|
page: z.number(),
|
||||||
|
limit: z.number(),
|
||||||
|
totalCount: z.number(),
|
||||||
|
totalPages: z.number(),
|
||||||
|
hasNextPage: z.boolean(),
|
||||||
|
hasPreviousPage: z.boolean(),
|
||||||
|
nextPage: z.number().nullable(),
|
||||||
|
previousPage: z.number().nullable(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TrainingMutate = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
data: trainingSchema,
|
||||||
|
});
|
||||||
@@ -78,9 +78,7 @@ export function ModalForm({
|
|||||||
parish: undefined
|
parish: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log(defaultValues);
|
||||||
|
|
||||||
console.log(defaultValues);
|
|
||||||
|
|
||||||
const form = useForm<UpdateUser>({
|
const form = useForm<UpdateUser>({
|
||||||
resolver: zodResolver(updateUser),
|
resolver: zodResolver(updateUser),
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
"add:api": "pnpm add --filter=api",
|
"add:api": "pnpm add --filter=api",
|
||||||
"add:web": "pnpm add --filter=web",
|
"add:web": "pnpm add --filter=web",
|
||||||
"build": "turbo build",
|
"build": "turbo build",
|
||||||
|
"build:api": "pnpm build --filter=api",
|
||||||
|
"build:web": "pnpm build --filter=web",
|
||||||
"changeset": "changeset",
|
"changeset": "changeset",
|
||||||
"clear:modules": "npx npkill",
|
"clear:modules": "npx npkill",
|
||||||
"commit": "cz",
|
"commit": "cz",
|
||||||
@@ -18,9 +20,7 @@
|
|||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"start": "turbo start",
|
"start": "turbo start",
|
||||||
"test": "turbo test",
|
"test": "turbo test"
|
||||||
"build:api": "pnpm build --filter=api",
|
|
||||||
"build:web": "pnpm build --filter=web"
|
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
|
|||||||
Reference in New Issue
Block a user