[MAI-009] feat: Complete quality module with schema fixes and ChecklistService
- Fix 8 entities schema from 'quality' to 'construction' to match DDL - Create ChecklistService with full CRUD, duplication, item management - Fix InspectionService and TicketService to use local ServiceContext - Fix InspectionController and TicketController pagination structure - Update services/index.ts to export ChecklistService Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
63013bb2f4
commit
60782a3cac
@ -19,7 +19,11 @@ import { AuthService } from '../../auth/services/auth.service';
|
|||||||
import { User } from '../../core/entities/user.entity';
|
import { User } from '../../core/entities/user.entity';
|
||||||
import { Tenant } from '../../core/entities/tenant.entity';
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||||
import { ServiceContext } from '../../../shared/services/base.service';
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function createInspectionController(dataSource: DataSource): Router {
|
export function createInspectionController(dataSource: DataSource): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -73,7 +77,7 @@ export function createInspectionController(dataSource: DataSource): Router {
|
|||||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||||
|
|
||||||
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||||
res.status(200).json({ success: true, data: result.data, pagination: result.meta });
|
res.status(200).json({ success: true, data: result.data, pagination: { total: result.total, page: result.page, limit: result.limit } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,11 @@ import { AuthService } from '../../auth/services/auth.service';
|
|||||||
import { User } from '../../core/entities/user.entity';
|
import { User } from '../../core/entities/user.entity';
|
||||||
import { Tenant } from '../../core/entities/tenant.entity';
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
||||||
import { ServiceContext } from '../../../shared/services/base.service';
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function createTicketController(dataSource: DataSource): Router {
|
export function createTicketController(dataSource: DataSource): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@ -72,7 +76,7 @@ export function createTicketController(dataSource: DataSource): Router {
|
|||||||
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
||||||
|
|
||||||
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
const result = await service.findWithFilters(getContext(req), filters, page, limit);
|
||||||
res.status(200).json({ success: true, data: result.data, pagination: result.meta });
|
res.status(200).json({ success: true, data: result.data, pagination: { total: result.total, page: result.page, limit: result.limit } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
import { Tenant } from '../../core/entities/tenant.entity';
|
import { Tenant } from '../../core/entities/tenant.entity';
|
||||||
import { Checklist } from './checklist.entity';
|
import { Checklist } from './checklist.entity';
|
||||||
|
|
||||||
@Entity({ schema: 'quality', name: 'checklist_items' })
|
@Entity({ schema: 'construction', name: 'checklist_items' })
|
||||||
@Index(['tenantId', 'checklistId', 'sequenceNumber'])
|
@Index(['tenantId', 'checklistId', 'sequenceNumber'])
|
||||||
export class ChecklistItem {
|
export class ChecklistItem {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import { Inspection } from './inspection.entity';
|
|||||||
|
|
||||||
export type ChecklistStage = 'foundation' | 'structure' | 'installations' | 'finishes' | 'delivery' | 'custom';
|
export type ChecklistStage = 'foundation' | 'structure' | 'installations' | 'finishes' | 'delivery' | 'custom';
|
||||||
|
|
||||||
@Entity({ schema: 'quality', name: 'checklists' })
|
@Entity({ schema: 'construction', name: 'checklists' })
|
||||||
@Index(['tenantId', 'code'], { unique: true })
|
@Index(['tenantId', 'code'], { unique: true })
|
||||||
export class Checklist {
|
export class Checklist {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { NonConformity } from './non-conformity.entity';
|
|||||||
export type ActionType = 'corrective' | 'preventive' | 'improvement';
|
export type ActionType = 'corrective' | 'preventive' | 'improvement';
|
||||||
export type ActionStatus = 'pending' | 'in_progress' | 'completed' | 'verified';
|
export type ActionStatus = 'pending' | 'in_progress' | 'completed' | 'verified';
|
||||||
|
|
||||||
@Entity({ schema: 'quality', name: 'corrective_actions' })
|
@Entity({ schema: 'construction', name: 'acciones_correctivas' })
|
||||||
@Index(['tenantId', 'nonConformityId'])
|
@Index(['tenantId', 'nonConformityId'])
|
||||||
export class CorrectiveAction {
|
export class CorrectiveAction {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@ -32,7 +32,7 @@ export class CorrectiveAction {
|
|||||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
|
|
||||||
@Column({ name: 'non_conformity_id', type: 'uuid' })
|
@Column({ name: 'no_conformidad_id', type: 'uuid' })
|
||||||
nonConformityId: string;
|
nonConformityId: string;
|
||||||
|
|
||||||
@Column({ name: 'action_type', type: 'varchar', length: 20 })
|
@Column({ name: 'action_type', type: 'varchar', length: 20 })
|
||||||
@ -83,7 +83,7 @@ export class CorrectiveAction {
|
|||||||
tenant: Tenant;
|
tenant: Tenant;
|
||||||
|
|
||||||
@ManyToOne(() => NonConformity, (nc) => nc.correctiveActions, { onDelete: 'CASCADE' })
|
@ManyToOne(() => NonConformity, (nc) => nc.correctiveActions, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'non_conformity_id' })
|
@JoinColumn({ name: 'no_conformidad_id' })
|
||||||
nonConformity: NonConformity;
|
nonConformity: NonConformity;
|
||||||
|
|
||||||
@ManyToOne(() => User)
|
@ManyToOne(() => User)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { ChecklistItem } from './checklist-item.entity';
|
|||||||
|
|
||||||
export type InspectionResultStatus = 'pending' | 'passed' | 'failed' | 'not_applicable';
|
export type InspectionResultStatus = 'pending' | 'passed' | 'failed' | 'not_applicable';
|
||||||
|
|
||||||
@Entity({ schema: 'quality', name: 'inspection_results' })
|
@Entity({ schema: 'construction', name: 'inspeccion_resultados' })
|
||||||
@Index(['tenantId', 'inspectionId', 'checklistItemId'], { unique: true })
|
@Index(['tenantId', 'inspectionId', 'checklistItemId'], { unique: true })
|
||||||
export class InspectionResult {
|
export class InspectionResult {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { NonConformity } from './non-conformity.entity';
|
|||||||
|
|
||||||
export type InspectionStatus = 'pending' | 'in_progress' | 'completed' | 'approved' | 'rejected';
|
export type InspectionStatus = 'pending' | 'in_progress' | 'completed' | 'approved' | 'rejected';
|
||||||
|
|
||||||
@Entity({ schema: 'quality', name: 'inspections' })
|
@Entity({ schema: 'construction', name: 'inspecciones' })
|
||||||
@Index(['tenantId', 'inspectionNumber'], { unique: true })
|
@Index(['tenantId', 'inspectionNumber'], { unique: true })
|
||||||
export class Inspection {
|
export class Inspection {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { CorrectiveAction } from './corrective-action.entity';
|
|||||||
export type NCSeverity = 'minor' | 'major' | 'critical';
|
export type NCSeverity = 'minor' | 'major' | 'critical';
|
||||||
export type NCStatus = 'open' | 'in_progress' | 'closed' | 'verified';
|
export type NCStatus = 'open' | 'in_progress' | 'closed' | 'verified';
|
||||||
|
|
||||||
@Entity({ schema: 'quality', name: 'non_conformities' })
|
@Entity({ schema: 'construction', name: 'no_conformidades' })
|
||||||
@Index(['tenantId', 'ncNumber'], { unique: true })
|
@Index(['tenantId', 'ncNumber'], { unique: true })
|
||||||
export class NonConformity {
|
export class NonConformity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export type TicketPriority = 'urgent' | 'high' | 'medium' | 'low';
|
|||||||
export type TicketStatus = 'created' | 'assigned' | 'in_progress' | 'resolved' | 'closed' | 'cancelled';
|
export type TicketStatus = 'created' | 'assigned' | 'in_progress' | 'resolved' | 'closed' | 'cancelled';
|
||||||
export type TicketCategory = 'plumbing' | 'electrical' | 'finishes' | 'carpentry' | 'structural' | 'other';
|
export type TicketCategory = 'plumbing' | 'electrical' | 'finishes' | 'carpentry' | 'structural' | 'other';
|
||||||
|
|
||||||
@Entity({ schema: 'quality', name: 'post_sale_tickets' })
|
@Entity({ schema: 'construction', name: 'tickets_postventa' })
|
||||||
@Index(['tenantId', 'ticketNumber'], { unique: true })
|
@Index(['tenantId', 'ticketNumber'], { unique: true })
|
||||||
export class PostSaleTicket {
|
export class PostSaleTicket {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { PostSaleTicket } from './post-sale-ticket.entity';
|
|||||||
|
|
||||||
export type AssignmentStatus = 'assigned' | 'accepted' | 'in_progress' | 'completed' | 'reassigned';
|
export type AssignmentStatus = 'assigned' | 'accepted' | 'in_progress' | 'completed' | 'reassigned';
|
||||||
|
|
||||||
@Entity({ schema: 'quality', name: 'ticket_assignments' })
|
@Entity({ schema: 'construction', name: 'ticket_asignaciones' })
|
||||||
@Index(['tenantId', 'ticketId', 'technicianId'])
|
@Index(['tenantId', 'ticketId', 'technicianId'])
|
||||||
export class TicketAssignment {
|
export class TicketAssignment {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
409
src/modules/quality/services/checklist.service.ts
Normal file
409
src/modules/quality/services/checklist.service.ts
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
/**
|
||||||
|
* ChecklistService - Servicio de plantillas de inspección
|
||||||
|
*
|
||||||
|
* Gestión de checklists y sus items para inspecciones de calidad.
|
||||||
|
*
|
||||||
|
* @module Quality (MAI-009)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Checklist, ChecklistStage } from '../entities/checklist.entity';
|
||||||
|
import { ChecklistItem } from '../entities/checklist-item.entity';
|
||||||
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateChecklistDto {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
stage: ChecklistStage;
|
||||||
|
prototypeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateChecklistDto {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
stage?: ChecklistStage;
|
||||||
|
prototypeId?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateChecklistItemDto {
|
||||||
|
sequenceNumber: number;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
isCritical?: boolean;
|
||||||
|
requiresPhoto?: boolean;
|
||||||
|
acceptanceCriteria?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateChecklistItemDto {
|
||||||
|
sequenceNumber?: number;
|
||||||
|
category?: string;
|
||||||
|
description?: string;
|
||||||
|
isCritical?: boolean;
|
||||||
|
requiresPhoto?: boolean;
|
||||||
|
acceptanceCriteria?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChecklistFilters {
|
||||||
|
stage?: ChecklistStage;
|
||||||
|
prototypeId?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChecklistService {
|
||||||
|
constructor(
|
||||||
|
private readonly checklistRepository: Repository<Checklist>,
|
||||||
|
private readonly itemRepository: Repository<ChecklistItem>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(
|
||||||
|
ctx: ServiceContext,
|
||||||
|
filters: ChecklistFilters = {},
|
||||||
|
): Promise<{ data: Checklist[]; total: number; page: number; limit: number }> {
|
||||||
|
const page = filters.page || 1;
|
||||||
|
const limit = filters.limit || 20;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const queryBuilder = this.checklistRepository
|
||||||
|
.createQueryBuilder('cl')
|
||||||
|
.leftJoinAndSelect('cl.items', 'items', 'items.is_active = true')
|
||||||
|
.where('cl.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
||||||
|
.andWhere('cl.deleted_at IS NULL');
|
||||||
|
|
||||||
|
if (filters.stage) {
|
||||||
|
queryBuilder.andWhere('cl.stage = :stage', { stage: filters.stage });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.prototypeId) {
|
||||||
|
queryBuilder.andWhere('cl.prototype_id = :prototypeId', {
|
||||||
|
prototypeId: filters.prototypeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.isActive !== undefined) {
|
||||||
|
queryBuilder.andWhere('cl.is_active = :isActive', { isActive: filters.isActive });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.search) {
|
||||||
|
queryBuilder.andWhere(
|
||||||
|
'(cl.name ILIKE :search OR cl.code ILIKE :search OR cl.description ILIKE :search)',
|
||||||
|
{ search: `%${filters.search}%` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await queryBuilder
|
||||||
|
.orderBy('cl.stage', 'ASC')
|
||||||
|
.addOrderBy('cl.name', 'ASC')
|
||||||
|
.skip(skip)
|
||||||
|
.take(limit)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
return { data, total, page, limit };
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(ctx: ServiceContext, id: string): Promise<Checklist | null> {
|
||||||
|
return this.checklistRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
tenantId: ctx.tenantId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWithItems(ctx: ServiceContext, id: string): Promise<Checklist | null> {
|
||||||
|
return this.checklistRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
tenantId: ctx.tenantId,
|
||||||
|
},
|
||||||
|
relations: ['items', 'createdBy'],
|
||||||
|
order: { items: { sequenceNumber: 'ASC' } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByCode(ctx: ServiceContext, code: string): Promise<Checklist | null> {
|
||||||
|
return this.checklistRepository.findOne({
|
||||||
|
where: {
|
||||||
|
code,
|
||||||
|
tenantId: ctx.tenantId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByStage(ctx: ServiceContext, stage: ChecklistStage): Promise<Checklist[]> {
|
||||||
|
return this.checklistRepository
|
||||||
|
.createQueryBuilder('cl')
|
||||||
|
.leftJoinAndSelect('cl.items', 'items', 'items.is_active = true')
|
||||||
|
.where('cl.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
||||||
|
.andWhere('cl.deleted_at IS NULL')
|
||||||
|
.andWhere('cl.is_active = true')
|
||||||
|
.andWhere('cl.stage = :stage', { stage })
|
||||||
|
.orderBy('cl.name', 'ASC')
|
||||||
|
.addOrderBy('items.sequence_number', 'ASC')
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(ctx: ServiceContext, dto: CreateChecklistDto): Promise<Checklist> {
|
||||||
|
// Check code uniqueness
|
||||||
|
const existing = await this.findByCode(ctx, dto.code);
|
||||||
|
if (existing) {
|
||||||
|
throw new Error(`Checklist with code ${dto.code} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const checklist = this.checklistRepository.create({
|
||||||
|
tenantId: ctx.tenantId,
|
||||||
|
createdById: ctx.userId,
|
||||||
|
code: dto.code,
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description,
|
||||||
|
stage: dto.stage,
|
||||||
|
prototypeId: dto.prototypeId,
|
||||||
|
isActive: true,
|
||||||
|
version: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.checklistRepository.save(checklist);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
ctx: ServiceContext,
|
||||||
|
id: string,
|
||||||
|
dto: UpdateChecklistDto,
|
||||||
|
): Promise<Checklist | null> {
|
||||||
|
const checklist = await this.findById(ctx, id);
|
||||||
|
if (!checklist) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(checklist, {
|
||||||
|
...dto,
|
||||||
|
updatedById: ctx.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.checklistRepository.save(checklist);
|
||||||
|
}
|
||||||
|
|
||||||
|
async incrementVersion(ctx: ServiceContext, id: string): Promise<Checklist | null> {
|
||||||
|
const checklist = await this.findById(ctx, id);
|
||||||
|
if (!checklist) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
checklist.version += 1;
|
||||||
|
if (ctx.userId) {
|
||||||
|
checklist.updatedById = ctx.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.checklistRepository.save(checklist);
|
||||||
|
}
|
||||||
|
|
||||||
|
async activate(ctx: ServiceContext, id: string): Promise<Checklist | null> {
|
||||||
|
return this.update(ctx, id, { isActive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deactivate(ctx: ServiceContext, id: string): Promise<Checklist | null> {
|
||||||
|
return this.update(ctx, id, { isActive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async duplicate(ctx: ServiceContext, id: string, newCode: string, newName: string): Promise<Checklist | null> {
|
||||||
|
const original = await this.findWithItems(ctx, id);
|
||||||
|
if (!original) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check new code uniqueness
|
||||||
|
const existing = await this.findByCode(ctx, newCode);
|
||||||
|
if (existing) {
|
||||||
|
throw new Error(`Checklist with code ${newCode} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new checklist
|
||||||
|
const newChecklist = this.checklistRepository.create({
|
||||||
|
tenantId: ctx.tenantId,
|
||||||
|
createdById: ctx.userId,
|
||||||
|
code: newCode,
|
||||||
|
name: newName,
|
||||||
|
description: original.description,
|
||||||
|
stage: original.stage,
|
||||||
|
prototypeId: original.prototypeId,
|
||||||
|
isActive: true,
|
||||||
|
version: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedChecklist = await this.checklistRepository.save(newChecklist);
|
||||||
|
|
||||||
|
// Duplicate items
|
||||||
|
if (original.items && original.items.length > 0) {
|
||||||
|
const newItems = original.items.map((item) =>
|
||||||
|
this.itemRepository.create({
|
||||||
|
tenantId: ctx.tenantId,
|
||||||
|
checklistId: savedChecklist.id,
|
||||||
|
sequenceNumber: item.sequenceNumber,
|
||||||
|
category: item.category,
|
||||||
|
description: item.description,
|
||||||
|
isCritical: item.isCritical,
|
||||||
|
requiresPhoto: item.requiresPhoto,
|
||||||
|
acceptanceCriteria: item.acceptanceCriteria,
|
||||||
|
isActive: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await this.itemRepository.save(newItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.findWithItems(ctx, savedChecklist.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
|
||||||
|
const checklist = await this.findById(ctx, id);
|
||||||
|
if (!checklist) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: { deletedAt: Date; deletedById?: string } = { deletedAt: new Date() };
|
||||||
|
if (ctx.userId) {
|
||||||
|
updateData.deletedById = ctx.userId;
|
||||||
|
}
|
||||||
|
await this.checklistRepository.update(
|
||||||
|
{ id, tenantId: ctx.tenantId },
|
||||||
|
updateData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item management
|
||||||
|
|
||||||
|
async addItem(
|
||||||
|
ctx: ServiceContext,
|
||||||
|
checklistId: string,
|
||||||
|
dto: CreateChecklistItemDto,
|
||||||
|
): Promise<ChecklistItem> {
|
||||||
|
const checklist = await this.findById(ctx, checklistId);
|
||||||
|
if (!checklist) {
|
||||||
|
throw new Error('Checklist not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = this.itemRepository.create({
|
||||||
|
tenantId: ctx.tenantId,
|
||||||
|
checklistId,
|
||||||
|
sequenceNumber: dto.sequenceNumber,
|
||||||
|
category: dto.category,
|
||||||
|
description: dto.description,
|
||||||
|
isCritical: dto.isCritical ?? false,
|
||||||
|
requiresPhoto: dto.requiresPhoto ?? false,
|
||||||
|
acceptanceCriteria: dto.acceptanceCriteria,
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedItem = await this.itemRepository.save(item);
|
||||||
|
|
||||||
|
// Increment checklist version
|
||||||
|
await this.incrementVersion(ctx, checklistId);
|
||||||
|
|
||||||
|
return savedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateItem(
|
||||||
|
ctx: ServiceContext,
|
||||||
|
itemId: string,
|
||||||
|
dto: UpdateChecklistItemDto,
|
||||||
|
): Promise<ChecklistItem | null> {
|
||||||
|
const item = await this.itemRepository.findOne({
|
||||||
|
where: { id: itemId, tenantId: ctx.tenantId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(item, dto);
|
||||||
|
|
||||||
|
const savedItem = await this.itemRepository.save(item);
|
||||||
|
|
||||||
|
// Increment checklist version
|
||||||
|
await this.incrementVersion(ctx, item.checklistId);
|
||||||
|
|
||||||
|
return savedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeItem(ctx: ServiceContext, itemId: string): Promise<boolean> {
|
||||||
|
const item = await this.itemRepository.findOne({
|
||||||
|
where: { id: itemId, tenantId: ctx.tenantId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.itemRepository.delete({ id: itemId, tenantId: ctx.tenantId });
|
||||||
|
|
||||||
|
// Increment checklist version
|
||||||
|
await this.incrementVersion(ctx, item.checklistId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reorderItems(
|
||||||
|
ctx: ServiceContext,
|
||||||
|
checklistId: string,
|
||||||
|
itemOrders: { itemId: string; sequenceNumber: number }[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
const checklist = await this.findById(ctx, checklistId);
|
||||||
|
if (!checklist) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const order of itemOrders) {
|
||||||
|
await this.itemRepository.update(
|
||||||
|
{ id: order.itemId, tenantId: ctx.tenantId, checklistId },
|
||||||
|
{ sequenceNumber: order.sequenceNumber },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.incrementVersion(ctx, checklistId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatistics(ctx: ServiceContext): Promise<{
|
||||||
|
totalChecklists: number;
|
||||||
|
activeChecklists: number;
|
||||||
|
byStage: Record<string, number>;
|
||||||
|
totalItems: number;
|
||||||
|
criticalItems: number;
|
||||||
|
}> {
|
||||||
|
const checklists = await this.checklistRepository.find({
|
||||||
|
where: { tenantId: ctx.tenantId, deletedAt: undefined },
|
||||||
|
relations: ['items'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const byStage: Record<string, number> = {};
|
||||||
|
let totalItems = 0;
|
||||||
|
let criticalItems = 0;
|
||||||
|
|
||||||
|
for (const cl of checklists) {
|
||||||
|
byStage[cl.stage] = (byStage[cl.stage] || 0) + 1;
|
||||||
|
if (cl.items) {
|
||||||
|
totalItems += cl.items.length;
|
||||||
|
criticalItems += cl.items.filter((i) => i.isCritical).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalChecklists: checklists.length,
|
||||||
|
activeChecklists: checklists.filter((c) => c.isActive).length,
|
||||||
|
byStage,
|
||||||
|
totalItems,
|
||||||
|
criticalItems,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,5 +3,6 @@
|
|||||||
* @module Quality
|
* @module Quality
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from './checklist.service';
|
||||||
export * from './inspection.service';
|
export * from './inspection.service';
|
||||||
export * from './ticket.service';
|
export * from './ticket.service';
|
||||||
|
|||||||
@ -11,7 +11,11 @@ import { Inspection, InspectionStatus } from '../entities/inspection.entity';
|
|||||||
import { InspectionResult } from '../entities/inspection-result.entity';
|
import { InspectionResult } from '../entities/inspection-result.entity';
|
||||||
import { NonConformity } from '../entities/non-conformity.entity';
|
import { NonConformity } from '../entities/non-conformity.entity';
|
||||||
import { Checklist } from '../entities/checklist.entity';
|
import { Checklist } from '../entities/checklist.entity';
|
||||||
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateInspectionDto {
|
export interface CreateInspectionDto {
|
||||||
checklistId: string;
|
checklistId: string;
|
||||||
@ -66,7 +70,7 @@ export class InspectionService {
|
|||||||
filters: InspectionFilters = {},
|
filters: InspectionFilters = {},
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20
|
limit: number = 20
|
||||||
): Promise<PaginatedResult<Inspection>> {
|
): Promise<{ data: Inspection[]; total: number; page: number; limit: number }> {
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const queryBuilder = this.inspectionRepository
|
const queryBuilder = this.inspectionRepository
|
||||||
@ -106,15 +110,7 @@ export class InspectionService {
|
|||||||
|
|
||||||
const [data, total] = await queryBuilder.getManyAndCount();
|
const [data, total] = await queryBuilder.getManyAndCount();
|
||||||
|
|
||||||
return {
|
return { data, total, page, limit };
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
totalPages: Math.ceil(total / limit),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(ctx: ServiceContext, id: string): Promise<Inspection | null> {
|
async findById(ctx: ServiceContext, id: string): Promise<Inspection | null> {
|
||||||
|
|||||||
@ -9,7 +9,11 @@
|
|||||||
import { Repository, FindOptionsWhere, LessThan } from 'typeorm';
|
import { Repository, FindOptionsWhere, LessThan } from 'typeorm';
|
||||||
import { PostSaleTicket, TicketStatus, TicketPriority, TicketCategory } from '../entities/post-sale-ticket.entity';
|
import { PostSaleTicket, TicketStatus, TicketPriority, TicketCategory } from '../entities/post-sale-ticket.entity';
|
||||||
import { TicketAssignment } from '../entities/ticket-assignment.entity';
|
import { TicketAssignment } from '../entities/ticket-assignment.entity';
|
||||||
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
|
||||||
|
interface ServiceContext {
|
||||||
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateTicketDto {
|
export interface CreateTicketDto {
|
||||||
loteId: string;
|
loteId: string;
|
||||||
@ -82,7 +86,7 @@ export class TicketService {
|
|||||||
filters: TicketFilters = {},
|
filters: TicketFilters = {},
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20
|
limit: number = 20
|
||||||
): Promise<PaginatedResult<PostSaleTicket>> {
|
): Promise<{ data: PostSaleTicket[]; total: number; page: number; limit: number }> {
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
const queryBuilder = this.ticketRepository
|
const queryBuilder = this.ticketRepository
|
||||||
@ -137,15 +141,7 @@ export class TicketService {
|
|||||||
|
|
||||||
const [data, total] = await queryBuilder.getManyAndCount();
|
const [data, total] = await queryBuilder.getManyAndCount();
|
||||||
|
|
||||||
return {
|
return { data, total, page, limit };
|
||||||
data,
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
totalPages: Math.ceil(total / limit),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(ctx: ServiceContext, id: string): Promise<PostSaleTicket | null> {
|
async findById(ctx: ServiceContext, id: string): Promise<PostSaleTicket | null> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user