251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-08 16:25:56 +07:00
parent dcd126d704
commit 863a727756
64 changed files with 5956 additions and 1256 deletions

View File

@@ -1,43 +1,44 @@
"use client";
'use client';
import { useState, useEffect } from "react";
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, Loader2 } from "lucide-react";
import { numberingApi } from "@/lib/api/numbering";
import { NumberingSequence } from "@/types/numbering";
import { useState, useEffect } from 'react';
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';
import { numberingApi, NumberSequence } from '@/lib/api/numbering';
export function SequenceViewer({ templateId }: { templateId: number }) {
const [sequences, setSequences] = useState<NumberingSequence[]>([]);
export function SequenceViewer() {
const [sequences, setSequences] = useState<NumberSequence[]>([]);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState("");
const fetchSequences = async () => {
setLoading(true);
try {
const data = await numberingApi.getSequences(templateId);
setSequences(data);
} catch (error) {
console.error("Failed to fetch sequences", error);
const data = await numberingApi.getSequences();
setSequences(data);
} finally {
setLoading(false);
setLoading(false);
}
};
useEffect(() => {
if (templateId) {
fetchSequences();
}
}, [templateId]);
fetchSequences();
}, []);
const filteredSequences = sequences.filter(s =>
s.year.toString().includes(search) ||
s.organization_code?.toLowerCase().includes(search.toLowerCase()) ||
s.discipline_code?.toLowerCase().includes(search.toLowerCase())
);
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" onClick={fetchSequences} disabled={loading}>
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
@@ -50,43 +51,36 @@ export function SequenceViewer({ templateId }: { templateId: number }) {
/>
</div>
{loading && sequences.length === 0 ? (
<div className="flex justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="space-y-2">
{sequences.length === 0 ? (
<p className="text-center text-muted-foreground py-4">No sequences found.</p>
) : (
sequences.map((seq) => (
<div
key={seq.sequence_id}
className="flex items-center justify-between p-3 bg-muted/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-muted-foreground">
Current: {seq.current_number} | Last Generated:{" "}
{seq.last_generated_number}
</div>
</div>
<div className="text-sm text-muted-foreground">
Updated {new Date(seq.updated_at).toLocaleDateString()}
</div>
<div className="space-y-2">
{filteredSequences.length === 0 && (
<div className="text-center text-muted-foreground py-4">No sequences found</div>
)}
{filteredSequences.map((seq) => (
<div
key={seq.sequence_id}
className="flex items-center justify-between p-3 bg-slate-50 dark:bg-slate-900 rounded border"
>
<div>
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">Year {seq.year}</span>
{seq.organization_code && (
<Badge>{seq.organization_code}</Badge>
)}
{seq.discipline_code && (
<Badge variant="outline">{seq.discipline_code}</Badge>
)}
</div>
))
)}
</div>
)}
<div className="text-sm text-muted-foreground">
<span className="text-foreground font-medium">Current: {seq.current_number}</span> | Last Generated:{' '}
<span className="font-mono">{seq.last_generated_number}</span>
</div>
</div>
<div className="text-sm text-gray-500">
Updated {new Date(seq.updated_at).toLocaleDateString()}
</div>
</div>
))}
</div>
</Card>
);
}

View File

@@ -1,99 +1,133 @@
"use client";
'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 { 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";
import { CreateTemplateDto } from "@/types/numbering";
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { NumberingTemplate } from '@/lib/api/numbering';
const VARIABLES = [
{ key: "{ORG}", name: "Organization Code", example: "กทท" },
{ key: "{DOCTYPE}", name: "Document Type", example: "CORR" },
{ key: "{DISC}", name: "Discipline", example: "STR" },
{ key: "{YYYY}", name: "Year (4-digit)", example: "2025" },
{ key: "{YY}", name: "Year (2-digit)", example: "25" },
{ key: "{MM}", name: "Month", example: "12" },
{ key: "{SEQ}", name: "Sequence Number", example: "0001" },
{ key: "{CONTRACT}", name: "Contract Code", example: "C01" },
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' },
];
interface TemplateEditorProps {
initialData?: Partial<CreateTemplateDto>;
onSave: (data: CreateTemplateDto) => void;
const VARIABLES = [
{ key: '{PROJECT}', name: 'Project Code', example: 'LCBP3' },
{ 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 interface TemplateEditorProps {
template?: NumberingTemplate;
projectId: number;
projectName: string;
onSave: (data: Partial<NumberingTemplate>) => void;
onCancel: () => void;
}
export function TemplateEditor({ initialData, onSave }: TemplateEditorProps) {
const [formData, setFormData] = useState<CreateTemplateDto>({
document_type_id: initialData?.document_type_id || "",
discipline_code: initialData?.discipline_code || "",
template_format: initialData?.template_format || "",
reset_annually: initialData?.reset_annually ?? true,
padding_length: initialData?.padding_length || 4,
starting_number: initialData?.starting_number || 1,
});
const [preview, setPreview] = useState("");
export function TemplateEditor({ template, projectId, projectName, onSave, onCancel }: TemplateEditorProps) {
const [format, setFormat] = useState(template?.template_format || '');
const [docType, setDocType] = useState(template?.document_type_name || '');
const [discipline, setDiscipline] = useState(template?.discipline_code || '');
const [padding, setPadding] = useState(template?.padding_length || 4);
const [reset, setReset] = useState(template?.reset_annually ?? true);
const [preview, setPreview] = useState('');
useEffect(() => {
// Generate preview
let previewText = formData.template_format;
let previewText = format || '';
VARIABLES.forEach((v) => {
// Escape special characters for regex if needed, but simple replaceAll is safer for fixed strings
previewText = previewText.split(v.key).join(v.example);
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);
previewText = previewText.replace(new RegExp(v.key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), replacement);
});
setPreview(previewText);
}, [formData.template_format]);
}, [format]);
const insertVariable = (variable: string) => {
setFormData((prev) => ({
...prev,
template_format: prev.template_format + variable,
}));
setFormat((prev) => prev + variable);
};
const handleSave = () => {
onSave({
...template,
project_id: projectId, // Ensure project_id is included
template_format: format,
document_type_name: docType,
discipline_code: discipline || undefined,
padding_length: padding,
reset_annually: reset,
example_number: preview
});
};
return (
<Card className="p-6 space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">Template Configuration</h3>
<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">
Project: {projectName}
</Badge>
</div>
<div className="grid gap-4">
<div>
<Label>Document Type *</Label>
<Select
value={formData.document_type_id}
onValueChange={(value) => setFormData({ ...formData, document_type_id: value })}
>
<Select value={docType} onValueChange={setDocType}>
<SelectTrigger>
<SelectValue placeholder="Select document type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="correspondence">Correspondence</SelectItem>
<SelectItem value="rfa">RFA</SelectItem>
<SelectItem value="drawing">Drawing</SelectItem>
{DOCUMENT_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Discipline (Optional)</Label>
<Select
value={formData.discipline_code}
onValueChange={(value) => setFormData({ ...formData, discipline_code: value })}
>
<Select value={discipline || "ALL"} onValueChange={(val) => setDiscipline(val === "ALL" ? "" : val)}>
<SelectTrigger>
<SelectValue placeholder="All disciplines" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All</SelectItem>
<SelectItem value="ALL">All</SelectItem>
<SelectItem value="STR">STR - Structure</SelectItem>
<SelectItem value="ARC">ARC - Architecture</SelectItem>
</SelectContent>
@@ -104,10 +138,10 @@ export function TemplateEditor({ initialData, onSave }: TemplateEditorProps) {
<Label>Template Format *</Label>
<div className="space-y-2">
<Input
value={formData.template_format}
onChange={(e) => setFormData({ ...formData, template_format: e.target.value })}
placeholder="e.g., {ORG}-{DOCTYPE}-{YYYY}-{SEQ}"
className="font-mono"
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) => (
@@ -117,6 +151,7 @@ export function TemplateEditor({ initialData, onSave }: TemplateEditorProps) {
size="sm"
onClick={() => insertVariable(v.key)}
type="button"
className="font-mono text-xs"
>
{v.key}
</Button>
@@ -128,9 +163,9 @@ export function TemplateEditor({ initialData, onSave }: TemplateEditorProps) {
<div>
<Label>Preview</Label>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-sm text-muted-foreground mb-1">Example number:</p>
<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"}
{preview || 'Enter format above'}
</p>
</div>
</div>
@@ -140,34 +175,20 @@ export function TemplateEditor({ initialData, onSave }: TemplateEditorProps) {
<Label>Sequence Padding Length</Label>
<Input
type="number"
value={formData.padding_length}
onChange={(e) => setFormData({ ...formData, padding_length: parseInt(e.target.value) })}
min={1}
max={10}
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, 0002)
Number of digits (e.g., 4 = 0001)
</p>
</div>
<div>
<Label>Starting Number</Label>
<Input
type="number"
value={formData.starting_number}
onChange={(e) => setFormData({ ...formData, starting_number: parseInt(e.target.value) })}
min={1}
/>
</div>
</div>
<div className="space-y-2">
<label className="flex items-center gap-2">
<Checkbox
checked={formData.reset_annually}
onCheckedChange={(checked) => setFormData({ ...formData, reset_annually: checked as boolean })}
/>
<span className="text-sm">Reset annually (on January 1st)</span>
<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>
</div>
</div>
@@ -176,27 +197,27 @@ export function TemplateEditor({ initialData, onSave }: TemplateEditorProps) {
{/* Variable Reference */}
<div>
<h4 className="font-semibold mb-3">Available Variables</h4>
<div className="grid grid-cols-2 gap-3">
<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-muted/50 rounded"
className="flex items-center justify-between p-2 bg-slate-50 dark:bg-slate-900 rounded border"
>
<div>
<Badge variant="outline" className="font-mono">
<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>
<span className="text-sm text-muted-foreground">{v.example}</span>
<span className="text-sm text-foreground">{v.example}</span>
</div>
))}
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => window.history.back()}>Cancel</Button>
<Button onClick={() => onSave(formData)}>Save Template</Button>
<Button variant="outline" onClick={onCancel}>Cancel</Button>
<Button onClick={handleSave}>Save Template</Button>
</div>
</Card>
);

View File

@@ -1,52 +1,48 @@
"use client";
'use client';
import { useState } from "react";
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
} 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";
import { NumberingTemplate } from "@/types/numbering";
import { numberingApi } from "@/lib/api/numbering";
import { Loader2 } from "lucide-react";
} from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card';
import { NumberingTemplate, numberingApi } from '@/lib/api/numbering';
import { Loader2 } from 'lucide-react';
interface TemplateTesterProps {
open: boolean;
onOpenChange: (open: boolean) => void;
template: NumberingTemplate | null;
open: boolean;
onOpenChange: (open: boolean) => void;
template: NumberingTemplate | null;
}
export function TemplateTester({ open, onOpenChange, template }: TemplateTesterProps) {
const [testData, setTestData] = useState({
organization_id: "1",
discipline_id: "1",
organization_id: '1',
discipline_id: '1',
year: new Date().getFullYear(),
});
const [generatedNumber, setGeneratedNumber] = useState("");
const [generatedNumber, setGeneratedNumber] = useState('');
const [loading, setLoading] = useState(false);
const handleTest = async () => {
if (!template) return;
setLoading(true);
try {
const result = await numberingApi.testTemplate(template.template_id, testData);
setGeneratedNumber(result.number);
} catch (error) {
console.error("Failed to generate test number", error);
setGeneratedNumber("Error generating number");
const result = await numberingApi.generateTestNumber(template.template_id, testData);
setGeneratedNumber(result.number);
} finally {
setLoading(false);
setLoading(false);
}
};
@@ -57,35 +53,34 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
<DialogTitle>Test Number Generation</DialogTitle>
</DialogHeader>
<div className="text-sm text-muted-foreground mb-2">
Template: <span className="font-mono font-bold text-foreground">{template?.template_format}</span>
</div>
<div className="space-y-4">
<div>
<Label>Organization</Label>
<Select
value={testData.organization_id}
onValueChange={(value) => setTestData({ ...testData, organization_id: value })}
>
<Label>Organization (Mock Context)</Label>
<Select value={testData.organization_id} onValueChange={v => setTestData({...testData, organization_id: v})}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">.</SelectItem>
<SelectItem value="2">©.</SelectItem>
<SelectItem value="1">Port Authority (PAT/)</SelectItem>
<SelectItem value="2">Contractor (CN/)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Discipline (Optional)</Label>
<Select
value={testData.discipline_id}
onValueChange={(value) => setTestData({ ...testData, discipline_id: value })}
>
<Label>Discipline (Mock Context)</Label>
<Select value={testData.discipline_id} onValueChange={v => setTestData({...testData, discipline_id: v})}>
<SelectTrigger>
<SelectValue placeholder="Select discipline" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">STR</SelectItem>
<SelectItem value="2">ARC</SelectItem>
<SelectItem value="1">Structure (STR)</SelectItem>
<SelectItem value="2">Architecture (ARC)</SelectItem>
<SelectItem value="3">General (GEN)</SelectItem>
</SelectContent>
</Select>
</div>
@@ -96,9 +91,9 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
</Button>
{generatedNumber && (
<Card className="p-4 bg-green-50 border-green-200">
<Card className="p-4 bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-900 border text-center">
<p className="text-sm text-muted-foreground mb-1">Generated Number:</p>
<p className="text-2xl font-mono font-bold text-green-700">
<p className="text-2xl font-mono font-bold text-green-700 dark:text-green-400">
{generatedNumber}
</p>
</Card>