- 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 <object> 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.
300 lines
8.7 KiB
TypeScript
300 lines
8.7 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import axios from 'axios';
|
|
|
|
// Define Document interface
|
|
export interface Document {
|
|
_id: string;
|
|
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;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
// Define filter interface
|
|
export interface DocumentFilter {
|
|
documentType?: string;
|
|
search?: string;
|
|
isPublic?: boolean;
|
|
page?: number;
|
|
limit?: number;
|
|
}
|
|
|
|
// Define pagination interface
|
|
export interface Pagination {
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
pages: number;
|
|
}
|
|
|
|
// Define hook return interface
|
|
export interface UseDocumentsReturn {
|
|
documents: Document[];
|
|
loading: boolean;
|
|
error: Error | null;
|
|
pagination: Pagination | null;
|
|
fetchDocuments: (filters?: DocumentFilter) => Promise<void>;
|
|
getDocumentById: (id: string) => Promise<Document | null>;
|
|
createDocument: (formData: FormData) => Promise<Document | null>;
|
|
updateDocument: (id: string, documentData: Partial<Document>) => Promise<Document | null>;
|
|
deleteDocument: (id: string) => Promise<boolean>;
|
|
uploadTextVersion: (id: string, formData: FormData) => Promise<Document | null>;
|
|
updatePublicStatus: (id: string, isPublic: boolean) => Promise<Document | null>;
|
|
markAccessibilityChecked: (id: string, checked: boolean) => Promise<Document | null>;
|
|
}
|
|
|
|
// API base URL
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api';
|
|
|
|
/**
|
|
* Hook for managing documents data
|
|
*/
|
|
export function useDocuments(): UseDocumentsReturn {
|
|
const [documents, setDocuments] = useState<Document[]>([]);
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [pagination, setPagination] = useState<Pagination | null>(null);
|
|
|
|
/**
|
|
* Fetch documents with optional filtering
|
|
*/
|
|
const fetchDocuments = useCallback(async (filters?: DocumentFilter) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Build query string from filters
|
|
const queryParams = new URLSearchParams();
|
|
if (filters) {
|
|
Object.entries(filters).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
queryParams.append(key, String(value));
|
|
}
|
|
});
|
|
}
|
|
|
|
const response = await axios.get(`${API_URL}/documents?${queryParams.toString()}`);
|
|
setDocuments(response.data.documents);
|
|
setPagination(response.data.pagination);
|
|
} catch (err) {
|
|
console.error('Error fetching documents:', err);
|
|
setError(err instanceof Error ? err : new Error('Failed to fetch documents'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Get a single document by ID
|
|
*/
|
|
const getDocumentById = useCallback(async (id: string): Promise<Document | null> => {
|
|
try {
|
|
const response = await axios.get(`${API_URL}/documents/${id}`);
|
|
return response.data.document;
|
|
} catch (err) {
|
|
console.error(`Error fetching document with ID ${id}:`, err);
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Create a new document
|
|
*/
|
|
const createDocument = useCallback(async (formData: FormData): Promise<Document | null> => {
|
|
try {
|
|
const response = await axios.post(`${API_URL}/documents`, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
// Add authorization header if needed
|
|
// 'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
// Refresh documents list after creating
|
|
fetchDocuments();
|
|
|
|
return response.data.document;
|
|
} catch (err) {
|
|
console.error('Error creating document:', err);
|
|
// Rethrow the error so it can be caught and handled by the component
|
|
throw err;
|
|
}
|
|
}, [fetchDocuments]);
|
|
|
|
/**
|
|
* Update an existing document
|
|
*/
|
|
const updateDocument = useCallback(async (id: string, documentData: Partial<Document>): Promise<Document | null> => {
|
|
try {
|
|
const response = await axios.put(`${API_URL}/documents/${id}`, documentData, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
// Add authorization header if needed
|
|
// 'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
// Update local state
|
|
setDocuments(prevDocuments =>
|
|
prevDocuments.map(document =>
|
|
document._id === id ? { ...document, ...response.data.document } : document
|
|
)
|
|
);
|
|
|
|
return response.data.document;
|
|
} catch (err) {
|
|
console.error(`Error updating document with ID ${id}:`, err);
|
|
// Rethrow the error so it can be caught and handled by the component
|
|
throw err;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Delete a document
|
|
*/
|
|
const deleteDocument = useCallback(async (id: string): Promise<boolean> => {
|
|
try {
|
|
await axios.delete(`${API_URL}/documents/${id}`, {
|
|
headers: {
|
|
// Add authorization header if needed
|
|
// 'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
// Update local state
|
|
setDocuments(prevDocuments => prevDocuments.filter(document => document._id !== id));
|
|
|
|
return true;
|
|
} catch (err) {
|
|
console.error(`Error deleting document with ID ${id}:`, err);
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Upload text version for a document
|
|
*/
|
|
const uploadTextVersion = useCallback(async (id: string, formData: FormData): Promise<Document | null> => {
|
|
try {
|
|
const response = await axios.post(
|
|
`${API_URL}/documents/${id}/textversion`,
|
|
formData,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
// Add authorization header if needed
|
|
// 'Authorization': `Bearer ${token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
// Update local state
|
|
setDocuments(prevDocuments =>
|
|
prevDocuments.map(document =>
|
|
document._id === id ? { ...document, ...response.data.document } : document
|
|
)
|
|
);
|
|
|
|
return response.data.document;
|
|
} catch (err) {
|
|
console.error(`Error uploading text version for document with ID ${id}:`, err);
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Update document public status
|
|
*/
|
|
const updatePublicStatus = useCallback(async (id: string, isPublic: boolean): Promise<Document | null> => {
|
|
try {
|
|
const response = await axios.put(
|
|
`${API_URL}/documents/${id}/public`,
|
|
{ isPublic },
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
// Add authorization header if needed
|
|
// 'Authorization': `Bearer ${token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
// Update local state
|
|
setDocuments(prevDocuments =>
|
|
prevDocuments.map(document =>
|
|
document._id === id ? { ...document, ...response.data.document } : document
|
|
)
|
|
);
|
|
|
|
return response.data.document;
|
|
} catch (err) {
|
|
console.error(`Error updating public status for document with ID ${id}:`, err);
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Mark document as accessibility checked
|
|
*/
|
|
const markAccessibilityChecked = useCallback(async (id: string, accessibilityChecked: boolean): Promise<Document | null> => {
|
|
try {
|
|
const response = await axios.put(
|
|
`${API_URL}/documents/${id}/accessibility`,
|
|
{ accessibilityChecked },
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
// Add authorization header if needed
|
|
// 'Authorization': `Bearer ${token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
// Update local state
|
|
setDocuments(prevDocuments =>
|
|
prevDocuments.map(document =>
|
|
document._id === id ? { ...document, ...response.data.document } : document
|
|
)
|
|
);
|
|
|
|
return response.data.document;
|
|
} catch (err) {
|
|
console.error(`Error marking document with ID ${id} as accessibility checked:`, err);
|
|
return null;
|
|
}
|
|
}, []);
|
|
|
|
// Load documents on initial render
|
|
useEffect(() => {
|
|
fetchDocuments();
|
|
}, [fetchDocuments]);
|
|
|
|
return {
|
|
documents,
|
|
loading,
|
|
error,
|
|
pagination,
|
|
fetchDocuments,
|
|
getDocumentById,
|
|
createDocument,
|
|
updateDocument,
|
|
deleteDocument,
|
|
uploadTextVersion,
|
|
updatePublicStatus,
|
|
markAccessibilityChecked
|
|
};
|
|
}
|