690402:2240 fix dashboard
This commit is contained in:
@@ -5,11 +5,14 @@ import { RecentActivity } from '@/components/dashboard/recent-activity';
|
||||
import { PendingTasks } from '@/components/dashboard/pending-tasks';
|
||||
import { QuickActions } from '@/components/dashboard/quick-actions';
|
||||
import { useDashboardStats, useRecentActivity, usePendingTasks } from '@/hooks/use-dashboard';
|
||||
import { useProjectStore } from '@/lib/stores/project-store';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: stats, isLoading: statsLoading } = useDashboardStats();
|
||||
const { data: activities, isLoading: activityLoading } = useRecentActivity();
|
||||
const { data: tasks, isLoading: tasksLoading } = usePendingTasks();
|
||||
const selectedProjectId = useProjectStore((state) => state.selectedProjectId);
|
||||
|
||||
const { data: stats, isLoading: statsLoading } = useDashboardStats(selectedProjectId);
|
||||
const { data: activities, isLoading: activityLoading } = useRecentActivity(selectedProjectId);
|
||||
const { data: tasks, isLoading: tasksLoading } = usePendingTasks(selectedProjectId);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
|
||||
@@ -10,7 +10,7 @@ import { format } from 'date-fns';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface CirculationStatusCardProps {
|
||||
correspondenceUuid: string;
|
||||
correspondencePublicId: string;
|
||||
}
|
||||
|
||||
const ROUTING_STATUS_META: Record<string, { icon: React.ElementType; color: string; label: string }> = {
|
||||
@@ -86,8 +86,8 @@ function CirculationItem({ circ }: { circ: Circulation }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function CirculationStatusCard({ correspondenceUuid }: CirculationStatusCardProps) {
|
||||
const { data, isLoading } = useCirculationsByCorrespondence(correspondenceUuid);
|
||||
export function CirculationStatusCard({ correspondencePublicId }: CirculationStatusCardProps) {
|
||||
const { data, isLoading } = useCirculationsByCorrespondence(correspondencePublicId);
|
||||
|
||||
const circulations: Circulation[] = Array.isArray(data)
|
||||
? data
|
||||
@@ -122,7 +122,7 @@ export function CirculationStatusCard({ correspondenceUuid }: CirculationStatusC
|
||||
))
|
||||
)}
|
||||
|
||||
<Link href={`/circulation/new?correspondenceUuid=${correspondenceUuid}`}>
|
||||
<Link href={`/circulation/new?correspondencePublicId=${correspondencePublicId}`}>
|
||||
<Button variant="outline" size="sm" className="w-full h-8 text-xs mt-1">
|
||||
<GitBranch className="h-3 w-3 mr-1.5" />
|
||||
New Circulation
|
||||
|
||||
@@ -416,7 +416,7 @@ export function CorrespondenceDetail({ data, selectedRevisionId }: Correspondenc
|
||||
</Card>
|
||||
|
||||
{/* Circulations */}
|
||||
<CirculationStatusCard correspondenceUuid={data.publicId} />
|
||||
<CirculationStatusCard correspondencePublicId={data.publicId} />
|
||||
|
||||
{/* Tags */}
|
||||
<TagManager
|
||||
|
||||
@@ -356,6 +356,12 @@ export function CorrespondenceForm({
|
||||
}
|
||||
|
||||
const fetchPreview = async () => {
|
||||
// Don't preview or change number in edit mode
|
||||
if (uuid) {
|
||||
setPreview(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await numberingApi.previewNumber({
|
||||
projectId,
|
||||
@@ -387,29 +393,21 @@ export function CorrespondenceForm({
|
||||
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 && (
|
||||
{/* Preview Section - Only for New Documents */}
|
||||
{preview && !uuid && (
|
||||
<div
|
||||
className={`p-4 rounded-md border ${preview.number !== initialData?.correspondenceNumber ? 'bg-amber-50 border-amber-200' : 'bg-muted border-border'}`}
|
||||
className="p-4 rounded-md border 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>
|
||||
)}
|
||||
Document Number Preview
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={`text-xl font-bold font-mono tracking-wide ${preview.number !== initialData?.correspondenceNumber ? 'text-amber-700' : 'text-primary'}`}
|
||||
className="text-xl font-bold font-mono tracking-wide text-primary"
|
||||
>
|
||||
{preview.number}
|
||||
</span>
|
||||
@@ -419,11 +417,6 @@ export function CorrespondenceForm({
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GlobalSearch } from './global-search';
|
||||
import { NotificationsDropdown } from './notifications-dropdown';
|
||||
import { MobileSidebar } from './sidebar';
|
||||
import { ThemeToggle } from './theme-toggle';
|
||||
import { ProjectSwitcher } from './project-switcher';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
@@ -18,6 +19,7 @@ export function Header() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<ProjectSwitcher />
|
||||
<ThemeToggle />
|
||||
<NotificationsDropdown />
|
||||
<UserMenu />
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// File: components/layout/project-switcher.tsx
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useProjects } from '@/hooks/use-projects';
|
||||
import { useProjectStore } from '@/lib/stores/project-store';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Building2 } from 'lucide-react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export function ProjectSwitcher() {
|
||||
const { data: projects, isLoading } = useProjects({ isActive: true });
|
||||
const { selectedProjectId, setSelectedProjectId } = useProjectStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
// Auto-select if there's only one project
|
||||
if (projects && projects.length === 1 && selectedProjectId !== projects[0].publicId) {
|
||||
setSelectedProjectId(projects[0].publicId);
|
||||
}
|
||||
}, [projects, selectedProjectId, setSelectedProjectId]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className="h-9 w-[200px] lg:w-[250px]" />;
|
||||
}
|
||||
|
||||
// If user has no projects, don't show the switcher
|
||||
if (!projects || projects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If user has exactly one project, show it as text (no dropdown needed)
|
||||
if (projects.length === 1) {
|
||||
return (
|
||||
<div className="flex h-9 items-center px-3 py-2 text-sm border rounded-md bg-muted/50 w-[200px] lg:w-[250px]">
|
||||
<Building2 className="h-4 w-4 mr-2 opacity-50 flex-shrink-0" />
|
||||
<span className="truncate font-medium">{projects[0].projectName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={selectedProjectId || 'global'}
|
||||
onValueChange={(value) => setSelectedProjectId(value === 'global' ? null : value)}
|
||||
>
|
||||
<SelectTrigger className="w-[200px] lg:w-[250px] h-9">
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<Building2 className="h-4 w-4 opacity-50 flex-shrink-0" />
|
||||
<SelectValue placeholder="Select Project..." className="truncate" />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="global">All Projects (Global)</SelectItem>
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.publicId} value={project.publicId}>
|
||||
{project.projectName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ describe('use-circulation hooks', () => {
|
||||
});
|
||||
|
||||
describe('useCirculationsByCorrespondence', () => {
|
||||
it('should fetch circulations for a correspondence UUID', async () => {
|
||||
it('should fetch circulations for a correspondence publicId', async () => {
|
||||
const mockData = {
|
||||
data: [
|
||||
{
|
||||
@@ -60,7 +60,7 @@ describe('use-circulation hooks', () => {
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
});
|
||||
|
||||
it('should not fetch when correspondenceUuid is empty', () => {
|
||||
it('should not fetch when correspondencePublicId is empty', () => {
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(
|
||||
() => useCirculationsByCorrespondence(''),
|
||||
|
||||
@@ -6,10 +6,10 @@ export const circulationKeys = {
|
||||
byCorrespondence: (uuid: string) => ['circulations', 'byCorrespondence', uuid] as const,
|
||||
};
|
||||
|
||||
export function useCirculationsByCorrespondence(correspondenceUuid: string) {
|
||||
export function useCirculationsByCorrespondence(correspondencePublicId: string) {
|
||||
return useQuery({
|
||||
queryKey: circulationKeys.byCorrespondence(correspondenceUuid),
|
||||
queryFn: () => circulationService.getByCorrespondenceUuid(correspondenceUuid),
|
||||
enabled: !!correspondenceUuid,
|
||||
queryKey: circulationKeys.byCorrespondence(correspondencePublicId),
|
||||
queryFn: () => circulationService.getByCorrespondenceUuid(correspondencePublicId),
|
||||
enabled: !!correspondencePublicId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,31 +3,31 @@ import { dashboardService } from '@/lib/services/dashboard.service';
|
||||
|
||||
export const dashboardKeys = {
|
||||
all: ['dashboard'] as const,
|
||||
stats: () => [...dashboardKeys.all, 'stats'] as const,
|
||||
activity: () => [...dashboardKeys.all, 'activity'] as const,
|
||||
pending: () => [...dashboardKeys.all, 'pending'] as const,
|
||||
stats: (projectId?: string | null) => [...dashboardKeys.all, 'stats', projectId] as const,
|
||||
activity: (projectId?: string | null) => [...dashboardKeys.all, 'activity', projectId] as const,
|
||||
pending: (projectId?: string | null) => [...dashboardKeys.all, 'pending', projectId] as const,
|
||||
};
|
||||
|
||||
export function useDashboardStats() {
|
||||
export function useDashboardStats(projectId?: string | null) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.stats(),
|
||||
queryFn: dashboardService.getStats,
|
||||
queryKey: dashboardKeys.stats(projectId),
|
||||
queryFn: () => dashboardService.getStats(projectId),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
export function useRecentActivity() {
|
||||
export function useRecentActivity(projectId?: string | null) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.activity(),
|
||||
queryFn: dashboardService.getRecentActivity,
|
||||
queryKey: dashboardKeys.activity(projectId),
|
||||
queryFn: () => dashboardService.getRecentActivity(projectId),
|
||||
staleTime: 1 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function usePendingTasks() {
|
||||
export function usePendingTasks(projectId?: string | null) {
|
||||
return useQuery({
|
||||
queryKey: dashboardKeys.pending(),
|
||||
queryFn: dashboardService.getPendingTasks,
|
||||
queryKey: dashboardKeys.pending(projectId),
|
||||
queryFn: () => dashboardService.getPendingTasks(projectId),
|
||||
staleTime: 2 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// File: lib/api/dashboard.ts
|
||||
// Change Log:
|
||||
// - Fixed TypeScript type error in mock data (id: number -> id: string)
|
||||
// - Updated PendingTask mock data to match interface
|
||||
|
||||
import { DashboardStats, ActivityLog, PendingTask } from '@/types/dashboard';
|
||||
|
||||
export const dashboardApi = {
|
||||
@@ -17,7 +22,7 @@ export const dashboardApi = {
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
id: 'activity-1',
|
||||
user: { name: 'John Doe', initials: 'JD' },
|
||||
action: 'Created RFA',
|
||||
description: 'RFA-001: Concrete Pouring Request',
|
||||
@@ -25,7 +30,7 @@ export const dashboardApi = {
|
||||
targetUrl: '/rfas/1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: 'activity-2',
|
||||
user: { name: 'Jane Smith', initials: 'JS' },
|
||||
action: 'Approved Correspondence',
|
||||
description: 'COR-005: Site Safety Report',
|
||||
@@ -33,7 +38,7 @@ export const dashboardApi = {
|
||||
targetUrl: '/correspondences/5',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
id: 'activity-3',
|
||||
user: { name: 'Mike Johnson', initials: 'MJ' },
|
||||
action: 'Uploaded Drawing',
|
||||
description: 'A-101: Ground Floor Plan Rev B',
|
||||
@@ -47,7 +52,14 @@ export const dashboardApi = {
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
publicId: 'task-1',
|
||||
workflowCode: 'RFA_WORKFLOW',
|
||||
currentState: 'REVIEWING',
|
||||
entityType: 'RFA',
|
||||
entityId: 'rfa-001-uuid',
|
||||
documentNumber: 'RFA-002',
|
||||
subject: 'Review RFA-002',
|
||||
assignedAt: new Date().toISOString(),
|
||||
title: 'Review RFA-002',
|
||||
description: 'Approval required for steel reinforcement',
|
||||
daysOverdue: 2,
|
||||
@@ -55,7 +67,14 @@ export const dashboardApi = {
|
||||
priority: 'HIGH',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
publicId: 'task-2',
|
||||
workflowCode: 'CORR_WORKFLOW',
|
||||
currentState: 'PENDING_APPROVAL',
|
||||
entityType: 'Correspondence',
|
||||
entityId: 'corr-010-uuid',
|
||||
documentNumber: 'COR-101',
|
||||
subject: 'Approve Monthly Report',
|
||||
assignedAt: new Date().toISOString(),
|
||||
title: 'Approve Monthly Report',
|
||||
description: 'January 2025 Progress Report',
|
||||
daysOverdue: 0,
|
||||
|
||||
@@ -45,11 +45,11 @@ export const circulationService = {
|
||||
},
|
||||
|
||||
/**
|
||||
* ดึงรายการใบเวียนของ correspondence (by correspondence UUID)
|
||||
* ดึงรายการใบเวียนของ correspondence (by correspondence publicId)
|
||||
*/
|
||||
getByCorrespondenceUuid: async (correspondenceUuid: string) => {
|
||||
getByCorrespondenceUuid: async (correspondencePublicId: string) => {
|
||||
const response = await apiClient.get('/circulations', {
|
||||
params: { correspondenceUuid, limit: 50 },
|
||||
params: { correspondencePublicId, limit: 50 },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -27,14 +27,16 @@ interface RawPendingTask {
|
||||
}
|
||||
|
||||
export const dashboardService = {
|
||||
getStats: async (): Promise<DashboardStats> => {
|
||||
const response = await apiClient.get('/dashboard/stats');
|
||||
getStats: async (projectId?: string | null): Promise<DashboardStats> => {
|
||||
const params = projectId ? { projectId } : undefined;
|
||||
const response = await apiClient.get('/dashboard/stats', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getRecentActivity: async (): Promise<ActivityLog[]> => {
|
||||
getRecentActivity: async (projectId?: string | null): Promise<ActivityLog[]> => {
|
||||
try {
|
||||
const response = await apiClient.get('/dashboard/activity');
|
||||
const params = projectId ? { projectId } : undefined;
|
||||
const response = await apiClient.get('/dashboard/activity', { params });
|
||||
if (Array.isArray(response.data)) {
|
||||
return (response.data as RawActivityLog[]).map((log) => {
|
||||
const firstName = log.user?.firstName || '';
|
||||
@@ -59,9 +61,10 @@ export const dashboardService = {
|
||||
}
|
||||
},
|
||||
|
||||
getPendingTasks: async (): Promise<PendingTask[]> => {
|
||||
getPendingTasks: async (projectId?: string | null): Promise<PendingTask[]> => {
|
||||
try {
|
||||
const response = await apiClient.get('/dashboard/pending');
|
||||
const params = projectId ? { projectId } : undefined;
|
||||
const response = await apiClient.get('/dashboard/pending', { params });
|
||||
const rawTasks = (response.data?.data || (Array.isArray(response.data) ? response.data : [])) as RawPendingTask[];
|
||||
|
||||
return rawTasks.map((task) => {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// File: lib/stores/project-store.ts
|
||||
// Change Log:
|
||||
// - Created store for managing currently selected project context
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface ProjectState {
|
||||
selectedProjectId: string | null;
|
||||
setSelectedProjectId: (projectId: string | null) => void;
|
||||
}
|
||||
|
||||
export const useProjectStore = create<ProjectState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
selectedProjectId: null,
|
||||
setSelectedProjectId: (projectId) => set({ selectedProjectId: projectId }),
|
||||
}),
|
||||
{
|
||||
name: 'project-storage',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -7,6 +7,9 @@ export interface SearchCirculationDto {
|
||||
/** OPEN, COMPLETED, CANCELLED */
|
||||
status?: string;
|
||||
|
||||
/** กรองตาม correspondence publicId (ADR-019) */
|
||||
correspondencePublicId?: string;
|
||||
|
||||
page?: number;
|
||||
|
||||
limit?: number;
|
||||
|
||||
Reference in New Issue
Block a user