feat: agregar generación de reportes PDF y recomendaciones para PDVs sin asignar
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
import { Employee, EmployeeInput } from '../../core/models/employee.model';
|
||||
import { EmployeeService } from '../../core/services/employee.service';
|
||||
@@ -31,19 +31,32 @@ export class EmployeesComponent implements OnDestroy, OnInit {
|
||||
importing = false;
|
||||
openRestDaysFor: string | null = null;
|
||||
readonly schedules = WORK_SCHEDULES;
|
||||
selectedIds = new Set<string>();
|
||||
bulkSchedule = '';
|
||||
bulkRestHours: number | null = null;
|
||||
bulkVisits: number | null = null;
|
||||
bulkRestDays = new Set<string>();
|
||||
bulkSaving = false;
|
||||
private destroyed = false;
|
||||
private loadSequence = 0;
|
||||
projectId = '';
|
||||
|
||||
constructor(
|
||||
private employeesService: EmployeeService,
|
||||
private auth: AuthService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private location: Location,
|
||||
public profile: UserProfileService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectId = this.activatedRoute.snapshot.paramMap.get('id') ?? '';
|
||||
if (!this.projectId) {
|
||||
void this.router.navigate(['/home']);
|
||||
return;
|
||||
}
|
||||
void this.load();
|
||||
}
|
||||
|
||||
@@ -60,14 +73,23 @@ export class EmployeesComponent implements OnDestroy, OnInit {
|
||||
return workSchedule(this.form.jornada_codigo).weeklyHours;
|
||||
}
|
||||
|
||||
get allSelected(): boolean {
|
||||
return this.employees.length > 0 && this.employees.every(employee => this.selectedIds.has(employee.id));
|
||||
}
|
||||
|
||||
get selectedEmployees(): Employee[] {
|
||||
return this.employees.filter(employee => this.selectedIds.has(employee.id));
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
const sequence = ++this.loadSequence;
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const employees = await this.employeesService.list();
|
||||
const employees = await this.employeesService.list(this.projectId);
|
||||
if (this.destroyed || sequence !== this.loadSequence) return;
|
||||
this.employees = employees;
|
||||
this.selectedIds.clear();
|
||||
this.cdr.detectChanges();
|
||||
} catch (error: any) {
|
||||
if (!this.destroyed && sequence === this.loadSequence) {
|
||||
@@ -144,7 +166,7 @@ export class EmployeesComponent implements OnDestroy, OnInit {
|
||||
};
|
||||
try {
|
||||
if (this.editingId) await this.employeesService.update(this.editingId, employee);
|
||||
else await this.employeesService.create(employee);
|
||||
else await this.employeesService.create(this.projectId, employee);
|
||||
this.cancelEdit();
|
||||
await this.load();
|
||||
} catch (error: any) {
|
||||
@@ -163,6 +185,90 @@ export class EmployeesComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelection(id: string, checked: boolean): void {
|
||||
checked ? this.selectedIds.add(id) : this.selectedIds.delete(id);
|
||||
}
|
||||
|
||||
toggleAll(checked: boolean): void {
|
||||
this.selectedIds = checked ? new Set(this.employees.map(employee => employee.id)) : new Set<string>();
|
||||
}
|
||||
|
||||
toggleBulkRestDay(day: string, checked: boolean): void {
|
||||
checked ? this.bulkRestDays.add(day) : this.bulkRestDays.delete(day);
|
||||
}
|
||||
|
||||
async applyBulkEmployeeChanges(): Promise<void> {
|
||||
const selected = this.selectedEmployees;
|
||||
if (!selected.length) return;
|
||||
if (!this.bulkSchedule && this.bulkRestHours === null && this.bulkVisits === null && !this.bulkRestDays.size) {
|
||||
this.error = 'Selecciona al menos un cambio masivo para aplicar.';
|
||||
return;
|
||||
}
|
||||
if (this.bulkRestHours !== null && (!Number.isInteger(this.bulkRestHours) || this.bulkRestHours < 0 || this.bulkRestHours > 3)) {
|
||||
this.error = 'Descanso/día debe ser un entero entre 0 y 3.';
|
||||
return;
|
||||
}
|
||||
if (this.bulkVisits !== null && (!Number.isInteger(this.bulkVisits) || this.bulkVisits < 1)) {
|
||||
this.error = 'Visitas/día debe ser un entero mayor que 0.';
|
||||
return;
|
||||
}
|
||||
this.bulkSaving = true;
|
||||
this.error = '';
|
||||
try {
|
||||
await Promise.all(selected.map(employee => {
|
||||
const schedule = this.bulkSchedule || employee.jornada_codigo;
|
||||
const restDays = this.bulkRestDays.size
|
||||
? Array.from(this.bulkRestDays).filter(day => workSchedule(schedule).days.includes(day))
|
||||
: (this.bulkSchedule ? employee.dias_descanso.filter(day => workSchedule(schedule).days.includes(day)) : employee.dias_descanso);
|
||||
return this.employeesService.update(employee.id, {
|
||||
...employee,
|
||||
jornada_codigo: schedule,
|
||||
dias_por_semana: workSchedule(schedule).days.length,
|
||||
dias_descanso: restDays,
|
||||
horas_descanso_por_dia: this.bulkRestHours ?? employee.horas_descanso_por_dia,
|
||||
visitas_maximas_por_dia: this.bulkVisits ?? employee.visitas_maximas_por_dia
|
||||
});
|
||||
}));
|
||||
this.importMessage = `${selected.length} empleados actualizados.`;
|
||||
this.resetBulkFields();
|
||||
await this.load();
|
||||
} catch (error: any) {
|
||||
this.error = error?.message ?? 'No se pudieron aplicar los cambios masivos.';
|
||||
} finally {
|
||||
this.bulkSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async setSelectedActive(active: boolean): Promise<void> {
|
||||
const selected = this.selectedEmployees;
|
||||
if (!selected.length) return;
|
||||
this.bulkSaving = true;
|
||||
try {
|
||||
await Promise.all(selected.map(employee => this.employeesService.update(employee.id, { ...employee, activo: active })));
|
||||
this.importMessage = `${selected.length} empleados ${active ? 'activados' : 'desactivados'}.`;
|
||||
await this.load();
|
||||
} catch (error: any) {
|
||||
this.error = error?.message ?? 'No se pudo actualizar el estado de los empleados.';
|
||||
} finally {
|
||||
this.bulkSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async removeSelected(): Promise<void> {
|
||||
const selected = this.selectedEmployees;
|
||||
if (!selected.length || !window.confirm(`¿Eliminar ${selected.length} empleados seleccionados? Esta acción no se puede deshacer.`)) return;
|
||||
this.bulkSaving = true;
|
||||
try {
|
||||
await Promise.all(selected.map(employee => this.employeesService.remove(employee.id)));
|
||||
this.importMessage = `${selected.length} empleados eliminados.`;
|
||||
await this.load();
|
||||
} catch (error: any) {
|
||||
this.error = error?.message ?? 'No se pudieron eliminar los empleados.';
|
||||
} finally {
|
||||
this.bulkSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleLabel(employee: Employee): string {
|
||||
return workSchedule(employee.jornada_codigo).label;
|
||||
}
|
||||
@@ -195,7 +301,7 @@ export class EmployeesComponent implements OnDestroy, OnInit {
|
||||
const invalid = employees.filter((employee): employee is null => employee === null).length;
|
||||
const valid = employees.filter((employee): employee is EmployeeInput => employee !== null);
|
||||
if (!valid.length) throw new Error('El archivo no contiene empleados válidos.');
|
||||
await this.employeesService.upsert(valid);
|
||||
await this.employeesService.upsert(this.projectId, valid);
|
||||
this.importMessage = `${valid.length} empleados guardados${invalid ? `; ${invalid} filas omitidas por datos inválidos` : ''}.`;
|
||||
await this.load();
|
||||
} catch (error: any) {
|
||||
@@ -226,18 +332,25 @@ export class EmployeesComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
async goHome(): Promise<void> {
|
||||
await this.router.navigate(['/home']);
|
||||
await this.router.navigate(['/proyectos', this.projectId]);
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
if (history.length > 1) this.location.back();
|
||||
else void this.router.navigate(['/home']);
|
||||
else void this.router.navigate(['/proyectos', this.projectId]);
|
||||
}
|
||||
|
||||
private nullable(value: string | null): string | null {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
private resetBulkFields(): void {
|
||||
this.bulkSchedule = '';
|
||||
this.bulkRestHours = null;
|
||||
this.bulkVisits = null;
|
||||
this.bulkRestDays.clear();
|
||||
}
|
||||
|
||||
private emptyForm(): EmployeeForm {
|
||||
return {
|
||||
employee_code: '', nombre: '', usuario: '', email: '', perfil: 'Promotor', pais: 'Republica Dominicana', regional: '',
|
||||
|
||||
Reference in New Issue
Block a user