From ea521e92c66408397b65a9bde0b5060478f22a6c Mon Sep 17 00:00:00 2001 From: TheMaddax Date: Wed, 26 Mar 2025 18:45:32 -0500 Subject: [PATCH] This commit simplifies the document management system to improve usability and user experience: - Model changes: - Remove 'category' field from Document model - Replace complex 'status' field with simple 'isPublic' boolean flag - Enhance document types to be more specific and user-friendly - UI improvements: - Update DocumentForm component to use simplified model - Improve document listing page with clearer public/private status - Enhance document detail page with better PDF preview using tag - Fix document edit page to work with new model - API fixes: - Fix uploads API route to properly handle PDF files - Add proper content types for various document formats - Fix params.path handling to prevent errors - UX enhancements: - Simplify form with clearer options - Make public/private toggle more intuitive - Improve document preview to avoid unwanted downloads - Enhance file type handling for better compatibility - Documentation: - Update memory bank (activeContext.md and progress.md) This change improves the overall user experience by making the document management system more intuitive while maintaining all necessary functionality. --- backend/src/controllers/documentController.ts | 542 +++++++++++ backend/src/index.ts | 2 + backend/src/middleware/upload.ts | 22 +- backend/src/models/Document.ts | 106 +++ backend/src/routes/documents.ts | 22 + backend/src/services/documentProcessor.ts | 127 +++ .../document-1743031855065-584387717.pdf | Bin 0 -> 74097 bytes .../textversions/text-1743031855070.txt | 111 +++ cline_docs/activeContext.md | 38 +- cline_docs/progress.md | 22 +- .../app/admin/documents/[id]/edit/page.tsx | 158 ++++ .../src/app/admin/documents/[id]/page.tsx | 549 +++++++++++ .../src/app/admin/documents/create/page.tsx | 44 + frontend/src/app/admin/documents/page.tsx | 877 ++++++++++-------- .../src/app/api/uploads/[...path]/route.ts | 14 +- .../src/components/admin/DocumentForm.tsx | 560 +++++++++++ frontend/src/hooks/useDocuments.ts | 300 ++++++ frontend/src/utils/formatters.ts | 76 ++ 18 files changed, 3142 insertions(+), 428 deletions(-) create mode 100644 backend/src/controllers/documentController.ts create mode 100644 backend/src/models/Document.ts create mode 100644 backend/src/routes/documents.ts create mode 100644 backend/src/services/documentProcessor.ts create mode 100644 backend/uploads/documents/document-1743031855065-584387717.pdf create mode 100644 backend/uploads/textversions/text-1743031855070.txt create mode 100644 frontend/src/app/admin/documents/[id]/edit/page.tsx create mode 100644 frontend/src/app/admin/documents/[id]/page.tsx create mode 100644 frontend/src/app/admin/documents/create/page.tsx create mode 100644 frontend/src/components/admin/DocumentForm.tsx create mode 100644 frontend/src/hooks/useDocuments.ts create mode 100644 frontend/src/utils/formatters.ts diff --git a/backend/src/controllers/documentController.ts b/backend/src/controllers/documentController.ts new file mode 100644 index 0000000..d90dd40 --- /dev/null +++ b/backend/src/controllers/documentController.ts @@ -0,0 +1,542 @@ +import { Request, Response } from 'express'; +import mongoose from 'mongoose'; +import Document from '../models/Document'; +import { getUploadedFilePaths } from '../middleware/upload'; +import { DocumentProcessor } from '../services/documentProcessor'; +import path from 'path'; +import fs from 'fs-extra'; + +/** + * Get all documents with optional filtering + */ +export const getDocuments = async (req: Request, res: Response) => { + try { + const { + documentType, + search, + page = 1, + limit = 10, + isPublic + } = req.query; + + // Build query + const query: any = {}; + + // Filter by document type if provided + if (documentType && documentType !== 'all') { + query.documentType = documentType; + } + + // Filter by public/private status if provided + if (isPublic !== undefined) { + query.isPublic = isPublic === 'true'; + } + + // Text search if provided + if (search) { + const searchRegex = new RegExp(search as string, 'i'); + query.$or = [ + { title: searchRegex }, + { description: searchRegex } + ]; + } + + // Calculate pagination + const skip = ((Number(page) || 1) - 1) * (Number(limit) || 10); + + // Execute query with pagination + const documents = await Document.find(query) + .sort({ uploadDate: -1 }) // Sort by upload date, newest first + .skip(skip) + .limit(Number(limit)); + + // Get total count for pagination + const total = await Document.countDocuments(query); + + return res.status(200).json({ + documents, + pagination: { + total, + page: Number(page), + limit: Number(limit), + pages: Math.ceil(total / Number(limit)) + } + }); + } catch (error) { + console.error('Error fetching documents:', error); + return res.status(500).json({ + error: { + message: 'Failed to fetch documents' + } + }); + } +}; + +/** + * Get a single document by ID + */ +export const getDocumentById = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + // Validate ID format + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + error: { + message: 'Invalid document ID format' + } + }); + } + + // Find document + const document = await Document.findById(id); + + // Check if document exists + if (!document) { + return res.status(404).json({ + error: { + message: 'Document not found' + } + }); + } + + return res.status(200).json({ document }); + } catch (error) { + console.error('Error fetching document:', error); + return res.status(500).json({ + error: { + message: 'Failed to fetch document' + } + }); + } +}; + +/** + * Create a new document + */ +export const createDocument = async (req: Request, res: Response) => { + try { + // Get uploaded file paths + const { documentPath, textVersionPath } = getUploadedFilePaths(req); + + // Check if document file was uploaded + if (!documentPath) { + return res.status(400).json({ + error: { + message: 'Document file is required' + } + }); + } + + // Get form data + const { + title, + description, + documentType, + meetingDate, + isPublic + } = req.body; + + // Get file size + let fileSize = 0; + try { + fileSize = await DocumentProcessor.getFileSize(documentPath); + } catch (error) { + console.error('Error getting file size:', error); + return res.status(400).json({ + error: { + message: 'Failed to process document file. Please check the file format.' + } + }); + } + + // Get file type + const fileType = DocumentProcessor.getFileType(documentPath); + + // Generate text version if not provided and file is PDF + let finalTextVersionPath = textVersionPath; + if (!finalTextVersionPath && fileType === 'pdf') { + console.log('No text version provided, generating one automatically'); + try { + // Ensure textversions directory exists + const textVersionsDir = path.join(process.cwd(), 'uploads/textversions'); + console.log('Text versions directory:', textVersionsDir); + await fs.ensureDir(textVersionsDir); + + // Log document path + console.log('Document path for text version generation:', documentPath); + console.log('Document path exists:', await fs.pathExists(documentPath)); + + // Generate text version + console.log('Calling DocumentProcessor.generateTextVersion'); + finalTextVersionPath = await DocumentProcessor.generateTextVersion(documentPath, textVersionsDir); + console.log('Generated text version at:', finalTextVersionPath); + console.log('Text version exists:', await fs.pathExists(finalTextVersionPath)); + } catch (error) { + console.error('Error generating text version:', error); + // Continue without text version if generation fails + } + } + + // Check if document has text version + const hasTextVersion = !!finalTextVersionPath; + + // Get original filename + const originalFilename = req.files && + Array.isArray(req.files) && + req.files.length > 0 && + req.files[0].originalname ? + req.files[0].originalname : + path.basename(documentPath); + + // Convert absolute paths to relative paths for storage + const relativeDocumentPath = DocumentProcessor.getRelativePath(documentPath); + const relativeTextVersionPath = finalTextVersionPath ? DocumentProcessor.getRelativePath(finalTextVersionPath) : undefined; + + // Create new document + const document = new Document({ + title, + description, + filePath: relativeDocumentPath, + originalFilename, + fileType, + fileSize, + documentType, + isPublic: isPublic === 'true' || isPublic === true, + meetingDate: meetingDate ? new Date(meetingDate) : undefined, + uploadDate: new Date(), + lastModified: new Date(), + accessibilityChecked: false, + hasTextVersion, + textVersionPath: relativeTextVersionPath + }); + + // Save document to database + await document.save(); + + return res.status(201).json({ + message: 'Document created successfully', + document + }); + } catch (error) { + console.error('Error creating document:', error); + + // Handle validation errors + if (error instanceof mongoose.Error.ValidationError) { + return res.status(400).json({ + error: { + message: 'Validation error', + details: error.errors + } + }); + } + + return res.status(500).json({ + error: { + message: 'Failed to create document' + } + }); + } +}; + +/** + * Update an existing document + */ +export const updateDocument = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const updateData = req.body; + + // Validate ID format + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + error: { + message: 'Invalid document ID format' + } + }); + } + + // Update lastModified date + updateData.lastModified = new Date(); + + // No need to parse tags anymore + + // Find and update document + const document = await Document.findByIdAndUpdate( + id, + updateData, + { new: true, runValidators: true } + ); + + // Check if document exists + if (!document) { + return res.status(404).json({ + error: { + message: 'Document not found' + } + }); + } + + return res.status(200).json({ + message: 'Document updated successfully', + document + }); + } catch (error) { + console.error('Error updating document:', error); + + // Handle validation errors + if (error instanceof mongoose.Error.ValidationError) { + return res.status(400).json({ + error: { + message: 'Validation error', + details: error.errors + } + }); + } + + return res.status(500).json({ + error: { + message: 'Failed to update document' + } + }); + } +}; + +/** + * Delete a document + */ +export const deleteDocument = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + // Validate ID format + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + error: { + message: 'Invalid document ID format' + } + }); + } + + // Find document to get file paths + const document = await Document.findById(id); + + // Check if document exists + if (!document) { + return res.status(404).json({ + error: { + message: 'Document not found' + } + }); + } + + // Delete document files + if (document.filePath) { + await DocumentProcessor.deleteFile(DocumentProcessor.getAbsolutePath(document.filePath)); + } + if (document.textVersionPath) { + await DocumentProcessor.deleteFile(DocumentProcessor.getAbsolutePath(document.textVersionPath)); + } + + // Delete document from database + await Document.findByIdAndDelete(id); + + return res.status(200).json({ + message: 'Document deleted successfully' + }); + } catch (error) { + console.error('Error deleting document:', error); + return res.status(500).json({ + error: { + message: 'Failed to delete document' + } + }); + } +}; + +/** + * Upload text version for a document + */ +export const uploadTextVersion = async (req: Request, res: Response) => { + try { + const { id } = req.params; + + // Get uploaded text version file + const { textVersionPath } = getUploadedFilePaths(req); + + // Validate ID format + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + error: { + message: 'Invalid document ID format' + } + }); + } + + // Validate required fields + if (!textVersionPath) { + return res.status(400).json({ + error: { + message: 'Text version file is required' + } + }); + } + + // Find document to get old text version path + const existingDocument = await Document.findById(id); + + // Check if document exists + if (!existingDocument) { + // Delete uploaded file if document not found + await DocumentProcessor.deleteFile(textVersionPath); + + return res.status(404).json({ + error: { + message: 'Document not found' + } + }); + } + + // Delete old text version if exists + if (existingDocument.textVersionPath) { + const oldTextVersionPath = DocumentProcessor.getAbsolutePath(existingDocument.textVersionPath); + await DocumentProcessor.deleteFile(oldTextVersionPath); + } + + // Convert absolute path to relative path + const relativeTextVersionPath = DocumentProcessor.getRelativePath(textVersionPath); + + // Update document with new text version + const document = await Document.findByIdAndUpdate( + id, + { + textVersionPath: relativeTextVersionPath, + hasTextVersion: true, + accessibilityChecked: true, + lastModified: new Date() + }, + { new: true } + ); + + return res.status(200).json({ + message: 'Text version uploaded successfully', + document + }); + } catch (error) { + console.error('Error uploading text version:', error); + return res.status(500).json({ + error: { + message: 'Failed to upload text version' + } + }); + } +}; + +/** + * Update document public status + */ +export const updatePublicStatus = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { isPublic } = req.body; + + // Validate ID format + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + error: { + message: 'Invalid document ID format' + } + }); + } + + // Validate isPublic is a boolean + if (typeof isPublic !== 'boolean') { + return res.status(400).json({ + error: { + message: 'Invalid isPublic value. Must be a boolean.' + } + }); + } + + // Find and update document + const document = await Document.findByIdAndUpdate( + id, + { + isPublic, + lastModified: new Date() + }, + { new: true } + ); + + // Check if document exists + if (!document) { + return res.status(404).json({ + error: { + message: 'Document not found' + } + }); + } + + return res.status(200).json({ + message: `Document ${isPublic ? 'made public' : 'made private'} successfully`, + document + }); + } catch (error) { + console.error('Error updating document public status:', error); + return res.status(500).json({ + error: { + message: 'Failed to update document public status' + } + }); + } +}; + +/** + * Mark document as accessibility checked + */ +export const markAccessibilityChecked = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { accessibilityChecked } = req.body; + + // Validate ID format + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + error: { + message: 'Invalid document ID format' + } + }); + } + + // Find and update document + const document = await Document.findByIdAndUpdate( + id, + { + accessibilityChecked: accessibilityChecked !== undefined ? accessibilityChecked : true, + lastModified: new Date() + }, + { new: true } + ); + + // Check if document exists + if (!document) { + return res.status(404).json({ + error: { + message: 'Document not found' + } + }); + } + + return res.status(200).json({ + message: `Document marked as ${accessibilityChecked ? 'accessibility checked' : 'not accessibility checked'} successfully`, + document + }); + } catch (error) { + console.error('Error marking document as accessibility checked:', error); + return res.status(500).json({ + error: { + message: 'Failed to mark document as accessibility checked' + } + }); + } +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index d4b3982..20b824f 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -14,6 +14,7 @@ dotenv.config(); import eventRoutes from './routes/events'; import memberRoutes from './routes/members'; import videoRoutes from './routes/videos'; +import documentRoutes from './routes/documents'; // Create Express app const app = express(); @@ -67,6 +68,7 @@ app.get('/health', (req, res) => { app.use('/api/events', eventRoutes); app.use('/api/members', memberRoutes); app.use('/api/videos', videoRoutes); +app.use('/api/documents', documentRoutes); // Error handling middleware app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { diff --git a/backend/src/middleware/upload.ts b/backend/src/middleware/upload.ts index 1e690d6..bf9009a 100644 --- a/backend/src/middleware/upload.ts +++ b/backend/src/middleware/upload.ts @@ -13,7 +13,9 @@ const createUploadDirectories = () => { 'uploads/videos', 'uploads/thumbnails', 'uploads/subtitles', - 'uploads/transcripts' + 'uploads/transcripts', + 'uploads/documents', + 'uploads/textversions' ]; dirs.forEach(dir => { @@ -41,6 +43,10 @@ const storage = multer.diskStorage({ uploadPath = path.join(uploadPath, 'subtitles'); } else if (file.fieldname === 'transcript') { uploadPath = path.join(uploadPath, 'transcripts'); + } else if (file.fieldname === 'document') { + uploadPath = path.join(uploadPath, 'documents'); + } else if (file.fieldname === 'textversion') { + uploadPath = path.join(uploadPath, 'textversions'); } cb(null, uploadPath); @@ -60,7 +66,9 @@ const fileFilter = (req: Request, file: MulterFile, cb: any) => { video: ['.mp4', '.mov', '.avi', '.webm'], thumbnail: ['.jpg', '.jpeg', '.png', '.gif', '.webp'], subtitles: ['.vtt', '.srt'], - transcript: ['.txt', '.json'] + transcript: ['.txt', '.json'], + document: ['.pdf', '.doc', '.docx', '.txt', '.rtf', '.odt', '.ppt', '.pptx'], + textversion: ['.txt'] }; const ext = path.extname(file.originalname).toLowerCase(); @@ -90,6 +98,12 @@ export const uploadVideo = upload.fields([ { name: 'transcript', maxCount: 1 } ]); +// Export document upload middleware +export const uploadDocument = upload.fields([ + { name: 'document', maxCount: 1 }, + { name: 'textversion', maxCount: 1 } +]); + // Helper to get file paths from multer request export const getUploadedFilePaths = (req: Request) => { const files = req.files as { [fieldname: string]: MulterFile[] } | undefined; @@ -98,6 +112,8 @@ export const getUploadedFilePaths = (req: Request) => { videoPath: files?.video?.[0]?.path, thumbnailPath: files?.thumbnail?.[0]?.path, subtitlesPath: files?.subtitles?.[0]?.path, - transcriptPath: files?.transcript?.[0]?.path + transcriptPath: files?.transcript?.[0]?.path, + documentPath: files?.document?.[0]?.path, + textVersionPath: files?.textversion?.[0]?.path }; }; diff --git a/backend/src/models/Document.ts b/backend/src/models/Document.ts new file mode 100644 index 0000000..2321765 --- /dev/null +++ b/backend/src/models/Document.ts @@ -0,0 +1,106 @@ +import mongoose, { Schema, Document } from 'mongoose'; + +// Document document interface +export interface IDocument extends Document { + title: string; + description: string; + filePath: string; + originalFilename: string; + fileType: string; + fileSize: number; + documentType: 'meeting_minutes' | 'board_meeting_minutes' | 'committee_meeting_minutes' | 'annual_meeting_minutes' | + 'bylaws' | 'financial_report' | 'annual_report' | 'board_report' | 'committee_report' | + 'meeting_agenda' | 'board_meeting_agenda' | 'committee_meeting_agenda' | 'program_document'; + isPublic: boolean; + meetingDate?: Date; + uploadDate: Date; + lastModified: Date; + accessibilityChecked: boolean; + hasTextVersion: boolean; + textVersionPath?: string; +} + +// Document schema +const DocumentSchema: Schema = new Schema({ + title: { + type: String, + required: true, + trim: true, + minLength: 3 + }, + description: { + type: String, + required: true, + trim: true + }, + filePath: { + type: String, + required: true, + trim: true + }, + originalFilename: { + type: String, + trim: true + }, + fileType: { + type: String, + required: true, + trim: true + }, + fileSize: { + type: Number, + required: true + }, + documentType: { + type: String, + required: true, + enum: [ + 'meeting_minutes', 'board_meeting_minutes', 'committee_meeting_minutes', 'annual_meeting_minutes', + 'bylaws', 'financial_report', 'annual_report', 'board_report', 'committee_report', + 'meeting_agenda', 'board_meeting_agenda', 'committee_meeting_agenda', 'program_document' + ], + index: true + }, + isPublic: { + type: Boolean, + required: true, + default: false, + index: true + }, + meetingDate: { + type: Date + }, + uploadDate: { + type: Date, + required: true, + default: Date.now + }, + lastModified: { + type: Date, + required: true, + default: Date.now + }, + accessibilityChecked: { + type: Boolean, + default: false + }, + hasTextVersion: { + type: Boolean, + default: false + }, + textVersionPath: { + type: String, + trim: true + } +}, { + timestamps: true +}); + +// Create indexes for performance +DocumentSchema.index({ title: 'text', description: 'text' }); +DocumentSchema.index({ documentType: 1 }); +DocumentSchema.index({ uploadDate: -1 }); +DocumentSchema.index({ isPublic: 1 }); +DocumentSchema.index({ meetingDate: 1 }); + +export default mongoose.model('Document', DocumentSchema); diff --git a/backend/src/routes/documents.ts b/backend/src/routes/documents.ts new file mode 100644 index 0000000..895e406 --- /dev/null +++ b/backend/src/routes/documents.ts @@ -0,0 +1,22 @@ +import express from 'express'; +import { checkAuth } from '../middleware/auth'; +import * as documentController from '../controllers/documentController'; +import { uploadDocument } from '../middleware/upload'; + +const router = express.Router(); + +// Public routes +router.get('/', documentController.getDocuments); +router.get('/:id', documentController.getDocumentById); + +// TEMPORARY: Authentication bypass for development +// TODO: CRITICAL - Re-enable authentication middleware before production deployment +// Protected routes (admin only) +router.post('/', /* checkAuth, */ uploadDocument, documentController.createDocument); // TODO: Re-enable checkAuth +router.put('/:id', /* checkAuth, */ documentController.updateDocument); // TODO: Re-enable checkAuth +router.delete('/:id', /* checkAuth, */ documentController.deleteDocument); // TODO: Re-enable checkAuth +router.post('/:id/textversion', /* checkAuth, */ uploadDocument, documentController.uploadTextVersion); // TODO: Re-enable checkAuth +router.put('/:id/public', /* checkAuth, */ documentController.updatePublicStatus); // TODO: Re-enable checkAuth +router.put('/:id/accessibility', /* checkAuth, */ documentController.markAccessibilityChecked); // TODO: Re-enable checkAuth + +export default router; diff --git a/backend/src/services/documentProcessor.ts b/backend/src/services/documentProcessor.ts new file mode 100644 index 0000000..7fb8870 --- /dev/null +++ b/backend/src/services/documentProcessor.ts @@ -0,0 +1,127 @@ +import path from 'path'; +import fs from 'fs-extra'; +import { promisify } from 'util'; +import { exec } from 'child_process'; + +// Promisify exec +const execPromise = promisify(exec); + +/** + * Document processing service + */ +export class DocumentProcessor { + /** + * Get file size in bytes + * @param filePath Path to document file + * @returns File size in bytes + */ + static async getFileSize(filePath: string): Promise { + try { + const stats = await fs.stat(filePath); + return stats.size; + } catch (error) { + console.error('Error getting file size:', error); + throw error; + } + } + + /** + * Get file type from extension + * @param filePath Path to document file + * @returns File type (extension without dot) + */ + static getFileType(filePath: string): string { + const ext = path.extname(filePath).toLowerCase(); + return ext.startsWith('.') ? ext.substring(1) : ext; + } + + /** + * Generate text version of PDF document + * @param pdfPath Path to PDF file + * @param outputDir Path to output directory + * @returns Path to generated text file + */ + static async generateTextVersion( + pdfPath: string, + outputDir: string + ): Promise { + console.log('Generating text version for document:', pdfPath); + console.log('Output directory:', outputDir); + + // Create unique filename + const filename = `text-${Date.now()}.txt`; + const outputPath = path.join(outputDir, filename); + console.log('Output path:', outputPath); + + // Check if document file exists + try { + const fileExists = await fs.pathExists(pdfPath); + console.log('Document file exists:', fileExists); + if (!fileExists) { + throw new Error(`Document file does not exist: ${pdfPath}`); + } + + // Check if output directory exists + const dirExists = await fs.pathExists(outputDir); + console.log('Output directory exists:', dirExists); + if (!dirExists) { + console.log('Creating output directory'); + await fs.ensureDir(outputDir); + } + } catch (err) { + console.error('Error checking files:', err); + throw err; + } + + // Only process PDF files + if (path.extname(pdfPath).toLowerCase() !== '.pdf') { + throw new Error('Only PDF files can be converted to text'); + } + + try { + // Use pdftotext if available (requires poppler-utils) + await execPromise(`pdftotext "${pdfPath}" "${outputPath}"`); + return outputPath; + } catch (error) { + console.error('Error generating text version:', error); + throw error; + } + } + + /** + * Get relative path from absolute path + * @param absolutePath Absolute file path + * @returns Relative path for storage in database + */ + static getRelativePath(absolutePath: string): string { + // Convert absolute path to relative path for storage + const relativePath = absolutePath.replace(process.cwd(), ''); + return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; + } + + /** + * Get absolute path from relative path + * @param relativePath Relative file path + * @returns Absolute path for file operations + */ + static getAbsolutePath(relativePath: string): string { + return path.join(process.cwd(), relativePath); + } + + /** + * Delete file if it exists + * @param filePath Path to file + */ + static async deleteFile(filePath: string): Promise { + if (!filePath) return; + + try { + const exists = await fs.pathExists(filePath); + if (exists) { + await fs.unlink(filePath); + } + } catch (error) { + console.error(`Error deleting file ${filePath}:`, error); + } + } +} diff --git a/backend/uploads/documents/document-1743031855065-584387717.pdf b/backend/uploads/documents/document-1743031855065-584387717.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d31c99ca2d67fff968b2568f0c14650e82da899a GIT binary patch literal 74097 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

8}Q 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 + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Get document ID from params + const documentId = Array.isArray(params.id) ? params.id[0] : params.id; + + // Fetch document data + const fetchDocument = async () => { + setLoading(true); + setError(null); + + try { + const doc = await getDocumentById(documentId); + + if (doc) { + setDocument(doc); + } else { + setError(new Error('Document not found')); + } + } catch (err) { + console.error('Error fetching document:', err); + setError(err instanceof Error ? err : new Error('Failed to fetch document')); + } finally { + setLoading(false); + } + }; + + // Handle form submission + const handleSubmit = async (formData: FormData) => { + try { + // If no document file is provided, we need to update the document data directly + if (!formData.get('document')) { + const updateData: Partial = { + title: formData.get('title') as string, + description: formData.get('description') as string, + documentType: formData.get('documentType') as any, // Cast to any to avoid type issues + isPublic: formData.get('isPublic') === 'true', + accessibilityChecked: formData.get('accessibilityChecked') === 'true' + }; + + // Add meeting date if provided + if (formData.get('meetingDate')) { + // Convert string date to Date object + updateData.meetingDate = new Date(formData.get('meetingDate') as string); + } + + return await updateDocument(documentId, updateData); + } + + // Otherwise, submit the form data directly (FormData will be handled by the API) + return await updateDocument(documentId, formData as any); + } catch (err) { + console.error('Error updating document:', err); + throw err; + } + }; + + // Load document on initial render + useEffect(() => { + fetchDocument(); + }, [documentId]); + + // Loading state + if (loading) { + return ( +

+ + + + +
+ ); + } + + // Error state + if (error) { + return ( +
+
+
+
+ + + +
+
+

+ Error loading document +

+
+

{error.message}

+
+
+
+
+
+ + Back to Documents + +
+
+ ); + } + + // Document not found + if (!document) { + return ( +
+
+ + + +

Document not found

+

+ The document you are looking for does not exist or has been deleted. +

+
+ + Back to Documents + +
+
+
+ ); + } + + return ( +
+
+

Edit Document

+ + Cancel + +
+ + +
+ ); +} diff --git a/frontend/src/app/admin/documents/[id]/page.tsx b/frontend/src/app/admin/documents/[id]/page.tsx new file mode 100644 index 0000000..60dcc00 --- /dev/null +++ b/frontend/src/app/admin/documents/[id]/page.tsx @@ -0,0 +1,549 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { useDocuments, Document } from '../../../../hooks/useDocuments'; +import { formatDate, formatFileSize } from '../../../../utils/formatters'; + +export default function DocumentDetailPage() { + const params = useParams(); + const router = useRouter(); + const { + getDocumentById, + deleteDocument, + updatePublicStatus, + markAccessibilityChecked, + uploadTextVersion + } = useDocuments(); + + // Document state + const [document, setDocument] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Confirmation dialog state + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + // File upload state + const [textVersionFile, setTextVersionFile] = useState(null); + const [uploadingTextVersion, setUploadingTextVersion] = useState(false); + + // Get document ID from params + const documentId = Array.isArray(params.id) ? params.id[0] : params.id; + + // Fetch document data + const fetchDocument = async () => { + setLoading(true); + setError(null); + + try { + const doc = await getDocumentById(documentId); + + if (doc) { + setDocument(doc); + } else { + setError(new Error('Document not found')); + } + } catch (err) { + console.error('Error fetching document:', err); + setError(err instanceof Error ? err : new Error('Failed to fetch document')); + } finally { + setLoading(false); + } + }; + + // Handle delete confirmation + const handleDeleteClick = () => { + setShowDeleteConfirm(true); + }; + + // Handle delete confirmation cancel + const handleDeleteCancel = () => { + setShowDeleteConfirm(false); + }; + + // Handle delete confirmation confirm + const handleDeleteConfirm = async () => { + try { + const success = await deleteDocument(documentId); + + if (success) { + router.push('/admin/documents'); + } else { + setError(new Error('Failed to delete document')); + } + } catch (err) { + console.error('Error deleting document:', err); + setError(err instanceof Error ? err : new Error('Failed to delete document')); + } + }; + + // Handle public status toggle + const handlePublicToggle = async () => { + if (!document) return; + + try { + const updatedDoc = await updatePublicStatus(documentId, !document.isPublic); + + if (updatedDoc) { + setDocument(updatedDoc); + } + } catch (err) { + console.error('Error updating public status:', err); + setError(err instanceof Error ? err : new Error('Failed to update public status')); + } + }; + + // Handle accessibility checked toggle + const handleAccessibilityToggle = async () => { + if (!document) return; + + try { + const updatedDoc = await markAccessibilityChecked(documentId, !document.accessibilityChecked); + + if (updatedDoc) { + setDocument(updatedDoc); + } + } catch (err) { + console.error('Error updating accessibility status:', err); + setError(err instanceof Error ? err : new Error('Failed to update accessibility status')); + } + }; + + // Handle text version file change + const handleTextVersionChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + setTextVersionFile(e.target.files[0]); + } + }; + + // Handle text version upload + const handleTextVersionUpload = async () => { + if (!textVersionFile) return; + + setUploadingTextVersion(true); + + try { + const formData = new FormData(); + formData.append('textversion', textVersionFile); + + const updatedDoc = await uploadTextVersion(documentId, formData); + + if (updatedDoc) { + setDocument(updatedDoc); + setTextVersionFile(null); + } + } catch (err) { + console.error('Error uploading text version:', err); + setError(err instanceof Error ? err : new Error('Failed to upload text version')); + } finally { + setUploadingTextVersion(false); + } + }; + + // Get document type display name + const getDocumentTypeDisplayName = (type: string): string => { + return type + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + }; + + // Load document on initial render + useEffect(() => { + fetchDocument(); + }, [documentId]); + + // Loading state + if (loading) { + return ( +
+ + + + +
+ ); + } + + // Error state + if (error) { + return ( +
+
+
+
+ + + +
+
+

+ Error loading document +

+
+

{error.message}

+
+
+
+
+
+ + Back to Documents + +
+
+ ); + } + + // Document not found + if (!document) { + return ( +
+
+ + + +

Document not found

+

+ The document you are looking for does not exist or has been deleted. +

+
+ + Back to Documents + +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

{document.title}

+

+ Uploaded on {formatDate(document.uploadDate)} +

+
+
+ + Edit + + +
+
+ + {/* Document details */} +
+ {/* Main content */} +
+ {/* Document preview */} +
+

Document Preview

+ {document.fileType === 'pdf' ? ( +
+ +

+ Your browser does not support PDF preview. + + Click here to open the PDF + +

+
+
+ ) : ( +
+
+ + + +

+ Preview not available for {document.fileType.toUpperCase()} files +

+ + Download to View + +
+
+ )} + +
+ + {/* Description */} +
+

Description

+

+ {document.description} +

+
+ + {/* Accessibility */} +
+

Accessibility

+
+
+
+

Accessibility Status

+

+ Mark this document as checked for accessibility +

+
+ +
+ +
+
+

Text Version

+

+ {document.hasTextVersion + ? 'This document has a text version available' + : 'No text version available for this document'} +

+
+ {document.hasTextVersion ? ( + + Download Text Version + + ) : ( +
+ + + {textVersionFile && ( + + )} +
+ )} +
+
+
+
+ + {/* Sidebar */} +
+ {/* Document info */} +
+

Document Information

+
+
+

Type

+

+ {getDocumentTypeDisplayName(document.documentType)} +

+
+ + {document.meetingDate && ( +
+

Meeting Date

+

+ {formatDate(document.meetingDate)} +

+
+ )} + +
+

File Type

+

+ {document.fileType.toUpperCase()} +

+
+ +
+

File Size

+

+ {formatFileSize(document.fileSize)} +

+
+ +
+

Original Filename

+

+ {document.originalFilename} +

+
+ +
+

Upload Date

+

+ {formatDate(document.uploadDate)} +

+
+ +
+

Last Modified

+

+ {formatDate(document.lastModified)} +

+
+
+
+ + {/* Visibility */} +
+

Visibility

+
+
+

Public Status

+

+ {document.isPublic + ? 'This document is visible to all users' + : 'This document is only visible to administrators'} +

+
+ +
+
+ + {/* Actions */} +
+

Actions

+
+ + Edit Document + + + + Back to Documents + +
+
+
+
+ + {/* Delete confirmation dialog */} + {showDeleteConfirm && ( +
+
+ + +
+
+
+
+ + + +
+
+

+ Delete Document +

+
+

+ Are you sure you want to delete this document? This action cannot be undone. +

+
+
+
+
+
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/frontend/src/app/admin/documents/create/page.tsx b/frontend/src/app/admin/documents/create/page.tsx new file mode 100644 index 0000000..c5e52f3 --- /dev/null +++ b/frontend/src/app/admin/documents/create/page.tsx @@ -0,0 +1,44 @@ +'use client'; + +import React from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { useDocuments } from '../../../../hooks/useDocuments'; +import DocumentForm from '../../../../components/admin/DocumentForm'; + +export default function CreateDocumentPage() { + const router = useRouter(); + const { createDocument } = useDocuments(); + + // Handle form submission + const handleSubmit = async (formData: FormData) => { + try { + const result = await createDocument(formData); + + if (result) { + router.push(`/admin/documents/${result._id}`); + } + + return result; + } catch (err) { + console.error('Error creating document:', err); + throw err; + } + }; + + return ( +
+
+

Create New Document

+ + Cancel + +
+ + +
+ ); +} diff --git a/frontend/src/app/admin/documents/page.tsx b/frontend/src/app/admin/documents/page.tsx index e1ca50c..0e69dd7 100644 --- a/frontend/src/app/admin/documents/page.tsx +++ b/frontend/src/app/admin/documents/page.tsx @@ -1,474 +1,424 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import Link from 'next/link'; - -// Document Filter Component -const DocumentFilter = ({ - onFilterChange -}: { - onFilterChange: (filter: { category: string; isPublic: string; search: string }) => void -}) => { - const [category, setCategory] = useState('all'); - const [isPublic, setIsPublic] = useState('all'); - const [search, setSearch] = useState(''); - - const handleFilterChange = () => { - onFilterChange({ category, isPublic, search }); - }; - - const handleReset = () => { - setCategory('all'); - setIsPublic('all'); - setSearch(''); - onFilterChange({ category: 'all', isPublic: 'all', search: '' }); - }; - - return ( -
-

Filter Documents

-
-
- - -
- -
- - -
- -
- - setSearch(e.target.value)} - /> -
-
- -
- - -
-
- ); -}; +import { useRouter } from 'next/navigation'; +import { useDocuments, Document } from '../../../hooks/useDocuments'; +import { formatDate, formatFileSize } from '../../../utils/formatters'; export default function DocumentsPage() { - const [filter, setFilter] = useState({ category: 'all', isPublic: 'all', search: '' }); - const [selectedDocuments, setSelectedDocuments] = useState([]); - const [selectAll, setSelectAll] = useState(false); + const router = useRouter(); + const { + documents, + loading, + error, + pagination, + fetchDocuments, + deleteDocument, + updatePublicStatus, + markAccessibilityChecked + } = useDocuments(); - // Mock documents data - would come from API in a real implementation - const allDocuments = [ - { - id: '1', - title: 'Board Meeting Minutes - March 2025', - description: 'Minutes from the March 10, 2025 board meeting', - category: 'minutes', - fileUrl: '/documents/minutes-march-2025.pdf', - mimeType: 'application/pdf', - fileSize: '284 KB', - uploadDate: '2025-03-15', - lastModified: '2025-03-15', - isPublic: false, - tags: ['board', 'minutes', 'march', '2025'] - }, - { - id: '2', - title: 'OCD Bylaws 2025', - description: 'Updated bylaws for the Olathe Club of the Deaf', - category: 'bylaws', - fileUrl: '/documents/ocd-bylaws-2025.pdf', - mimeType: 'application/pdf', - fileSize: '567 KB', - uploadDate: '2025-01-05', - lastModified: '2025-01-05', - isPublic: true, - tags: ['bylaws', 'rules', 'organization'] - }, - { - id: '3', - title: 'Membership Application Form', - description: 'Form for new membership applications', - category: 'forms', - fileUrl: '/documents/membership-form.pdf', - mimeType: 'application/pdf', - fileSize: '125 KB', - uploadDate: '2024-12-10', - lastModified: '2025-01-15', - isPublic: true, - tags: ['membership', 'application', 'form'] - }, - { - id: '4', - title: 'Annual Financial Report 2024', - description: 'Financial report for the fiscal year 2024', - category: 'reports', - fileUrl: '/documents/financial-report-2024.pdf', - mimeType: 'application/pdf', - fileSize: '1.2 MB', - uploadDate: '2025-02-15', - lastModified: '2025-02-15', - isPublic: false, - tags: ['financial', 'annual', 'report', '2024'] - }, - { - id: '5', - title: 'Winter Newsletter 2025', - description: 'Newsletter for winter 2025 with club updates and events', - category: 'newsletters', - fileUrl: '/documents/newsletter-winter-2025.pdf', - mimeType: 'application/pdf', - fileSize: '3.4 MB', - uploadDate: '2025-01-10', - lastModified: '2025-01-10', - isPublic: true, - tags: ['newsletter', 'winter', '2025', 'events'] - }, - { - id: '6', - title: 'Event Proposal Form', - description: 'Form for members to propose new events', - category: 'forms', - fileUrl: '/documents/event-proposal-form.docx', - mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - fileSize: '68 KB', - uploadDate: '2024-11-20', - lastModified: '2024-11-20', - isPublic: true, - tags: ['events', 'proposal', 'form'] - }, - { - id: '7', - title: 'Board Meeting Minutes - February 2025', - description: 'Minutes from the February 12, 2025 board meeting', - category: 'minutes', - fileUrl: '/documents/minutes-february-2025.pdf', - mimeType: 'application/pdf', - fileSize: '312 KB', - uploadDate: '2025-02-18', - lastModified: '2025-02-18', - isPublic: false, - tags: ['board', 'minutes', 'february', '2025'] - }, - { - id: '8', - title: 'Member Handbook', - description: 'Comprehensive handbook for all OCD members', - category: 'bylaws', - fileUrl: '/documents/member-handbook.pdf', - mimeType: 'application/pdf', - fileSize: '4.2 MB', - uploadDate: '2025-01-05', - lastModified: '2025-01-05', - isPublic: true, - tags: ['handbook', 'members', 'rules', 'guidelines'] - }, - ]; - - // Filter documents based on current filter settings - const filteredDocuments = allDocuments.filter(document => { - if (filter.category !== 'all' && document.category !== filter.category) return false; - - if (filter.isPublic !== 'all') { - const isPublicBool = filter.isPublic === 'public'; - if (document.isPublic !== isPublicBool) return false; - } - - if (filter.search) { - const searchLower = filter.search.toLowerCase(); - if (!document.title.toLowerCase().includes(searchLower) && - !document.description.toLowerCase().includes(searchLower) && - !document.tags.some(tag => tag.toLowerCase().includes(searchLower))) { - return false; - } - } - - return true; + // Filter state + const [filters, setFilters] = useState({ + documentType: 'all', + search: '', + isPublic: undefined as boolean | undefined, + page: 1, + limit: 10 }); - - // Handle filter changes - const handleFilterChange = (newFilter: { category: string; isPublic: string; search: string }) => { - setFilter(newFilter); - }; - - // Handle bulk actions - const handleBulkAction = (action: string) => { - // In a real app, this would call an API - alert(`Perform ${action} on ${selectedDocuments.length} documents`); - }; - - // Handle select all checkbox - const handleSelectAll = (event: React.ChangeEvent) => { - const checked = event.target.checked; - setSelectAll(checked); + + // Selected documents for bulk actions + const [selectedDocuments, setSelectedDocuments] = useState([]); + + // Confirmation dialog state + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [documentToDelete, setDocumentToDelete] = useState(null); + + // Apply filters + const handleFilterChange = (e: React.ChangeEvent) => { + const { name, value, type } = e.target; + if (type === 'checkbox') { + const checked = (e.target as HTMLInputElement).checked; + setFilters(prev => ({ + ...prev, + [name]: checked + })); + } else { + setFilters(prev => ({ + ...prev, + [name]: value + })); + } + }; + + // Apply filters + const applyFilters = () => { + // Reset page to 1 when applying new filters + setFilters(prev => ({ + ...prev, + page: 1 + })); + + fetchDocuments({ + ...filters, + page: 1 + }); + }; + + // Reset filters + const resetFilters = () => { + setFilters({ + documentType: 'all', + search: '', + isPublic: undefined, + page: 1, + limit: 10 + }); + + fetchDocuments({ + documentType: 'all', + search: '', + page: 1, + limit: 10 + }); + }; + + // Handle pagination + const handlePageChange = (newPage: number) => { + if (newPage < 1 || (pagination && newPage > pagination.pages)) { + return; + } + + setFilters(prev => ({ + ...prev, + page: newPage + })); + + fetchDocuments({ + ...filters, + page: newPage + }); + }; + + // Handle document selection + const handleSelectDocument = (id: string, checked: boolean) => { if (checked) { - setSelectedDocuments(filteredDocuments.map(doc => doc.id)); + setSelectedDocuments(prev => [...prev, id]); + } else { + setSelectedDocuments(prev => prev.filter(docId => docId !== id)); + } + }; + + // Handle select all documents + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedDocuments(documents.map(doc => doc._id)); } else { setSelectedDocuments([]); } }; - - // Handle individual checkbox selection - const handleSelectDocument = (documentId: string, checked: boolean) => { - if (checked) { - setSelectedDocuments(prev => [...prev, documentId]); - } else { - setSelectedDocuments(prev => prev.filter(id => id !== documentId)); + + // Handle delete confirmation + const handleDeleteClick = (id: string) => { + setDocumentToDelete(id); + setShowDeleteConfirm(true); + }; + + // Handle delete confirmation cancel + const handleDeleteCancel = () => { + setShowDeleteConfirm(false); + setDocumentToDelete(null); + }; + + // Handle delete confirmation confirm + const handleDeleteConfirm = async () => { + if (documentToDelete) { + const success = await deleteDocument(documentToDelete); + + if (success) { + // Remove from selected documents if it was selected + setSelectedDocuments(prev => prev.filter(id => id !== documentToDelete)); + } + + setShowDeleteConfirm(false); + setDocumentToDelete(null); } }; - - // Format date for display - const formatDate = (dateString: string) => { - const date = new Date(dateString); - return new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }).format(date); + + // Handle public status toggle + const handlePublicToggle = async (id: string, isPublic: boolean) => { + await updatePublicStatus(id, !isPublic); }; - - // Get file icon based on MIME type - const getFileIcon = (mimeType: string) => { - if (mimeType.includes('pdf')) { - return ( - - - - ); - } else if (mimeType.includes('word') || mimeType.includes('document')) { - return ( - - - - ); - } else if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) { - return ( - - - - ); - } else { - return ( - - - - ); - } + + // Handle accessibility checked toggle + const handleAccessibilityToggle = async (id: string, checked: boolean) => { + await markAccessibilityChecked(id, !checked); }; - + + // Get document type display name + const getDocumentTypeDisplayName = (type: string): string => { + return type + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + }; + + // Load documents on initial render + useEffect(() => { + fetchDocuments(filters); + }, [fetchDocuments]); + return ( -
-
-
-

Documents

-

- Manage bylaws, meeting minutes, forms, and other documents. -

+
+
+

Documents

+ + Add New Document + +
+ + {/* Filters */} +
+

Filters

+
+
+ + +
+ +
+ + +
+ +
+ + +
-
- + +
- - {/* Filter component */} - - - {/* Bulk actions */} - {selectedDocuments.length > 0 && ( -
-
- {selectedDocuments.length} document{selectedDocuments.length > 1 ? 's' : ''} selected -
-
- - + + {/* Error message */} + {error && ( +
+
+
+ + + +
+
+

+ Error loading documents +

+
+

{error.message}

+
+
)} - + + {/* Loading state */} + {loading && ( +
+ + + + +
+ )} + {/* Documents table */} -
+ {!loading && documents.length > 0 && (
- - - - + + - {filteredDocuments.map((document) => ( - + {documents.map((document) => ( + - - - - + + +
+
0} + onChange={(e) => handleSelectAll(e.target.checked)} /> -
- Document + Title - Category - - Size - - Dates + Type Visibility + + Accessibility + + Date + Actions
-
- handleSelectDocument(document.id, e.target.checked)} - /> - -
+ handleSelectDocument(document._id, e.target.checked)} + />
-
- {getFileIcon(document.mimeType)} +
+ + +
-
-
+
+
{document.title}
- {document.description} -
-
- {document.tags.map((tag) => ( - - {tag} - - ))} + {document.fileType.toUpperCase()} • {formatFileSize(document.fileSize)}
-
{document.category}
-
-
{document.fileSize}
-
-
- Uploaded: {formatDate(document.uploadDate)} -
-
- Modified: {formatDate(document.lastModified)} -
-
- - {document.isPublic ? 'Public' : 'Private'} + + {getDocumentTypeDisplayName(document.documentType)} - +
+ + {document.hasTextVersion && ( + + Text Version + + )} +
+
+ {formatDate(document.uploadDate)} + +
+ View - - + + Edit - @@ -479,13 +429,124 @@ export default function DocumentsPage() {
- - {filteredDocuments.length === 0 && ( -
-

No documents found matching the current filters.

+ )} + + {/* Empty state */} + {!loading && documents.length === 0 && ( +
+ + + +

No documents found

+

+ Get started by creating a new document. +

+
+ + Add New Document +
- )} -
+
+ )} + + {/* Pagination */} + {pagination && pagination.pages > 1 && ( +
+
+ Showing {(pagination.page - 1) * pagination.limit + 1} to{' '} + + {Math.min(pagination.page * pagination.limit, pagination.total)} + {' '} + of {pagination.total} results +
+ +
+ )} + + {/* Delete confirmation dialog */} + {showDeleteConfirm && ( +
+
+ + +
+
+
+
+ + + +
+
+

+ Delete Document +

+
+

+ Are you sure you want to delete this document? This action cannot be undone. +

+
+
+
+
+
+ + +
+
+
+
+ )}
); } diff --git a/frontend/src/app/api/uploads/[...path]/route.ts b/frontend/src/app/api/uploads/[...path]/route.ts index 6e9d07c..a47b557 100644 --- a/frontend/src/app/api/uploads/[...path]/route.ts +++ b/frontend/src/app/api/uploads/[...path]/route.ts @@ -12,8 +12,7 @@ export async function GET( ) { try { // Get the file path from the URL - const pathParams = await Promise.resolve(params.path); - const filePath = pathParams.join('/'); + const filePath = params.path.join('/'); // Construct the absolute path to the file // This assumes the backend is in the same directory as the frontend @@ -64,6 +63,17 @@ export async function GET( case '.txt': contentType = 'text/plain'; break; + case '.pdf': + contentType = 'application/pdf'; + break; + case '.doc': + case '.docx': + contentType = 'application/msword'; + break; + case '.ppt': + case '.pptx': + contentType = 'application/vnd.ms-powerpoint'; + break; } // Return the file with appropriate headers diff --git a/frontend/src/components/admin/DocumentForm.tsx b/frontend/src/components/admin/DocumentForm.tsx new file mode 100644 index 0000000..edb7862 --- /dev/null +++ b/frontend/src/components/admin/DocumentForm.tsx @@ -0,0 +1,560 @@ +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; +import { useRouter } from 'next/navigation'; +import { Document } from '../../hooks/useDocuments'; + +interface DocumentFormProps { + initialData?: Partial; + onSubmit: (data: FormData) => Promise; + isEdit?: boolean; +} + +interface FormErrors { + [key: string]: string; +} + +const DocumentForm: React.FC = ({ + initialData = {}, + onSubmit, + isEdit = false +}) => { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [formErrors, setFormErrors] = useState({}); + + // File input refs + const documentInputRef = useRef(null); + const textVersionInputRef = useRef(null); + + // Form state + const [formData, setFormData] = useState({ + title: '', + description: '', + documentType: 'meeting_minutes', + isPublic: false, + meetingDate: '', + accessibilityChecked: false, + hasTextVersion: false + }); + + // File state + const [documentFile, setDocumentFile] = useState(null); + const [textVersionFile, setTextVersionFile] = useState(null); + + // Preview state + const [documentPreview, setDocumentPreview] = useState(null); + const [documentName, setDocumentName] = useState(''); + const [documentSize, setDocumentSize] = useState(0); + const [documentType, setDocumentType] = useState(''); + + // Initialize form with initial data if provided + useEffect(() => { + if (initialData && Object.keys(initialData).length > 0) { + // Set basic form data + setFormData({ + title: initialData.title || '', + description: initialData.description || '', + documentType: initialData.documentType || 'meeting_minutes', + isPublic: initialData.isPublic || false, + meetingDate: initialData.meetingDate ? new Date(initialData.meetingDate).toISOString().split('T')[0] : '', + accessibilityChecked: initialData.accessibilityChecked || false, + hasTextVersion: initialData.hasTextVersion || false + }); + + // Set document size if available + if (initialData.fileSize) { + setDocumentSize(initialData.fileSize); + } + + // Set document name if available + if (initialData.originalFilename) { + setDocumentName(initialData.originalFilename); + } + + // Set document type if available + if (initialData.fileType) { + setDocumentType(initialData.fileType); + } + + // Set preview URLs if available + if (initialData.filePath) { + setDocumentPreview(`/api/uploads/${initialData.filePath}`); + } + } + }, [initialData]); + + // Handle input changes for text fields + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, value, type } = e.target as HTMLInputElement; + + // Handle checkbox inputs + if (type === 'checkbox') { + const checked = (e.target as HTMLInputElement).checked; + setFormData(prev => ({ + ...prev, + [name]: checked + })); + return; + } + + // Handle regular fields + setFormData(prev => ({ + ...prev, + [name]: value + })); + + // Clear error when user types + if (formErrors[name]) { + setFormErrors({ + ...formErrors, + [name]: undefined, + }); + } + }; + + // Handle file input changes + const handleFileChange = (e: React.ChangeEvent) => { + const { name, files } = e.target; + + if (!files || files.length === 0) return; + + const file = files[0]; + + // Handle different file types + switch (name) { + case 'document': + setDocumentFile(file); + setDocumentName(file.name); + setDocumentSize(file.size); + setDocumentType(file.name.split('.').pop()?.toLowerCase() || ''); + + // Create object URL for preview + const documentUrl = URL.createObjectURL(file); + setDocumentPreview(documentUrl); + + // Clear error + if (formErrors.document) { + setFormErrors({ + ...formErrors, + document: undefined, + }); + } + break; + + case 'textVersion': + setTextVersionFile(file); + setFormData(prev => ({ + ...prev, + hasTextVersion: true + })); + + // Clear error + if (formErrors.textVersion) { + setFormErrors({ + ...formErrors, + textVersion: undefined, + }); + } + break; + } + }; + + // Format file size for display + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + // Validate form + const validateForm = (): boolean => { + const errors: FormErrors = {}; + + // Required fields + if (!formData.title.trim()) { + errors.title = 'Title is required'; + } + + if (!formData.description.trim()) { + errors.description = 'Description is required'; + } + + if (!documentFile && !initialData.filePath) { + errors.document = 'Document file is required'; + } + + if (!formData.documentType) { + errors.documentType = 'Document type is required'; + } + + // Validate meeting date if document type includes "minutes" + if (formData.documentType.includes('minutes') && !formData.meetingDate) { + errors.meetingDate = 'Meeting date is required for minutes'; + } + + // Validate file types + if (documentFile) { + const allowedTypes = [ + 'pdf', 'doc', 'docx', 'txt', 'rtf', 'odt', 'ppt', 'pptx' + ]; + const fileExt = documentFile.name.split('.').pop()?.toLowerCase() || ''; + + if (!allowedTypes.includes(fileExt)) { + errors.document = 'Please upload a valid document file (PDF, DOC, DOCX, TXT, RTF, ODT, PPT, PPTX)'; + } + } + + if (textVersionFile && textVersionFile.type !== 'text/plain' && !textVersionFile.name.endsWith('.txt')) { + errors.textVersion = 'Please upload a valid text file (.txt)'; + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + // Handle form submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Validate form + if (!validateForm()) { + return; + } + + setLoading(true); + setError(null); + + try { + // Create FormData object + const formDataObj = new FormData(); + + // Add text fields + formDataObj.append('title', formData.title); + formDataObj.append('description', formData.description); + formDataObj.append('documentType', formData.documentType); + formDataObj.append('isPublic', formData.isPublic.toString()); + + if (formData.meetingDate) { + formDataObj.append('meetingDate', formData.meetingDate); + } + + formDataObj.append('accessibilityChecked', formData.accessibilityChecked.toString()); + + // Add files if they exist + if (documentFile) { + formDataObj.append('document', documentFile); + } + + if (textVersionFile) { + formDataObj.append('textversion', textVersionFile); + } + + // Submit form data + const result = await onSubmit(formDataObj); + + if (result) { + // Redirect to document details page or documents list + if (isEdit) { + router.push(`/admin/documents/${result._id}`); + } else { + router.push('/admin/documents'); + } + } + } catch (err: any) { + // Handle error + setError(err.message || 'Failed to save document. Please try again.'); + console.error('Error saving document:', err); + } finally { + setLoading(false); + } + }; + + // Get document type display name + const getDocumentTypeDisplayName = (type: string): string => { + return type + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + }; + + return ( +
+ {/* Error alert */} + {error && ( +
+
+
+ + + +
+
+

+ {error} +

+
+
+
+ )} + + {/* Basic Information */} +
+

Basic Information

+
+
+ + + {formErrors.title && ( +

{formErrors.title}

+ )} +
+ +
+ +