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 (
<span
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
)}
>

View File

@ -8,6 +8,8 @@ export interface Column<T> {
render?: (item: T) => ReactNode;
align?: "left" | "right" | "center";
mobileLabel?: string;
width?: string;
className?: string;
}
interface DataTableProps<T> {
@ -76,9 +78,10 @@ export const DataTable = <T,>({
{/* Desktop Table Empty State */}
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
<div className="inline-block min-w-full align-middle">
<table className="w-full">
<table className="w-full table-auto">
<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 && (
<th
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-left";
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
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}
</th>
@ -129,12 +137,14 @@ export const DataTable = <T,>({
{/* Desktop Table */}
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
<div className="inline-block min-w-full align-middle">
<table className="w-full">
<table className="w-full table-auto">
<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 && (
<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"
/>
)}
@ -146,9 +156,14 @@ export const DataTable = <T,>({
? "text-center"
: "text-left";
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
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}
</th>
@ -163,11 +178,14 @@ export const DataTable = <T,>({
return (
<Fragment key={rowId}>
<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}
>
{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
type="button"
className="p-1 rounded hover:bg-gray-200/80 text-[#64748b]"
@ -191,9 +209,14 @@ export const DataTable = <T,>({
? "text-center"
: "text-left";
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
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(item)
@ -203,7 +226,7 @@ export const DataTable = <T,>({
})}
</tr>
{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}>
<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)}

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]"
>
{/* 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="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]" />
@ -94,7 +94,7 @@ export const DeleteConfirmationModal = ({
</div>
{/* Modal Body */}
<div className="p-5">
<div className='pb-4 px-5'>
<p className="text-sm text-[#6b7280] leading-relaxed">
{message}
{itemName && (
@ -106,7 +106,7 @@ export const DeleteConfirmationModal = ({
</div>
{/* 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
type="button"
onClick={onClose}

View File

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

View File

@ -95,7 +95,7 @@ export const NewDesignationModal = ({
>
<form
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">
<FormField
@ -232,7 +232,7 @@ export const EditDesignationModal = ({
>
<form
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">
<FormField
@ -316,7 +316,7 @@ export const ViewDesignationModal = ({
footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>}
>
{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">
<h3 className="text-sm font-semibold text-[#0f1724]">
Basic Information

View File

@ -465,7 +465,7 @@ export const EditRoleModal = ({
{!isLoadingRole && (
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-0"
className="flex flex-col gap-0"
>
{/* Role Name and Role Code Row */}
<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 && (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />

View File

@ -1,17 +1,24 @@
import React, { useState, useEffect } from 'react';
import { failedEmailsService, type FailedEmail } from '../../services/failed-emails-service';
import { DataTable, type Column } from './DataTable';
import { Modal } from './Modal';
import { Button } from '@/components/ui/button';
import { StatusBadge } from './StatusBadge';
import { Eye, RefreshCw, Trash2, Loader2 } from 'lucide-react';
import { format } from 'date-fns';
import { toast } from 'sonner';
import { Pagination } from './Pagination';
import { ActionDropdown } from './ActionDropdown';
import { PrimaryButton } from './PrimaryButton';
import React, { useState, useEffect } from "react";
import {
failedEmailsService,
type FailedEmail,
} from "../../services/failed-emails-service";
import { DataTable, type Column } from "./DataTable";
import { Modal } from "./Modal";
// import { Button } from "@/components/ui/button";
import { StatusBadge } from "./StatusBadge";
import { Eye, RefreshCw, Trash2, Loader2 } from "lucide-react";
import { format } from "date-fns";
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 [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0);
@ -28,7 +35,10 @@ export const FailedEmailsTable: React.FC = () => {
setLoading(true);
try {
const offset = (page - 1) * currentLimit;
const res = await failedEmailsService.getFailedEmails(currentLimit, offset);
const res = await failedEmailsService.getFailedEmails(
currentLimit,
offset,
);
setEmails(res.data || []);
setTotal(res.total || 0);
setCurrentPage(page);
@ -49,10 +59,10 @@ export const FailedEmailsTable: React.FC = () => {
setResendingId(id);
try {
await failedEmailsService.resendEmail(id);
toast.success('Email resent successfully.');
toast.success("Email resent successfully.");
fetchEmails(currentPage);
} catch (error: any) {
toast.error('Failed to resend email', { description: error.message });
toast.error("Failed to resend email", { description: error.message });
} finally {
setResendingId(null);
}
@ -65,19 +75,49 @@ export const FailedEmailsTable: React.FC = () => {
toast.success(res.message);
fetchEmails(currentPage);
} catch (error: any) {
toast.error('Failed to resend all emails', { description: error.message });
toast.error("Failed to resend all emails", {
description: error.message,
});
} finally {
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) => {
try {
await failedEmailsService.deleteEmail(id);
toast.success('Email deleted successfully.');
toast.success("Email deleted successfully.");
fetchEmails(currentPage);
} 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>[] = [
{
label: 'Date',
key: 'created_at',
render: (record: FailedEmail) => format(new Date(record.created_at), 'yyyy-MM-dd HH:mm:ss')
label: "Date",
key: "created_at",
render: (record: FailedEmail) =>
format(new Date(record.created_at), "yyyy-MM-dd HH:mm:ss"),
},
{
label: 'To',
key: 'to_email',
render: (record: FailedEmail) => record.to_email
label: "To",
key: "to_email",
render: (record: FailedEmail) => record.to_email,
},
{
label: 'Subject',
key: 'subject',
render: (record: FailedEmail) => record.subject
label: "Subject",
key: "subject",
render: (record: FailedEmail) => record.subject,
},
{
label: 'Status',
key: 'status',
label: "Status",
key: "status",
render: (record: FailedEmail) => (
<StatusBadge variant={record.status === 'failed' ? 'failure' : 'success'}>
<StatusBadge
variant={record.status === "failed" ? "failure" : "success"}
>
{record.status}
</StatusBadge>
)
),
},
{
label: 'Actions',
key: 'actions',
align: 'right',
label: "Actions",
key: "actions",
align: "right",
render: (record: FailedEmail) => (
<div className="flex justify-end">
<ActionDropdown
actions={[
{
label: 'View Details',
label: "View Details",
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),
icon: 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" />
)
}
icon:
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" />
),
},
]
: []),
{
label: 'Delete Email',
label: "Delete Email",
onClick: () => handleDelete(record.id),
icon: <Trash2 className="w-3.5 h-3.5 text-red-600" />,
variant: 'danger'
}
variant: "danger",
},
]}
/>
</div>
)
}
),
},
];
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 */}
<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">
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
<Button variant="outline" size="sm" onClick={() => fetchEmails(currentPage)}>
<RefreshCw className="mr-2 w-3.5 h-3.5" />
Refresh
</Button>
<PrimaryButton
size="small"
onClick={handleResendAll}
disabled={isResendingAll || emails.filter(e => e.status === 'failed').length === 0}
{!onRegisterResendAll && (
<div className="pb-2 flex justify-end">
{/* <div className="flex items-center gap-2 w-full sm:w-auto justify-end"> */}
{/* <Button variant="outline" onClick={() => fetchEmails(currentPage)}>
<RefreshCw className="mr-2 w-3.5 h-3.5" />
Refresh
</Button> */}
<PrimaryButton
onClick={handleResendAll}
disabled={
isResendingAll ||
emails.filter((e) => e.status === "failed").length === 0
}
>
{isResendingAll ? (
<>
@ -170,11 +220,12 @@ export const FailedEmailsTable: React.FC = () => {
Resending...
</>
) : (
'Resend All Failed'
"Resend All Failed"
)}
</PrimaryButton>
{/* </div> */}
</div>
</div>
)}
{/* Table Section */}
<DataTable
@ -185,7 +236,7 @@ export const FailedEmailsTable: React.FC = () => {
emptyMessage="No failed emails found"
error={error}
/>
{/* Pagination Footer */}
{total > limit && (
<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"
>
{selectedEmail && (
<div className="p-5">
<div>
<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><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>
<p>
<strong className="text-[#0f1724]">To:</strong>{" "}
<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 className="mt-4">
<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"
dangerouslySetInnerHTML={{ __html: selectedEmail.body }}
dangerouslySetInnerHTML={{ __html: selectedEmail.body }}
/>
</div>
</div>

View File

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

View File

@ -449,10 +449,10 @@ export const FileUploadModal = ({
maxWidth="md"
footer={footer}
>
<div className="px-6 py-5 space-y-5">
<div className="space-y-4">
{/* Drop Zone */}
<div>
<p className="text-[13px] font-medium text-[#0e1b2a] mb-2">Attach Files <span className="text-[#e02424]">*</span></p>
<div className="flex flex-col pb-4 gap-0.5">
<p className="text-[13px] font-medium text-[#0e1b2a]">Attach Files <span className="text-[#e02424]">*</span></p>
{fileEntries.length === 0 ? (
<div
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>
<p className="text-[13px] font-medium text-[#0e1b2a] mb-2">Select New File <span className="text-[#e02424]">*</span></p>
<div

View File

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

View File

@ -134,7 +134,7 @@ export const FormSelect = ({
};
return (
<div className="flex flex-col gap-2 pb-4">
<div className="flex flex-col gap-0.5 pb-4">
<label
htmlFor={fieldId}
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...",
}: FormTagInputProps): ReactElement => {
return (
<div className="flex flex-col gap-2 pb-1">
<div className="flex flex-col gap-0.5 pb-1">
{label && (
<label className="text-[13px] font-medium text-[#0e1b2a]">
{label}

View File

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

View File

@ -1,6 +1,6 @@
import React from 'react';
import type { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import React from "react";
import type { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface GradientStatCardProps {
icon: LucideIcon;
@ -8,16 +8,22 @@ interface GradientStatCardProps {
label: string;
badge?: {
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 (
<div
className="rounded-[8px] p-[1px] h-full"
<div
className="rounded-[8px] p-[1px] h-full"
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">
@ -26,14 +32,20 @@ export const GradientStatCard: React.FC<GradientStatCardProps> = ({ icon: Icon,
<Icon className="w-5 h-5 stroke-[1.8]" />
</div>
{badge && (
<div className={cn(
"px-2.5 py-1 rounded-full text-[12px] font-bold tracking-tight whitespace-nowrap",
(badge.variant === 'success' || badge.variant === 'green') ? "bg-[#f1fffb] text-[#16c784]" :
badge.variant === 'warning' ? "bg-[#fff5e5] text-[#fca004]" :
badge.variant === 'info' ? "bg-[#f0f9ff] text-[#0ea5e9]" :
badge.variant === 'error' ? "bg-[#fdf5f4] text-[#e0352a]" :
"bg-[#f3f4f6] text-[#6b7280]" // default / gray
)}>
<div
className={cn(
"px-2.5 py-1 rounded-full text-[12px] font-medium whitespace-nowrap capitalize leading-normal",
badge.variant === "success" || badge.variant === "green"
? "bg-[#f1fffb] text-[#16c784]"
: badge.variant === "warning"
? "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}
</div>
)}

View File

@ -94,11 +94,11 @@ export const Modal = ({
)}
>
{/* 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">
<h2 className="text-lg font-semibold text-[#0e1b2a]">{title}</h2>
<h2 className="text-lg font-semibold text-[#0F1724]">{title}</h2>
{description && (
<p className="text-sm font-normal text-[#9aa6b2]">{description}</p>
<p className="text-sm font-normal text-[#6B7280]">{description}</p>
)}
</div>
{showCloseButton && (
@ -114,11 +114,13 @@ export const Modal = ({
</div>
{/* 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 */}
{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}
</div>
)}

View File

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

View File

@ -60,7 +60,7 @@ export const PageHeader = ({
.sort((a, b) => b.path.length - a.path.length)[0]?.path ?? null;
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 */}
<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]">

View File

@ -207,7 +207,7 @@ export const PaginatedSelect = ({
};
return (
<div className="flex flex-col gap-2 pb-4">
<div className="flex flex-col gap-0.5 pb-4">
<label
htmlFor={fieldId}
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);
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 */}
<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'}
</div>
<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">
<button
ref={limitButtonRef}
type="button"
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>
<ChevronDown
@ -142,7 +142,7 @@ export const Pagination = ({
createPortal(
<div
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}
>
<ul className="py-1.5">
@ -177,7 +177,7 @@ export const Pagination = ({
type="button"
onClick={handlePrevious}
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" />
<span className="hidden sm:inline">Previous</span>
@ -191,7 +191,7 @@ export const Pagination = ({
type="button"
onClick={handleNext}
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 }}
onMouseEnter={(e) => {
if (currentPage < totalPages) {

View File

@ -1,31 +1,32 @@
import type { ReactElement, ButtonHTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { useAppTheme } from '@/hooks/useAppTheme';
import type { ReactElement, ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { useAppTheme } from "@/hooks/useAppTheme";
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: {
size: {
default: 'h-10',
small: 'h-8',
large: 'h-12',
default: "h-10",
small: "h-8",
large: "h-12",
},
variant: {
default: 'bg-[#112868] text-[#23dce1] hover:bg-[#23dce1] hover:text-[#112868]',
disabled: 'bg-[#112868] text-[#23dce1] opacity-50',
default: "",
disabled: "",
},
},
defaultVariants: {
size: 'default',
variant: 'default',
size: "default",
variant: "default",
},
}
},
);
interface PrimaryButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
extends
ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof primaryButtonVariants> {
children: React.ReactNode;
}
@ -38,34 +39,28 @@ export const PrimaryButton = ({
disabled,
...props
}: PrimaryButtonProps): ReactElement => {
const buttonVariant = disabled ? 'disabled' : variant || 'default';
const buttonVariant = disabled ? "disabled" : variant || "default";
const { primaryColor, secondaryColor } = useAppTheme();
return (
<button
className={cn(primaryButtonVariants({ size, variant: buttonVariant }), className)}
style={
buttonVariant === 'default'
? {
backgroundColor: primaryColor,
color: secondaryColor,
}
: buttonVariant === 'disabled'
? {
backgroundColor: primaryColor,
color: secondaryColor,
opacity: 0.5,
}
: undefined
}
className={cn(
primaryButtonVariants({ size, variant: buttonVariant }),
className,
)}
style={{
backgroundColor: primaryColor, // #112868
color: secondaryColor,
opacity: buttonVariant === "disabled" ? 0.5 : 1,
}}
onMouseEnter={(e) => {
if (buttonVariant === 'default' && !disabled) {
if (!disabled) {
e.currentTarget.style.backgroundColor = secondaryColor;
e.currentTarget.style.color = primaryColor;
}
}}
onMouseLeave={(e) => {
if (buttonVariant === 'default' && !disabled) {
if (!disabled) {
e.currentTarget.style.backgroundColor = primaryColor;
e.currentTarget.style.color = secondaryColor;
}

View File

@ -1,25 +1,26 @@
import type { ReactElement, ButtonHTMLAttributes } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { useAppTheme } from '@/hooks/useAppTheme';
import type { ReactElement, ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { useAppTheme } from "@/hooks/useAppTheme";
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: {
variant: {
default: 'bg-[#23dce1] text-[#112868] hover:bg-[#112868] hover:text-[#23dce1]',
disabled: 'bg-[#23dce1] text-[#112868] opacity-50',
default: "",
disabled: "",
},
},
defaultVariants: {
variant: 'default',
variant: "default",
},
}
},
);
interface SecondaryButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
extends
ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof secondaryButtonVariants> {
children: React.ReactNode;
}
@ -31,34 +32,28 @@ export const SecondaryButton = ({
disabled,
...props
}: SecondaryButtonProps): ReactElement => {
const buttonVariant = disabled ? 'disabled' : variant || 'default';
const buttonVariant = disabled ? "disabled" : variant || "default";
const { primaryColor, secondaryColor } = useAppTheme();
return (
<button
className={cn(secondaryButtonVariants({ variant: buttonVariant }), className)}
style={
buttonVariant === 'default'
? {
backgroundColor: secondaryColor,
color: primaryColor,
}
: buttonVariant === 'disabled'
? {
backgroundColor: secondaryColor,
color: primaryColor,
opacity: 0.5,
}
: undefined
}
className={cn(
secondaryButtonVariants({ variant: buttonVariant }),
className,
)}
style={{
backgroundColor: secondaryColor, // #112868
color: primaryColor,
opacity: buttonVariant === "disabled" ? 0.5 : 1,
}}
onMouseEnter={(e) => {
if (buttonVariant === 'default' && !disabled) {
if (!disabled) {
e.currentTarget.style.backgroundColor = primaryColor;
e.currentTarget.style.color = secondaryColor;
}
}}
onMouseLeave={(e) => {
if (buttonVariant === 'default' && !disabled) {
if (!disabled) {
e.currentTarget.style.backgroundColor = secondaryColor;
e.currentTarget.style.color = primaryColor;
}

View File

@ -324,7 +324,7 @@ export const SupplierModal = ({
}
maxWidth="lg"
footer={
<div className="p-3 flex justify-end gap-3">
<div className="flex justify-end gap-3">
<SecondaryButton
type="button"
onClick={onClose}
@ -348,7 +348,7 @@ export const SupplierModal = ({
{isLoading ? (
<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 */}
<div className="space-y-4">
<h4 className="text-sm font-semibold text-[#112868] border-b pb-2">

View File

@ -170,7 +170,7 @@ export const SuppliersTable = ({
key: "supplier_type",
label: "Type",
render: (supplier) => (
<span className="text-sm text-[#4b5563] capitalize">
<span className="">
{supplier.supplier_type.replace(/_/g, " ")}
</span>
),
@ -233,10 +233,10 @@ export const SuppliersTable = ({
];
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 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]" />
</div>
<div>
@ -269,7 +269,7 @@ export const SuppliersTable = ({
return (
<div className="flex flex-col gap-4">
{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">
<SearchBox
@ -307,7 +307,7 @@ export const SuppliersTable = ({
</div>
)}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden">
<div className="overflow-hidden">
<DataTable
columns={columns}
data={suppliers}

View File

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

View File

@ -89,13 +89,13 @@ export const ViewRoleModal = ({
)}
{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>
</div>
)}
{!isLoading && !error && role && (
<div className="p-5 flex flex-col gap-6">
<div className="flex flex-col gap-6">
{/* Basic Information */}
<div className="flex flex-col gap-4">
<h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3>

View File

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

View File

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

View File

@ -97,7 +97,7 @@ export const WorkflowDefinitionViewModal = ({
title="Workflow Definition"
maxWidth="xl"
>
<div className="p-6 min-h-[400px]">
<div className="">
{/* Loading */}
{isLoading && (
<div className="flex flex-col items-center justify-center h-64 gap-3">
@ -116,7 +116,7 @@ export const WorkflowDefinitionViewModal = ({
{/* Content */}
{definition && !isLoading && (
<div className="space-y-6">
<div className="space-y-4">
{/* ── 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>

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 {
PrimaryButton,
StatusBadge,
DataTable,
Pagination,
@ -13,7 +18,7 @@ import {
type Column,
ActionDropdown,
} 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 type { WorkflowDefinition } from "@/types/workflow";
import { showToast } from "@/utils/toast";
@ -21,6 +26,11 @@ import type { RootState } from "@/store/store";
import { formatDate } from "@/utils/format-date";
import CodeBadge from "./CodeBadge";
export interface WorkflowDefinitionsTableRef {
openNewModal: () => void;
refresh: () => void;
}
interface WorkflowDefinitionsTableProps {
tenantId?: string | null; // If provided, use this tenantId (Super Admin mode)
compact?: boolean; // Compact mode for tabs
@ -28,380 +38,393 @@ interface WorkflowDefinitionsTableProps {
entityType?: string; // Filter by entity type
}
const WorkflowDefinitionsTable = ({
tenantId: tenantId,
compact = false,
showHeader = true,
entityType,
}: WorkflowDefinitionsTableProps): ReactElement => {
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId);
const effectiveTenantId = tenantId || reduxTenantId || undefined;
const WorkflowDefinitionsTable = forwardRef<
WorkflowDefinitionsTableRef,
WorkflowDefinitionsTableProps
>(
(
{ tenantId: tenantId, compact = false, showHeader = true, entityType },
ref,
): ReactElement => {
const reduxTenantId = useSelector(
(state: RootState) => state.auth.tenantId,
);
const effectiveTenantId = tenantId || reduxTenantId || undefined;
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Pagination state
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(compact ? 10 : 10);
const [totalItems, setTotalItems] = useState<number>(0);
// Pagination state
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(compact ? 10 : 10);
const [totalItems, setTotalItems] = useState<number>(0);
// Filter state
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState<string>("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState<string>("");
// Filter state
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState<string>("");
const [debouncedSearchQuery, setDebouncedSearchQuery] =
useState<string>("");
// Modal states
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedDefinition, setSelectedDefinition] =
useState<WorkflowDefinition | null>(null);
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const [viewDefinitionId, setViewDefinitionId] = useState<string | null>(null);
const [isActionLoading, setIsActionLoading] = useState(false);
const fetchDefinitions = async () => {
try {
setIsLoading(true);
setError(null);
const response = await workflowService.listDefinitions({
tenantId: effectiveTenantId,
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>;
// Modal states
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedDefinition, setSelectedDefinition] =
useState<WorkflowDefinition | null>(null);
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const [viewDefinitionId, setViewDefinitionId] = useState<string | null>(
null,
);
const [isActionLoading, setIsActionLoading] = useState(false);
// Expose imperative methods
useImperativeHandle(ref, () => ({
openNewModal: () => {
setSelectedDefinition(null);
setIsModalOpen(true);
},
},
{
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>
),
},
];
refresh: () => {
fetchDefinitions();
},
}));
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 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,
)
}
const fetchDefinitions = async () => {
try {
setIsLoading(true);
setError(null);
const response = await workflowService.listDefinitions({
tenantId: effectiveTenantId,
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>;
},
},
{
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>
<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
data={definitions}
columns={columns}
keyExtractor={(wf) => wf.id}
isLoading={isLoading}
error={error}
emptyMessage="No workflow definitions found"
/>
return (
<div className={`flex flex-col gap-2`}>
{showHeader && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="flex items-center gap-3 w-full sm:w-auto">
<SearchBox
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 && (
<Pagination
currentPage={currentPage}
totalPages={Math.ceil(totalItems / limit)}
totalItems={totalItems}
limit={limit}
onPageChange={setCurrentPage}
onLimitChange={(newLimit) => {
setLimit(newLimit);
setCurrentPage(1);
}}
<DataTable
data={definitions}
columns={columns}
keyExtractor={(wf) => wf.id}
isLoading={isLoading}
error={error}
emptyMessage="No workflow definitions found"
/>
)}
<DeleteConfirmationModal
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
setSelectedDefinition(null);
}}
onConfirm={handleDelete}
title="Delete Workflow Definition"
message="Are you sure you want to delete this workflow definition? This action cannot be undone."
itemName={selectedDefinition?.name || ""}
isLoading={isActionLoading}
/>
{totalItems > 0 && (
<Pagination
currentPage={currentPage}
totalPages={Math.ceil(totalItems / limit)}
totalItems={totalItems}
limit={limit}
onPageChange={setCurrentPage}
onLimitChange={(newLimit) => {
setLimit(newLimit);
setCurrentPage(1);
}}
/>
)}
<WorkflowDefinitionModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedDefinition(null);
}}
definition={selectedDefinition}
tenantId={effectiveTenantId}
onSuccess={fetchDefinitions}
initialEntityType={entityType}
/>
<DeleteConfirmationModal
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
setSelectedDefinition(null);
}}
onConfirm={handleDelete}
title="Delete Workflow Definition"
message="Are you sure you want to delete this workflow definition? This action cannot be undone."
itemName={selectedDefinition?.name || ""}
isLoading={isActionLoading}
/>
<WorkflowDefinitionViewModal
isOpen={isViewModalOpen}
onClose={() => {
setIsViewModalOpen(false);
setViewDefinitionId(null);
}}
definitionId={viewDefinitionId}
tenantId={effectiveTenantId}
/>
</div>
);
};
<WorkflowDefinitionModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedDefinition(null);
}}
definition={selectedDefinition}
tenantId={effectiveTenantId}
onSuccess={fetchDefinitions}
initialEntityType={entityType}
/>
<WorkflowDefinitionViewModal
isOpen={isViewModalOpen}
onClose={() => {
setIsViewModalOpen(false);
setViewDefinitionId(null);
}}
definitionId={viewDefinitionId}
tenantId={effectiveTenantId}
/>
</div>
);
},
);
export default WorkflowDefinitionsTable;

View File

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

View File

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

View File

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

View File

@ -250,7 +250,7 @@ export const DepartmentTreeView = ({
}
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) => (
<TreeItem
key={item.id}

View File

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

View File

@ -168,7 +168,7 @@ export const EditModuleModal = ({
</div>
}
>
<div className="p-5">
<div>
{/* API Key Display Section (Only if webhookurl changed) */}
{apiKey && (
<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 */}
{apiKey && (
<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 */}
// {errors.root && (
// <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",
label: "Name",
render: (role) => (
<span className="text-sm font-normal text-[#0f1724]">
<span className="">
{role.name}
</span>
),
@ -254,7 +254,23 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
key: "description",
label: "Description",
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"}
</span>
),
@ -272,7 +288,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
key: "user_count",
label: "Users",
render: (role) => (
<span className="text-sm font-normal text-[#0f1724]">
<span className="">
{role.user_count || 0}
</span>
),
@ -281,7 +297,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
key: "created_at",
label: "Created Date",
render: (role) => (
<span className="text-sm font-normal text-[#6b7280]">
<span className="">
{formatDate(role.created_at)}
</span>
),
@ -474,10 +490,10 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
return (
<>
{/* 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 */}
{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 */}
<div className="flex flex-wrap items-center gap-3">
{/* Global Search */}
@ -519,14 +535,6 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
{/* Actions */}
<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 */}
{canCreate("roles") && (

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -116,7 +116,7 @@ export const WebhookSyncModal = ({
</div>
}
>
<div className="p-5">
<div>
{isLoading && (
<div className="flex items-center justify-center py-8">
<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."
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 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Provider Card */}

View File

@ -1,5 +1,11 @@
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 type { AIPrompt } from "@/types/ai";
import { showToast } from "@/utils/toast";
@ -61,7 +67,9 @@ export const PromptVersionsModal = ({
{
key: "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",
@ -75,7 +83,9 @@ export const PromptVersionsModal = ({
{
key: "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",
@ -86,7 +96,9 @@ export const PromptVersionsModal = ({
key: "updated_at",
label: "Created At",
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>
)}
{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>
),
@ -122,7 +136,7 @@ export const PromptVersionsModal = ({
description="View previous versions of this prompt and rollback if needed."
maxWidth="lg"
>
<div className="border-t border-gray-100">
<div className="mx-3">
<DataTable
data={versions}
columns={columns}

View File

@ -1,6 +1,7 @@
import type { ReactElement } from "react";
import { Modal } from "@/components/shared";
import type { TenantAIConfig } from "@/types/ai";
import CodeBadge from "../shared/CodeBadge";
interface ViewAIProviderModalProps {
isOpen: boolean;
@ -19,7 +20,10 @@ export const ViewAIProviderModal = ({
if (!val) return [];
if (Array.isArray(val)) return val;
if (typeof val === "string") {
return val.split(",").map(s => s.trim()).filter(Boolean);
return val
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
return [];
};
@ -32,14 +36,12 @@ export const ViewAIProviderModal = ({
description="View detailed settings for this AI Provider configuration."
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>
<span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5">
Provider
</span>
<span className="text-slate-800 font-medium">
{config.provider}
</span>
<span className="text-slate-800 font-medium">{config.provider}</span>
</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">
Config Type
</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">
{config.config_type || "direct"}
</span>
<CodeBadge className="uppercase" label={config.config_type || "direct"} />
</div>
<div>
@ -115,14 +115,16 @@ export const ViewAIProviderModal = ({
</span>
{parseArray((config as any).custom_embedding_models).length > 0 ? (
<div className="flex flex-wrap gap-1.5 mt-1">
{parseArray((config as any).custom_embedding_models).map((m: any, idx: any) => (
<span
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>
))}
{parseArray((config as any).custom_embedding_models).map(
(m: any, idx: any) => (
<span
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>
),
)}
</div>
) : (
<span className="text-slate-800 font-medium"></span>

View File

@ -15,8 +15,10 @@ import {
FilterDropdown,
Pagination,
type Column,
DeleteConfirmationModal,
ActionDropdown,
} 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 { moduleService } from '@/services/module-service';
import { toast } from 'sonner';
@ -58,6 +60,11 @@ const AuditLogResourceTypes = (): ReactElement => {
const [isEditing, setIsEditing] = useState<boolean>(false);
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
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(10);
@ -189,16 +196,26 @@ const AuditLogResourceTypes = (): ReactElement => {
}
};
const handleDelete = async (id: string) => {
if (!window.confirm('Are you sure you want to delete this resource type?')) return;
const handleDeleteClick = (rt: ResourceType) => {
setRtToDelete(rt);
setIsDeleteModalOpen(true);
};
const onConfirmDelete = async () => {
if (!rtToDelete) return;
try {
const response = await auditLogService.deleteResourceType(id);
setIsDeleting(true);
const response = await auditLogService.deleteResourceType(rtToDelete.id);
if (response.success) {
toast.success('Resource type deleted successfully');
setIsDeleteModalOpen(false);
fetchResourceTypes();
}
} catch (err) {
toast.error('Failed to delete resource type');
} finally {
setIsDeleting(false);
setRtToDelete(null);
}
};
@ -209,7 +226,7 @@ const AuditLogResourceTypes = (): ReactElement => {
render: (rt) => (
<div className="flex items-center gap-2">
<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>
),
},
@ -226,7 +243,7 @@ const AuditLogResourceTypes = (): ReactElement => {
key: 'module',
label: 'Associated Module',
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" />
<span>{rt.module?.name || 'Loading...'}</span>
</div>
@ -249,19 +266,11 @@ const AuditLogResourceTypes = (): ReactElement => {
label: 'Actions',
align: 'right',
render: (rt) => (
<div className="flex justify-end gap-2">
<button
onClick={() => handleOpenModal(rt)}
className="p-1.5 text-[#475569] hover:text-[#112868] hover:bg-gray-100 rounded-md transition-colors"
>
<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 className="flex justify-end gap-2 pr-2">
<ActionDropdown
onEdit={() => handleOpenModal(rt)}
onDelete={() => handleDeleteClick(rt)}
/>
</div>
),
},
@ -284,9 +293,9 @@ const AuditLogResourceTypes = (): ReactElement => {
),
}}
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
{/* 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">
{/* Search Input */}
<div className="relative w-full md:w-64">
@ -367,7 +376,7 @@ const AuditLogResourceTypes = (): ReactElement => {
</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
columns={columns}
data={resourceTypes}
@ -398,11 +407,11 @@ const AuditLogResourceTypes = (): ReactElement => {
title={isEditing ? 'Edit Resource Type' : 'Create Resource Type'}
maxWidth="md"
footer={
<div className="flex gap-3 w-full">
<div className="flex gap-3">
<SecondaryButton
type="button"
onClick={() => setIsModalOpen(false)}
className="flex-1"
// className="flex-1"
disabled={isSubmitting}
>
Cancel
@ -410,7 +419,7 @@ const AuditLogResourceTypes = (): ReactElement => {
<PrimaryButton
type="button"
onClick={handleSubmit(onFormSubmit as any)}
className="flex-1"
// className="flex-1"
disabled={isSubmitting}
>
{isSubmitting ? 'Saving...' : (isEditing ? 'Save Changes' : 'Create Type')}
@ -418,7 +427,7 @@ const AuditLogResourceTypes = (): ReactElement => {
</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
label="Resource Name"
required
@ -427,7 +436,7 @@ const AuditLogResourceTypes = (): ReactElement => {
{...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]">
Type <span className="text-[#e02424]">*</span>
</label>
@ -485,6 +494,19 @@ const AuditLogResourceTypes = (): ReactElement => {
</div>
</form>
</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>
);
};

View File

@ -1,16 +1,20 @@
import { useState } from "react";
import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable";
import { Layout } from "@/components/layout/Layout";
export default function FailedEmails() {
const [resendAllButton, setResendAllButton] = useState<React.ReactNode>(null);
return (
<Layout
currentPage="Failed Emails"
pageHeader={{
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>
);
}

View File

@ -227,7 +227,7 @@ const Modules = (): ReactElement => {
key: "description",
label: "Description",
render: (module) => (
<span className="text-sm font-normal text-[#0f1724] line-clamp-1">
<span className="">
{module.description}
</span>
),
@ -236,7 +236,7 @@ const Modules = (): ReactElement => {
key: "version",
label: "Version",
render: (module) => (
<span className="text-sm font-normal text-[#0f1724]">
<span className="">
{module.version}
</span>
),
@ -263,7 +263,7 @@ const Modules = (): ReactElement => {
key: "runtime_language",
label: "Runtime",
render: (module) => (
<span className="text-sm font-normal text-[#0f1724]">
<span className="">
{module.runtime_language || "N/A"}
</span>
),
@ -272,7 +272,7 @@ const Modules = (): ReactElement => {
key: "created_at",
label: "Registered Date",
render: (module) => (
<span className="text-sm font-normal text-[#6b7280]">
<span className="">
{formatDate(module.created_at)}
</span>
),
@ -416,9 +416,9 @@ const Modules = (): ReactElement => {
}}
>
{/* 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 */}
<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 */}
<div className="flex flex-wrap items-center gap-3">
{/* Global Search */}

View File

@ -13,8 +13,9 @@ import {
FilterDropdown,
Pagination,
type Column,
SearchBox,
} 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 { moduleService } from '@/services/module-service';
import { showToast } from '@/utils/toast';
@ -334,19 +335,18 @@ const NotificationMaster = (): ReactElement => {
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="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
<div className="overflow-hidden flex flex-col min-h-[500px]">
<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="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search categories..."
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}
onChange={(e) => { setSearch(e.target.value); setCurrentPage(1); }}
/>
</div>
<SearchBox
value={search}
onChange={(val) => {
setSearch(val);
setCurrentPage(1);
}}
placeholder="Search categories..."
containerClassName="relative flex-1 max-w-sm"
/>
<FilterDropdown
label="Module"

View File

@ -12,8 +12,9 @@ import {
Pagination,
FilterDropdown,
type Column,
SearchBox,
} 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 { moduleService } from "@/services/module-service";
import { showToast } from "@/utils/toast";
@ -347,22 +348,18 @@ const NotificationTemplateMaster = (): ReactElement => {
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="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
<div className="overflow-hidden flex flex-col min-h-[600px]">
<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="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search templates..."
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}
onChange={(e) => {
setSearch(e.target.value);
setCurrentPage(1);
}}
/>
</div>
<SearchBox
value={search}
onChange={(val) => {
setSearch(val);
setCurrentPage(1);
}}
placeholder="Search templates..."
containerClassName="relative flex-1 max-w-sm"
/>
<div className="flex items-center gap-2">
<div className="w-full">

View File

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

View File

@ -235,7 +235,7 @@ const Tenants = (): ReactElement => {
// {getTenantInitials(tenant.name)}
// </span>
// </div>
// <span className="text-sm font-normal text-[#0f1724]">
// <span className="">
// {tenant.name}
// </span>
// </div>
@ -255,7 +255,7 @@ const Tenants = (): ReactElement => {
key: "user_count",
label: "Users",
render: (tenant) => (
<span className="text-sm font-normal text-[#0f1724]">
<span className="">
{tenant.user_count ?? 0}
</span>
),
@ -264,7 +264,7 @@ const Tenants = (): ReactElement => {
key: "subscription_tier",
label: "Subscription Tier",
render: (tenant) => (
<span className="text-sm font-normal text-[#0f1724]">
<span className="">
{formatSubscriptionTier(tenant.subscription_tier)}
</span>
),
@ -273,7 +273,7 @@ const Tenants = (): ReactElement => {
key: "module_count",
label: "Modules",
render: (tenant) => (
<span className="text-sm font-normal text-[#0f1724]">
<span className="">
{tenant.module_count ?? 0}
</span>
),
@ -282,7 +282,7 @@ const Tenants = (): ReactElement => {
key: "created_at",
label: "Joined Date",
render: (tenant) => (
<span className="text-sm font-normal text-[#6b7280]">
<span className="">
{formatDate(tenant.created_at)}
</span>
),
@ -373,9 +373,9 @@ const Tenants = (): ReactElement => {
}}
>
{/* 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 */}
<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 */}
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
{/* Global Search */}

View File

@ -477,9 +477,9 @@ const AuditLogs = ({
const content = (
<>
{/* 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 */}
<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">
{/* Search and Filters */}
<div className="flex flex-wrap items-center gap-3">

View File

@ -351,12 +351,8 @@ const CompletionHistory = (): ReactElement => {
}}
>
<div className="space-y-5">
<section className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden">
<div className="p-4 md:p-5 border-b border-[rgba(0,0,0,0.08)]">
{/* <h3 className="text-sm md:text-base font-semibold text-[#0f1724] mb-3">
Completion List
</h3> */}
<section className="overflow-hidden">
<div className="pb-2 border-b border-[#D1D5DB]">
<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-wrap items-center gap-2">
@ -436,7 +432,7 @@ const CompletionHistory = (): ReactElement => {
<button
type="button"
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
</button>

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ const TaskCard = ({ task }: { task: WorkflowTask }) => {
};
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">
<span className="text-[10px] font-bold text-[#94A3B8] uppercase tracking-[0.05em] leading-none">
{task.entity.type}
@ -82,7 +82,7 @@ const TaskCard = ({ task }: { task: WorkflowTask }) => {
<button
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
</button>
@ -218,7 +218,7 @@ const Dashboard = (): ReactElement => {
</h2>
<button
onClick={() => navigate("/tenant/workflows/tasks")}
className="text-[11px] font-bold hover:underline"
className="text-[11px] font-bold hover:underline cursor-pointer"
style={{ color: primaryColor }}
>
View all

View File

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

View File

@ -1,17 +1,36 @@
import { type ReactElement } from 'react';
import { Layout } from '@/components/layout/Layout';
import { DesignationsTable } from '@/components/superadmin';
import { useRef, type ReactElement } from "react";
import { Layout } from "@/components/layout/Layout";
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 tableRef = useRef<DesignationsTableRef>(null);
const { canCreate } = usePermissions();
return (
<Layout
currentPage="Designations"
pageHeader={{
title: 'Designation Management',
description: 'View and manage all designations within your organization.',
title: "Designation Management",
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>
);
};

View File

@ -14,12 +14,14 @@ import {
ActionDropdown,
DeleteConfirmationModal,
type Column,
SecondaryButton,
} from "@/components/shared";
import { documentService } from "@/services/document-service";
import type { DocumentCategory } from "@/types/document";
import { showToast } from "@/utils/toast";
import { Plus, Eye, Edit, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import CodeBadge from "@/components/shared/CodeBadge";
const categorySchema = z.object({
name: z.string().min(1, "Category name is required"),
@ -139,18 +141,12 @@ const DocumentCategories = (): ReactElement => {
{
key: "name",
label: "Name",
render: (cat) => (
<span className="text-[#0f1724] font-medium">{cat.name}</span>
),
render: (cat) => <span className="">{cat.name}</span>,
},
{
key: "code",
label: "Code",
render: (cat) => (
<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>
),
render: (cat) => <CodeBadge label={cat.code} />,
},
{
key: "review_frequency_months",
@ -293,7 +289,7 @@ const DocumentCategories = (): ReactElement => {
}}
>
<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
data={categories}
columns={columns}
@ -317,9 +313,34 @@ const DocumentCategories = (): ReactElement => {
: "Create Document Category"
}
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">
<p className="text-sm text-gray-500 -mt-2">
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
<p className="text-sm text-gray-500">
Add a document category with review, retention, and training
requirements.
</p>
@ -447,30 +468,6 @@ const DocumentCategories = (): ReactElement => {
/>
</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>
</Modal>
@ -484,7 +481,7 @@ const DocumentCategories = (): ReactElement => {
title="Document Category Details"
maxWidth="lg"
>
<div className="p-6 space-y-6">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-6">
<div>
<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 [documents, setDocuments] = useState<DocumentSummary[]>([]);
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 [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<string | null>(null);
@ -57,12 +57,13 @@ const Documents = (): ReactElement => {
useEffect(() => {
const loadDropdownData = async (): Promise<void> => {
try {
const [categoriesRes, statusesRes, typesRes, modulesRes] = await Promise.all([
documentService.getCategories(),
documentService.getStatuses(),
documentService.getTypes(),
moduleService.getAvailable(),
]);
const [categoriesRes, statusesRes, typesRes, modulesRes] =
await Promise.all([
documentService.getCategories(),
documentService.getStatuses(),
documentService.getTypes(),
moduleService.getAvailable(),
]);
setCategories(categoriesRes.data || []);
setStatuses(statusesRes.data || []);
setTypes(typesRes.data || []);
@ -101,7 +102,15 @@ const Documents = (): ReactElement => {
};
void loadDocuments();
}, [statusFilter, categoryFilter, typeFilter, moduleFilter, search, limit, offset]);
}, [
statusFilter,
categoryFilter,
typeFilter,
moduleFilter,
search,
limit,
offset,
]);
const columns: Column<DocumentSummary>[] = useMemo(
() => [
@ -111,7 +120,7 @@ const Documents = (): ReactElement => {
render: (doc) => (
<button
type="button"
className="hover:underline transition-colors"
className="hover:underline transition-colors cursor-pointer"
style={{ color: primaryColor }}
onClick={() => navigate(`/tenant/documents/${doc.id}`)}
>
@ -122,27 +131,32 @@ const Documents = (): ReactElement => {
{
key: "title",
label: "Title",
render: (doc) => <span className="text-[#0f1724]">{doc.title}</span>,
render: (doc) => <span className="">{doc.title}</span>,
},
{
key: "document_type",
label: "Type",
render: (doc) => (
<span className="text-[#0f1724]">{doc.document_type || "-"}</span>
<span className="">{doc.document_type || "-"}</span>
),
},
{
key: "category",
label: "Category",
render: (doc) => <span className="text-[#0f1724]">{doc.category || "-"}</span>,
render: (doc) => (
<span className="">{doc.category || "-"}</span>
),
},
{
key: "status",
label: "Status",
render: (doc) => (
<span
<span
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)}
</span>
@ -152,21 +166,23 @@ const Documents = (): ReactElement => {
key: "module_name",
label: "Module",
render: (doc) => (
<span className="text-[#0f1724]">{doc.module_name || "Platform"}</span>
<span className="">
{doc.module_name || "Platform"}
</span>
),
},
{
key: "current_version",
label: "Version",
render: (doc) => (
<span className="text-[#0f1724]">{doc.current_version || "-"}</span>
<span className="">{doc.current_version || "-"}</span>
),
},
{
key: "updated_at",
label: "Updated",
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="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-4">
{/* Left side: Search and Filters */}
<div className="flex flex-1 flex-wrap items-center gap-3">
{/* Search Bar */}
<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">
<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 className="overflow-hidden">
<div className="flex flex-col md:flex-row items-center justify-between gap-3 pb-2">
{/* Left side: Search and Filters */}
<div className="flex flex-1 flex-wrap items-center gap-3">
{/* Search Bar */}
<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">
<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>
{/* Filters */}
<div className="flex items-center gap-2">
<FilterDropdown
label="Status"
options={statuses.map((status) => ({
value: status.code,
label: status.name,
}))}
value={statusFilter}
onChange={(value) => {
setStatusFilter(value as string | null);
setCurrentPage(1);
}}
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"
/>
{/* Filters */}
<div className="flex items-center gap-2">
<FilterDropdown
label="Status"
options={statuses.map((status) => ({
value: status.code,
label: status.name,
}))}
value={statusFilter}
onChange={(value) => {
setStatusFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All"
/>
<FilterDropdown
label="Type"
options={types.map((type) => ({
value: type.code,
label: type.name,
}))}
value={typeFilter}
onChange={(value) => {
setTypeFilter(value as string | null);
setCurrentPage(1);
}}
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
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="Type"
options={types.map((type) => ({
value: type.code,
label: type.name,
}))}
value={typeFilter}
onChange={(value) => {
setTypeFilter(value as string | null);
setCurrentPage(1);
}}
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"
options={[
{ value: "high", label: "High" },
@ -330,26 +345,25 @@ const Documents = (): ReactElement => {
onChange={() => {}}
placeholder="More"
/> */}
</div>
</div>
</div>
{/* Right side: Clear Filters */}
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => {
setSearch("");
setStatusFilter(null);
setCategoryFilter(null);
setTypeFilter(null);
setModuleFilter(null);
setCurrentPage(1);
}}
className="text-sm font-medium text-[#6b7280] hover:text-[#0f1724] transition-colors"
>
Clear filters
</button>
</div>
{/* Right side: Clear Filters */}
<div className="flex items-center">
<button
type="button"
onClick={() => {
setSearch("");
setStatusFilter(null);
setCategoryFilter(null);
setTypeFilter(null);
setModuleFilter(null);
setCurrentPage(1);
}}
className="text-[13px] font-medium text-[#6b7280] hover:text-[#94A3B8] transition-colors cursor-pointer"
>
Clear filters
</button>
</div>
</div>
@ -381,4 +395,3 @@ const Documents = (): ReactElement => {
};
export default Documents;

View File

@ -1,16 +1,20 @@
import { useState } from "react";
import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable";
import { Layout } from "@/components/layout/Layout";
export default function FailedEmails() {
const [resendAllButton, setResendAllButton] = useState<React.ReactNode>(null);
return (
<Layout
currentPage="Failed Emails"
pageHeader={{
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>
);
}

View File

@ -580,9 +580,9 @@ const FilesList = (): ReactElement => {
) : 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 */}
<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">
{/* Search */}
<SearchBox

View File

@ -87,7 +87,7 @@ const Modules = (): ReactElement => {
key: 'module_id',
label: 'Module ID',
render: (module) => (
<span className="text-sm font-normal text-[#0f1724] font-mono">{module.module_id}</span>
<span className="">{module.module_id}</span>
),
mobileLabel: 'ID',
},
@ -95,14 +95,14 @@ const Modules = (): ReactElement => {
key: 'name',
label: 'Name',
render: (module) => (
<span className="text-sm font-normal text-[#0f1724]">{module.name}</span>
<span className="">{module.name}</span>
),
},
{
key: 'version',
label: 'Version',
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',
label: 'Frontend URL',
render: (module) => (
<span className="text-sm font-normal text-[#6b7280] font-mono truncate max-w-[200px]">
<span className="">
{module.frontend_base_url || 'N/A'}
</span>
),
@ -194,7 +194,7 @@ const Modules = (): ReactElement => {
}}
>
{/* 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 */}
<DataTable
data={modules}

View File

@ -214,7 +214,7 @@ const NotificationTemplates = (): ReactElement => {
{
key: "code",
label: "Event",
render: (t) => <span className="text-sm font-semibold">{t.code}</span>,
render: (t) => <span className="">{t.code}</span>,
},
{
key: "source",
@ -229,7 +229,7 @@ const NotificationTemplates = (): ReactElement => {
key: "preview",
label: "Title Preview",
render: (t) => (
<span className="text-xs truncate max-w-xs block text-gray-500">
<span className="">
{t.title_template}
</span>
),
@ -295,8 +295,8 @@ const NotificationTemplates = (): ReactElement => {
"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="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/30 gap-4">
<div className="overflow-hidden">
<div className="pb-2 flex flex-wrap justify-between items-center">
<div className="flex items-center gap-2">
<Building className="w-4 h-4 text-blue-500" />
<h2 className="text-sm font-semibold text-gray-700">
@ -304,7 +304,7 @@ const NotificationTemplates = (): ReactElement => {
</h2>
</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">
<Filter className="w-3.5 h-3.5" /> Filter by Module
</div>

View File

@ -2,7 +2,6 @@ import { useEffect, useMemo, useState, type ReactElement } from "react";
import { useNavigate } from "react-router-dom";
import {
Plus,
Search,
Copy,
History,
Trash2,
@ -17,6 +16,7 @@ import {
PrimaryButton,
ActionDropdown,
DeleteConfirmationModal,
SearchBox,
} from "@/components/shared";
import { aiService } from "@/services/ai-service";
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="p-4 border-b border-[rgba(0,0,0,0.06)] bg-gray-50/50">
<div className="relative w-full md:w-80">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#94a3b8]" />
<input
type="text"
value={search}
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 className="overflow-hidden">
<div className="pb-2">
<SearchBox
value={search}
onChange={setSearch}
placeholder="Search by name or description..."
containerClassName="relative w-full md:w-80"
/>
</div>
<DataTable

View File

@ -1,11 +1,6 @@
import { useEffect, useMemo, useState, type ReactElement } from "react";
import { useNavigate, useSearchParams, useParams } from "react-router-dom";
import {
Plus,
Play,
Eye,
Loader2,
} from "lucide-react";
import { Plus, Play, Eye, Loader2 } from "lucide-react";
import { Layout } from "@/components/layout/Layout";
import {
DataTable,
@ -19,6 +14,7 @@ import { showToast } from "@/utils/toast";
import { formatDate } from "@/utils/format-date";
import { PromptTestCaseResultModal } from "@/components/tenant/PromptTestCaseResultModal";
import { PromptTestCaseResultsListModal } from "@/components/tenant/PromptTestCaseResultsListModal";
import CodeBadge from "@/components/shared/CodeBadge";
const PromptTestCases = (): ReactElement => {
const navigate = useNavigate();
@ -46,7 +42,8 @@ const PromptTestCases = (): ReactElement => {
const [selectedTestCaseId, setSelectedTestCaseId] = useState<string>("");
const [selectedTestCaseName, setSelectedTestCaseName] = useState<string>("");
const [isResultsListModalOpen, setIsResultsListModalOpen] = useState<boolean>(false);
const [isResultsListModalOpen, setIsResultsListModalOpen] =
useState<boolean>(false);
const handleRunTestCase = async (testCaseId: string) => {
setRunningCases((prev) => ({ ...prev, [testCaseId]: true }));
@ -92,7 +89,8 @@ const PromptTestCases = (): ReactElement => {
} catch (err: unknown) {
const message =
(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);
showToast.error(message);
} finally {
@ -104,7 +102,6 @@ const PromptTestCases = (): ReactElement => {
void loadData();
}, [promptId]);
const columns: Column<AIPromptTestCase>[] = useMemo(
() => [
{
@ -144,14 +141,9 @@ const PromptTestCases = (): ReactElement => {
return <span className="text-xs text-[#94a3b8]"></span>;
}
return (
<div className="flex flex-wrap gap-1.5 max-w-[150px]">
<div className="flex flex-wrap gap-1">
{tags.map((tag, index) => (
<span
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>
<CodeBadge key={index} label={tag} />
))}
</div>
);
@ -161,7 +153,7 @@ const PromptTestCases = (): ReactElement => {
key: "updatedAt",
label: "Last Updated",
render: (row) => (
<span className="text-xs text-[#64748b] select-none">
<span className="">
{formatDate(row.updated_at || row.created_at || "")}
</span>
),
@ -220,10 +212,9 @@ const PromptTestCases = (): ReactElement => {
},
},
],
[prompt, testCases, runningCases]
[prompt, testCases, runningCases],
);
return (
<Layout
currentPage="Prompt Management"
@ -236,7 +227,9 @@ const PromptTestCases = (): ReactElement => {
description: "Manage and execute test cases for your prompt templates.",
action: (
<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"
>
<Plus className="w-4 h-4" />
@ -246,8 +239,7 @@ const PromptTestCases = (): ReactElement => {
}}
>
<div className="flex flex-col gap-5">
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none">
<div className="overflow-hidden select-none">
<DataTable
data={testCases}
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">
Input Variables
</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">
{Object.entries(item.input_variables).map(([key, value]) => (
<div key={key} className="flex flex-col gap-0.5">
<span className="text-xs font-bold text-slate-600">{key}:</span>
<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>
))}
{Object.entries(item.input_variables).map(
([key, value]) => (
<div key={key} className="flex flex-col gap-0.5">
<span className="text-xs font-bold text-slate-600">
{key}:
</span>
<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>
) : (
<span className="text-slate-400 italic">No input variables defined.</span>
<span className="text-slate-400 italic">
No input variables defined.
</span>
)}
</div>
@ -291,7 +290,9 @@ const PromptTestCases = (): ReactElement => {
{item.expected_output}
</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>

View File

@ -12,7 +12,7 @@ const Suppliers = (): ReactElement => {
}}
>
<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 items-center justify-between">
<h2 className="text-lg font-semibold text-[#0f1724]">
@ -21,7 +21,7 @@ const Suppliers = (): ReactElement => {
</div> */}
<SuppliersTable showHeader={true} compact={false} />
{/* </div> */}
</div>
{/* </div> */}
</div>
</Layout>
);

View File

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

View File

@ -9,6 +9,7 @@ import {
ActionDropdown,
PrimaryButton,
DeleteConfirmationModal,
StatusBadge,
} from "@/components/shared";
import { Plus, Play, Loader2, Eye, Trash2 } from "lucide-react";
import { aiService } from "@/services/ai-service";
@ -16,6 +17,7 @@ import type { TenantAIConfig } from "@/types/ai";
import { showToast } from "@/utils/toast";
import { formatDate } from "@/utils/format-date";
import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal";
import CodeBadge from "@/components/shared/CodeBadge";
export const TenantAIProviders = (): ReactElement => {
const navigate = useNavigate();
@ -25,8 +27,12 @@ export const TenantAIProviders = (): ReactElement => {
const [searchQuery, setSearchQuery] = useState<string>("");
const [statusFilter, setStatusFilter] = useState<string>("");
const [testingProviders, setTestingProviders] = useState<Record<string, boolean>>({});
const [selectedConfig, setSelectedConfig] = useState<TenantAIConfig | null>(null);
const [testingProviders, setTestingProviders] = useState<
Record<string, boolean>
>({});
const [selectedConfig, setSelectedConfig] = useState<TenantAIConfig | null>(
null,
);
const [isViewModalOpen, setIsViewModalOpen] = useState<boolean>(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
const [providerToDelete, setProviderToDelete] = useState<string | null>(null);
@ -39,7 +45,8 @@ export const TenantAIProviders = (): ReactElement => {
const data = await aiService.listConfigs();
setConfigs(data || []);
} 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);
showToast.error(msg);
} finally {
@ -57,13 +64,14 @@ export const TenantAIProviders = (): ReactElement => {
const resp = await aiService.testConfig(provider);
if (resp && resp.healthy) {
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 {
showToast.error(`Connection test failed for ${provider}.`);
}
} 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);
} finally {
setTestingProviders((prev) => ({ ...prev, [provider]: false }));
@ -76,7 +84,9 @@ export const TenantAIProviders = (): ReactElement => {
setSelectedConfig(cfg);
setIsViewModalOpen(true);
} 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);
}
};
@ -114,9 +124,15 @@ export const TenantAIProviders = (): ReactElement => {
return configs.filter((cfg) => {
const searchMatches =
!searchQuery.trim() ||
(cfg.provider || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
(cfg.display_name || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
(cfg.default_model || "").toLowerCase().includes(searchQuery.toLowerCase());
(cfg.provider || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(cfg.display_name || "")
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
(cfg.default_model || "")
.toLowerCase()
.includes(searchQuery.toLowerCase());
const statusMatches =
!statusFilter ||
@ -149,24 +165,14 @@ export const TenantAIProviders = (): ReactElement => {
label: "Config Type",
render: (row) => {
const type = row.config_type || "direct";
return (
<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>
);
return <CodeBadge className="uppercase" label={type} />;
},
},
{
key: "default_model",
label: "Default Model",
render: (row) => (
<span className="text-xs text-slate-800 font-medium select-none">
<span className="">
{row.default_model || "—"}
</span>
),
@ -174,31 +180,17 @@ export const TenantAIProviders = (): ReactElement => {
{
key: "is_active",
label: "Status",
render: (row) => {
const active = row.is_active;
return (
<span
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>
);
},
render: (row) => (
<StatusBadge variant={row.is_active ? "success" : "failure"}>
{row.is_active ? "Active" : "Disabled"}
</StatusBadge>
),
},
{
key: "last_verified_at",
label: "Last Verified",
render: (row) => (
<span className="text-xs text-[#64748b]">
<span className="">
{row.last_verified_at ? formatDate(row.last_verified_at) : "Never"}
</span>
),
@ -232,7 +224,9 @@ export const TenantAIProviders = (): ReactElement => {
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",
onClick: () => handleDeleteConfig(row.provider),
},
@ -243,7 +237,7 @@ export const TenantAIProviders = (): ReactElement => {
},
},
],
[testingProviders]
[testingProviders],
);
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 */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex flex-wrap items-center gap-3">
@ -297,16 +291,14 @@ export const TenantAIProviders = (): ReactElement => {
</div>
{/* Table list */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none">
<DataTable
data={filteredConfigs}
columns={columns}
keyExtractor={(item) => item.id || item.provider}
isLoading={isLoading}
error={error}
emptyMessage="No tenant AI providers configured."
/>
</div>
<DataTable
data={filteredConfigs}
columns={columns}
keyExtractor={(item) => item.id || item.provider}
isLoading={isLoading}
error={error}
emptyMessage="No tenant AI providers configured."
/>
</div>
<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 { 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 tableRef = useRef<UsersTableRef>(null);
const { canCreate } = usePermissions();
return (
<Layout
currentPage="Users"
pageHeader={{
title: "User Management",
title: "User List",
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>
);
};

View File

@ -1643,7 +1643,7 @@ const ViewDocument = (): ReactElement => {
</>
}
>
<div className="p-5 space-y-3">
<div className="space-y-4">
{activeAction === "submit" && (
<div className="space-y-3">
<FormSelect
@ -1786,7 +1786,7 @@ const ViewDocument = (): ReactElement => {
description="Track the progress of this document's approval workflow."
maxWidth="2xl"
>
<div className="p-6">
<div className="">
{isWorkflowLoading ? (
<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>

View File

@ -1,24 +1,35 @@
import { type ReactElement } from "react";
import { useRef, type ReactElement } from "react";
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 tableRef = useRef<WorkflowDefinitionsTableRef>(null);
return (
<Layout
currentPage="Workflow Definitions"
// breadcrumbs={[
// // { label: "Platform", path: "/tenant" },
// { label: "Workflow Definitions" },
// ]}
pageHeader={{
title: "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 items-center justify-between">
<h1 className="text-2xl font-bold text-[#0f1724] tracking-[-0.48px]">
Workflow Definitions
</h1>
</div>
<WorkflowDefinitionsTable compact={false} />
<WorkflowDefinitionsTable ref={tableRef} compact={false} />
</div>
</Layout>
);