260322:1648 Correct Coresspondence / Doing RFA / Correct CI
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s

This commit is contained in:
admin
2026-03-22 16:48:12 +07:00
parent e5deedb42e
commit 11984bfa29
683 changed files with 105251 additions and 29068 deletions
@@ -1,8 +1,8 @@
// File: components/layout/dashboard-shell.tsx
"use client";
'use client';
import { useUIStore } from "@/lib/stores/ui-store";
import { cn } from "@/lib/utils";
import { useUIStore } from '@/lib/stores/ui-store';
import { cn } from '@/lib/utils';
export function DashboardShell({ children }: { children: React.ReactNode }) {
const { isSidebarOpen } = useUIStore();
@@ -10,12 +10,12 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
return (
<div
className={cn(
"flex flex-col min-h-screen transition-all duration-300 ease-in-out",
'flex flex-col min-h-screen transition-all duration-300 ease-in-out',
// ปรับ Margin ซ้าย ตามสถานะ Sidebar
isSidebarOpen ? "md:ml-[240px]" : "md:ml-[70px]"
isSidebarOpen ? 'md:ml-[240px]' : 'md:ml-[70px]'
)}
>
{children}
</div>
);
}
}
+25 -27
View File
@@ -1,18 +1,12 @@
"use client";
'use client';
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Search, FileText, Clipboard, Image, Loader2 } from "lucide-react";
import { Input } from "@/components/ui/input";
import {
Command, CommandGroup, CommandItem, CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useSearchSuggestions } from "@/hooks/use-search";
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Search, FileText, Clipboard, Image, Loader2 } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useSearchSuggestions } from '@/hooks/use-search';
/** Search suggestion item returned from the API */
interface SearchSuggestion {
@@ -39,7 +33,7 @@ function useDebounceValue<T>(value: T, delay: number): T {
export function GlobalSearch() {
const router = useRouter();
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [query, setQuery] = useState('');
const debouncedQuery = useDebounceValue(query, 300);
@@ -62,10 +56,14 @@ export function GlobalSearch() {
const getIcon = (type: string) => {
switch (type) {
case "correspondence": return <FileText className="mr-2 h-4 w-4" />;
case "rfa": return <Clipboard className="mr-2 h-4 w-4" />;
case "drawing": return <Image className="mr-2 h-4 w-4" />;
default: return <Search className="mr-2 h-4 w-4" />;
case 'correspondence':
return <FileText className="mr-2 h-4 w-4" />;
case 'rfa':
return <Clipboard className="mr-2 h-4 w-4" />;
case 'drawing':
return <Image className="mr-2 h-4 w-4" />;
default:
return <Search className="mr-2 h-4 w-4" />;
}
};
@@ -81,17 +79,19 @@ export function GlobalSearch() {
className="pl-8 w-full bg-background"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
onFocus={() => {
if (suggestions && suggestions.length > 0) setOpen(true);
}}
/>
{isLoading && (
<Loader2 className="absolute right-2.5 top-2.5 h-4 w-4 animate-spin text-muted-foreground" />
)}
{isLoading && <Loader2 className="absolute right-2.5 top-2.5 h-4 w-4 animate-spin text-muted-foreground" />}
</div>
</PopoverTrigger>
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]" align="start" onOpenAutoFocus={(e) => e.preventDefault()}>
<PopoverContent
className="p-0 w-[var(--radix-popover-trigger-width)]"
align="start"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<Command>
<CommandList>
{suggestions && suggestions.length > 0 && (
@@ -114,9 +114,7 @@ export function GlobalSearch() {
</CommandGroup>
)}
{(!suggestions || suggestions.length === 0) && !isLoading && (
<div className="py-6 text-center text-sm text-muted-foreground">
No suggestions found.
</div>
<div className="py-6 text-center text-sm text-muted-foreground">No suggestions found.</div>
)}
</CommandList>
</Command>
+10 -17
View File
@@ -1,11 +1,11 @@
// File: components/layout/navbar.tsx
"use client";
'use client';
import Link from "next/link";
import { Menu, Bell } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useUIStore } from "@/lib/stores/ui-store";
import { UserNav } from "./user-nav";
import _Link from 'next/link';
import { Menu, Bell } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useUIStore } from '@/lib/stores/ui-store';
import { UserNav } from './user-nav';
export function Navbar() {
const { toggleSidebar } = useUIStore();
@@ -13,21 +13,14 @@ export function Navbar() {
return (
<header className="flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:pr-6 lg:pl-1 sticky top-0 z-30">
{/* Toggle Sidebar Button (Mobile Only) */}
<Button
variant="outline"
size="icon"
className="shrink-0 md:hidden"
onClick={toggleSidebar}
>
<Button variant="outline" size="icon" className="shrink-0 md:hidden" onClick={toggleSidebar}>
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle navigation menu</span>
</Button>
<div className="w-full flex-1">
{/* Breadcrumbs หรือ Search Bar จะมาใส่ตรงนี้ */}
<h1 className="text-lg font-semibold md:text-xl hidden md:block">
Document Management System
</h1>
<h1 className="text-lg font-semibold md:text-xl hidden md:block">Document Management System</h1>
</div>
{/* Right Actions (เหลือชุดเดียวที่ถูกต้อง) */}
@@ -36,10 +29,10 @@ export function Navbar() {
<Bell className="h-5 w-5" />
<span className="sr-only">Notifications</span>
</Button>
{/* User Menu */}
<UserNav />
</div>
</header>
);
}
}
@@ -1,7 +1,7 @@
"use client";
'use client';
import { Bell, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Bell, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -9,12 +9,12 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge";
import { useNotifications, useMarkNotificationRead } from "@/hooks/use-notification";
import { formatDistanceToNow } from "date-fns";
import { useRouter } from "next/navigation";
import type { Notification } from "@/types/notification";
} from '@/components/ui/dropdown-menu';
import { Badge } from '@/components/ui/badge';
import { useNotifications, useMarkNotificationRead } from '@/hooks/use-notification';
import { formatDistanceToNow } from 'date-fns';
import { useRouter } from 'next/navigation';
import type { Notification } from '@/types/notification';
export function NotificationsDropdown() {
const router = useRouter();
@@ -54,30 +54,24 @@ export function NotificationsDropdown() {
<DropdownMenuSeparator />
{isLoading ? (
<div className="flex justify-center p-4">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : notifications.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
No new notifications
<div className="flex justify-center p-4">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : notifications.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">No new notifications</div>
) : (
<div className="max-h-96 overflow-y-auto">
{notifications.slice(0, 5).map((notification: Notification) => (
<DropdownMenuItem
key={notification.notificationId}
className={`flex flex-col items-start p-3 cursor-pointer ${
!notification.isRead ? 'bg-muted/30' : ''
}`}
className={`flex flex-col items-start p-3 cursor-pointer ${!notification.isRead ? 'bg-muted/30' : ''}`}
onClick={() => handleNotificationClick(notification)}
>
<div className="flex justify-between w-full">
<span className="font-medium text-sm">{notification.title}</span>
{!notification.isRead && <span className="h-2 w-2 rounded-full bg-blue-500 mt-1" />}
</div>
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
{notification.message}
<span className="font-medium text-sm">{notification.title}</span>
{!notification.isRead && <span className="h-2 w-2 rounded-full bg-blue-500 mt-1" />}
</div>
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">{notification.message}</div>
<div className="text-[10px] text-muted-foreground mt-1 self-end">
{formatDistanceToNow(new Date(notification.createdAt), {
addSuffix: true,
+16 -22
View File
@@ -1,4 +1,4 @@
"use client";
'use client';
import {
DropdownMenu,
@@ -7,12 +7,12 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { LogOut, Settings, User } from "lucide-react";
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { signOut, useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { LogOut, Settings, User } from 'lucide-react';
export function UserMenu() {
const router = useRouter();
@@ -24,18 +24,18 @@ export function UserMenu() {
// Generate initials from name or username
const getInitials = (name: string) => {
return name
.split(" ")
.split(' ')
.map((n) => n[0])
.join("")
.join('')
.toUpperCase()
.slice(0, 2);
};
const initials = user.name ? getInitials(user.name) : "U";
const initials = user.name ? getInitials(user.name) : 'U';
const handleLogout = async () => {
await signOut({ redirect: false });
router.push("/login");
router.push('/login');
};
return (
@@ -43,9 +43,7 @@ export function UserMenu() {
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-10 w-10 rounded-full">
<Avatar className="h-10 w-10">
<AvatarFallback className="bg-primary/10 text-primary">
{initials}
</AvatarFallback>
<AvatarFallback className="bg-primary/10 text-primary">{initials}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
@@ -53,20 +51,16 @@ export function UserMenu() {
<DropdownMenuLabel>
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user.name}</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
<p className="text-xs leading-none text-muted-foreground mt-1">
Role: {user.role}
</p>
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
<p className="text-xs leading-none text-muted-foreground mt-1">Role: {user.role}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push("/profile")}>
<DropdownMenuItem onClick={() => router.push('/profile')}>
<User className="mr-2 h-4 w-4" />
<span>Profile</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/settings")}>
<DropdownMenuItem onClick={() => router.push('/settings')}>
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
</DropdownMenuItem>
+22 -28
View File
@@ -1,12 +1,8 @@
// File: components/layout/user-nav.tsx
"use client";
'use client';
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
@@ -16,9 +12,9 @@ import {
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
} from '@/components/ui/dropdown-menu';
import { signOut, useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
export function UserNav() {
const { data: session } = useSession();
@@ -26,22 +22,24 @@ export function UserNav() {
// Helper function to get initials from name
const getInitials = (name: string) => {
return name
?.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.substring(0, 2) || "US";
return (
name
?.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.substring(0, 2) || 'US'
);
};
const userName = session?.user?.name || "User";
const userEmail = session?.user?.email || "user@example.com";
const userName = session?.user?.name || 'User';
const userEmail = session?.user?.email || 'user@example.com';
// ใช้ role หรือ organization หากมีใน session (ต้องแก้ type ใน next-auth.d.ts แล้ว)
const userRole = session?.user?.role || "Viewer";
const userRole = session?.user?.role || 'Viewer';
const handleLogout = async () => {
await signOut({ redirect: false });
router.push("/login");
router.push('/login');
};
return (
@@ -50,7 +48,7 @@ export function UserNav() {
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-8 w-8">
{/* ใส่ URL รูปถ้ามี */}
<AvatarImage src={session?.user?.image || ""} alt={userName} />
<AvatarImage src={session?.user?.image || ''} alt={userName} />
<AvatarFallback>{getInitials(userName)}</AvatarFallback>
</Avatar>
</Button>
@@ -59,12 +57,8 @@ export function UserNav() {
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{userName}</p>
<p className="text-xs leading-none text-muted-foreground">
{userEmail}
</p>
<p className="text-xs leading-none text-primary mt-1 font-semibold">
{userRole}
</p>
<p className="text-xs leading-none text-muted-foreground">{userEmail}</p>
<p className="text-xs leading-none text-primary mt-1 font-semibold">{userRole}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
@@ -86,4 +80,4 @@ export function UserNav() {
</DropdownMenuContent>
</DropdownMenu>
);
}
}