259 lines
7.8 KiB
TypeScript
Executable File
259 lines
7.8 KiB
TypeScript
Executable File
import { Router } from "express";
|
|
import { Request, Response } from "express";
|
|
import { storage } from "../storage";
|
|
import multer from "multer";
|
|
import { z } from "zod";
|
|
|
|
const router = Router();
|
|
|
|
// Configure multer for file uploads
|
|
const upload = multer({
|
|
storage: multer.memoryStorage(),
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024, // 10MB limit
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
// Accept common document and image formats
|
|
const allowedTypes = [
|
|
'application/pdf',
|
|
'image/jpeg',
|
|
'image/jpg',
|
|
'image/png',
|
|
'image/gif',
|
|
'application/msword',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'text/plain',
|
|
];
|
|
|
|
if (allowedTypes.includes(file.mimetype)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Invalid file type. Only PDF, images, and documents are allowed.'));
|
|
}
|
|
}
|
|
});
|
|
|
|
// Validation schemas
|
|
const uploadDocumentSchema = z.object({
|
|
patientId: z.string().transform((val) => parseInt(val, 10)),
|
|
});
|
|
|
|
const getDocumentsSchema = z.object({
|
|
patientId: z.string().transform((val) => parseInt(val, 10)),
|
|
limit: z.string().optional().transform((val) => val ? parseInt(val, 10) : undefined),
|
|
offset: z.string().optional().transform((val) => val ? parseInt(val, 10) : undefined),
|
|
});
|
|
|
|
const deleteDocumentSchema = z.object({
|
|
id: z.string().transform((val) => parseInt(val, 10)),
|
|
});
|
|
|
|
// POST /api/patient-documents/upload
|
|
// Upload a document for a specific patient
|
|
router.post("/upload", upload.single("file"), async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { patientId } = uploadDocumentSchema.parse(req.body);
|
|
const file = req.file;
|
|
|
|
if (!file) {
|
|
return res.status(400).json({ error: "No file uploaded" });
|
|
}
|
|
|
|
const document = await storage.createPatientDocument(
|
|
patientId,
|
|
file.originalname,
|
|
file.originalname,
|
|
file.mimetype,
|
|
file.size,
|
|
file.buffer
|
|
);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
document: {
|
|
...document,
|
|
fileSize: Number(document.fileSize), // Convert BigInt to Number for JSON serialization
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error("Error uploading document:", error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ error: "Invalid request data", details: error.errors });
|
|
}
|
|
|
|
if (error instanceof Error && error.message.includes('Invalid file type')) {
|
|
return res.status(400).json({ error: error.message });
|
|
}
|
|
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// GET /api/patient-documents/patient/:patientId
|
|
// Get all documents for a specific patient
|
|
router.get("/patient/:patientId", async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { patientId, limit, offset } = getDocumentsSchema.parse({
|
|
patientId: req.params.patientId,
|
|
limit: req.query.limit,
|
|
offset: req.query.offset,
|
|
});
|
|
|
|
if (limit !== undefined && offset !== undefined) {
|
|
// Paginated response
|
|
const result = await storage.getDocumentsByPatientIdPaginated(patientId, limit, offset);
|
|
res.json({
|
|
success: true,
|
|
documents: result.documents.map(doc => ({
|
|
...doc,
|
|
fileSize: Number(doc.fileSize), // Convert BigInt to Number
|
|
})),
|
|
total: result.total,
|
|
});
|
|
} else {
|
|
// Non-paginated response
|
|
const documents = await storage.getDocumentsByPatientId(patientId);
|
|
res.json({
|
|
success: true,
|
|
documents: documents.map(doc => ({
|
|
...doc,
|
|
fileSize: Number(doc.fileSize), // Convert BigInt to Number
|
|
})),
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching documents:", error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ error: "Invalid patient ID", details: error.errors });
|
|
}
|
|
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// GET /api/patient-documents/:id/download
|
|
// Download a specific document
|
|
router.get("/:id/download", async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { id } = deleteDocumentSchema.parse({ id: req.params.id });
|
|
|
|
const result = await storage.getDocumentFile(id);
|
|
|
|
if (!result) {
|
|
return res.status(404).json({ error: "Document not found" });
|
|
}
|
|
|
|
const { buffer, document } = result;
|
|
|
|
// Set appropriate headers
|
|
res.setHeader("Content-Type", document.mimeType);
|
|
res.setHeader("Content-Length", document.fileSize.toString());
|
|
res.setHeader(
|
|
"Content-Disposition",
|
|
`attachment; filename="${encodeURIComponent(document.originalName)}"`
|
|
);
|
|
|
|
res.send(buffer);
|
|
} catch (error) {
|
|
console.error("Error downloading document:", error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ error: "Invalid document ID", details: error.errors });
|
|
}
|
|
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// GET /api/patient-documents/:id/view
|
|
// View a specific document (inline display)
|
|
router.get("/:id/view", async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { id } = deleteDocumentSchema.parse({ id: req.params.id });
|
|
|
|
const result = await storage.getDocumentFile(id);
|
|
|
|
if (!result) {
|
|
return res.status(404).json({ error: "Document not found" });
|
|
}
|
|
|
|
const { buffer, document } = result;
|
|
|
|
// Set appropriate headers for inline viewing
|
|
res.setHeader("Content-Type", document.mimeType);
|
|
res.setHeader("Content-Length", document.fileSize.toString());
|
|
res.setHeader(
|
|
"Content-Disposition",
|
|
`inline; filename="${encodeURIComponent(document.originalName)}"`
|
|
);
|
|
|
|
res.send(buffer);
|
|
} catch (error) {
|
|
console.error("Error viewing document:", error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ error: "Invalid document ID", details: error.errors });
|
|
}
|
|
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// DELETE /api/patient-documents/:id
|
|
// Delete a specific document
|
|
router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { id } = deleteDocumentSchema.parse({ id: req.params.id });
|
|
|
|
const success = await storage.deleteDocument(id);
|
|
|
|
if (!success) {
|
|
return res.status(404).json({ error: "Document not found" });
|
|
}
|
|
|
|
res.json({ success: true, message: "Document deleted successfully" });
|
|
} catch (error) {
|
|
console.error("Error deleting document:", error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ error: "Invalid document ID", details: error.errors });
|
|
}
|
|
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// POST /api/patient-documents/scan
|
|
// Simulate document scanning (placeholder for actual scanner integration)
|
|
router.post("/scan", async (req: Request, res: Response): Promise<any> => {
|
|
try {
|
|
const { patientId } = uploadDocumentSchema.parse(req.body);
|
|
|
|
// This is a placeholder for actual scanner integration
|
|
// In a real implementation, you would:
|
|
// 1. Interface with scanner hardware/software
|
|
// 2. Capture the scanned image
|
|
// 3. Process and save the image
|
|
// 4. Return the document info
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Scanner interface ready. Please integrate with your scanner hardware/software.",
|
|
patientId,
|
|
note: "This endpoint requires integration with scanner hardware/software SDK."
|
|
});
|
|
} catch (error) {
|
|
console.error("Error scanning document:", error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return res.status(400).json({ error: "Invalid request data", details: error.errors });
|
|
}
|
|
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
export default router;
|