251215:1719 Docunment Number Rule not correct
This commit is contained in:
@@ -4,16 +4,28 @@ import { useState, useEffect } from "react";
|
||||
import { TemplateEditor } from "@/components/numbering/template-editor";
|
||||
import { SequenceViewer } from "@/components/numbering/sequence-viewer";
|
||||
import { numberingApi } from "@/lib/api/numbering";
|
||||
import { NumberingTemplate } from "@/types/numbering";
|
||||
import { NumberingTemplate } from "@/lib/api/numbering"; // Correct import
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useCorrespondenceTypes, useContracts, useDisciplines } from "@/hooks/use-master-data";
|
||||
import { useProjects } from "@/hooks/use-projects";
|
||||
|
||||
export default function EditTemplatePage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [template, setTemplate] = useState<NumberingTemplate | null>(null);
|
||||
|
||||
// Master Data
|
||||
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
|
||||
const { data: projects = [] } = useProjects();
|
||||
const projectId = template?.projectId || 1;
|
||||
const { data: contracts = [] } = useContracts(projectId);
|
||||
const contractId = contracts[0]?.id;
|
||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||
|
||||
const selectedProjectName = projects.find((p: any) => p.id === projectId)?.projectName || 'LCBP3';
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTemplate = async () => {
|
||||
setLoading(true);
|
||||
@@ -76,7 +88,9 @@ export default function EditTemplatePage({ params }: { params: { id: string } })
|
||||
<TemplateEditor
|
||||
template={template}
|
||||
projectId={template.projectId || 1}
|
||||
projectName="LCBP3"
|
||||
projectName={selectedProjectName}
|
||||
correspondenceTypes={correspondenceTypes}
|
||||
disciplines={disciplines}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
|
||||
@@ -3,10 +3,22 @@
|
||||
import { TemplateEditor } from "@/components/numbering/template-editor";
|
||||
import { numberingApi, NumberingTemplate } from "@/lib/api/numbering";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCorrespondenceTypes, useContracts, useDisciplines } from "@/hooks/use-master-data";
|
||||
import { useProjects } from "@/hooks/use-projects";
|
||||
|
||||
export default function NewTemplatePage() {
|
||||
const router = useRouter();
|
||||
|
||||
// Master Data
|
||||
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
|
||||
const { data: projects = [] } = useProjects();
|
||||
const projectId = 1; // Default or sync with selection
|
||||
const { data: contracts = [] } = useContracts(projectId);
|
||||
const contractId = contracts[0]?.id;
|
||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||
|
||||
const selectedProjectName = projects.find((p: any) => p.id === projectId)?.projectName || 'LCBP3';
|
||||
|
||||
const handleSave = async (data: Partial<NumberingTemplate>) => {
|
||||
try {
|
||||
await numberingApi.saveTemplate(data);
|
||||
@@ -25,8 +37,10 @@ export default function NewTemplatePage() {
|
||||
<div className="p-6 space-y-6">
|
||||
<h1 className="text-3xl font-bold">New Numbering Template</h1>
|
||||
<TemplateEditor
|
||||
projectId={1}
|
||||
projectName="LCBP3"
|
||||
projectId={projectId}
|
||||
projectName={selectedProjectName}
|
||||
correspondenceTypes={correspondenceTypes}
|
||||
disciplines={disciplines}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -141,7 +141,9 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id?
|
||||
// Map recipients structure matching backend expectation
|
||||
recipients: [{ organizationId: toOrgId, type: 'TO' }],
|
||||
// Add date just to be safe, though service uses 'now'
|
||||
dueDate: new Date().toISOString()
|
||||
dueDate: new Date().toISOString(),
|
||||
// [Fix] Subject is required by DTO validation, send placeholder if empty
|
||||
subject: watch('subject') || "Preview Subject"
|
||||
});
|
||||
setPreview(res);
|
||||
} catch (err) {
|
||||
@@ -157,18 +159,47 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id?
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="max-w-3xl space-y-6">
|
||||
{/* Existing Document Number (Read Only) */}
|
||||
{initialData?.correspondenceNumber && (
|
||||
<div className="space-y-2">
|
||||
<Label>Current Document Number</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input value={initialData.correspondenceNumber} disabled readOnly className="bg-muted font-mono font-bold text-lg w-full" />
|
||||
{preview && preview.number !== initialData.correspondenceNumber && (
|
||||
<span className="text-xs text-amber-600 font-semibold whitespace-nowrap px-2">
|
||||
Start Change Detected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview Section */}
|
||||
{preview && (
|
||||
<div className="p-4 rounded-md bg-muted border border-border">
|
||||
<p className="text-sm text-muted-foreground mb-1">Document Number Preview</p>
|
||||
<div className={`p-4 rounded-md border ${preview.number !== initialData?.correspondenceNumber ? 'bg-amber-50 border-amber-200' : 'bg-muted border-border'}`}>
|
||||
<p className="text-sm font-semibold mb-1 flex items-center gap-2">
|
||||
{initialData?.correspondenceNumber ? "New Document Number (Preview)" : "Document Number Preview"}
|
||||
{preview.number !== initialData?.correspondenceNumber && initialData?.correspondenceNumber && (
|
||||
<span className="text-[10px] bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full border border-amber-200">
|
||||
Will Update
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl font-bold font-mono text-primary tracking-wide">{preview.number}</span>
|
||||
<span className={`text-xl font-bold font-mono tracking-wide ${preview.number !== initialData?.correspondenceNumber ? 'text-amber-700' : 'text-primary'}`}>
|
||||
{preview.number}
|
||||
</span>
|
||||
{preview.isDefaultTemplate && (
|
||||
<span className="text-[10px] uppercase font-semibold px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800 border border-yellow-200">
|
||||
Default Template
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{preview.number !== initialData?.correspondenceNumber && initialData?.correspondenceNumber && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
* The document number will be regenerated because critical fields were changed.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -15,48 +15,52 @@ import {
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { NumberingTemplate } from '@/lib/api/numbering';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/hover-card';
|
||||
|
||||
const DOCUMENT_TYPES = [
|
||||
{ value: 'RFA', label: 'Request for Approval (RFA)' },
|
||||
{ value: 'RFI', label: 'Request for Information (RFI)' },
|
||||
{ value: 'TRANSMITTAL', label: 'Transmittal' },
|
||||
{ value: 'EMAIL', label: 'Email' },
|
||||
{ value: 'INSTRUCTION', label: 'Instruction' },
|
||||
{ value: 'LETTER', label: 'Letter' },
|
||||
{ value: 'MEMO', label: 'Memorandum' },
|
||||
{ value: 'MOM', label: 'Minutes of Meeting' },
|
||||
{ value: 'NOTICE', label: 'Notice' },
|
||||
{ value: 'OTHER', label: 'Other' },
|
||||
];
|
||||
|
||||
// Aligned with Backend replacement logic
|
||||
const VARIABLES = [
|
||||
{ key: '{PROJECT}', name: 'Project Code', example: 'LCBP3' },
|
||||
{ key: '{ORIGINATOR}', name: 'Originator Code', example: 'PAT' },
|
||||
{ key: '{ORG}', name: 'Originator Code', example: 'PAT' },
|
||||
{ key: '{RECIPIENT}', name: 'Recipient Code', example: 'CN' },
|
||||
{ key: '{CORR_TYPE}', name: 'Correspondence Type', example: 'RFA' },
|
||||
{ key: '{SUB_TYPE}', name: 'Sub Type', example: '21' },
|
||||
{ key: '{DISCIPLINE}', name: 'Discipline', example: 'STR' },
|
||||
{ key: '{RFA_TYPE}', name: 'RFA Type', example: 'SDW' },
|
||||
{ key: '{YEAR:B.E.}', name: 'Year (B.E.)', example: '2568' },
|
||||
{ key: '{YEAR:A.D.}', name: 'Year (A.D.)', example: '2025' },
|
||||
{ key: '{TYPE}', name: 'Type Code', example: 'RFA' },
|
||||
{ key: '{DISCIPLINE}', name: 'Discipline Code', example: 'STR' },
|
||||
{ key: '{SUBTYPE}', name: 'Sub-Type Code', example: 'GEN' },
|
||||
{ key: '{SUBTYPE_NUM}', name: 'Sub-Type Number', example: '01' },
|
||||
{ key: '{YEAR}', name: 'Year (B.E.)', example: '2568' },
|
||||
{ key: '{YEAR_SHORT}', name: 'Year Short (68)', example: '68' },
|
||||
{ key: '{SEQ:4}', name: 'Sequence (4-digit)', example: '0001' },
|
||||
{ key: '{REV}', name: 'Revision', example: 'A' },
|
||||
];
|
||||
|
||||
export interface TemplateEditorProps {
|
||||
template?: NumberingTemplate;
|
||||
projectId: number;
|
||||
projectName: string;
|
||||
correspondenceTypes: any[];
|
||||
disciplines: any[];
|
||||
onSave: (data: Partial<NumberingTemplate>) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function TemplateEditor({ template, projectId, projectName, onSave, onCancel }: TemplateEditorProps) {
|
||||
const [format, setFormat] = useState(template?.templateFormat || '');
|
||||
const [docType, setDocType] = useState(template?.documentTypeName || '');
|
||||
const [discipline, setDiscipline] = useState(template?.disciplineCode || '');
|
||||
export function TemplateEditor({
|
||||
template,
|
||||
projectId,
|
||||
projectName,
|
||||
correspondenceTypes,
|
||||
disciplines,
|
||||
onSave,
|
||||
onCancel
|
||||
}: TemplateEditorProps) {
|
||||
const [format, setFormat] = useState(template?.formatTemplate || template?.templateFormat || '');
|
||||
const [typeId, setTypeId] = useState<string>(template?.correspondenceTypeId?.toString() || '');
|
||||
const [disciplineId, setDisciplineId] = useState<string>(template?.disciplineId?.toString() || '0');
|
||||
const [padding, setPadding] = useState(template?.paddingLength || 4);
|
||||
const [reset, setReset] = useState(template?.resetAnnually ?? true);
|
||||
const [isActive, setIsActive] = useState(template?.isActive ?? true);
|
||||
|
||||
const [preview, setPreview] = useState('');
|
||||
|
||||
@@ -64,17 +68,25 @@ export function TemplateEditor({ template, projectId, projectName, onSave, onCan
|
||||
// Generate preview
|
||||
let previewText = format || '';
|
||||
VARIABLES.forEach((v) => {
|
||||
// Simple mock replacement for preview
|
||||
let replacement = v.example;
|
||||
// Dynamic preview for dates to be more realistic
|
||||
if (v.key === '{YYYY}') replacement = new Date().getFullYear().toString();
|
||||
if (v.key === '{YY}') replacement = new Date().getFullYear().toString().slice(-2);
|
||||
if (v.key === '{THXXXX}') replacement = (new Date().getFullYear() + 543).toString();
|
||||
if (v.key === '{THXX}') replacement = (new Date().getFullYear() + 543).toString().slice(-2);
|
||||
if (v.key === '{YEAR}') replacement = (new Date().getFullYear() + 543).toString();
|
||||
if (v.key === '{YEAR_SHORT}') replacement = (new Date().getFullYear() + 543).toString().slice(-2);
|
||||
|
||||
// Dynamic context based on selection (optional visual enhancement)
|
||||
if (v.key === '{TYPE}' && typeId) {
|
||||
const t = correspondenceTypes.find(ct => ct.id.toString() === typeId);
|
||||
if (t) replacement = t.typeCode;
|
||||
}
|
||||
if (v.key === '{DISCIPLINE}' && disciplineId !== '0') {
|
||||
const d = disciplines.find(di => di.id.toString() === disciplineId);
|
||||
if (d) replacement = d.disciplineCode;
|
||||
}
|
||||
|
||||
previewText = previewText.replace(new RegExp(v.key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), replacement);
|
||||
});
|
||||
setPreview(previewText);
|
||||
}, [format]);
|
||||
}, [format, typeId, disciplineId, correspondenceTypes, disciplines]);
|
||||
|
||||
const insertVariable = (variable: string) => {
|
||||
setFormat((prev) => prev + variable);
|
||||
@@ -84,140 +96,151 @@ export function TemplateEditor({ template, projectId, projectName, onSave, onCan
|
||||
onSave({
|
||||
...template,
|
||||
projectId: projectId,
|
||||
templateFormat: format,
|
||||
documentTypeName: docType,
|
||||
disciplineCode: discipline || undefined,
|
||||
correspondenceTypeId: Number(typeId),
|
||||
disciplineId: Number(disciplineId),
|
||||
formatTemplate: format,
|
||||
templateFormat: format, // Legacy support
|
||||
paddingLength: padding,
|
||||
resetAnnually: reset,
|
||||
isActive: isActive,
|
||||
exampleNumber: preview
|
||||
});
|
||||
};
|
||||
|
||||
const isValid = format.length > 0 && typeId;
|
||||
|
||||
return (
|
||||
<Card className="p-6 space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">{template ? 'Edit Template' : 'New Template'}</h3>
|
||||
<Badge variant="outline" className="text-base px-3 py-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-lg font-semibold">{template ? 'Edit Template' : 'New Template'}</h3>
|
||||
<Badge variant={isActive ? "default" : "secondary"}>
|
||||
{isActive ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Define how document numbers are generated for this project.</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<Badge variant="outline" className="text-base px-3 py-1 bg-slate-50">
|
||||
Project: {projectName}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<Label>Document Type *</Label>
|
||||
<Select value={docType} onValueChange={setDocType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select document type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DOCUMENT_TYPES.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Discipline (Optional)</Label>
|
||||
<Select value={discipline || "ALL"} onValueChange={(val) => setDiscipline(val === "ALL" ? "" : val)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All disciplines" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">All</SelectItem>
|
||||
<SelectItem value="STR">STR - Structure</SelectItem>
|
||||
<SelectItem value="ARC">ARC - Architecture</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Template Format *</Label>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={format}
|
||||
onChange={(e) => setFormat(e.target.value)}
|
||||
placeholder="e.g., {ORIGINATOR}-{RECIPIENT}-{DOCTYPE}-{YYYY}-{SEQ}"
|
||||
className="font-mono text-base"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{VARIABLES.map((v) => (
|
||||
<Button
|
||||
key={v.key}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => insertVariable(v.key)}
|
||||
type="button"
|
||||
className="font-mono text-xs"
|
||||
>
|
||||
{v.key}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Preview</Label>
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-600 mb-1">Example number:</p>
|
||||
<p className="text-2xl font-mono font-bold text-green-700">
|
||||
{preview || 'Enter format above'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Sequence Padding Length</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={padding}
|
||||
onChange={e => setPadding(Number(e.target.value))}
|
||||
min={1} max={10}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Number of digits (e.g., 4 = 0001)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox checked={reset} onCheckedChange={(checked) => setReset(!!checked)} />
|
||||
<span className="text-sm select-none">Reset annually (on January 1st)</span>
|
||||
<label className="flex items-center gap-2 cursor-pointer text-sm">
|
||||
<Checkbox checked={isActive} onCheckedChange={(c) => setIsActive(!!c)} />
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Variable Reference */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-3">Available Variables</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{VARIABLES.map((v) => (
|
||||
<div
|
||||
key={v.key}
|
||||
className="flex items-center justify-between p-2 bg-slate-50 dark:bg-slate-900 rounded border"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="outline" className="font-mono bg-white dark:bg-black">
|
||||
{v.key}
|
||||
</Badge>
|
||||
<p className="text-xs text-muted-foreground mt-1">{v.name}</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Configuration Column */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Document Type *</Label>
|
||||
<Select value={typeId} onValueChange={setTypeId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select type..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{correspondenceTypes.map((type) => (
|
||||
<SelectItem key={type.id} value={type.id.toString()}>
|
||||
{type.typeCode} - {type.typeName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<span className="text-sm text-foreground">{v.example}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Discipline (Optional)</Label>
|
||||
<Select value={disciplineId} onValueChange={setDisciplineId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All/None" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">All Disciplines (Default)</SelectItem>
|
||||
{disciplines.map((d) => (
|
||||
<SelectItem key={d.id} value={d.id.toString()}>
|
||||
{d.disciplineCode} - {d.disciplineName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Specific discipline templates take precedence over 'All'.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Padding Length</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={padding}
|
||||
onChange={e => setPadding(Number(e.target.value))}
|
||||
min={1} max={10}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Reset Rule</Label>
|
||||
<div className="flex items-center h-10">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox checked={reset} onCheckedChange={(c) => setReset(!!c)} />
|
||||
<span className="text-sm">Reset Annually</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Format Column */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Template Format *</Label>
|
||||
<Input
|
||||
value={format}
|
||||
onChange={(e) => setFormat(e.target.value)}
|
||||
placeholder="{ORG}-{TYPE}-{SEQ:4}"
|
||||
className="font-mono text-base mb-2"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{VARIABLES.map((v) => (
|
||||
<HoverCard key={v.key}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => insertVariable(v.key)}
|
||||
type="button"
|
||||
className="font-mono text-xs bg-slate-50 hover:bg-slate-100"
|
||||
>
|
||||
{v.key}
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-60 p-3">
|
||||
<p className="font-semibold text-sm">{v.name}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Example: <span className="font-mono">{v.example}</span></p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50/50 border border-green-200 rounded-lg p-4">
|
||||
<p className="text-xs uppercase tracking-wide text-green-700 font-semibold mb-2">Preview Output</p>
|
||||
<p className="text-2xl font-mono font-bold text-green-800 tracking-tight">
|
||||
{preview || '...'}
|
||||
</p>
|
||||
<p className="text-xs text-green-600 mt-2">
|
||||
* This is an approximation. Actual numbers depend on runtime context.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button variant="outline" onClick={onCancel}>Cancel</Button>
|
||||
<Button onClick={handleSave}>Save Template</Button>
|
||||
<Button onClick={handleSave} disabled={!isValid}>Save Template</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
||||
setLoading(true);
|
||||
try {
|
||||
// Note: generateTestNumber expects keys: organizationId, disciplineId
|
||||
const result = await numberingApi.generateTestNumber(template.templateId, {
|
||||
const result = await numberingApi.generateTestNumber(template.id || template.templateId || 0, {
|
||||
organizationId: testData.organizationId,
|
||||
disciplineId: testData.disciplineId
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useProcessRFA } from "@/hooks/use-rfa";
|
||||
|
||||
interface RFADetailProps {
|
||||
data: RFA;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export function RFADetail({ data }: RFADetailProps) {
|
||||
@@ -152,7 +152,7 @@ export function RFADetail({ data }: RFADetailProps) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{data.items.map((item) => (
|
||||
{data.items.map((item: any) => (
|
||||
<tr key={item.id}>
|
||||
<td className="px-4 py-3 font-medium">{item.itemNo}</td>
|
||||
<td className="px-4 py-3">{item.description}</td>
|
||||
|
||||
29
frontend/components/ui/hover-card.tsx
Normal file
29
frontend/components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
@@ -4,10 +4,12 @@ export const dashboardApi = {
|
||||
getStats: async (): Promise<DashboardStats> => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
return {
|
||||
correspondences: 124,
|
||||
rfas: 45,
|
||||
totalDocuments: 124,
|
||||
documentsThisMonth: 12,
|
||||
pendingApprovals: 4,
|
||||
approved: 89,
|
||||
pending: 12,
|
||||
totalRfas: 45,
|
||||
totalCirculations: 15,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
// Types
|
||||
export interface NumberingTemplate {
|
||||
templateId: number;
|
||||
projectId?: number;
|
||||
documentTypeId?: string;
|
||||
documentTypeName: string;
|
||||
disciplineCode?: string;
|
||||
templateFormat: string;
|
||||
exampleNumber: string;
|
||||
currentNumber: number;
|
||||
resetAnnually: boolean;
|
||||
id?: number; // Backend uses 'id'
|
||||
templateId?: number; // Legacy, optional
|
||||
projectId: number;
|
||||
correspondenceTypeId: number;
|
||||
correspondenceType?: { typeCode: string; typeName: string }; // Relation
|
||||
documentTypeName?: string; // Optional (joined)
|
||||
disciplineId: number;
|
||||
discipline?: { disciplineCode: string; disciplineName: string }; // Relation
|
||||
disciplineCode?: string; // Optional (joined)
|
||||
formatTemplate: string; // Backend uses 'formatTemplate'
|
||||
templateFormat?: string; // Legacy alias
|
||||
exampleNumber?: string;
|
||||
paddingLength: number;
|
||||
resetAnnually: boolean;
|
||||
isActive: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface NumberingTemplateDto {
|
||||
projectId: number;
|
||||
correspondenceTypeId: number;
|
||||
disciplineId?: number; // 0 = All
|
||||
formatTemplate: string;
|
||||
exampleNumber?: string;
|
||||
paddingLength: number;
|
||||
resetAnnually: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@@ -23,138 +42,82 @@ export interface NumberSequence {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// Mock Data
|
||||
const mockTemplates: NumberingTemplate[] = [
|
||||
{
|
||||
templateId: 1,
|
||||
projectId: 1,
|
||||
documentTypeName: 'Correspondence',
|
||||
disciplineCode: '',
|
||||
templateFormat: '{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.}',
|
||||
exampleNumber: 'PAT-CN-0001-2568',
|
||||
currentNumber: 142,
|
||||
resetAnnually: true,
|
||||
paddingLength: 4,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
templateId: 2,
|
||||
projectId: 1,
|
||||
documentTypeName: 'RFA',
|
||||
disciplineCode: 'STR',
|
||||
templateFormat: '{PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV}',
|
||||
exampleNumber: 'LCBP3-RFA-STR-SDW-0056-A',
|
||||
currentNumber: 56,
|
||||
resetAnnually: true,
|
||||
paddingLength: 4,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
templateId: 3,
|
||||
projectId: 2,
|
||||
documentTypeName: 'Maintenance Request',
|
||||
disciplineCode: '',
|
||||
templateFormat: 'MAINT-{SEQ:4}',
|
||||
exampleNumber: 'MAINT-0001',
|
||||
currentNumber: 1,
|
||||
resetAnnually: true,
|
||||
paddingLength: 4,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
const mockSequences: NumberSequence[] = [
|
||||
{
|
||||
sequenceId: 1,
|
||||
year: 2025,
|
||||
organizationCode: 'PAT',
|
||||
currentNumber: 142,
|
||||
lastGeneratedNumber: 'PAT-CORR-2025-0142',
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
sequenceId: 2,
|
||||
year: 2025,
|
||||
disciplineCode: 'STR',
|
||||
currentNumber: 56,
|
||||
lastGeneratedNumber: 'RFA-STR-2025-0056',
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
export const numberingApi = {
|
||||
getTemplates: async (): Promise<NumberingTemplate[]> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve([...mockTemplates]), 500);
|
||||
});
|
||||
const res = await apiClient.get<NumberingTemplate[]>('/admin/document-numbering/templates');
|
||||
return res.data.map(t => ({
|
||||
...t,
|
||||
templateId: t.id,
|
||||
templateFormat: t.formatTemplate,
|
||||
// Map joined data if available, else placeholders
|
||||
documentTypeName: t.correspondenceType?.typeCode || 'UNKNOWN',
|
||||
disciplineCode: t.discipline?.disciplineCode || 'ALL',
|
||||
}));
|
||||
},
|
||||
|
||||
getTemplate: async (id: number): Promise<NumberingTemplate | undefined> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(mockTemplates.find(t => t.templateId === id)), 300);
|
||||
});
|
||||
// Currently no single get endpoint
|
||||
const templates = await numberingApi.getTemplates();
|
||||
return templates.find(t => t.id === id);
|
||||
},
|
||||
|
||||
saveTemplate: async (template: Partial<NumberingTemplate>): Promise<NumberingTemplate> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (template.templateId) {
|
||||
// Update
|
||||
const index = mockTemplates.findIndex(t => t.templateId === template.templateId);
|
||||
if (index !== -1) {
|
||||
mockTemplates[index] = { ...mockTemplates[index], ...template } as NumberingTemplate;
|
||||
resolve(mockTemplates[index]);
|
||||
}
|
||||
} else {
|
||||
// Create
|
||||
const newTemplate: NumberingTemplate = {
|
||||
templateId: Math.floor(Math.random() * 1000),
|
||||
documentTypeName: 'New Type',
|
||||
isActive: true,
|
||||
currentNumber: 0,
|
||||
exampleNumber: 'PREVIEW',
|
||||
templateFormat: template.templateFormat || '',
|
||||
disciplineCode: template.disciplineCode,
|
||||
paddingLength: template.paddingLength ?? 4,
|
||||
resetAnnually: template.resetAnnually ?? true,
|
||||
...template
|
||||
} as NumberingTemplate;
|
||||
mockTemplates.push(newTemplate);
|
||||
resolve(newTemplate);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
// Map frontend interface to backend entity DTO
|
||||
const payload = {
|
||||
id: template.id || template.templateId, // Update if ID exists
|
||||
projectId: template.projectId,
|
||||
correspondenceTypeId: template.correspondenceTypeId,
|
||||
disciplineId: template.disciplineId || 0,
|
||||
formatTemplate: template.templateFormat || template.formatTemplate,
|
||||
exampleNumber: template.exampleNumber,
|
||||
paddingLength: template.paddingLength,
|
||||
resetAnnually: template.resetAnnually,
|
||||
isActive: template.isActive ?? true
|
||||
};
|
||||
const res = await apiClient.post<NumberingTemplate>('/admin/document-numbering/templates', payload);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
getSequences: async (): Promise<NumberSequence[]> => {
|
||||
// TODO: Implement backend endpoint for sequences list
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve([...mockSequences]), 500);
|
||||
setTimeout(() => resolve([]), 500);
|
||||
});
|
||||
},
|
||||
|
||||
generateTestNumber: async (templateId: number, context: { organizationId: string, disciplineId: string }): Promise<{ number: string }> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const template = mockTemplates.find(t => t.templateId === templateId);
|
||||
if (!template) return resolve({ number: 'ERROR' });
|
||||
// Use preview endpoint
|
||||
// We need to know projectId, typeId etc from template.
|
||||
// But preview endpoint needs context.
|
||||
// For now, let's just return a mock or call preview endpoint if we have enough info.
|
||||
|
||||
let format = template.templateFormat;
|
||||
// Mock replacement
|
||||
format = format.replace('{PROJECT}', 'LCBP3');
|
||||
format = format.replace('{ORIGINATOR}', context.organizationId === '1' ? 'PAT' : 'CN');
|
||||
format = format.replace('{RECIPIENT}', context.organizationId === '1' ? 'CN' : 'PAT');
|
||||
format = format.replace('{CORR_TYPE}', template.documentTypeName === 'Correspondence' ? 'CORR' : 'RFA');
|
||||
format = format.replace('{DISCIPLINE}', context.disciplineId === '1' ? 'STR' : (context.disciplineId === '2' ? 'ARC' : 'GEN'));
|
||||
format = format.replace('{RFA_TYPE}', 'SDW'); // Mock
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Generating test number for:', templateId, context);
|
||||
return new Promise((resolve) => resolve({ number: 'TEST-1234' }));
|
||||
},
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
format = format.replace('{YEAR:A.D.}', year.toString());
|
||||
format = format.replace('{YEAR:B.E.}', (year + 543).toString());
|
||||
format = format.replace('{SEQ:4}', '0001');
|
||||
format = format.replace('{REV}', 'A');
|
||||
// --- Admin Tools ---
|
||||
|
||||
resolve({ number: format });
|
||||
}, 800);
|
||||
});
|
||||
}
|
||||
getMetrics: async (): Promise<{ audit: any[], errors: any[] }> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const res = await apiClient.get<{ audit: any[], errors: any[] }>('/admin/document-numbering/metrics');
|
||||
return res.data;
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
manualOverride: async (data: any): Promise<void> => {
|
||||
await apiClient.post('/admin/document-numbering/manual-override', data);
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
voidAndReplace: async (data: any): Promise<string> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const res = await apiClient.post<any>('/admin/document-numbering/void-and-replace', data);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cancelNumber: async (data: any): Promise<void> => {
|
||||
await apiClient.post('/admin/document-numbering/cancel', data);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
|
||||
Reference in New Issue
Block a user