251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
This commit is contained in:
1281
specs/09-history/20251208-TASK-BE-004-document-numbering.md
Normal file
1281
specs/09-history/20251208-TASK-BE-004-document-numbering.md
Normal file
File diff suppressed because it is too large
Load Diff
537
specs/09-history/20251208-TASK-FE-012-numbering-config-ui.md
Normal file
537
specs/09-history/20251208-TASK-FE-012-numbering-config-ui.md
Normal file
@@ -0,0 +1,537 @@
|
||||
# TASK-FE-012: Document Numbering Configuration UI
|
||||
|
||||
**ID:** TASK-FE-012
|
||||
**Title:** Document Numbering Template Management UI
|
||||
**Category:** Administration
|
||||
**Priority:** P2 (Medium)
|
||||
**Effort:** 3-4 days
|
||||
**Dependencies:** TASK-FE-010, TASK-BE-004
|
||||
**Assigned To:** Frontend Developer
|
||||
|
||||
---
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
Build UI for configuring and managing document numbering templates including template builder, preview generator, and number sequence management.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Objectives
|
||||
|
||||
1. Create numbering template list and management
|
||||
2. Build template editor with format preview
|
||||
3. Implement template variable selector
|
||||
4. Add numbering sequence viewer
|
||||
5. Create template testing interface
|
||||
6. Implement annual reset configuration
|
||||
|
||||
---
|
||||
|
||||
## ✅ Acceptance Criteria
|
||||
|
||||
- [x] List all numbering templates by document type
|
||||
- [x] Create/edit templates with format preview
|
||||
- [x] Template variables easily selectable
|
||||
- [x] Preview shows example numbers
|
||||
- [x] View current number sequences
|
||||
- [x] Annual reset configurable
|
||||
- [x] Validation prevents conflicts
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Implementation Steps
|
||||
|
||||
### Step 1: Template List Page
|
||||
|
||||
```typescript
|
||||
// File: src/app/(admin)/admin/numbering/page.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, Edit, Eye } from 'lucide-react';
|
||||
|
||||
export default function NumberingPage() {
|
||||
const [templates, setTemplates] = useState([]);
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">
|
||||
Document Numbering Configuration
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Manage document numbering templates and sequences
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select defaultValue="1">
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select Project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">LCBP3</SelectItem>
|
||||
<SelectItem value="2">LCBP3-Maintenance</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{templates.map((template: any) => (
|
||||
<Card key={template.template_id} className="p-6">
|
||||
<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.document_type_name}
|
||||
</h3>
|
||||
<Badge>{template.discipline_code || 'All'}</Badge>
|
||||
<Badge variant={template.is_active ? 'success' : 'secondary'}>
|
||||
{template.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-100 rounded px-3 py-2 mb-3 font-mono text-sm">
|
||||
{template.template_format}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">Example: </span>
|
||||
<span className="font-medium">
|
||||
{template.example_number}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Current Sequence: </span>
|
||||
<span className="font-medium">
|
||||
{template.current_number}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Annual Reset: </span>
|
||||
<span className="font-medium">
|
||||
{template.reset_annually ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Padding: </span>
|
||||
<span className="font-medium">
|
||||
{template.padding_length} digits
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Sequences
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Template Editor Component
|
||||
|
||||
```typescript
|
||||
// File: src/components/numbering/template-editor.tsx
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
const VARIABLES = [
|
||||
{ key: '{ORIGINATOR}', 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: '{SEQ:4}', name: 'Sequence (4-digit)', example: '0001' },
|
||||
{ key: '{REV}', name: 'Revision', example: 'A' },
|
||||
];
|
||||
|
||||
export function TemplateEditor({ template, onSave }: any) {
|
||||
const [format, setFormat] = useState(template?.template_format || '');
|
||||
const [preview, setPreview] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Generate preview
|
||||
let previewText = format;
|
||||
VARIABLES.forEach((v) => {
|
||||
previewText = previewText.replace(new RegExp(v.key, 'g'), v.example);
|
||||
});
|
||||
setPreview(previewText);
|
||||
}, [format]);
|
||||
|
||||
const insertVariable = (variable: string) => {
|
||||
setFormat((prev) => prev + variable);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Template Configuration</h3>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<Label>Document Type *</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select document type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="RFA">RFA</SelectItem>
|
||||
<SelectItem value="RFI">RFI</SelectItem>
|
||||
<SelectItem value="TRANSMITTAL">Transmittal</SelectItem>
|
||||
<SelectItem value="LETTER">Letter</SelectItem>
|
||||
<SelectItem value="MEMO">Memorandum</SelectItem>
|
||||
<SelectItem value="EMAIL">Email</SelectItem>
|
||||
<SelectItem value="MOM">Minutes of Meeting</SelectItem>
|
||||
<SelectItem value="INSTRUCTION">Instruction</SelectItem>
|
||||
<SelectItem value="NOTICE">Notice</SelectItem>
|
||||
<SelectItem value="OTHER">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Discipline (Optional)</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All disciplines" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">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., {ORG}-{DOCTYPE}-{YYYY}-{SEQ}"
|
||||
className="font-mono"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{VARIABLES.map((v) => (
|
||||
<Button
|
||||
key={v.key}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => insertVariable(v.key)}
|
||||
type="button"
|
||||
>
|
||||
{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" defaultValue={4} min={1} max={10} />
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Number of digits (e.g., 4 = 0001, 0002)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Starting Number</Label>
|
||||
<Input type="number" defaultValue={1} min={1} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<Checkbox defaultChecked />
|
||||
<span className="text-sm">Reset annually (on January 1st)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Variable Reference */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-3">Available Variables</h4>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{VARIABLES.map((v) => (
|
||||
<div
|
||||
key={v.key}
|
||||
className="flex items-center justify-between p-2 bg-gray-50 rounded"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{v.key}
|
||||
</Badge>
|
||||
<p className="text-xs text-gray-600 mt-1">{v.name}</p>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{v.example}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button onClick={onSave}>Save Template</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Number Sequence Viewer
|
||||
|
||||
```typescript
|
||||
// File: src/components/numbering/sequence-viewer.tsx
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
export function SequenceViewer({ templateId }: { templateId: number }) {
|
||||
const [sequences, setSequences] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">Number Sequences</h3>
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
placeholder="Search by year, organization..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{sequences.map((seq: any) => (
|
||||
<div
|
||||
key={seq.sequence_id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium">{seq.year}</span>
|
||||
{seq.organization_code && (
|
||||
<Badge>{seq.organization_code}</Badge>
|
||||
)}
|
||||
{seq.discipline_code && (
|
||||
<Badge variant="outline">{seq.discipline_code}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Current: {seq.current_number} | Last Generated:{' '}
|
||||
{seq.last_generated_number}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Updated {new Date(seq.updated_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Template Testing Dialog
|
||||
|
||||
```typescript
|
||||
// File: src/components/numbering/template-tester.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
export function TemplateTester({ open, onOpenChange, template }: any) {
|
||||
const [testData, setTestData] = useState({
|
||||
organization_id: 1,
|
||||
discipline_id: null,
|
||||
year: new Date().getFullYear(),
|
||||
});
|
||||
const [generatedNumber, setGeneratedNumber] = useState('');
|
||||
|
||||
const handleTest = async () => {
|
||||
// Call API to generate test number
|
||||
const response = await fetch('/api/numbering/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ template_id: template.template_id, ...testData }),
|
||||
});
|
||||
const result = await response.json();
|
||||
setGeneratedNumber(result.number);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Test Number Generation</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Organization</Label>
|
||||
<Select value={testData.organization_id.toString()}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">กทท.</SelectItem>
|
||||
<SelectItem value="2">สค©.</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Discipline (Optional)</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select discipline" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">STR</SelectItem>
|
||||
<SelectItem value="2">ARC</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleTest} className="w-full">
|
||||
Generate Test Number
|
||||
</Button>
|
||||
|
||||
{generatedNumber && (
|
||||
<Card className="p-4 bg-green-50 border-green-200">
|
||||
<p className="text-sm text-gray-600 mb-1">Generated Number:</p>
|
||||
<p className="text-2xl font-mono font-bold text-green-700">
|
||||
{generatedNumber}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Deliverables
|
||||
|
||||
- [ ] Template list page
|
||||
- [ ] Template editor with variable selector
|
||||
- [ ] Live preview generator
|
||||
- [ ] Number sequence viewer
|
||||
- [ ] Template testing interface
|
||||
- [ ] Annual reset configuration
|
||||
- [ ] Validation rules
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
1. **Template Creation**
|
||||
|
||||
- Create template → Preview updates
|
||||
- Insert variables → Format correct
|
||||
- Save template → Persists
|
||||
|
||||
2. **Number Generation**
|
||||
|
||||
- Test template → Generates number
|
||||
- Variables replaced correctly
|
||||
- Sequence increments
|
||||
|
||||
3. **Sequence Management**
|
||||
- View sequences → Shows all active sequences
|
||||
- Search sequences → Filters correctly
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Documents
|
||||
|
||||
- [TASK-BE-004: Document Numbering](./TASK-BE-004-document-numbering.md)
|
||||
- [ADR-002: Document Numbering Strategy](../../05-decisions/ADR-002-document-numbering-strategy.md)
|
||||
|
||||
---
|
||||
|
||||
**Created:** 2025-12-01
|
||||
**Status:** Ready
|
||||
Reference in New Issue
Block a user