From f5edd33912bc6b73721b776690f9ca09355d352f Mon Sep 17 00:00:00 2001 From: Robert Isaacs Date: Mon, 4 Nov 2024 13:38:29 -0500 Subject: [PATCH] New documents screen --- docs/AI_coding_standards.md | 4 + server/src/app/msp/documents/page.tsx | 173 ++++++++++++++++++ server/src/components/documents/Documents.tsx | 85 ++++----- server/src/components/ui/DateRangePicker.tsx | 40 ++++ server/src/config/menuConfig.ts | 22 +-- server/src/interfaces/document.interface.tsx | 4 - .../document-actions/documentActions.ts | 81 +++++++- 7 files changed, 333 insertions(+), 76 deletions(-) create mode 100644 server/src/app/msp/documents/page.tsx create mode 100644 server/src/components/ui/DateRangePicker.tsx diff --git a/docs/AI_coding_standards.md b/docs/AI_coding_standards.md index f70071e5..fcfbf670 100644 --- a/docs/AI_coding_standards.md +++ b/docs/AI_coding_standards.md @@ -20,6 +20,10 @@ Prefer radix components over other libraries - [Table](../server/src/components/ui/Table.tsx) - [TextArea](../server/src/components/ui/TextArea.tsx) +## Server Communication + +We use server actions that are located in the `/server/src/lib/actions` folder. + # Database server migrations are stored in the `/server/migrations` folder. seeds are stored in the `/server/seeds` folder. diff --git a/server/src/app/msp/documents/page.tsx b/server/src/app/msp/documents/page.tsx new file mode 100644 index 00000000..91bbf84f --- /dev/null +++ b/server/src/app/msp/documents/page.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useState, useEffect, KeyboardEvent } from 'react'; +import { IDocument } from '../../../interfaces/document.interface'; +import Documents from '../../../components/documents/Documents'; +import { Card } from '../../../components/ui/Card'; +import { Input } from '../../../components/ui/Input'; +import CustomSelect from '../../../components/ui/CustomSelect'; +import { SelectOption } from '../../../components/ui/Select'; +import { getAllDocuments } from '../../../lib/actions/document-actions/documentActions'; +import { toast } from 'react-hot-toast'; + +export default function DocumentsPage() { + const [documents, setDocuments] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const [filterInputs, setFilterInputs] = useState({ + type: 'all', + entityType: '', + searchTerm: '' + }); + + const documentTypes: SelectOption[] = [ + { value: 'all', label: 'All Document Types' }, + { value: 'application/pdf', label: 'PDF' }, + { value: 'image', label: 'Images' }, + { value: 'text', label: 'Documents' }, + { value: 'application', label: 'Other' } + ]; + + const entityTypes: SelectOption[] = [ + { value: 'ticket', label: 'Tickets' }, + { value: 'client', label: 'Clients' }, + { value: 'contact', label: 'Contacts' }, + { value: 'project', label: 'Projects' } + ]; + + const handleSearch = async () => { + try { + setIsLoading(true); + // Only include type in filters if it's not 'all' + const searchFilters = { + ...filterInputs, + type: filterInputs.type === 'all' ? '' : filterInputs.type + }; + const docs = await getAllDocuments(searchFilters); + setDocuments(docs); + } catch (error) { + console.error('Error fetching documents:', error); + toast.error('Failed to fetch documents'); + } finally { + setIsLoading(false); + } + }; + + // Run initial search on component mount + useEffect(() => { + handleSearch(); + }, []); // Empty dependency array means this runs once on mount + + const handleKeyPress = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleSearch(); + } + }; + + const handleDocumentUpdate = async () => { + try { + setIsLoading(true); + const searchFilters = { + ...filterInputs, + type: filterInputs.type === 'all' ? '' : filterInputs.type + }; + const updatedDocs = await getAllDocuments(searchFilters); + setDocuments(updatedDocs); + } catch (error) { + console.error('Error refreshing documents:', error); + toast.error('Failed to refresh documents'); + } finally { + setIsLoading(false); + } + }; + + const handleClearFilters = () => { + setFilterInputs({ + type: 'all', + entityType: '', + searchTerm: '' + }); + handleSearch(); + }; + + return ( +
+
+

Documents

+
+ +
+ {/* Left Column - Filters */} +
+ +
+
+ + setFilterInputs({ ...filterInputs, searchTerm: e.target.value })} + onKeyPress={handleKeyPress} + /> +
+ +
+ + { + setFilterInputs({ ...filterInputs, type: value }); + handleSearch(); + }} + /> +
+ +
+ + { + setFilterInputs({ ...filterInputs, entityType: value }); + handleSearch(); + }} + placeholder="All Entities" + /> +
+ +
+ +
+
+
+
+ + {/* Right Column - Documents */} +
+ + + +
+
+
+ ); +} diff --git a/server/src/components/documents/Documents.tsx b/server/src/components/documents/Documents.tsx index 75774b2e..6295ee5a 100644 --- a/server/src/components/documents/Documents.tsx +++ b/server/src/components/documents/Documents.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { IDocument, IDocumentUploadResponse } from '../../interfaces/document.interface'; import DocumentStorageCard from './DocumentStorageCard'; import DocumentUpload from './DocumentUpload'; import DocumentsPagination from './DocumentsPagination'; -import { getDocumentByCompanyId, deleteDocument } from '../../lib/actions/document-actions/documentActions'; +import { getAllDocuments, deleteDocument } from '../../lib/actions/document-actions/documentActions'; import { toast } from 'react-hot-toast'; interface DocumentsProps { @@ -14,40 +14,31 @@ interface DocumentsProps { userId: string; companyId?: string; onDocumentCreated?: (document: IDocument) => void; + filters?: { + type?: string; + entityType?: string; + uploadedBy?: string; + searchTerm?: string; + }; + isLoading?: boolean; } -const Documents = ({ documents: initialDocuments, gridColumns, userId, companyId, onDocumentCreated }: DocumentsProps): JSX.Element => { - const [documents, setDocuments] = useState(initialDocuments); - const [searchTerm, setSearchTerm] = useState(''); +const Documents = ({ + documents, + gridColumns, + userId, + companyId, + onDocumentCreated, + filters, + isLoading = false +}: DocumentsProps): JSX.Element => { const [showUpload, setShowUpload] = useState(false); - // Fetch latest documents whenever component mounts or companyId changes - useEffect(() => { - const fetchLatestDocuments = async () => { - if (companyId) { - try { - const latestDocuments = await getDocumentByCompanyId(companyId); - setDocuments(latestDocuments); - } catch (error) { - console.error('Error fetching documents:', error); - toast.error('Failed to fetch documents'); - } - } - }; - - fetchLatestDocuments(); - }, [companyId]); - // Set grid columns based on the number of columns const gridColumnsClass = gridColumns === 4 ? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4' : 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'; - // Set filter based on search term - const handleSearch = (e: React.ChangeEvent): void => { - setSearchTerm(e.target.value); - }; - // Handle file upload completion - const handleUploadComplete = (fileData: IDocumentUploadResponse) => { + const handleUploadComplete = async (fileData: IDocumentUploadResponse) => { setShowUpload(false); const newDocument: IDocument = { document_id: '', // This will be set by the server @@ -65,13 +56,12 @@ const Documents = ({ documents: initialDocuments, gridColumns, userId, companyId file_size: fileData.file_size }; - // Update local state with the new document - setDocuments(prevDocuments => [...prevDocuments, newDocument]); - // Call the parent callback if provided if (onDocumentCreated) { onDocumentCreated(newDocument); } + + // Parent will handle refreshing the documents list }; // Handle document deletion @@ -79,11 +69,11 @@ const Documents = ({ documents: initialDocuments, gridColumns, userId, companyId try { if (document.document_id) { await deleteDocument(document.document_id, userId); - // Update local state to remove the deleted document - setDocuments(prevDocuments => - prevDocuments.filter(doc => doc.document_id !== document.document_id) - ); toast.success('Document deleted successfully'); + // Trigger a refresh in the parent component + if (onDocumentCreated) { + onDocumentCreated(document); // Reuse this callback to trigger refresh + } } } catch (error) { console.error('Error deleting document:', error); @@ -91,23 +81,9 @@ const Documents = ({ documents: initialDocuments, gridColumns, userId, companyId } }; - // Filter documents based on document name search term - const filteredDocuments = documents.filter(doc => - doc.document_name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - return (
-
- {/* Search */} - - +
{/* New document button */}