refactor: update UI styling and layout for dashboard task components
This commit is contained in:
parent
d8d7e542d0
commit
f4838422c2
@ -1,8 +1,15 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Plus, UserPlus, Shield, Settings, Building2, BadgeCheck } from 'lucide-react';
|
import {
|
||||||
import { useAppSelector } from '@/hooks/redux-hooks';
|
Plus,
|
||||||
import type { QuickAction } from '@/types/dashboard';
|
UserPlus,
|
||||||
import { useAppTheme } from '@/hooks/useAppTheme';
|
Shield,
|
||||||
|
Settings,
|
||||||
|
Building2,
|
||||||
|
BadgeCheck,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useAppSelector } from "@/hooks/redux-hooks";
|
||||||
|
import type { QuickAction } from "@/types/dashboard";
|
||||||
|
import { useAppTheme } from "@/hooks/useAppTheme";
|
||||||
|
|
||||||
export const QuickActions = () => {
|
export const QuickActions = () => {
|
||||||
const { primaryColor } = useAppTheme();
|
const { primaryColor } = useAppTheme();
|
||||||
@ -11,50 +18,102 @@ export const QuickActions = () => {
|
|||||||
|
|
||||||
// Helper to check permission
|
// Helper to check permission
|
||||||
const hasPermission = (resource: string, action: string) => {
|
const hasPermission = (resource: string, action: string) => {
|
||||||
if (roles.includes('super_admin') || roles.includes('tenant_admin')) return true;
|
if (roles.includes("super_admin") || roles.includes("tenant_admin"))
|
||||||
return permissions.some(p => p.resource === resource && p.action === action);
|
return true;
|
||||||
|
return permissions.some(
|
||||||
|
(p) => p.resource === resource && p.action === action,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSuperAdmin = roles.includes('super_admin');
|
const isSuperAdmin = roles.includes("super_admin");
|
||||||
|
|
||||||
// Define actions based on role
|
// Define actions based on role
|
||||||
const superAdminActions: QuickAction[] = [
|
const superAdminActions: QuickAction[] = [
|
||||||
{ icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants/create-wizard') },
|
{
|
||||||
{ icon: UserPlus, label: 'Module', onClick: () => navigate('/modules') },
|
icon: Plus,
|
||||||
{ icon: Shield, label: 'Notification', onClick: () => navigate('/notifications') },
|
label: "New Tenant",
|
||||||
{ icon: Settings, label: 'Audit Logs', onClick: () => navigate('/audit-logs') },
|
onClick: () => navigate("/tenants/create-wizard"),
|
||||||
|
},
|
||||||
|
{ icon: UserPlus, label: "Module", onClick: () => navigate("/modules") },
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
label: "Notification",
|
||||||
|
onClick: () => navigate("/notifications"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Settings,
|
||||||
|
label: "Audit Logs",
|
||||||
|
onClick: () => navigate("/audit-logs"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const tenantAdminActions: QuickAction[] = [
|
const tenantAdminActions: QuickAction[] = [
|
||||||
hasPermission('users', 'create') && { icon: UserPlus, label: 'New User', onClick: () => navigate('/tenant/users') },
|
hasPermission("users", "create") && {
|
||||||
hasPermission('roles', 'create') && { icon: Shield, label: 'New Role', onClick: () => navigate('/tenant/roles') },
|
icon: UserPlus,
|
||||||
hasPermission('departments', 'create') && { icon: Building2, label: 'New Dept', onClick: () => navigate('/tenant/departments') },
|
label: "New User",
|
||||||
hasPermission('designations', 'create') && { icon: BadgeCheck, label: 'New Desig', onClick: () => navigate('/tenant/designations') },
|
onClick: () => navigate("/tenant/users"),
|
||||||
|
},
|
||||||
|
hasPermission("roles", "create") && {
|
||||||
|
icon: Shield,
|
||||||
|
label: "New Role",
|
||||||
|
onClick: () => navigate("/tenant/roles"),
|
||||||
|
},
|
||||||
|
hasPermission("departments", "create") && {
|
||||||
|
icon: Building2,
|
||||||
|
label: "New Dept",
|
||||||
|
onClick: () => navigate("/tenant/departments"),
|
||||||
|
},
|
||||||
|
hasPermission("designations", "create") && {
|
||||||
|
icon: BadgeCheck,
|
||||||
|
label: "New Desig",
|
||||||
|
onClick: () => navigate("/tenant/designations"),
|
||||||
|
},
|
||||||
].filter(Boolean) as QuickAction[];
|
].filter(Boolean) as QuickAction[];
|
||||||
|
|
||||||
const actions = isSuperAdmin ? superAdminActions : tenantAdminActions;
|
const actions = isSuperAdmin ? superAdminActions : tenantAdminActions;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl p-6 h-full flex flex-col shadow-sm border border-[#e5e7eb]">
|
<div className="flex flex-col items-start self-stretch rounded-[8px] border border-[#D1D5DB] bg-white overflow-hidden">
|
||||||
<div className="flex justify-between items-center mb-6">
|
{/* Header */}
|
||||||
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">Quick Actions</h2>
|
<div className="flex w-full h-[48px] px-[20px] py-[16px] items-center border-b border-[#D1D5DB]">
|
||||||
<span className="w-1 h-1 bg-gray-200 rounded-full" />
|
<h2 className="text-[16px] font-semibold text-[#111827] leading-none">
|
||||||
|
Quick Actions
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{/* Actions Grid */}
|
||||||
|
<div className="inline-grid grid-cols-2 gap-[10px] p-[16px] w-full">
|
||||||
{actions.map((action, index) => {
|
{actions.map((action, index) => {
|
||||||
const Icon = action.icon;
|
const Icon = action.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
className="bg-white border-2 border-dashed border-[#f1f5f9] hover:border-[#cbd5e1] hover:bg-gray-50 flex flex-col items-center justify-center p-4 min-h-[100px] rounded-xl transition-all gap-3 group"
|
className="
|
||||||
|
flex flex-col justify-center items-center
|
||||||
|
w-full min-h-[100px]
|
||||||
|
p-[16px]
|
||||||
|
gap-[8px]
|
||||||
|
rounded-[6px]
|
||||||
|
border border-dashed border-[#D1D5DB]
|
||||||
|
bg-white
|
||||||
|
transition-all duration-200
|
||||||
|
hover:bg-[#F9FAFB]
|
||||||
|
hover:border-[#9CA3AF]
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 rounded-full bg-gray-50 border border-gray-100 flex items-center justify-center group-hover:bg-white transition-colors overflow-hidden">
|
{/* Icon */}
|
||||||
<Icon className="w-4 h-4" color={primaryColor} strokeWidth={2} />
|
<div className="flex items-center justify-center">
|
||||||
|
<Icon
|
||||||
|
className="w-[20px] h-[20px]"
|
||||||
|
color={primaryColor}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] font-bold text-[#111827] text-center leading-none">
|
|
||||||
|
{/* Label */}
|
||||||
|
<span className="text-[14px] font-medium text-[#111827] text-center leading-[20px]">
|
||||||
{action.label}
|
{action.label}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,49 +1,50 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Loader2, User, ArrowRight } from 'lucide-react';
|
import { Loader2, User, ArrowRight } from "lucide-react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import { auditLogService } from '@/services/audit-log-service';
|
import { auditLogService } from "@/services/audit-log-service";
|
||||||
import type { AuditLog } from '@/types/audit-log';
|
import type { AuditLog } from "@/types/audit-log";
|
||||||
import { useAppSelector } from '@/hooks/redux-hooks';
|
import { useAppSelector } from "@/hooks/redux-hooks";
|
||||||
import { useAppTheme } from '@/hooks/useAppTheme';
|
import { useAppTheme } from "@/hooks/useAppTheme";
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from "@/lib/utils";
|
||||||
import { Card, CardHeader, CardContent } from '@/components/ui/card';
|
import { StatusBadge, DataTable, type Column } from "@/components/shared";
|
||||||
import { StatusBadge } from '@/components/shared';
|
import { Button } from "@/components/ui/button";
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const formatRelativeTime = (dateString: string): string => {
|
const formatRelativeTime = (dateString: string): string => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||||
if (diffInSeconds < 60) return 'Just now';
|
if (diffInSeconds < 60) return "Just now";
|
||||||
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
||||||
if (diffInMinutes < 60) return `${diffInMinutes} min ago`;
|
if (diffInMinutes < 60) return `${diffInMinutes} min ago`;
|
||||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||||
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
||||||
const diffInDays = Math.floor(diffInHours / 24);
|
const diffInDays = Math.floor(diffInHours / 24);
|
||||||
if (diffInDays === 1) return 'Yesterday';
|
if (diffInDays === 1) return "Yesterday";
|
||||||
return date.toLocaleDateString();
|
return date.toLocaleDateString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusColor = (status: number | null): string => {
|
const getStatusColor = (status: number | null): string => {
|
||||||
if (!status) return 'text-[#6b7280]';
|
if (!status) return "text-[#6b7280]";
|
||||||
if (status >= 200 && status < 300) return 'text-[#10b981]';
|
if (status >= 200 && status < 300) return "text-[#10b981]";
|
||||||
if (status >= 400) return 'text-[#ef4444]';
|
if (status >= 400) return "text-[#ef4444]";
|
||||||
return 'text-[#f59e0b]';
|
return "text-[#f59e0b]";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMethodVariant = (method: string | null): 'success' | 'failure' | 'info' | 'process' => {
|
const getMethodVariant = (
|
||||||
if (!method) return 'info';
|
method: string | null,
|
||||||
|
): "success" | "failure" | "info" | "process" => {
|
||||||
|
if (!method) return "info";
|
||||||
const upperMethod = method.toUpperCase();
|
const upperMethod = method.toUpperCase();
|
||||||
if (upperMethod === 'GET') return 'success';
|
if (upperMethod === "GET") return "success";
|
||||||
if (upperMethod === 'POST') return 'info';
|
if (upperMethod === "POST") return "info";
|
||||||
if (upperMethod === 'PUT' || upperMethod === 'PATCH') return 'process';
|
if (upperMethod === "PUT" || upperMethod === "PATCH") return "process";
|
||||||
if (upperMethod === 'DELETE') return 'failure';
|
if (upperMethod === "DELETE") return "failure";
|
||||||
return 'info';
|
return "info";
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RecentActivityProps {
|
export interface RecentActivityProps {
|
||||||
variant?: 'list' | 'table';
|
variant?: "list" | "table";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
||||||
@ -53,10 +54,12 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
|||||||
const { tenantId, roles } = useAppSelector((state) => state.auth);
|
const { tenantId, roles } = useAppSelector((state) => state.auth);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const auditLogPath = roles?.includes('super_admin') ? '/audit-logs' : '/tenant/audit-logs';
|
const auditLogPath = roles?.includes("super_admin")
|
||||||
|
? "/audit-logs"
|
||||||
|
: "/tenant/audit-logs";
|
||||||
|
|
||||||
// Default to table variant for a more professional look
|
// Default to table variant for a more professional look
|
||||||
const activeVariant = variant || 'table';
|
const activeVariant = variant || "table";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRecentActivity = async (): Promise<void> => {
|
const fetchRecentActivity = async (): Promise<void> => {
|
||||||
@ -67,7 +70,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
|||||||
setAuditLogs(response.data);
|
setAuditLogs(response.data);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch recent activity:', err);
|
console.error("Failed to fetch recent activity:", err);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -76,15 +79,73 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
|||||||
fetchRecentActivity();
|
fetchRecentActivity();
|
||||||
}, [tenantId, activeVariant]);
|
}, [tenantId, activeVariant]);
|
||||||
|
|
||||||
if (activeVariant === 'list') {
|
const columns = useMemo<Column<AuditLog>[]>(() => [
|
||||||
|
{
|
||||||
|
key: "created_at",
|
||||||
|
label: "Timestamp",
|
||||||
|
render: (log) => (
|
||||||
|
<span className="text-[12px] font-medium text-gray-500">
|
||||||
|
{formatRelativeTime(log.created_at)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "resource_type",
|
||||||
|
label: "Resource Type",
|
||||||
|
render: (log) => (
|
||||||
|
<span
|
||||||
|
className="text-[12px] font-bold truncate max-w-[150px] inline-block"
|
||||||
|
style={{ color: primaryColor }}
|
||||||
|
>
|
||||||
|
{log.resource_type}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "request_method",
|
||||||
|
label: "Method",
|
||||||
|
render: (log) => (
|
||||||
|
<StatusBadge variant={getMethodVariant(log.request_method)}>
|
||||||
|
{log.request_method || "N/A"}
|
||||||
|
</StatusBadge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "response_status",
|
||||||
|
label: "Status",
|
||||||
|
render: (log) => (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[12px] font-bold",
|
||||||
|
getStatusColor(log.response_status),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{log.response_status || "---"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ip_address",
|
||||||
|
label: "IP Address",
|
||||||
|
render: (log) => (
|
||||||
|
<span className="text-[12px] font-mono text-gray-500">
|
||||||
|
{log.ip_address || "---"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], [primaryColor]);
|
||||||
|
|
||||||
|
if (activeVariant === "list") {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl flex flex-col h-full border border-[#e5e7eb] shadow-sm overflow-hidden">
|
<div className="flex flex-col items-start self-stretch rounded-[8px] border border-[#D1D5DB] bg-white overflow-hidden">
|
||||||
<div className="px-6 py-5 border-b border-[#f1f5f9] flex justify-between items-center">
|
<div className="flex w-full h-[48px] px-[20px] py-[16px] items-center border-b border-[#D1D5DB]">
|
||||||
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">Recent Activity</h2>
|
<h2 className="text-[15px] font-bold text-[#111827] tracking-tight">
|
||||||
|
Recent Activity
|
||||||
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="text-[11px] font-bold gap-1 h-7"
|
className="text-[15px] font-bold gap-1 h-7"
|
||||||
style={{ color: primaryColor }}
|
style={{ color: primaryColor }}
|
||||||
onClick={() => navigate(auditLogPath)}
|
onClick={() => navigate(auditLogPath)}
|
||||||
>
|
>
|
||||||
@ -92,24 +153,40 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1 w-full">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-20">
|
<div className="flex items-center justify-center py-20">
|
||||||
<Loader2 className="w-6 h-6 animate-spin" style={{ color: primaryColor }} />
|
<Loader2
|
||||||
|
className="w-6 h-6 animate-spin"
|
||||||
|
style={{ color: primaryColor }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : auditLogs.length === 0 ? (
|
) : auditLogs.length === 0 ? (
|
||||||
<div className="p-12 text-center text-[12px] text-gray-400 font-medium">No recent activity</div>
|
<div className="p-12 text-center text-[12px] text-gray-400 font-medium">
|
||||||
|
No recent activity
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{auditLogs.map((log, index) => (
|
{auditLogs.map((log, index) => (
|
||||||
<div key={log.id} className={cn("px-6 py-4 flex items-center gap-4 transition-colors hover:bg-gray-50", index !== auditLogs.length - 1 && "border-b border-[#f1f5f9 ]")}>
|
<div
|
||||||
<span className="text-[11px] font-medium text-gray-400 w-20 shrink-0">{formatRelativeTime(log.created_at)}</span>
|
key={log.id}
|
||||||
|
className={cn(
|
||||||
|
"px-6 py-4 flex items-center gap-4 transition-colors hover:bg-gray-50",
|
||||||
|
index !== auditLogs.length - 1 &&
|
||||||
|
"border-b border-[#f1f5f9 ]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 w-20 shrink-0">
|
||||||
|
{formatRelativeTime(log.created_at)}
|
||||||
|
</span>
|
||||||
<div className="w-8 h-8 rounded-full bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0 overflow-hidden">
|
<div className="w-8 h-8 rounded-full bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0 overflow-hidden">
|
||||||
<User className="w-4 h-4 text-gray-400" />
|
<User className="w-4 h-4 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
<span className="text-[12px] font-bold">{log.action}</span>
|
<span className="text-[12px] font-bold">
|
||||||
|
{log.action}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
className="text-[12px] font-bold hover:underline cursor-pointer truncate"
|
className="text-[12px] font-bold hover:underline cursor-pointer truncate"
|
||||||
style={{ color: primaryColor }}
|
style={{ color: primaryColor }}
|
||||||
@ -129,9 +206,11 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
|||||||
|
|
||||||
// TABLE VARIANT
|
// TABLE VARIANT
|
||||||
return (
|
return (
|
||||||
<Card className="flex-1 border-[rgba(0,0,0,0.12)] w-full">
|
<div className="flex flex-col items-start self-stretch rounded-[8px] border border-[#D1D5DB] bg-white overflow-hidden w-full">
|
||||||
<CardHeader className="border-b border-[rgba(0,0,0,0.08)] pb-3 md:pb-4 pt-3 md:pt-4 px-4 md:px-5 flex flex-row items-center justify-between gap-2">
|
<div className="flex w-full h-[48px] px-[20px] py-[16px] items-center border-b border-[#D1D5DB] justify-between">
|
||||||
<h2 className="text-sm md:text-[15px] font-semibold text-[#0f1724]">Recent Activity</h2>
|
<h2 className="text-[16px] font-semibold text-[#111827] leading-none">
|
||||||
|
Recent Activity
|
||||||
|
</h2>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -140,63 +219,17 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
|||||||
>
|
>
|
||||||
View All <ArrowRight className="w-3 h-3" />
|
View All <ArrowRight className="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
<Loader2 className="w-5 h-5 text-[#112868] animate-spin" />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto">
|
<div className="w-full">
|
||||||
<table className="w-full border-collapse">
|
<DataTable
|
||||||
<thead>
|
data={auditLogs}
|
||||||
<tr className="bg-gray-50/50 border-b border-[rgba(0,0,0,0.08)]">
|
columns={columns}
|
||||||
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">Timestamp</th>
|
keyExtractor={(log) => log.id}
|
||||||
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">Resource Type</th>
|
isLoading={isLoading}
|
||||||
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">Method</th>
|
emptyMessage="No recent activity recorded"
|
||||||
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">Status</th>
|
/>
|
||||||
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">IP Address</th>
|
</div>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-[rgba(0,0,0,0.04)]">
|
|
||||||
{auditLogs.map((log) => (
|
|
||||||
<tr key={log.id} className="hover:bg-gray-50/30 transition-colors">
|
|
||||||
<td className="px-5 py-4 whitespace-nowrap">
|
|
||||||
<span className="text-[12px] font-medium text-gray-500">{formatRelativeTime(log.created_at)}</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-5 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
className="text-[12px] font-bold truncate max-w-[150px] inline-block"
|
|
||||||
style={{ color: primaryColor }}
|
|
||||||
>
|
|
||||||
{log.resource_type}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-5 py-4 whitespace-nowrap">
|
|
||||||
<StatusBadge variant={getMethodVariant(log.request_method)}>
|
|
||||||
{log.request_method || 'N/A'}
|
|
||||||
</StatusBadge>
|
|
||||||
</td>
|
|
||||||
<td className="px-5 py-4 whitespace-nowrap">
|
|
||||||
<span className={cn("text-[12px] font-bold", getStatusColor(log.response_status))}>
|
|
||||||
{log.response_status || '---'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-5 py-4 whitespace-nowrap">
|
|
||||||
<span className="text-[12px] font-mono text-gray-500">{log.ip_address || '---'}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{auditLogs.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={5} className="px-5 py-10 text-center text-xs text-gray-400">No recent activity recorded</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,60 +1,60 @@
|
|||||||
import { Activity } from 'lucide-react';
|
// import { Activity } from 'lucide-react';
|
||||||
import { Card, CardHeader, CardContent } from '@/components/ui/card';
|
// import { Card, CardHeader, CardContent } from '@/components/ui/card';
|
||||||
import { cn } from '@/lib/utils';
|
// import { cn } from '@/lib/utils';
|
||||||
import type { HealthMetric } from '@/types/dashboard';
|
// import type { HealthMetric } from '@/types/dashboard';
|
||||||
|
|
||||||
const healthMetrics: HealthMetric[] = [
|
// const healthMetrics: HealthMetric[] = [
|
||||||
{ label: 'API Latency', value: '45ms', percentage: 100, variant: 'success' },
|
// { label: 'API Latency', value: '45ms', percentage: 100, variant: 'success' },
|
||||||
{ label: 'Database Load', value: '42%', percentage: 42, variant: 'info' },
|
// { label: 'Database Load', value: '42%', percentage: 42, variant: 'info' },
|
||||||
{ label: 'Storage Usage', value: '78%', percentage: 78, variant: 'warning' },
|
// { label: 'Storage Usage', value: '78%', percentage: 78, variant: 'warning' },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
const getVariantStyles = (variant: HealthMetric['variant']) => {
|
// const getVariantStyles = (variant: HealthMetric['variant']) => {
|
||||||
switch (variant) {
|
// switch (variant) {
|
||||||
case 'success':
|
// case 'success':
|
||||||
return { text: 'text-[#059669]', bg: 'bg-[#059669]' };
|
// return { text: 'text-[#059669]', bg: 'bg-[#059669]' };
|
||||||
case 'info':
|
// case 'info':
|
||||||
return { text: 'text-[#23dce1]', bg: 'bg-[#23dce1]' };
|
// return { text: 'text-[#23dce1]', bg: 'bg-[#23dce1]' };
|
||||||
case 'warning':
|
// case 'warning':
|
||||||
return { text: 'text-[#f59e0b]', bg: 'bg-[#f59e0b]' };
|
// return { text: 'text-[#f59e0b]', bg: 'bg-[#f59e0b]' };
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const SystemHealth = () => {
|
// export const SystemHealth = () => {
|
||||||
return (
|
// return (
|
||||||
<Card className="w-[300px] shrink-0">
|
// <Card className="w-[300px] shrink-0">
|
||||||
<CardHeader className="border-b border-[rgba(0,0,0,0.08)] pb-4 pt-4 px-5 h-12 flex items-center justify-between">
|
// <CardHeader className="border-b border-[rgba(0,0,0,0.08)] pb-4 pt-4 px-5 h-12 flex items-center justify-between">
|
||||||
<h2 className="text-[15px] font-semibold text-[#0f1724] h-[19px]">System Health</h2>
|
// <h2 className="text-[15px] font-semibold text-[#0f1724] h-[19px]">System Health</h2>
|
||||||
<Activity className="w-4 h-4" />
|
// <Activity className="w-4 h-4" />
|
||||||
</CardHeader>
|
// </CardHeader>
|
||||||
<CardContent className="p-4">
|
// <CardContent className="p-4">
|
||||||
<div className="flex flex-col gap-4">
|
// <div className="flex flex-col gap-4">
|
||||||
{healthMetrics.map((metric, index) => {
|
// {healthMetrics.map((metric, index) => {
|
||||||
const styles = getVariantStyles(metric.variant);
|
// const styles = getVariantStyles(metric.variant);
|
||||||
return (
|
// return (
|
||||||
<div key={index} className="flex flex-col gap-2">
|
// <div key={index} className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
// <div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-medium text-[#0f1724] h-4 leading-4">
|
// <span className="text-xs font-medium text-[#0f1724] h-4 leading-4">
|
||||||
{metric.label}
|
// {metric.label}
|
||||||
</span>
|
// </span>
|
||||||
<span className={cn('text-xs font-medium h-4 leading-4', styles.text)}>
|
// <span className={cn('text-xs font-medium h-4 leading-4', styles.text)}>
|
||||||
{metric.value}
|
// {metric.value}
|
||||||
</span>
|
// </span>
|
||||||
</div>
|
// </div>
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.2)] rounded-full h-1.5 overflow-hidden relative">
|
// <div className="bg-white border border-[rgba(0,0,0,0.2)] rounded-full h-1.5 overflow-hidden relative">
|
||||||
<div
|
// <div
|
||||||
className={cn(
|
// className={cn(
|
||||||
'absolute top-[-1px] bottom-[-1px] left-[-1px] rounded-full',
|
// 'absolute top-[-1px] bottom-[-1px] left-[-1px] rounded-full',
|
||||||
styles.bg
|
// styles.bg
|
||||||
)}
|
// )}
|
||||||
style={{ width: `${metric.percentage}%` }}
|
// style={{ width: `${metric.percentage}%` }}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
})}
|
// })}
|
||||||
</div>
|
// </div>
|
||||||
</CardContent>
|
// </CardContent>
|
||||||
</Card>
|
// </Card>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
import { Layout } from "@/components/layout/Layout";
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import {
|
import { FileCheck, Briefcase, FileText, Users, Bell } from "lucide-react";
|
||||||
FileCheck,
|
|
||||||
Briefcase,
|
|
||||||
FileText,
|
|
||||||
Users,
|
|
||||||
Bell,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { QuickActions } from "@/features/dashboard/components/QuickActions";
|
import { QuickActions } from "@/features/dashboard/components/QuickActions";
|
||||||
import { RecentActivity } from "@/features/dashboard/components/RecentActivity";
|
import { RecentActivity } from "@/features/dashboard/components/RecentActivity";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useAppTheme } from "@/hooks/useAppTheme";
|
import { useAppTheme } from "@/hooks/useAppTheme";
|
||||||
import { workflowService } from "@/services/workflow-service";
|
import { workflowService } from "@/services/workflow-service";
|
||||||
import { dashboardService, type TenantDashboardStats } from "@/services/dashboard-service";
|
import {
|
||||||
|
dashboardService,
|
||||||
|
type TenantDashboardStats,
|
||||||
|
} from "@/services/dashboard-service";
|
||||||
import type { WorkflowTask } from "@/types/workflow";
|
import type { WorkflowTask } from "@/types/workflow";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { GradientStatCard } from "@/components/shared";
|
import { GradientStatCard } from "@/components/shared";
|
||||||
@ -25,12 +22,11 @@ interface StatCardProps {
|
|||||||
label: string;
|
label: string;
|
||||||
badge?: {
|
badge?: {
|
||||||
text: string;
|
text: string;
|
||||||
variant: 'success' | 'warning' | 'info' | 'error';
|
variant: "success" | "warning" | "info" | "error";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskCard = ({ task }: { task: WorkflowTask }) => {
|
const TaskCard = ({ task }: { task: WorkflowTask }) => {
|
||||||
// const { primaryColor } = useAppTheme();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formatDeadline = (dueDate: string) => {
|
const formatDeadline = (dueDate: string) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -38,135 +34,74 @@ const TaskCard = ({ task }: { task: WorkflowTask }) => {
|
|||||||
const diffTime = due.getTime() - now.getTime();
|
const diffTime = due.getTime() - now.getTime();
|
||||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
if (diffDays === 0) return 'Due today';
|
if (diffDays === 0) return "Due today";
|
||||||
if (diffDays === 1) return 'Tomorrow';
|
if (diffDays === 1) return "Tomorrow";
|
||||||
if (diffDays < 0) return 'Overdue';
|
if (diffDays < 0) return "Overdue";
|
||||||
return `In ${diffDays} Days`;
|
return `In ${diffDays} Days`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleView = () => {
|
const handleView = () => {
|
||||||
if (task.entity.type.toLowerCase() === 'document') {
|
if (task.entity.type.toLowerCase() === "document") {
|
||||||
navigate(`/tenant/documents/${task.entity.id}`);
|
navigate(`/tenant/documents/${task.entity.id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-[#e5e7eb] rounded-xl p-4 flex flex-col gap-3 bg-white hover:border-[#cbd5e1] transition-colors">
|
<div className="flex flex-col items-start self-stretch p-3 gap-2.5 rounded-[6px] border border-[#D1D5DB] bg-white hover:border-[#9CA3AF] transition-colors group">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-start self-stretch">
|
||||||
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest leading-none">{task.entity.type}</span>
|
<span className="text-[10px] font-bold text-[#94A3B8] uppercase tracking-[0.05em] leading-none">
|
||||||
<span className={cn(
|
{task.entity.type}
|
||||||
"text-[10px] font-bold",
|
</span>
|
||||||
task.is_overdue ? "text-[#ef4444]" : "text-gray-400"
|
<span
|
||||||
)}>{formatDeadline(task.due_at)}</span>
|
className={cn(
|
||||||
</div>
|
"text-[10px] font-bold leading-none",
|
||||||
|
task.is_overdue ? "text-[#F87171]" : "text-[#94A3B8]",
|
||||||
<div className="text-[13px] font-semibold text-[#111827] leading-tight">{task.entity.name}</div>
|
)}
|
||||||
|
>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 overflow-hidden mt-1">
|
{formatDeadline(task.due_at)}
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
</span>
|
||||||
<div className={cn(
|
</div>
|
||||||
"w-2 h-2 rounded-full",
|
|
||||||
task.is_overdue ? "bg-[#ef4444]" : "bg-[#10b981]"
|
<div className="text-[14px] font-semibold text-[#1E293B] leading-[20px] self-stretch line-clamp-2">
|
||||||
)} />
|
{task.entity.name}
|
||||||
<span className="text-[11px] text-[#6b7280] font-medium leading-none">
|
</div>
|
||||||
{task.step.name} • {
|
|
||||||
(() => {
|
<div className="flex justify-between items-center self-stretch mt-0.5">
|
||||||
const role = task.assignment?.assigned_role;
|
<div className="flex items-center gap-1.5 overflow-hidden mr-2">
|
||||||
if (role) return Array.isArray(role) ? role.join(", ") : role;
|
<div
|
||||||
return task.assignment?.assigned_to_name || 'Unassigned';
|
className={cn(
|
||||||
})()
|
"w-2 h-2 rounded-full shrink-0",
|
||||||
}
|
task.is_overdue ? "bg-[#F87171]" : "bg-[#34D399]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-[12px] text-[#64748B] font-medium leading-[16px] truncate">
|
||||||
|
{task.step.name} •{" "}
|
||||||
|
{(() => {
|
||||||
|
const role = task.assignment?.assigned_role;
|
||||||
|
if (role) return Array.isArray(role) ? role.join(", ") : role;
|
||||||
|
return task.assignment?.assigned_to_name || "Unassigned";
|
||||||
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={handleView}
|
onClick={handleView}
|
||||||
className="text-[11px] px-2.5 py-1.5 border border-[#e5e7eb] rounded-md font-bold text-[#374151] hover:bg-gray-50 transition-colors shrink-0"
|
className="flex px-3 py-1.5 justify-center items-center rounded-[4px] border border-[#D1D5DB] bg-white text-[12px] font-bold text-[#1E293B] hover:bg-[#F9FAFB] hover:border-[#9CA3AF] transition-all shrink-0"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</button>
|
</button>
|
||||||
{/* <button
|
|
||||||
className="text-[11px] px-2.5 py-1.5 text-white rounded-md font-bold transition-colors shadow-sm"
|
|
||||||
style={{ backgroundColor: primaryColor, boxShadow: `0 2px 4px ${primaryColor}33` }}
|
|
||||||
>
|
|
||||||
Complete
|
|
||||||
</button> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
const CAPASummaryChart = () => {
|
|
||||||
const data = [
|
|
||||||
{ name: 'Jan', open: 35, inProgress: 48, closed: 25, trend: 15 },
|
|
||||||
{ name: 'Feb', open: 28, inProgress: 35, closed: 20, trend: 12 },
|
|
||||||
{ name: 'Mar', open: 45, inProgress: 75, closed: 38, trend: 32 },
|
|
||||||
{ name: 'Apr', open: 40, inProgress: 65, closed: 42, trend: 28 },
|
|
||||||
{ name: 'May', open: 55, inProgress: 95, closed: 78, trend: 52 },
|
|
||||||
{ name: 'Jun', open: 42, inProgress: 82, closed: 72, trend: 45 },
|
|
||||||
{ name: 'Jul', open: 38, inProgress: 70, closed: 65, trend: 38 },
|
|
||||||
{ name: 'Aug', open: 48, inProgress: 94, closed: 82, trend: 48 },
|
|
||||||
{ name: 'Sep', open: 32, inProgress: 65, closed: 58, trend: 35 },
|
|
||||||
{ name: 'Oct', open: 44, inProgress: 88, closed: 85, trend: 58 },
|
|
||||||
{ name: 'Nov', open: 52, inProgress: 92, closed: 98, trend: 62 },
|
|
||||||
{ name: 'Dec', open: 60, inProgress: 105, closed: 115, trend: 58 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-[320px] w-full">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<ComposedChart data={data}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
|
||||||
<XAxis
|
|
||||||
dataKey="name"
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 11, fill: '#64748b', fontWeight: 500 }}
|
|
||||||
dy={10}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
tick={{ fontSize: 11, fill: '#64748b', fontWeight: 500 }}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)', fontSize: '12px' }}
|
|
||||||
/>
|
|
||||||
<Legend
|
|
||||||
verticalAlign="bottom"
|
|
||||||
align="left"
|
|
||||||
wrapperStyle={{ paddingTop: '24px', fontSize: '11px', fontWeight: 600, color: '#64748b' }}
|
|
||||||
iconType="circle"
|
|
||||||
iconSize={8}
|
|
||||||
/>
|
|
||||||
<Bar dataKey="open" name="Open CAPA" fill="#94a3b8" radius={[2, 2, 0, 0]} barSize={8} />
|
|
||||||
<Bar dataKey="inProgress" name="In Progress" fill="#6366f1" radius={[2, 2, 0, 0]} barSize={8} />
|
|
||||||
<Bar dataKey="closed" name="Closed" fill="#1e1b4b" radius={[2, 2, 0, 0]} barSize={8} />
|
|
||||||
<Line
|
|
||||||
type="monotone"
|
|
||||||
dataKey="trend"
|
|
||||||
name="Total Trend"
|
|
||||||
stroke="#06b6d4"
|
|
||||||
strokeWidth={2}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
</ComposedChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Dashboard = (): ReactElement => {
|
const Dashboard = (): ReactElement => {
|
||||||
const { primaryColor } = useAppTheme();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { primaryColor } = useAppTheme();
|
||||||
const [tasks, setTasks] = useState<WorkflowTask[]>([]);
|
const [tasks, setTasks] = useState<WorkflowTask[]>([]);
|
||||||
|
const [tasksLoading, setTasksLoading] = useState(true);
|
||||||
const [stats, setStats] = useState<TenantDashboardStats | null>(null);
|
const [stats, setStats] = useState<TenantDashboardStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [tasksLoading, setTasksLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
@ -176,15 +111,15 @@ const Dashboard = (): ReactElement => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setTasksLoading(true);
|
setTasksLoading(true);
|
||||||
try {
|
try {
|
||||||
// Fetch tasks independently to avoid one failing the other
|
// Fetch tasks independently
|
||||||
workflowService.listTasks({ limit: 3 })
|
workflowService
|
||||||
.then(response => {
|
.listTasks({ limit: 3 })
|
||||||
console.log("[Dashboard] Tasks response:", response);
|
.then((response) => {
|
||||||
if (response.success && Array.isArray(response.data)) {
|
if (response.success && Array.isArray(response.data)) {
|
||||||
setTasks(response.data);
|
setTasks(response.data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error("Error fetching tasks:", error);
|
console.error("Error fetching tasks:", error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -192,13 +127,14 @@ const Dashboard = (): ReactElement => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch statistics independently
|
// Fetch statistics independently
|
||||||
dashboardService.getTenantStatistics()
|
dashboardService
|
||||||
.then(response => {
|
.getTenantStatistics()
|
||||||
|
.then((response) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setStats(response.data);
|
setStats(response.data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error("Error fetching dashboard statistics:", error);
|
console.error("Error fetching dashboard statistics:", error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -211,51 +147,42 @@ const Dashboard = (): ReactElement => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const statCards: StatCardProps[] = [
|
const statCards: StatCardProps[] = [
|
||||||
stats?.documentsCount !== undefined && {
|
stats?.documentsCount !== undefined && {
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
value: stats.documentsCount,
|
value: stats.documentsCount,
|
||||||
label: "Total Documents",
|
label: "Total Documents",
|
||||||
badge: { text: "Controlled", variant: "info" }
|
badge: { text: "Controlled", variant: "info" },
|
||||||
},
|
},
|
||||||
stats?.pendingTasks !== undefined && {
|
stats?.pendingTasks !== undefined && {
|
||||||
icon: FileCheck,
|
icon: FileCheck,
|
||||||
value: stats.pendingTasks,
|
value: stats.pendingTasks,
|
||||||
label: "My Tasks",
|
label: "My Tasks",
|
||||||
badge: { text: "Action Needed", variant: "warning" }
|
badge: { text: "Action Needed", variant: "warning" },
|
||||||
},
|
},
|
||||||
stats?.usersCount !== undefined && {
|
stats?.usersCount !== undefined && {
|
||||||
icon: Users,
|
icon: Users,
|
||||||
value: stats.usersCount,
|
value: stats.usersCount,
|
||||||
label: "Total Users",
|
label: "Total Users",
|
||||||
badge: { text: "Team Members", variant: "success" }
|
badge: { text: "Team Members", variant: "success" },
|
||||||
},
|
},
|
||||||
stats?.unreadNotificationsCount !== undefined && {
|
stats?.unreadNotificationsCount !== undefined && {
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
value: stats.unreadNotificationsCount,
|
value: stats.unreadNotificationsCount,
|
||||||
label: "Notifications",
|
label: "Notifications",
|
||||||
badge: { text: "Unread", variant: "error" }
|
badge: { text: "Unread", variant: "error" },
|
||||||
},
|
},
|
||||||
stats?.activeModulesCount !== undefined && {
|
stats?.activeModulesCount !== undefined && {
|
||||||
icon: Briefcase,
|
icon: Briefcase,
|
||||||
value: stats.activeModulesCount,
|
value: stats.activeModulesCount,
|
||||||
label: "Running Modules",
|
label: "Running Modules",
|
||||||
badge: { text: "Operational", variant: "success" }
|
badge: { text: "Operational", variant: "success" },
|
||||||
},
|
},
|
||||||
// Training Compliance is still static/placeholder for now
|
|
||||||
// {
|
|
||||||
// icon: GraduationCap,
|
|
||||||
// value: "94%",
|
|
||||||
// label: "Compliance",
|
|
||||||
// badge: { text: "Target Met", variant: "success" }
|
|
||||||
// },
|
|
||||||
].filter(Boolean) as StatCardProps[];
|
].filter(Boolean) as StatCardProps[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Dashboard"
|
currentPage="Dashboard"
|
||||||
// breadcrumbs={[{ label: "QAssure - Tenant" }, { label: "Dashboard" }]}
|
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: "Tenant Overview",
|
title: "Tenant Overview",
|
||||||
description: "Key quality metrics and performance indicators.",
|
description: "Key quality metrics and performance indicators.",
|
||||||
@ -263,51 +190,38 @@ const Dashboard = (): ReactElement => {
|
|||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6 pb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6 pb-8">
|
||||||
{/* Main Content Area (Left) */}
|
{/* Main Content Area (Left) */}
|
||||||
<div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-6">
|
<div className="lg:col-span-8 flex flex-col gap-6">
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="col-span-full text-center py-8 text-gray-400 text-sm">Loading statistics...</div>
|
<div className="col-span-full text-center py-8 text-gray-400 text-sm">
|
||||||
|
Loading statistics...
|
||||||
|
</div>
|
||||||
) : statCards.length > 0 ? (
|
) : statCards.length > 0 ? (
|
||||||
statCards.map((card, index) => (
|
statCards.map((card, index) => (
|
||||||
<GradientStatCard key={index} {...card} />
|
<GradientStatCard key={index} {...card} />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="col-span-full text-center py-8 text-gray-400 text-sm">No statistics available</div>
|
<div className="col-span-full text-center py-8 text-gray-400 text-sm">
|
||||||
|
No statistics available
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CAPA Summary Card (Commented out for now) */}
|
|
||||||
{/* <div className="bg-white border border-[#e5e7eb] rounded-xl p-6 shadow-sm">
|
|
||||||
<div className="flex justify-between items-center mb-8">
|
|
||||||
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">
|
|
||||||
CAPA Summary
|
|
||||||
</h2>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="text-[11px] font-bold text-gray-400 tracking-wide">Data Range</span>
|
|
||||||
<select className="text-[11px] font-bold border border-[#e2e8f0] rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-[#084cc8]/10 bg-white">
|
|
||||||
<option>Last Year</option>
|
|
||||||
<option>Last 6 Months</option>
|
|
||||||
<option>Last Month</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CAPASummaryChart />
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* Recent Activity Card */}
|
{/* Recent Activity Card */}
|
||||||
<RecentActivity />
|
<RecentActivity />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar area (Right) */}
|
{/* Sidebar area (Right) */}
|
||||||
<div className="lg:col-span-4 xl:col-span-3 flex flex-col gap-6">
|
<div className="lg:col-span-4 flex flex-col gap-6">
|
||||||
{/* My Tasks Card */}
|
{/* My Tasks Card */}
|
||||||
<div className="bg-white border border-[#e5e7eb] rounded-xl p-6 shadow-sm">
|
<div className="flex flex-col items-start self-stretch rounded-[8px] border border-[#D1D5DB] bg-white overflow-hidden">
|
||||||
<div className="flex justify-between items-center mb-5">
|
<div className="flex w-full h-[48px] px-[20px] py-[16px] items-center border-b border-[#D1D5DB] justify-between">
|
||||||
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">My Tasks</h2>
|
<h2 className="text-[16px] font-semibold text-[#111827] leading-none">
|
||||||
|
My Tasks
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/tenant/workflows/tasks')}
|
onClick={() => navigate("/tenant/workflows/tasks")}
|
||||||
className="text-[11px] font-bold hover:underline"
|
className="text-[11px] font-bold hover:underline"
|
||||||
style={{ color: primaryColor }}
|
style={{ color: primaryColor }}
|
||||||
>
|
>
|
||||||
@ -315,20 +229,21 @@ const Dashboard = (): ReactElement => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3 p-4 w-full">
|
||||||
{tasksLoading ? (
|
{tasksLoading ? (
|
||||||
<div className="text-center py-4 text-gray-400 text-sm">Loading tasks...</div>
|
<div className="text-center py-4 text-gray-400 text-sm">
|
||||||
|
Loading tasks...
|
||||||
|
</div>
|
||||||
) : tasks.length > 0 ? (
|
) : tasks.length > 0 ? (
|
||||||
tasks.map((task) => (
|
tasks.map((task) => <TaskCard key={task.id} task={task} />)
|
||||||
<TaskCard key={task.id} task={task} />
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-4 text-gray-400 text-sm">No pending tasks</div>
|
<div className="text-center py-4 text-gray-400 text-sm">
|
||||||
|
No pending tasks
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions Card */}
|
|
||||||
<QuickActions />
|
<QuickActions />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user