feat: comprehensive project structure improvements and Cloud for Sovereignty landing zone
- Add Cloud for Sovereignty landing zone architecture and deployment - Implement complete legal document management system - Reorganize documentation with improved navigation - Add infrastructure improvements (Dockerfiles, K8s, monitoring) - Add operational improvements (graceful shutdown, rate limiting, caching) - Create comprehensive project structure documentation - Add Azure deployment automation scripts - Improve repository navigation and organization
This commit is contained in:
86
apps/README.md
Normal file
86
apps/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Applications Directory
|
||||
|
||||
**Last Updated**: 2025-01-27
|
||||
**Purpose**: Frontend applications overview
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains frontend applications built with React, Next.js, and TypeScript.
|
||||
|
||||
## Available Applications
|
||||
|
||||
### MCP Legal (`mcp-legal/`)
|
||||
- **Purpose**: Legal document management portal
|
||||
- **Technology**: React, Material-UI, React Query
|
||||
- **Features**: Document management, matter management, template library
|
||||
- **Documentation**: [MCP Legal README](mcp-legal/README.md)
|
||||
|
||||
### Portal Public (`portal-public/`)
|
||||
- **Purpose**: Public-facing member portal
|
||||
- **Technology**: Next.js, React, Tailwind CSS
|
||||
- **Features**: Member services, credential management
|
||||
- **Documentation**: [Portal Public README](portal-public/README.md)
|
||||
|
||||
### Portal Internal (`portal-internal/`)
|
||||
- **Purpose**: Internal administrative portal
|
||||
- **Technology**: Next.js, React, Tailwind CSS
|
||||
- **Features**: Administration, reporting, analytics
|
||||
- **Documentation**: [Portal Internal README](portal-internal/README.md)
|
||||
|
||||
## Application Structure
|
||||
|
||||
All applications follow a consistent structure:
|
||||
|
||||
```
|
||||
app/
|
||||
├── src/
|
||||
│ ├── app/ # Next.js app directory (if using App Router)
|
||||
│ ├── components/ # React components
|
||||
│ ├── pages/ # Next.js pages (if using Pages Router)
|
||||
│ ├── hooks/ # React hooks
|
||||
│ ├── utils/ # Utility functions
|
||||
│ └── types/ # TypeScript types
|
||||
├── public/ # Static assets
|
||||
├── package.json # Dependencies
|
||||
└── README.md # Application documentation
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Applications
|
||||
|
||||
```bash
|
||||
# Start all applications
|
||||
pnpm dev
|
||||
|
||||
# Start specific application
|
||||
pnpm --filter portal-public dev
|
||||
```
|
||||
|
||||
### Building Applications
|
||||
|
||||
```bash
|
||||
# Build all applications
|
||||
pnpm build
|
||||
|
||||
# Build specific application
|
||||
pnpm --filter portal-public build
|
||||
```
|
||||
|
||||
## Shared Components
|
||||
|
||||
Applications use shared UI components from `packages/ui/`:
|
||||
```typescript
|
||||
import { Button, Card, Modal } from '@the-order/ui';
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Project Structure](../PROJECT_STRUCTURE.md)
|
||||
- [Packages Documentation](../packages/)
|
||||
- [Architecture Documentation](../docs/architecture/)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-01-27
|
||||
|
||||
230
apps/mcp-legal/src/components/CourtFiling.tsx
Normal file
230
apps/mcp-legal/src/components/CourtFiling.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Court Filing Component
|
||||
* UI for court filing management
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Alert,
|
||||
} from '@mui/material';
|
||||
import { Add as AddIcon, CalendarToday as CalendarIcon } from '@mui/icons-material';
|
||||
|
||||
interface Filing {
|
||||
id: string;
|
||||
document_id: string;
|
||||
matter_id: string;
|
||||
court_name: string;
|
||||
case_number?: string;
|
||||
filing_type: string;
|
||||
status: string;
|
||||
filing_deadline?: string;
|
||||
}
|
||||
|
||||
export function CourtFiling({ matterId }: { matterId: string }) {
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: filings } = useQuery<Filing[]>({
|
||||
queryKey: ['filings', matterId],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`/api/filings?matter_id=${matterId}`);
|
||||
const data = await response.json();
|
||||
return data.filings || [];
|
||||
},
|
||||
});
|
||||
|
||||
const { data: deadlines } = useQuery<Filing[]>({
|
||||
queryKey: ['filing-deadlines', matterId],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`/api/matters/${matterId}/filing-deadlines`);
|
||||
const data = await response.json();
|
||||
return data.deadlines || [];
|
||||
},
|
||||
});
|
||||
|
||||
const createFiling = useMutation({
|
||||
mutationFn: async (filing: Partial<Filing>) => {
|
||||
const response = await fetch('/api/filings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(filing),
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['filings', matterId] });
|
||||
setCreateDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateFiling = (formData: FormData) => {
|
||||
createFiling.mutate({
|
||||
matter_id: matterId,
|
||||
document_id: formData.get('document_id') as string,
|
||||
court_name: formData.get('court_name') as string,
|
||||
case_number: formData.get('case_number') as string,
|
||||
filing_type: formData.get('filing_type') as string,
|
||||
filing_deadline: formData.get('filing_deadline') as string,
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'filed':
|
||||
return 'success';
|
||||
case 'accepted':
|
||||
return 'success';
|
||||
case 'rejected':
|
||||
return 'error';
|
||||
case 'submitted':
|
||||
return 'info';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Typography variant="h5">Court Filings</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
>
|
||||
New Filing
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{deadlines && deadlines.length > 0 && (
|
||||
<Alert severity="warning" sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle2">Upcoming Deadlines</Typography>
|
||||
{deadlines.map((deadline) => (
|
||||
<Typography key={deadline.id} variant="body2">
|
||||
{deadline.court_name}: {new Date(deadline.filing_deadline!).toLocaleDateString()}
|
||||
</Typography>
|
||||
))}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Court</TableCell>
|
||||
<TableCell>Case Number</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Deadline</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filings?.map((filing) => (
|
||||
<TableRow key={filing.id}>
|
||||
<TableCell>{filing.court_name}</TableCell>
|
||||
<TableCell>{filing.case_number || 'N/A'}</TableCell>
|
||||
<TableCell>{filing.filing_type}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={filing.status} size="small" color={getStatusColor(filing.status)} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{filing.filing_deadline ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
<CalendarIcon sx={{ mr: 1, fontSize: 16 }} />
|
||||
{new Date(filing.filing_deadline).toLocaleDateString()}
|
||||
</Box>
|
||||
) : (
|
||||
'N/A'
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreateFiling(new FormData(e.currentTarget));
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Create Court Filing</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
name="document_id"
|
||||
label="Document ID"
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="court_name"
|
||||
label="Court Name"
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="case_number"
|
||||
label="Case Number"
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<FormControl fullWidth margin="dense" sx={{ mb: 2 }}>
|
||||
<InputLabel>Filing Type</InputLabel>
|
||||
<Select name="filing_type" label="Filing Type" required>
|
||||
<MenuItem value="pleading">Pleading</MenuItem>
|
||||
<MenuItem value="motion">Motion</MenuItem>
|
||||
<MenuItem value="brief">Brief</MenuItem>
|
||||
<MenuItem value="exhibit">Exhibit</MenuItem>
|
||||
<MenuItem value="affidavit">Affidavit</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="filing_deadline"
|
||||
label="Filing Deadline"
|
||||
type="date"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateDialogOpen(false)}>Cancel</Button>
|
||||
<Button type="submit" variant="contained">
|
||||
Create Filing
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
208
apps/mcp-legal/src/components/DocumentAssembly.tsx
Normal file
208
apps/mcp-legal/src/components/DocumentAssembly.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Document Assembly Component
|
||||
* UI for template-based document generation
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
TextField,
|
||||
Typography,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { CheckCircle as CheckCircleIcon } from '@mui/icons-material';
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
variables?: string[];
|
||||
}
|
||||
|
||||
export function DocumentAssembly() {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('');
|
||||
const [variables, setVariables] = useState<Record<string, string>>({});
|
||||
const [preview, setPreview] = useState<string>('');
|
||||
|
||||
const { data: templates } = useQuery<Template[]>({
|
||||
queryKey: ['templates'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('/api/templates');
|
||||
const data = await response.json();
|
||||
return data.templates || [];
|
||||
},
|
||||
});
|
||||
|
||||
const previewMutation = useMutation({
|
||||
mutationFn: async ({ templateId, vars }: { templateId: string; vars: Record<string, string> }) => {
|
||||
const response = await fetch(`/api/templates/${templateId}/render`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ variables: vars }),
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setPreview(data.rendered);
|
||||
setActiveStep(2);
|
||||
},
|
||||
});
|
||||
|
||||
const generateMutation = useMutation({
|
||||
mutationFn: async (data: any) => {
|
||||
const response = await fetch('/api/assembly/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
setActiveStep(3);
|
||||
},
|
||||
});
|
||||
|
||||
const handleNext = () => {
|
||||
if (activeStep === 0 && selectedTemplate) {
|
||||
// Get template variables
|
||||
fetch(`/api/templates/${selectedTemplate}/variables`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const vars: Record<string, string> = {};
|
||||
data.variables?.forEach((v: string) => {
|
||||
vars[v] = '';
|
||||
});
|
||||
setVariables(vars);
|
||||
setActiveStep(1);
|
||||
});
|
||||
} else if (activeStep === 1) {
|
||||
previewMutation.mutate({ templateId: selectedTemplate, vars: variables });
|
||||
} else if (activeStep === 2) {
|
||||
generateMutation.mutate({
|
||||
template_id: selectedTemplate,
|
||||
variables,
|
||||
title: `Document from ${templates?.find((t) => t.id === selectedTemplate)?.name}`,
|
||||
save_document: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const steps = ['Select Template', 'Enter Variables', 'Preview', 'Complete'];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Document Assembly
|
||||
</Typography>
|
||||
|
||||
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
{activeStep === 0 && (
|
||||
<Grid container spacing={3}>
|
||||
{templates?.map((template) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={template.id}>
|
||||
<Card
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
border: selectedTemplate === template.id ? 2 : 1,
|
||||
borderColor: selectedTemplate === template.id ? 'primary.main' : 'divider',
|
||||
}}
|
||||
onClick={() => setSelectedTemplate(template.id)}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography variant="h6">{template.name}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{activeStep === 1 && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Enter Variable Values
|
||||
</Typography>
|
||||
{Object.keys(variables).map((key) => (
|
||||
<TextField
|
||||
key={key}
|
||||
fullWidth
|
||||
label={key}
|
||||
value={variables[key]}
|
||||
onChange={(e) => setVariables({ ...variables, [key]: e.target.value })}
|
||||
margin="normal"
|
||||
/>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeStep === 2 && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Preview
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: 'grey.100',
|
||||
borderRadius: 1,
|
||||
whiteSpace: 'pre-wrap',
|
||||
maxHeight: 400,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{preview || 'Generating preview...'}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeStep === 3 && (
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center' }}>
|
||||
<CheckCircleIcon color="success" sx={{ fontSize: 64, mb: 2 }} />
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Document Generated Successfully!
|
||||
</Typography>
|
||||
<Button variant="contained" href="/documents">
|
||||
View Documents
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Box display="flex" justifyContent="space-between" mt={4}>
|
||||
<Button disabled={activeStep === 0} onClick={() => setActiveStep(activeStep - 1)}>
|
||||
Back
|
||||
</Button>
|
||||
{activeStep < 3 && (
|
||||
<Button variant="contained" onClick={handleNext} disabled={!selectedTemplate}>
|
||||
{activeStep === 2 ? 'Generate Document' : 'Next'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
286
apps/mcp-legal/src/components/DocumentManagement.tsx
Normal file
286
apps/mcp-legal/src/components/DocumentManagement.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Document Management Component
|
||||
* Main UI for document management in MCP Legal app
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
Visibility as ViewIcon,
|
||||
History as HistoryIcon,
|
||||
Download as DownloadIcon,
|
||||
Share as ShareIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface DocumentManagementProps {
|
||||
matterId?: string;
|
||||
}
|
||||
|
||||
export function DocumentManagement({ matterId }: DocumentManagementProps) {
|
||||
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [viewDialogOpen, setViewDialogOpen] = useState(false);
|
||||
const [filterType, setFilterType] = useState<string>('all');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch documents
|
||||
const { data: documents, isLoading } = useQuery<Document[]>({
|
||||
queryKey: ['documents', matterId, filterType],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (matterId) params.append('matter_id', matterId);
|
||||
if (filterType !== 'all') params.append('type', filterType);
|
||||
const response = await fetch(`/api/documents?${params}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch documents');
|
||||
const data = await response.json();
|
||||
return data.documents || [];
|
||||
},
|
||||
});
|
||||
|
||||
// Create document mutation
|
||||
const createDocument = useMutation({
|
||||
mutationFn: async (doc: Partial<Document>) => {
|
||||
const response = await fetch('/api/documents', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(doc),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to create document');
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] });
|
||||
setCreateDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
// Delete document mutation
|
||||
const deleteDocument = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const response = await fetch(`/api/documents/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete document');
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateDocument = (formData: FormData) => {
|
||||
createDocument.mutate({
|
||||
title: formData.get('title') as string,
|
||||
type: formData.get('type') as string,
|
||||
content: formData.get('content') as string,
|
||||
matter_id: matterId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewDocument = (doc: Document) => {
|
||||
setSelectedDocument(doc);
|
||||
setViewDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteDocument = (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this document?')) {
|
||||
deleteDocument.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Typography variant="h4">Documents</Typography>
|
||||
<Box>
|
||||
<FormControl size="small" sx={{ minWidth: 120, mr: 2 }}>
|
||||
<InputLabel>Filter</InputLabel>
|
||||
<Select
|
||||
value={filterType}
|
||||
label="Filter"
|
||||
onChange={(e) => setFilterType(e.target.value)}
|
||||
>
|
||||
<MenuItem value="all">All</MenuItem>
|
||||
<MenuItem value="legal">Legal</MenuItem>
|
||||
<MenuItem value="treaty">Treaty</MenuItem>
|
||||
<MenuItem value="finance">Finance</MenuItem>
|
||||
<MenuItem value="history">History</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
>
|
||||
New Document
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isLoading ? (
|
||||
<Typography>Loading...</Typography>
|
||||
) : (
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Title</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{documents?.map((doc) => (
|
||||
<TableRow key={doc.id}>
|
||||
<TableCell>{doc.title}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={doc.type} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={doc.status} size="small" color="primary" />
|
||||
</TableCell>
|
||||
<TableCell>{new Date(doc.created_at).toLocaleDateString()}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title="View">
|
||||
<IconButton size="small" onClick={() => handleViewDocument(doc)}>
|
||||
<ViewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="History">
|
||||
<IconButton size="small" href={`/documents/${doc.id}/versions`}>
|
||||
<HistoryIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Download">
|
||||
<IconButton size="small">
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => handleDeleteDocument(doc.id)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Create Document Dialog */}
|
||||
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreateDocument(new FormData(e.currentTarget));
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Create New Document</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
name="title"
|
||||
label="Title"
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<FormControl fullWidth margin="dense">
|
||||
<InputLabel>Type</InputLabel>
|
||||
<Select name="type" label="Type" required>
|
||||
<MenuItem value="legal">Legal</MenuItem>
|
||||
<MenuItem value="treaty">Treaty</MenuItem>
|
||||
<MenuItem value="finance">Finance</MenuItem>
|
||||
<MenuItem value="history">History</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="content"
|
||||
label="Content"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={10}
|
||||
sx={{ mt: 2 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateDialogOpen(false)}>Cancel</Button>
|
||||
<Button type="submit" variant="contained">
|
||||
Create
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
{/* View Document Dialog */}
|
||||
<Dialog open={viewDialogOpen} onClose={() => setViewDialogOpen(false)} maxWidth="lg" fullWidth>
|
||||
<DialogTitle>{selectedDocument?.title}</DialogTitle>
|
||||
<DialogContent>
|
||||
{selectedDocument && (
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Type: {selectedDocument.type} | Status: {selectedDocument.status}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Created: {new Date(selectedDocument.created_at).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setViewDialogOpen(false)}>Close</Button>
|
||||
<Button variant="contained" href={`/documents/${selectedDocument?.id}`}>
|
||||
Open Full View
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
218
apps/mcp-legal/src/components/DocumentWorkflow.tsx
Normal file
218
apps/mcp-legal/src/components/DocumentWorkflow.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Document Workflow Component
|
||||
* UI for workflow management and approval
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
LinearProgress,
|
||||
Typography,
|
||||
Chip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CheckCircle as ApproveIcon,
|
||||
Cancel as RejectIcon,
|
||||
Assignment as AssignmentIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface Workflow {
|
||||
id: string;
|
||||
document_id: string;
|
||||
workflow_type: string;
|
||||
status: string;
|
||||
steps: WorkflowStep[];
|
||||
}
|
||||
|
||||
interface WorkflowStep {
|
||||
id: string;
|
||||
step_number: number;
|
||||
step_type: string;
|
||||
status: string;
|
||||
assigned_to?: string;
|
||||
due_date?: string;
|
||||
}
|
||||
|
||||
export function DocumentWorkflow({ documentId }: { documentId: string }) {
|
||||
const [approvalDialogOpen, setApprovalDialogOpen] = useState(false);
|
||||
const [selectedStep, setSelectedStep] = useState<WorkflowStep | null>(null);
|
||||
const [comments, setComments] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: workflow } = useQuery<Workflow>({
|
||||
queryKey: ['workflow', documentId],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`/api/documents/${documentId}/workflows`);
|
||||
const data = await response.json();
|
||||
return data.workflows?.[0] || null;
|
||||
},
|
||||
});
|
||||
|
||||
const approveStep = useMutation({
|
||||
mutationFn: async ({ stepId, comments: cmts }: { stepId: string; comments?: string }) => {
|
||||
const response = await fetch(`/api/workflows/steps/${stepId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'approved', comments: cmts }),
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['workflow', documentId] });
|
||||
setApprovalDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const rejectStep = useMutation({
|
||||
mutationFn: async ({ stepId, comments: cmts }: { stepId: string; comments?: string }) => {
|
||||
const response = await fetch(`/api/workflows/steps/${stepId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'rejected', comments: cmts }),
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['workflow', documentId] });
|
||||
setApprovalDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
if (!workflow) {
|
||||
return <Typography>No workflow found for this document</Typography>;
|
||||
}
|
||||
|
||||
const completedSteps = workflow.steps?.filter((s) => s.status === 'approved' || s.status === 'rejected').length || 0;
|
||||
const totalSteps = workflow.steps?.length || 1;
|
||||
const progress = (completedSteps / totalSteps) * 100;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Typography variant="h6">Workflow: {workflow.workflow_type}</Typography>
|
||||
<Chip label={workflow.status} color={workflow.status === 'completed' ? 'success' : 'default'} />
|
||||
</Box>
|
||||
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Progress
|
||||
</Typography>
|
||||
<LinearProgress variant="determinate" value={progress} sx={{ mb: 1 }} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{completedSteps} of {totalSteps} steps completed
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Workflow Steps
|
||||
</Typography>
|
||||
<List>
|
||||
{workflow.steps?.map((step) => (
|
||||
<ListItem
|
||||
key={step.id}
|
||||
secondaryAction={
|
||||
step.status === 'pending' || step.status === 'in_progress' ? (
|
||||
<Box>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<ApproveIcon />}
|
||||
color="success"
|
||||
onClick={() => {
|
||||
setSelectedStep(step);
|
||||
setApprovalDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<RejectIcon />}
|
||||
color="error"
|
||||
onClick={() => {
|
||||
setSelectedStep(step);
|
||||
setApprovalDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Chip
|
||||
label={step.status}
|
||||
size="small"
|
||||
color={step.status === 'approved' ? 'success' : 'error'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<AssignmentIcon sx={{ mr: 2 }} />
|
||||
<ListItemText
|
||||
primary={`Step ${step.step_number}: ${step.step_type}`}
|
||||
secondary={step.due_date ? `Due: ${new Date(step.due_date).toLocaleDateString()}` : ''}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={approvalDialogOpen} onClose={() => setApprovalDialogOpen(false)}>
|
||||
<DialogTitle>
|
||||
{selectedStep?.status === 'pending' ? 'Approve or Reject Step' : 'Add Comments'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
label="Comments"
|
||||
value={comments}
|
||||
onChange={(e) => setComments(e.target.value)}
|
||||
sx={{ mt: 2 }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setApprovalDialogOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
color="error"
|
||||
onClick={() => {
|
||||
if (selectedStep) {
|
||||
rejectStep.mutate({ stepId: selectedStep.id, comments });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
color="success"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
if (selectedStep) {
|
||||
approveStep.mutate({ stepId: selectedStep.id, comments });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
228
apps/mcp-legal/src/components/MatterManagement.tsx
Normal file
228
apps/mcp-legal/src/components/MatterManagement.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Matter Management Component
|
||||
* UI for legal matter management
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Folder as FolderIcon,
|
||||
People as PeopleIcon,
|
||||
Description as DescriptionIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { DocumentManagement } from './DocumentManagement';
|
||||
|
||||
interface Matter {
|
||||
id: string;
|
||||
matter_number: string;
|
||||
title: string;
|
||||
status: string;
|
||||
matter_type?: string;
|
||||
client_id?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function MatterManagement() {
|
||||
const [selectedMatter, setSelectedMatter] = useState<Matter | null>(null);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: matters, isLoading } = useQuery<Matter[]>({
|
||||
queryKey: ['matters'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('/api/matters');
|
||||
if (!response.ok) throw new Error('Failed to fetch matters');
|
||||
const data = await response.json();
|
||||
return data.matters || [];
|
||||
},
|
||||
});
|
||||
|
||||
const createMatter = useMutation({
|
||||
mutationFn: async (matter: Partial<Matter>) => {
|
||||
const response = await fetch('/api/matters', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(matter),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to create matter');
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['matters'] });
|
||||
setCreateDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateMatter = (formData: FormData) => {
|
||||
createMatter.mutate({
|
||||
matter_number: formData.get('matter_number') as string,
|
||||
title: formData.get('title') as string,
|
||||
description: formData.get('description') as string,
|
||||
matter_type: formData.get('matter_type') as string,
|
||||
status: 'open',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Typography variant="h4">Legal Matters</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
>
|
||||
New Matter
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{selectedMatter ? (
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<IconButton onClick={() => setSelectedMatter(null)}>←</IconButton>
|
||||
<Typography variant="h5">{selectedMatter.title}</Typography>
|
||||
<Chip label={selectedMatter.status} sx={{ ml: 2 }} />
|
||||
</Box>
|
||||
|
||||
<Tabs value={tabValue} onChange={(_, v) => setTabValue(v)}>
|
||||
<Tab icon={<DescriptionIcon />} label="Documents" />
|
||||
<Tab icon={<PeopleIcon />} label="Participants" />
|
||||
<Tab icon={<FolderIcon />} label="Details" />
|
||||
</Tabs>
|
||||
|
||||
<Box mt={3}>
|
||||
{tabValue === 0 && <DocumentManagement matterId={selectedMatter.id} />}
|
||||
{tabValue === 1 && <Typography>Participants coming soon</Typography>}
|
||||
{tabValue === 2 && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6">Matter Details</Typography>
|
||||
<Typography>Number: {selectedMatter.matter_number}</Typography>
|
||||
<Typography>Type: {selectedMatter.matter_type || 'N/A'}</Typography>
|
||||
<Typography>Status: {selectedMatter.status}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Typography>Loading...</Typography>
|
||||
) : (
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Matter Number</TableCell>
|
||||
<TableCell>Title</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{matters?.map((matter) => (
|
||||
<TableRow
|
||||
key={matter.id}
|
||||
onClick={() => setSelectedMatter(matter)}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<TableCell>{matter.matter_number}</TableCell>
|
||||
<TableCell>{matter.title}</TableCell>
|
||||
<TableCell>{matter.matter_type || 'N/A'}</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={matter.status} size="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton size="small">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreateMatter(new FormData(e.currentTarget));
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Create New Matter</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
name="matter_number"
|
||||
label="Matter Number"
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="title"
|
||||
label="Title"
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="description"
|
||||
label="Description"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="matter_type"
|
||||
label="Matter Type"
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateDialogOpen(false)}>Cancel</Button>
|
||||
<Button type="submit" variant="contained">
|
||||
Create
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
252
apps/mcp-legal/src/components/TemplateLibrary.tsx
Normal file
252
apps/mcp-legal/src/components/TemplateLibrary.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Template Library Component
|
||||
* UI for document template management
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Typography,
|
||||
Grid,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Preview as PreviewIcon,
|
||||
FileCopy as FileCopyIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
version: number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export function TemplateLibrary() {
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>('all');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: templates, isLoading } = useQuery<Template[]>({
|
||||
queryKey: ['templates', categoryFilter],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
if (categoryFilter !== 'all') params.append('category', categoryFilter);
|
||||
const response = await fetch(`/api/templates?${params}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch templates');
|
||||
const data = await response.json();
|
||||
return data.templates || [];
|
||||
},
|
||||
});
|
||||
|
||||
const createTemplate = useMutation({
|
||||
mutationFn: async (template: Partial<Template & { template_content: string }>) => {
|
||||
const response = await fetch('/api/templates', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(template),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to create template');
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['templates'] });
|
||||
setCreateDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const renderTemplate = useMutation({
|
||||
mutationFn: async ({ templateId, variables }: { templateId: string; variables: Record<string, unknown> }) => {
|
||||
const response = await fetch(`/api/templates/${templateId}/render`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ variables }),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to render template');
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreateTemplate = (formData: FormData) => {
|
||||
createTemplate.mutate({
|
||||
name: formData.get('name') as string,
|
||||
description: formData.get('description') as string,
|
||||
category: formData.get('category') as string,
|
||||
template_content: formData.get('template_content') as string,
|
||||
is_active: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePreview = async (template: Template) => {
|
||||
setSelectedTemplate(template);
|
||||
// For preview, we'd typically show a form to input variables
|
||||
// For now, just open the dialog
|
||||
setPreviewDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Typography variant="h4">Template Library</Typography>
|
||||
<Box>
|
||||
<FormControl size="small" sx={{ minWidth: 120, mr: 2 }}>
|
||||
<InputLabel>Category</InputLabel>
|
||||
<Select
|
||||
value={categoryFilter}
|
||||
label="Category"
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
>
|
||||
<MenuItem value="all">All</MenuItem>
|
||||
<MenuItem value="contract">Contract</MenuItem>
|
||||
<MenuItem value="pleading">Pleading</MenuItem>
|
||||
<MenuItem value="brief">Brief</MenuItem>
|
||||
<MenuItem value="letter">Letter</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
>
|
||||
New Template
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isLoading ? (
|
||||
<Typography>Loading...</Typography>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
{templates?.map((template) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={template.id}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={template.name}
|
||||
subheader={template.category}
|
||||
action={
|
||||
<Box>
|
||||
<IconButton size="small" onClick={() => handlePreview(template)}>
|
||||
<PreviewIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small">
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{template.description}
|
||||
</Typography>
|
||||
<Box display="flex" gap={1}>
|
||||
<Chip label={`v${template.version}`} size="small" />
|
||||
{template.is_active && <Chip label="Active" size="small" color="success" />}
|
||||
</Box>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
startIcon={<FileCopyIcon />}
|
||||
sx={{ mt: 2 }}
|
||||
onClick={() => {
|
||||
// Navigate to document assembly with this template
|
||||
window.location.href = `/assembly?template=${template.id}`;
|
||||
}}
|
||||
>
|
||||
Use Template
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="md" fullWidth>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreateTemplate(new FormData(e.currentTarget));
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Create New Template</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
name="name"
|
||||
label="Template Name"
|
||||
fullWidth
|
||||
required
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="description"
|
||||
label="Description"
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<FormControl fullWidth margin="dense" sx={{ mb: 2 }}>
|
||||
<InputLabel>Category</InputLabel>
|
||||
<Select name="category" label="Category">
|
||||
<MenuItem value="contract">Contract</MenuItem>
|
||||
<MenuItem value="pleading">Pleading</MenuItem>
|
||||
<MenuItem value="brief">Brief</MenuItem>
|
||||
<MenuItem value="letter">Letter</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
margin="dense"
|
||||
name="template_content"
|
||||
label="Template Content"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={15}
|
||||
required
|
||||
placeholder="Use {{variable}} for variables"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateDialogOpen(false)}>Cancel</Button>
|
||||
<Button type="submit" variant="contained">
|
||||
Create
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={previewDialogOpen} onClose={() => setPreviewDialogOpen(false)} maxWidth="lg" fullWidth>
|
||||
<DialogTitle>Preview: {selectedTemplate?.name}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>Template preview and variable input form would go here</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setPreviewDialogOpen(false)}>Close</Button>
|
||||
<Button variant="contained">Generate Document</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user