Nouvelles fonctionnalités : **1. Import automatique depuis dossier (folderImportService.ts)** ✅ Surveillance périodique d'un dossier local configuré ✅ Détection automatique des nouveaux fichiers PDF ✅ Traitement identique à l'upload manuel (extraction Mistral AI, détection doublons, stockage) ✅ Déplacement automatique des fichiers traités vers un sous-dossier "processed" ✅ Scheduler configurable (fréquence en minutes) ✅ Routes tRPC pour démarrer/arrêter/vérifier le statut du service ✅ Boutons de contrôle dans la page Paramètres de réception **2. Système de notifications (notificationService.ts)** ✅ Notifications automatiques après chaque import (email ou dossier) ✅ Résumé détaillé : nombre de factures importées, doublons ignorés, erreurs ✅ Format adapté selon la source (email avec nom de fichier, dossier avec nombre de fichiers) ✅ Intégration avec le système de notifications Manus (notifyOwner) ✅ Notifications envoyées au propriétaire du projet **Fonctionnement de l'import dossier :** 1. L'utilisateur configure le chemin du dossier source dans "Paramètres de réception" 2. Il active l'import automatique et configure la fréquence de vérification 3. Il clique sur "Démarrer le service" pour lancer la surveillance 4. Le service scanne le dossier selon la fréquence configurée 5. Il détecte les nouveaux fichiers PDF (non encore traités) 6. Il traite chaque PDF avec la même logique que l'upload manuel 7. Les fichiers traités sont déplacés vers un sous-dossier "processed" 8. Une notification est envoyée avec le résumé des résultats **Fonctionnement des notifications :** - Après chaque import automatique (email ou dossier), une notification est envoyée - La notification contient : * Source de l'import (📧 email ou 📁 dossier) * Nom du fichier (email) ou nombre de fichiers (dossier) * ✅ Nombre de factures importées * 🔄 Nombre de doublons ignorés * ❌ Nombre d'erreurs * Total de factures détectées - Les notifications apparaissent dans l'interface Manus du propriétaire **Interface utilisateur :** - Chaque carte (Import dossier, Import email) dispose maintenant de : * Indicateur visuel "Service actif" avec point vert animé * Bouton "Démarrer le service" (désactivé si l'import n'est pas activé) * Bouton "Arrêter le service" (rouge) pour stopper la surveillance * Messages toast pour confirmer le démarrage/arrêt **Dépendances ajoutées :** - chokidar : Surveillance de fichiers et dossiers pour Node.js **Architecture :** - folderImportService.ts : Service d'import automatique depuis dossier - notificationService.ts : Service de notifications centralisé - Routes tRPC : folderImportService.{start, stop, status} - Intégration des notifications dans emailImportService et folderImportService Les deux services d'import automatique (email et dossier) sont maintenant opérationnels avec notifications intégrées.
666 lines
23 KiB
TypeScript
666 lines
23 KiB
TypeScript
import { z } from "zod";
|
|
import { COOKIE_NAME } from "@shared/const";
|
|
import { getSessionCookieOptions } from "./_core/cookies";
|
|
import { systemRouter } from "./_core/systemRouter";
|
|
import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
|
|
import {
|
|
createInvoice,
|
|
getInvoiceById,
|
|
getInvoicesByUser,
|
|
updateInvoice,
|
|
deleteInvoice,
|
|
searchInvoices,
|
|
getInvoiceStats,
|
|
createSourceFile,
|
|
getSourceFileById,
|
|
updateSourceFile,
|
|
getUserSettings,
|
|
upsertUserSettings,
|
|
createLocalUser,
|
|
getAllUsers,
|
|
updateUserPassword,
|
|
toggleUserActive,
|
|
deleteUser,
|
|
findDuplicateInvoice,
|
|
createImportLog,
|
|
getImportLogsByUser,
|
|
getLlmLogsBySourceFile,
|
|
getLlmLogsByInvoice,
|
|
getImportSettingsByUser,
|
|
upsertImportSettings,
|
|
} from "./db";
|
|
import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth";
|
|
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
|
|
import { localStoragePut, generateStorageKey } from "./localStorage";
|
|
import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport";
|
|
import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning } from "./emailImportService";
|
|
import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService";
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
// Admin-only procedure
|
|
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
|
|
if (ctx.user.role !== "admin") {
|
|
throw new TRPCError({ code: "FORBIDDEN", message: "Admin access required" });
|
|
}
|
|
return next({ ctx });
|
|
});
|
|
|
|
export const appRouter = router({
|
|
system: systemRouter,
|
|
|
|
// ============= AUTH ROUTES =============
|
|
auth: router({
|
|
me: publicProcedure.query(opts => opts.ctx.user),
|
|
|
|
// Local login (email + password)
|
|
loginLocal: publicProcedure
|
|
.input(z.object({
|
|
email: z.string().email(),
|
|
password: z.string().min(6),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const result = await loginLocal(input.email, input.password);
|
|
|
|
if (!result) {
|
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid email or password" });
|
|
}
|
|
|
|
// Set auth cookie
|
|
ctx.res.cookie("auth_token", result.token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "lax",
|
|
path: "/",
|
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
});
|
|
|
|
return { user: result.user };
|
|
}),
|
|
|
|
// Get Azure AD login URL
|
|
getAzureLoginUrl: publicProcedure.query(async () => {
|
|
if (!isAzureAdConfigured()) {
|
|
throw new TRPCError({ code: "BAD_REQUEST", message: "Azure AD not configured" });
|
|
}
|
|
|
|
const url = await getAzureAuthUrl();
|
|
return { url };
|
|
}),
|
|
|
|
// Check if Azure AD is available
|
|
isAzureAdAvailable: publicProcedure.query(() => {
|
|
return { available: isAzureAdConfigured() };
|
|
}),
|
|
|
|
logout: publicProcedure.mutation(({ ctx }) => {
|
|
const cookieOptions = getSessionCookieOptions(ctx.req);
|
|
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
|
|
ctx.res.clearCookie("auth_token", { path: "/", maxAge: -1 });
|
|
return { success: true };
|
|
}),
|
|
}),
|
|
|
|
// ============= INVOICE ROUTES =============
|
|
invoices: router({
|
|
// Upload and process PDF file
|
|
upload: protectedProcedure
|
|
.input(z.object({
|
|
fileName: z.string(),
|
|
fileData: z.string(), // Base64 encoded PDF
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const userId = ctx.user.id;
|
|
|
|
// Decode base64 file data
|
|
const fileBuffer = Buffer.from(input.fileData, "base64");
|
|
console.log(`[Upload] Received file: ${input.fileName}, size: ${fileBuffer.length} bytes`);
|
|
|
|
// Store source file
|
|
const sourceFileKey = generateStorageKey(userId, input.fileName);
|
|
console.log(`[Upload] Generated storage key: ${sourceFileKey}`);
|
|
|
|
let sourceFileUrl: string;
|
|
try {
|
|
const result = await localStoragePut(sourceFileKey, fileBuffer, "application/pdf");
|
|
sourceFileUrl = result.url;
|
|
console.log(`[Upload] File stored successfully at: ${sourceFileUrl}`);
|
|
} catch (error) {
|
|
console.error(`[Upload] FAILED to store file:`, error);
|
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to store PDF file" });
|
|
}
|
|
|
|
// Create source file record
|
|
const sourceFile = await createSourceFile({
|
|
userId,
|
|
fileName: input.fileName,
|
|
fileKey: sourceFileKey,
|
|
fileUrl: sourceFileUrl,
|
|
processingStatus: "processing",
|
|
});
|
|
|
|
// Start extraction process (async - don't wait)
|
|
(async () => {
|
|
try {
|
|
// Get user settings for custom keywords
|
|
const settings = await getUserSettings(userId);
|
|
const customKeywords = settings ? {
|
|
invoiceNumber: settings.invoiceNumberKeywords,
|
|
deliveryNote: settings.deliveryNoteKeywords,
|
|
orderNumber: settings.orderNumberKeywords,
|
|
supplier: settings.supplierKeywords,
|
|
totalAmount: settings.totalAmountKeywords,
|
|
} : undefined;
|
|
|
|
const model = settings?.llmModel || "mistral-large-latest";
|
|
|
|
// Extract invoices
|
|
const result = await extractInvoicesWithMistral(
|
|
fileBuffer,
|
|
userId,
|
|
sourceFile.id,
|
|
model,
|
|
customKeywords
|
|
);
|
|
|
|
// Update source file with total count
|
|
await updateSourceFile(sourceFile.id, {
|
|
totalInvoicesDetected: result.invoiceCount,
|
|
processingProgress: `Extraction ${result.invoiceCount} facture(s) détectée(s)`,
|
|
});
|
|
|
|
// Process each invoice
|
|
let importedCount = 0;
|
|
let duplicatesCount = 0;
|
|
let errorsCount = 0;
|
|
const duplicateDetails: any[] = [];
|
|
const errorDetails: any[] = [];
|
|
|
|
for (let i = 0; i < result.invoices.length; i++) {
|
|
const invoiceData = result.invoices[i]!;
|
|
|
|
try {
|
|
// Update progress
|
|
await updateSourceFile(sourceFile.id, {
|
|
processingProgress: `Extraction ${i + 1}/${result.invoiceCount} factures...`,
|
|
});
|
|
|
|
// Check for duplicates
|
|
const duplicate = await findDuplicateInvoice(
|
|
invoiceData.supplierName,
|
|
invoiceData.invoiceNumber,
|
|
invoiceData.invoiceDate
|
|
);
|
|
|
|
if (duplicate) {
|
|
duplicatesCount++;
|
|
duplicateDetails.push({
|
|
supplierName: invoiceData.supplierName,
|
|
invoiceNumber: invoiceData.invoiceNumber,
|
|
invoiceDate: invoiceData.invoiceDate,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Generate metadata JSON
|
|
const metadataJson = generateMetadataJSON(invoiceData);
|
|
const metadataKey = generateStorageKey(userId, `${input.fileName}-${i + 1}-metadata.json`);
|
|
const { url: metadataUrl } = await localStoragePut(
|
|
metadataKey,
|
|
Buffer.from(metadataJson),
|
|
"application/json"
|
|
);
|
|
|
|
// Create invoice record
|
|
await createInvoice({
|
|
userId,
|
|
sourceFileId: sourceFile.id,
|
|
invoiceIndexInFile: i + 1,
|
|
fileName: `${input.fileName} - Facture ${i + 1}`,
|
|
fileKey: sourceFileKey, // Same as source for now
|
|
fileUrl: sourceFileUrl,
|
|
supplierName: invoiceData.supplierName,
|
|
invoiceNumber: invoiceData.invoiceNumber,
|
|
invoiceDate: invoiceData.invoiceDate,
|
|
deliveryNoteNumber: invoiceData.deliveryNoteNumber,
|
|
orderNumber: invoiceData.orderNumber,
|
|
totalAmount: invoiceData.totalAmount?.toString(),
|
|
pageRange: invoiceData.pageRange,
|
|
qualityScore: invoiceData.qualityScore,
|
|
metadataFileKey: metadataKey,
|
|
metadataFileUrl: metadataUrl,
|
|
status: "completed",
|
|
});
|
|
|
|
importedCount++;
|
|
} catch (error: any) {
|
|
errorsCount++;
|
|
errorDetails.push({
|
|
invoiceIndex: i + 1,
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update source file status
|
|
await updateSourceFile(sourceFile.id, {
|
|
processingStatus: "completed",
|
|
processingProgress: `Terminé: ${importedCount} importée(s), ${duplicatesCount} doublon(s)`,
|
|
});
|
|
|
|
// Create import log
|
|
await createImportLog({
|
|
userId,
|
|
sourceFileId: sourceFile.id,
|
|
fileName: input.fileName,
|
|
totalInvoicesDetected: result.invoiceCount,
|
|
invoicesImported: importedCount,
|
|
duplicatesIgnored: duplicatesCount,
|
|
errors: errorsCount,
|
|
duplicateDetails: JSON.stringify(duplicateDetails),
|
|
errorDetails: JSON.stringify(errorDetails),
|
|
});
|
|
|
|
} catch (error: any) {
|
|
console.error("[Upload] Extraction failed:", error);
|
|
await updateSourceFile(sourceFile.id, {
|
|
processingStatus: "error",
|
|
processingProgress: `Erreur: ${error.message}`,
|
|
});
|
|
}
|
|
})();
|
|
|
|
return { sourceFileId: sourceFile.id };
|
|
}),
|
|
|
|
list: protectedProcedure.query(async ({ ctx }) => {
|
|
return getInvoicesByUser(ctx.user.id);
|
|
}),
|
|
|
|
getById: protectedProcedure
|
|
.input(z.object({ id: z.number() }))
|
|
.query(async ({ input, ctx }) => {
|
|
const invoice = await getInvoiceById(input.id);
|
|
if (!invoice || invoice.userId !== ctx.user.id) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
return invoice;
|
|
}),
|
|
|
|
update: protectedProcedure
|
|
.input(z.object({
|
|
id: z.number(),
|
|
data: z.object({
|
|
supplierName: z.string().optional(),
|
|
invoiceNumber: z.string().optional(),
|
|
invoiceDate: z.date().optional(),
|
|
deliveryNoteNumber: z.string().optional(),
|
|
orderNumber: z.string().optional(),
|
|
totalAmount: z.string().optional(),
|
|
}),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const invoice = await getInvoiceById(input.id);
|
|
if (!invoice || invoice.userId !== ctx.user.id) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
|
|
await updateInvoice(input.id, {
|
|
...input.data,
|
|
manuallyEdited: 1,
|
|
});
|
|
|
|
return { success: true };
|
|
}),
|
|
|
|
delete: protectedProcedure
|
|
.input(z.object({ id: z.number() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const invoice = await getInvoiceById(input.id);
|
|
if (!invoice || invoice.userId !== ctx.user.id) {
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
}
|
|
|
|
await deleteInvoice(input.id);
|
|
return { success: true };
|
|
}),
|
|
|
|
search: protectedProcedure
|
|
.input(z.object({ query: z.string() }))
|
|
.query(async ({ input, ctx }) => {
|
|
return searchInvoices(ctx.user.id, input.query);
|
|
}),
|
|
|
|
getStats: protectedProcedure.query(async ({ ctx }) => {
|
|
return getInvoiceStats(ctx.user.id);
|
|
}),
|
|
}),
|
|
|
|
// ============= SOURCE FILES ROUTES =============
|
|
sourceFiles: router({
|
|
getByIds: protectedProcedure
|
|
.input(z.object({ ids: z.array(z.number()) }))
|
|
.query(async ({ input, ctx }) => {
|
|
const files = await Promise.all(
|
|
input.ids.map(id => getSourceFileById(id))
|
|
);
|
|
return files.filter(f => f && f.userId === ctx.user.id);
|
|
}),
|
|
}),
|
|
|
|
// ============= SETTINGS ROUTES =============
|
|
settings: router({
|
|
get: protectedProcedure.query(async ({ ctx }) => {
|
|
return getUserSettings(ctx.user.id);
|
|
}),
|
|
|
|
upsert: protectedProcedure
|
|
.input(z.object({
|
|
llmModel: z.string().optional(),
|
|
orderNumberFormat: z.string().optional(),
|
|
invoiceNumberKeywords: z.string().optional(),
|
|
deliveryNoteKeywords: z.string().optional(),
|
|
orderNumberKeywords: z.string().optional(),
|
|
supplierKeywords: z.string().optional(),
|
|
totalAmountKeywords: z.string().optional(),
|
|
sftpHost: z.string().optional(),
|
|
sftpPort: z.number().optional(),
|
|
sftpUsername: z.string().optional(),
|
|
sftpPassword: z.string().optional(),
|
|
sftpRemotePath: z.string().optional(),
|
|
sftpAutoExport: z.number().optional(),
|
|
llmLogsRetentionMonths: z.number().optional(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
await upsertUserSettings({
|
|
userId: ctx.user.id,
|
|
...input,
|
|
});
|
|
return { success: true };
|
|
}),
|
|
}),
|
|
|
|
// ============= ADMIN ROUTES =============
|
|
admin: router({
|
|
createUser: adminProcedure
|
|
.input(z.object({
|
|
email: z.string().email(),
|
|
password: z.string().min(6),
|
|
name: z.string(),
|
|
role: z.enum(["user", "admin"]),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
const passwordHash = await hashPassword(input.password);
|
|
const user = await createLocalUser(input.email, passwordHash, input.name, input.role);
|
|
return { user };
|
|
}),
|
|
|
|
getAllUsers: adminProcedure.query(async () => {
|
|
return getAllUsers();
|
|
}),
|
|
|
|
updateUserPassword: adminProcedure
|
|
.input(z.object({
|
|
userId: z.number(),
|
|
newPassword: z.string().min(6),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
const passwordHash = await hashPassword(input.newPassword);
|
|
await updateUserPassword(input.userId, passwordHash);
|
|
return { success: true };
|
|
}),
|
|
|
|
toggleUserActive: adminProcedure
|
|
.input(z.object({
|
|
userId: z.number(),
|
|
isActive: z.number(),
|
|
}))
|
|
.mutation(async ({ input }) => {
|
|
await toggleUserActive(input.userId, input.isActive);
|
|
return { success: true };
|
|
}),
|
|
|
|
deleteUser: adminProcedure
|
|
.input(z.object({ userId: z.number() }))
|
|
.mutation(async ({ input }) => {
|
|
await deleteUser(input.userId);
|
|
return { success: true };
|
|
}),
|
|
}),
|
|
|
|
// ============= SFTP ROUTES =============
|
|
sftp: router({
|
|
testConnection: protectedProcedure.mutation(async ({ ctx }) => {
|
|
const config = await getUserSftpConfig(ctx.user.id);
|
|
if (!config) {
|
|
throw new TRPCError({ code: "BAD_REQUEST", message: "SFTP not configured" });
|
|
}
|
|
|
|
const success = await testSftpConnection(config);
|
|
return { success };
|
|
}),
|
|
|
|
exportToExcel: protectedProcedure
|
|
.input(z.object({ invoiceIds: z.array(z.number()) }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const invoices = await Promise.all(
|
|
input.invoiceIds.map(id => getInvoiceById(id))
|
|
);
|
|
|
|
const validInvoices = invoices.filter(
|
|
inv => inv && inv.userId === ctx.user.id
|
|
);
|
|
|
|
// Return invoice data for Excel generation on client side
|
|
return {
|
|
success: true,
|
|
invoices: validInvoices.map(inv => ({
|
|
id: inv!.id,
|
|
supplierName: inv!.supplierName || '',
|
|
invoiceNumber: inv!.invoiceNumber || '',
|
|
invoiceDate: inv!.invoiceDate ? new Date(inv!.invoiceDate).toLocaleDateString('fr-FR') : '',
|
|
deliveryNoteNumber: inv!.deliveryNoteNumber || '',
|
|
orderNumber: inv!.orderNumber || '',
|
|
totalAmount: inv!.totalAmount ? parseFloat(inv!.totalAmount as string) : 0,
|
|
qualityScore: inv!.qualityScore || 0,
|
|
exportStatus: inv!.exportStatus || 'not_exported',
|
|
exportedAt: inv!.exportedAt ? new Date(inv!.exportedAt).toLocaleDateString('fr-FR') : '',
|
|
createdAt: new Date(inv!.createdAt).toLocaleDateString('fr-FR'),
|
|
}))
|
|
};
|
|
}),
|
|
|
|
exportToPdf: protectedProcedure
|
|
.input(z.object({ invoiceIds: z.array(z.number()) }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
// Validate that all invoices have quality score of 100
|
|
const invoices = await Promise.all(
|
|
input.invoiceIds.map(id => getInvoiceById(id))
|
|
);
|
|
|
|
const invalidInvoices = invoices.filter(
|
|
inv => !inv || inv.userId !== ctx.user.id || (inv.qualityScore || 0) < 100
|
|
);
|
|
|
|
if (invalidInvoices.length > 0) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Toutes les factures doivent avoir un score de qualit\u00e9 de 100% pour \u00eatre export\u00e9es"
|
|
});
|
|
}
|
|
|
|
// Update export status
|
|
for (const invoice of invoices) {
|
|
if (invoice) {
|
|
await updateInvoice(invoice.id, {
|
|
exportStatus: "exported",
|
|
exportedAt: new Date(),
|
|
exportMode: "manual",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Return the file URLs for PDF generation on client side
|
|
return {
|
|
success: true,
|
|
invoices: invoices.filter(Boolean).map(inv => ({
|
|
id: inv!.id,
|
|
fileUrl: inv!.fileUrl,
|
|
supplierName: inv!.supplierName,
|
|
invoiceNumber: inv!.invoiceNumber,
|
|
invoiceDate: inv!.invoiceDate,
|
|
}))
|
|
};
|
|
}),
|
|
|
|
exportInvoices: protectedProcedure
|
|
.input(z.object({ invoiceIds: z.array(z.number()) }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const config = await getUserSftpConfig(ctx.user.id);
|
|
if (!config) {
|
|
throw new TRPCError({ code: "BAD_REQUEST", message: "SFTP not configured" });
|
|
}
|
|
|
|
let successCount = 0;
|
|
let errorCount = 0;
|
|
|
|
for (const invoiceId of input.invoiceIds) {
|
|
try {
|
|
const invoice = await getInvoiceById(invoiceId);
|
|
if (!invoice || invoice.userId !== ctx.user.id) {
|
|
errorCount++;
|
|
continue;
|
|
}
|
|
|
|
await exportInvoiceToSftp(
|
|
config,
|
|
invoice.fileKey,
|
|
invoice.metadataFileKey,
|
|
invoice.invoiceDate || new Date()
|
|
);
|
|
|
|
// Update invoice export status
|
|
await updateInvoice(invoiceId, {
|
|
exportedAt: new Date(),
|
|
exportMode: "manual",
|
|
});
|
|
|
|
successCount++;
|
|
} catch (error) {
|
|
console.error(`[SFTP] Failed to export invoice ${invoiceId}:`, error);
|
|
errorCount++;
|
|
}
|
|
}
|
|
|
|
return { successCount, errorCount };
|
|
}),
|
|
}),
|
|
|
|
// ============= IMPORT LOGS ROUTES =============
|
|
importLogs: router({
|
|
getByUser: protectedProcedure.query(async ({ ctx }) => {
|
|
return getImportLogsByUser(ctx.user.id);
|
|
}),
|
|
}),
|
|
|
|
// ============= LLM LOGS ROUTES =============
|
|
llmLogs: router({
|
|
getBySourceFile: protectedProcedure
|
|
.input(z.object({ sourceFileId: z.number() }))
|
|
.query(async ({ input }) => {
|
|
return getLlmLogsBySourceFile(input.sourceFileId);
|
|
}),
|
|
|
|
getByInvoice: protectedProcedure
|
|
.input(z.object({ invoiceId: z.number() }))
|
|
.query(async ({ input }) => {
|
|
return getLlmLogsByInvoice(input.invoiceId);
|
|
}),
|
|
}),
|
|
|
|
// ============= FOLDER IMPORT SERVICE ROUTES =============
|
|
folderImportService: router({
|
|
start: protectedProcedure.mutation(async ({ ctx }) => {
|
|
const started = await startFolderImportService(ctx.user.id);
|
|
return { success: started };
|
|
}),
|
|
|
|
stop: protectedProcedure.mutation(async ({ ctx }) => {
|
|
stopFolderImportService(ctx.user.id);
|
|
return { success: true };
|
|
}),
|
|
|
|
status: protectedProcedure.query(async ({ ctx }) => {
|
|
const isRunning = isFolderImportServiceRunning(ctx.user.id);
|
|
return { isRunning };
|
|
}),
|
|
}),
|
|
|
|
// ============= EMAIL IMPORT SERVICE ROUTES =============
|
|
emailImportService: router({
|
|
start: protectedProcedure.mutation(async ({ ctx }) => {
|
|
const started = await startEmailImportService(ctx.user.id);
|
|
return { success: started };
|
|
}),
|
|
|
|
stop: protectedProcedure.mutation(async ({ ctx }) => {
|
|
stopEmailImportService(ctx.user.id);
|
|
return { success: true };
|
|
}),
|
|
|
|
status: protectedProcedure.query(async ({ ctx }) => {
|
|
const isRunning = isEmailImportServiceRunning(ctx.user.id);
|
|
return { isRunning };
|
|
}),
|
|
}),
|
|
|
|
// ============= IMPORT SETTINGS ROUTES =============
|
|
importSettings: router({
|
|
get: protectedProcedure.query(async ({ ctx }) => {
|
|
const settings = await getImportSettingsByUser(ctx.user.id);
|
|
|
|
// Return default settings if none exist
|
|
if (!settings) {
|
|
return {
|
|
userId: ctx.user.id,
|
|
manualImportEnabled: 1,
|
|
autoImportEnabled: 0,
|
|
autoImportSourcePath: null,
|
|
autoImportFrequency: 60,
|
|
emailImportEnabled: 0,
|
|
emailImportAddress: null,
|
|
emailImportPassword: null,
|
|
emailImportHost: null,
|
|
emailImportPort: 993,
|
|
emailImportFrequency: 30,
|
|
};
|
|
}
|
|
|
|
return settings;
|
|
}),
|
|
|
|
update: protectedProcedure
|
|
.input(z.object({
|
|
manualImportEnabled: z.number().min(0).max(1),
|
|
autoImportEnabled: z.number().min(0).max(1),
|
|
autoImportSourcePath: z.string().nullable().optional(),
|
|
autoImportFrequency: z.number().min(1).optional(),
|
|
emailImportEnabled: z.number().min(0).max(1),
|
|
emailImportAddress: z.string().email().nullable().optional(),
|
|
emailImportPassword: z.string().nullable().optional(),
|
|
emailImportHost: z.string().nullable().optional(),
|
|
emailImportPort: z.number().min(1).max(65535).optional(),
|
|
emailImportFrequency: z.number().min(1).optional(),
|
|
}))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const settings = await upsertImportSettings({
|
|
userId: ctx.user.id,
|
|
...input,
|
|
});
|
|
|
|
return settings;
|
|
}),
|
|
}),
|
|
});
|
|
|
|
export type AppRouter = typeof appRouter;
|