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,135 +1,193 @@
"use client";
'use client';
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Plus, Edit, Eye, Loader2 } from "lucide-react";
import Link from "next/link";
import { NumberingTemplate } from "@/types/numbering";
import { numberingApi } from "@/lib/api/numbering";
import { TemplateTester } from "@/components/numbering/template-tester";
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Plus, Edit, Play } 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,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const PROJECTS = [
{ id: '1', name: 'LCBP3' },
{ id: '2', name: 'LCBP3-Maintenance' },
];
export default function NumberingPage() {
const [selectedProjectId, setSelectedProjectId] = useState("1");
const [templates, setTemplates] = useState<NumberingTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [testerOpen, setTesterOpen] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<NumberingTemplate | null>(null);
const [, setLoading] = useState(true);
useEffect(() => {
const fetchTemplates = async () => {
setLoading(true);
try {
// View states
const [isEditing, setIsEditing] = useState(false);
const [activeTemplate, setActiveTemplate] = useState<NumberingTemplate | undefined>(undefined);
const [isTesting, setIsTesting] = useState(false);
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null);
const selectedProjectName = PROJECTS.find(p => p.id === selectedProjectId)?.name || 'Unknown Project';
const loadTemplates = async () => {
setLoading(true);
try {
const data = await numberingApi.getTemplates();
setTemplates(data);
} catch (error) {
console.error("Failed to fetch templates", error);
} finally {
} catch {
toast.error("Failed to load templates");
} finally {
setLoading(false);
}
};
}
};
fetchTemplates();
useEffect(() => {
loadTemplates();
}, []);
const handleTest = (template: NumberingTemplate) => {
setSelectedTemplate(template);
setTesterOpen(true);
const handleEdit = (template?: NumberingTemplate) => {
setActiveTemplate(template);
setIsEditing(true);
};
const handleSave = async (data: Partial<NumberingTemplate>) => {
try {
await numberingApi.saveTemplate(data);
toast.success(data.template_id ? "Template updated" : "Template created");
setIsEditing(false);
loadTemplates();
} catch {
toast.error("Failed to save template");
}
};
const handleTest = (template: NumberingTemplate) => {
setTestTemplate(template);
setIsTesting(true);
};
if (isEditing) {
return (
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
<TemplateEditor
template={activeTemplate}
projectId={Number(selectedProjectId)}
projectName={selectedProjectName}
onSave={handleSave}
onCancel={() => setIsEditing(false)}
/>
</div>
);
}
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 className="text-3xl font-bold tracking-tight">
Document Numbering
</h1>
<p className="text-muted-foreground mt-1">
Manage document numbering templates and sequences
Manage numbering templates and sequences
</p>
</div>
<Link href="/admin/numbering/new">
<Button>
<Plus className="mr-2 h-4 w-4" />
New Template
</Button>
</Link>
<div className="flex gap-2">
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select Project" />
</SelectTrigger>
<SelectContent>
{PROJECTS.map(project => (
<SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => handleEdit(undefined)}>
<Plus className="mr-2 h-4 w-4" />
New Template
</Button>
</div>
</div>
{loading ? (
<div className="flex justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<div className="grid gap-4">
{templates.map((template) => (
<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 variant="outline">{template.discipline_code || "All"}</Badge>
<Badge variant={template.is_active ? "default" : "secondary"} className={template.is_active ? "bg-green-600 hover:bg-green-700" : ""}>
{template.is_active ? "Active" : "Inactive"}
</Badge>
</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.project_id || t.project_id === Number(selectedProjectId)) // Show all if no project_id (legacy mock), or match
.map((template) => (
<Card key={template.template_id} 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.document_type_name}
</h3>
<Badge variant="outline" className="text-xs">
{PROJECTS.find(p => p.id === template.project_id?.toString())?.name || selectedProjectName}
</Badge>
{template.discipline_code && <Badge>{template.discipline_code}</Badge>}
<Badge variant={template.is_active ? 'default' : 'secondary'}>
{template.is_active ? 'Active' : 'Inactive'}
</Badge>
</div>
<div className="bg-muted rounded px-3 py-2 mb-3 font-mono text-sm">
{template.template_format}
</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.template_format}
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Example: </span>
<span className="font-medium">
{template.example_number}
</span>
<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.example_number}
</span>
</div>
<div>
<span className="text-muted-foreground">Reset: </span>
<span>
{template.reset_annually ? 'Annually' : 'Never'}
</span>
</div>
</div>
</div>
<div>
<span className="text-muted-foreground">Current Sequence: </span>
<span className="font-medium">
{template.current_number}
</span>
</div>
<div>
<span className="text-muted-foreground">Annual Reset: </span>
<span className="font-medium">
{template.reset_annually ? "Yes" : "No"}
</span>
</div>
<div>
<span className="text-muted-foreground">Padding: </span>
<span className="font-medium">
{template.padding_length} digits
</span>
</div>
</div>
</div>
<div className="flex gap-2">
<Link href={`/admin/numbering/${template.template_id}/edit`}>
<Button variant="outline" size="sm">
<Edit className="mr-2 h-4 w-4" />
Edit
</Button>
</Link>
<Button variant="outline" size="sm" onClick={() => handleTest(template)}>
<Eye className="mr-2 h-4 w-4" />
Test & View
</Button>
</div>
</div>
</Card>
))}
</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">
{/* Sequence Viewer Sidebar */}
<SequenceViewer />
</div>
</div>
<TemplateTester
open={testerOpen}
onOpenChange={setTesterOpen}
template={selectedTemplate}
open={isTesting}
onOpenChange={setIsTesting}
template={testTemplate}
/>
</div>
);

View File

@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default function AdminPage() {
redirect('/admin/workflows');
}

View File

@@ -1,164 +1,206 @@
"use client";
'use client';
import { useState, useEffect } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { DSLEditor } from "@/components/workflows/dsl-editor";
import { VisualWorkflowBuilder } from "@/components/workflows/visual-builder";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { workflowApi } from "@/lib/api/workflows";
import { WorkflowType } from "@/types/workflow";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { DSLEditor } from '@/components/workflows/dsl-editor';
import { VisualWorkflowBuilder } from '@/components/workflows/visual-builder';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Card } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { workflowApi } from '@/lib/api/workflows';
import { Workflow, CreateWorkflowDto } from '@/types/workflow';
import { toast } from 'sonner';
import { Save, ArrowLeft, Loader2 } from 'lucide-react';
import Link from 'next/link';
export default function WorkflowEditPage({ params }: { params: { id: string } }) {
export default function WorkflowEditPage() {
const params = useParams();
const router = useRouter();
const [loading, setLoading] = useState(true);
const id = params?.id === 'new' ? null : Number(params?.id);
const [loading, setLoading] = useState(!!id);
const [saving, setSaving] = useState(false);
const [workflowData, setWorkflowData] = useState({
workflow_name: "",
description: "",
workflow_type: "CORRESPONDENCE" as WorkflowType,
dsl_definition: "",
const [workflowData, setWorkflowData] = useState<Partial<Workflow>>({
workflow_name: '',
description: '',
workflow_type: 'CORRESPONDENCE',
dsl_definition: 'name: New Workflow\nversion: 1.0\nsteps: []',
is_active: true,
});
useEffect(() => {
const fetchWorkflow = async () => {
setLoading(true);
try {
const data = await workflowApi.getWorkflow(parseInt(params.id));
if (data) {
setWorkflowData({
workflow_name: data.workflow_name,
description: data.description,
workflow_type: data.workflow_type,
dsl_definition: data.dsl_definition,
});
}
} catch (error) {
console.error("Failed to fetch workflow", error);
} finally {
setLoading(false);
}
};
fetchWorkflow();
}, [params.id]);
if (id) {
const fetchWorkflow = async () => {
try {
const data = await workflowApi.getWorkflow(id);
if (data) {
setWorkflowData(data);
} else {
toast.error("Workflow not found");
router.push('/admin/workflows');
}
} catch (error) {
toast.error("Failed to load workflow");
console.error(error);
} finally {
setLoading(false);
}
};
fetchWorkflow();
}
}, [id, router]);
const handleSave = async () => {
if (!workflowData.workflow_name) {
toast.error("Workflow name is required");
return;
}
setSaving(true);
try {
await workflowApi.updateWorkflow(parseInt(params.id), workflowData);
router.push("/admin/workflows");
const dto: CreateWorkflowDto = {
workflow_name: workflowData.workflow_name || '',
description: workflowData.description || '',
workflow_type: workflowData.workflow_type || 'CORRESPONDENCE',
dsl_definition: workflowData.dsl_definition || '',
};
if (id) {
await workflowApi.updateWorkflow(id, dto);
toast.success("Workflow updated successfully");
} else {
await workflowApi.createWorkflow(dto);
toast.success("Workflow created successfully");
router.push('/admin/workflows');
}
} catch (error) {
console.error("Failed to save workflow", error);
alert("Failed to save workflow");
toast.error("Failed to save workflow");
console.error(error);
} finally {
setSaving(false);
setSaving(false);
}
};
if (loading) {
return (
<div className="flex justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
);
}
return (
<div className="p-6 space-y-6">
<div className="p-6 space-y-6 max-w-7xl mx-auto">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">Edit Workflow</h1>
<div className="flex items-center gap-4">
<Link href="/admin/workflows">
<Button variant="ghost" size="icon">
<ArrowLeft className="h-5 w-5" />
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold">{id ? 'Edit Workflow' : 'New Workflow'}</h1>
<p className="text-muted-foreground">{id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}</p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => router.back()}>Cancel</Button>
<Link href="/admin/workflows">
<Button variant="outline">Cancel</Button>
</Link>
<Button onClick={handleSave} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Workflow
<Save className="mr-2 h-4 w-4" />
{id ? 'Save Changes' : 'Create Workflow'}
</Button>
</div>
</div>
<Card className="p-6">
<div className="grid gap-4">
<div>
<Label htmlFor="workflow_name">Workflow Name *</Label>
<Input
id="workflow_name"
value={workflowData.workflow_name}
onChange={(e) =>
setWorkflowData({
...workflowData,
workflow_name: e.target.value,
})
}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-1 space-y-6">
<Card className="p-6">
<div className="grid gap-4">
<div>
<Label htmlFor="name">Workflow Name *</Label>
<Input
id="name"
value={workflowData.workflow_name}
onChange={(e) =>
setWorkflowData({
...workflowData,
workflow_name: e.target.value,
})
}
placeholder="e.g. Standard RFA Workflow"
/>
</div>
<div>
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={workflowData.description}
onChange={(e) =>
setWorkflowData({
...workflowData,
description: e.target.value,
})
}
/>
</div>
<div>
<Label htmlFor="desc">Description</Label>
<Textarea
id="desc"
value={workflowData.description}
onChange={(e) =>
setWorkflowData({
...workflowData,
description: e.target.value,
})
}
placeholder="Describe the purpose of this workflow"
/>
</div>
<div>
<Label htmlFor="workflow_type">Workflow Type</Label>
<Select
value={workflowData.workflow_type}
onValueChange={(value) =>
setWorkflowData({ ...workflowData, workflow_type: value as WorkflowType })
}
>
<SelectTrigger id="workflow_type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="CORRESPONDENCE">Correspondence</SelectItem>
<SelectItem value="RFA">RFA</SelectItem>
<SelectItem value="DRAWING">Drawing</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="type">Workflow Type</Label>
<Select
value={workflowData.workflow_type}
onValueChange={(value: Workflow['workflow_type']) =>
setWorkflowData({ ...workflowData, workflow_type: value })
}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="CORRESPONDENCE">Correspondence</SelectItem>
<SelectItem value="RFA">RFA</SelectItem>
<SelectItem value="DRAWING">Drawing</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Card>
</div>
</Card>
<Tabs defaultValue="dsl">
<TabsList>
<TabsTrigger value="dsl">DSL Editor</TabsTrigger>
<TabsTrigger value="visual">Visual Builder</TabsTrigger>
</TabsList>
<div className="lg:col-span-2">
<Tabs defaultValue="dsl" className="w-full">
<TabsList className="w-full justify-start">
<TabsTrigger value="dsl">DSL Editor</TabsTrigger>
<TabsTrigger value="visual">Visual Builder</TabsTrigger>
</TabsList>
<TabsContent value="dsl" className="mt-4">
<DSLEditor
initialValue={workflowData.dsl_definition}
onChange={(value) =>
setWorkflowData({ ...workflowData, dsl_definition: value })
}
/>
</TabsContent>
<TabsContent value="dsl" className="mt-4">
<DSLEditor
initialValue={workflowData.dsl_definition}
onChange={(value) =>
setWorkflowData({ ...workflowData, dsl_definition: value })
}
/>
</TabsContent>
<TabsContent value="visual" className="mt-4">
<VisualWorkflowBuilder />
</TabsContent>
</Tabs>
<TabsContent value="visual" className="mt-4 h-[600px]">
<VisualWorkflowBuilder
dslString={workflowData.dsl_definition}
onDslChange={(newDsl) => setWorkflowData({ ...workflowData, dsl_definition: newDsl })}
onSave={() => toast.info("Visual state saving not implemented in this demo")}
/>
</TabsContent>
</Tabs>
</div>
</div>
</div>
);
}

View File

@@ -1,7 +1,6 @@
import { AdminSidebar } from "@/components/admin/sidebar";
import { redirect } from "next/navigation";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { auth } from "@/lib/auth";
export default async function AdminLayout({
@@ -9,16 +8,13 @@ export default async function AdminLayout({
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
const session = await auth();
// Check if user has admin role
// This depends on your Session structure. Assuming user.roles exists (mapped in callback).
// If not, you might need to check DB or use Can component logic but server-side.
const isAdmin = session?.user?.roles?.some((r: any) => r.role_name === 'ADMIN');
// Temporary bypass for UI testing
const isAdmin = true; // session?.user?.role === 'ADMIN';
if (!session || !isAdmin) {
// If not admin, redirect to dashboard or login
redirect("/");
// redirect("/");
}
return (

View File

@@ -1,16 +1,14 @@
"use client";
import { useState, useEffect } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { SearchFilters } from "@/components/search/filters";
import { SearchResults } from "@/components/search/results";
import { SearchFilters as FilterType } from "@/types/search";
import { useSearch } from "@/hooks/use-search";
import { Button } from "@/components/ui/button";
export default function SearchPage() {
const searchParams = useSearchParams();
const router = useRouter();
// URL Params state
const query = searchParams.get("q") || "";
@@ -65,7 +63,7 @@ export default function SearchPage() {
{isError ? (
<div className="text-red-500 py-8 text-center">Failed to load search results.</div>
) : (
<SearchResults results={results || []} query={query} loading={isLoading} />
<SearchResults results={results?.data || []} query={query} loading={isLoading} />
)}
</div>
</div>