Skip to content

Commit

Permalink
Merge pull request #1 from Nine-Minds/new-documents-screen
Browse files Browse the repository at this point in the history
New documents screen
  • Loading branch information
RobertAtNineMinds authored Nov 5, 2024
2 parents 5b999fd + f5edd33 commit 21f3a50
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 76 deletions.
4 changes: 4 additions & 0 deletions docs/AI_coding_standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
173 changes: 173 additions & 0 deletions server/src/app/msp/documents/page.tsx
Original file line number Diff line number Diff line change
@@ -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<IDocument[]>([]);
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<HTMLInputElement>) => {
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 (
<div className="p-6">
<div className="mb-6">
<h1 className="text-2xl font-semibold">Documents</h1>
</div>

<div className="flex gap-6">
{/* Left Column - Filters */}
<div className="w-80">
<Card className="p-4 sticky top-6">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Search Documents
</label>
<Input
placeholder="Search by document name..."
value={filterInputs.searchTerm}
onChange={(e) => setFilterInputs({ ...filterInputs, searchTerm: e.target.value })}
onKeyPress={handleKeyPress}
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Document Type
</label>
<CustomSelect
options={documentTypes}
value={filterInputs.type}
onValueChange={(value: string) => {
setFilterInputs({ ...filterInputs, type: value });
handleSearch();
}}
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Entity Type
</label>
<CustomSelect
options={entityTypes}
value={filterInputs.entityType}
onValueChange={(value: string) => {
setFilterInputs({ ...filterInputs, entityType: value });
handleSearch();
}}
placeholder="All Entities"
/>
</div>

<div className="pt-4">
<button
onClick={handleClearFilters}
className="w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Clear Filters
</button>
</div>
</div>
</Card>
</div>

{/* Right Column - Documents */}
<div className="flex-1">
<Card className="p-4">
<Documents
documents={documents}
gridColumns={3}
userId="current-user-id"
filters={filterInputs}
isLoading={isLoading}
onDocumentCreated={handleDocumentUpdate}
/>
</Card>
</div>
</div>
</div>
);
}
85 changes: 34 additions & 51 deletions server/src/components/documents/Documents.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<IDocument[]>(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<HTMLInputElement>): 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
Expand All @@ -65,49 +56,34 @@ 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
const handleDelete = async (document: IDocument) => {
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);
toast.error('Failed to delete document. Please try again.');
}
};

// Filter documents based on document name search term
const filteredDocuments = documents.filter(doc =>
doc.document_name.toLowerCase().includes(searchTerm.toLowerCase())
);

return (
<div>
<div className="flex justify-between items-center mb-3 space-x-4 flex-wrap">
{/* Search */}
<input
type="text"
placeholder="Search"
className="px-4 py-1 border border-gray-300 rounded-md w-48"
value={searchTerm}
onChange={handleSearch}
/>

<div className="flex justify-between items-center mb-3">
{/* New document button */}
<button
className="bg-[#6941C6] text-white px-4 py-1 rounded-md whitespace-nowrap"
Expand All @@ -128,9 +104,16 @@ const Documents = ({ documents: initialDocuments, gridColumns, userId, companyId
</div>
)}

{/* Loading State */}
{isLoading && (
<div className="flex justify-center items-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#6941C6]"></div>
</div>
)}

{/* Documents */}
<div className={`grid ${gridColumnsClass} gap-2 items-start`}>
{filteredDocuments.map((document: IDocument): JSX.Element => (
{!isLoading && documents.map((document: IDocument): JSX.Element => (
<DocumentStorageCard
key={document.document_id || document.file_id}
document={document}
Expand Down
40 changes: 40 additions & 0 deletions server/src/components/ui/DateRangePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { Label } from './Label';
import { Input } from './Input';

interface DateRange {
from: string;
to: string;
}

interface DateRangePickerProps {
label?: string;
value: DateRange;
onChange: (range: DateRange) => void;
}

export const DateRangePicker: React.FC<DateRangePickerProps> = ({
label,
value,
onChange
}) => {
return (
<div className="space-y-2">
{label && <Label>{label}</Label>}
<div className="flex gap-2">
<Input
type="date"
value={value.from}
onChange={(e) => onChange({ ...value, from: e.target.value })}
placeholder="From"
/>
<Input
type="date"
value={value.to}
onChange={(e) => onChange({ ...value, to: e.target.value })}
placeholder="To"
/>
</div>
</div>
);
};
Loading

0 comments on commit 21f3a50

Please sign in to comment.