import { Request, Response } from 'express'; import * as dataSheetService from '../services/dataSheetService.js'; import fs from 'fs'; import * as pdfExtractionService from '../services/pdfExtractionService.js'; import { IAppUser } from '../middleware/authMiddleware.js'; import { notificationService } from '../services/notificationService.js'; interface AuthRequest extends Request { appUser?: IAppUser; } export const getAllDataSheets = async (req: AuthRequest, res: Response) => { try { const organizationId = req.appUser?.organizationId; console.log('Backend: Fetching datasheets for org:', organizationId); const sheets = await dataSheetService.getAllDataSheets(organizationId); console.log(`Backend: Found ${sheets.length} sheets`); res.json(sheets); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); } }; export const extractData = async (req: AuthRequest, res: Response) => { try { const file = req.file; if (!file) { return res.status(400).json({ error: 'File is required' }); } const fileBuffer = fs.readFileSync(file.path); const data = await pdfExtractionService.extractDataFromPdf(fileBuffer); // Return extracted data AND the file path so we don't need to re-upload res.json({ ...data, tempFilePath: file.path }); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); } }; export const createDataSheet = async (req: AuthRequest, res: Response) => { try { const file = req.file; const { name, manufacturer, type, solidsVolume, density, mixingRatio, mixingRatioWeight, mixingRatioVolume, yieldTheoretical, dftReference, yieldFactor, wftMin, wftMax, dftMin, dftMax, reducer, dilution, notes, fileUrl, manufacturerCode, minStock, typicalApplication } = req.body; const organizationId = req.appUser?.organizationId; // Note: New logic prefers 'file' upload which we store in DB. // If fileUrl is provided (legacy or external link), we use that but don't store binary. // eslint-disable-next-line @typescript-eslint/no-explicit-any let fileId: any = undefined; let finalFileUrl = fileUrl || ''; if (file) { // Read file buffer const buffer = fs.readFileSync(file.path); // Save to StoredFile collection const { default: StoredFile } = await import('../models/StoredFile.js'); const newFile = await StoredFile.create({ filename: file.originalname, contentType: file.mimetype, data: buffer, size: file.size, uploadDate: new Date() }); fileId = newFile._id; finalFileUrl = newFile._id.toString(); // Use ID as URL reference for consistency with frontend expectations if possible, or we might need to adjust frontend to use /api/datasheets/file/:id // Clean up temp file try { fs.unlinkSync(file.path); } catch (error) { console.warn('Failed to delete temp file:', file.path, error); } } if (!fileId && !finalFileUrl) { // Check if fileUrl allows empty. The schema says optional now, but logically a datasheet usually has a file. // However, for simplified Diluent registration, we might not have one. // If the user didn't send a file and didn't send a URL, and schema is optional, we can proceed. // But let's check if we want to enforce it. // If manufacturerCode (Diluent indicator?) is present, maybe skip check? // Actually, I removed 'required' from schema, so I should probably relax this check too. // return res.status(400).json({ error: 'File is required' }); } const newSheet = await dataSheetService.createDataSheet({ name, manufacturer, manufacturerCode, type, minStock: minStock ? Number(minStock) : undefined, typicalApplication, fileUrl: finalFileUrl, fileId: fileId, solidsVolume: solidsVolume ? Number(solidsVolume) : undefined, density: density ? Number(density) : undefined, mixingRatio, mixingRatioWeight, mixingRatioVolume, yieldTheoretical: yieldTheoretical ? Number(yieldTheoretical) : undefined, dftReference: dftReference ? Number(dftReference) : undefined, yieldFactor: yieldFactor ? Number(yieldFactor) : undefined, wftMin: wftMin ? Number(wftMin) : undefined, wftMax: wftMax ? Number(wftMax) : undefined, dftMin: dftMin ? Number(dftMin) : undefined, dftMax: dftMax ? Number(dftMax) : undefined, reducer, dilution: dilution ? Number(dilution) : undefined, notes, organizationId }); // Notificação de Nova Ficha Técnica if (organizationId) { await notificationService.create({ organizationId, title: 'Nova Ficha Técnica', message: `A ficha técnica "${name}" (${manufacturer}) foi adicionada à biblioteca.`, type: 'info', metadata: { dataSheetId: newSheet._id, triggerType: 'datasheet_created' } }); } res.status(201).json(newSheet); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; console.error('Error creating datasheet:', error); res.status(500).json({ error: message }); } }; export const deleteDataSheet = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const organizationId = req.appUser?.organizationId; // Find sheet to delete file if exists // (Optional: Implement file deletion logic here if strict cleanup needed) const success = await dataSheetService.deleteDataSheet(id as string, organizationId); if (success) { res.status(204).send(); } else { res.status(404).json({ error: 'Data sheet not found' }); } } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); } }; export const updateDataSheet = async (req: AuthRequest, res: Response) => { try { const id = req.params.id as string; const file = req.file; const organizationId = req.appUser?.organizationId; const { name, manufacturer, type, solidsVolume, density, mixingRatio, mixingRatioWeight, mixingRatioVolume, yieldTheoretical, dftReference, yieldFactor, wftMin, wftMax, dftMin, dftMax, reducer, dilution, notes, fileUrl } = req.body; // eslint-disable-next-line @typescript-eslint/no-explicit-any const updates: Record = { name, manufacturer, type, notes, solidsVolume: solidsVolume ? Number(solidsVolume) : undefined, density: density ? Number(density) : undefined, yieldTheoretical: yieldTheoretical ? Number(yieldTheoretical) : undefined, dftReference: dftReference ? Number(dftReference) : undefined, yieldFactor: yieldFactor ? Number(yieldFactor) : undefined, wftMin: wftMin ? Number(wftMin) : undefined, wftMax: wftMax ? Number(wftMax) : undefined, dftMin: dftMin ? Number(dftMin) : undefined, dftMax: dftMax ? Number(dftMax) : undefined, reducer, dilution: dilution ? Number(dilution) : undefined, mixingRatio, mixingRatioWeight, mixingRatioVolume }; if (file) { // Read file buffer const buffer = fs.readFileSync(file.path); // Save to StoredFile collection const { default: StoredFile } = await import('../models/StoredFile.js'); const newFile = await StoredFile.create({ filename: file.originalname, contentType: file.mimetype, data: buffer, size: file.size, uploadDate: new Date() }); updates.fileId = newFile._id; updates.fileUrl = newFile._id.toString(); // Clean up temp file try { fs.unlinkSync(file.path); } catch (error) { console.warn('Failed to delete temp file:', file.path, error); } } else if (fileUrl) { updates.fileUrl = String(fileUrl); // If fileUrl is being updated but not file, we might lose fileId reference? // If the user sends the same fileUrl (which is the ID), it's fine. // But if they send a new external URL, we should probably unset fileId. // For now, let's assume if it's an external URL, fileId should remain unless explicitly cleared? // Safer: if fileUrl is explicitly sent and doesn't match an ID format, maybe clear fileId? // Actually, keep it simple. } const updatedSheet = await dataSheetService.updateDataSheet(id, updates, organizationId); if (updatedSheet) { res.json(updatedSheet); } else { res.status(404).json({ error: 'Data sheet not found' }); } } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; console.error('Error updating datasheet:', error); res.status(500).json({ error: message }); } }; export const getFile = async (req: Request, res: Response) => { try { const id_or_filename = req.params.id as string; // Check if it's a MongoDB ObjectId (24 hex chars) if (/^[0-9a-fA-F]{24}$/.test(id_or_filename)) { const { default: StoredFile } = await import('../models/StoredFile.js'); const fileDoc = await StoredFile.findById(id_or_filename); if (fileDoc) { res.set('Content-Type', fileDoc.contentType || 'application/pdf'); res.set('Content-Disposition', `inline; filename="${fileDoc.filename}"`); return res.send(fileDoc.data); } } // Fallback to file system (legacy) const stream = dataSheetService.getFileStream(id_or_filename); stream.on('file', (file) => { res.set('Content-Type', 'application/pdf'); res.set('Content-Disposition', `inline; filename="${file.filename}"`); }); stream.on('error', () => { res.status(404).json({ error: 'File not found' }); }); stream.pipe(res); } catch (error: unknown) { const message = error instanceof Error ? error.message : 'Unknown error'; console.error('Error getting file:', error); res.status(500).json({ error: message }); } };