import { DataSource, Repository, LessThanOrEqual } from 'typeorm'; import { ReportSchedule, ReportTemplate, ScheduleConfig } from '../entities'; import { CreateReportScheduleDto, UpdateReportScheduleDto, ReportScheduleQueryDto, } from '../dto'; export class ReportScheduleService { private repository: Repository; private templateRepository: Repository; constructor(dataSource: DataSource) { this.repository = dataSource.getRepository(ReportSchedule); this.templateRepository = dataSource.getRepository(ReportTemplate); } async findAll(tenantId: string, query: ReportScheduleQueryDto): Promise<{ data: ReportSchedule[]; total: number }> { const { templateId, status, page = 1, limit = 20 } = query; const queryBuilder = this.repository.createQueryBuilder('schedule') .leftJoinAndSelect('schedule.template', 'template') .where('schedule.tenant_id = :tenantId', { tenantId }) .andWhere('schedule.deleted_at IS NULL'); if (templateId) { queryBuilder.andWhere('schedule.template_id = :templateId', { templateId }); } if (status) { queryBuilder.andWhere('schedule.status = :status', { status }); } queryBuilder .orderBy('schedule.next_run_at', 'ASC') .skip((page - 1) * limit) .take(limit); const [data, total] = await queryBuilder.getManyAndCount(); return { data, total }; } async findById(tenantId: string, id: string): Promise { return this.repository.findOne({ where: { id, tenantId }, relations: ['template'], }); } async findActiveSchedules(tenantId: string): Promise { return this.repository.find({ where: { tenantId, status: 'active' }, relations: ['template'], order: { nextRunAt: 'ASC' }, }); } async findDueSchedules(): Promise { const now = new Date(); return this.repository.find({ where: { status: 'active', nextRunAt: LessThanOrEqual(now), }, relations: ['template'], }); } async create(tenantId: string, dto: CreateReportScheduleDto, createdBy: string): Promise { const template = await this.templateRepository.findOne({ where: { id: dto.templateId, tenantId, status: 'active' }, }); if (!template) { throw new Error('Report template not found or inactive'); } const nextRunAt = this.calculateNextRunTime(dto.scheduleConfig); const schedule = this.repository.create({ tenantId, templateId: dto.templateId, name: dto.name, description: dto.description, format: dto.format, reportParameters: dto.reportParameters, scheduleConfig: dto.scheduleConfig, recipients: dto.recipients, emailSubject: dto.emailSubject || `Report: ${template.name}`, emailBody: dto.emailBody, status: 'active', nextRunAt, createdBy, }); return this.repository.save(schedule); } async update(tenantId: string, id: string, dto: UpdateReportScheduleDto): Promise { const schedule = await this.findById(tenantId, id); if (!schedule) return null; if (dto.scheduleConfig) { schedule.scheduleConfig = dto.scheduleConfig; schedule.nextRunAt = this.calculateNextRunTime(dto.scheduleConfig); } Object.assign(schedule, { ...dto, scheduleConfig: dto.scheduleConfig || schedule.scheduleConfig, }); return this.repository.save(schedule); } async pause(tenantId: string, id: string): Promise { const schedule = await this.findById(tenantId, id); if (!schedule) return null; schedule.status = 'paused'; return this.repository.save(schedule); } async resume(tenantId: string, id: string): Promise { const schedule = await this.findById(tenantId, id); if (!schedule) return null; schedule.status = 'active'; schedule.nextRunAt = this.calculateNextRunTime(schedule.scheduleConfig); return this.repository.save(schedule); } async disable(tenantId: string, id: string): Promise { const schedule = await this.findById(tenantId, id); if (!schedule) return null; schedule.status = 'disabled'; schedule.nextRunAt = undefined; return this.repository.save(schedule); } async softDelete(tenantId: string, id: string): Promise { const schedule = await this.findById(tenantId, id); if (!schedule) return false; await this.repository.softDelete({ id, tenantId }); return true; } async recordRun(id: string, success: boolean, error?: string): Promise { const schedule = await this.repository.findOne({ where: { id } }); if (!schedule) return; schedule.lastRunAt = new Date(); schedule.lastRunStatus = success ? 'success' : 'failed'; schedule.lastRunError = error || undefined; schedule.runCount += 1; if (!success) { schedule.failureCount += 1; } if (schedule.status === 'active') { schedule.nextRunAt = this.calculateNextRunTime(schedule.scheduleConfig); } await this.repository.save(schedule); } async getScheduleStats(tenantId: string): Promise<{ total: number; active: number; paused: number; disabled: number; dueSoon: number; failedRecently: number; }> { const total = await this.repository.count({ where: { tenantId } }); const active = await this.repository.count({ where: { tenantId, status: 'active' } }); const paused = await this.repository.count({ where: { tenantId, status: 'paused' } }); const disabled = await this.repository.count({ where: { tenantId, status: 'disabled' } }); const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000); const dueSoon = await this.repository.count({ where: { tenantId, status: 'active', nextRunAt: LessThanOrEqual(oneHourFromNow), }, }); const failedRecently = await this.repository.count({ where: { tenantId, lastRunStatus: 'failed' }, }); return { total, active, paused, disabled, dueSoon, failedRecently }; } private calculateNextRunTime(config: ScheduleConfig): Date { const now = new Date(); const next = new Date(); next.setHours(config.hour, config.minute, 0, 0); switch (config.frequency) { case 'daily': if (next <= now) { next.setDate(next.getDate() + 1); } break; case 'weekly': const currentDay = next.getDay(); const targetDay = config.dayOfWeek ?? 1; let daysUntil = targetDay - currentDay; if (daysUntil <= 0 || (daysUntil === 0 && next <= now)) { daysUntil += 7; } next.setDate(next.getDate() + daysUntil); break; case 'monthly': const targetDayOfMonth = config.dayOfMonth ?? 1; next.setDate(targetDayOfMonth); if (next <= now) { next.setMonth(next.getMonth() + 1); } break; case 'quarterly': const targetMonth = Math.floor(now.getMonth() / 3) * 3 + (config.monthOfYear ?? 0); next.setMonth(targetMonth); next.setDate(config.dayOfMonth ?? 1); if (next <= now) { next.setMonth(next.getMonth() + 3); } break; case 'yearly': next.setMonth((config.monthOfYear ?? 1) - 1); next.setDate(config.dayOfMonth ?? 1); if (next <= now) { next.setFullYear(next.getFullYear() + 1); } break; } return next; } }