refactor: modernize UI components by stripping default container styles and standardizing layout elements across application pages

This commit is contained in:
Yashwin 2026-05-19 18:28:01 +05:30
parent 12954e5ba1
commit fd6436e389
76 changed files with 2946 additions and 2536 deletions

View File

@ -9,7 +9,7 @@ export default function CodeBadge({ label, className }: CodeBadgeProps) {
return ( return (
<span <span
className={cn( className={cn(
"inline-flex items-center justify-center rounded-full bg-[#EDF3FE] px-3 py-1 text-sm font-medium text-[#3B82F6]", "inline-flex items-center justify-center rounded-full bg-[#EDF3FE] px-3 py-1 text-[12px] font-medium text-[#4C89FA]",
className className
)} )}
> >

View File

@ -8,6 +8,8 @@ export interface Column<T> {
render?: (item: T) => ReactNode; render?: (item: T) => ReactNode;
align?: "left" | "right" | "center"; align?: "left" | "right" | "center";
mobileLabel?: string; mobileLabel?: string;
width?: string;
className?: string;
} }
interface DataTableProps<T> { interface DataTableProps<T> {
@ -76,9 +78,10 @@ export const DataTable = <T,>({
{/* Desktop Table Empty State */} {/* Desktop Table Empty State */}
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0"> <div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
<div className="inline-block min-w-full align-middle"> <div className="inline-block min-w-full align-middle">
<table className="w-full"> <table className="w-full table-auto">
<thead> <thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]"> {/* <tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]"> */}
<tr className="bg-[#F9F9F9] border-b border-[#D1D5DB]">
{canExpand && showExpandColumn && ( {canExpand && showExpandColumn && (
<th <th
className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left" className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left"
@ -93,9 +96,14 @@ export const DataTable = <T,>({
? "text-center" ? "text-center"
: "text-left"; : "text-left";
return ( return (
// <th
// key={column.key}
// className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
// >
<th <th
key={column.key} key={column.key}
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`} className={`h-[41px] px-2 py-3 ${alignClass} bg-[#F9F9F9] text-[13px] font-medium text-[#6B7280] align-bottom whitespace-nowrap ${column.className || ""}`}
style={column.width ? { width: column.width } : undefined}
> >
{column.label} {column.label}
</th> </th>
@ -129,12 +137,14 @@ export const DataTable = <T,>({
{/* Desktop Table */} {/* Desktop Table */}
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0"> <div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
<div className="inline-block min-w-full align-middle"> <div className="inline-block min-w-full align-middle">
<table className="w-full"> <table className="w-full table-auto">
<thead> <thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]"> {/* <tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]"> */}
<tr className="bg-[#F9F9F9] border-b border-[#D1D5DB]">
{canExpand && showExpandColumn && ( {canExpand && showExpandColumn && (
<th <th
className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left" // className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left"
className="w-10 px-2 py-3 text-left"
aria-label="Expand" aria-label="Expand"
/> />
)} )}
@ -146,9 +156,14 @@ export const DataTable = <T,>({
? "text-center" ? "text-center"
: "text-left"; : "text-left";
return ( return (
// <th
// key={column.key}
// className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
// >
<th <th
key={column.key} key={column.key}
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`} className={`h-[41px] px-2 py-3 ${alignClass} bg-[#F9F9F9] text-[13px] font-medium text-[#6B7280] align-bottom whitespace-nowrap ${column.className || ""}`}
style={column.width ? { width: column.width } : undefined}
> >
{column.label} {column.label}
</th> </th>
@ -163,11 +178,14 @@ export const DataTable = <T,>({
return ( return (
<Fragment key={rowId}> <Fragment key={rowId}>
<tr <tr
className="border-b border-[rgba(0,0,0,0.08)] hover:bg-gray-50 transition-colors" className="border-b border-[#D1D5DB] hover:bg-gray-50 transition-colors"
onClick={onRowClick ? () => onRowClick(item) : undefined} onClick={onRowClick ? () => onRowClick(item) : undefined}
> >
{canExpand && showExpandColumn && ( {canExpand && showExpandColumn && (
<td className="px-2 py-2.5 md:py-1.5 lg:py-3 xl:py-4 align-middle"> <td
// className="px-2 py-2.5 md:py-1.5 lg:py-3 xl:py-4 align-middle"
className="w-10 px-2 py-3 align-middle"
>
<button <button
type="button" type="button"
className="p-1 rounded hover:bg-gray-200/80 text-[#64748b]" className="p-1 rounded hover:bg-gray-200/80 text-[#64748b]"
@ -191,9 +209,14 @@ export const DataTable = <T,>({
? "text-center" ? "text-center"
: "text-left"; : "text-left";
return ( return (
// <td
// key={column.key}
// className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-[13px]`}
// >
<td <td
key={column.key} key={column.key}
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-[13px]`} className={`h-[56px] px-2 py-3 ${alignClass} text-[13px] font-normal text-[#0F1724] align-middle ${column.className || ""}`}
style={column.width ? { width: column.width } : undefined}
> >
{column.render {column.render
? column.render(item) ? column.render(item)
@ -203,7 +226,7 @@ export const DataTable = <T,>({
})} })}
</tr> </tr>
{canExpand && expanded && ( {canExpand && expanded && (
<tr className="border-t border-[rgba(0,0,0,0.08)] bg-[#F9F9F9]"> <tr className="border-t border-[#D1D5DB] bg-[#F9F9F9]">
<td colSpan={desktopColSpan}> <td colSpan={desktopColSpan}>
<div className="flex flex-col items-start w-full bg-[#FFF] border border-gray-300 rounded-md p-4 text-xs text-gray-700 m-4"> <div className="flex flex-col items-start w-full bg-[#FFF] border border-gray-300 rounded-md p-4 text-xs text-gray-700 m-4">
{renderExpandedRow(item)} {renderExpandedRow(item)}

View File

@ -73,7 +73,7 @@ export const DeleteConfirmationModal = ({
className="bg-white rounded-xl shadow-[0px_20px_25px_-5px_rgba(0,0,0,0.1),0px_10px_10px_-5px_rgba(0,0,0,0.04)] w-full max-w-[400px] z-[301]" className="bg-white rounded-xl shadow-[0px_20px_25px_-5px_rgba(0,0,0,0.1),0px_10px_10px_-5px_rgba(0,0,0,0.04)] w-full max-w-[400px] z-[301]"
> >
{/* Modal Header */} {/* Modal Header */}
<div className="flex items-start justify-between pb-4 pt-5 px-5 border-b border-[rgba(0,0,0,0.08)]"> <div className="flex items-start justify-between px-5 pt-5 pb-[15px]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-[rgba(239,68,68,0.1)] rounded-full flex items-center justify-center shrink-0"> <div className="w-10 h-10 bg-[rgba(239,68,68,0.1)] rounded-full flex items-center justify-center shrink-0">
<AlertTriangle className="w-5 h-5 text-[#ef4444]" /> <AlertTriangle className="w-5 h-5 text-[#ef4444]" />
@ -94,7 +94,7 @@ export const DeleteConfirmationModal = ({
</div> </div>
{/* Modal Body */} {/* Modal Body */}
<div className="p-5"> <div className='pb-4 px-5'>
<p className="text-sm text-[#6b7280] leading-relaxed"> <p className="text-sm text-[#6b7280] leading-relaxed">
{message} {message}
{itemName && ( {itemName && (
@ -106,7 +106,7 @@ export const DeleteConfirmationModal = ({
</div> </div>
{/* Modal Footer */} {/* Modal Footer */}
<div className="flex items-center justify-end gap-3 pt-4 px-5 pb-5 border-t border-[rgba(0,0,0,0.08)]"> <div className="flex items-center justify-end gap-3 px-5 pb-5">
<SecondaryButton <SecondaryButton
type="button" type="button"
onClick={onClose} onClick={onClose}

View File

@ -122,24 +122,24 @@ export const NewDepartmentModal = ({
> >
<form <form
onSubmit={handleSubmit(handleFormSubmit)} onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-4" className="flex flex-col gap-3"
> >
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {/* <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> */}
<FormField <FormField
label="Department Name" label="Department Name"
required required
placeholder="e.g. Engineering" placeholder="e.g. Engineering"
error={errors.name?.message} error={errors.name?.message}
{...register("name")} {...register("name")}
/> />
<FormField <FormField
label="Department Code" label="Code"
required required
placeholder="e.g. ENG" placeholder="e.g. ENG"
error={errors.code?.message} error={errors.code?.message}
{...register("code")} {...register("code")}
/> />
</div> {/* </div> */}
{/* <FormField {/* <FormField
label="Description" label="Description"
@ -155,23 +155,23 @@ export const NewDepartmentModal = ({
rows={4} rows={4}
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {/* <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> */}
<PaginatedSelect <PaginatedSelect
label="Parent Department" label="Parent Department"
placeholder="Select parent (optional)" placeholder="Select parent (optional)"
value={parentIdValue || ""} value={parentIdValue || ""}
onValueChange={(value) => setValue("parent_id", value || null)} onValueChange={(value) => setValue("parent_id", value || null)}
onLoadOptions={loadDepartments} onLoadOptions={loadDepartments}
error={errors.parent_id?.message} error={errors.parent_id?.message}
/> />
<FormField <FormField
label="Sort Order" label="Sort Order"
type="number" type="number"
placeholder="0" placeholder="0"
error={errors.sort_order?.message} error={errors.sort_order?.message}
{...register("sort_order", { valueAsNumber: true })} {...register("sort_order", { valueAsNumber: true })}
/> />
</div> {/* </div> */}
{/* <FormSelect {/* <FormSelect
label="Status" label="Status"
@ -309,24 +309,24 @@ export const EditDepartmentModal = ({
> >
<form <form
onSubmit={handleSubmit(handleFormSubmit)} onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-4" className="flex flex-col gap-4"
> >
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {/* <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> */}
<FormField <FormField
label="Department Name" label="Department Name"
required required
placeholder="e.g. Engineering" placeholder="e.g. Engineering"
error={errors.name?.message} error={errors.name?.message}
{...register("name")} {...register("name")}
/> />
<FormField <FormField
label="Department Code" label="Department Code"
required required
placeholder="e.g. ENG" placeholder="e.g. ENG"
error={errors.code?.message} error={errors.code?.message}
{...register("code")} {...register("code")}
/> />
</div> {/* </div> */}
{/* <FormField {/* <FormField
label="Description" label="Description"
@ -342,23 +342,23 @@ export const EditDepartmentModal = ({
rows={4} rows={4}
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {/* <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> */}
<PaginatedSelect <PaginatedSelect
label="Parent Department" label="Parent Department"
placeholder="Select parent (optional)" placeholder="Select parent (optional)"
value={parentIdValue || ""} value={parentIdValue || ""}
onValueChange={(value) => setValue("parent_id", value || null)} onValueChange={(value) => setValue("parent_id", value || null)}
onLoadOptions={loadDepartments} onLoadOptions={loadDepartments}
error={errors.parent_id?.message} error={errors.parent_id?.message}
/> />
<FormField <FormField
label="Sort Order" label="Sort Order"
type="number" type="number"
placeholder="0" placeholder="0"
error={errors.sort_order?.message} error={errors.sort_order?.message}
{...register("sort_order", { valueAsNumber: true })} {...register("sort_order", { valueAsNumber: true })}
/> />
</div> {/* </div> */}
<FormSelect <FormSelect
label="Status" label="Status"
@ -394,7 +394,7 @@ export const ViewDepartmentModal = ({
footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>} footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>}
> >
{department && ( {department && (
<div className="p-5 grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-[#0f1724]"> <h3 className="text-sm font-semibold text-[#0f1724]">
Basic Information Basic Information

View File

@ -95,7 +95,7 @@ export const NewDesignationModal = ({
> >
<form <form
onSubmit={handleSubmit(handleFormSubmit)} onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-4" className="flex flex-col gap-4"
> >
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField <FormField
@ -232,7 +232,7 @@ export const EditDesignationModal = ({
> >
<form <form
onSubmit={handleSubmit(handleFormSubmit)} onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-4" className="flex flex-col gap-4"
> >
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField <FormField
@ -316,7 +316,7 @@ export const ViewDesignationModal = ({
footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>} footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>}
> >
{designation && ( {designation && (
<div className="p-5 grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-[#0f1724]"> <h3 className="text-sm font-semibold text-[#0f1724]">
Basic Information Basic Information

View File

@ -465,7 +465,7 @@ export const EditRoleModal = ({
{!isLoadingRole && ( {!isLoadingRole && (
<form <form
onSubmit={handleSubmit(handleFormSubmit)} onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-0" className="flex flex-col gap-0"
> >
{/* Role Name and Role Code Row */} {/* Role Name and Role Code Row */}
<div className="grid grid-cols-2 gap-5 pb-4"> <div className="grid grid-cols-2 gap-5 pb-4">

View File

@ -546,7 +546,7 @@ export const EditUserModal = ({
</> </>
} }
> >
<form onSubmit={handleSubmit(handleFormSubmit as any)} className="p-5"> <form onSubmit={handleSubmit(handleFormSubmit as any)}>
{isLoadingUser && ( {isLoadingUser && (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" /> <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />

View File

@ -1,17 +1,24 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { failedEmailsService, type FailedEmail } from '../../services/failed-emails-service'; import {
import { DataTable, type Column } from './DataTable'; failedEmailsService,
import { Modal } from './Modal'; type FailedEmail,
import { Button } from '@/components/ui/button'; } from "../../services/failed-emails-service";
import { StatusBadge } from './StatusBadge'; import { DataTable, type Column } from "./DataTable";
import { Eye, RefreshCw, Trash2, Loader2 } from 'lucide-react'; import { Modal } from "./Modal";
import { format } from 'date-fns'; // import { Button } from "@/components/ui/button";
import { toast } from 'sonner'; import { StatusBadge } from "./StatusBadge";
import { Pagination } from './Pagination'; import { Eye, RefreshCw, Trash2, Loader2 } from "lucide-react";
import { ActionDropdown } from './ActionDropdown'; import { format } from "date-fns";
import { PrimaryButton } from './PrimaryButton'; import { toast } from "sonner";
import { Pagination } from "./Pagination";
import { ActionDropdown } from "./ActionDropdown";
import { PrimaryButton } from "./PrimaryButton";
export const FailedEmailsTable: React.FC = () => { interface FailedEmailsTableProps {
onRegisterResendAll?: (node: React.ReactNode) => void;
}
export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegisterResendAll }) => {
const [emails, setEmails] = useState<FailedEmail[]>([]); const [emails, setEmails] = useState<FailedEmail[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@ -28,7 +35,10 @@ export const FailedEmailsTable: React.FC = () => {
setLoading(true); setLoading(true);
try { try {
const offset = (page - 1) * currentLimit; const offset = (page - 1) * currentLimit;
const res = await failedEmailsService.getFailedEmails(currentLimit, offset); const res = await failedEmailsService.getFailedEmails(
currentLimit,
offset,
);
setEmails(res.data || []); setEmails(res.data || []);
setTotal(res.total || 0); setTotal(res.total || 0);
setCurrentPage(page); setCurrentPage(page);
@ -49,10 +59,10 @@ export const FailedEmailsTable: React.FC = () => {
setResendingId(id); setResendingId(id);
try { try {
await failedEmailsService.resendEmail(id); await failedEmailsService.resendEmail(id);
toast.success('Email resent successfully.'); toast.success("Email resent successfully.");
fetchEmails(currentPage); fetchEmails(currentPage);
} catch (error: any) { } catch (error: any) {
toast.error('Failed to resend email', { description: error.message }); toast.error("Failed to resend email", { description: error.message });
} finally { } finally {
setResendingId(null); setResendingId(null);
} }
@ -65,19 +75,49 @@ export const FailedEmailsTable: React.FC = () => {
toast.success(res.message); toast.success(res.message);
fetchEmails(currentPage); fetchEmails(currentPage);
} catch (error: any) { } catch (error: any) {
toast.error('Failed to resend all emails', { description: error.message }); toast.error("Failed to resend all emails", {
description: error.message,
});
} finally { } finally {
setIsResendingAll(false); setIsResendingAll(false);
} }
}; };
useEffect(() => {
if (onRegisterResendAll) {
onRegisterResendAll(
<PrimaryButton
onClick={handleResendAll}
disabled={
isResendingAll ||
emails.filter((e) => e.status === "failed").length === 0
}
>
{isResendingAll ? (
<>
<Loader2 className="mr-2 w-3.5 h-3.5 animate-spin" />
Resending...
</>
) : (
"Resend All Failed"
)}
</PrimaryButton>
);
}
return () => {
if (onRegisterResendAll) {
onRegisterResendAll(null);
}
};
}, [isResendingAll, emails, onRegisterResendAll]);
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
try { try {
await failedEmailsService.deleteEmail(id); await failedEmailsService.deleteEmail(id);
toast.success('Email deleted successfully.'); toast.success("Email deleted successfully.");
fetchEmails(currentPage); fetchEmails(currentPage);
} catch (error: any) { } catch (error: any) {
toast.error('Failed to delete email', { description: error.message }); toast.error("Failed to delete email", { description: error.message });
} }
}; };
@ -88,81 +128,91 @@ export const FailedEmailsTable: React.FC = () => {
const columns: Column<FailedEmail>[] = [ const columns: Column<FailedEmail>[] = [
{ {
label: 'Date', label: "Date",
key: 'created_at', key: "created_at",
render: (record: FailedEmail) => format(new Date(record.created_at), 'yyyy-MM-dd HH:mm:ss') render: (record: FailedEmail) =>
format(new Date(record.created_at), "yyyy-MM-dd HH:mm:ss"),
}, },
{ {
label: 'To', label: "To",
key: 'to_email', key: "to_email",
render: (record: FailedEmail) => record.to_email render: (record: FailedEmail) => record.to_email,
}, },
{ {
label: 'Subject', label: "Subject",
key: 'subject', key: "subject",
render: (record: FailedEmail) => record.subject render: (record: FailedEmail) => record.subject,
}, },
{ {
label: 'Status', label: "Status",
key: 'status', key: "status",
render: (record: FailedEmail) => ( render: (record: FailedEmail) => (
<StatusBadge variant={record.status === 'failed' ? 'failure' : 'success'}> <StatusBadge
variant={record.status === "failed" ? "failure" : "success"}
>
{record.status} {record.status}
</StatusBadge> </StatusBadge>
) ),
}, },
{ {
label: 'Actions', label: "Actions",
key: 'actions', key: "actions",
align: 'right', align: "right",
render: (record: FailedEmail) => ( render: (record: FailedEmail) => (
<div className="flex justify-end"> <div className="flex justify-end">
<ActionDropdown <ActionDropdown
actions={[ actions={[
{ {
label: 'View Details', label: "View Details",
onClick: () => showEmailDetails(record), onClick: () => showEmailDetails(record),
icon: <Eye className="w-3.5 h-3.5 text-gray-500" /> icon: <Eye className="w-3.5 h-3.5 text-gray-500" />,
}, },
...(record.status === 'failed' ...(record.status === "failed"
? [ ? [
{ {
label: resendingId === record.id ? 'Resending...' : 'Resend Email', label:
resendingId === record.id
? "Resending..."
: "Resend Email",
onClick: () => handleResend(record.id), onClick: () => handleResend(record.id),
icon: resendingId === record.id ? ( icon:
<Loader2 className="w-3.5 h-3.5 animate-spin text-blue-600" /> resendingId === record.id ? (
) : ( <Loader2 className="w-3.5 h-3.5 animate-spin text-blue-600" />
<RefreshCw className="w-3.5 h-3.5 text-gray-500" /> ) : (
) <RefreshCw className="w-3.5 h-3.5 text-gray-500" />
} ),
},
] ]
: []), : []),
{ {
label: 'Delete Email', label: "Delete Email",
onClick: () => handleDelete(record.id), onClick: () => handleDelete(record.id),
icon: <Trash2 className="w-3.5 h-3.5 text-red-600" />, icon: <Trash2 className="w-3.5 h-3.5 text-red-600" />,
variant: 'danger' variant: "danger",
} },
]} ]}
/> />
</div> </div>
) ),
} },
]; ];
return ( return (
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden"> <div className="overflow-hidden">
{/* Toolbar / Actions Header */} {/* Toolbar / Actions Header */}
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row justify-end items-center gap-3"> {!onRegisterResendAll && (
<div className="flex items-center gap-2 w-full sm:w-auto justify-end"> <div className="pb-2 flex justify-end">
<Button variant="outline" size="sm" onClick={() => fetchEmails(currentPage)}> {/* <div className="flex items-center gap-2 w-full sm:w-auto justify-end"> */}
<RefreshCw className="mr-2 w-3.5 h-3.5" /> {/* <Button variant="outline" onClick={() => fetchEmails(currentPage)}>
Refresh <RefreshCw className="mr-2 w-3.5 h-3.5" />
</Button> Refresh
<PrimaryButton </Button> */}
size="small" <PrimaryButton
onClick={handleResendAll} onClick={handleResendAll}
disabled={isResendingAll || emails.filter(e => e.status === 'failed').length === 0} disabled={
isResendingAll ||
emails.filter((e) => e.status === "failed").length === 0
}
> >
{isResendingAll ? ( {isResendingAll ? (
<> <>
@ -170,11 +220,12 @@ export const FailedEmailsTable: React.FC = () => {
Resending... Resending...
</> </>
) : ( ) : (
'Resend All Failed' "Resend All Failed"
)} )}
</PrimaryButton> </PrimaryButton>
{/* </div> */}
</div> </div>
</div> )}
{/* Table Section */} {/* Table Section */}
<DataTable <DataTable
@ -185,7 +236,7 @@ export const FailedEmailsTable: React.FC = () => {
emptyMessage="No failed emails found" emptyMessage="No failed emails found"
error={error} error={error}
/> />
{/* Pagination Footer */} {/* Pagination Footer */}
{total > limit && ( {total > limit && (
<div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3"> <div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3">
@ -211,17 +262,28 @@ export const FailedEmailsTable: React.FC = () => {
maxWidth="xl" maxWidth="xl"
> >
{selectedEmail && ( {selectedEmail && (
<div className="p-5"> <div>
<div className="grid grid-cols-1 gap-2 mb-4"> <div className="grid grid-cols-1 gap-2 mb-4">
<p><strong className="text-[#0f1724]">To:</strong> <span className="text-[#6b7280]">{selectedEmail.to_email}</span></p> <p>
<p><strong className="text-[#0f1724]">Subject:</strong> <span className="text-[#6b7280]">{selectedEmail.subject}</span></p> <strong className="text-[#0f1724]">To:</strong>{" "}
<p><strong className="text-[#0f1724]">Error Message:</strong> <span className="text-[#ef4444]">{selectedEmail.error_message}</span></p> <span className="text-[#6b7280]">{selectedEmail.to_email}</span>
</p>
<p>
<strong className="text-[#0f1724]">Subject:</strong>{" "}
<span className="text-[#6b7280]">{selectedEmail.subject}</span>
</p>
<p>
<strong className="text-[#0f1724]">Error Message:</strong>{" "}
<span className="text-[#ef4444]">
{selectedEmail.error_message}
</span>
</p>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<strong className="text-[#0f1724] mb-2 block">Body:</strong> <strong className="text-[#0f1724] mb-2 block">Body:</strong>
<div <div
className="border border-[rgba(0,0,0,0.08)] rounded p-4 mt-2 max-h-[400px] overflow-auto bg-gray-50 text-sm" className="border border-[rgba(0,0,0,0.08)] rounded p-4 mt-2 max-h-[400px] overflow-auto bg-gray-50 text-sm"
dangerouslySetInnerHTML={{ __html: selectedEmail.body }} dangerouslySetInnerHTML={{ __html: selectedEmail.body }}
/> />
</div> </div>
</div> </div>

View File

@ -113,7 +113,7 @@ export const FileShareModal: React.FC<FileShareModalProps> = ({
maxWidth="md" maxWidth="md"
preventCloseOnClickOutside={showRevokeConfirm} preventCloseOnClickOutside={showRevokeConfirm}
> >
<div className="p-6 space-y-6"> <div className="space-y-4">
{!shareData ? ( {!shareData ? (
<> <>
{/* Expiry */} {/* Expiry */}

View File

@ -449,10 +449,10 @@ export const FileUploadModal = ({
maxWidth="md" maxWidth="md"
footer={footer} footer={footer}
> >
<div className="px-6 py-5 space-y-5"> <div className="space-y-4">
{/* Drop Zone */} {/* Drop Zone */}
<div> <div className="flex flex-col pb-4 gap-0.5">
<p className="text-[13px] font-medium text-[#0e1b2a] mb-2">Attach Files <span className="text-[#e02424]">*</span></p> <p className="text-[13px] font-medium text-[#0e1b2a]">Attach Files <span className="text-[#e02424]">*</span></p>
{fileEntries.length === 0 ? ( {fileEntries.length === 0 ? (
<div <div
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }} onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}

View File

@ -169,7 +169,7 @@ export const FileVersionUploadModal = ({
</> </>
} }
> >
<div className="px-6 py-5 space-y-5"> <div className="space-y-4">
<div> <div>
<p className="text-[13px] font-medium text-[#0e1b2a] mb-2">Select New File <span className="text-[#e02424]">*</span></p> <p className="text-[13px] font-medium text-[#0e1b2a] mb-2">Select New File <span className="text-[#e02424]">*</span></p>
<div <div

View File

@ -26,7 +26,7 @@ export const FormField = ({
const [showPassword, setShowPassword] = useState<boolean>(false); const [showPassword, setShowPassword] = useState<boolean>(false);
return ( return (
<div className="flex flex-col gap-2 pb-4"> <div className="flex flex-col gap-0.5 pb-4">
<label <label
htmlFor={fieldId} htmlFor={fieldId}
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]" className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"

View File

@ -134,7 +134,7 @@ export const FormSelect = ({
}; };
return ( return (
<div className="flex flex-col gap-2 pb-4"> <div className="flex flex-col gap-0.5 pb-4">
<label <label
htmlFor={fieldId} htmlFor={fieldId}
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]" className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"

View File

@ -18,7 +18,7 @@ export const FormTagInput = ({
placeholder = "Type and press enter...", placeholder = "Type and press enter...",
}: FormTagInputProps): ReactElement => { }: FormTagInputProps): ReactElement => {
return ( return (
<div className="flex flex-col gap-2 pb-1"> <div className="flex flex-col gap-0.5 pb-1">
{label && ( {label && (
<label className="text-[13px] font-medium text-[#0e1b2a]"> <label className="text-[13px] font-medium text-[#0e1b2a]">
{label} {label}

View File

@ -21,7 +21,7 @@ export const FormTextArea = ({
const hasError = Boolean(error); const hasError = Boolean(error);
return ( return (
<div className="flex flex-col gap-2 pb-4"> <div className="flex flex-col gap-0.5 pb-4">
<label <label
htmlFor={fieldId} htmlFor={fieldId}
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]" className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from "react";
import type { LucideIcon } from 'lucide-react'; import type { LucideIcon } from "lucide-react";
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
interface GradientStatCardProps { interface GradientStatCardProps {
icon: LucideIcon; icon: LucideIcon;
@ -8,16 +8,22 @@ interface GradientStatCardProps {
label: string; label: string;
badge?: { badge?: {
text: string; text: string;
variant: 'success' | 'warning' | 'info' | 'error' | 'green' | 'gray'; variant: "success" | "warning" | "info" | "error" | "green" | "gray";
}; };
} }
export const GradientStatCard: React.FC<GradientStatCardProps> = ({ icon: Icon, value, label, badge }) => { export const GradientStatCard: React.FC<GradientStatCardProps> = ({
icon: Icon,
value,
label,
badge,
}) => {
return ( return (
<div <div
className="rounded-[8px] p-[1px] h-full" className="rounded-[8px] p-[1px] h-full"
style={{ style={{
background: 'var(--Linear, linear-gradient(161deg, #084CC8 -1.15%, #75C044 44.29%, #FED314 89.74%))', background:
"var(--Linear, linear-gradient(161deg, #084CC8 -1.15%, #75C044 44.29%, #FED314 89.74%))",
}} }}
> >
<div className="flex flex-col items-start gap-3 px-4 py-4 min-h-[108px] h-full w-full rounded-[7px] bg-white"> <div className="flex flex-col items-start gap-3 px-4 py-4 min-h-[108px] h-full w-full rounded-[7px] bg-white">
@ -26,14 +32,20 @@ export const GradientStatCard: React.FC<GradientStatCardProps> = ({ icon: Icon,
<Icon className="w-5 h-5 stroke-[1.8]" /> <Icon className="w-5 h-5 stroke-[1.8]" />
</div> </div>
{badge && ( {badge && (
<div className={cn( <div
"px-2.5 py-1 rounded-full text-[12px] font-bold tracking-tight whitespace-nowrap", className={cn(
(badge.variant === 'success' || badge.variant === 'green') ? "bg-[#f1fffb] text-[#16c784]" : "px-2.5 py-1 rounded-full text-[12px] font-medium whitespace-nowrap capitalize leading-normal",
badge.variant === 'warning' ? "bg-[#fff5e5] text-[#fca004]" : badge.variant === "success" || badge.variant === "green"
badge.variant === 'info' ? "bg-[#f0f9ff] text-[#0ea5e9]" : ? "bg-[#f1fffb] text-[#16c784]"
badge.variant === 'error' ? "bg-[#fdf5f4] text-[#e0352a]" : : badge.variant === "warning"
"bg-[#f3f4f6] text-[#6b7280]" // default / gray ? "bg-[#fff5e5] text-[#fca004]"
)}> : badge.variant === "info"
? "bg-[#f0f9ff] text-[#0ea5e9]"
: badge.variant === "error"
? "bg-[#fdf5f4] text-[#e0352a]"
: "bg-[#f3f4f6] text-[#6b7280]", // default / gray
)}
>
{badge.text} {badge.text}
</div> </div>
)} )}

View File

@ -94,11 +94,11 @@ export const Modal = ({
)} )}
> >
{/* Modal Header */} {/* Modal Header */}
<div className="flex items-start justify-between pb-4 pt-5 px-5 border-b border-[rgba(0,0,0,0.08)] shrink-0"> <div className="flex items-start justify-between shrink-0 px-5 pt-5 pb-[15px]">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h2 className="text-lg font-semibold text-[#0e1b2a]">{title}</h2> <h2 className="text-lg font-semibold text-[#0F1724]">{title}</h2>
{description && ( {description && (
<p className="text-sm font-normal text-[#9aa6b2]">{description}</p> <p className="text-sm font-normal text-[#6B7280]">{description}</p>
)} )}
</div> </div>
{showCloseButton && ( {showCloseButton && (
@ -114,11 +114,13 @@ export const Modal = ({
</div> </div>
{/* Modal Body - Scrollable */} {/* Modal Body - Scrollable */}
<div className="flex-1 min-h-0 overflow-y-auto">{children}</div> <div className={cn("flex-1 min-h-0 overflow-y-auto px-5", footer ? "pb-4" : "pb-5")}>
{children}
</div>
{/* Modal Footer */} {/* Modal Footer */}
{footer && ( {footer && (
<div className="flex items-center justify-end gap-3 pt-4 px-5 pb-5 border-t border-[rgba(0,0,0,0.08)] shrink-0"> <div className="flex items-center justify-end gap-3 shrink-0 px-5 pb-5 pt-2">
{footer} {footer}
</div> </div>
)} )}

View File

@ -348,7 +348,7 @@ export const NewRoleModal = ({
> >
<form <form
onSubmit={handleSubmit(handleFormSubmit)} onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-0" className="flex-col gap-0"
> >
{/* General Error Display */} {/* General Error Display */}
{errors.root && ( {errors.root && (

View File

@ -230,7 +230,7 @@ export const NewUserModal = ({
</> </>
} }
> >
<form onSubmit={handleSubmit(handleFormSubmit as any)} className="p-5"> <form onSubmit={handleSubmit(handleFormSubmit as any)}>
{errors.root && ( {errors.root && (
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4"> <div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
<p className="text-sm text-[#ef4444]">{errors.root.message}</p> <p className="text-sm text-[#ef4444]">{errors.root.message}</p>

View File

@ -60,7 +60,7 @@ export const PageHeader = ({
.sort((a, b) => b.path.length - a.path.length)[0]?.path ?? null; .sort((a, b) => b.path.length - a.path.length)[0]?.path ?? null;
return ( return (
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 md:gap-6 mb-6"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 md:gap-6 mb-4">
{/* Title and Description */} {/* Title and Description */}
<div className="flex flex-col gap-1 max-w-full md:max-w-[434px]"> <div className="flex flex-col gap-1 max-w-full md:max-w-[434px]">
<h1 className="text-xl md:text-2xl font-bold text-[#0f1724] tracking-[-0.48px]"> <h1 className="text-xl md:text-2xl font-bold text-[#0f1724] tracking-[-0.48px]">

View File

@ -207,7 +207,7 @@ export const PaginatedSelect = ({
}; };
return ( return (
<div className="flex flex-col gap-2 pb-4"> <div className="flex flex-col gap-0.5 pb-4">
<label <label
htmlFor={fieldId} htmlFor={fieldId}
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]" className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"

View File

@ -117,20 +117,20 @@ export const Pagination = ({
const selectedLimitOption = limitOptions.find((opt) => Number(opt.value) === limit); const selectedLimitOption = limitOptions.find((opt) => Number(opt.value) === limit);
return ( return (
<div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-center justify-between gap-3"> <div className="p-3 flex flex-col sm:flex-row items-center justify-between gap-3">
{/* Items Info and Limit Selector */} {/* Items Info and Limit Selector */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3"> <div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<div className="text-xs text-[#6b7280]"> <div className="text-xs text-[#6B7280]">
Showing {startItem} to {endItem} of {totalItems} {totalItems === 1 ? 'item' : 'items'} Showing {startItem} to {endItem} of {totalItems} {totalItems === 1 ? 'item' : 'items'}
</div> </div>
<div className="flex items-center gap-2 relative" ref={limitDropdownRef}> <div className="flex items-center gap-2 relative" ref={limitDropdownRef}>
<span className="text-xs text-[#6b7280]">Show:</span> <span className="text-xs text-[#6B7280]">Show:</span>
<div className="w-[120px] relative"> <div className="w-[120px] relative">
<button <button
ref={limitButtonRef} ref={limitButtonRef}
type="button" type="button"
onClick={() => setIsLimitOpen(!isLimitOpen)} onClick={() => setIsLimitOpen(!isLimitOpen)}
className="h-8 w-full px-3.5 py-1 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0e1b2a] flex items-center justify-between hover:bg-gray-50 transition-colors min-h-[44px]" className="h-8 w-full px-3.5 py-2 border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0e1b2a] flex items-center justify-between hover:bg-gray-50 transition-colors"
> >
<span>{selectedLimitOption ? selectedLimitOption.label : `${limit} per page`}</span> <span>{selectedLimitOption ? selectedLimitOption.label : `${limit} per page`}</span>
<ChevronDown <ChevronDown
@ -142,7 +142,7 @@ export const Pagination = ({
createPortal( createPortal(
<div <div
data-limit-dropdown="true" data-limit-dropdown="true"
className="fixed bg-white border border-[rgba(0,0,0,0.2)] rounded-md shadow-lg z-[250] max-h-60 overflow-y-auto" className="fixed border border-[rgba(0,0,0,0.2)] rounded-md shadow-lg z-[250] max-h-60 overflow-y-auto"
style={limitDropdownStyle} style={limitDropdownStyle}
> >
<ul className="py-1.5"> <ul className="py-1.5">
@ -177,7 +177,7 @@ export const Pagination = ({
type="button" type="button"
onClick={handlePrevious} onClick={handlePrevious}
disabled={currentPage === 1} disabled={currentPage === 1}
className="flex items-center gap-1 px-3 py-1.5 bg-white border border-[rgba(0,0,0,0.08)] rounded text-xs text-[#0f1724] hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px] min-w-[44px]" className="flex items-center gap-1 px-3 py-1.5 border border-[rgba(0,0,0,0.08)] rounded text-xs text-[#0f1724] hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
<ChevronLeft className="w-3.5 h-3.5" /> <ChevronLeft className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Previous</span> <span className="hidden sm:inline">Previous</span>
@ -191,7 +191,7 @@ export const Pagination = ({
type="button" type="button"
onClick={handleNext} onClick={handleNext}
disabled={currentPage >= totalPages} disabled={currentPage >= totalPages}
className="flex items-center gap-1 px-3 py-1.5 text-white rounded text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px] min-w-[44px]" className="flex items-center gap-1 px-3 py-1.5 text-white rounded text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
style={currentPage < totalPages ? { backgroundColor: primaryColor } : { backgroundColor: '#112868', opacity: 0.5 }} style={currentPage < totalPages ? { backgroundColor: primaryColor } : { backgroundColor: '#112868', opacity: 0.5 }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (currentPage < totalPages) { if (currentPage < totalPages) {

View File

@ -1,31 +1,32 @@
import type { ReactElement, ButtonHTMLAttributes } from 'react'; import type { ReactElement, ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { useAppTheme } from '@/hooks/useAppTheme'; import { useAppTheme } from "@/hooks/useAppTheme";
const primaryButtonVariants = cva( const primaryButtonVariants = cva(
'inline-flex items-center justify-center px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer', "inline-flex items-center justify-center gap-2 px-4 py-2 rounded text-[14px] font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer leading-normal",
{ {
variants: { variants: {
size: { size: {
default: 'h-10', default: "h-10",
small: 'h-8', small: "h-8",
large: 'h-12', large: "h-12",
}, },
variant: { variant: {
default: 'bg-[#112868] text-[#23dce1] hover:bg-[#23dce1] hover:text-[#112868]', default: "",
disabled: 'bg-[#112868] text-[#23dce1] opacity-50', disabled: "",
}, },
}, },
defaultVariants: { defaultVariants: {
size: 'default', size: "default",
variant: 'default', variant: "default",
}, },
} },
); );
interface PrimaryButtonProps interface PrimaryButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>, extends
ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof primaryButtonVariants> { VariantProps<typeof primaryButtonVariants> {
children: React.ReactNode; children: React.ReactNode;
} }
@ -38,34 +39,28 @@ export const PrimaryButton = ({
disabled, disabled,
...props ...props
}: PrimaryButtonProps): ReactElement => { }: PrimaryButtonProps): ReactElement => {
const buttonVariant = disabled ? 'disabled' : variant || 'default'; const buttonVariant = disabled ? "disabled" : variant || "default";
const { primaryColor, secondaryColor } = useAppTheme(); const { primaryColor, secondaryColor } = useAppTheme();
return ( return (
<button <button
className={cn(primaryButtonVariants({ size, variant: buttonVariant }), className)} className={cn(
style={ primaryButtonVariants({ size, variant: buttonVariant }),
buttonVariant === 'default' className,
? { )}
backgroundColor: primaryColor, style={{
color: secondaryColor, backgroundColor: primaryColor, // #112868
} color: secondaryColor,
: buttonVariant === 'disabled' opacity: buttonVariant === "disabled" ? 0.5 : 1,
? { }}
backgroundColor: primaryColor,
color: secondaryColor,
opacity: 0.5,
}
: undefined
}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (buttonVariant === 'default' && !disabled) { if (!disabled) {
e.currentTarget.style.backgroundColor = secondaryColor; e.currentTarget.style.backgroundColor = secondaryColor;
e.currentTarget.style.color = primaryColor; e.currentTarget.style.color = primaryColor;
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (buttonVariant === 'default' && !disabled) { if (!disabled) {
e.currentTarget.style.backgroundColor = primaryColor; e.currentTarget.style.backgroundColor = primaryColor;
e.currentTarget.style.color = secondaryColor; e.currentTarget.style.color = secondaryColor;
} }

View File

@ -1,25 +1,26 @@
import type { ReactElement, ButtonHTMLAttributes } from 'react'; import type { ReactElement, ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from '@/lib/utils'; import { cn } from "@/lib/utils";
import { useAppTheme } from '@/hooks/useAppTheme'; import { useAppTheme } from "@/hooks/useAppTheme";
const secondaryButtonVariants = cva( const secondaryButtonVariants = cva(
'inline-flex items-center justify-center px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer', "inline-flex items-center justify-center gap-2 px-4 py-2 rounded text-[14px] font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer leading-normal",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-[#23dce1] text-[#112868] hover:bg-[#112868] hover:text-[#23dce1]', default: "",
disabled: 'bg-[#23dce1] text-[#112868] opacity-50', disabled: "",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
}, },
} },
); );
interface SecondaryButtonProps interface SecondaryButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>, extends
ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof secondaryButtonVariants> { VariantProps<typeof secondaryButtonVariants> {
children: React.ReactNode; children: React.ReactNode;
} }
@ -31,34 +32,28 @@ export const SecondaryButton = ({
disabled, disabled,
...props ...props
}: SecondaryButtonProps): ReactElement => { }: SecondaryButtonProps): ReactElement => {
const buttonVariant = disabled ? 'disabled' : variant || 'default'; const buttonVariant = disabled ? "disabled" : variant || "default";
const { primaryColor, secondaryColor } = useAppTheme(); const { primaryColor, secondaryColor } = useAppTheme();
return ( return (
<button <button
className={cn(secondaryButtonVariants({ variant: buttonVariant }), className)} className={cn(
style={ secondaryButtonVariants({ variant: buttonVariant }),
buttonVariant === 'default' className,
? { )}
backgroundColor: secondaryColor, style={{
color: primaryColor, backgroundColor: secondaryColor, // #112868
} color: primaryColor,
: buttonVariant === 'disabled' opacity: buttonVariant === "disabled" ? 0.5 : 1,
? { }}
backgroundColor: secondaryColor,
color: primaryColor,
opacity: 0.5,
}
: undefined
}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (buttonVariant === 'default' && !disabled) { if (!disabled) {
e.currentTarget.style.backgroundColor = primaryColor; e.currentTarget.style.backgroundColor = primaryColor;
e.currentTarget.style.color = secondaryColor; e.currentTarget.style.color = secondaryColor;
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (buttonVariant === 'default' && !disabled) { if (!disabled) {
e.currentTarget.style.backgroundColor = secondaryColor; e.currentTarget.style.backgroundColor = secondaryColor;
e.currentTarget.style.color = primaryColor; e.currentTarget.style.color = primaryColor;
} }

View File

@ -324,7 +324,7 @@ export const SupplierModal = ({
} }
maxWidth="lg" maxWidth="lg"
footer={ footer={
<div className="p-3 flex justify-end gap-3"> <div className="flex justify-end gap-3">
<SecondaryButton <SecondaryButton
type="button" type="button"
onClick={onClose} onClick={onClose}
@ -348,7 +348,7 @@ export const SupplierModal = ({
{isLoading ? ( {isLoading ? (
<div className="py-10 text-center text-[#6b7280]">Loading...</div> <div className="py-10 text-center text-[#6b7280]">Loading...</div>
) : ( ) : (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-5"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Basic Information */} {/* Basic Information */}
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-sm font-semibold text-[#112868] border-b pb-2"> <h4 className="text-sm font-semibold text-[#112868] border-b pb-2">

View File

@ -170,7 +170,7 @@ export const SuppliersTable = ({
key: "supplier_type", key: "supplier_type",
label: "Type", label: "Type",
render: (supplier) => ( render: (supplier) => (
<span className="text-sm text-[#4b5563] capitalize"> <span className="">
{supplier.supplier_type.replace(/_/g, " ")} {supplier.supplier_type.replace(/_/g, " ")}
</span> </span>
), ),
@ -233,10 +233,10 @@ export const SuppliersTable = ({
]; ];
const mobileCardRenderer = (supplier: Supplier) => ( const mobileCardRenderer = (supplier: Supplier) => (
<div className="p-4 border-b border-[rgba(0,0,0,0.08)]"> <div className="p-4 border-b border-[#D1D5DB]">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gray-50 border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-gray-50 border border-[#D1D5DB] rounded-lg flex items-center justify-center">
<Building2 className="w-5 h-5 text-[#9aa6b2]" /> <Building2 className="w-5 h-5 text-[#9aa6b2]" />
</div> </div>
<div> <div>
@ -269,7 +269,7 @@ export const SuppliersTable = ({
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{showHeader && ( {showHeader && (
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 py-3 bg-white border-b border-[rgba(0,0,0,0.08)]"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 pb-2 border-b border-[#D1D5DB]">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<SearchBox <SearchBox
@ -307,7 +307,7 @@ export const SuppliersTable = ({
</div> </div>
)} )}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden"> <div className="overflow-hidden">
<DataTable <DataTable
columns={columns} columns={columns}
data={suppliers} data={suppliers}

View File

@ -76,7 +76,7 @@ export const ViewAuditLogModal = ({
</SecondaryButton> </SecondaryButton>
} }
> >
<div className="p-5"> <div>
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" /> <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />

View File

@ -89,13 +89,13 @@ export const ViewRoleModal = ({
)} )}
{error && ( {error && (
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md"> <div className="bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
<p className="text-sm text-[#ef4444]">{error}</p> <p className="text-sm text-[#ef4444]">{error}</p>
</div> </div>
)} )}
{!isLoading && !error && role && ( {!isLoading && !error && role && (
<div className="p-5 flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Basic Information */} {/* Basic Information */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3> <h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3>

View File

@ -91,7 +91,7 @@ export const ViewUserModal = ({
</SecondaryButton> </SecondaryButton>
} }
> >
<div className="p-5"> <div>
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" /> <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />

View File

@ -669,10 +669,10 @@ export const WorkflowDefinitionModal = ({
</div> </div>
} }
> >
<div className="flex flex-col h-full min-h-[500px]"> <div className="flex flex-col">
{/* Tabs - Only show when creating new workflow */} {/* Tabs - Only show when creating new workflow */}
{!isEdit && ( {!isEdit && (
<div className="flex border-b border-[rgba(0,0,0,0.08)] bg-white sticky top-0 z-10"> <div className="flex border-b border-[rgba(0,0,0,0.08)] bg-white sticky top-0 z-10 mb-2">
<button <button
type="button" type="button"
onClick={() => setActiveTab("general")} onClick={() => setActiveTab("general")}
@ -698,7 +698,7 @@ export const WorkflowDefinitionModal = ({
)} )}
{/* Tab Content */} {/* Tab Content */}
<div className="p-6 flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{(activeTab === "general" || isEdit) && ( {(activeTab === "general" || isEdit) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<FormField <FormField

View File

@ -97,7 +97,7 @@ export const WorkflowDefinitionViewModal = ({
title="Workflow Definition" title="Workflow Definition"
maxWidth="xl" maxWidth="xl"
> >
<div className="p-6 min-h-[400px]"> <div className="">
{/* Loading */} {/* Loading */}
{isLoading && ( {isLoading && (
<div className="flex flex-col items-center justify-center h-64 gap-3"> <div className="flex flex-col items-center justify-center h-64 gap-3">
@ -116,7 +116,7 @@ export const WorkflowDefinitionViewModal = ({
{/* Content */} {/* Content */}
{definition && !isLoading && ( {definition && !isLoading && (
<div className="space-y-6"> <div className="space-y-4">
{/* ── Header Info ─────────────────────────────── */} {/* ── Header Info ─────────────────────────────── */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 p-4 bg-[#f8fafc] rounded-xl border border-[rgba(0,0,0,0.06)]"> <div className="grid grid-cols-2 md:grid-cols-3 gap-4 p-4 bg-[#f8fafc] rounded-xl border border-[rgba(0,0,0,0.06)]">
<div> <div>

View File

@ -1,7 +1,12 @@
import { useState, useEffect, type ReactElement } from "react"; import {
useState,
useEffect,
type ReactElement,
forwardRef,
useImperativeHandle,
} from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import {
PrimaryButton,
StatusBadge, StatusBadge,
DataTable, DataTable,
Pagination, Pagination,
@ -13,7 +18,7 @@ import {
type Column, type Column,
ActionDropdown, ActionDropdown,
} from "@/components/shared"; } from "@/components/shared";
import { Plus, Play, Power, Trash2, Copy, Edit, Eye } from "lucide-react"; import { Play, Power, Trash2, Copy, Edit, Eye } from "lucide-react";
import { workflowService } from "@/services/workflow-service"; import { workflowService } from "@/services/workflow-service";
import type { WorkflowDefinition } from "@/types/workflow"; import type { WorkflowDefinition } from "@/types/workflow";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
@ -21,6 +26,11 @@ import type { RootState } from "@/store/store";
import { formatDate } from "@/utils/format-date"; import { formatDate } from "@/utils/format-date";
import CodeBadge from "./CodeBadge"; import CodeBadge from "./CodeBadge";
export interface WorkflowDefinitionsTableRef {
openNewModal: () => void;
refresh: () => void;
}
interface WorkflowDefinitionsTableProps { interface WorkflowDefinitionsTableProps {
tenantId?: string | null; // If provided, use this tenantId (Super Admin mode) tenantId?: string | null; // If provided, use this tenantId (Super Admin mode)
compact?: boolean; // Compact mode for tabs compact?: boolean; // Compact mode for tabs
@ -28,380 +38,393 @@ interface WorkflowDefinitionsTableProps {
entityType?: string; // Filter by entity type entityType?: string; // Filter by entity type
} }
const WorkflowDefinitionsTable = ({ const WorkflowDefinitionsTable = forwardRef<
tenantId: tenantId, WorkflowDefinitionsTableRef,
compact = false, WorkflowDefinitionsTableProps
showHeader = true, >(
entityType, (
}: WorkflowDefinitionsTableProps): ReactElement => { { tenantId: tenantId, compact = false, showHeader = true, entityType },
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId); ref,
const effectiveTenantId = tenantId || reduxTenantId || undefined; ): ReactElement => {
const reduxTenantId = useSelector(
(state: RootState) => state.auth.tenantId,
);
const effectiveTenantId = tenantId || reduxTenantId || undefined;
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]); const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Pagination state // Pagination state
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(compact ? 10 : 10); const [limit, setLimit] = useState<number>(compact ? 10 : 10);
const [totalItems, setTotalItems] = useState<number>(0); const [totalItems, setTotalItems] = useState<number>(0);
// Filter state // Filter state
const [statusFilter, setStatusFilter] = useState<string | null>(null); const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState<string>(""); const [debouncedSearchQuery, setDebouncedSearchQuery] =
useState<string>("");
// Modal states // Modal states
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedDefinition, setSelectedDefinition] = const [selectedDefinition, setSelectedDefinition] =
useState<WorkflowDefinition | null>(null); useState<WorkflowDefinition | null>(null);
const [isViewModalOpen, setIsViewModalOpen] = useState(false); const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const [viewDefinitionId, setViewDefinitionId] = useState<string | null>(null); const [viewDefinitionId, setViewDefinitionId] = useState<string | null>(
const [isActionLoading, setIsActionLoading] = useState(false); null,
);
const fetchDefinitions = async () => { const [isActionLoading, setIsActionLoading] = useState(false);
try { // Expose imperative methods
setIsLoading(true); useImperativeHandle(ref, () => ({
setError(null); openNewModal: () => {
const response = await workflowService.listDefinitions({ setSelectedDefinition(null);
tenantId: effectiveTenantId, setIsModalOpen(true);
entity_type: entityType,
status: statusFilter || undefined,
limit,
offset: (currentPage - 1) * limit,
search: debouncedSearchQuery || undefined,
});
if (response.success) {
setDefinitions(response.data);
setTotalItems(response.pagination?.total || response.data.length);
} else {
setError("Failed to load workflow definitions");
}
} catch (err: any) {
setError(
err?.response?.data?.error?.message ||
"Failed to load workflow definitions",
);
} finally {
setIsLoading(false);
}
};
// Debouncing search query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery]);
useEffect(() => {
setCurrentPage(1);
}, [debouncedSearchQuery, statusFilter]);
useEffect(() => {
fetchDefinitions();
}, [
effectiveTenantId,
statusFilter,
currentPage,
limit,
debouncedSearchQuery,
]);
const handleDelete = async () => {
if (!selectedDefinition) return;
try {
setIsActionLoading(true);
const response = await workflowService.deleteDefinition(
selectedDefinition.id,
effectiveTenantId,
);
if (response.success) {
showToast.success("Workflow definition deleted successfully");
setIsDeleteModalOpen(false);
fetchDefinitions();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message ||
"Failed to delete workflow definition",
);
} finally {
setIsActionLoading(false);
}
};
const handleActivate = async (id: string) => {
try {
setIsActionLoading(true);
const response = await workflowService.activateDefinition(
id,
effectiveTenantId,
);
if (response.success) {
showToast.success("Workflow definition activated");
fetchDefinitions();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to activate",
);
} finally {
setIsActionLoading(false);
}
};
const handleDeprecate = async (id: string) => {
try {
setIsActionLoading(true);
const response = await workflowService.deprecateDefinition(
id,
effectiveTenantId,
);
if (response.success) {
showToast.success("Workflow definition deprecated");
fetchDefinitions();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to deprecate",
);
} finally {
setIsActionLoading(false);
}
};
const handleClone = async (id: string, name: string) => {
try {
setIsActionLoading(true);
const response = await workflowService.cloneDefinition(
id,
`${name} (Clone)`,
effectiveTenantId,
);
if (response.success) {
showToast.success("Workflow definition cloned");
fetchDefinitions();
}
} catch (err: any) {
showToast.error(err?.response?.data?.error?.message || "Failed to clone");
} finally {
setIsActionLoading(false);
}
};
const columns: Column<WorkflowDefinition>[] = [
{
key: "name",
label: "Workflow Component",
render: (wf) => (
<div className="flex flex-col">
<span className="text-sm font-medium text-[#0f1724]">{wf.name}</span>
<span className="text-xs text-[#6b7280] font-mono">{wf.code}</span>
</div>
),
},
{
key: "entity_type",
label: "Entity Type",
render: (wf) => <CodeBadge label={wf.entity_type} />,
},
{
key: "version",
label: "Version",
render: (wf) => (
<span className="text-sm text-[#6b7280]">v{wf.version}</span>
),
},
{
key: "status",
label: "Status",
render: (wf) => {
let variant: "success" | "failure" | "info" | "process" = "info";
if (wf.status === "active") variant = "success";
if (wf.status === "deprecated") variant = "failure";
if (wf.status === "draft") variant = "process";
return <StatusBadge variant={variant}>{wf.status}</StatusBadge>;
}, },
}, refresh: () => {
{ fetchDefinitions();
key: "source_module", },
label: "Module", }));
render: (wf) => (
<span className="text-sm text-[#6b7280]">
{wf.source_module?.join(", ")}
</span>
),
},
{
key: "created_at",
label: "Created Date",
render: (wf) => (
<span className="text-sm text-[#6b7280]">
{formatDate(wf.created_at)}
</span>
),
},
{
key: "actions",
label: "Actions",
align: "right",
render: (wf) => (
<div className="flex justify-end">
<ActionDropdown
actions={[
{
icon: <Copy className="w-4 h-4" />,
label: "Clone",
onClick: () => handleClone(wf.id, wf.name),
},
{
icon: <Eye className="w-4 h-4" />,
label: "View",
onClick: () => {
setViewDefinitionId(wf.id);
setIsViewModalOpen(true);
},
},
{
icon: <Edit className="w-4 h-4" />,
label: "Edit",
onClick: () => {
setSelectedDefinition(wf);
setIsModalOpen(true);
},
},
(wf.status === "draft" || wf.status === "deprecated") ? {
icon: <Play className="w-4 h-4" />,
label: "Activate",
onClick: () => handleActivate(wf.id),
} : null,
wf.status === "active" ? {
icon: <Power className="w-4 h-4" />,
label: "Deprecate",
onClick: () => handleDeprecate(wf.id),
} : null,
{
icon: <Trash2 className="w-4 h-4" />,
label: "Delete",
variant: "danger",
onClick: () => {
setSelectedDefinition(wf);
setIsDeleteModalOpen(true);
},
},
].filter((a): a is any => a !== null)}
/>
</div>
),
},
];
return ( const fetchDefinitions = async () => {
<div try {
className={`flex flex-col gap-4 ${!compact ? "bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm" : ""}`} setIsLoading(true);
> setError(null);
{showHeader && ( const response = await workflowService.listDefinitions({
<div className="p-4 border-b border-[rgba(0,0,0,0.08)] flex flex-col sm:flex-row items-center justify-between gap-4"> tenantId: effectiveTenantId,
<div className="flex items-center gap-3 w-full sm:w-auto"> entity_type: entityType,
<SearchBox status: statusFilter || undefined,
value={searchQuery} limit,
onChange={setSearchQuery} offset: (currentPage - 1) * limit,
placeholder="Search name, code or description" search: debouncedSearchQuery || undefined,
/> });
<FilterDropdown
label="Status" if (response.success) {
options={[ setDefinitions(response.data);
{ value: "active", label: "Active" }, setTotalItems(response.pagination?.total || response.data.length);
{ value: "draft", label: "Draft" }, } else {
{ value: "deprecated", label: "Deprecated" }, setError("Failed to load workflow definitions");
]} }
value={statusFilter || ""} } catch (err: any) {
onChange={(value) => setError(
setStatusFilter( err?.response?.data?.error?.message ||
value ? (Array.isArray(value) ? value[0] : value) : null, "Failed to load workflow definitions",
) );
} } finally {
setIsLoading(false);
}
};
// Debouncing search query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery]);
useEffect(() => {
setCurrentPage(1);
}, [debouncedSearchQuery, statusFilter]);
useEffect(() => {
fetchDefinitions();
}, [
effectiveTenantId,
statusFilter,
currentPage,
limit,
debouncedSearchQuery,
]);
const handleDelete = async () => {
if (!selectedDefinition) return;
try {
setIsActionLoading(true);
const response = await workflowService.deleteDefinition(
selectedDefinition.id,
effectiveTenantId,
);
if (response.success) {
showToast.success("Workflow definition deleted successfully");
setIsDeleteModalOpen(false);
fetchDefinitions();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message ||
"Failed to delete workflow definition",
);
} finally {
setIsActionLoading(false);
}
};
const handleActivate = async (id: string) => {
try {
setIsActionLoading(true);
const response = await workflowService.activateDefinition(
id,
effectiveTenantId,
);
if (response.success) {
showToast.success("Workflow definition activated");
fetchDefinitions();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to activate",
);
} finally {
setIsActionLoading(false);
}
};
const handleDeprecate = async (id: string) => {
try {
setIsActionLoading(true);
const response = await workflowService.deprecateDefinition(
id,
effectiveTenantId,
);
if (response.success) {
showToast.success("Workflow definition deprecated");
fetchDefinitions();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to deprecate",
);
} finally {
setIsActionLoading(false);
}
};
const handleClone = async (id: string, name: string) => {
try {
setIsActionLoading(true);
const response = await workflowService.cloneDefinition(
id,
`${name} (Clone)`,
effectiveTenantId,
);
if (response.success) {
showToast.success("Workflow definition cloned");
fetchDefinitions();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to clone",
);
} finally {
setIsActionLoading(false);
}
};
const columns: Column<WorkflowDefinition>[] = [
{
key: "name",
label: "Workflow Component",
render: (wf) => (
<div className="flex flex-col">
<span className="text-sm font-medium text-[#0f1724]">
{wf.name}
</span>
<span className="text-xs text-[#6b7280] font-mono">{wf.code}</span>
</div>
),
},
{
key: "entity_type",
label: "Entity Type",
render: (wf) => <CodeBadge label={wf.entity_type} />,
},
{
key: "version",
label: "Version",
render: (wf) => (
<span className="text-sm text-[#6b7280]">v{wf.version}</span>
),
},
{
key: "status",
label: "Status",
render: (wf) => {
let variant: "success" | "failure" | "info" | "process" = "info";
if (wf.status === "active") variant = "success";
if (wf.status === "deprecated") variant = "failure";
if (wf.status === "draft") variant = "process";
return <StatusBadge variant={variant}>{wf.status}</StatusBadge>;
},
},
{
key: "source_module",
label: "Module",
render: (wf) => (
<span className="text-sm text-[#6b7280]">
{wf.source_module?.join(", ")}
</span>
),
},
{
key: "created_at",
label: "Created Date",
render: (wf) => (
<span className="text-sm text-[#6b7280]">
{formatDate(wf.created_at)}
</span>
),
},
{
key: "actions",
label: "Actions",
align: "right",
render: (wf) => (
<div className="flex justify-end">
<ActionDropdown
actions={[
{
icon: <Copy className="w-4 h-4" />,
label: "Clone",
onClick: () => handleClone(wf.id, wf.name),
},
{
icon: <Eye className="w-4 h-4" />,
label: "View",
onClick: () => {
setViewDefinitionId(wf.id);
setIsViewModalOpen(true);
},
},
{
icon: <Edit className="w-4 h-4" />,
label: "Edit",
onClick: () => {
setSelectedDefinition(wf);
setIsModalOpen(true);
},
},
wf.status === "draft" || wf.status === "deprecated"
? {
icon: <Play className="w-4 h-4" />,
label: "Activate",
onClick: () => handleActivate(wf.id),
}
: null,
wf.status === "active"
? {
icon: <Power className="w-4 h-4" />,
label: "Deprecate",
onClick: () => handleDeprecate(wf.id),
}
: null,
{
icon: <Trash2 className="w-4 h-4" />,
label: "Delete",
variant: "danger",
onClick: () => {
setSelectedDefinition(wf);
setIsDeleteModalOpen(true);
},
},
].filter((a): a is any => a !== null)}
/> />
</div> </div>
<PrimaryButton ),
size="default" },
className="flex items-center gap-2 w-full sm:w-auto" ];
onClick={() => {
setSelectedDefinition(null);
setIsModalOpen(true);
}}
>
<Plus className="w-4 h-4" />
<span>New Workflow</span>
</PrimaryButton>
</div>
)}
<DataTable return (
data={definitions} <div className={`flex flex-col gap-2`}>
columns={columns} {showHeader && (
keyExtractor={(wf) => wf.id} <div className="flex flex-col sm:flex-row items-center justify-between gap-3">
isLoading={isLoading} <div className="flex items-center gap-3 w-full sm:w-auto">
error={error} <SearchBox
emptyMessage="No workflow definitions found" value={searchQuery}
/> onChange={setSearchQuery}
placeholder="Search name, code or description"
/>
<FilterDropdown
label="Status"
options={[
{ value: "active", label: "Active" },
{ value: "draft", label: "Draft" },
{ value: "deprecated", label: "Deprecated" },
]}
value={statusFilter || ""}
onChange={(value) =>
setStatusFilter(
value ? (Array.isArray(value) ? value[0] : value) : null,
)
}
/>
</div>
</div>
)}
{totalItems > 0 && ( <DataTable
<Pagination data={definitions}
currentPage={currentPage} columns={columns}
totalPages={Math.ceil(totalItems / limit)} keyExtractor={(wf) => wf.id}
totalItems={totalItems} isLoading={isLoading}
limit={limit} error={error}
onPageChange={setCurrentPage} emptyMessage="No workflow definitions found"
onLimitChange={(newLimit) => {
setLimit(newLimit);
setCurrentPage(1);
}}
/> />
)}
<DeleteConfirmationModal {totalItems > 0 && (
isOpen={isDeleteModalOpen} <Pagination
onClose={() => { currentPage={currentPage}
setIsDeleteModalOpen(false); totalPages={Math.ceil(totalItems / limit)}
setSelectedDefinition(null); totalItems={totalItems}
}} limit={limit}
onConfirm={handleDelete} onPageChange={setCurrentPage}
title="Delete Workflow Definition" onLimitChange={(newLimit) => {
message="Are you sure you want to delete this workflow definition? This action cannot be undone." setLimit(newLimit);
itemName={selectedDefinition?.name || ""} setCurrentPage(1);
isLoading={isActionLoading} }}
/> />
)}
<WorkflowDefinitionModal <DeleteConfirmationModal
isOpen={isModalOpen} isOpen={isDeleteModalOpen}
onClose={() => { onClose={() => {
setIsModalOpen(false); setIsDeleteModalOpen(false);
setSelectedDefinition(null); setSelectedDefinition(null);
}} }}
definition={selectedDefinition} onConfirm={handleDelete}
tenantId={effectiveTenantId} title="Delete Workflow Definition"
onSuccess={fetchDefinitions} message="Are you sure you want to delete this workflow definition? This action cannot be undone."
initialEntityType={entityType} itemName={selectedDefinition?.name || ""}
/> isLoading={isActionLoading}
/>
<WorkflowDefinitionViewModal <WorkflowDefinitionModal
isOpen={isViewModalOpen} isOpen={isModalOpen}
onClose={() => { onClose={() => {
setIsViewModalOpen(false); setIsModalOpen(false);
setViewDefinitionId(null); setSelectedDefinition(null);
}} }}
definitionId={viewDefinitionId} definition={selectedDefinition}
tenantId={effectiveTenantId} tenantId={effectiveTenantId}
/> onSuccess={fetchDefinitions}
</div> initialEntityType={entityType}
); />
};
<WorkflowDefinitionViewModal
isOpen={isViewModalOpen}
onClose={() => {
setIsViewModalOpen(false);
setViewDefinitionId(null);
}}
definitionId={viewDefinitionId}
tenantId={effectiveTenantId}
/>
</div>
);
},
);
export default WorkflowDefinitionsTable; export default WorkflowDefinitionsTable;

View File

@ -26,7 +26,7 @@ export type { TabItem } from './PageHeader';
export { AuthenticatedImage } from './AuthenticatedImage'; export { AuthenticatedImage } from './AuthenticatedImage';
export * from './DepartmentModals'; export * from './DepartmentModals';
export * from './DesignationModals'; export * from './DesignationModals';
export { default as WorkflowDefinitionsTable } from './WorkflowDefinitionsTable'; export { default as WorkflowDefinitionsTable, type WorkflowDefinitionsTableRef } from './WorkflowDefinitionsTable';
export { WorkflowDefinitionModal } from './WorkflowDefinitionModal'; export { WorkflowDefinitionModal } from './WorkflowDefinitionModal';
export { WorkflowDefinitionViewModal } from './WorkflowDefinitionViewModal'; export { WorkflowDefinitionViewModal } from './WorkflowDefinitionViewModal';
export { SuppliersTable } from './SuppliersTable'; export { SuppliersTable } from './SuppliersTable';

View File

@ -145,7 +145,7 @@ export const ApikeyReissueModal = ({
</div> </div>
} }
> >
<div className="p-5"> <div>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" /> <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />

View File

@ -28,7 +28,7 @@ export const DepartmentListView = ({
onLimitChange, onLimitChange,
}: DepartmentListViewProps): ReactElement => { }: DepartmentListViewProps): ReactElement => {
return ( return (
<div className="bg-white rounded-2xl border-2 border-slate-50 shadow-sm overflow-hidden"> <div className="overflow-hidden">
<DataTable <DataTable
data={data} data={data}
columns={columns} columns={columns}

View File

@ -250,7 +250,7 @@ export const DepartmentTreeView = ({
} }
return ( return (
<div className="flex flex-col gap-2 p-4 border border-[#D1D5DB] bg-white rounded-lg self-stretch"> <div className="flex flex-col gap-2 p-4 border border-[#D1D5DB] bg-white self-stretch">
{data.map((item) => ( {data.map((item) => (
<TreeItem <TreeItem
key={item.id} key={item.id}

View File

@ -222,7 +222,7 @@ export const DepartmentsTable = forwardRef<
key: "name", key: "name",
label: "Department Name", label: "Department Name",
render: (dept) => ( render: (dept) => (
<span className="text-sm font-medium text-[#0f1724]"> <span className="">
{dept.name} {dept.name}
</span> </span>
), ),
@ -236,7 +236,7 @@ export const DepartmentsTable = forwardRef<
key: "parent_name", key: "parent_name",
label: "Parent", label: "Parent",
render: (dept) => ( render: (dept) => (
<span className="text-sm text-[#6b7280]"> <span className="">
{dept.parent_name || "-"} {dept.parent_name || "-"}
</span> </span>
), ),
@ -245,21 +245,21 @@ export const DepartmentsTable = forwardRef<
key: "level", key: "level",
label: "Level", label: "Level",
render: (dept) => ( render: (dept) => (
<span className="text-sm text-[#6b7280]">{dept.level}</span> <span className="">{dept.level}</span>
), ),
}, },
{ {
key: "sort_order", key: "sort_order",
label: "Order", label: "Order",
render: (dept) => ( render: (dept) => (
<span className="text-sm text-[#6b7280]">{dept.sort_order}</span> <span className="">{dept.sort_order}</span>
), ),
}, },
{ {
key: "child_count", key: "child_count",
label: "Sub-depts", label: "Sub-depts",
render: (dept) => ( render: (dept) => (
<span className="text-sm text-[#6b7280]"> <span className="">
{dept.child_count || 0} {dept.child_count || 0}
</span> </span>
), ),
@ -268,7 +268,7 @@ export const DepartmentsTable = forwardRef<
key: "user_count", key: "user_count",
label: "Users", label: "Users",
render: (dept) => ( render: (dept) => (
<span className="text-sm text-[#6b7280]">{dept.user_count || 0}</span> <span className="">{dept.user_count || 0}</span>
), ),
}, },
{ {
@ -306,11 +306,11 @@ export const DepartmentsTable = forwardRef<
]; ];
return ( return (
<div className={`flex flex-col gap-4 `}> <div className={`flex flex-col gap-2`}>
{showHeader && ( {showHeader && (
<div className="flex flex-col border-b border-[rgba(0,0,0,0.08)]"> <div className="flex flex-col border-b border-[rgba(0,0,0,0.08)]">
{/* Tabs */} {/* Tabs */}
<div className="px-4 pt-3 flex items-center justify-between border-b border-transparent"> <div className="flex items-center justify-between border-b border-transparent">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<button <button
className="pb-3 text-sm font-medium transition-all relative" className="pb-3 text-sm font-medium transition-all relative"
@ -344,7 +344,7 @@ export const DepartmentsTable = forwardRef<
</button> </button>
</div> </div>
<div> <div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 pb-2">
{viewMode === "list" && ( {viewMode === "list" && (
<SearchBox <SearchBox
value={searchQuery} value={searchQuery}

View File

@ -1,7 +1,12 @@
import { useState, useEffect, type ReactElement, forwardRef, useImperativeHandle } from "react"; import {
useState,
useEffect,
type ReactElement,
forwardRef,
useImperativeHandle,
} from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import {
PrimaryButton,
StatusBadge, StatusBadge,
ActionDropdown, ActionDropdown,
DataTable, DataTable,
@ -15,10 +20,7 @@ import {
EditDesignationModal, EditDesignationModal,
ViewDesignationModal, ViewDesignationModal,
} from "@/components/shared/DesignationModals"; } from "@/components/shared/DesignationModals";
import { // import { Plus } from "lucide-react";
Plus,
// , Search
} from "lucide-react";
import { designationService } from "@/services/designation-service"; import { designationService } from "@/services/designation-service";
import type { import type {
Designation, Designation,
@ -42,298 +44,299 @@ interface DesignationsTableProps {
showHeader?: boolean; showHeader?: boolean;
} }
const DesignationsTable = forwardRef<DesignationsTableRef, DesignationsTableProps>(({ const DesignationsTable = forwardRef<
tenantId: propsTenantId, DesignationsTableRef,
compact = false, DesignationsTableProps
showHeader = true, >(
}, ref): ReactElement => { (
const { canCreate, canUpdate } = usePermissions(); { tenantId: propsTenantId, compact = false, showHeader = true },
// const { primaryColor } = useAppTheme(); ref,
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId); ): ReactElement => {
const effectiveTenantId = propsTenantId || reduxTenantId; const { canUpdate } = usePermissions();
// const { primaryColor } = useAppTheme();
const reduxTenantId = useSelector(
(state: RootState) => state.auth.tenantId,
);
const effectiveTenantId = propsTenantId || reduxTenantId;
const [designations, setDesignations] = useState<Designation[]>([]); const [designations, setDesignations] = useState<Designation[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Expose imperative methods // Expose imperative methods
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
openNewModal: () => setIsNewModalOpen(true), openNewModal: () => setIsNewModalOpen(true),
refresh: () => fetchDesignations(), refresh: () => fetchDesignations(),
})); }));
// Pagination state // Pagination state
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(compact ? 10 : 5); const [limit, setLimit] = useState<number>(compact ? 10 : 5);
// Filter state // Filter state
const [activeOnly, setActiveOnly] = useState<boolean>(false); const [activeOnly, setActiveOnly] = useState<boolean>(false);
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState<string>(""); const [debouncedSearchQuery, setDebouncedSearchQuery] =
useState<string>("");
// Modal states // Modal states
const [isNewModalOpen, setIsNewModalOpen] = useState(false); const [isNewModalOpen, setIsNewModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isViewModalOpen, setIsViewModalOpen] = useState(false); const [isViewModalOpen, setIsViewModalOpen] = useState(false);
// const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); // const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedDesignation, setSelectedDesignation] = const [selectedDesignation, setSelectedDesignation] =
useState<Designation | null>(null); useState<Designation | null>(null);
const [isActionLoading, setIsActionLoading] = useState(false); const [isActionLoading, setIsActionLoading] = useState(false);
const fetchDesignations = async () => { const fetchDesignations = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
const response = await designationService.list(effectiveTenantId, { const response = await designationService.list(effectiveTenantId, {
active_only: activeOnly, active_only: activeOnly,
search: debouncedSearchQuery, search: debouncedSearchQuery,
}); });
if (response.success) { if (response.success) {
setDesignations(response.data); setDesignations(response.data);
} else { } else {
setError("Failed to load designations"); setError("Failed to load designations");
}
} catch (err: any) {
setError(
err?.response?.data?.error?.message || "Failed to load designations",
);
} finally {
setIsLoading(false);
} }
} catch (err: any) { };
setError(
err?.response?.data?.error?.message || "Failed to load designations",
);
} finally {
setIsLoading(false);
}
};
// Debouncing search query // Debouncing search query
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery); setDebouncedSearchQuery(searchQuery);
}, 500); }, 500);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [searchQuery]); }, [searchQuery]);
useEffect(() => { useEffect(() => {
fetchDesignations(); fetchDesignations();
}, [effectiveTenantId, activeOnly, debouncedSearchQuery]); }, [effectiveTenantId, activeOnly, debouncedSearchQuery]);
const handleCreate = async (data: CreateDesignationRequest) => { const handleCreate = async (data: CreateDesignationRequest) => {
try { try {
setIsActionLoading(true); setIsActionLoading(true);
const response = await designationService.create(data, effectiveTenantId); const response = await designationService.create(
if (response.success) { data,
showToast.success("Designation created successfully"); effectiveTenantId,
setIsNewModalOpen(false); );
fetchDesignations(); if (response.success) {
showToast.success("Designation created successfully");
setIsNewModalOpen(false);
fetchDesignations();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to create designation",
);
} finally {
setIsActionLoading(false);
} }
} catch (err: any) { };
showToast.error(
err?.response?.data?.error?.message || "Failed to create designation",
);
} finally {
setIsActionLoading(false);
}
};
const handleUpdate = async (id: string, data: UpdateDesignationRequest) => { const handleUpdate = async (id: string, data: UpdateDesignationRequest) => {
try { try {
setIsActionLoading(true); setIsActionLoading(true);
const response = await designationService.update( const response = await designationService.update(
id, id,
data, data,
effectiveTenantId, effectiveTenantId,
); );
if (response.success) { if (response.success) {
showToast.success("Designation updated successfully"); showToast.success("Designation updated successfully");
setIsEditModalOpen(false); setIsEditModalOpen(false);
fetchDesignations(); fetchDesignations();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to update designation",
);
} finally {
setIsActionLoading(false);
} }
} catch (err: any) { };
showToast.error(
err?.response?.data?.error?.message || "Failed to update designation",
);
} finally {
setIsActionLoading(false);
}
};
// const handleDelete = async () => { // const handleDelete = async () => {
// if (!selectedDesignation) return; // if (!selectedDesignation) return;
// try { // try {
// setIsActionLoading(true); // setIsActionLoading(true);
// const response = await designationService.delete( // const response = await designationService.delete(
// selectedDesignation.id, // selectedDesignation.id,
// effectiveTenantId, // effectiveTenantId,
// ); // );
// if (response.success) { // if (response.success) {
// showToast.success("Designation deleted successfully"); // showToast.success("Designation deleted successfully");
// setIsDeleteModalOpen(false); // setIsDeleteModalOpen(false);
// fetchDesignations(); // fetchDesignations();
// } // }
// } catch (err: any) { // } catch (err: any) {
// showToast.error( // showToast.error(
// err?.response?.data?.error?.message || "Failed to delete designation", // err?.response?.data?.error?.message || "Failed to delete designation",
// ); // );
// } finally { // } finally {
// setIsActionLoading(false); // setIsActionLoading(false);
// } // }
// }; // };
// Client-side pagination logic // Client-side pagination logic
const totalItems = designations.length; const totalItems = designations.length;
const totalPages = Math.ceil(totalItems / limit); const totalPages = Math.ceil(totalItems / limit);
const paginatedData = designations.slice( const paginatedData = designations.slice(
(currentPage - 1) * limit, (currentPage - 1) * limit,
currentPage * limit, currentPage * limit,
); );
const columns: Column<Designation>[] = [ const columns: Column<Designation>[] = [
{ {
key: "name", key: "name",
label: "Designation Name", label: "Designation Name",
render: (desig) => ( render: (desig) => (
<span className="text-sm font-medium text-[#0f1724]">{desig.name}</span> <span className="text-sm font-medium text-[#0f1724]">
), {desig.name}
}, </span>
{ ),
key: "code", },
label: "Code", {
render: (desig) => <CodeBadge label={desig.code} />, key: "code",
}, label: "Code",
{ render: (desig) => <CodeBadge label={desig.code} />,
key: "level", },
label: "Level", {
render: (desig) => ( key: "level",
<span className="text-sm text-[#6b7280]">{desig.level}</span> label: "Level",
), render: (desig) => (
}, <span className="text-sm text-[#6b7280]">{desig.level}</span>
{ ),
key: "sort_order", },
label: "Order", {
render: (desig) => ( key: "sort_order",
<span className="text-sm text-[#6b7280]">{desig.sort_order}</span> label: "Order",
), render: (desig) => (
}, <span className="text-sm text-[#6b7280]">{desig.sort_order}</span>
{ ),
key: "user_count", },
label: "Users", {
render: (desig) => ( key: "user_count",
<span className="text-sm text-[#6b7280]">{desig.user_count || 0}</span> label: "Users",
), render: (desig) => (
}, <span className="text-sm text-[#6b7280]">
{ {desig.user_count || 0}
key: "status", </span>
label: "Status", ),
render: (desig) => ( },
<StatusBadge variant={desig.is_active ? "success" : "failure"}> {
{desig.is_active ? "Active" : "Inactive"} key: "status",
</StatusBadge> label: "Status",
), render: (desig) => (
}, <StatusBadge variant={desig.is_active ? "success" : "failure"}>
{ {desig.is_active ? "Active" : "Inactive"}
key: "actions", </StatusBadge>
label: "Actions", ),
align: "right", },
render: (desig) => ( {
<div className="flex justify-end"> key: "actions",
<ActionDropdown label: "Actions",
onView={() => { align: "right",
setSelectedDesignation(desig); render: (desig) => (
setIsViewModalOpen(true); <div className="flex justify-end">
}} <ActionDropdown
onEdit={ onView={() => {
canUpdate("designations") setSelectedDesignation(desig);
? () => { setIsViewModalOpen(true);
setSelectedDesignation(desig); }}
setIsEditModalOpen(true); onEdit={
} canUpdate("designations")
: undefined ? () => {
} setSelectedDesignation(desig);
/> setIsEditModalOpen(true);
</div> }
), : undefined
}, }
];
return (
<div
className={`flex flex-col gap-4 ${!compact ? "bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm" : ""}`}
>
{showHeader && (
<div className="p-4 border-b border-[rgba(0,0,0,0.08)] flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3 w-full sm:w-auto">
<SearchBox
value={searchQuery}
onChange={setSearchQuery}
placeholder="Search designations..."
/> />
</div>
),
},
];
return (
<div className={`flex flex-col gap-4 ${!compact ? "bg-white" : ""}`}>
{showHeader && (
<div className="pb-2 border-b border-[rgba(0,0,0,0.08)] flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3 w-full sm:w-auto">
<SearchBox
value={searchQuery}
onChange={setSearchQuery}
placeholder="Search designations..."
/>
</div>
<ActiveOnlyToggle <ActiveOnlyToggle
activeOnly={activeOnly} activeOnly={activeOnly}
onChange={(val) => setActiveOnly(val)} onChange={(val) => setActiveOnly(val)}
/> />
</div> </div>
{canCreate("designations") && ( )}
<PrimaryButton
size="default"
className="flex items-center gap-2 w-full sm:w-auto"
onClick={() => setIsNewModalOpen(true)}
>
<Plus className="w-4 h-4" />
<span>New Designation</span>
</PrimaryButton>
)}
</div>
)}
<DataTable <DataTable
data={paginatedData} data={paginatedData}
columns={columns} columns={columns}
keyExtractor={(desig) => desig.id} keyExtractor={(desig) => desig.id}
isLoading={isLoading} isLoading={isLoading}
error={error} error={error}
emptyMessage="No designations found" emptyMessage="No designations found"
/>
{totalItems > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
limit={limit}
onPageChange={setCurrentPage}
onLimitChange={(newLimit) => {
setLimit(newLimit);
setCurrentPage(1);
}}
/> />
)}
<NewDesignationModal {totalItems > 0 && (
isOpen={isNewModalOpen} <Pagination
onClose={() => setIsNewModalOpen(false)} currentPage={currentPage}
onSubmit={handleCreate} totalPages={totalPages}
isLoading={isActionLoading} totalItems={totalItems}
/> limit={limit}
onPageChange={setCurrentPage}
onLimitChange={(newLimit) => {
setLimit(newLimit);
setCurrentPage(1);
}}
/>
)}
<EditDesignationModal <NewDesignationModal
isOpen={isEditModalOpen} isOpen={isNewModalOpen}
onClose={() => { onClose={() => setIsNewModalOpen(false)}
setIsEditModalOpen(false); onSubmit={handleCreate}
setSelectedDesignation(null); isLoading={isActionLoading}
}} />
designation={selectedDesignation}
onSubmit={handleUpdate}
isLoading={isActionLoading}
/>
<ViewDesignationModal <EditDesignationModal
isOpen={isViewModalOpen} isOpen={isEditModalOpen}
onClose={() => { onClose={() => {
setIsViewModalOpen(false); setIsEditModalOpen(false);
setSelectedDesignation(null); setSelectedDesignation(null);
}} }}
designation={selectedDesignation} designation={selectedDesignation}
/> onSubmit={handleUpdate}
isLoading={isActionLoading}
/>
{/* <DeleteConfirmationModal <ViewDesignationModal
isOpen={isViewModalOpen}
onClose={() => {
setIsViewModalOpen(false);
setSelectedDesignation(null);
}}
designation={selectedDesignation}
/>
{/* <DeleteConfirmationModal
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
onClose={() => { onClose={() => {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
@ -345,8 +348,9 @@ const DesignationsTable = forwardRef<DesignationsTableRef, DesignationsTableProp
itemName={selectedDesignation?.name || ""} itemName={selectedDesignation?.name || ""}
isLoading={isActionLoading} isLoading={isActionLoading}
/> */} /> */}
</div> </div>
); );
}); },
);
export default DesignationsTable; export default DesignationsTable;

View File

@ -168,7 +168,7 @@ export const EditModuleModal = ({
</div> </div>
} }
> >
<div className="p-5"> <div>
{/* API Key Display Section (Only if webhookurl changed) */} {/* API Key Display Section (Only if webhookurl changed) */}
{apiKey && ( {apiKey && (
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md"> <div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">

View File

@ -339,7 +339,7 @@ export const NewModuleModal = ({
</> </>
} }
> >
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-5"> <form onSubmit={handleSubmit(handleFormSubmit)}>
{/* API Key Display Section */} {/* API Key Display Section */}
{apiKey && ( {apiKey && (
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md"> <div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">

View File

@ -193,7 +193,7 @@
// </> // </>
// } // }
// > // >
// <form onSubmit={handleSubmit(handleFormSubmit)} className="p-5"> // <form onSubmit={handleSubmit(handleFormSubmit)}>
// {/* General Error Display */} // {/* General Error Display */}
// {errors.root && ( // {errors.root && (
// <div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4"> // <div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">

View File

@ -240,7 +240,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
key: "name", key: "name",
label: "Name", label: "Name",
render: (role) => ( render: (role) => (
<span className="text-sm font-normal text-[#0f1724]"> <span className="">
{role.name} {role.name}
</span> </span>
), ),
@ -254,7 +254,23 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
key: "description", key: "description",
label: "Description", label: "Description",
render: (role) => ( render: (role) => (
<span className="text-sm font-normal text-[#6b7280]"> <span
style={{
display: "-webkit-box",
width: "auto",
maxWidth: "300px",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 1,
overflow: "hidden",
color: "var(--Global-Colors-Text-text-primary, #0F1724)",
textOverflow: "ellipsis",
fontFamily: "Figtree",
fontSize: "14px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "normal",
}}
>
{role.description || "N/A"} {role.description || "N/A"}
</span> </span>
), ),
@ -272,7 +288,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
key: "user_count", key: "user_count",
label: "Users", label: "Users",
render: (role) => ( render: (role) => (
<span className="text-sm font-normal text-[#0f1724]"> <span className="">
{role.user_count || 0} {role.user_count || 0}
</span> </span>
), ),
@ -281,7 +297,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
key: "created_at", key: "created_at",
label: "Created Date", label: "Created Date",
render: (role) => ( render: (role) => (
<span className="text-sm font-normal text-[#6b7280]"> <span className="">
{formatDate(role.created_at)} {formatDate(role.created_at)}
</span> </span>
), ),
@ -474,10 +490,10 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
return ( return (
<> <>
{/* Table Container */} {/* Table Container */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden"> <div className="overflow-hidden">
{/* Table Header with Filters */} {/* Table Header with Filters */}
{showHeader && ( {showHeader && (
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3"> <div className="pb-2.5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Filters */} {/* Filters */}
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{/* Global Search */} {/* Global Search */}
@ -519,14 +535,6 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
{/* Actions */} {/* Actions */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Export Button */}
{/* <button
type="button"
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
>
<Download className="w-3.5 h-3.5" />
<span>Export</span>
</button> */}
{/* New Role Button */} {/* New Role Button */}
{canCreate("roles") && ( {canCreate("roles") && (

File diff suppressed because it is too large Load Diff

View File

@ -86,7 +86,7 @@ export const ViewModuleModal = ({
</SecondaryButton> </SecondaryButton>
} }
> >
<div className="p-5"> <div>
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" /> <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />

View File

@ -82,7 +82,7 @@
// </SecondaryButton> // </SecondaryButton>
// } // }
// > // >
// <div className="p-5"> // <div>
// {isLoading && ( // {isLoading && (
// <div className="flex items-center justify-center py-12"> // <div className="flex items-center justify-center py-12">
// <Loader2 className="w-6 h-6 text-[#112868] animate-spin" /> // <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />

View File

@ -116,7 +116,7 @@ export const WebhookSyncModal = ({
</div> </div>
} }
> >
<div className="p-5"> <div>
{isLoading && ( {isLoading && (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" /> <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />

View File

@ -23,7 +23,7 @@ export const PromptTestCaseResultModal = ({
description="Review LLM testing details, latency, and token consumption." description="Review LLM testing details, latency, and token consumption."
maxWidth="2xl" maxWidth="2xl"
> >
<div className="p-6 flex flex-col gap-6 bg-slate-50/40 select-none"> <div className="flex flex-col gap-6 bg-slate-50/40 select-none">
{/* Performance & Usage Metrics Cards */} {/* Performance & Usage Metrics Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Provider Card */} {/* Provider Card */}

View File

@ -1,5 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Modal, DataTable, type Column, PrimaryButton, StatusBadge } from "@/components/shared"; import {
Modal,
DataTable,
type Column,
PrimaryButton,
StatusBadge,
} from "@/components/shared";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
import type { AIPrompt } from "@/types/ai"; import type { AIPrompt } from "@/types/ai";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
@ -61,7 +67,9 @@ export const PromptVersionsModal = ({
{ {
key: "version", key: "version",
label: "Version", label: "Version",
render: (row) => <span className="font-semibold text-gray-900">v{row.version}</span>, render: (row) => (
<span className="font-semibold text-gray-900">v{row.version}</span>
),
}, },
{ {
key: "status", key: "status",
@ -75,7 +83,9 @@ export const PromptVersionsModal = ({
{ {
key: "change_notes", key: "change_notes",
label: "Change Notes", label: "Change Notes",
render: (row) => <span className="text-xs text-gray-500">{row.change_notes}</span>, render: (row) => (
<span className="text-xs text-gray-500">{row.change_notes}</span>
),
}, },
// { // {
// key: "created_by_email", // key: "created_by_email",
@ -86,7 +96,9 @@ export const PromptVersionsModal = ({
key: "updated_at", key: "updated_at",
label: "Created At", label: "Created At",
render: (row) => ( render: (row) => (
<span className="text-xs text-gray-500">{formatDate(row.updated_at || row.created_at || "")}</span> <span className="text-xs text-gray-500">
{formatDate(row.updated_at || row.created_at || "")}
</span>
), ),
}, },
{ {
@ -107,7 +119,9 @@ export const PromptVersionsModal = ({
</PrimaryButton> </PrimaryButton>
)} )}
{row.version === prompt?.version && ( {row.version === prompt?.version && (
<span className="text-[11px] font-medium text-gray-400 py-1 px-2 border border-gray-100 rounded bg-gray-50">Current</span> <span className="text-[11px] font-medium text-gray-400 py-1 px-2 border border-gray-100 rounded bg-gray-50">
Current
</span>
)} )}
</div> </div>
), ),
@ -122,7 +136,7 @@ export const PromptVersionsModal = ({
description="View previous versions of this prompt and rollback if needed." description="View previous versions of this prompt and rollback if needed."
maxWidth="lg" maxWidth="lg"
> >
<div className="border-t border-gray-100"> <div className="mx-3">
<DataTable <DataTable
data={versions} data={versions}
columns={columns} columns={columns}

View File

@ -1,6 +1,7 @@
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import { Modal } from "@/components/shared"; import { Modal } from "@/components/shared";
import type { TenantAIConfig } from "@/types/ai"; import type { TenantAIConfig } from "@/types/ai";
import CodeBadge from "../shared/CodeBadge";
interface ViewAIProviderModalProps { interface ViewAIProviderModalProps {
isOpen: boolean; isOpen: boolean;
@ -19,7 +20,10 @@ export const ViewAIProviderModal = ({
if (!val) return []; if (!val) return [];
if (Array.isArray(val)) return val; if (Array.isArray(val)) return val;
if (typeof val === "string") { if (typeof val === "string") {
return val.split(",").map(s => s.trim()).filter(Boolean); return val
.split(",")
.map((s) => s.trim())
.filter(Boolean);
} }
return []; return [];
}; };
@ -32,14 +36,12 @@ export const ViewAIProviderModal = ({
description="View detailed settings for this AI Provider configuration." description="View detailed settings for this AI Provider configuration."
maxWidth="lg" maxWidth="lg"
> >
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4 text-sm select-none p-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4 text-sm select-none">
<div> <div>
<span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5"> <span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5">
Provider Provider
</span> </span>
<span className="text-slate-800 font-medium"> <span className="text-slate-800 font-medium">{config.provider}</span>
{config.provider}
</span>
</div> </div>
<div> <div>
@ -55,9 +57,7 @@ export const ViewAIProviderModal = ({
<span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5"> <span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5">
Config Type Config Type
</span> </span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-[11px] font-bold uppercase tracking-wider bg-blue-50 text-blue-600 border border-blue-100 mt-1"> <CodeBadge className="uppercase" label={config.config_type || "direct"} />
{config.config_type || "direct"}
</span>
</div> </div>
<div> <div>
@ -115,14 +115,16 @@ export const ViewAIProviderModal = ({
</span> </span>
{parseArray((config as any).custom_embedding_models).length > 0 ? ( {parseArray((config as any).custom_embedding_models).length > 0 ? (
<div className="flex flex-wrap gap-1.5 mt-1"> <div className="flex flex-wrap gap-1.5 mt-1">
{parseArray((config as any).custom_embedding_models).map((m: any, idx: any) => ( {parseArray((config as any).custom_embedding_models).map(
<span (m: any, idx: any) => (
key={idx} <span
className="px-2 py-0.5 bg-slate-50 text-slate-700 rounded text-xs font-medium border border-slate-200" key={idx}
> className="px-2 py-0.5 bg-slate-50 text-slate-700 rounded text-xs font-medium border border-slate-200"
{m} >
</span> {m}
))} </span>
),
)}
</div> </div>
) : ( ) : (
<span className="text-slate-800 font-medium"></span> <span className="text-slate-800 font-medium"></span>

View File

@ -15,8 +15,10 @@ import {
FilterDropdown, FilterDropdown,
Pagination, Pagination,
type Column, type Column,
DeleteConfirmationModal,
ActionDropdown,
} from '@/components/shared'; } from '@/components/shared';
import { Plus, Pencil, Trash2, Settings, Check, X, Building2, Search } from 'lucide-react'; import { Plus, Settings, Check, X, Building2, Search } from 'lucide-react';
import { auditLogService } from '@/services/audit-log-service'; import { auditLogService } from '@/services/audit-log-service';
import { moduleService } from '@/services/module-service'; import { moduleService } from '@/services/module-service';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -58,6 +60,11 @@ const AuditLogResourceTypes = (): ReactElement => {
const [isEditing, setIsEditing] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false);
const [selectedRT, setSelectedRT] = useState<ResourceType | null>(null); const [selectedRT, setSelectedRT] = useState<ResourceType | null>(null);
// Delete Confirmation State
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
const [rtToDelete, setRtToDelete] = useState<ResourceType | null>(null);
const [isDeleting, setIsDeleting] = useState<boolean>(false);
// Pagination & Filtering State // Pagination & Filtering State
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(10); const [limit, setLimit] = useState<number>(10);
@ -189,16 +196,26 @@ const AuditLogResourceTypes = (): ReactElement => {
} }
}; };
const handleDelete = async (id: string) => { const handleDeleteClick = (rt: ResourceType) => {
if (!window.confirm('Are you sure you want to delete this resource type?')) return; setRtToDelete(rt);
setIsDeleteModalOpen(true);
};
const onConfirmDelete = async () => {
if (!rtToDelete) return;
try { try {
const response = await auditLogService.deleteResourceType(id); setIsDeleting(true);
const response = await auditLogService.deleteResourceType(rtToDelete.id);
if (response.success) { if (response.success) {
toast.success('Resource type deleted successfully'); toast.success('Resource type deleted successfully');
setIsDeleteModalOpen(false);
fetchResourceTypes(); fetchResourceTypes();
} }
} catch (err) { } catch (err) {
toast.error('Failed to delete resource type'); toast.error('Failed to delete resource type');
} finally {
setIsDeleting(false);
setRtToDelete(null);
} }
}; };
@ -209,7 +226,7 @@ const AuditLogResourceTypes = (): ReactElement => {
render: (rt) => ( render: (rt) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="w-4 h-4 text-[#475569]" /> <Settings className="w-4 h-4 text-[#475569]" />
<span className="text-sm font-medium text-[#0f1724]">{rt.name}</span> <span className="">{rt.name}</span>
</div> </div>
), ),
}, },
@ -226,7 +243,7 @@ const AuditLogResourceTypes = (): ReactElement => {
key: 'module', key: 'module',
label: 'Associated Module', label: 'Associated Module',
render: (rt) => rt.type === 'MODULE' && rt.module_id ? ( render: (rt) => rt.type === 'MODULE' && rt.module_id ? (
<div className="flex items-center gap-1.5 text-sm text-[#475569]"> <div className="flex items-center gap-1.5">
<Building2 className="w-3.5 h-3.5" /> <Building2 className="w-3.5 h-3.5" />
<span>{rt.module?.name || 'Loading...'}</span> <span>{rt.module?.name || 'Loading...'}</span>
</div> </div>
@ -249,19 +266,11 @@ const AuditLogResourceTypes = (): ReactElement => {
label: 'Actions', label: 'Actions',
align: 'right', align: 'right',
render: (rt) => ( render: (rt) => (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2 pr-2">
<button <ActionDropdown
onClick={() => handleOpenModal(rt)} onEdit={() => handleOpenModal(rt)}
className="p-1.5 text-[#475569] hover:text-[#112868] hover:bg-gray-100 rounded-md transition-colors" onDelete={() => handleDeleteClick(rt)}
> />
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(rt.id)}
className="p-1.5 text-[#ef4444] hover:bg-red-50 rounded-md transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div> </div>
), ),
}, },
@ -284,9 +293,9 @@ const AuditLogResourceTypes = (): ReactElement => {
), ),
}} }}
> >
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-2">
{/* Filter Row */} {/* Filter Row */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 bg-white p-4 border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm"> <div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-3 w-full md:w-auto"> <div className="flex flex-wrap items-center gap-3 w-full md:w-auto">
{/* Search Input */} {/* Search Input */}
<div className="relative w-full md:w-64"> <div className="relative w-full md:w-64">
@ -367,7 +376,7 @@ const AuditLogResourceTypes = (): ReactElement => {
</div> </div>
</div> </div>
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm overflow-hidden"> <div className="overflow-hidden">
<DataTable <DataTable
columns={columns} columns={columns}
data={resourceTypes} data={resourceTypes}
@ -398,11 +407,11 @@ const AuditLogResourceTypes = (): ReactElement => {
title={isEditing ? 'Edit Resource Type' : 'Create Resource Type'} title={isEditing ? 'Edit Resource Type' : 'Create Resource Type'}
maxWidth="md" maxWidth="md"
footer={ footer={
<div className="flex gap-3 w-full"> <div className="flex gap-3">
<SecondaryButton <SecondaryButton
type="button" type="button"
onClick={() => setIsModalOpen(false)} onClick={() => setIsModalOpen(false)}
className="flex-1" // className="flex-1"
disabled={isSubmitting} disabled={isSubmitting}
> >
Cancel Cancel
@ -410,7 +419,7 @@ const AuditLogResourceTypes = (): ReactElement => {
<PrimaryButton <PrimaryButton
type="button" type="button"
onClick={handleSubmit(onFormSubmit as any)} onClick={handleSubmit(onFormSubmit as any)}
className="flex-1" // className="flex-1"
disabled={isSubmitting} disabled={isSubmitting}
> >
{isSubmitting ? 'Saving...' : (isEditing ? 'Save Changes' : 'Create Type')} {isSubmitting ? 'Saving...' : (isEditing ? 'Save Changes' : 'Create Type')}
@ -418,7 +427,7 @@ const AuditLogResourceTypes = (): ReactElement => {
</div> </div>
} }
> >
<form onSubmit={handleSubmit(onFormSubmit as any)} className="p-6 flex flex-col gap-1"> <form onSubmit={handleSubmit(onFormSubmit as any)} className="flex flex-col gap-1">
<FormField <FormField
label="Resource Name" label="Resource Name"
required required
@ -427,7 +436,7 @@ const AuditLogResourceTypes = (): ReactElement => {
{...register('name')} {...register('name')}
/> />
<div className="flex flex-col gap-2 pb-4"> <div className="flex flex-col gap-0.5 pb-4">
<label className="text-[13px] font-medium text-[#0e1b2a]"> <label className="text-[13px] font-medium text-[#0e1b2a]">
Type <span className="text-[#e02424]">*</span> Type <span className="text-[#e02424]">*</span>
</label> </label>
@ -485,6 +494,19 @@ const AuditLogResourceTypes = (): ReactElement => {
</div> </div>
</form> </form>
</Modal> </Modal>
<DeleteConfirmationModal
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
setRtToDelete(null);
}}
onConfirm={onConfirmDelete}
title="Delete Resource Type"
message="Are you sure you want to delete this resource type? This action cannot be undone."
itemName={rtToDelete?.name || ''}
isLoading={isDeleting}
/>
</Layout> </Layout>
); );
}; };

View File

@ -1,16 +1,20 @@
import { useState } from "react";
import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable"; import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
export default function FailedEmails() { export default function FailedEmails() {
const [resendAllButton, setResendAllButton] = useState<React.ReactNode>(null);
return ( return (
<Layout <Layout
currentPage="Failed Emails" currentPage="Failed Emails"
pageHeader={{ pageHeader={{
title: "Platform Failed Emails", title: "Platform Failed Emails",
description: "Global monitoring of all failed system email dispatches and automatic/manual retry logs across all tenants." description: "Global monitoring of all failed system email dispatches and automatic/manual retry logs across all tenants.",
action: resendAllButton
}} }}
> >
<FailedEmailsTable /> <FailedEmailsTable onRegisterResendAll={setResendAllButton} />
</Layout> </Layout>
); );
} }

View File

@ -227,7 +227,7 @@ const Modules = (): ReactElement => {
key: "description", key: "description",
label: "Description", label: "Description",
render: (module) => ( render: (module) => (
<span className="text-sm font-normal text-[#0f1724] line-clamp-1"> <span className="">
{module.description} {module.description}
</span> </span>
), ),
@ -236,7 +236,7 @@ const Modules = (): ReactElement => {
key: "version", key: "version",
label: "Version", label: "Version",
render: (module) => ( render: (module) => (
<span className="text-sm font-normal text-[#0f1724]"> <span className="">
{module.version} {module.version}
</span> </span>
), ),
@ -263,7 +263,7 @@ const Modules = (): ReactElement => {
key: "runtime_language", key: "runtime_language",
label: "Runtime", label: "Runtime",
render: (module) => ( render: (module) => (
<span className="text-sm font-normal text-[#0f1724]"> <span className="">
{module.runtime_language || "N/A"} {module.runtime_language || "N/A"}
</span> </span>
), ),
@ -272,7 +272,7 @@ const Modules = (): ReactElement => {
key: "created_at", key: "created_at",
label: "Registered Date", label: "Registered Date",
render: (module) => ( render: (module) => (
<span className="text-sm font-normal text-[#6b7280]"> <span className="">
{formatDate(module.created_at)} {formatDate(module.created_at)}
</span> </span>
), ),
@ -416,9 +416,9 @@ const Modules = (): ReactElement => {
}} }}
> >
{/* Table Container */} {/* Table Container */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden"> <div className="overflow-hidden">
{/* Table Header with Filters */} {/* Table Header with Filters */}
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3"> <div className="pb-2 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Filters */} {/* Filters */}
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{/* Global Search */} {/* Global Search */}

View File

@ -13,8 +13,9 @@ import {
FilterDropdown, FilterDropdown,
Pagination, Pagination,
type Column, type Column,
SearchBox,
} from '@/components/shared'; } from '@/components/shared';
import { Plus, Code, Search, X, Tag } from 'lucide-react'; import { Plus, Code, X, Tag } from 'lucide-react';
import { notificationService } from '@/services/notification-service'; import { notificationService } from '@/services/notification-service';
import { moduleService } from '@/services/module-service'; import { moduleService } from '@/services/module-service';
import { showToast } from '@/utils/toast'; import { showToast } from '@/utils/toast';
@ -334,19 +335,18 @@ const NotificationMaster = (): ReactElement => {
description: 'Manage notification categories and event codes across the platform.', description: 'Manage notification categories and event codes across the platform.',
}} }}
> >
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden flex flex-col min-h-[500px]"> <div className="overflow-hidden flex flex-col min-h-[500px]">
<div className="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/50 gap-4"> <div className="pb-2 flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
<div className="flex items-center gap-4 flex-1"> <div className="flex items-center gap-4 flex-1">
<div className="relative flex-1 max-w-sm"> <SearchBox
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" /> value={search}
<input onChange={(val) => {
type="text" setSearch(val);
placeholder="Search categories..." setCurrentPage(1);
className="w-full pl-9 pr-4 py-1.5 border rounded-md text-sm outline-none focus:ring-2 focus:ring-blue-500/20" }}
value={search} placeholder="Search categories..."
onChange={(e) => { setSearch(e.target.value); setCurrentPage(1); }} containerClassName="relative flex-1 max-w-sm"
/> />
</div>
<FilterDropdown <FilterDropdown
label="Module" label="Module"

View File

@ -12,8 +12,9 @@ import {
Pagination, Pagination,
FilterDropdown, FilterDropdown,
type Column, type Column,
SearchBox,
} from "@/components/shared"; } from "@/components/shared";
import { Plus, Search, Copy, CheckCheck } from "lucide-react"; import { Plus, Copy, CheckCheck } from "lucide-react";
import { notificationService } from "@/services/notification-service"; import { notificationService } from "@/services/notification-service";
import { moduleService } from "@/services/module-service"; import { moduleService } from "@/services/module-service";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
@ -347,22 +348,18 @@ const NotificationTemplateMaster = (): ReactElement => {
description: "Define default notification templates for all tenants.", description: "Define default notification templates for all tenants.",
}} }}
> >
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden flex flex-col min-h-[600px]"> <div className="overflow-hidden flex flex-col min-h-[600px]">
<div className="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/50 gap-4"> <div className="pb-2 flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
<div className="flex items-center gap-4 flex-1"> <div className="flex items-center gap-4 flex-1">
<div className="relative flex-1 max-w-sm"> <SearchBox
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" /> value={search}
<input onChange={(val) => {
type="text" setSearch(val);
placeholder="Search templates..." setCurrentPage(1);
className="w-full pl-9 pr-4 py-2 border rounded-md text-sm focus:ring-2 focus:ring-blue-500 outline-none transition-all" }}
value={search} placeholder="Search templates..."
onChange={(e) => { containerClassName="relative flex-1 max-w-sm"
setSearch(e.target.value); />
setCurrentPage(1);
}}
/>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-full"> <div className="w-full">

View File

@ -1,18 +1,21 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { Layout } from '@/components/layout/Layout'; import { Layout } from "@/components/layout/Layout";
import { import {
DataTable, DataTable,
Pagination, Pagination,
StatusBadge, StatusBadge,
ActionDropdown, ActionDropdown,
PrimaryButton, PrimaryButton,
DeleteConfirmationModal, DeleteConfirmationModal,
type Column type Column,
} from '@/components/shared'; } from "@/components/shared";
import { Plus, Server, Globe, Building } from 'lucide-react'; import { Plus, Globe, Building } from "lucide-react";
import { smtpConfigService, type SmtpConfig } from '@/services/smtp-config-service'; import {
import { SmtpConfigModal } from '@/components/superadmin/SmtpConfigModal'; smtpConfigService,
import { showToast } from '@/utils/toast'; type SmtpConfig,
} from "@/services/smtp-config-service";
import { SmtpConfigModal } from "@/components/superadmin/SmtpConfigModal";
import { showToast } from "@/utils/toast";
const SmtpConfigPage = () => { const SmtpConfigPage = () => {
const [configs, setConfigs] = useState<SmtpConfig[]>([]); const [configs, setConfigs] = useState<SmtpConfig[]>([]);
@ -32,16 +35,16 @@ const SmtpConfigPage = () => {
try { try {
const res = await smtpConfigService.listAll({ const res = await smtpConfigService.listAll({
offset: (currentPage - 1) * limit, offset: (currentPage - 1) * limit,
limit: limit limit: limit,
}); });
if (res.success) { if (res.success) {
setConfigs(res.data); setConfigs(res.data);
// Assuming the API would return total items if we had many, // Assuming the API would return total items if we had many,
// for now let's just use the length or a fixed number // for now let's just use the length or a fixed number
setTotalItems(res.data.length); setTotalItems(res.data.length);
} }
} catch (err) { } catch (err) {
showToast.error('Failed to load SMTP configurations'); showToast.error("Failed to load SMTP configurations");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -65,12 +68,15 @@ const SmtpConfigPage = () => {
if (!selectedConfig?.id) return; if (!selectedConfig?.id) return;
setIsDeleting(true); setIsDeleting(true);
try { try {
await smtpConfigService.deleteConfig(selectedConfig.id, selectedConfig.tenant_id); await smtpConfigService.deleteConfig(
showToast.success('Configuration deleted'); selectedConfig.id,
selectedConfig.tenant_id,
);
showToast.success("Configuration deleted");
setDeleteModalOpen(false); setDeleteModalOpen(false);
fetchConfigs(); fetchConfigs();
} catch (err) { } catch (err) {
showToast.error('Failed to delete configuration'); showToast.error("Failed to delete configuration");
} finally { } finally {
setIsDeleting(false); setIsDeleting(false);
} }
@ -78,78 +84,89 @@ const SmtpConfigPage = () => {
const columns: Column<SmtpConfig>[] = [ const columns: Column<SmtpConfig>[] = [
{ {
key: 'scope', key: "scope",
label: 'Scope', label: "Scope",
render: (config) => ( render: (config) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{config.scope === 'super_admin' ? ( {config.scope === "super_admin" ? (
<Globe className="w-4 h-4 text-blue-600" /> <Globe className="w-4 h-4 text-blue-600" />
) : ( ) : (
<Building className="w-4 h-4 text-purple-600" /> <Building className="w-4 h-4 text-purple-600" />
)} )}
<span className="text-sm font-medium"> <span>
{config.scope === 'super_admin' ? 'Global' : config.tenant_name || 'Tenant'} {config.scope === "super_admin"
? "Global"
: config.tenant_name || "Tenant"}
</span> </span>
</div> </div>
) ),
}, },
{ {
key: 'host', key: "host",
label: 'Server', label: "Server",
render: (config) => ( render: (config) => (
<div className="flex items-center gap-2"> <span>
<Server className="w-4 h-4 text-gray-400" /> {config.host}:{config.port}
<span className="text-sm">{config.host}:{config.port}</span> </span>
</div> ),
)
}, },
{ {
key: 'from_email', key: "from_email",
label: 'Sender', label: "Sender",
render: (config) => ( render: (config) => (
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium">{config.from_name || 'N/A'}</span> <span className="text-sm font-medium">
<span className="text-xs text-gray-500">{config.from_email || 'N/A'}</span> {config.from_name || "N/A"}
</span>
<span className="text-xs text-gray-500">
{config.from_email || "N/A"}
</span>
</div> </div>
) ),
}, },
{ {
key: 'is_active', key: "is_active",
label: 'Status', label: "Status",
render: (config) => ( render: (config) => (
<StatusBadge variant={config.is_active ? 'success' : 'failure'}> <StatusBadge variant={config.is_active ? "success" : "failure"}>
{config.is_active ? 'Active' : 'Inactive'} {config.is_active ? "Active" : "Inactive"}
</StatusBadge> </StatusBadge>
) ),
}, },
{ {
key: 'actions', key: "actions",
label: 'Actions', label: "Actions",
align: 'right', align: "right",
render: (config) => ( render: (config) => (
<ActionDropdown <ActionDropdown
onEdit={() => handleEdit(config)} onEdit={() => handleEdit(config)}
onDelete={() => handleDelete(config)} onDelete={() => handleDelete(config)}
/> />
) ),
} },
]; ];
return ( return (
<Layout <Layout
currentPage="Settings" currentPage="Settings"
pageHeader={{ pageHeader={{
title: 'SMTP Configurations', title: "SMTP Configurations",
description: 'Manage email delivery settings for the entire platform and individual tenants.', description:
"Manage email delivery settings for the entire platform and individual tenants.",
action: ( action: (
<PrimaryButton onClick={() => { setSelectedConfig(null); setIsModalOpen(true); }}> <PrimaryButton
onClick={() => {
setSelectedConfig(null);
setIsModalOpen(true);
}}
>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Configuration Add Configuration
</PrimaryButton> </PrimaryButton>
) ),
}} }}
> >
<div className="bg-white border border-gray-100 rounded-lg shadow-sm overflow-hidden"> <div className="overflow-hidden">
<DataTable <DataTable
columns={columns} columns={columns}
data={configs} data={configs}
@ -157,7 +174,7 @@ const SmtpConfigPage = () => {
keyExtractor={(item) => item.id!} keyExtractor={(item) => item.id!}
emptyMessage="No SMTP configurations found" emptyMessage="No SMTP configurations found"
/> />
{totalItems > limit && ( {totalItems > limit && (
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
@ -183,7 +200,11 @@ const SmtpConfigPage = () => {
onConfirm={confirmDelete} onConfirm={confirmDelete}
title="Delete SMTP Configuration" title="Delete SMTP Configuration"
message="Are you sure you want to delete this SMTP configuration? This action cannot be undone." message="Are you sure you want to delete this SMTP configuration? This action cannot be undone."
itemName={selectedConfig?.scope === 'super_admin' ? 'Global Config' : selectedConfig?.tenant_name || 'Tenant Config'} itemName={
selectedConfig?.scope === "super_admin"
? "Global Config"
: selectedConfig?.tenant_name || "Tenant Config"
}
isLoading={isDeleting} isLoading={isDeleting}
/> />
</Layout> </Layout>

View File

@ -235,7 +235,7 @@ const Tenants = (): ReactElement => {
// {getTenantInitials(tenant.name)} // {getTenantInitials(tenant.name)}
// </span> // </span>
// </div> // </div>
// <span className="text-sm font-normal text-[#0f1724]"> // <span className="">
// {tenant.name} // {tenant.name}
// </span> // </span>
// </div> // </div>
@ -255,7 +255,7 @@ const Tenants = (): ReactElement => {
key: "user_count", key: "user_count",
label: "Users", label: "Users",
render: (tenant) => ( render: (tenant) => (
<span className="text-sm font-normal text-[#0f1724]"> <span className="">
{tenant.user_count ?? 0} {tenant.user_count ?? 0}
</span> </span>
), ),
@ -264,7 +264,7 @@ const Tenants = (): ReactElement => {
key: "subscription_tier", key: "subscription_tier",
label: "Subscription Tier", label: "Subscription Tier",
render: (tenant) => ( render: (tenant) => (
<span className="text-sm font-normal text-[#0f1724]"> <span className="">
{formatSubscriptionTier(tenant.subscription_tier)} {formatSubscriptionTier(tenant.subscription_tier)}
</span> </span>
), ),
@ -273,7 +273,7 @@ const Tenants = (): ReactElement => {
key: "module_count", key: "module_count",
label: "Modules", label: "Modules",
render: (tenant) => ( render: (tenant) => (
<span className="text-sm font-normal text-[#0f1724]"> <span className="">
{tenant.module_count ?? 0} {tenant.module_count ?? 0}
</span> </span>
), ),
@ -282,7 +282,7 @@ const Tenants = (): ReactElement => {
key: "created_at", key: "created_at",
label: "Joined Date", label: "Joined Date",
render: (tenant) => ( render: (tenant) => (
<span className="text-sm font-normal text-[#6b7280]"> <span className="">
{formatDate(tenant.created_at)} {formatDate(tenant.created_at)}
</span> </span>
), ),
@ -373,9 +373,9 @@ const Tenants = (): ReactElement => {
}} }}
> >
{/* Table Container */} {/* Table Container */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden"> <div className="overflow-hidden">
{/* Table Header with Filters */} {/* Table Header with Filters */}
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3"> <div className="pb-2 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Search & Filters */} {/* Search & Filters */}
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto"> <div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
{/* Global Search */} {/* Global Search */}

View File

@ -477,9 +477,9 @@ const AuditLogs = ({
const content = ( const content = (
<> <>
{/* Table Container */} {/* Table Container */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden"> <div className="overflow-hidden">
{/* Table Header with Filters */} {/* Table Header with Filters */}
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col gap-4"> <div className="pb-2 flex flex-col gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Search and Filters */} {/* Search and Filters */}
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">

View File

@ -351,12 +351,8 @@ const CompletionHistory = (): ReactElement => {
}} }}
> >
<div className="space-y-5"> <div className="space-y-5">
<section className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden"> <section className="overflow-hidden">
<div className="p-4 md:p-5 border-b border-[rgba(0,0,0,0.08)]"> <div className="pb-2 border-b border-[#D1D5DB]">
{/* <h3 className="text-sm md:text-base font-semibold text-[#0f1724] mb-3">
Completion List
</h3> */}
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-3"> <div className="flex flex-col lg:flex-row lg:items-start justify-between gap-3">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
@ -436,7 +432,7 @@ const CompletionHistory = (): ReactElement => {
<button <button
type="button" type="button"
onClick={clearFilters} onClick={clearFilters}
className="text-sm font-medium text-[#6b7280] hover:text-[#0f1724] transition-colors self-start lg:self-center" className="text-[13px] font-medium text-[#6b7280] hover:text-[#94A3B8] transition-colors cursor-pointer"
> >
Clear filters Clear filters
</button> </button>

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ const TaskCard = ({ task }: { task: WorkflowTask }) => {
}; };
return ( return (
<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 flex-col items-start self-stretch p-3 gap-2.5 rounded-[6px] border border-[#D1D5DB] bg-[#F9F9F9] hover:border-[#9CA3AF] transition-colors group">
<div className="flex justify-between items-start self-stretch"> <div className="flex justify-between items-start self-stretch">
<span className="text-[10px] font-bold text-[#94A3B8] uppercase tracking-[0.05em] leading-none"> <span className="text-[10px] font-bold text-[#94A3B8] uppercase tracking-[0.05em] leading-none">
{task.entity.type} {task.entity.type}
@ -82,7 +82,7 @@ const TaskCard = ({ task }: { task: WorkflowTask }) => {
<button <button
onClick={handleView} onClick={handleView}
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" 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 cursor-pointer"
> >
View View
</button> </button>
@ -218,7 +218,7 @@ const Dashboard = (): ReactElement => {
</h2> </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 cursor-pointer"
style={{ color: primaryColor }} style={{ color: primaryColor }}
> >
View all View all

View File

@ -1,9 +1,12 @@
import { useRef, type ReactElement } from 'react'; import { useRef, type ReactElement } from "react";
import { Layout } from '@/components/layout/Layout'; import { Layout } from "@/components/layout/Layout";
import { DepartmentsTable, type DepartmentsTableRef } from '@/components/superadmin/DepartmentsTable'; import {
import { PrimaryButton } from '@/components/shared'; DepartmentsTable,
import { Plus } from 'lucide-react'; type DepartmentsTableRef,
import { usePermissions } from '@/hooks/usePermissions'; } from "@/components/superadmin/DepartmentsTable";
import { PrimaryButton } from "@/components/shared";
import { Plus } from "lucide-react";
import { usePermissions } from "@/hooks/usePermissions";
const Departments = (): ReactElement => { const Departments = (): ReactElement => {
const tableRef = useRef<DepartmentsTableRef>(null); const tableRef = useRef<DepartmentsTableRef>(null);
@ -13,8 +16,9 @@ const Departments = (): ReactElement => {
<Layout <Layout
currentPage="Departments" currentPage="Departments"
pageHeader={{ pageHeader={{
title: 'Department Management', title: "Department Management",
description: 'View and manage all departments within your organization.', description:
"View and manage all departments within your organization.",
action: canCreate("departments") ? ( action: canCreate("departments") ? (
<PrimaryButton <PrimaryButton
size="default" size="default"

View File

@ -1,17 +1,36 @@
import { type ReactElement } from 'react'; import { useRef, type ReactElement } from "react";
import { Layout } from '@/components/layout/Layout'; import { Layout } from "@/components/layout/Layout";
import { DesignationsTable } from '@/components/superadmin'; import {
DesignationsTable,
type DesignationsTableRef,
} from "@/components/superadmin";
import { PrimaryButton } from "@/components/shared";
import { Plus } from "lucide-react";
import { usePermissions } from "@/hooks/usePermissions";
const Designations = (): ReactElement => { const Designations = (): ReactElement => {
const tableRef = useRef<DesignationsTableRef>(null);
const { canCreate } = usePermissions();
return ( return (
<Layout <Layout
currentPage="Designations" currentPage="Designations"
pageHeader={{ pageHeader={{
title: 'Designation Management', title: "Designation Management",
description: 'View and manage all designations within your organization.', description: "View and manage all designations within your organization.",
action: canCreate("designations") ? (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => tableRef.current?.openNewModal()}
>
<Plus className="w-4 h-4" />
<span>New Designation</span>
</PrimaryButton>
) : null,
}} }}
> >
<DesignationsTable /> <DesignationsTable ref={tableRef} />
</Layout> </Layout>
); );
}; };

View File

@ -14,12 +14,14 @@ import {
ActionDropdown, ActionDropdown,
DeleteConfirmationModal, DeleteConfirmationModal,
type Column, type Column,
SecondaryButton,
} from "@/components/shared"; } from "@/components/shared";
import { documentService } from "@/services/document-service"; import { documentService } from "@/services/document-service";
import type { DocumentCategory } from "@/types/document"; import type { DocumentCategory } from "@/types/document";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
import { Plus, Eye, Edit, Trash2 } from "lucide-react"; import { Plus, Eye, Edit, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import CodeBadge from "@/components/shared/CodeBadge";
const categorySchema = z.object({ const categorySchema = z.object({
name: z.string().min(1, "Category name is required"), name: z.string().min(1, "Category name is required"),
@ -139,18 +141,12 @@ const DocumentCategories = (): ReactElement => {
{ {
key: "name", key: "name",
label: "Name", label: "Name",
render: (cat) => ( render: (cat) => <span className="">{cat.name}</span>,
<span className="text-[#0f1724] font-medium">{cat.name}</span>
),
}, },
{ {
key: "code", key: "code",
label: "Code", label: "Code",
render: (cat) => ( render: (cat) => <CodeBadge label={cat.code} />,
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-600 border border-blue-100">
{cat.code}
</span>
),
}, },
{ {
key: "review_frequency_months", key: "review_frequency_months",
@ -293,7 +289,7 @@ const DocumentCategories = (): ReactElement => {
}} }}
> >
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden"> <div className="overflow-hidden">
<DataTable <DataTable
data={categories} data={categories}
columns={columns} columns={columns}
@ -317,9 +313,34 @@ const DocumentCategories = (): ReactElement => {
: "Create Document Category" : "Create Document Category"
} }
maxWidth="lg" maxWidth="lg"
footer={
<>
<SecondaryButton
onClick={() => {
setIsModalOpen(false);
setEditingCategory(null);
}}
disabled={isSubmitting}
>
Cancel
</SecondaryButton>
<PrimaryButton
type="button"
onClick={handleSubmit(onFormSubmit)}
disabled={isSubmitting}
className="px-6"
>
{isSubmitting
? "Processing..."
: editingCategory
? "Update Category"
: "Create Category"}
</PrimaryButton>
</>
}
> >
<form onSubmit={handleSubmit(onFormSubmit)} className="p-6 space-y-5"> <form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
<p className="text-sm text-gray-500 -mt-2"> <p className="text-sm text-gray-500">
Add a document category with review, retention, and training Add a document category with review, retention, and training
requirements. requirements.
</p> </p>
@ -447,30 +468,6 @@ const DocumentCategories = (): ReactElement => {
/> />
</div> </div>
</div> </div>
<div className="flex justify-end gap-3 pt-4 border-t border-gray-100">
<button
type="button"
onClick={() => {
setIsModalOpen(false);
setEditingCategory(null);
}}
className="px-6 py-2 border border-gray-200 rounded-md text-sm font-bold text-[#0f1724] hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<PrimaryButton
type="submit"
disabled={isSubmitting}
className="px-6"
>
{isSubmitting
? "Processing..."
: editingCategory
? "Update Category"
: "Create Category"}
</PrimaryButton>
</div>
</form> </form>
</Modal> </Modal>
@ -484,7 +481,7 @@ const DocumentCategories = (): ReactElement => {
title="Document Category Details" title="Document Category Details"
maxWidth="lg" maxWidth="lg"
> >
<div className="p-6 space-y-6"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
<div> <div>
<label className="text-xs text-gray-500 uppercase font-bold tracking-wider"> <label className="text-xs text-gray-500 uppercase font-bold tracking-wider">

View File

@ -35,9 +35,9 @@ const Documents = (): ReactElement => {
const navigate = useNavigate(); const navigate = useNavigate();
const [documents, setDocuments] = useState<DocumentSummary[]>([]); const [documents, setDocuments] = useState<DocumentSummary[]>([]);
const [categories, setCategories] = useState<DocumentCategory[]>([]); const [categories, setCategories] = useState<DocumentCategory[]>([]);
const [statuses, setStatuses] = useState<Array<{ code: string; name: string }>>( const [statuses, setStatuses] = useState<
[], Array<{ code: string; name: string }>
); >([]);
const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]); const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<string | null>(null); const [statusFilter, setStatusFilter] = useState<string | null>(null);
@ -57,12 +57,13 @@ const Documents = (): ReactElement => {
useEffect(() => { useEffect(() => {
const loadDropdownData = async (): Promise<void> => { const loadDropdownData = async (): Promise<void> => {
try { try {
const [categoriesRes, statusesRes, typesRes, modulesRes] = await Promise.all([ const [categoriesRes, statusesRes, typesRes, modulesRes] =
documentService.getCategories(), await Promise.all([
documentService.getStatuses(), documentService.getCategories(),
documentService.getTypes(), documentService.getStatuses(),
moduleService.getAvailable(), documentService.getTypes(),
]); moduleService.getAvailable(),
]);
setCategories(categoriesRes.data || []); setCategories(categoriesRes.data || []);
setStatuses(statusesRes.data || []); setStatuses(statusesRes.data || []);
setTypes(typesRes.data || []); setTypes(typesRes.data || []);
@ -101,7 +102,15 @@ const Documents = (): ReactElement => {
}; };
void loadDocuments(); void loadDocuments();
}, [statusFilter, categoryFilter, typeFilter, moduleFilter, search, limit, offset]); }, [
statusFilter,
categoryFilter,
typeFilter,
moduleFilter,
search,
limit,
offset,
]);
const columns: Column<DocumentSummary>[] = useMemo( const columns: Column<DocumentSummary>[] = useMemo(
() => [ () => [
@ -111,7 +120,7 @@ const Documents = (): ReactElement => {
render: (doc) => ( render: (doc) => (
<button <button
type="button" type="button"
className="hover:underline transition-colors" className="hover:underline transition-colors cursor-pointer"
style={{ color: primaryColor }} style={{ color: primaryColor }}
onClick={() => navigate(`/tenant/documents/${doc.id}`)} onClick={() => navigate(`/tenant/documents/${doc.id}`)}
> >
@ -122,27 +131,32 @@ const Documents = (): ReactElement => {
{ {
key: "title", key: "title",
label: "Title", label: "Title",
render: (doc) => <span className="text-[#0f1724]">{doc.title}</span>, render: (doc) => <span className="">{doc.title}</span>,
}, },
{ {
key: "document_type", key: "document_type",
label: "Type", label: "Type",
render: (doc) => ( render: (doc) => (
<span className="text-[#0f1724]">{doc.document_type || "-"}</span> <span className="">{doc.document_type || "-"}</span>
), ),
}, },
{ {
key: "category", key: "category",
label: "Category", label: "Category",
render: (doc) => <span className="text-[#0f1724]">{doc.category || "-"}</span>, render: (doc) => (
<span className="">{doc.category || "-"}</span>
),
}, },
{ {
key: "status", key: "status",
label: "Status", label: "Status",
render: (doc) => ( render: (doc) => (
<span <span
className="inline-flex items-center rounded-md px-2 py-1 text-[11px]" className="inline-flex items-center rounded-md px-2 py-1 text-[11px]"
style={{ backgroundColor: `${primaryColor}1A`, color: primaryColor }} style={{
backgroundColor: `${primaryColor}1A`,
color: primaryColor,
}}
> >
{toLabel(doc.status)} {toLabel(doc.status)}
</span> </span>
@ -152,21 +166,23 @@ const Documents = (): ReactElement => {
key: "module_name", key: "module_name",
label: "Module", label: "Module",
render: (doc) => ( render: (doc) => (
<span className="text-[#0f1724]">{doc.module_name || "Platform"}</span> <span className="">
{doc.module_name || "Platform"}
</span>
), ),
}, },
{ {
key: "current_version", key: "current_version",
label: "Version", label: "Version",
render: (doc) => ( render: (doc) => (
<span className="text-[#0f1724]">{doc.current_version || "-"}</span> <span className="">{doc.current_version || "-"}</span>
), ),
}, },
{ {
key: "updated_at", key: "updated_at",
label: "Updated", label: "Updated",
render: (doc) => ( render: (doc) => (
<span className="text-[#6b7280]">{formatDate(doc.updated_at)}</span> <span className="">{formatDate(doc.updated_at)}</span>
), ),
}, },
{ {
@ -214,100 +230,99 @@ const Documents = (): ReactElement => {
), ),
}} }}
> >
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden"> <div className="overflow-hidden">
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-4"> <div className="flex flex-col md:flex-row items-center justify-between gap-3 pb-2">
<div className="flex flex-col md:flex-row items-center justify-between gap-4"> {/* Left side: Search and Filters */}
{/* Left side: Search and Filters */} <div className="flex flex-1 flex-wrap items-center gap-3">
<div className="flex flex-1 flex-wrap items-center gap-3"> {/* Search Bar */}
{/* Search Bar */} <div className="relative w-full max-w-[280px]">
<div className="relative w-full max-w-[280px]"> <div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-gray-400">
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-gray-400"> <Search className="w-4 h-4" />
<Search className="w-4 h-4" />
</div>
<input
type="text"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setCurrentPage(1);
}}
placeholder="Search by name, ID..."
className="h-10 w-full pl-9 pr-3 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-2 transition-all"
style={{
// @ts-ignore
'--tw-ring-color': `${primaryColor}33`,
borderColor: 'rgba(0,0,0,0.08)'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = primaryColor;
e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}1A`;
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = 'rgba(0,0,0,0.08)';
e.currentTarget.style.boxShadow = 'none';
}}
/>
</div> </div>
<input
type="text"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setCurrentPage(1);
}}
placeholder="Search by name, ID..."
className="h-10 w-full pl-9 pr-3 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-2 transition-all"
style={{
// @ts-ignore
"--tw-ring-color": `${primaryColor}33`,
borderColor: "rgba(0,0,0,0.08)",
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = primaryColor;
e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}1A`;
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = "rgba(0,0,0,0.08)";
e.currentTarget.style.boxShadow = "none";
}}
/>
</div>
{/* Filters */} {/* Filters */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FilterDropdown <FilterDropdown
label="Status" label="Status"
options={statuses.map((status) => ({ options={statuses.map((status) => ({
value: status.code, value: status.code,
label: status.name, label: status.name,
}))} }))}
value={statusFilter} value={statusFilter}
onChange={(value) => { onChange={(value) => {
setStatusFilter(value as string | null); setStatusFilter(value as string | null);
setCurrentPage(1); setCurrentPage(1);
}} }}
placeholder="All" placeholder="All"
/> />
<FilterDropdown
label="Category"
options={categories.map((category) => ({
value: category.id,
label: category.name,
}))}
value={categoryFilter}
onChange={(value) => {
setCategoryFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All"
/>
<FilterDropdown <FilterDropdown
label="Type" label="Category"
options={types.map((type) => ({ options={categories.map((category) => ({
value: type.code, value: category.id,
label: type.name, label: category.name,
}))} }))}
value={typeFilter} value={categoryFilter}
onChange={(value) => { onChange={(value) => {
setTypeFilter(value as string | null); setCategoryFilter(value as string | null);
setCurrentPage(1); setCurrentPage(1);
}} }}
placeholder="All" placeholder="All"
/> />
<FilterDropdown <FilterDropdown
label="Module" label="Type"
options={modules.map((module) => ({ options={types.map((type) => ({
value: module.id, value: type.code,
label: module.name, label: type.name,
}))} }))}
value={moduleFilter} value={typeFilter}
onChange={(value) => { onChange={(value) => {
setModuleFilter(value as string | null); setTypeFilter(value as string | null);
setCurrentPage(1); setCurrentPage(1);
}} }}
placeholder="All Modules" placeholder="All"
/> />
{/* <FilterDropdown <FilterDropdown
label="Module"
options={modules.map((module) => ({
value: module.id,
label: module.name,
}))}
value={moduleFilter}
onChange={(value) => {
setModuleFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All Modules"
/>
{/* <FilterDropdown
label="Priority" label="Priority"
options={[ options={[
{ value: "high", label: "High" }, { value: "high", label: "High" },
@ -330,26 +345,25 @@ const Documents = (): ReactElement => {
onChange={() => {}} onChange={() => {}}
placeholder="More" placeholder="More"
/> */} /> */}
</div>
</div> </div>
</div>
{/* Right side: Clear Filters */} {/* Right side: Clear Filters */}
<div className="flex items-center gap-4"> <div className="flex items-center">
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
setSearch(""); setSearch("");
setStatusFilter(null); setStatusFilter(null);
setCategoryFilter(null); setCategoryFilter(null);
setTypeFilter(null); setTypeFilter(null);
setModuleFilter(null); setModuleFilter(null);
setCurrentPage(1); setCurrentPage(1);
}} }}
className="text-sm font-medium text-[#6b7280] hover:text-[#0f1724] transition-colors" className="text-[13px] font-medium text-[#6b7280] hover:text-[#94A3B8] transition-colors cursor-pointer"
> >
Clear filters Clear filters
</button> </button>
</div>
</div> </div>
</div> </div>
@ -381,4 +395,3 @@ const Documents = (): ReactElement => {
}; };
export default Documents; export default Documents;

View File

@ -1,16 +1,20 @@
import { useState } from "react";
import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable"; import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
export default function FailedEmails() { export default function FailedEmails() {
const [resendAllButton, setResendAllButton] = useState<React.ReactNode>(null);
return ( return (
<Layout <Layout
currentPage="Failed Emails" currentPage="Failed Emails"
pageHeader={{ pageHeader={{
title: "Failed Emails Log", title: "Failed Emails Log",
description: "View and resend failed system email dispatches and transaction logs for this tenant." description: "View and resend failed system email dispatches and transaction logs for this tenant.",
action: resendAllButton
}} }}
> >
<FailedEmailsTable /> <FailedEmailsTable onRegisterResendAll={setResendAllButton} />
</Layout> </Layout>
); );
} }

View File

@ -580,9 +580,9 @@ const FilesList = (): ReactElement => {
) : null, ) : null,
}} }}
> >
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden"> <div className="overflow-hidden">
{/* Filter bar */} {/* Filter bar */}
<div className="border-b border-[rgba(0,0,0,0.08)] px-5 py-3.5"> <div className="border-b border-[rgba(0,0,0,0.08)] pb-2">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{/* Search */} {/* Search */}
<SearchBox <SearchBox

View File

@ -87,7 +87,7 @@ const Modules = (): ReactElement => {
key: 'module_id', key: 'module_id',
label: 'Module ID', label: 'Module ID',
render: (module) => ( render: (module) => (
<span className="text-sm font-normal text-[#0f1724] font-mono">{module.module_id}</span> <span className="">{module.module_id}</span>
), ),
mobileLabel: 'ID', mobileLabel: 'ID',
}, },
@ -95,14 +95,14 @@ const Modules = (): ReactElement => {
key: 'name', key: 'name',
label: 'Name', label: 'Name',
render: (module) => ( render: (module) => (
<span className="text-sm font-normal text-[#0f1724]">{module.name}</span> <span className="">{module.name}</span>
), ),
}, },
{ {
key: 'version', key: 'version',
label: 'Version', label: 'Version',
render: (module) => ( render: (module) => (
<span className="text-sm font-normal text-[#0f1724]">{module.version}</span> <span className="">{module.version}</span>
), ),
}, },
{ {
@ -118,7 +118,7 @@ const Modules = (): ReactElement => {
key: 'frontend_base_url', key: 'frontend_base_url',
label: 'Frontend URL', label: 'Frontend URL',
render: (module) => ( render: (module) => (
<span className="text-sm font-normal text-[#6b7280] font-mono truncate max-w-[200px]"> <span className="">
{module.frontend_base_url || 'N/A'} {module.frontend_base_url || 'N/A'}
</span> </span>
), ),
@ -194,7 +194,7 @@ const Modules = (): ReactElement => {
}} }}
> >
{/* Table Container */} {/* Table Container */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden w-full max-w-full"> <div className="overflow-hidden">
{/* Data Table */} {/* Data Table */}
<DataTable <DataTable
data={modules} data={modules}

View File

@ -214,7 +214,7 @@ const NotificationTemplates = (): ReactElement => {
{ {
key: "code", key: "code",
label: "Event", label: "Event",
render: (t) => <span className="text-sm font-semibold">{t.code}</span>, render: (t) => <span className="">{t.code}</span>,
}, },
{ {
key: "source", key: "source",
@ -229,7 +229,7 @@ const NotificationTemplates = (): ReactElement => {
key: "preview", key: "preview",
label: "Title Preview", label: "Title Preview",
render: (t) => ( render: (t) => (
<span className="text-xs truncate max-w-xs block text-gray-500"> <span className="">
{t.title_template} {t.title_template}
</span> </span>
), ),
@ -295,8 +295,8 @@ const NotificationTemplates = (): ReactElement => {
"Customize the content and delivery of platform notifications for your organization.", "Customize the content and delivery of platform notifications for your organization.",
}} }}
> >
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden flex flex-col min-h-[500px]"> <div className="overflow-hidden">
<div className="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/30 gap-4"> <div className="pb-2 flex flex-wrap justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Building className="w-4 h-4 text-blue-500" /> <Building className="w-4 h-4 text-blue-500" />
<h2 className="text-sm font-semibold text-gray-700"> <h2 className="text-sm font-semibold text-gray-700">
@ -304,7 +304,7 @@ const NotificationTemplates = (): ReactElement => {
</h2> </h2>
</div> </div>
<div className="flex items-center gap-3 min-w-[300px]"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-xs font-medium text-gray-400 mr-1"> <div className="flex items-center gap-2 text-xs font-medium text-gray-400 mr-1">
<Filter className="w-3.5 h-3.5" /> Filter by Module <Filter className="w-3.5 h-3.5" /> Filter by Module
</div> </div>

View File

@ -2,7 +2,6 @@ import { useEffect, useMemo, useState, type ReactElement } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
Plus, Plus,
Search,
Copy, Copy,
History, History,
Trash2, Trash2,
@ -17,6 +16,7 @@ import {
PrimaryButton, PrimaryButton,
ActionDropdown, ActionDropdown,
DeleteConfirmationModal, DeleteConfirmationModal,
SearchBox,
} from "@/components/shared"; } from "@/components/shared";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
import type { AIPrompt } from "@/types/ai"; import type { AIPrompt } from "@/types/ai";
@ -300,18 +300,14 @@ const PromptManagement = (): ReactElement => {
), ),
}} }}
> >
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden"> <div className="overflow-hidden">
<div className="p-4 border-b border-[rgba(0,0,0,0.06)] bg-gray-50/50"> <div className="pb-2">
<div className="relative w-full md:w-80"> <SearchBox
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#94a3b8]" /> value={search}
<input onChange={setSearch}
type="text" placeholder="Search by name or description..."
value={search} containerClassName="relative w-full md:w-80"
onChange={(e) => setSearch(e.target.value)} />
placeholder="Search by name or description..."
className="w-full pl-9 pr-4 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-lg text-sm transition-all focus:outline-none focus:ring-2 focus:ring-[#112868]/10 focus:border-[#112868]"
/>
</div>
</div> </div>
<DataTable <DataTable

View File

@ -1,11 +1,6 @@
import { useEffect, useMemo, useState, type ReactElement } from "react"; import { useEffect, useMemo, useState, type ReactElement } from "react";
import { useNavigate, useSearchParams, useParams } from "react-router-dom"; import { useNavigate, useSearchParams, useParams } from "react-router-dom";
import { import { Plus, Play, Eye, Loader2 } from "lucide-react";
Plus,
Play,
Eye,
Loader2,
} from "lucide-react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { import {
DataTable, DataTable,
@ -19,6 +14,7 @@ import { showToast } from "@/utils/toast";
import { formatDate } from "@/utils/format-date"; import { formatDate } from "@/utils/format-date";
import { PromptTestCaseResultModal } from "@/components/tenant/PromptTestCaseResultModal"; import { PromptTestCaseResultModal } from "@/components/tenant/PromptTestCaseResultModal";
import { PromptTestCaseResultsListModal } from "@/components/tenant/PromptTestCaseResultsListModal"; import { PromptTestCaseResultsListModal } from "@/components/tenant/PromptTestCaseResultsListModal";
import CodeBadge from "@/components/shared/CodeBadge";
const PromptTestCases = (): ReactElement => { const PromptTestCases = (): ReactElement => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -46,7 +42,8 @@ const PromptTestCases = (): ReactElement => {
const [selectedTestCaseId, setSelectedTestCaseId] = useState<string>(""); const [selectedTestCaseId, setSelectedTestCaseId] = useState<string>("");
const [selectedTestCaseName, setSelectedTestCaseName] = useState<string>(""); const [selectedTestCaseName, setSelectedTestCaseName] = useState<string>("");
const [isResultsListModalOpen, setIsResultsListModalOpen] = useState<boolean>(false); const [isResultsListModalOpen, setIsResultsListModalOpen] =
useState<boolean>(false);
const handleRunTestCase = async (testCaseId: string) => { const handleRunTestCase = async (testCaseId: string) => {
setRunningCases((prev) => ({ ...prev, [testCaseId]: true })); setRunningCases((prev) => ({ ...prev, [testCaseId]: true }));
@ -92,7 +89,8 @@ const PromptTestCases = (): ReactElement => {
} catch (err: unknown) { } catch (err: unknown) {
const message = const message =
(err as { response?: { data?: { error?: { message?: string } } } }) (err as { response?: { data?: { error?: { message?: string } } } })
?.response?.data?.error?.message || "Failed to load prompt test cases"; ?.response?.data?.error?.message ||
"Failed to load prompt test cases";
setError(message); setError(message);
showToast.error(message); showToast.error(message);
} finally { } finally {
@ -104,7 +102,6 @@ const PromptTestCases = (): ReactElement => {
void loadData(); void loadData();
}, [promptId]); }, [promptId]);
const columns: Column<AIPromptTestCase>[] = useMemo( const columns: Column<AIPromptTestCase>[] = useMemo(
() => [ () => [
{ {
@ -144,14 +141,9 @@ const PromptTestCases = (): ReactElement => {
return <span className="text-xs text-[#94a3b8]"></span>; return <span className="text-xs text-[#94a3b8]"></span>;
} }
return ( return (
<div className="flex flex-wrap gap-1.5 max-w-[150px]"> <div className="flex flex-wrap gap-1">
{tags.map((tag, index) => ( {tags.map((tag, index) => (
<span <CodeBadge key={index} label={tag} />
key={index}
className="inline-flex items-center px-2 py-0.5 rounded-md text-[11px] font-medium bg-blue-50 text-blue-600 border border-blue-100"
>
{tag}
</span>
))} ))}
</div> </div>
); );
@ -161,7 +153,7 @@ const PromptTestCases = (): ReactElement => {
key: "updatedAt", key: "updatedAt",
label: "Last Updated", label: "Last Updated",
render: (row) => ( render: (row) => (
<span className="text-xs text-[#64748b] select-none"> <span className="">
{formatDate(row.updated_at || row.created_at || "")} {formatDate(row.updated_at || row.created_at || "")}
</span> </span>
), ),
@ -220,10 +212,9 @@ const PromptTestCases = (): ReactElement => {
}, },
}, },
], ],
[prompt, testCases, runningCases] [prompt, testCases, runningCases],
); );
return ( return (
<Layout <Layout
currentPage="Prompt Management" currentPage="Prompt Management"
@ -236,7 +227,9 @@ const PromptTestCases = (): ReactElement => {
description: "Manage and execute test cases for your prompt templates.", description: "Manage and execute test cases for your prompt templates.",
action: ( action: (
<PrimaryButton <PrimaryButton
onClick={() => navigate(`/tenant/ai/prompts/${promptId}/test-cases/create`)} onClick={() =>
navigate(`/tenant/ai/prompts/${promptId}/test-cases/create`)
}
className="flex items-center gap-2 h-10 shadow-sm bg-[#112868] text-white hover:bg-[#112868]/90" className="flex items-center gap-2 h-10 shadow-sm bg-[#112868] text-white hover:bg-[#112868]/90"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
@ -246,8 +239,7 @@ const PromptTestCases = (): ReactElement => {
}} }}
> >
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<div className="overflow-hidden select-none">
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none">
<DataTable <DataTable
data={testCases} data={testCases}
columns={columns} columns={columns}
@ -266,19 +258,26 @@ const PromptTestCases = (): ReactElement => {
<span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none"> <span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none">
Input Variables Input Variables
</span> </span>
{item.input_variables && Object.keys(item.input_variables).length > 0 ? ( {item.input_variables &&
Object.keys(item.input_variables).length > 0 ? (
<div className="bg-white border border-slate-200/60 p-3 rounded-lg flex flex-col gap-2"> <div className="bg-white border border-slate-200/60 p-3 rounded-lg flex flex-col gap-2">
{Object.entries(item.input_variables).map(([key, value]) => ( {Object.entries(item.input_variables).map(
<div key={key} className="flex flex-col gap-0.5"> ([key, value]) => (
<span className="text-xs font-bold text-slate-600">{key}:</span> <div key={key} className="flex flex-col gap-0.5">
<span className="text-xs text-slate-700 font-mono bg-slate-50 p-1.5 rounded border border-slate-100/80 break-words whitespace-pre-wrap"> <span className="text-xs font-bold text-slate-600">
{String(value)} {key}:
</span> </span>
</div> <span className="text-xs text-slate-700 font-mono bg-slate-50 p-1.5 rounded border border-slate-100/80 break-words whitespace-pre-wrap">
))} {String(value)}
</span>
</div>
),
)}
</div> </div>
) : ( ) : (
<span className="text-slate-400 italic">No input variables defined.</span> <span className="text-slate-400 italic">
No input variables defined.
</span>
)} )}
</div> </div>
@ -291,7 +290,9 @@ const PromptTestCases = (): ReactElement => {
{item.expected_output} {item.expected_output}
</div> </div>
) : ( ) : (
<span className="text-slate-400 italic">No expected output defined.</span> <span className="text-slate-400 italic">
No expected output defined.
</span>
)} )}
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@ const Suppliers = (): ReactElement => {
}} }}
> >
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg bg-white overflow-hidden p-2 md:p-6"> {/* <div className="bg-white overflow-hidden"> */}
{/* <div className="flex flex-col gap-4"> */} {/* <div className="flex flex-col gap-4"> */}
{/* <div className="flex items-center justify-between"> {/* <div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-[#0f1724]"> <h2 className="text-lg font-semibold text-[#0f1724]">
@ -21,7 +21,7 @@ const Suppliers = (): ReactElement => {
</div> */} </div> */}
<SuppliersTable showHeader={true} compact={false} /> <SuppliersTable showHeader={true} compact={false} />
{/* </div> */} {/* </div> */}
</div> {/* </div> */}
</div> </div>
</Layout> </Layout>
); );

View File

@ -6,6 +6,7 @@ import {
Pagination, Pagination,
FilterDropdown, FilterDropdown,
type Column, type Column,
GradientStatCard,
} from "@/components/shared"; } from "@/components/shared";
import { workflowService } from "@/services/workflow-service"; import { workflowService } from "@/services/workflow-service";
import { moduleService } from "@/services/module-service"; import { moduleService } from "@/services/module-service";
@ -21,21 +22,11 @@ const formatDate = (value?: string | null): string => {
day: "numeric", day: "numeric",
year: "numeric", year: "numeric",
hour: "2-digit", hour: "2-digit",
minute: "2-digit" minute: "2-digit",
}); });
}; };
const StatCard = ({ icon: Icon, label, value, color, style }: { icon: any, label: string, value: number, color?: string, style?: React.CSSProperties }) => (
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl p-4 flex items-center gap-4 shadow-sm">
<div className="shrink-0">
<Icon className={cn("w-7 h-7", color)} style={style} />
</div>
<div>
<div className="text-2xl font-bold text-gray-900">{value}</div>
<div className="text-sm font-medium text-gray-500">{label}</div>
</div>
</div>
);
const Tasks = (): ReactElement => { const Tasks = (): ReactElement => {
const { primaryColor } = useAppTheme(); const { primaryColor } = useAppTheme();
@ -47,7 +38,7 @@ const Tasks = (): ReactElement => {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Filters // Filters
const [statusFilter, setStatusFilter] = useState<string | null>("pending"); const [statusFilter, setStatusFilter] = useState<string | null>("pending");
const [moduleFilter, setModuleFilter] = useState<string | null>(null); const [moduleFilter, setModuleFilter] = useState<string | null>(null);
@ -61,7 +52,7 @@ const Tasks = (): ReactElement => {
try { try {
const res = await moduleService.getMyModules(); const res = await moduleService.getMyModules();
if (res.success) { if (res.success) {
setModules(res.data.map(m => ({ id: m.id, name: m.name }))); setModules(res.data.map((m) => ({ id: m.id, name: m.name })));
} }
} catch (err) { } catch (err) {
console.error("Failed to load modules", err); console.error("Failed to load modules", err);
@ -75,20 +66,20 @@ const Tasks = (): ReactElement => {
try { try {
setIsLoading(true); setIsLoading(true);
const [tasksRes, countsRes] = await Promise.all([ const [tasksRes, countsRes] = await Promise.all([
workflowService.listTasks({ workflowService.listTasks({
limit, limit,
offset, offset,
status: statusFilter, status: statusFilter,
module_id: moduleFilter module_id: moduleFilter,
}), }),
workflowService.getTaskCounts({ module_id: moduleFilter }) workflowService.getTaskCounts({ module_id: moduleFilter }),
]); ]);
if (tasksRes.success) { if (tasksRes.success) {
setTasks(tasksRes.data); setTasks(tasksRes.data);
setTotal(tasksRes.pagination.total); setTotal(tasksRes.pagination.total);
} }
if (countsRes.success) { if (countsRes.success) {
setCounts(countsRes.data); setCounts(countsRes.data);
} }
@ -109,8 +100,12 @@ const Tasks = (): ReactElement => {
label: "Entity", label: "Entity",
render: (task) => ( render: (task) => (
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-medium text-gray-900">{task.entity.name}</span> <span className="font-medium text-gray-900">
<span className="text-[11px] text-gray-500 uppercase tracking-tight">{task.entity.type}</span> {task.entity.name}
</span>
<span className="text-[11px] text-gray-500 uppercase tracking-tight">
{task.entity.type}
</span>
</div> </div>
), ),
}, },
@ -118,7 +113,7 @@ const Tasks = (): ReactElement => {
key: "workflow_name", key: "workflow_name",
label: "Workflow", label: "Workflow",
render: (task) => ( render: (task) => (
<span className="text-gray-700">{task.workflow.name}</span> <span className="">{task.workflow.name}</span>
), ),
}, },
{ {
@ -126,9 +121,12 @@ const Tasks = (): ReactElement => {
label: "Step", label: "Step",
render: (task) => ( render: (task) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className="inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium" className="inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium"
style={{ backgroundColor: `${primaryColor}1A`, color: primaryColor }} style={{
backgroundColor: `${primaryColor}1A`,
color: primaryColor,
}}
> >
{task.step.name} {task.step.name}
</span> </span>
@ -142,8 +140,11 @@ const Tasks = (): ReactElement => {
const user = task.assignment.assigned_to_name; const user = task.assignment.assigned_to_name;
const roleIds = task.assignment.assigned_role_ids; const roleIds = task.assignment.assigned_role_ids;
return ( return (
<span className="text-gray-600"> <span className="">
{user || (roleIds && roleIds.length > 0 ? `${roleIds.length} roles` : "-")} {user ||
(roleIds && roleIds.length > 0
? `${roleIds.length} roles`
: "-")}
</span> </span>
); );
}, },
@ -152,15 +153,25 @@ const Tasks = (): ReactElement => {
key: "status", key: "status",
label: "Status", label: "Status",
render: (task) => { render: (task) => {
const isOverdueActive = task.is_overdue && !["completed", "rejected", "cancelled"].includes(task.status.toLowerCase()); const isOverdueActive =
task.is_overdue &&
!["completed", "rejected", "cancelled"].includes(
task.status.toLowerCase(),
);
return ( return (
<span className={cn( <span
"inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset", className={cn(
isOverdueActive "inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset",
? "bg-red-50 text-red-700 ring-red-600/10" isOverdueActive
: "bg-green-50 text-green-700 ring-green-600/10" ? "bg-red-50 text-red-700 ring-red-600/10"
)}> : "bg-green-50 text-green-700 ring-green-600/10",
{isOverdueActive ? "Overdue" : task.status.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())} )}
>
{isOverdueActive
? "Overdue"
: task.status
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase())}
</span> </span>
); );
}, },
@ -169,9 +180,17 @@ const Tasks = (): ReactElement => {
key: "due_at", key: "due_at",
label: "Due Date", label: "Due Date",
render: (task) => { render: (task) => {
const isOverdueActive = task.is_overdue && !["completed", "rejected", "cancelled"].includes(task.status.toLowerCase()); const isOverdueActive =
task.is_overdue &&
!["completed", "rejected", "cancelled"].includes(
task.status.toLowerCase(),
);
return ( return (
<span className={cn("text-sm", isOverdueActive ? "text-red-600 font-medium" : "text-gray-600")}> <span
className={cn(
isOverdueActive ? "text-red-600 font-medium" : "text-gray-600",
)}
>
{formatDate(task.due_at)} {formatDate(task.due_at)}
</span> </span>
); );
@ -183,11 +202,11 @@ const Tasks = (): ReactElement => {
render: (task) => ( render: (task) => (
<button <button
onClick={() => { onClick={() => {
if (task.entity.type.toLowerCase() === 'document') { if (task.entity.type.toLowerCase() === "document") {
navigate(`/tenant/documents/${task.entity.id}`); navigate(`/tenant/documents/${task.entity.id}`);
} }
}} }}
className="font-bold text-sm transition-colors hover:opacity-80" className="font-semibold text-sm transition-colors hover:opacity-80"
style={{ color: primaryColor }} style={{ color: primaryColor }}
> >
View View
@ -206,49 +225,44 @@ const Tasks = (): ReactElement => {
description: "Manage your pending workflow tasks and approvals.", description: "Manage your pending workflow tasks and approvals.",
}} }}
> >
<div className="flex flex-col gap-6 pb-8"> <div className="flex flex-col gap-3">
{/* Count Stats Area */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard <GradientStatCard
icon={Inbox} icon={Inbox}
label="Pending Tasks" label="Pending Tasks"
value={counts?.pending || 0} value={counts?.pending || 0}
style={{ color: primaryColor }}
/> />
<StatCard
icon={Clock} <GradientStatCard
label="Overdue" icon={Clock}
value={counts?.overdue || 0} label="Overdue"
color="text-red-500" value={counts?.overdue || 0}
/> />
<StatCard <GradientStatCard
icon={Calendar} icon={Calendar}
label="Due Soon" label="Due Soon"
value={counts?.due_soon || 0} value={counts?.due_soon || 0}
color="text-yellow-500"
/> />
<StatCard <GradientStatCard
icon={CheckCircle2} icon={CheckCircle2}
label="Completed (Week)" label="Completed (Week)"
value={counts?.completed_this_week || 0} value={counts?.completed_this_week || 0}
color="text-green-500"
/> />
</div> </div>
{/* Task Table Area */} {/* Task Table Area */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden"> <div className="overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex flex-col md:flex-row md:items-center justify-between gap-4"> <div className="pb-2 flex flex-col md:flex-row md:items-center justify-between gap-4">
<h3 className="text-lg font-bold text-gray-900"> <h3 className="text-lg font-bold text-gray-900">
{statusFilter {statusFilter
? `${statusFilter.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())} Tasks` ? `${statusFilter.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())} Tasks`
: "All Tasks" : "All Tasks"}
}
</h3> </h3>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<FilterDropdown <FilterDropdown
label="Module" label="Module"
options={modules.map(m => ({ value: m.id, label: m.name }))} options={modules.map((m) => ({ value: m.id, label: m.name }))}
value={moduleFilter} value={moduleFilter}
onChange={(val) => { onChange={(val) => {
setModuleFilter(val as string | null); setModuleFilter(val as string | null);
@ -289,7 +303,7 @@ const Tasks = (): ReactElement => {
)} )}
</div> </div>
</div> </div>
<DataTable <DataTable
data={tasks} data={tasks}
columns={columns} columns={columns}

View File

@ -9,6 +9,7 @@ import {
ActionDropdown, ActionDropdown,
PrimaryButton, PrimaryButton,
DeleteConfirmationModal, DeleteConfirmationModal,
StatusBadge,
} from "@/components/shared"; } from "@/components/shared";
import { Plus, Play, Loader2, Eye, Trash2 } from "lucide-react"; import { Plus, Play, Loader2, Eye, Trash2 } from "lucide-react";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
@ -16,6 +17,7 @@ import type { TenantAIConfig } from "@/types/ai";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
import { formatDate } from "@/utils/format-date"; import { formatDate } from "@/utils/format-date";
import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal"; import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal";
import CodeBadge from "@/components/shared/CodeBadge";
export const TenantAIProviders = (): ReactElement => { export const TenantAIProviders = (): ReactElement => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -25,8 +27,12 @@ export const TenantAIProviders = (): ReactElement => {
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
const [statusFilter, setStatusFilter] = useState<string>(""); const [statusFilter, setStatusFilter] = useState<string>("");
const [testingProviders, setTestingProviders] = useState<Record<string, boolean>>({}); const [testingProviders, setTestingProviders] = useState<
const [selectedConfig, setSelectedConfig] = useState<TenantAIConfig | null>(null); Record<string, boolean>
>({});
const [selectedConfig, setSelectedConfig] = useState<TenantAIConfig | null>(
null,
);
const [isViewModalOpen, setIsViewModalOpen] = useState<boolean>(false); const [isViewModalOpen, setIsViewModalOpen] = useState<boolean>(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
const [providerToDelete, setProviderToDelete] = useState<string | null>(null); const [providerToDelete, setProviderToDelete] = useState<string | null>(null);
@ -39,7 +45,8 @@ export const TenantAIProviders = (): ReactElement => {
const data = await aiService.listConfigs(); const data = await aiService.listConfigs();
setConfigs(data || []); setConfigs(data || []);
} catch (err: any) { } catch (err: any) {
const msg = err?.response?.data?.error?.message || "Failed to load configs"; const msg =
err?.response?.data?.error?.message || "Failed to load configs";
setError(msg); setError(msg);
showToast.error(msg); showToast.error(msg);
} finally { } finally {
@ -57,13 +64,14 @@ export const TenantAIProviders = (): ReactElement => {
const resp = await aiService.testConfig(provider); const resp = await aiService.testConfig(provider);
if (resp && resp.healthy) { if (resp && resp.healthy) {
showToast.success( showToast.success(
`Connection healthy for ${provider}! Latency: ${resp.latency_ms || "N/A"} ms` `Connection healthy for ${provider}! Latency: ${resp.latency_ms || "N/A"} ms`,
); );
} else { } else {
showToast.error(`Connection test failed for ${provider}.`); showToast.error(`Connection test failed for ${provider}.`);
} }
} catch (err: any) { } catch (err: any) {
const msg = err?.response?.data?.error?.message || "Failed to test connection."; const msg =
err?.response?.data?.error?.message || "Failed to test connection.";
showToast.error(msg); showToast.error(msg);
} finally { } finally {
setTestingProviders((prev) => ({ ...prev, [provider]: false })); setTestingProviders((prev) => ({ ...prev, [provider]: false }));
@ -76,7 +84,9 @@ export const TenantAIProviders = (): ReactElement => {
setSelectedConfig(cfg); setSelectedConfig(cfg);
setIsViewModalOpen(true); setIsViewModalOpen(true);
} catch (err: any) { } catch (err: any) {
const msg = err?.response?.data?.error?.message || "Failed to fetch AI Provider config details."; const msg =
err?.response?.data?.error?.message ||
"Failed to fetch AI Provider config details.";
showToast.error(msg); showToast.error(msg);
} }
}; };
@ -114,9 +124,15 @@ export const TenantAIProviders = (): ReactElement => {
return configs.filter((cfg) => { return configs.filter((cfg) => {
const searchMatches = const searchMatches =
!searchQuery.trim() || !searchQuery.trim() ||
(cfg.provider || "").toLowerCase().includes(searchQuery.toLowerCase()) || (cfg.provider || "")
(cfg.display_name || "").toLowerCase().includes(searchQuery.toLowerCase()) || .toLowerCase()
(cfg.default_model || "").toLowerCase().includes(searchQuery.toLowerCase()); .includes(searchQuery.toLowerCase()) ||
(cfg.display_name || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(cfg.default_model || "")
.toLowerCase()
.includes(searchQuery.toLowerCase());
const statusMatches = const statusMatches =
!statusFilter || !statusFilter ||
@ -149,24 +165,14 @@ export const TenantAIProviders = (): ReactElement => {
label: "Config Type", label: "Config Type",
render: (row) => { render: (row) => {
const type = row.config_type || "direct"; const type = row.config_type || "direct";
return ( return <CodeBadge className="uppercase" label={type} />;
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-md text-[11px] font-bold uppercase tracking-wider ${
type.toLowerCase() === "azure"
? "bg-purple-50 text-purple-600 border border-purple-100"
: "bg-blue-50 text-blue-600 border border-blue-100"
}`}
>
{type}
</span>
);
}, },
}, },
{ {
key: "default_model", key: "default_model",
label: "Default Model", label: "Default Model",
render: (row) => ( render: (row) => (
<span className="text-xs text-slate-800 font-medium select-none"> <span className="">
{row.default_model || "—"} {row.default_model || "—"}
</span> </span>
), ),
@ -174,31 +180,17 @@ export const TenantAIProviders = (): ReactElement => {
{ {
key: "is_active", key: "is_active",
label: "Status", label: "Status",
render: (row) => { render: (row) => (
const active = row.is_active; <StatusBadge variant={row.is_active ? "success" : "failure"}>
return ( {row.is_active ? "Active" : "Disabled"}
<span </StatusBadge>
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium w-fit ${ ),
active
? "text-green-700 bg-green-50 border border-green-100"
: "text-gray-500 bg-gray-50 border border-gray-100"
}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${
active ? "bg-green-500" : "bg-gray-400"
}`}
/>
{active ? "Active" : "Disabled"}
</span>
);
},
}, },
{ {
key: "last_verified_at", key: "last_verified_at",
label: "Last Verified", label: "Last Verified",
render: (row) => ( render: (row) => (
<span className="text-xs text-[#64748b]"> <span className="">
{row.last_verified_at ? formatDate(row.last_verified_at) : "Never"} {row.last_verified_at ? formatDate(row.last_verified_at) : "Never"}
</span> </span>
), ),
@ -232,7 +224,9 @@ export const TenantAIProviders = (): ReactElement => {
onClick: () => handleViewConfig(row.provider), onClick: () => handleViewConfig(row.provider),
}, },
{ {
icon: <Trash2 className="w-3.5 h-3.5 text-red-500 shrink-0" />, icon: (
<Trash2 className="w-3.5 h-3.5 text-red-500 shrink-0" />
),
label: "Delete Config", label: "Delete Config",
onClick: () => handleDeleteConfig(row.provider), onClick: () => handleDeleteConfig(row.provider),
}, },
@ -243,7 +237,7 @@ export const TenantAIProviders = (): ReactElement => {
}, },
}, },
], ],
[testingProviders] [testingProviders],
); );
return ( return (
@ -263,7 +257,7 @@ export const TenantAIProviders = (): ReactElement => {
), ),
}} }}
> >
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-2">
{/* Subhead Toolbar matching Screenshot filter design */} {/* Subhead Toolbar matching Screenshot filter design */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
@ -297,16 +291,14 @@ export const TenantAIProviders = (): ReactElement => {
</div> </div>
{/* Table list */} {/* Table list */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none"> <DataTable
<DataTable data={filteredConfigs}
data={filteredConfigs} columns={columns}
columns={columns} keyExtractor={(item) => item.id || item.provider}
keyExtractor={(item) => item.id || item.provider} isLoading={isLoading}
isLoading={isLoading} error={error}
error={error} emptyMessage="No tenant AI providers configured."
emptyMessage="No tenant AI providers configured." />
/>
</div>
</div> </div>
<ViewAIProviderModal <ViewAIProviderModal

View File

@ -1,17 +1,33 @@
import { type ReactElement } from "react"; import { useRef, type ReactElement } from "react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { UsersTable } from "@/components/superadmin"; import { UsersTable, type UsersTableRef } from "@/components/superadmin";
import { PrimaryButton } from "@/components/shared";
import { Plus } from "lucide-react";
import { usePermissions } from "@/hooks/usePermissions";
const Users = (): ReactElement => { const Users = (): ReactElement => {
const tableRef = useRef<UsersTableRef>(null);
const { canCreate } = usePermissions();
return ( return (
<Layout <Layout
currentPage="Users" currentPage="Users"
pageHeader={{ pageHeader={{
title: "User Management", title: "User List",
description: "View and manage all users within your organization.", description: "View and manage all users within your organization.",
action: canCreate("users") ? (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => tableRef.current?.openNewModal()}
>
<Plus className="w-4 h-4" />
<span>New User</span>
</PrimaryButton>
) : null,
}} }}
> >
<UsersTable showHeader={true} /> <UsersTable ref={tableRef} showHeader={true} />
</Layout> </Layout>
); );
}; };

View File

@ -1643,7 +1643,7 @@ const ViewDocument = (): ReactElement => {
</> </>
} }
> >
<div className="p-5 space-y-3"> <div className="space-y-4">
{activeAction === "submit" && ( {activeAction === "submit" && (
<div className="space-y-3"> <div className="space-y-3">
<FormSelect <FormSelect
@ -1786,7 +1786,7 @@ const ViewDocument = (): ReactElement => {
description="Track the progress of this document's approval workflow." description="Track the progress of this document's approval workflow."
maxWidth="2xl" maxWidth="2xl"
> >
<div className="p-6"> <div className="">
{isWorkflowLoading ? ( {isWorkflowLoading ? (
<div className="flex flex-col items-center justify-center py-10 space-y-3"> <div className="flex flex-col items-center justify-center py-10 space-y-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#112868]"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#112868]"></div>

View File

@ -1,24 +1,35 @@
import { type ReactElement } from "react"; import { useRef, type ReactElement } from "react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { WorkflowDefinitionsTable } from "@/components/shared"; import {
WorkflowDefinitionsTable,
type WorkflowDefinitionsTableRef,
PrimaryButton,
} from "@/components/shared";
import { Plus } from "lucide-react";
const WorkflowDefinationPage = (): ReactElement => { const WorkflowDefinationPage = (): ReactElement => {
const tableRef = useRef<WorkflowDefinitionsTableRef>(null);
return ( return (
<Layout <Layout
currentPage="Workflow Definitions" currentPage="Workflow Definitions"
// breadcrumbs={[ pageHeader={{
// // { label: "Platform", path: "/tenant" }, title: "Workflow Definitions",
// { label: "Workflow Definitions" }, description: "Create and manage document approval workflow definitions.",
// ]} action: (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => tableRef.current?.openNewModal()}
>
<Plus className="w-4 h-4" />
<span>New Workflow</span>
</PrimaryButton>
),
}}
> >
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <WorkflowDefinitionsTable ref={tableRef} compact={false} />
<h1 className="text-2xl font-bold text-[#0f1724] tracking-[-0.48px]">
Workflow Definitions
</h1>
</div>
<WorkflowDefinitionsTable compact={false} />
</div> </div>
</Layout> </Layout>
); );