251215:1719 Docunment Number Rule not correct
This commit is contained in:
@@ -2,15 +2,18 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, Edit, Play } from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Plus, Edit, Play, AlertTriangle, ShieldAlert, CheckCircle2 } from 'lucide-react';
|
||||
import { numberingApi, NumberingTemplate } from '@/lib/api/numbering';
|
||||
import { TemplateEditor } from '@/components/numbering/template-editor';
|
||||
import { SequenceViewer } from '@/components/numbering/sequence-viewer';
|
||||
import { TemplateTester } from '@/components/numbering/template-tester';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -18,15 +21,148 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useProjects, useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
import { useProjects } from '@/hooks/use-master-data';
|
||||
// --- Sub-components for Tools ---
|
||||
function ManualOverrideForm({ onSuccess, projectId }: { onSuccess: () => void, projectId: number }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
typeId: '',
|
||||
disciplineId: '',
|
||||
year: new Date().getFullYear().toString(),
|
||||
newSequence: '',
|
||||
reason: ''
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
await numberingApi.manualOverride({
|
||||
projectId,
|
||||
typeId: parseInt(formData.typeId),
|
||||
disciplineId: formData.disciplineId ? parseInt(formData.disciplineId) : undefined,
|
||||
year: parseInt(formData.year),
|
||||
newSequence: parseInt(formData.newSequence),
|
||||
reason: formData.reason,
|
||||
userId: 1 // TODO: Get from auth context
|
||||
});
|
||||
toast.success("Manual override applied successfully");
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
toast.error("Failed to apply override");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Manual Override</CardTitle>
|
||||
<CardDescription>Force set a counter sequence. Use with caution.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<AlertDescription>Changing counters manually can cause duplication errors.</AlertDescription>
|
||||
</Alert>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Type ID</Label>
|
||||
<Input
|
||||
placeholder="e.g. 1"
|
||||
value={formData.typeId}
|
||||
onChange={e => setFormData({...formData, typeId: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Discipline ID</Label>
|
||||
<Input
|
||||
placeholder="Optional"
|
||||
value={formData.disciplineId}
|
||||
onChange={e => setFormData({...formData, disciplineId: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Year</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.year}
|
||||
onChange={e => setFormData({...formData, year: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>New Sequence</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="e.g. 5"
|
||||
value={formData.newSequence}
|
||||
onChange={e => setFormData({...formData, newSequence: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Reason</Label>
|
||||
<Textarea
|
||||
placeholder="Why is this override needed?"
|
||||
value={formData.reason}
|
||||
onChange={e => setFormData({...formData, reason: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={loading} className="w-full">
|
||||
{loading && <ShieldAlert className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Apply Override
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function AdminMetrics() {
|
||||
// Fetch metrics from /admin/document-numbering/metrics
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Generation Success Rate</CardTitle>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">99.9%</div>
|
||||
<p className="text-xs text-muted-foreground">+0.1% from last month</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* More cards... */}
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Audit Logs</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">Log viewer implementation pending.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function NumberingPage() {
|
||||
const { data: projects = [] } = useProjects();
|
||||
const [selectedProjectId, setSelectedProjectId] = useState("1");
|
||||
const [activeTab, setActiveTab] = useState("templates");
|
||||
|
||||
const [templates, setTemplates] = useState<NumberingTemplate[]>([]);
|
||||
const [, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// View states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -36,6 +172,12 @@ export default function NumberingPage() {
|
||||
|
||||
const selectedProjectName = projects.find((p: any) => p.id.toString() === selectedProjectId)?.projectName || 'Unknown Project';
|
||||
|
||||
// Master Data
|
||||
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
|
||||
const { data: contracts = [] } = useContracts(Number(selectedProjectId));
|
||||
const contractId = contracts[0]?.id;
|
||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -60,7 +202,7 @@ export default function NumberingPage() {
|
||||
const handleSave = async (data: Partial<NumberingTemplate>) => {
|
||||
try {
|
||||
await numberingApi.saveTemplate(data);
|
||||
toast.success(data.templateId ? "Template updated" : "Template created");
|
||||
toast.success(data.id || data.templateId ? "Template updated" : "Template created");
|
||||
setIsEditing(false);
|
||||
loadTemplates();
|
||||
} catch {
|
||||
@@ -73,6 +215,8 @@ export default function NumberingPage() {
|
||||
setIsTesting(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
|
||||
@@ -80,6 +224,8 @@ export default function NumberingPage() {
|
||||
template={activeTemplate}
|
||||
projectId={Number(selectedProjectId)}
|
||||
projectName={selectedProjectName}
|
||||
correspondenceTypes={correspondenceTypes}
|
||||
disciplines={disciplines}
|
||||
onSave={handleSave}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
/>
|
||||
@@ -95,7 +241,7 @@ export default function NumberingPage() {
|
||||
Document Numbering
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage numbering templates and sequences
|
||||
Manage numbering templates, audit logs, and tools
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -111,77 +257,117 @@ export default function NumberingPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => handleEdit(undefined)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<h2 className="text-lg font-semibold">Templates - {selectedProjectName}</h2>
|
||||
<div className="grid gap-4">
|
||||
{templates
|
||||
.filter(t => !t.projectId || t.projectId === Number(selectedProjectId))
|
||||
.map((template) => (
|
||||
<Card key={template.templateId} className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{template.documentTypeName}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{projects.find((p: any) => p.id.toString() === template.projectId?.toString())?.projectName || selectedProjectName}
|
||||
</Badge>
|
||||
{template.disciplineCode && <Badge>{template.disciplineCode}</Badge>}
|
||||
<Badge variant={template.isActive ? 'default' : 'secondary'}>
|
||||
{template.isActive ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="templates">Templates</TabsTrigger>
|
||||
<TabsTrigger value="metrics">Metrics & Audit</TabsTrigger>
|
||||
<TabsTrigger value="tools">Admin Tools</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="bg-slate-100 dark:bg-slate-900 rounded px-3 py-2 mb-3 font-mono text-sm inline-block border">
|
||||
{template.templateFormat}
|
||||
</div>
|
||||
<TabsContent value="templates" className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => handleEdit(undefined)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Example: </span>
|
||||
<span className="font-medium font-mono text-green-600 dark:text-green-400">
|
||||
{template.exampleNumber}
|
||||
</span>
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="grid gap-4">
|
||||
{templates
|
||||
.filter(t => !t.projectId || t.projectId === Number(selectedProjectId))
|
||||
.map((template) => (
|
||||
<Card key={template.templateId} className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{template.documentTypeName}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{projects.find((p: any) => p.id.toString() === template.projectId?.toString())?.projectName || selectedProjectName}
|
||||
</Badge>
|
||||
{template.disciplineCode && <Badge>{template.disciplineCode}</Badge>}
|
||||
<Badge variant={template.isActive ? 'default' : 'secondary'}>
|
||||
{template.isActive ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-100 dark:bg-slate-900 rounded px-3 py-2 mb-3 font-mono text-sm inline-block border">
|
||||
{template.templateFormat}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Example: </span>
|
||||
<span className="font-medium font-mono text-green-600 dark:text-green-400">
|
||||
{template.exampleNumber}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Reset: </span>
|
||||
<span>
|
||||
{template.resetAnnually ? 'Annually' : 'Never'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Reset: </span>
|
||||
<span>
|
||||
{template.resetAnnually ? 'Annually' : 'Never'}
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleEdit(template)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => handleTest(template)}>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Test
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleEdit(template)}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => handleTest(template)}>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Test
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SequenceViewer />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Sequence Viewer Sidebar */}
|
||||
<SequenceViewer />
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="metrics" className="space-y-4">
|
||||
<AdminMetrics />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tools" className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<ManualOverrideForm onSuccess={() => {}} projectId={Number(selectedProjectId)} />
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Void & Replace</CardTitle>
|
||||
<CardDescription>Safe voiding of issued numbers.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded border border-yellow-200 dark:border-yellow-900 text-sm">
|
||||
To void and replace numbers, please use the <strong>Correspondences</strong> list view actions or edit specific documents directly.
|
||||
<br/><br/>
|
||||
This ensures the void action is linked to the correct document record.
|
||||
</div>
|
||||
<Button variant="outline" className="w-full" disabled>
|
||||
Standalone Void Tool (Coming Soon)
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<TemplateTester
|
||||
open={isTesting}
|
||||
|
||||
Reference in New Issue
Block a user