mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 09:06:40 +02:00
Backend (CRM-Expert Phase 1): - New enums: ContactSource, EntityStatus, CompanySize, OwnerRole, LostReason, EmailType, PhoneType - Contact: add linkedinUrl, birthday, source, department, status - Company: add vatId, taxId, tradeRegisterNumber, registerCourt, companySize, deliveryAddress, dataEnrichedAt/Source, status - Deal: add lostReason + lostReasonText (required when status=LOST) - Multi-value emails/phones tables (contact_emails, contact_phones) - Owner m:n model (contact_owners, company_owners, deal_owners) - Redis Pub/Sub CRM events (crm.contact.created, crm.deal.won, etc.) - Activity due_soon scheduler (cron every 15 min) - SQL migration with data migration for existing records Frontend integration: - types.ts: all new enums, interfaces, label maps - api.ts: owner CRUD endpoints (add/remove for contacts/companies/deals) - hooks.ts: 6 new owner mutation hooks - ContactFormModal: LinkedIn, birthday, source, department, status fields - ContactDetailPage: display new fields (LinkedIn, department, birthday, source, status badge) - CompanyDetailPage: display vatId, taxId, trade register, company size, delivery address, data enrichment info - DealFormModal: lost reason dropdown + text (shown when status=LOST) - DealDetailPage: display lost reason with label - CompaniesPage: EntityStatus-aware status dots (ACTIVE/INACTIVE/BLOCKED) - ActivityType: add FOLLOWUP to all label maps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
// ============================================================
|
|
// Scheduler: Activity Due Soon - alle 15 Minuten
|
|
// ============================================================
|
|
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { Cron } from '@nestjs/schedule';
|
|
import { CrmPrismaService } from '../prisma/crm-prisma.service';
|
|
import { RedisService } from '../redis/redis.service';
|
|
import { CrmEventPublisher } from './crm-event-publisher.service';
|
|
|
|
@Injectable()
|
|
export class ActivityDueSoonScheduler {
|
|
private readonly logger = new Logger(ActivityDueSoonScheduler.name);
|
|
private static readonly LOCK_KEY = 'lock:activity-due-soon';
|
|
private static readonly LOCK_TTL = 600; // 10 Minuten
|
|
|
|
constructor(
|
|
private readonly prisma: CrmPrismaService,
|
|
private readonly redis: RedisService,
|
|
private readonly eventPublisher: CrmEventPublisher,
|
|
) {}
|
|
|
|
@Cron('0 */15 * * * *')
|
|
async checkDueSoon(): Promise<void> {
|
|
// Distributed Lock um parallele Ausfuehrung zu verhindern
|
|
const lockAcquired = await this.redis.setNx(
|
|
ActivityDueSoonScheduler.LOCK_KEY,
|
|
process.pid.toString(),
|
|
ActivityDueSoonScheduler.LOCK_TTL,
|
|
);
|
|
|
|
if (!lockAcquired) {
|
|
this.logger.debug('Activity Due Soon Check: Lock bereits vergeben, ueberspringe.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const now = new Date();
|
|
const in24h = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
|
|
// Activities die in den naechsten 24h faellig sind und nicht completed
|
|
const dueActivities = await this.prisma.activity.findMany({
|
|
where: {
|
|
scheduledAt: {
|
|
gte: now,
|
|
lte: in24h,
|
|
},
|
|
completedAt: null,
|
|
},
|
|
select: {
|
|
id: true,
|
|
tenantId: true,
|
|
scheduledAt: true,
|
|
contactId: true,
|
|
companyId: true,
|
|
},
|
|
take: 100, // Batch-Limit
|
|
});
|
|
|
|
if (dueActivities.length === 0) {
|
|
this.logger.debug('Keine faelligen Activities gefunden.');
|
|
return;
|
|
}
|
|
|
|
this.logger.log(
|
|
`${dueActivities.length} faellige Activities gefunden, publiziere Events...`,
|
|
);
|
|
|
|
for (const activity of dueActivities) {
|
|
await this.eventPublisher.activityDueSoon(
|
|
activity.tenantId,
|
|
activity.id,
|
|
{
|
|
scheduledAt: activity.scheduledAt?.toISOString() ?? '',
|
|
contactId: activity.contactId ?? undefined,
|
|
},
|
|
);
|
|
}
|
|
|
|
this.logger.log(`${dueActivities.length} activity.due_soon Events publiziert.`);
|
|
} catch (err) {
|
|
this.logger.error(`Fehler im Activity Due Soon Scheduler: ${String(err)}`);
|
|
}
|
|
}
|
|
}
|