refactor: modernize UI components by stripping default container styles and standardizing layout elements across application pages
This commit is contained in:
parent
12954e5ba1
commit
fd6436e389
@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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); }}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]"
|
||||
|
||||
@ -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]"
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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]"
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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]">
|
||||
|
||||
@ -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]"
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
@ -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" />
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user