- 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
253 lines
8.2 KiB
TypeScript
253 lines
8.2 KiB
TypeScript
/**
|
|
* 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>
|
|
);
|
|
}
|
|
|