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:
defiQUG
2025-11-13 09:32:55 -08:00
parent 92cc41d26d
commit 6a8582e54d
202 changed files with 22699 additions and 981 deletions

86
apps/README.md Normal file
View 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

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}