diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 0a8e927..56b87bb 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -11,6 +11,7 @@ import { LdapModule } from './ldap/ldap.module'; import { MailModule } from './mail/mail.module'; import { CalendarModule } from './calendar/calendar.module'; import { DashboardModule } from './dashboard/dashboard.module'; +import { DkvModule } from './dkv/dkv.module'; import { DomaincheckModule } from './domaincheck/domaincheck.module'; import { ModuleRegistryModule } from './module-registry/module-registry.module'; import { PrismaModule } from './prisma/prisma.module'; @@ -35,6 +36,7 @@ import { UserModule } from './user/user.module'; DashboardModule, CalendarModule, SettingsModule, + DkvModule, ], providers: [ // Global JWT guard: all routes require auth unless @Public() diff --git a/apps/api/src/dkv/dkv.module.ts b/apps/api/src/dkv/dkv.module.ts new file mode 100644 index 0000000..60dd346 --- /dev/null +++ b/apps/api/src/dkv/dkv.module.ts @@ -0,0 +1,70 @@ +import { Logger, Module, OnModuleInit } from '@nestjs/common'; +import { ModuleRegistryModule } from '../module-registry/module-registry.module'; +import { ModuleRegistryService } from '../module-registry/module-registry.service'; +import { CalendarModule } from '../calendar/calendar.module'; +import { DkvController } from './dkv.controller'; +import { DkvExportService } from './dkv-export.service'; +import { DkvMailService } from './dkv-mail.service'; +import { DkvParserService } from './dkv-parser.service'; +import { DkvSchedulerService } from './dkv-scheduler.service'; +import { DkvService } from './dkv.service'; +import { ExchangeInboxProvider } from './providers/exchange-inbox.provider'; +import { ImapProvider } from './providers/imap.provider'; +import { seedDkvModule } from './dkv.seed'; + +/** + * NestJS module for the DKV Fleet feature. + * + * Provides the full DKV processing pipeline: + * - DkvService: orchestration (poll → parse → export → send → history) + * - DkvSchedulerService: dynamic cron job via SchedulerRegistry + * - DkvParserService: pdf-parse v2 text extraction + DKV-specific regex + * - DkvExportService: xlsx buffer generation + user-files/ management + * - DkvMailService: nodemailer.createTransport() at send time + * - ImapProvider / ExchangeInboxProvider: inbox access providers + * + * Imports: + * - ModuleRegistryModule: for registry self-seed on init + * - CalendarModule: provides CalendarCryptoService (AES-256-GCM encryption) + * re-exported from CalendarModule.exports — no re-declaration needed here. + * + * PrismaModule is global (no explicit import needed). + * ScheduleModule.forRoot() was registered in AppModule by Plan 01. + * SettingsModule (for DkvMailService → SettingsService) is imported in AppModule. + * + * Seeds itself into the module registry on application startup via + * OnModuleInit lifecycle hook — same pattern as DomaincheckModule. + */ +@Module({ + imports: [ + ModuleRegistryModule, + CalendarModule, // Provides CalendarCryptoService for inbox credential encryption + ], + controllers: [DkvController], + providers: [ + DkvService, + DkvSchedulerService, + DkvParserService, + DkvExportService, + DkvMailService, + ImapProvider, + ExchangeInboxProvider, + ], + exports: [DkvService], +}) +export class DkvModule implements OnModuleInit { + private readonly logger = new Logger(DkvModule.name); + + constructor( + private readonly moduleRegistryService: ModuleRegistryService, + ) {} + + async onModuleInit(): Promise { + try { + await seedDkvModule(this.moduleRegistryService); + this.logger.log('DKV Fleet module seeded in registry'); + } catch (error) { + this.logger.error('Failed to seed DKV Fleet module', error); + } + } +} diff --git a/apps/api/src/dkv/dkv.seed.ts b/apps/api/src/dkv/dkv.seed.ts new file mode 100644 index 0000000..9d39f23 --- /dev/null +++ b/apps/api/src/dkv/dkv.seed.ts @@ -0,0 +1,26 @@ +import { ModuleRegistryService } from '../module-registry/module-registry.service'; + +/** + * Seeds the DKV Fleet module into the module registry. + * + * Called during DkvModule initialization (OnModuleInit) to ensure + * the 'dkv-fleet' module record exists in the database. + * + * Category 'fleet' identifies this as a fleet-management module. + * isSystem: true — not tenant-licensable in v1; available to all tenants. + */ +export async function seedDkvModule( + moduleRegistryService: ModuleRegistryService, +): Promise { + await moduleRegistryService.seedModule({ + slug: 'dkv-fleet', + name: 'DKV Flotte', + version: '1.0.0', + category: 'fleet', + description: { + de: 'DKV-Tankkartenrechnungen automatisch verarbeiten', + en: 'Automatically process DKV fuel card invoices', + }, + isSystem: true, + }); +} diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 917f6a9..ceac8e7 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -113,11 +113,25 @@ "categoryDashboard": "Dashboard", "categoryWidgets": "Widgets", "categoryCalendar": "Kalender", + "categoryGeneral": "Allgemein", + "categorySmtp": "SMTP", "sourceDeleteConfirm": "Moechten Sie diese Kalenderquelle wirklich loeschen?", "sourceDeleteCta": "Quelle loeschen", "providerDeleteConfirm": "Moechten Sie diesen Suchanbieter wirklich loeschen?", "providerDeleteCta": "Anbieter loeschen", - "cancel": "Abbrechen" + "cancel": "Abbrechen", + "smtp": { + "title": "SMTP-Konfiguration", + "host": "Host", + "port": "Port", + "encryption": "Verschluesselung", + "username": "Benutzername", + "password": "Passwort", + "fromAddress": "Absenderadresse (Von)", + "fromAddressHelp": "Die E-Mail-Adresse, die als Absender angezeigt wird.", + "save": "Einstellungen speichern", + "test": "Verbindung testen" + } }, "widgets": { "emptyHeading": "Keine Widgets aktiv", @@ -237,6 +251,87 @@ "checking": "Pruefe...", "error": "Fehler bei der Pruefung" }, + "dkvFleet": { + "pageTitle": "DKV Flotte", + "checkNow": "Jetzt pruefen", + "checking": "Wird geprueft...", + "lastChecked": "Zuletzt geprueft: {time} Uhr", + "neverChecked": "Noch nie geprueft", + "settingsTitle": "Moduleinstellungen", + "historyTitle": "Verarbeitungshistorie", + "exportFilesTitle": "Letzte Exportdateien", + "emptyHistory": "Noch keine Rechnungen verarbeitet", + "emptyHistoryBody": "Klicke auf 'Jetzt pruefen', um den Posteingang zu pruefen. Neue Rechnungen erscheinen hier automatisch.", + "emptyExports": "Noch keine Exportdateien vorhanden", + "emptyVehicles": "Keine Fahrzeuge hinterlegt", + "emptyVehiclesBody": "Fuege Fahrzeuge manuell hinzu oder lade eine CSV-Datei hoch.", + "status": { + "processed": "Verarbeitet", + "error": "Fehler", + "sendFailed": "Versand fehlgeschlagen" + }, + "col": { + "dateTime": "Datum/Zeit", + "invoiceNumber": "Rechnungsnummer", + "vehicles": "Fahrzeuge", + "transactions": "Transaktionen", + "status": "Status", + "exportFile": "Exportdatei", + "licensePlate": "Kennzeichen", + "brand": "Marke", + "model": "Modell", + "driver": "Fahrer", + "actions": "Aktionen" + }, + "addVehicle": "Fahrzeug hinzufuegen", + "editVehicle": "Fahrzeug bearbeiten", + "deleteVehicle": "Fahrzeug loeschen", + "deleteVehicleBody": "Das Fahrzeug {plate} wird dauerhaft aus der Stammdatenliste entfernt. Diese Aktion kann nicht rueckgaengig gemacht werden.", + "importCsv": "CSV importieren", + "importMode": "Importmodus", + "importMerge": "Zusammenfuehren — neue Fahrzeuge hinzufuegen, vorhandene behalten", + "importReplace": "Ersetzen — alle vorhandenen Fahrzeuge loeschen und neu importieren", + "importReplaceWarning": "Alle vorhandenen Fahrzeuge werden ersetzt.", + "importReplaceConfirmTitle": "Fahrzeuge ersetzen?", + "importReplaceConfirmBody": "Alle vorhandenen Fahrzeuge werden durch die CSV-Daten ersetzt. Diese Aktion kann nicht rueckgaengig gemacht werden.", + "importReplaceConfirmButton": "Ersetzen", + "importSuccess": "{count} Fahrzeuge importiert", + "vehicleCount": "{count} Fahrzeuge", + "tabs": { + "inbox": "Posteingang", + "vehicles": "Fahrzeuge" + }, + "form": { + "protocol": "Protokoll", + "host": "Host", + "port": "Port", + "encryption": "Verschluesselung", + "encryptionNone": "Keine", + "folder": "Ordner", + "senderFilter": "Absenderfilter", + "pollInterval": "Abrufintervall", + "pollIntervalSuffix": "Minuten", + "username": "Benutzername", + "password": "Passwort", + "exportRecipient": "Export-Empfaenger", + "vehicleFormat": "Fahrzeug-Formatstring", + "vehicleFormatHelp": "Verfuegbare Platzhalter: {Marke}, {Modell}, {Kennzeichen}, {Fahrer}", + "active": "Aktiv", + "testConnection": "Verbindung testen", + "testTesting": "Verbindung wird getestet...", + "testSuccess": "Verbindung erfolgreich", + "testFailed": "Verbindung fehlgeschlagen: {error}", + "save": "Einstellungen speichern", + "cancel": "Bearbeitung abbrechen", + "cancelDialog": "Abbrechen", + "showPassword": "Passwort anzeigen", + "hidePassword": "Passwort verbergen" + }, + "errors": { + "pollFailed": "Posteingang konnte nicht geprueft werden. Ueberpruefe die Konfiguration unter 'Moduleinstellungen > Posteingang'.", + "smtpFailed": "Export wurde erstellt, konnte aber nicht gesendet werden. Die Datei ist weiterhin zum Download verfuegbar." + } + }, "marketplace": { "title": "Marktplatz", "subtitle": "Entdecken und aktivieren Sie Module fuer Ihren Mandanten", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 112cd3b..db617de 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -113,11 +113,25 @@ "categoryDashboard": "Dashboard", "categoryWidgets": "Widgets", "categoryCalendar": "Calendar", + "categoryGeneral": "General", + "categorySmtp": "SMTP", "sourceDeleteConfirm": "Are you sure you want to delete this calendar source?", "sourceDeleteCta": "Delete source", "providerDeleteConfirm": "Are you sure you want to delete this search provider?", "providerDeleteCta": "Delete provider", - "cancel": "Cancel" + "cancel": "Cancel", + "smtp": { + "title": "SMTP Configuration", + "host": "Host", + "port": "Port", + "encryption": "Encryption", + "username": "Username", + "password": "Password", + "fromAddress": "Sender Address (From)", + "fromAddressHelp": "The email address shown as the sender.", + "save": "Save Settings", + "test": "Test Connection" + } }, "widgets": { "emptyHeading": "No active widgets", @@ -237,6 +251,87 @@ "checking": "Checking...", "error": "Error checking domain" }, + "dkvFleet": { + "pageTitle": "DKV Fleet", + "checkNow": "Check Now", + "checking": "Checking...", + "lastChecked": "Last checked: {time}", + "neverChecked": "Never checked", + "settingsTitle": "Module Settings", + "historyTitle": "Processing History", + "exportFilesTitle": "Recent Export Files", + "emptyHistory": "No invoices processed yet", + "emptyHistoryBody": "Click 'Check Now' to poll the inbox. New invoices will appear here automatically.", + "emptyExports": "No export files available yet", + "emptyVehicles": "No vehicles configured", + "emptyVehiclesBody": "Add vehicles manually or upload a CSV file.", + "status": { + "processed": "Processed", + "error": "Error", + "sendFailed": "Send Failed" + }, + "col": { + "dateTime": "Date/Time", + "invoiceNumber": "Invoice Number", + "vehicles": "Vehicles", + "transactions": "Transactions", + "status": "Status", + "exportFile": "Export File", + "licensePlate": "License Plate", + "brand": "Make", + "model": "Model", + "driver": "Driver", + "actions": "Actions" + }, + "addVehicle": "Add Vehicle", + "editVehicle": "Edit Vehicle", + "deleteVehicle": "Delete Vehicle", + "deleteVehicleBody": "Vehicle {plate} will be permanently removed from the master list. This action cannot be undone.", + "importCsv": "Import CSV", + "importMode": "Import Mode", + "importMerge": "Merge — add new vehicles, keep existing", + "importReplace": "Replace — delete all existing vehicles and re-import", + "importReplaceWarning": "All existing vehicles will be replaced.", + "importReplaceConfirmTitle": "Replace Vehicles?", + "importReplaceConfirmBody": "All existing vehicles will be replaced with the CSV data. This action cannot be undone.", + "importReplaceConfirmButton": "Replace", + "importSuccess": "{count} vehicles imported", + "vehicleCount": "{count} vehicles", + "tabs": { + "inbox": "Inbox", + "vehicles": "Vehicles" + }, + "form": { + "protocol": "Protocol", + "host": "Host", + "port": "Port", + "encryption": "Encryption", + "encryptionNone": "None", + "folder": "Folder", + "senderFilter": "Sender Filter", + "pollInterval": "Poll Interval", + "pollIntervalSuffix": "Minutes", + "username": "Username", + "password": "Password", + "exportRecipient": "Export Recipient", + "vehicleFormat": "Vehicle Format String", + "vehicleFormatHelp": "Available placeholders: {Marke}, {Modell}, {Kennzeichen}, {Fahrer}", + "active": "Active", + "testConnection": "Test Connection", + "testTesting": "Testing connection...", + "testSuccess": "Connection successful", + "testFailed": "Connection failed: {error}", + "save": "Save Settings", + "cancel": "Cancel Editing", + "cancelDialog": "Cancel", + "showPassword": "Show password", + "hidePassword": "Hide password" + }, + "errors": { + "pollFailed": "Could not check inbox. Review the configuration under 'Module Settings > Inbox'.", + "smtpFailed": "Export was created but could not be sent. The file is still available for manual download." + } + }, "marketplace": { "title": "Marketplace", "subtitle": "Discover and activate modules for your tenant",