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 (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center justify-center rounded-full bg-[#EDF3FE] px-3 py-1 text-sm font-medium text-[#3B82F6]",
|
"inline-flex items-center justify-center rounded-full bg-[#EDF3FE] px-3 py-1 text-[12px] font-medium text-[#4C89FA]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ export interface Column<T> {
|
|||||||
render?: (item: T) => ReactNode;
|
render?: (item: T) => ReactNode;
|
||||||
align?: "left" | "right" | "center";
|
align?: "left" | "right" | "center";
|
||||||
mobileLabel?: string;
|
mobileLabel?: string;
|
||||||
|
width?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataTableProps<T> {
|
interface DataTableProps<T> {
|
||||||
@ -76,9 +78,10 @@ export const DataTable = <T,>({
|
|||||||
{/* Desktop Table Empty State */}
|
{/* Desktop Table Empty State */}
|
||||||
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
|
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
|
||||||
<div className="inline-block min-w-full align-middle">
|
<div className="inline-block min-w-full align-middle">
|
||||||
<table className="w-full">
|
<table className="w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
|
{/* <tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]"> */}
|
||||||
|
<tr className="bg-[#F9F9F9] border-b border-[#D1D5DB]">
|
||||||
{canExpand && showExpandColumn && (
|
{canExpand && showExpandColumn && (
|
||||||
<th
|
<th
|
||||||
className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left"
|
className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left"
|
||||||
@ -93,9 +96,14 @@ export const DataTable = <T,>({
|
|||||||
? "text-center"
|
? "text-center"
|
||||||
: "text-left";
|
: "text-left";
|
||||||
return (
|
return (
|
||||||
|
// <th
|
||||||
|
// key={column.key}
|
||||||
|
// className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
|
||||||
|
// >
|
||||||
<th
|
<th
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
|
className={`h-[41px] px-2 py-3 ${alignClass} bg-[#F9F9F9] text-[13px] font-medium text-[#6B7280] align-bottom whitespace-nowrap ${column.className || ""}`}
|
||||||
|
style={column.width ? { width: column.width } : undefined}
|
||||||
>
|
>
|
||||||
{column.label}
|
{column.label}
|
||||||
</th>
|
</th>
|
||||||
@ -129,12 +137,14 @@ export const DataTable = <T,>({
|
|||||||
{/* Desktop Table */}
|
{/* Desktop Table */}
|
||||||
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
|
<div className="hidden md:block overflow-x-auto -mx-2 md:mx-0">
|
||||||
<div className="inline-block min-w-full align-middle">
|
<div className="inline-block min-w-full align-middle">
|
||||||
<table className="w-full">
|
<table className="w-full table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
|
{/* <tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]"> */}
|
||||||
|
<tr className="bg-[#F9F9F9] border-b border-[#D1D5DB]">
|
||||||
{canExpand && showExpandColumn && (
|
{canExpand && showExpandColumn && (
|
||||||
<th
|
<th
|
||||||
className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left"
|
// className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left"
|
||||||
|
className="w-10 px-2 py-3 text-left"
|
||||||
aria-label="Expand"
|
aria-label="Expand"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -146,9 +156,14 @@ export const DataTable = <T,>({
|
|||||||
? "text-center"
|
? "text-center"
|
||||||
: "text-left";
|
: "text-left";
|
||||||
return (
|
return (
|
||||||
|
// <th
|
||||||
|
// key={column.key}
|
||||||
|
// className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
|
||||||
|
// >
|
||||||
<th
|
<th
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
|
className={`h-[41px] px-2 py-3 ${alignClass} bg-[#F9F9F9] text-[13px] font-medium text-[#6B7280] align-bottom whitespace-nowrap ${column.className || ""}`}
|
||||||
|
style={column.width ? { width: column.width } : undefined}
|
||||||
>
|
>
|
||||||
{column.label}
|
{column.label}
|
||||||
</th>
|
</th>
|
||||||
@ -163,11 +178,14 @@ export const DataTable = <T,>({
|
|||||||
return (
|
return (
|
||||||
<Fragment key={rowId}>
|
<Fragment key={rowId}>
|
||||||
<tr
|
<tr
|
||||||
className="border-b border-[rgba(0,0,0,0.08)] hover:bg-gray-50 transition-colors"
|
className="border-b border-[#D1D5DB] hover:bg-gray-50 transition-colors"
|
||||||
onClick={onRowClick ? () => onRowClick(item) : undefined}
|
onClick={onRowClick ? () => onRowClick(item) : undefined}
|
||||||
>
|
>
|
||||||
{canExpand && showExpandColumn && (
|
{canExpand && showExpandColumn && (
|
||||||
<td className="px-2 py-2.5 md:py-1.5 lg:py-3 xl:py-4 align-middle">
|
<td
|
||||||
|
// className="px-2 py-2.5 md:py-1.5 lg:py-3 xl:py-4 align-middle"
|
||||||
|
className="w-10 px-2 py-3 align-middle"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 rounded hover:bg-gray-200/80 text-[#64748b]"
|
className="p-1 rounded hover:bg-gray-200/80 text-[#64748b]"
|
||||||
@ -191,9 +209,14 @@ export const DataTable = <T,>({
|
|||||||
? "text-center"
|
? "text-center"
|
||||||
: "text-left";
|
: "text-left";
|
||||||
return (
|
return (
|
||||||
|
// <td
|
||||||
|
// key={column.key}
|
||||||
|
// className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-[13px]`}
|
||||||
|
// >
|
||||||
<td
|
<td
|
||||||
key={column.key}
|
key={column.key}
|
||||||
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-[13px]`}
|
className={`h-[56px] px-2 py-3 ${alignClass} text-[13px] font-normal text-[#0F1724] align-middle ${column.className || ""}`}
|
||||||
|
style={column.width ? { width: column.width } : undefined}
|
||||||
>
|
>
|
||||||
{column.render
|
{column.render
|
||||||
? column.render(item)
|
? column.render(item)
|
||||||
@ -203,7 +226,7 @@ export const DataTable = <T,>({
|
|||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
{canExpand && expanded && (
|
{canExpand && expanded && (
|
||||||
<tr className="border-t border-[rgba(0,0,0,0.08)] bg-[#F9F9F9]">
|
<tr className="border-t border-[#D1D5DB] bg-[#F9F9F9]">
|
||||||
<td colSpan={desktopColSpan}>
|
<td colSpan={desktopColSpan}>
|
||||||
<div className="flex flex-col items-start w-full bg-[#FFF] border border-gray-300 rounded-md p-4 text-xs text-gray-700 m-4">
|
<div className="flex flex-col items-start w-full bg-[#FFF] border border-gray-300 rounded-md p-4 text-xs text-gray-700 m-4">
|
||||||
{renderExpandedRow(item)}
|
{renderExpandedRow(item)}
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export const DeleteConfirmationModal = ({
|
|||||||
className="bg-white rounded-xl shadow-[0px_20px_25px_-5px_rgba(0,0,0,0.1),0px_10px_10px_-5px_rgba(0,0,0,0.04)] w-full max-w-[400px] z-[301]"
|
className="bg-white rounded-xl shadow-[0px_20px_25px_-5px_rgba(0,0,0,0.1),0px_10px_10px_-5px_rgba(0,0,0,0.04)] w-full max-w-[400px] z-[301]"
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-start justify-between pb-4 pt-5 px-5 border-b border-[rgba(0,0,0,0.08)]">
|
<div className="flex items-start justify-between px-5 pt-5 pb-[15px]">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-[rgba(239,68,68,0.1)] rounded-full flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 bg-[rgba(239,68,68,0.1)] rounded-full flex items-center justify-center shrink-0">
|
||||||
<AlertTriangle className="w-5 h-5 text-[#ef4444]" />
|
<AlertTriangle className="w-5 h-5 text-[#ef4444]" />
|
||||||
@ -94,7 +94,7 @@ export const DeleteConfirmationModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div className="p-5">
|
<div className='pb-4 px-5'>
|
||||||
<p className="text-sm text-[#6b7280] leading-relaxed">
|
<p className="text-sm text-[#6b7280] leading-relaxed">
|
||||||
{message}
|
{message}
|
||||||
{itemName && (
|
{itemName && (
|
||||||
@ -106,7 +106,7 @@ export const DeleteConfirmationModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
<div className="flex items-center justify-end gap-3 pt-4 px-5 pb-5 border-t border-[rgba(0,0,0,0.08)]">
|
<div className="flex items-center justify-end gap-3 px-5 pb-5">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|||||||
@ -122,24 +122,24 @@ export const NewDepartmentModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(handleFormSubmit)}
|
onSubmit={handleSubmit(handleFormSubmit)}
|
||||||
className="p-5 flex flex-col gap-4"
|
className="flex flex-col gap-3"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{/* <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Department Name"
|
label="Department Name"
|
||||||
required
|
required
|
||||||
placeholder="e.g. Engineering"
|
placeholder="e.g. Engineering"
|
||||||
error={errors.name?.message}
|
error={errors.name?.message}
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
label="Department Code"
|
label="Code"
|
||||||
required
|
required
|
||||||
placeholder="e.g. ENG"
|
placeholder="e.g. ENG"
|
||||||
error={errors.code?.message}
|
error={errors.code?.message}
|
||||||
{...register("code")}
|
{...register("code")}
|
||||||
/>
|
/>
|
||||||
</div>
|
{/* </div> */}
|
||||||
|
|
||||||
{/* <FormField
|
{/* <FormField
|
||||||
label="Description"
|
label="Description"
|
||||||
@ -155,23 +155,23 @@ export const NewDepartmentModal = ({
|
|||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{/* <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> */}
|
||||||
<PaginatedSelect
|
<PaginatedSelect
|
||||||
label="Parent Department"
|
label="Parent Department"
|
||||||
placeholder="Select parent (optional)"
|
placeholder="Select parent (optional)"
|
||||||
value={parentIdValue || ""}
|
value={parentIdValue || ""}
|
||||||
onValueChange={(value) => setValue("parent_id", value || null)}
|
onValueChange={(value) => setValue("parent_id", value || null)}
|
||||||
onLoadOptions={loadDepartments}
|
onLoadOptions={loadDepartments}
|
||||||
error={errors.parent_id?.message}
|
error={errors.parent_id?.message}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
label="Sort Order"
|
label="Sort Order"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
error={errors.sort_order?.message}
|
error={errors.sort_order?.message}
|
||||||
{...register("sort_order", { valueAsNumber: true })}
|
{...register("sort_order", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
{/* </div> */}
|
||||||
|
|
||||||
{/* <FormSelect
|
{/* <FormSelect
|
||||||
label="Status"
|
label="Status"
|
||||||
@ -309,24 +309,24 @@ export const EditDepartmentModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(handleFormSubmit)}
|
onSubmit={handleSubmit(handleFormSubmit)}
|
||||||
className="p-5 flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{/* <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Department Name"
|
label="Department Name"
|
||||||
required
|
required
|
||||||
placeholder="e.g. Engineering"
|
placeholder="e.g. Engineering"
|
||||||
error={errors.name?.message}
|
error={errors.name?.message}
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
label="Department Code"
|
label="Department Code"
|
||||||
required
|
required
|
||||||
placeholder="e.g. ENG"
|
placeholder="e.g. ENG"
|
||||||
error={errors.code?.message}
|
error={errors.code?.message}
|
||||||
{...register("code")}
|
{...register("code")}
|
||||||
/>
|
/>
|
||||||
</div>
|
{/* </div> */}
|
||||||
|
|
||||||
{/* <FormField
|
{/* <FormField
|
||||||
label="Description"
|
label="Description"
|
||||||
@ -342,23 +342,23 @@ export const EditDepartmentModal = ({
|
|||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{/* <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> */}
|
||||||
<PaginatedSelect
|
<PaginatedSelect
|
||||||
label="Parent Department"
|
label="Parent Department"
|
||||||
placeholder="Select parent (optional)"
|
placeholder="Select parent (optional)"
|
||||||
value={parentIdValue || ""}
|
value={parentIdValue || ""}
|
||||||
onValueChange={(value) => setValue("parent_id", value || null)}
|
onValueChange={(value) => setValue("parent_id", value || null)}
|
||||||
onLoadOptions={loadDepartments}
|
onLoadOptions={loadDepartments}
|
||||||
error={errors.parent_id?.message}
|
error={errors.parent_id?.message}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
label="Sort Order"
|
label="Sort Order"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
error={errors.sort_order?.message}
|
error={errors.sort_order?.message}
|
||||||
{...register("sort_order", { valueAsNumber: true })}
|
{...register("sort_order", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
{/* </div> */}
|
||||||
|
|
||||||
<FormSelect
|
<FormSelect
|
||||||
label="Status"
|
label="Status"
|
||||||
@ -394,7 +394,7 @@ export const ViewDepartmentModal = ({
|
|||||||
footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>}
|
footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>}
|
||||||
>
|
>
|
||||||
{department && (
|
{department && (
|
||||||
<div className="p-5 grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h3 className="text-sm font-semibold text-[#0f1724]">
|
<h3 className="text-sm font-semibold text-[#0f1724]">
|
||||||
Basic Information
|
Basic Information
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export const NewDesignationModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(handleFormSubmit)}
|
onSubmit={handleSubmit(handleFormSubmit)}
|
||||||
className="p-5 flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
@ -232,7 +232,7 @@ export const EditDesignationModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(handleFormSubmit)}
|
onSubmit={handleSubmit(handleFormSubmit)}
|
||||||
className="p-5 flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
@ -316,7 +316,7 @@ export const ViewDesignationModal = ({
|
|||||||
footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>}
|
footer={<SecondaryButton onClick={onClose}>Close</SecondaryButton>}
|
||||||
>
|
>
|
||||||
{designation && (
|
{designation && (
|
||||||
<div className="p-5 grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h3 className="text-sm font-semibold text-[#0f1724]">
|
<h3 className="text-sm font-semibold text-[#0f1724]">
|
||||||
Basic Information
|
Basic Information
|
||||||
|
|||||||
@ -465,7 +465,7 @@ export const EditRoleModal = ({
|
|||||||
{!isLoadingRole && (
|
{!isLoadingRole && (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(handleFormSubmit)}
|
onSubmit={handleSubmit(handleFormSubmit)}
|
||||||
className="p-5 flex flex-col gap-0"
|
className="flex flex-col gap-0"
|
||||||
>
|
>
|
||||||
{/* Role Name and Role Code Row */}
|
{/* Role Name and Role Code Row */}
|
||||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||||
|
|||||||
@ -546,7 +546,7 @@ export const EditUserModal = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit as any)} className="p-5">
|
<form onSubmit={handleSubmit(handleFormSubmit as any)}>
|
||||||
{isLoadingUser && (
|
{isLoadingUser && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
||||||
|
|||||||
@ -1,17 +1,24 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { failedEmailsService, type FailedEmail } from '../../services/failed-emails-service';
|
import {
|
||||||
import { DataTable, type Column } from './DataTable';
|
failedEmailsService,
|
||||||
import { Modal } from './Modal';
|
type FailedEmail,
|
||||||
import { Button } from '@/components/ui/button';
|
} from "../../services/failed-emails-service";
|
||||||
import { StatusBadge } from './StatusBadge';
|
import { DataTable, type Column } from "./DataTable";
|
||||||
import { Eye, RefreshCw, Trash2, Loader2 } from 'lucide-react';
|
import { Modal } from "./Modal";
|
||||||
import { format } from 'date-fns';
|
// import { Button } from "@/components/ui/button";
|
||||||
import { toast } from 'sonner';
|
import { StatusBadge } from "./StatusBadge";
|
||||||
import { Pagination } from './Pagination';
|
import { Eye, RefreshCw, Trash2, Loader2 } from "lucide-react";
|
||||||
import { ActionDropdown } from './ActionDropdown';
|
import { format } from "date-fns";
|
||||||
import { PrimaryButton } from './PrimaryButton';
|
import { toast } from "sonner";
|
||||||
|
import { Pagination } from "./Pagination";
|
||||||
|
import { ActionDropdown } from "./ActionDropdown";
|
||||||
|
import { PrimaryButton } from "./PrimaryButton";
|
||||||
|
|
||||||
export const FailedEmailsTable: React.FC = () => {
|
interface FailedEmailsTableProps {
|
||||||
|
onRegisterResendAll?: (node: React.ReactNode) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegisterResendAll }) => {
|
||||||
const [emails, setEmails] = useState<FailedEmail[]>([]);
|
const [emails, setEmails] = useState<FailedEmail[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
@ -28,7 +35,10 @@ export const FailedEmailsTable: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const offset = (page - 1) * currentLimit;
|
const offset = (page - 1) * currentLimit;
|
||||||
const res = await failedEmailsService.getFailedEmails(currentLimit, offset);
|
const res = await failedEmailsService.getFailedEmails(
|
||||||
|
currentLimit,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
setEmails(res.data || []);
|
setEmails(res.data || []);
|
||||||
setTotal(res.total || 0);
|
setTotal(res.total || 0);
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
@ -49,10 +59,10 @@ export const FailedEmailsTable: React.FC = () => {
|
|||||||
setResendingId(id);
|
setResendingId(id);
|
||||||
try {
|
try {
|
||||||
await failedEmailsService.resendEmail(id);
|
await failedEmailsService.resendEmail(id);
|
||||||
toast.success('Email resent successfully.');
|
toast.success("Email resent successfully.");
|
||||||
fetchEmails(currentPage);
|
fetchEmails(currentPage);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to resend email', { description: error.message });
|
toast.error("Failed to resend email", { description: error.message });
|
||||||
} finally {
|
} finally {
|
||||||
setResendingId(null);
|
setResendingId(null);
|
||||||
}
|
}
|
||||||
@ -65,19 +75,49 @@ export const FailedEmailsTable: React.FC = () => {
|
|||||||
toast.success(res.message);
|
toast.success(res.message);
|
||||||
fetchEmails(currentPage);
|
fetchEmails(currentPage);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to resend all emails', { description: error.message });
|
toast.error("Failed to resend all emails", {
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsResendingAll(false);
|
setIsResendingAll(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onRegisterResendAll) {
|
||||||
|
onRegisterResendAll(
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={handleResendAll}
|
||||||
|
disabled={
|
||||||
|
isResendingAll ||
|
||||||
|
emails.filter((e) => e.status === "failed").length === 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isResendingAll ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 w-3.5 h-3.5 animate-spin" />
|
||||||
|
Resending...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Resend All Failed"
|
||||||
|
)}
|
||||||
|
</PrimaryButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (onRegisterResendAll) {
|
||||||
|
onRegisterResendAll(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isResendingAll, emails, onRegisterResendAll]);
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await failedEmailsService.deleteEmail(id);
|
await failedEmailsService.deleteEmail(id);
|
||||||
toast.success('Email deleted successfully.');
|
toast.success("Email deleted successfully.");
|
||||||
fetchEmails(currentPage);
|
fetchEmails(currentPage);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to delete email', { description: error.message });
|
toast.error("Failed to delete email", { description: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -88,81 +128,91 @@ export const FailedEmailsTable: React.FC = () => {
|
|||||||
|
|
||||||
const columns: Column<FailedEmail>[] = [
|
const columns: Column<FailedEmail>[] = [
|
||||||
{
|
{
|
||||||
label: 'Date',
|
label: "Date",
|
||||||
key: 'created_at',
|
key: "created_at",
|
||||||
render: (record: FailedEmail) => format(new Date(record.created_at), 'yyyy-MM-dd HH:mm:ss')
|
render: (record: FailedEmail) =>
|
||||||
|
format(new Date(record.created_at), "yyyy-MM-dd HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'To',
|
label: "To",
|
||||||
key: 'to_email',
|
key: "to_email",
|
||||||
render: (record: FailedEmail) => record.to_email
|
render: (record: FailedEmail) => record.to_email,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Subject',
|
label: "Subject",
|
||||||
key: 'subject',
|
key: "subject",
|
||||||
render: (record: FailedEmail) => record.subject
|
render: (record: FailedEmail) => record.subject,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Status',
|
label: "Status",
|
||||||
key: 'status',
|
key: "status",
|
||||||
render: (record: FailedEmail) => (
|
render: (record: FailedEmail) => (
|
||||||
<StatusBadge variant={record.status === 'failed' ? 'failure' : 'success'}>
|
<StatusBadge
|
||||||
|
variant={record.status === "failed" ? "failure" : "success"}
|
||||||
|
>
|
||||||
{record.status}
|
{record.status}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Actions',
|
label: "Actions",
|
||||||
key: 'actions',
|
key: "actions",
|
||||||
align: 'right',
|
align: "right",
|
||||||
render: (record: FailedEmail) => (
|
render: (record: FailedEmail) => (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<ActionDropdown
|
<ActionDropdown
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
label: 'View Details',
|
label: "View Details",
|
||||||
onClick: () => showEmailDetails(record),
|
onClick: () => showEmailDetails(record),
|
||||||
icon: <Eye className="w-3.5 h-3.5 text-gray-500" />
|
icon: <Eye className="w-3.5 h-3.5 text-gray-500" />,
|
||||||
},
|
},
|
||||||
...(record.status === 'failed'
|
...(record.status === "failed"
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: resendingId === record.id ? 'Resending...' : 'Resend Email',
|
label:
|
||||||
|
resendingId === record.id
|
||||||
|
? "Resending..."
|
||||||
|
: "Resend Email",
|
||||||
onClick: () => handleResend(record.id),
|
onClick: () => handleResend(record.id),
|
||||||
icon: resendingId === record.id ? (
|
icon:
|
||||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-blue-600" />
|
resendingId === record.id ? (
|
||||||
) : (
|
<Loader2 className="w-3.5 h-3.5 animate-spin text-blue-600" />
|
||||||
<RefreshCw className="w-3.5 h-3.5 text-gray-500" />
|
) : (
|
||||||
)
|
<RefreshCw className="w-3.5 h-3.5 text-gray-500" />
|
||||||
}
|
),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
label: 'Delete Email',
|
label: "Delete Email",
|
||||||
onClick: () => handleDelete(record.id),
|
onClick: () => handleDelete(record.id),
|
||||||
icon: <Trash2 className="w-3.5 h-3.5 text-red-600" />,
|
icon: <Trash2 className="w-3.5 h-3.5 text-red-600" />,
|
||||||
variant: 'danger'
|
variant: "danger",
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
{/* Toolbar / Actions Header */}
|
{/* Toolbar / Actions Header */}
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row justify-end items-center gap-3">
|
{!onRegisterResendAll && (
|
||||||
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
<div className="pb-2 flex justify-end">
|
||||||
<Button variant="outline" size="sm" onClick={() => fetchEmails(currentPage)}>
|
{/* <div className="flex items-center gap-2 w-full sm:w-auto justify-end"> */}
|
||||||
<RefreshCw className="mr-2 w-3.5 h-3.5" />
|
{/* <Button variant="outline" onClick={() => fetchEmails(currentPage)}>
|
||||||
Refresh
|
<RefreshCw className="mr-2 w-3.5 h-3.5" />
|
||||||
</Button>
|
Refresh
|
||||||
<PrimaryButton
|
</Button> */}
|
||||||
size="small"
|
<PrimaryButton
|
||||||
onClick={handleResendAll}
|
onClick={handleResendAll}
|
||||||
disabled={isResendingAll || emails.filter(e => e.status === 'failed').length === 0}
|
disabled={
|
||||||
|
isResendingAll ||
|
||||||
|
emails.filter((e) => e.status === "failed").length === 0
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isResendingAll ? (
|
{isResendingAll ? (
|
||||||
<>
|
<>
|
||||||
@ -170,11 +220,12 @@ export const FailedEmailsTable: React.FC = () => {
|
|||||||
Resending...
|
Resending...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Resend All Failed'
|
"Resend All Failed"
|
||||||
)}
|
)}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
|
{/* </div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Table Section */}
|
{/* Table Section */}
|
||||||
<DataTable
|
<DataTable
|
||||||
@ -185,7 +236,7 @@ export const FailedEmailsTable: React.FC = () => {
|
|||||||
emptyMessage="No failed emails found"
|
emptyMessage="No failed emails found"
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pagination Footer */}
|
{/* Pagination Footer */}
|
||||||
{total > limit && (
|
{total > limit && (
|
||||||
<div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3">
|
<div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3">
|
||||||
@ -211,17 +262,28 @@ export const FailedEmailsTable: React.FC = () => {
|
|||||||
maxWidth="xl"
|
maxWidth="xl"
|
||||||
>
|
>
|
||||||
{selectedEmail && (
|
{selectedEmail && (
|
||||||
<div className="p-5">
|
<div>
|
||||||
<div className="grid grid-cols-1 gap-2 mb-4">
|
<div className="grid grid-cols-1 gap-2 mb-4">
|
||||||
<p><strong className="text-[#0f1724]">To:</strong> <span className="text-[#6b7280]">{selectedEmail.to_email}</span></p>
|
<p>
|
||||||
<p><strong className="text-[#0f1724]">Subject:</strong> <span className="text-[#6b7280]">{selectedEmail.subject}</span></p>
|
<strong className="text-[#0f1724]">To:</strong>{" "}
|
||||||
<p><strong className="text-[#0f1724]">Error Message:</strong> <span className="text-[#ef4444]">{selectedEmail.error_message}</span></p>
|
<span className="text-[#6b7280]">{selectedEmail.to_email}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong className="text-[#0f1724]">Subject:</strong>{" "}
|
||||||
|
<span className="text-[#6b7280]">{selectedEmail.subject}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong className="text-[#0f1724]">Error Message:</strong>{" "}
|
||||||
|
<span className="text-[#ef4444]">
|
||||||
|
{selectedEmail.error_message}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<strong className="text-[#0f1724] mb-2 block">Body:</strong>
|
<strong className="text-[#0f1724] mb-2 block">Body:</strong>
|
||||||
<div
|
<div
|
||||||
className="border border-[rgba(0,0,0,0.08)] rounded p-4 mt-2 max-h-[400px] overflow-auto bg-gray-50 text-sm"
|
className="border border-[rgba(0,0,0,0.08)] rounded p-4 mt-2 max-h-[400px] overflow-auto bg-gray-50 text-sm"
|
||||||
dangerouslySetInnerHTML={{ __html: selectedEmail.body }}
|
dangerouslySetInnerHTML={{ __html: selectedEmail.body }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -113,7 +113,7 @@ export const FileShareModal: React.FC<FileShareModalProps> = ({
|
|||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
preventCloseOnClickOutside={showRevokeConfirm}
|
preventCloseOnClickOutside={showRevokeConfirm}
|
||||||
>
|
>
|
||||||
<div className="p-6 space-y-6">
|
<div className="space-y-4">
|
||||||
{!shareData ? (
|
{!shareData ? (
|
||||||
<>
|
<>
|
||||||
{/* Expiry */}
|
{/* Expiry */}
|
||||||
|
|||||||
@ -449,10 +449,10 @@ export const FileUploadModal = ({
|
|||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
footer={footer}
|
footer={footer}
|
||||||
>
|
>
|
||||||
<div className="px-6 py-5 space-y-5">
|
<div className="space-y-4">
|
||||||
{/* Drop Zone */}
|
{/* Drop Zone */}
|
||||||
<div>
|
<div className="flex flex-col pb-4 gap-0.5">
|
||||||
<p className="text-[13px] font-medium text-[#0e1b2a] mb-2">Attach Files <span className="text-[#e02424]">*</span></p>
|
<p className="text-[13px] font-medium text-[#0e1b2a]">Attach Files <span className="text-[#e02424]">*</span></p>
|
||||||
{fileEntries.length === 0 ? (
|
{fileEntries.length === 0 ? (
|
||||||
<div
|
<div
|
||||||
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
||||||
|
|||||||
@ -169,7 +169,7 @@ export const FileVersionUploadModal = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="px-6 py-5 space-y-5">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[13px] font-medium text-[#0e1b2a] mb-2">Select New File <span className="text-[#e02424]">*</span></p>
|
<p className="text-[13px] font-medium text-[#0e1b2a] mb-2">Select New File <span className="text-[#e02424]">*</span></p>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export const FormField = ({
|
|||||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 pb-4">
|
<div className="flex flex-col gap-0.5 pb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor={fieldId}
|
htmlFor={fieldId}
|
||||||
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"
|
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"
|
||||||
|
|||||||
@ -134,7 +134,7 @@ export const FormSelect = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 pb-4">
|
<div className="flex flex-col gap-0.5 pb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor={fieldId}
|
htmlFor={fieldId}
|
||||||
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"
|
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export const FormTagInput = ({
|
|||||||
placeholder = "Type and press enter...",
|
placeholder = "Type and press enter...",
|
||||||
}: FormTagInputProps): ReactElement => {
|
}: FormTagInputProps): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 pb-1">
|
<div className="flex flex-col gap-0.5 pb-1">
|
||||||
{label && (
|
{label && (
|
||||||
<label className="text-[13px] font-medium text-[#0e1b2a]">
|
<label className="text-[13px] font-medium text-[#0e1b2a]">
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const FormTextArea = ({
|
|||||||
const hasError = Boolean(error);
|
const hasError = Boolean(error);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 pb-4">
|
<div className="flex flex-col gap-0.5 pb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor={fieldId}
|
htmlFor={fieldId}
|
||||||
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"
|
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface GradientStatCardProps {
|
interface GradientStatCardProps {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
@ -8,16 +8,22 @@ interface GradientStatCardProps {
|
|||||||
label: string;
|
label: string;
|
||||||
badge?: {
|
badge?: {
|
||||||
text: string;
|
text: string;
|
||||||
variant: 'success' | 'warning' | 'info' | 'error' | 'green' | 'gray';
|
variant: "success" | "warning" | "info" | "error" | "green" | "gray";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GradientStatCard: React.FC<GradientStatCardProps> = ({ icon: Icon, value, label, badge }) => {
|
export const GradientStatCard: React.FC<GradientStatCardProps> = ({
|
||||||
|
icon: Icon,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
badge,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded-[8px] p-[1px] h-full"
|
className="rounded-[8px] p-[1px] h-full"
|
||||||
style={{
|
style={{
|
||||||
background: 'var(--Linear, linear-gradient(161deg, #084CC8 -1.15%, #75C044 44.29%, #FED314 89.74%))',
|
background:
|
||||||
|
"var(--Linear, linear-gradient(161deg, #084CC8 -1.15%, #75C044 44.29%, #FED314 89.74%))",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-start gap-3 px-4 py-4 min-h-[108px] h-full w-full rounded-[7px] bg-white">
|
<div className="flex flex-col items-start gap-3 px-4 py-4 min-h-[108px] h-full w-full rounded-[7px] bg-white">
|
||||||
@ -26,14 +32,20 @@ export const GradientStatCard: React.FC<GradientStatCardProps> = ({ icon: Icon,
|
|||||||
<Icon className="w-5 h-5 stroke-[1.8]" />
|
<Icon className="w-5 h-5 stroke-[1.8]" />
|
||||||
</div>
|
</div>
|
||||||
{badge && (
|
{badge && (
|
||||||
<div className={cn(
|
<div
|
||||||
"px-2.5 py-1 rounded-full text-[12px] font-bold tracking-tight whitespace-nowrap",
|
className={cn(
|
||||||
(badge.variant === 'success' || badge.variant === 'green') ? "bg-[#f1fffb] text-[#16c784]" :
|
"px-2.5 py-1 rounded-full text-[12px] font-medium whitespace-nowrap capitalize leading-normal",
|
||||||
badge.variant === 'warning' ? "bg-[#fff5e5] text-[#fca004]" :
|
badge.variant === "success" || badge.variant === "green"
|
||||||
badge.variant === 'info' ? "bg-[#f0f9ff] text-[#0ea5e9]" :
|
? "bg-[#f1fffb] text-[#16c784]"
|
||||||
badge.variant === 'error' ? "bg-[#fdf5f4] text-[#e0352a]" :
|
: badge.variant === "warning"
|
||||||
"bg-[#f3f4f6] text-[#6b7280]" // default / gray
|
? "bg-[#fff5e5] text-[#fca004]"
|
||||||
)}>
|
: badge.variant === "info"
|
||||||
|
? "bg-[#f0f9ff] text-[#0ea5e9]"
|
||||||
|
: badge.variant === "error"
|
||||||
|
? "bg-[#fdf5f4] text-[#e0352a]"
|
||||||
|
: "bg-[#f3f4f6] text-[#6b7280]", // default / gray
|
||||||
|
)}
|
||||||
|
>
|
||||||
{badge.text}
|
{badge.text}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -94,11 +94,11 @@ export const Modal = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-start justify-between pb-4 pt-5 px-5 border-b border-[rgba(0,0,0,0.08)] shrink-0">
|
<div className="flex items-start justify-between shrink-0 px-5 pt-5 pb-[15px]">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h2 className="text-lg font-semibold text-[#0e1b2a]">{title}</h2>
|
<h2 className="text-lg font-semibold text-[#0F1724]">{title}</h2>
|
||||||
{description && (
|
{description && (
|
||||||
<p className="text-sm font-normal text-[#9aa6b2]">{description}</p>
|
<p className="text-sm font-normal text-[#6B7280]">{description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
@ -114,11 +114,13 @@ export const Modal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Body - Scrollable */}
|
{/* Modal Body - Scrollable */}
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto">{children}</div>
|
<div className={cn("flex-1 min-h-0 overflow-y-auto px-5", footer ? "pb-4" : "pb-5")}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
{footer && (
|
{footer && (
|
||||||
<div className="flex items-center justify-end gap-3 pt-4 px-5 pb-5 border-t border-[rgba(0,0,0,0.08)] shrink-0">
|
<div className="flex items-center justify-end gap-3 shrink-0 px-5 pb-5 pt-2">
|
||||||
{footer}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -348,7 +348,7 @@ export const NewRoleModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(handleFormSubmit)}
|
onSubmit={handleSubmit(handleFormSubmit)}
|
||||||
className="p-5 flex flex-col gap-0"
|
className="flex-col gap-0"
|
||||||
>
|
>
|
||||||
{/* General Error Display */}
|
{/* General Error Display */}
|
||||||
{errors.root && (
|
{errors.root && (
|
||||||
|
|||||||
@ -230,7 +230,7 @@ export const NewUserModal = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit as any)} className="p-5">
|
<form onSubmit={handleSubmit(handleFormSubmit as any)}>
|
||||||
{errors.root && (
|
{errors.root && (
|
||||||
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
||||||
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
|
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
|
||||||
|
|||||||
@ -60,7 +60,7 @@ export const PageHeader = ({
|
|||||||
.sort((a, b) => b.path.length - a.path.length)[0]?.path ?? null;
|
.sort((a, b) => b.path.length - a.path.length)[0]?.path ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 md:gap-6 mb-6">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 md:gap-6 mb-4">
|
||||||
{/* Title and Description */}
|
{/* Title and Description */}
|
||||||
<div className="flex flex-col gap-1 max-w-full md:max-w-[434px]">
|
<div className="flex flex-col gap-1 max-w-full md:max-w-[434px]">
|
||||||
<h1 className="text-xl md:text-2xl font-bold text-[#0f1724] tracking-[-0.48px]">
|
<h1 className="text-xl md:text-2xl font-bold text-[#0f1724] tracking-[-0.48px]">
|
||||||
|
|||||||
@ -207,7 +207,7 @@ export const PaginatedSelect = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 pb-4">
|
<div className="flex flex-col gap-0.5 pb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor={fieldId}
|
htmlFor={fieldId}
|
||||||
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"
|
className="flex items-center gap-1 text-[13px] font-medium text-[#0e1b2a]"
|
||||||
|
|||||||
@ -117,20 +117,20 @@ export const Pagination = ({
|
|||||||
const selectedLimitOption = limitOptions.find((opt) => Number(opt.value) === limit);
|
const selectedLimitOption = limitOptions.find((opt) => Number(opt.value) === limit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-center justify-between gap-3">
|
<div className="p-3 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||||
{/* Items Info and Limit Selector */}
|
{/* Items Info and Limit Selector */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
||||||
<div className="text-xs text-[#6b7280]">
|
<div className="text-xs text-[#6B7280]">
|
||||||
Showing {startItem} to {endItem} of {totalItems} {totalItems === 1 ? 'item' : 'items'}
|
Showing {startItem} to {endItem} of {totalItems} {totalItems === 1 ? 'item' : 'items'}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 relative" ref={limitDropdownRef}>
|
<div className="flex items-center gap-2 relative" ref={limitDropdownRef}>
|
||||||
<span className="text-xs text-[#6b7280]">Show:</span>
|
<span className="text-xs text-[#6B7280]">Show:</span>
|
||||||
<div className="w-[120px] relative">
|
<div className="w-[120px] relative">
|
||||||
<button
|
<button
|
||||||
ref={limitButtonRef}
|
ref={limitButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsLimitOpen(!isLimitOpen)}
|
onClick={() => setIsLimitOpen(!isLimitOpen)}
|
||||||
className="h-8 w-full px-3.5 py-1 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0e1b2a] flex items-center justify-between hover:bg-gray-50 transition-colors min-h-[44px]"
|
className="h-8 w-full px-3.5 py-2 border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0e1b2a] flex items-center justify-between hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<span>{selectedLimitOption ? selectedLimitOption.label : `${limit} per page`}</span>
|
<span>{selectedLimitOption ? selectedLimitOption.label : `${limit} per page`}</span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
@ -142,7 +142,7 @@ export const Pagination = ({
|
|||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
data-limit-dropdown="true"
|
data-limit-dropdown="true"
|
||||||
className="fixed bg-white border border-[rgba(0,0,0,0.2)] rounded-md shadow-lg z-[250] max-h-60 overflow-y-auto"
|
className="fixed border border-[rgba(0,0,0,0.2)] rounded-md shadow-lg z-[250] max-h-60 overflow-y-auto"
|
||||||
style={limitDropdownStyle}
|
style={limitDropdownStyle}
|
||||||
>
|
>
|
||||||
<ul className="py-1.5">
|
<ul className="py-1.5">
|
||||||
@ -177,7 +177,7 @@ export const Pagination = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handlePrevious}
|
onClick={handlePrevious}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 bg-white border border-[rgba(0,0,0,0.08)] rounded text-xs text-[#0f1724] hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px] min-w-[44px]"
|
className="flex items-center gap-1 px-3 py-1.5 border border-[rgba(0,0,0,0.08)] rounded text-xs text-[#0f1724] hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-3.5 h-3.5" />
|
<ChevronLeft className="w-3.5 h-3.5" />
|
||||||
<span className="hidden sm:inline">Previous</span>
|
<span className="hidden sm:inline">Previous</span>
|
||||||
@ -191,7 +191,7 @@ export const Pagination = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
disabled={currentPage >= totalPages}
|
disabled={currentPage >= totalPages}
|
||||||
className="flex items-center gap-1 px-3 py-1.5 text-white rounded text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px] min-w-[44px]"
|
className="flex items-center gap-1 px-3 py-1.5 text-white rounded text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
style={currentPage < totalPages ? { backgroundColor: primaryColor } : { backgroundColor: '#112868', opacity: 0.5 }}
|
style={currentPage < totalPages ? { backgroundColor: primaryColor } : { backgroundColor: '#112868', opacity: 0.5 }}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (currentPage < totalPages) {
|
if (currentPage < totalPages) {
|
||||||
|
|||||||
@ -1,31 +1,32 @@
|
|||||||
import type { ReactElement, ButtonHTMLAttributes } from 'react';
|
import type { ReactElement, ButtonHTMLAttributes } from "react";
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from "@/lib/utils";
|
||||||
import { useAppTheme } from '@/hooks/useAppTheme';
|
import { useAppTheme } from "@/hooks/useAppTheme";
|
||||||
|
|
||||||
const primaryButtonVariants = cva(
|
const primaryButtonVariants = cva(
|
||||||
'inline-flex items-center justify-center px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer',
|
"inline-flex items-center justify-center gap-2 px-4 py-2 rounded text-[14px] font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer leading-normal",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
default: 'h-10',
|
default: "h-10",
|
||||||
small: 'h-8',
|
small: "h-8",
|
||||||
large: 'h-12',
|
large: "h-12",
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-[#112868] text-[#23dce1] hover:bg-[#23dce1] hover:text-[#112868]',
|
default: "",
|
||||||
disabled: 'bg-[#112868] text-[#23dce1] opacity-50',
|
disabled: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
size: 'default',
|
size: "default",
|
||||||
variant: 'default',
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface PrimaryButtonProps
|
interface PrimaryButtonProps
|
||||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
extends
|
||||||
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof primaryButtonVariants> {
|
VariantProps<typeof primaryButtonVariants> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@ -38,34 +39,28 @@ export const PrimaryButton = ({
|
|||||||
disabled,
|
disabled,
|
||||||
...props
|
...props
|
||||||
}: PrimaryButtonProps): ReactElement => {
|
}: PrimaryButtonProps): ReactElement => {
|
||||||
const buttonVariant = disabled ? 'disabled' : variant || 'default';
|
const buttonVariant = disabled ? "disabled" : variant || "default";
|
||||||
const { primaryColor, secondaryColor } = useAppTheme();
|
const { primaryColor, secondaryColor } = useAppTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={cn(primaryButtonVariants({ size, variant: buttonVariant }), className)}
|
className={cn(
|
||||||
style={
|
primaryButtonVariants({ size, variant: buttonVariant }),
|
||||||
buttonVariant === 'default'
|
className,
|
||||||
? {
|
)}
|
||||||
backgroundColor: primaryColor,
|
style={{
|
||||||
color: secondaryColor,
|
backgroundColor: primaryColor, // #112868
|
||||||
}
|
color: secondaryColor,
|
||||||
: buttonVariant === 'disabled'
|
opacity: buttonVariant === "disabled" ? 0.5 : 1,
|
||||||
? {
|
}}
|
||||||
backgroundColor: primaryColor,
|
|
||||||
color: secondaryColor,
|
|
||||||
opacity: 0.5,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (buttonVariant === 'default' && !disabled) {
|
if (!disabled) {
|
||||||
e.currentTarget.style.backgroundColor = secondaryColor;
|
e.currentTarget.style.backgroundColor = secondaryColor;
|
||||||
e.currentTarget.style.color = primaryColor;
|
e.currentTarget.style.color = primaryColor;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
if (buttonVariant === 'default' && !disabled) {
|
if (!disabled) {
|
||||||
e.currentTarget.style.backgroundColor = primaryColor;
|
e.currentTarget.style.backgroundColor = primaryColor;
|
||||||
e.currentTarget.style.color = secondaryColor;
|
e.currentTarget.style.color = secondaryColor;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,26 @@
|
|||||||
import type { ReactElement, ButtonHTMLAttributes } from 'react';
|
import type { ReactElement, ButtonHTMLAttributes } from "react";
|
||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from "@/lib/utils";
|
||||||
import { useAppTheme } from '@/hooks/useAppTheme';
|
import { useAppTheme } from "@/hooks/useAppTheme";
|
||||||
|
|
||||||
const secondaryButtonVariants = cva(
|
const secondaryButtonVariants = cva(
|
||||||
'inline-flex items-center justify-center px-3 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer',
|
"inline-flex items-center justify-center gap-2 px-4 py-2 rounded text-[14px] font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer leading-normal",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-[#23dce1] text-[#112868] hover:bg-[#112868] hover:text-[#23dce1]',
|
default: "",
|
||||||
disabled: 'bg-[#23dce1] text-[#112868] opacity-50',
|
disabled: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'default',
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface SecondaryButtonProps
|
interface SecondaryButtonProps
|
||||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
extends
|
||||||
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof secondaryButtonVariants> {
|
VariantProps<typeof secondaryButtonVariants> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@ -31,34 +32,28 @@ export const SecondaryButton = ({
|
|||||||
disabled,
|
disabled,
|
||||||
...props
|
...props
|
||||||
}: SecondaryButtonProps): ReactElement => {
|
}: SecondaryButtonProps): ReactElement => {
|
||||||
const buttonVariant = disabled ? 'disabled' : variant || 'default';
|
const buttonVariant = disabled ? "disabled" : variant || "default";
|
||||||
const { primaryColor, secondaryColor } = useAppTheme();
|
const { primaryColor, secondaryColor } = useAppTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={cn(secondaryButtonVariants({ variant: buttonVariant }), className)}
|
className={cn(
|
||||||
style={
|
secondaryButtonVariants({ variant: buttonVariant }),
|
||||||
buttonVariant === 'default'
|
className,
|
||||||
? {
|
)}
|
||||||
backgroundColor: secondaryColor,
|
style={{
|
||||||
color: primaryColor,
|
backgroundColor: secondaryColor, // #112868
|
||||||
}
|
color: primaryColor,
|
||||||
: buttonVariant === 'disabled'
|
opacity: buttonVariant === "disabled" ? 0.5 : 1,
|
||||||
? {
|
}}
|
||||||
backgroundColor: secondaryColor,
|
|
||||||
color: primaryColor,
|
|
||||||
opacity: 0.5,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (buttonVariant === 'default' && !disabled) {
|
if (!disabled) {
|
||||||
e.currentTarget.style.backgroundColor = primaryColor;
|
e.currentTarget.style.backgroundColor = primaryColor;
|
||||||
e.currentTarget.style.color = secondaryColor;
|
e.currentTarget.style.color = secondaryColor;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
if (buttonVariant === 'default' && !disabled) {
|
if (!disabled) {
|
||||||
e.currentTarget.style.backgroundColor = secondaryColor;
|
e.currentTarget.style.backgroundColor = secondaryColor;
|
||||||
e.currentTarget.style.color = primaryColor;
|
e.currentTarget.style.color = primaryColor;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -324,7 +324,7 @@ export const SupplierModal = ({
|
|||||||
}
|
}
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
footer={
|
footer={
|
||||||
<div className="p-3 flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@ -348,7 +348,7 @@ export const SupplierModal = ({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="py-10 text-center text-[#6b7280]">Loading...</div>
|
<div className="py-10 text-center text-[#6b7280]">Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 p-5">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
{/* Basic Information */}
|
{/* Basic Information */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h4 className="text-sm font-semibold text-[#112868] border-b pb-2">
|
<h4 className="text-sm font-semibold text-[#112868] border-b pb-2">
|
||||||
|
|||||||
@ -170,7 +170,7 @@ export const SuppliersTable = ({
|
|||||||
key: "supplier_type",
|
key: "supplier_type",
|
||||||
label: "Type",
|
label: "Type",
|
||||||
render: (supplier) => (
|
render: (supplier) => (
|
||||||
<span className="text-sm text-[#4b5563] capitalize">
|
<span className="">
|
||||||
{supplier.supplier_type.replace(/_/g, " ")}
|
{supplier.supplier_type.replace(/_/g, " ")}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -233,10 +233,10 @@ export const SuppliersTable = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const mobileCardRenderer = (supplier: Supplier) => (
|
const mobileCardRenderer = (supplier: Supplier) => (
|
||||||
<div className="p-4 border-b border-[rgba(0,0,0,0.08)]">
|
<div className="p-4 border-b border-[#D1D5DB]">
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 bg-gray-50 border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center">
|
<div className="w-10 h-10 bg-gray-50 border border-[#D1D5DB] rounded-lg flex items-center justify-center">
|
||||||
<Building2 className="w-5 h-5 text-[#9aa6b2]" />
|
<Building2 className="w-5 h-5 text-[#9aa6b2]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -269,7 +269,7 @@ export const SuppliersTable = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{showHeader && (
|
{showHeader && (
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 py-3 bg-white border-b border-[rgba(0,0,0,0.08)]">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 pb-2 border-b border-[#D1D5DB]">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
|
||||||
<SearchBox
|
<SearchBox
|
||||||
@ -307,7 +307,7 @@ export const SuppliersTable = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={suppliers}
|
data={suppliers}
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export const ViewAuditLogModal = ({
|
|||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-5">
|
<div>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
||||||
|
|||||||
@ -89,13 +89,13 @@ export const ViewRoleModal = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
|
<div className="bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
|
||||||
<p className="text-sm text-[#ef4444]">{error}</p>
|
<p className="text-sm text-[#ef4444]">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !error && role && (
|
{!isLoading && !error && role && (
|
||||||
<div className="p-5 flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Basic Information */}
|
{/* Basic Information */}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3>
|
<h3 className="text-sm font-semibold text-[#0e1b2a]">Basic Information</h3>
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export const ViewUserModal = ({
|
|||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-5">
|
<div>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
||||||
|
|||||||
@ -669,10 +669,10 @@ export const WorkflowDefinitionModal = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col h-full min-h-[500px]">
|
<div className="flex flex-col">
|
||||||
{/* Tabs - Only show when creating new workflow */}
|
{/* Tabs - Only show when creating new workflow */}
|
||||||
{!isEdit && (
|
{!isEdit && (
|
||||||
<div className="flex border-b border-[rgba(0,0,0,0.08)] bg-white sticky top-0 z-10">
|
<div className="flex border-b border-[rgba(0,0,0,0.08)] bg-white sticky top-0 z-10 mb-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveTab("general")}
|
onClick={() => setActiveTab("general")}
|
||||||
@ -698,7 +698,7 @@ export const WorkflowDefinitionModal = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<div className="p-6 flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{(activeTab === "general" || isEdit) && (
|
{(activeTab === "general" || isEdit) && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@ -97,7 +97,7 @@ export const WorkflowDefinitionViewModal = ({
|
|||||||
title="Workflow Definition"
|
title="Workflow Definition"
|
||||||
maxWidth="xl"
|
maxWidth="xl"
|
||||||
>
|
>
|
||||||
<div className="p-6 min-h-[400px]">
|
<div className="">
|
||||||
{/* Loading */}
|
{/* Loading */}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex flex-col items-center justify-center h-64 gap-3">
|
<div className="flex flex-col items-center justify-center h-64 gap-3">
|
||||||
@ -116,7 +116,7 @@ export const WorkflowDefinitionViewModal = ({
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{definition && !isLoading && (
|
{definition && !isLoading && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{/* ── Header Info ─────────────────────────────── */}
|
{/* ── Header Info ─────────────────────────────── */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 p-4 bg-[#f8fafc] rounded-xl border border-[rgba(0,0,0,0.06)]">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 p-4 bg-[#f8fafc] rounded-xl border border-[rgba(0,0,0,0.06)]">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { useState, useEffect, type ReactElement } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
type ReactElement,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
PrimaryButton,
|
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
DataTable,
|
DataTable,
|
||||||
Pagination,
|
Pagination,
|
||||||
@ -13,7 +18,7 @@ import {
|
|||||||
type Column,
|
type Column,
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import { Plus, Play, Power, Trash2, Copy, Edit, Eye } from "lucide-react";
|
import { Play, Power, Trash2, Copy, Edit, Eye } from "lucide-react";
|
||||||
import { workflowService } from "@/services/workflow-service";
|
import { workflowService } from "@/services/workflow-service";
|
||||||
import type { WorkflowDefinition } from "@/types/workflow";
|
import type { WorkflowDefinition } from "@/types/workflow";
|
||||||
import { showToast } from "@/utils/toast";
|
import { showToast } from "@/utils/toast";
|
||||||
@ -21,6 +26,11 @@ import type { RootState } from "@/store/store";
|
|||||||
import { formatDate } from "@/utils/format-date";
|
import { formatDate } from "@/utils/format-date";
|
||||||
import CodeBadge from "./CodeBadge";
|
import CodeBadge from "./CodeBadge";
|
||||||
|
|
||||||
|
export interface WorkflowDefinitionsTableRef {
|
||||||
|
openNewModal: () => void;
|
||||||
|
refresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface WorkflowDefinitionsTableProps {
|
interface WorkflowDefinitionsTableProps {
|
||||||
tenantId?: string | null; // If provided, use this tenantId (Super Admin mode)
|
tenantId?: string | null; // If provided, use this tenantId (Super Admin mode)
|
||||||
compact?: boolean; // Compact mode for tabs
|
compact?: boolean; // Compact mode for tabs
|
||||||
@ -28,380 +38,393 @@ interface WorkflowDefinitionsTableProps {
|
|||||||
entityType?: string; // Filter by entity type
|
entityType?: string; // Filter by entity type
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkflowDefinitionsTable = ({
|
const WorkflowDefinitionsTable = forwardRef<
|
||||||
tenantId: tenantId,
|
WorkflowDefinitionsTableRef,
|
||||||
compact = false,
|
WorkflowDefinitionsTableProps
|
||||||
showHeader = true,
|
>(
|
||||||
entityType,
|
(
|
||||||
}: WorkflowDefinitionsTableProps): ReactElement => {
|
{ tenantId: tenantId, compact = false, showHeader = true, entityType },
|
||||||
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId);
|
ref,
|
||||||
const effectiveTenantId = tenantId || reduxTenantId || undefined;
|
): ReactElement => {
|
||||||
|
const reduxTenantId = useSelector(
|
||||||
|
(state: RootState) => state.auth.tenantId,
|
||||||
|
);
|
||||||
|
const effectiveTenantId = tenantId || reduxTenantId || undefined;
|
||||||
|
|
||||||
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
|
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [limit, setLimit] = useState<number>(compact ? 10 : 10);
|
const [limit, setLimit] = useState<number>(compact ? 10 : 10);
|
||||||
const [totalItems, setTotalItems] = useState<number>(0);
|
const [totalItems, setTotalItems] = useState<number>(0);
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState<string>("");
|
const [debouncedSearchQuery, setDebouncedSearchQuery] =
|
||||||
|
useState<string>("");
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [selectedDefinition, setSelectedDefinition] =
|
const [selectedDefinition, setSelectedDefinition] =
|
||||||
useState<WorkflowDefinition | null>(null);
|
useState<WorkflowDefinition | null>(null);
|
||||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
|
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
|
||||||
const [viewDefinitionId, setViewDefinitionId] = useState<string | null>(null);
|
const [viewDefinitionId, setViewDefinitionId] = useState<string | null>(
|
||||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
null,
|
||||||
|
);
|
||||||
const fetchDefinitions = async () => {
|
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||||
try {
|
// Expose imperative methods
|
||||||
setIsLoading(true);
|
useImperativeHandle(ref, () => ({
|
||||||
setError(null);
|
openNewModal: () => {
|
||||||
const response = await workflowService.listDefinitions({
|
setSelectedDefinition(null);
|
||||||
tenantId: effectiveTenantId,
|
setIsModalOpen(true);
|
||||||
entity_type: entityType,
|
|
||||||
status: statusFilter || undefined,
|
|
||||||
limit,
|
|
||||||
offset: (currentPage - 1) * limit,
|
|
||||||
search: debouncedSearchQuery || undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
setDefinitions(response.data);
|
|
||||||
setTotalItems(response.pagination?.total || response.data.length);
|
|
||||||
} else {
|
|
||||||
setError("Failed to load workflow definitions");
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(
|
|
||||||
err?.response?.data?.error?.message ||
|
|
||||||
"Failed to load workflow definitions",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Debouncing search query
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedSearchQuery(searchQuery);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [searchQuery]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentPage(1);
|
|
||||||
}, [debouncedSearchQuery, statusFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDefinitions();
|
|
||||||
}, [
|
|
||||||
effectiveTenantId,
|
|
||||||
statusFilter,
|
|
||||||
currentPage,
|
|
||||||
limit,
|
|
||||||
debouncedSearchQuery,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (!selectedDefinition) return;
|
|
||||||
try {
|
|
||||||
setIsActionLoading(true);
|
|
||||||
const response = await workflowService.deleteDefinition(
|
|
||||||
selectedDefinition.id,
|
|
||||||
effectiveTenantId,
|
|
||||||
);
|
|
||||||
if (response.success) {
|
|
||||||
showToast.success("Workflow definition deleted successfully");
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
fetchDefinitions();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
showToast.error(
|
|
||||||
err?.response?.data?.error?.message ||
|
|
||||||
"Failed to delete workflow definition",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsActionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleActivate = async (id: string) => {
|
|
||||||
try {
|
|
||||||
setIsActionLoading(true);
|
|
||||||
const response = await workflowService.activateDefinition(
|
|
||||||
id,
|
|
||||||
effectiveTenantId,
|
|
||||||
);
|
|
||||||
if (response.success) {
|
|
||||||
showToast.success("Workflow definition activated");
|
|
||||||
fetchDefinitions();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
showToast.error(
|
|
||||||
err?.response?.data?.error?.message || "Failed to activate",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsActionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeprecate = async (id: string) => {
|
|
||||||
try {
|
|
||||||
setIsActionLoading(true);
|
|
||||||
const response = await workflowService.deprecateDefinition(
|
|
||||||
id,
|
|
||||||
effectiveTenantId,
|
|
||||||
);
|
|
||||||
if (response.success) {
|
|
||||||
showToast.success("Workflow definition deprecated");
|
|
||||||
fetchDefinitions();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
showToast.error(
|
|
||||||
err?.response?.data?.error?.message || "Failed to deprecate",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsActionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClone = async (id: string, name: string) => {
|
|
||||||
try {
|
|
||||||
setIsActionLoading(true);
|
|
||||||
const response = await workflowService.cloneDefinition(
|
|
||||||
id,
|
|
||||||
`${name} (Clone)`,
|
|
||||||
effectiveTenantId,
|
|
||||||
);
|
|
||||||
if (response.success) {
|
|
||||||
showToast.success("Workflow definition cloned");
|
|
||||||
fetchDefinitions();
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
showToast.error(err?.response?.data?.error?.message || "Failed to clone");
|
|
||||||
} finally {
|
|
||||||
setIsActionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: Column<WorkflowDefinition>[] = [
|
|
||||||
{
|
|
||||||
key: "name",
|
|
||||||
label: "Workflow Component",
|
|
||||||
render: (wf) => (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium text-[#0f1724]">{wf.name}</span>
|
|
||||||
<span className="text-xs text-[#6b7280] font-mono">{wf.code}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "entity_type",
|
|
||||||
label: "Entity Type",
|
|
||||||
render: (wf) => <CodeBadge label={wf.entity_type} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "version",
|
|
||||||
label: "Version",
|
|
||||||
render: (wf) => (
|
|
||||||
<span className="text-sm text-[#6b7280]">v{wf.version}</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "status",
|
|
||||||
label: "Status",
|
|
||||||
render: (wf) => {
|
|
||||||
let variant: "success" | "failure" | "info" | "process" = "info";
|
|
||||||
if (wf.status === "active") variant = "success";
|
|
||||||
if (wf.status === "deprecated") variant = "failure";
|
|
||||||
if (wf.status === "draft") variant = "process";
|
|
||||||
|
|
||||||
return <StatusBadge variant={variant}>{wf.status}</StatusBadge>;
|
|
||||||
},
|
},
|
||||||
},
|
refresh: () => {
|
||||||
{
|
fetchDefinitions();
|
||||||
key: "source_module",
|
},
|
||||||
label: "Module",
|
}));
|
||||||
render: (wf) => (
|
|
||||||
<span className="text-sm text-[#6b7280]">
|
|
||||||
{wf.source_module?.join(", ")}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "created_at",
|
|
||||||
label: "Created Date",
|
|
||||||
render: (wf) => (
|
|
||||||
<span className="text-sm text-[#6b7280]">
|
|
||||||
{formatDate(wf.created_at)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "actions",
|
|
||||||
label: "Actions",
|
|
||||||
align: "right",
|
|
||||||
render: (wf) => (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<ActionDropdown
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
icon: <Copy className="w-4 h-4" />,
|
|
||||||
label: "Clone",
|
|
||||||
onClick: () => handleClone(wf.id, wf.name),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Eye className="w-4 h-4" />,
|
|
||||||
label: "View",
|
|
||||||
onClick: () => {
|
|
||||||
setViewDefinitionId(wf.id);
|
|
||||||
setIsViewModalOpen(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Edit className="w-4 h-4" />,
|
|
||||||
label: "Edit",
|
|
||||||
onClick: () => {
|
|
||||||
setSelectedDefinition(wf);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
(wf.status === "draft" || wf.status === "deprecated") ? {
|
|
||||||
icon: <Play className="w-4 h-4" />,
|
|
||||||
label: "Activate",
|
|
||||||
onClick: () => handleActivate(wf.id),
|
|
||||||
} : null,
|
|
||||||
wf.status === "active" ? {
|
|
||||||
icon: <Power className="w-4 h-4" />,
|
|
||||||
label: "Deprecate",
|
|
||||||
onClick: () => handleDeprecate(wf.id),
|
|
||||||
} : null,
|
|
||||||
{
|
|
||||||
icon: <Trash2 className="w-4 h-4" />,
|
|
||||||
label: "Delete",
|
|
||||||
variant: "danger",
|
|
||||||
onClick: () => {
|
|
||||||
setSelectedDefinition(wf);
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
].filter((a): a is any => a !== null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
const fetchDefinitions = async () => {
|
||||||
<div
|
try {
|
||||||
className={`flex flex-col gap-4 ${!compact ? "bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm" : ""}`}
|
setIsLoading(true);
|
||||||
>
|
setError(null);
|
||||||
{showHeader && (
|
const response = await workflowService.listDefinitions({
|
||||||
<div className="p-4 border-b border-[rgba(0,0,0,0.08)] flex flex-col sm:flex-row items-center justify-between gap-4">
|
tenantId: effectiveTenantId,
|
||||||
<div className="flex items-center gap-3 w-full sm:w-auto">
|
entity_type: entityType,
|
||||||
<SearchBox
|
status: statusFilter || undefined,
|
||||||
value={searchQuery}
|
limit,
|
||||||
onChange={setSearchQuery}
|
offset: (currentPage - 1) * limit,
|
||||||
placeholder="Search name, code or description"
|
search: debouncedSearchQuery || undefined,
|
||||||
/>
|
});
|
||||||
<FilterDropdown
|
|
||||||
label="Status"
|
if (response.success) {
|
||||||
options={[
|
setDefinitions(response.data);
|
||||||
{ value: "active", label: "Active" },
|
setTotalItems(response.pagination?.total || response.data.length);
|
||||||
{ value: "draft", label: "Draft" },
|
} else {
|
||||||
{ value: "deprecated", label: "Deprecated" },
|
setError("Failed to load workflow definitions");
|
||||||
]}
|
}
|
||||||
value={statusFilter || ""}
|
} catch (err: any) {
|
||||||
onChange={(value) =>
|
setError(
|
||||||
setStatusFilter(
|
err?.response?.data?.error?.message ||
|
||||||
value ? (Array.isArray(value) ? value[0] : value) : null,
|
"Failed to load workflow definitions",
|
||||||
)
|
);
|
||||||
}
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debouncing search query
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedSearchQuery(searchQuery);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [debouncedSearchQuery, statusFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDefinitions();
|
||||||
|
}, [
|
||||||
|
effectiveTenantId,
|
||||||
|
statusFilter,
|
||||||
|
currentPage,
|
||||||
|
limit,
|
||||||
|
debouncedSearchQuery,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!selectedDefinition) return;
|
||||||
|
try {
|
||||||
|
setIsActionLoading(true);
|
||||||
|
const response = await workflowService.deleteDefinition(
|
||||||
|
selectedDefinition.id,
|
||||||
|
effectiveTenantId,
|
||||||
|
);
|
||||||
|
if (response.success) {
|
||||||
|
showToast.success("Workflow definition deleted successfully");
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
fetchDefinitions();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error(
|
||||||
|
err?.response?.data?.error?.message ||
|
||||||
|
"Failed to delete workflow definition",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActivate = async (id: string) => {
|
||||||
|
try {
|
||||||
|
setIsActionLoading(true);
|
||||||
|
const response = await workflowService.activateDefinition(
|
||||||
|
id,
|
||||||
|
effectiveTenantId,
|
||||||
|
);
|
||||||
|
if (response.success) {
|
||||||
|
showToast.success("Workflow definition activated");
|
||||||
|
fetchDefinitions();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error(
|
||||||
|
err?.response?.data?.error?.message || "Failed to activate",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeprecate = async (id: string) => {
|
||||||
|
try {
|
||||||
|
setIsActionLoading(true);
|
||||||
|
const response = await workflowService.deprecateDefinition(
|
||||||
|
id,
|
||||||
|
effectiveTenantId,
|
||||||
|
);
|
||||||
|
if (response.success) {
|
||||||
|
showToast.success("Workflow definition deprecated");
|
||||||
|
fetchDefinitions();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error(
|
||||||
|
err?.response?.data?.error?.message || "Failed to deprecate",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClone = async (id: string, name: string) => {
|
||||||
|
try {
|
||||||
|
setIsActionLoading(true);
|
||||||
|
const response = await workflowService.cloneDefinition(
|
||||||
|
id,
|
||||||
|
`${name} (Clone)`,
|
||||||
|
effectiveTenantId,
|
||||||
|
);
|
||||||
|
if (response.success) {
|
||||||
|
showToast.success("Workflow definition cloned");
|
||||||
|
fetchDefinitions();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error(
|
||||||
|
err?.response?.data?.error?.message || "Failed to clone",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: Column<WorkflowDefinition>[] = [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "Workflow Component",
|
||||||
|
render: (wf) => (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium text-[#0f1724]">
|
||||||
|
{wf.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-[#6b7280] font-mono">{wf.code}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "entity_type",
|
||||||
|
label: "Entity Type",
|
||||||
|
render: (wf) => <CodeBadge label={wf.entity_type} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "version",
|
||||||
|
label: "Version",
|
||||||
|
render: (wf) => (
|
||||||
|
<span className="text-sm text-[#6b7280]">v{wf.version}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "Status",
|
||||||
|
render: (wf) => {
|
||||||
|
let variant: "success" | "failure" | "info" | "process" = "info";
|
||||||
|
if (wf.status === "active") variant = "success";
|
||||||
|
if (wf.status === "deprecated") variant = "failure";
|
||||||
|
if (wf.status === "draft") variant = "process";
|
||||||
|
|
||||||
|
return <StatusBadge variant={variant}>{wf.status}</StatusBadge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "source_module",
|
||||||
|
label: "Module",
|
||||||
|
render: (wf) => (
|
||||||
|
<span className="text-sm text-[#6b7280]">
|
||||||
|
{wf.source_module?.join(", ")}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "created_at",
|
||||||
|
label: "Created Date",
|
||||||
|
render: (wf) => (
|
||||||
|
<span className="text-sm text-[#6b7280]">
|
||||||
|
{formatDate(wf.created_at)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
label: "Actions",
|
||||||
|
align: "right",
|
||||||
|
render: (wf) => (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<ActionDropdown
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
icon: <Copy className="w-4 h-4" />,
|
||||||
|
label: "Clone",
|
||||||
|
onClick: () => handleClone(wf.id, wf.name),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Eye className="w-4 h-4" />,
|
||||||
|
label: "View",
|
||||||
|
onClick: () => {
|
||||||
|
setViewDefinitionId(wf.id);
|
||||||
|
setIsViewModalOpen(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Edit className="w-4 h-4" />,
|
||||||
|
label: "Edit",
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedDefinition(wf);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wf.status === "draft" || wf.status === "deprecated"
|
||||||
|
? {
|
||||||
|
icon: <Play className="w-4 h-4" />,
|
||||||
|
label: "Activate",
|
||||||
|
onClick: () => handleActivate(wf.id),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
wf.status === "active"
|
||||||
|
? {
|
||||||
|
icon: <Power className="w-4 h-4" />,
|
||||||
|
label: "Deprecate",
|
||||||
|
onClick: () => handleDeprecate(wf.id),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
{
|
||||||
|
icon: <Trash2 className="w-4 h-4" />,
|
||||||
|
label: "Delete",
|
||||||
|
variant: "danger",
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedDefinition(wf);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].filter((a): a is any => a !== null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PrimaryButton
|
),
|
||||||
size="default"
|
},
|
||||||
className="flex items-center gap-2 w-full sm:w-auto"
|
];
|
||||||
onClick={() => {
|
|
||||||
setSelectedDefinition(null);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
<span>New Workflow</span>
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DataTable
|
return (
|
||||||
data={definitions}
|
<div className={`flex flex-col gap-2`}>
|
||||||
columns={columns}
|
{showHeader && (
|
||||||
keyExtractor={(wf) => wf.id}
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||||
isLoading={isLoading}
|
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||||
error={error}
|
<SearchBox
|
||||||
emptyMessage="No workflow definitions found"
|
value={searchQuery}
|
||||||
/>
|
onChange={setSearchQuery}
|
||||||
|
placeholder="Search name, code or description"
|
||||||
|
/>
|
||||||
|
<FilterDropdown
|
||||||
|
label="Status"
|
||||||
|
options={[
|
||||||
|
{ value: "active", label: "Active" },
|
||||||
|
{ value: "draft", label: "Draft" },
|
||||||
|
{ value: "deprecated", label: "Deprecated" },
|
||||||
|
]}
|
||||||
|
value={statusFilter || ""}
|
||||||
|
onChange={(value) =>
|
||||||
|
setStatusFilter(
|
||||||
|
value ? (Array.isArray(value) ? value[0] : value) : null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{totalItems > 0 && (
|
<DataTable
|
||||||
<Pagination
|
data={definitions}
|
||||||
currentPage={currentPage}
|
columns={columns}
|
||||||
totalPages={Math.ceil(totalItems / limit)}
|
keyExtractor={(wf) => wf.id}
|
||||||
totalItems={totalItems}
|
isLoading={isLoading}
|
||||||
limit={limit}
|
error={error}
|
||||||
onPageChange={setCurrentPage}
|
emptyMessage="No workflow definitions found"
|
||||||
onLimitChange={(newLimit) => {
|
|
||||||
setLimit(newLimit);
|
|
||||||
setCurrentPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<DeleteConfirmationModal
|
{totalItems > 0 && (
|
||||||
isOpen={isDeleteModalOpen}
|
<Pagination
|
||||||
onClose={() => {
|
currentPage={currentPage}
|
||||||
setIsDeleteModalOpen(false);
|
totalPages={Math.ceil(totalItems / limit)}
|
||||||
setSelectedDefinition(null);
|
totalItems={totalItems}
|
||||||
}}
|
limit={limit}
|
||||||
onConfirm={handleDelete}
|
onPageChange={setCurrentPage}
|
||||||
title="Delete Workflow Definition"
|
onLimitChange={(newLimit) => {
|
||||||
message="Are you sure you want to delete this workflow definition? This action cannot be undone."
|
setLimit(newLimit);
|
||||||
itemName={selectedDefinition?.name || ""}
|
setCurrentPage(1);
|
||||||
isLoading={isActionLoading}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<WorkflowDefinitionModal
|
<DeleteConfirmationModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setSelectedDefinition(null);
|
setSelectedDefinition(null);
|
||||||
}}
|
}}
|
||||||
definition={selectedDefinition}
|
onConfirm={handleDelete}
|
||||||
tenantId={effectiveTenantId}
|
title="Delete Workflow Definition"
|
||||||
onSuccess={fetchDefinitions}
|
message="Are you sure you want to delete this workflow definition? This action cannot be undone."
|
||||||
initialEntityType={entityType}
|
itemName={selectedDefinition?.name || ""}
|
||||||
/>
|
isLoading={isActionLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
<WorkflowDefinitionViewModal
|
<WorkflowDefinitionModal
|
||||||
isOpen={isViewModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsViewModalOpen(false);
|
setIsModalOpen(false);
|
||||||
setViewDefinitionId(null);
|
setSelectedDefinition(null);
|
||||||
}}
|
}}
|
||||||
definitionId={viewDefinitionId}
|
definition={selectedDefinition}
|
||||||
tenantId={effectiveTenantId}
|
tenantId={effectiveTenantId}
|
||||||
/>
|
onSuccess={fetchDefinitions}
|
||||||
</div>
|
initialEntityType={entityType}
|
||||||
);
|
/>
|
||||||
};
|
|
||||||
|
<WorkflowDefinitionViewModal
|
||||||
|
isOpen={isViewModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsViewModalOpen(false);
|
||||||
|
setViewDefinitionId(null);
|
||||||
|
}}
|
||||||
|
definitionId={viewDefinitionId}
|
||||||
|
tenantId={effectiveTenantId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default WorkflowDefinitionsTable;
|
export default WorkflowDefinitionsTable;
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export type { TabItem } from './PageHeader';
|
|||||||
export { AuthenticatedImage } from './AuthenticatedImage';
|
export { AuthenticatedImage } from './AuthenticatedImage';
|
||||||
export * from './DepartmentModals';
|
export * from './DepartmentModals';
|
||||||
export * from './DesignationModals';
|
export * from './DesignationModals';
|
||||||
export { default as WorkflowDefinitionsTable } from './WorkflowDefinitionsTable';
|
export { default as WorkflowDefinitionsTable, type WorkflowDefinitionsTableRef } from './WorkflowDefinitionsTable';
|
||||||
export { WorkflowDefinitionModal } from './WorkflowDefinitionModal';
|
export { WorkflowDefinitionModal } from './WorkflowDefinitionModal';
|
||||||
export { WorkflowDefinitionViewModal } from './WorkflowDefinitionViewModal';
|
export { WorkflowDefinitionViewModal } from './WorkflowDefinitionViewModal';
|
||||||
export { SuppliersTable } from './SuppliersTable';
|
export { SuppliersTable } from './SuppliersTable';
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export const ApikeyReissueModal = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-5">
|
<div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export const DepartmentListView = ({
|
|||||||
onLimitChange,
|
onLimitChange,
|
||||||
}: DepartmentListViewProps): ReactElement => {
|
}: DepartmentListViewProps): ReactElement => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-2xl border-2 border-slate-50 shadow-sm overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={data}
|
data={data}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@ -250,7 +250,7 @@ export const DepartmentTreeView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 p-4 border border-[#D1D5DB] bg-white rounded-lg self-stretch">
|
<div className="flex flex-col gap-2 p-4 border border-[#D1D5DB] bg-white self-stretch">
|
||||||
{data.map((item) => (
|
{data.map((item) => (
|
||||||
<TreeItem
|
<TreeItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|||||||
@ -222,7 +222,7 @@ export const DepartmentsTable = forwardRef<
|
|||||||
key: "name",
|
key: "name",
|
||||||
label: "Department Name",
|
label: "Department Name",
|
||||||
render: (dept) => (
|
render: (dept) => (
|
||||||
<span className="text-sm font-medium text-[#0f1724]">
|
<span className="">
|
||||||
{dept.name}
|
{dept.name}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -236,7 +236,7 @@ export const DepartmentsTable = forwardRef<
|
|||||||
key: "parent_name",
|
key: "parent_name",
|
||||||
label: "Parent",
|
label: "Parent",
|
||||||
render: (dept) => (
|
render: (dept) => (
|
||||||
<span className="text-sm text-[#6b7280]">
|
<span className="">
|
||||||
{dept.parent_name || "-"}
|
{dept.parent_name || "-"}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -245,21 +245,21 @@ export const DepartmentsTable = forwardRef<
|
|||||||
key: "level",
|
key: "level",
|
||||||
label: "Level",
|
label: "Level",
|
||||||
render: (dept) => (
|
render: (dept) => (
|
||||||
<span className="text-sm text-[#6b7280]">{dept.level}</span>
|
<span className="">{dept.level}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "sort_order",
|
key: "sort_order",
|
||||||
label: "Order",
|
label: "Order",
|
||||||
render: (dept) => (
|
render: (dept) => (
|
||||||
<span className="text-sm text-[#6b7280]">{dept.sort_order}</span>
|
<span className="">{dept.sort_order}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "child_count",
|
key: "child_count",
|
||||||
label: "Sub-depts",
|
label: "Sub-depts",
|
||||||
render: (dept) => (
|
render: (dept) => (
|
||||||
<span className="text-sm text-[#6b7280]">
|
<span className="">
|
||||||
{dept.child_count || 0}
|
{dept.child_count || 0}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -268,7 +268,7 @@ export const DepartmentsTable = forwardRef<
|
|||||||
key: "user_count",
|
key: "user_count",
|
||||||
label: "Users",
|
label: "Users",
|
||||||
render: (dept) => (
|
render: (dept) => (
|
||||||
<span className="text-sm text-[#6b7280]">{dept.user_count || 0}</span>
|
<span className="">{dept.user_count || 0}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -306,11 +306,11 @@ export const DepartmentsTable = forwardRef<
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-4 `}>
|
<div className={`flex flex-col gap-2`}>
|
||||||
{showHeader && (
|
{showHeader && (
|
||||||
<div className="flex flex-col border-b border-[rgba(0,0,0,0.08)]">
|
<div className="flex flex-col border-b border-[rgba(0,0,0,0.08)]">
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="px-4 pt-3 flex items-center justify-between border-b border-transparent">
|
<div className="flex items-center justify-between border-b border-transparent">
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<button
|
<button
|
||||||
className="pb-3 text-sm font-medium transition-all relative"
|
className="pb-3 text-sm font-medium transition-all relative"
|
||||||
@ -344,7 +344,7 @@ export const DepartmentsTable = forwardRef<
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3 pb-2">
|
||||||
{viewMode === "list" && (
|
{viewMode === "list" && (
|
||||||
<SearchBox
|
<SearchBox
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { useState, useEffect, type ReactElement, forwardRef, useImperativeHandle } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
type ReactElement,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
PrimaryButton,
|
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
DataTable,
|
DataTable,
|
||||||
@ -15,10 +20,7 @@ import {
|
|||||||
EditDesignationModal,
|
EditDesignationModal,
|
||||||
ViewDesignationModal,
|
ViewDesignationModal,
|
||||||
} from "@/components/shared/DesignationModals";
|
} from "@/components/shared/DesignationModals";
|
||||||
import {
|
// import { Plus } from "lucide-react";
|
||||||
Plus,
|
|
||||||
// , Search
|
|
||||||
} from "lucide-react";
|
|
||||||
import { designationService } from "@/services/designation-service";
|
import { designationService } from "@/services/designation-service";
|
||||||
import type {
|
import type {
|
||||||
Designation,
|
Designation,
|
||||||
@ -42,298 +44,299 @@ interface DesignationsTableProps {
|
|||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DesignationsTable = forwardRef<DesignationsTableRef, DesignationsTableProps>(({
|
const DesignationsTable = forwardRef<
|
||||||
tenantId: propsTenantId,
|
DesignationsTableRef,
|
||||||
compact = false,
|
DesignationsTableProps
|
||||||
showHeader = true,
|
>(
|
||||||
}, ref): ReactElement => {
|
(
|
||||||
const { canCreate, canUpdate } = usePermissions();
|
{ tenantId: propsTenantId, compact = false, showHeader = true },
|
||||||
// const { primaryColor } = useAppTheme();
|
ref,
|
||||||
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId);
|
): ReactElement => {
|
||||||
const effectiveTenantId = propsTenantId || reduxTenantId;
|
const { canUpdate } = usePermissions();
|
||||||
|
// const { primaryColor } = useAppTheme();
|
||||||
|
const reduxTenantId = useSelector(
|
||||||
|
(state: RootState) => state.auth.tenantId,
|
||||||
|
);
|
||||||
|
const effectiveTenantId = propsTenantId || reduxTenantId;
|
||||||
|
|
||||||
const [designations, setDesignations] = useState<Designation[]>([]);
|
const [designations, setDesignations] = useState<Designation[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Expose imperative methods
|
// Expose imperative methods
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
openNewModal: () => setIsNewModalOpen(true),
|
openNewModal: () => setIsNewModalOpen(true),
|
||||||
refresh: () => fetchDesignations(),
|
refresh: () => fetchDesignations(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [limit, setLimit] = useState<number>(compact ? 10 : 5);
|
const [limit, setLimit] = useState<number>(compact ? 10 : 5);
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const [activeOnly, setActiveOnly] = useState<boolean>(false);
|
const [activeOnly, setActiveOnly] = useState<boolean>(false);
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState<string>("");
|
const [debouncedSearchQuery, setDebouncedSearchQuery] =
|
||||||
|
useState<string>("");
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
const [isNewModalOpen, setIsNewModalOpen] = useState(false);
|
const [isNewModalOpen, setIsNewModalOpen] = useState(false);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
|
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
|
||||||
// const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
// const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [selectedDesignation, setSelectedDesignation] =
|
const [selectedDesignation, setSelectedDesignation] =
|
||||||
useState<Designation | null>(null);
|
useState<Designation | null>(null);
|
||||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||||
|
|
||||||
const fetchDesignations = async () => {
|
const fetchDesignations = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await designationService.list(effectiveTenantId, {
|
const response = await designationService.list(effectiveTenantId, {
|
||||||
active_only: activeOnly,
|
active_only: activeOnly,
|
||||||
search: debouncedSearchQuery,
|
search: debouncedSearchQuery,
|
||||||
});
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setDesignations(response.data);
|
setDesignations(response.data);
|
||||||
} else {
|
} else {
|
||||||
setError("Failed to load designations");
|
setError("Failed to load designations");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(
|
||||||
|
err?.response?.data?.error?.message || "Failed to load designations",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
};
|
||||||
setError(
|
|
||||||
err?.response?.data?.error?.message || "Failed to load designations",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Debouncing search query
|
// Debouncing search query
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setDebouncedSearchQuery(searchQuery);
|
setDebouncedSearchQuery(searchQuery);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDesignations();
|
fetchDesignations();
|
||||||
}, [effectiveTenantId, activeOnly, debouncedSearchQuery]);
|
}, [effectiveTenantId, activeOnly, debouncedSearchQuery]);
|
||||||
|
|
||||||
const handleCreate = async (data: CreateDesignationRequest) => {
|
const handleCreate = async (data: CreateDesignationRequest) => {
|
||||||
try {
|
try {
|
||||||
setIsActionLoading(true);
|
setIsActionLoading(true);
|
||||||
const response = await designationService.create(data, effectiveTenantId);
|
const response = await designationService.create(
|
||||||
if (response.success) {
|
data,
|
||||||
showToast.success("Designation created successfully");
|
effectiveTenantId,
|
||||||
setIsNewModalOpen(false);
|
);
|
||||||
fetchDesignations();
|
if (response.success) {
|
||||||
|
showToast.success("Designation created successfully");
|
||||||
|
setIsNewModalOpen(false);
|
||||||
|
fetchDesignations();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error(
|
||||||
|
err?.response?.data?.error?.message || "Failed to create designation",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsActionLoading(false);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
};
|
||||||
showToast.error(
|
|
||||||
err?.response?.data?.error?.message || "Failed to create designation",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsActionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = async (id: string, data: UpdateDesignationRequest) => {
|
const handleUpdate = async (id: string, data: UpdateDesignationRequest) => {
|
||||||
try {
|
try {
|
||||||
setIsActionLoading(true);
|
setIsActionLoading(true);
|
||||||
const response = await designationService.update(
|
const response = await designationService.update(
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
effectiveTenantId,
|
effectiveTenantId,
|
||||||
);
|
);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
showToast.success("Designation updated successfully");
|
showToast.success("Designation updated successfully");
|
||||||
setIsEditModalOpen(false);
|
setIsEditModalOpen(false);
|
||||||
fetchDesignations();
|
fetchDesignations();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
showToast.error(
|
||||||
|
err?.response?.data?.error?.message || "Failed to update designation",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsActionLoading(false);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
};
|
||||||
showToast.error(
|
|
||||||
err?.response?.data?.error?.message || "Failed to update designation",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsActionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// const handleDelete = async () => {
|
// const handleDelete = async () => {
|
||||||
// if (!selectedDesignation) return;
|
// if (!selectedDesignation) return;
|
||||||
// try {
|
// try {
|
||||||
// setIsActionLoading(true);
|
// setIsActionLoading(true);
|
||||||
// const response = await designationService.delete(
|
// const response = await designationService.delete(
|
||||||
// selectedDesignation.id,
|
// selectedDesignation.id,
|
||||||
// effectiveTenantId,
|
// effectiveTenantId,
|
||||||
// );
|
// );
|
||||||
// if (response.success) {
|
// if (response.success) {
|
||||||
// showToast.success("Designation deleted successfully");
|
// showToast.success("Designation deleted successfully");
|
||||||
// setIsDeleteModalOpen(false);
|
// setIsDeleteModalOpen(false);
|
||||||
// fetchDesignations();
|
// fetchDesignations();
|
||||||
// }
|
// }
|
||||||
// } catch (err: any) {
|
// } catch (err: any) {
|
||||||
// showToast.error(
|
// showToast.error(
|
||||||
// err?.response?.data?.error?.message || "Failed to delete designation",
|
// err?.response?.data?.error?.message || "Failed to delete designation",
|
||||||
// );
|
// );
|
||||||
// } finally {
|
// } finally {
|
||||||
// setIsActionLoading(false);
|
// setIsActionLoading(false);
|
||||||
// }
|
// }
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// Client-side pagination logic
|
// Client-side pagination logic
|
||||||
const totalItems = designations.length;
|
const totalItems = designations.length;
|
||||||
const totalPages = Math.ceil(totalItems / limit);
|
const totalPages = Math.ceil(totalItems / limit);
|
||||||
const paginatedData = designations.slice(
|
const paginatedData = designations.slice(
|
||||||
(currentPage - 1) * limit,
|
(currentPage - 1) * limit,
|
||||||
currentPage * limit,
|
currentPage * limit,
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns: Column<Designation>[] = [
|
const columns: Column<Designation>[] = [
|
||||||
{
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
label: "Designation Name",
|
label: "Designation Name",
|
||||||
render: (desig) => (
|
render: (desig) => (
|
||||||
<span className="text-sm font-medium text-[#0f1724]">{desig.name}</span>
|
<span className="text-sm font-medium text-[#0f1724]">
|
||||||
),
|
{desig.name}
|
||||||
},
|
</span>
|
||||||
{
|
),
|
||||||
key: "code",
|
},
|
||||||
label: "Code",
|
{
|
||||||
render: (desig) => <CodeBadge label={desig.code} />,
|
key: "code",
|
||||||
},
|
label: "Code",
|
||||||
{
|
render: (desig) => <CodeBadge label={desig.code} />,
|
||||||
key: "level",
|
},
|
||||||
label: "Level",
|
{
|
||||||
render: (desig) => (
|
key: "level",
|
||||||
<span className="text-sm text-[#6b7280]">{desig.level}</span>
|
label: "Level",
|
||||||
),
|
render: (desig) => (
|
||||||
},
|
<span className="text-sm text-[#6b7280]">{desig.level}</span>
|
||||||
{
|
),
|
||||||
key: "sort_order",
|
},
|
||||||
label: "Order",
|
{
|
||||||
render: (desig) => (
|
key: "sort_order",
|
||||||
<span className="text-sm text-[#6b7280]">{desig.sort_order}</span>
|
label: "Order",
|
||||||
),
|
render: (desig) => (
|
||||||
},
|
<span className="text-sm text-[#6b7280]">{desig.sort_order}</span>
|
||||||
{
|
),
|
||||||
key: "user_count",
|
},
|
||||||
label: "Users",
|
{
|
||||||
render: (desig) => (
|
key: "user_count",
|
||||||
<span className="text-sm text-[#6b7280]">{desig.user_count || 0}</span>
|
label: "Users",
|
||||||
),
|
render: (desig) => (
|
||||||
},
|
<span className="text-sm text-[#6b7280]">
|
||||||
{
|
{desig.user_count || 0}
|
||||||
key: "status",
|
</span>
|
||||||
label: "Status",
|
),
|
||||||
render: (desig) => (
|
},
|
||||||
<StatusBadge variant={desig.is_active ? "success" : "failure"}>
|
{
|
||||||
{desig.is_active ? "Active" : "Inactive"}
|
key: "status",
|
||||||
</StatusBadge>
|
label: "Status",
|
||||||
),
|
render: (desig) => (
|
||||||
},
|
<StatusBadge variant={desig.is_active ? "success" : "failure"}>
|
||||||
{
|
{desig.is_active ? "Active" : "Inactive"}
|
||||||
key: "actions",
|
</StatusBadge>
|
||||||
label: "Actions",
|
),
|
||||||
align: "right",
|
},
|
||||||
render: (desig) => (
|
{
|
||||||
<div className="flex justify-end">
|
key: "actions",
|
||||||
<ActionDropdown
|
label: "Actions",
|
||||||
onView={() => {
|
align: "right",
|
||||||
setSelectedDesignation(desig);
|
render: (desig) => (
|
||||||
setIsViewModalOpen(true);
|
<div className="flex justify-end">
|
||||||
}}
|
<ActionDropdown
|
||||||
onEdit={
|
onView={() => {
|
||||||
canUpdate("designations")
|
setSelectedDesignation(desig);
|
||||||
? () => {
|
setIsViewModalOpen(true);
|
||||||
setSelectedDesignation(desig);
|
}}
|
||||||
setIsEditModalOpen(true);
|
onEdit={
|
||||||
}
|
canUpdate("designations")
|
||||||
: undefined
|
? () => {
|
||||||
}
|
setSelectedDesignation(desig);
|
||||||
/>
|
setIsEditModalOpen(true);
|
||||||
</div>
|
}
|
||||||
),
|
: undefined
|
||||||
},
|
}
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`flex flex-col gap-4 ${!compact ? "bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm" : ""}`}
|
|
||||||
>
|
|
||||||
{showHeader && (
|
|
||||||
<div className="p-4 border-b border-[rgba(0,0,0,0.08)] flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
||||||
<div className="flex items-center gap-3 w-full sm:w-auto">
|
|
||||||
<SearchBox
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={setSearchQuery}
|
|
||||||
placeholder="Search designations..."
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-4 ${!compact ? "bg-white" : ""}`}>
|
||||||
|
{showHeader && (
|
||||||
|
<div className="pb-2 border-b border-[rgba(0,0,0,0.08)] flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||||
|
<SearchBox
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={setSearchQuery}
|
||||||
|
placeholder="Search designations..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ActiveOnlyToggle
|
<ActiveOnlyToggle
|
||||||
activeOnly={activeOnly}
|
activeOnly={activeOnly}
|
||||||
onChange={(val) => setActiveOnly(val)}
|
onChange={(val) => setActiveOnly(val)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{canCreate("designations") && (
|
)}
|
||||||
<PrimaryButton
|
|
||||||
size="default"
|
|
||||||
className="flex items-center gap-2 w-full sm:w-auto"
|
|
||||||
onClick={() => setIsNewModalOpen(true)}
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
<span>New Designation</span>
|
|
||||||
</PrimaryButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={paginatedData}
|
data={paginatedData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
keyExtractor={(desig) => desig.id}
|
keyExtractor={(desig) => desig.id}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
emptyMessage="No designations found"
|
emptyMessage="No designations found"
|
||||||
/>
|
|
||||||
|
|
||||||
{totalItems > 0 && (
|
|
||||||
<Pagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
totalItems={totalItems}
|
|
||||||
limit={limit}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
onLimitChange={(newLimit) => {
|
|
||||||
setLimit(newLimit);
|
|
||||||
setCurrentPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<NewDesignationModal
|
{totalItems > 0 && (
|
||||||
isOpen={isNewModalOpen}
|
<Pagination
|
||||||
onClose={() => setIsNewModalOpen(false)}
|
currentPage={currentPage}
|
||||||
onSubmit={handleCreate}
|
totalPages={totalPages}
|
||||||
isLoading={isActionLoading}
|
totalItems={totalItems}
|
||||||
/>
|
limit={limit}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
onLimitChange={(newLimit) => {
|
||||||
|
setLimit(newLimit);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<EditDesignationModal
|
<NewDesignationModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isNewModalOpen}
|
||||||
onClose={() => {
|
onClose={() => setIsNewModalOpen(false)}
|
||||||
setIsEditModalOpen(false);
|
onSubmit={handleCreate}
|
||||||
setSelectedDesignation(null);
|
isLoading={isActionLoading}
|
||||||
}}
|
/>
|
||||||
designation={selectedDesignation}
|
|
||||||
onSubmit={handleUpdate}
|
|
||||||
isLoading={isActionLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ViewDesignationModal
|
<EditDesignationModal
|
||||||
isOpen={isViewModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsViewModalOpen(false);
|
setIsEditModalOpen(false);
|
||||||
setSelectedDesignation(null);
|
setSelectedDesignation(null);
|
||||||
}}
|
}}
|
||||||
designation={selectedDesignation}
|
designation={selectedDesignation}
|
||||||
/>
|
onSubmit={handleUpdate}
|
||||||
|
isLoading={isActionLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* <DeleteConfirmationModal
|
<ViewDesignationModal
|
||||||
|
isOpen={isViewModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsViewModalOpen(false);
|
||||||
|
setSelectedDesignation(null);
|
||||||
|
}}
|
||||||
|
designation={selectedDesignation}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <DeleteConfirmationModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
@ -345,8 +348,9 @@ const DesignationsTable = forwardRef<DesignationsTableRef, DesignationsTableProp
|
|||||||
itemName={selectedDesignation?.name || ""}
|
itemName={selectedDesignation?.name || ""}
|
||||||
isLoading={isActionLoading}
|
isLoading={isActionLoading}
|
||||||
/> */}
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default DesignationsTable;
|
export default DesignationsTable;
|
||||||
|
|||||||
@ -168,7 +168,7 @@ export const EditModuleModal = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-5">
|
<div>
|
||||||
{/* API Key Display Section (Only if webhookurl changed) */}
|
{/* API Key Display Section (Only if webhookurl changed) */}
|
||||||
{apiKey && (
|
{apiKey && (
|
||||||
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">
|
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">
|
||||||
|
|||||||
@ -339,7 +339,7 @@ export const NewModuleModal = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-5">
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
{/* API Key Display Section */}
|
{/* API Key Display Section */}
|
||||||
{apiKey && (
|
{apiKey && (
|
||||||
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">
|
<div className="mb-6 p-4 bg-[rgba(34,197,94,0.1)] border border-[#22c55e] rounded-md">
|
||||||
|
|||||||
@ -193,7 +193,7 @@
|
|||||||
// </>
|
// </>
|
||||||
// }
|
// }
|
||||||
// >
|
// >
|
||||||
// <form onSubmit={handleSubmit(handleFormSubmit)} className="p-5">
|
// <form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
// {/* General Error Display */}
|
// {/* General Error Display */}
|
||||||
// {errors.root && (
|
// {errors.root && (
|
||||||
// <div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
// <div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
||||||
|
|||||||
@ -240,7 +240,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
|||||||
key: "name",
|
key: "name",
|
||||||
label: "Name",
|
label: "Name",
|
||||||
render: (role) => (
|
render: (role) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="">
|
||||||
{role.name}
|
{role.name}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -254,7 +254,23 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
|||||||
key: "description",
|
key: "description",
|
||||||
label: "Description",
|
label: "Description",
|
||||||
render: (role) => (
|
render: (role) => (
|
||||||
<span className="text-sm font-normal text-[#6b7280]">
|
<span
|
||||||
|
style={{
|
||||||
|
display: "-webkit-box",
|
||||||
|
width: "auto",
|
||||||
|
maxWidth: "300px",
|
||||||
|
WebkitBoxOrient: "vertical",
|
||||||
|
WebkitLineClamp: 1,
|
||||||
|
overflow: "hidden",
|
||||||
|
color: "var(--Global-Colors-Text-text-primary, #0F1724)",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
fontFamily: "Figtree",
|
||||||
|
fontSize: "14px",
|
||||||
|
fontStyle: "normal",
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{role.description || "N/A"}
|
{role.description || "N/A"}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -272,7 +288,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
|||||||
key: "user_count",
|
key: "user_count",
|
||||||
label: "Users",
|
label: "Users",
|
||||||
render: (role) => (
|
render: (role) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="">
|
||||||
{role.user_count || 0}
|
{role.user_count || 0}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -281,7 +297,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
|||||||
key: "created_at",
|
key: "created_at",
|
||||||
label: "Created Date",
|
label: "Created Date",
|
||||||
render: (role) => (
|
render: (role) => (
|
||||||
<span className="text-sm font-normal text-[#6b7280]">
|
<span className="">
|
||||||
{formatDate(role.created_at)}
|
{formatDate(role.created_at)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -474,10 +490,10 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
{/* Table Header with Filters */}
|
{/* Table Header with Filters */}
|
||||||
{showHeader && (
|
{showHeader && (
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="pb-2.5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{/* Global Search */}
|
{/* Global Search */}
|
||||||
@ -519,14 +535,6 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Export Button */}
|
|
||||||
{/* <button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
<Download className="w-3.5 h-3.5" />
|
|
||||||
<span>Export</span>
|
|
||||||
</button> */}
|
|
||||||
|
|
||||||
{/* New Role Button */}
|
{/* New Role Button */}
|
||||||
{canCreate("roles") && (
|
{canCreate("roles") && (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -86,7 +86,7 @@ export const ViewModuleModal = ({
|
|||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-5">
|
<div>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
// </SecondaryButton>
|
// </SecondaryButton>
|
||||||
// }
|
// }
|
||||||
// >
|
// >
|
||||||
// <div className="p-5">
|
// <div>
|
||||||
// {isLoading && (
|
// {isLoading && (
|
||||||
// <div className="flex items-center justify-center py-12">
|
// <div className="flex items-center justify-center py-12">
|
||||||
// <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
// <Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
||||||
|
|||||||
@ -116,7 +116,7 @@ export const WebhookSyncModal = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-5">
|
<div>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const PromptTestCaseResultModal = ({
|
|||||||
description="Review LLM testing details, latency, and token consumption."
|
description="Review LLM testing details, latency, and token consumption."
|
||||||
maxWidth="2xl"
|
maxWidth="2xl"
|
||||||
>
|
>
|
||||||
<div className="p-6 flex flex-col gap-6 bg-slate-50/40 select-none">
|
<div className="flex flex-col gap-6 bg-slate-50/40 select-none">
|
||||||
{/* Performance & Usage Metrics Cards */}
|
{/* Performance & Usage Metrics Cards */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{/* Provider Card */}
|
{/* Provider Card */}
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Modal, DataTable, type Column, PrimaryButton, StatusBadge } from "@/components/shared";
|
import {
|
||||||
|
Modal,
|
||||||
|
DataTable,
|
||||||
|
type Column,
|
||||||
|
PrimaryButton,
|
||||||
|
StatusBadge,
|
||||||
|
} from "@/components/shared";
|
||||||
import { aiService } from "@/services/ai-service";
|
import { aiService } from "@/services/ai-service";
|
||||||
import type { AIPrompt } from "@/types/ai";
|
import type { AIPrompt } from "@/types/ai";
|
||||||
import { showToast } from "@/utils/toast";
|
import { showToast } from "@/utils/toast";
|
||||||
@ -61,7 +67,9 @@ export const PromptVersionsModal = ({
|
|||||||
{
|
{
|
||||||
key: "version",
|
key: "version",
|
||||||
label: "Version",
|
label: "Version",
|
||||||
render: (row) => <span className="font-semibold text-gray-900">v{row.version}</span>,
|
render: (row) => (
|
||||||
|
<span className="font-semibold text-gray-900">v{row.version}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
@ -75,7 +83,9 @@ export const PromptVersionsModal = ({
|
|||||||
{
|
{
|
||||||
key: "change_notes",
|
key: "change_notes",
|
||||||
label: "Change Notes",
|
label: "Change Notes",
|
||||||
render: (row) => <span className="text-xs text-gray-500">{row.change_notes}</span>,
|
render: (row) => (
|
||||||
|
<span className="text-xs text-gray-500">{row.change_notes}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// key: "created_by_email",
|
// key: "created_by_email",
|
||||||
@ -86,7 +96,9 @@ export const PromptVersionsModal = ({
|
|||||||
key: "updated_at",
|
key: "updated_at",
|
||||||
label: "Created At",
|
label: "Created At",
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<span className="text-xs text-gray-500">{formatDate(row.updated_at || row.created_at || "")}</span>
|
<span className="text-xs text-gray-500">
|
||||||
|
{formatDate(row.updated_at || row.created_at || "")}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -107,7 +119,9 @@ export const PromptVersionsModal = ({
|
|||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
)}
|
)}
|
||||||
{row.version === prompt?.version && (
|
{row.version === prompt?.version && (
|
||||||
<span className="text-[11px] font-medium text-gray-400 py-1 px-2 border border-gray-100 rounded bg-gray-50">Current</span>
|
<span className="text-[11px] font-medium text-gray-400 py-1 px-2 border border-gray-100 rounded bg-gray-50">
|
||||||
|
Current
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -122,7 +136,7 @@ export const PromptVersionsModal = ({
|
|||||||
description="View previous versions of this prompt and rollback if needed."
|
description="View previous versions of this prompt and rollback if needed."
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
>
|
>
|
||||||
<div className="border-t border-gray-100">
|
<div className="mx-3">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={versions}
|
data={versions}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import { Modal } from "@/components/shared";
|
import { Modal } from "@/components/shared";
|
||||||
import type { TenantAIConfig } from "@/types/ai";
|
import type { TenantAIConfig } from "@/types/ai";
|
||||||
|
import CodeBadge from "../shared/CodeBadge";
|
||||||
|
|
||||||
interface ViewAIProviderModalProps {
|
interface ViewAIProviderModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -19,7 +20,10 @@ export const ViewAIProviderModal = ({
|
|||||||
if (!val) return [];
|
if (!val) return [];
|
||||||
if (Array.isArray(val)) return val;
|
if (Array.isArray(val)) return val;
|
||||||
if (typeof val === "string") {
|
if (typeof val === "string") {
|
||||||
return val.split(",").map(s => s.trim()).filter(Boolean);
|
return val
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
@ -32,14 +36,12 @@ export const ViewAIProviderModal = ({
|
|||||||
description="View detailed settings for this AI Provider configuration."
|
description="View detailed settings for this AI Provider configuration."
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4 text-sm select-none p-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4 text-sm select-none">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5">
|
<span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5">
|
||||||
Provider
|
Provider
|
||||||
</span>
|
</span>
|
||||||
<span className="text-slate-800 font-medium">
|
<span className="text-slate-800 font-medium">{config.provider}</span>
|
||||||
{config.provider}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -55,9 +57,7 @@ export const ViewAIProviderModal = ({
|
|||||||
<span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5">
|
<span className="text-xs text-slate-400 font-semibold uppercase tracking-wider block mb-0.5">
|
||||||
Config Type
|
Config Type
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-[11px] font-bold uppercase tracking-wider bg-blue-50 text-blue-600 border border-blue-100 mt-1">
|
<CodeBadge className="uppercase" label={config.config_type || "direct"} />
|
||||||
{config.config_type || "direct"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -115,14 +115,16 @@ export const ViewAIProviderModal = ({
|
|||||||
</span>
|
</span>
|
||||||
{parseArray((config as any).custom_embedding_models).length > 0 ? (
|
{parseArray((config as any).custom_embedding_models).length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||||
{parseArray((config as any).custom_embedding_models).map((m: any, idx: any) => (
|
{parseArray((config as any).custom_embedding_models).map(
|
||||||
<span
|
(m: any, idx: any) => (
|
||||||
key={idx}
|
<span
|
||||||
className="px-2 py-0.5 bg-slate-50 text-slate-700 rounded text-xs font-medium border border-slate-200"
|
key={idx}
|
||||||
>
|
className="px-2 py-0.5 bg-slate-50 text-slate-700 rounded text-xs font-medium border border-slate-200"
|
||||||
{m}
|
>
|
||||||
</span>
|
{m}
|
||||||
))}
|
</span>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-slate-800 font-medium">—</span>
|
<span className="text-slate-800 font-medium">—</span>
|
||||||
|
|||||||
@ -15,8 +15,10 @@ import {
|
|||||||
FilterDropdown,
|
FilterDropdown,
|
||||||
Pagination,
|
Pagination,
|
||||||
type Column,
|
type Column,
|
||||||
|
DeleteConfirmationModal,
|
||||||
|
ActionDropdown,
|
||||||
} from '@/components/shared';
|
} from '@/components/shared';
|
||||||
import { Plus, Pencil, Trash2, Settings, Check, X, Building2, Search } from 'lucide-react';
|
import { Plus, Settings, Check, X, Building2, Search } from 'lucide-react';
|
||||||
import { auditLogService } from '@/services/audit-log-service';
|
import { auditLogService } from '@/services/audit-log-service';
|
||||||
import { moduleService } from '@/services/module-service';
|
import { moduleService } from '@/services/module-service';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -58,6 +60,11 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
const [selectedRT, setSelectedRT] = useState<ResourceType | null>(null);
|
const [selectedRT, setSelectedRT] = useState<ResourceType | null>(null);
|
||||||
|
|
||||||
|
// Delete Confirmation State
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
|
||||||
|
const [rtToDelete, setRtToDelete] = useState<ResourceType | null>(null);
|
||||||
|
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||||
|
|
||||||
// Pagination & Filtering State
|
// Pagination & Filtering State
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [limit, setLimit] = useState<number>(10);
|
const [limit, setLimit] = useState<number>(10);
|
||||||
@ -189,16 +196,26 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDeleteClick = (rt: ResourceType) => {
|
||||||
if (!window.confirm('Are you sure you want to delete this resource type?')) return;
|
setRtToDelete(rt);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfirmDelete = async () => {
|
||||||
|
if (!rtToDelete) return;
|
||||||
try {
|
try {
|
||||||
const response = await auditLogService.deleteResourceType(id);
|
setIsDeleting(true);
|
||||||
|
const response = await auditLogService.deleteResourceType(rtToDelete.id);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success('Resource type deleted successfully');
|
toast.success('Resource type deleted successfully');
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
fetchResourceTypes();
|
fetchResourceTypes();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Failed to delete resource type');
|
toast.error('Failed to delete resource type');
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setRtToDelete(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -209,7 +226,7 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
render: (rt) => (
|
render: (rt) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="w-4 h-4 text-[#475569]" />
|
<Settings className="w-4 h-4 text-[#475569]" />
|
||||||
<span className="text-sm font-medium text-[#0f1724]">{rt.name}</span>
|
<span className="">{rt.name}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -226,7 +243,7 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
key: 'module',
|
key: 'module',
|
||||||
label: 'Associated Module',
|
label: 'Associated Module',
|
||||||
render: (rt) => rt.type === 'MODULE' && rt.module_id ? (
|
render: (rt) => rt.type === 'MODULE' && rt.module_id ? (
|
||||||
<div className="flex items-center gap-1.5 text-sm text-[#475569]">
|
<div className="flex items-center gap-1.5">
|
||||||
<Building2 className="w-3.5 h-3.5" />
|
<Building2 className="w-3.5 h-3.5" />
|
||||||
<span>{rt.module?.name || 'Loading...'}</span>
|
<span>{rt.module?.name || 'Loading...'}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -249,19 +266,11 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
label: 'Actions',
|
label: 'Actions',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (rt) => (
|
render: (rt) => (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2 pr-2">
|
||||||
<button
|
<ActionDropdown
|
||||||
onClick={() => handleOpenModal(rt)}
|
onEdit={() => handleOpenModal(rt)}
|
||||||
className="p-1.5 text-[#475569] hover:text-[#112868] hover:bg-gray-100 rounded-md transition-colors"
|
onDelete={() => handleDeleteClick(rt)}
|
||||||
>
|
/>
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(rt.id)}
|
|
||||||
className="p-1.5 text-[#ef4444] hover:bg-red-50 rounded-md transition-colors"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -284,9 +293,9 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-2">
|
||||||
{/* Filter Row */}
|
{/* Filter Row */}
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 bg-white p-4 border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||||
<div className="flex flex-wrap items-center gap-3 w-full md:w-auto">
|
<div className="flex flex-wrap items-center gap-3 w-full md:w-auto">
|
||||||
{/* Search Input */}
|
{/* Search Input */}
|
||||||
<div className="relative w-full md:w-64">
|
<div className="relative w-full md:w-64">
|
||||||
@ -367,7 +376,7 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={resourceTypes}
|
data={resourceTypes}
|
||||||
@ -398,11 +407,11 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
title={isEditing ? 'Edit Resource Type' : 'Create Resource Type'}
|
title={isEditing ? 'Edit Resource Type' : 'Create Resource Type'}
|
||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
footer={
|
footer={
|
||||||
<div className="flex gap-3 w-full">
|
<div className="flex gap-3">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={() => setIsModalOpen(false)}
|
||||||
className="flex-1"
|
// className="flex-1"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@ -410,7 +419,7 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmit(onFormSubmit as any)}
|
onClick={handleSubmit(onFormSubmit as any)}
|
||||||
className="flex-1"
|
// className="flex-1"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Saving...' : (isEditing ? 'Save Changes' : 'Create Type')}
|
{isSubmitting ? 'Saving...' : (isEditing ? 'Save Changes' : 'Create Type')}
|
||||||
@ -418,7 +427,7 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit as any)} className="p-6 flex flex-col gap-1">
|
<form onSubmit={handleSubmit(onFormSubmit as any)} className="flex flex-col gap-1">
|
||||||
<FormField
|
<FormField
|
||||||
label="Resource Name"
|
label="Resource Name"
|
||||||
required
|
required
|
||||||
@ -427,7 +436,7 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
{...register('name')}
|
{...register('name')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 pb-4">
|
<div className="flex flex-col gap-0.5 pb-4">
|
||||||
<label className="text-[13px] font-medium text-[#0e1b2a]">
|
<label className="text-[13px] font-medium text-[#0e1b2a]">
|
||||||
Type <span className="text-[#e02424]">*</span>
|
Type <span className="text-[#e02424]">*</span>
|
||||||
</label>
|
</label>
|
||||||
@ -485,6 +494,19 @@ const AuditLogResourceTypes = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<DeleteConfirmationModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setRtToDelete(null);
|
||||||
|
}}
|
||||||
|
onConfirm={onConfirmDelete}
|
||||||
|
title="Delete Resource Type"
|
||||||
|
message="Are you sure you want to delete this resource type? This action cannot be undone."
|
||||||
|
itemName={rtToDelete?.name || ''}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable";
|
import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable";
|
||||||
import { Layout } from "@/components/layout/Layout";
|
import { Layout } from "@/components/layout/Layout";
|
||||||
|
|
||||||
export default function FailedEmails() {
|
export default function FailedEmails() {
|
||||||
|
const [resendAllButton, setResendAllButton] = useState<React.ReactNode>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Failed Emails"
|
currentPage="Failed Emails"
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: "Platform Failed Emails",
|
title: "Platform Failed Emails",
|
||||||
description: "Global monitoring of all failed system email dispatches and automatic/manual retry logs across all tenants."
|
description: "Global monitoring of all failed system email dispatches and automatic/manual retry logs across all tenants.",
|
||||||
|
action: resendAllButton
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FailedEmailsTable />
|
<FailedEmailsTable onRegisterResendAll={setResendAllButton} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -227,7 +227,7 @@ const Modules = (): ReactElement => {
|
|||||||
key: "description",
|
key: "description",
|
||||||
label: "Description",
|
label: "Description",
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724] line-clamp-1">
|
<span className="">
|
||||||
{module.description}
|
{module.description}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -236,7 +236,7 @@ const Modules = (): ReactElement => {
|
|||||||
key: "version",
|
key: "version",
|
||||||
label: "Version",
|
label: "Version",
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="">
|
||||||
{module.version}
|
{module.version}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -263,7 +263,7 @@ const Modules = (): ReactElement => {
|
|||||||
key: "runtime_language",
|
key: "runtime_language",
|
||||||
label: "Runtime",
|
label: "Runtime",
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="">
|
||||||
{module.runtime_language || "N/A"}
|
{module.runtime_language || "N/A"}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -272,7 +272,7 @@ const Modules = (): ReactElement => {
|
|||||||
key: "created_at",
|
key: "created_at",
|
||||||
label: "Registered Date",
|
label: "Registered Date",
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<span className="text-sm font-normal text-[#6b7280]">
|
<span className="">
|
||||||
{formatDate(module.created_at)}
|
{formatDate(module.created_at)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -416,9 +416,9 @@ const Modules = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
{/* Table Header with Filters */}
|
{/* Table Header with Filters */}
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="pb-2 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{/* Global Search */}
|
{/* Global Search */}
|
||||||
|
|||||||
@ -13,8 +13,9 @@ import {
|
|||||||
FilterDropdown,
|
FilterDropdown,
|
||||||
Pagination,
|
Pagination,
|
||||||
type Column,
|
type Column,
|
||||||
|
SearchBox,
|
||||||
} from '@/components/shared';
|
} from '@/components/shared';
|
||||||
import { Plus, Code, Search, X, Tag } from 'lucide-react';
|
import { Plus, Code, X, Tag } from 'lucide-react';
|
||||||
import { notificationService } from '@/services/notification-service';
|
import { notificationService } from '@/services/notification-service';
|
||||||
import { moduleService } from '@/services/module-service';
|
import { moduleService } from '@/services/module-service';
|
||||||
import { showToast } from '@/utils/toast';
|
import { showToast } from '@/utils/toast';
|
||||||
@ -334,19 +335,18 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
description: 'Manage notification categories and event codes across the platform.',
|
description: 'Manage notification categories and event codes across the platform.',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden flex flex-col min-h-[500px]">
|
<div className="overflow-hidden flex flex-col min-h-[500px]">
|
||||||
<div className="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
|
<div className="pb-2 flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
|
||||||
<div className="flex items-center gap-4 flex-1">
|
<div className="flex items-center gap-4 flex-1">
|
||||||
<div className="relative flex-1 max-w-sm">
|
<SearchBox
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
value={search}
|
||||||
<input
|
onChange={(val) => {
|
||||||
type="text"
|
setSearch(val);
|
||||||
placeholder="Search categories..."
|
setCurrentPage(1);
|
||||||
className="w-full pl-9 pr-4 py-1.5 border rounded-md text-sm outline-none focus:ring-2 focus:ring-blue-500/20"
|
}}
|
||||||
value={search}
|
placeholder="Search categories..."
|
||||||
onChange={(e) => { setSearch(e.target.value); setCurrentPage(1); }}
|
containerClassName="relative flex-1 max-w-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<FilterDropdown
|
<FilterDropdown
|
||||||
label="Module"
|
label="Module"
|
||||||
|
|||||||
@ -12,8 +12,9 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
FilterDropdown,
|
FilterDropdown,
|
||||||
type Column,
|
type Column,
|
||||||
|
SearchBox,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import { Plus, Search, Copy, CheckCheck } from "lucide-react";
|
import { Plus, Copy, CheckCheck } from "lucide-react";
|
||||||
import { notificationService } from "@/services/notification-service";
|
import { notificationService } from "@/services/notification-service";
|
||||||
import { moduleService } from "@/services/module-service";
|
import { moduleService } from "@/services/module-service";
|
||||||
import { showToast } from "@/utils/toast";
|
import { showToast } from "@/utils/toast";
|
||||||
@ -347,22 +348,18 @@ const NotificationTemplateMaster = (): ReactElement => {
|
|||||||
description: "Define default notification templates for all tenants.",
|
description: "Define default notification templates for all tenants.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden flex flex-col min-h-[600px]">
|
<div className="overflow-hidden flex flex-col min-h-[600px]">
|
||||||
<div className="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
|
<div className="pb-2 flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
|
||||||
<div className="flex items-center gap-4 flex-1">
|
<div className="flex items-center gap-4 flex-1">
|
||||||
<div className="relative flex-1 max-w-sm">
|
<SearchBox
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
value={search}
|
||||||
<input
|
onChange={(val) => {
|
||||||
type="text"
|
setSearch(val);
|
||||||
placeholder="Search templates..."
|
setCurrentPage(1);
|
||||||
className="w-full pl-9 pr-4 py-2 border rounded-md text-sm focus:ring-2 focus:ring-blue-500 outline-none transition-all"
|
}}
|
||||||
value={search}
|
placeholder="Search templates..."
|
||||||
onChange={(e) => {
|
containerClassName="relative flex-1 max-w-sm"
|
||||||
setSearch(e.target.value);
|
/>
|
||||||
setCurrentPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { Layout } from '@/components/layout/Layout';
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
Pagination,
|
Pagination,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
DeleteConfirmationModal,
|
DeleteConfirmationModal,
|
||||||
type Column
|
type Column,
|
||||||
} from '@/components/shared';
|
} from "@/components/shared";
|
||||||
import { Plus, Server, Globe, Building } from 'lucide-react';
|
import { Plus, Globe, Building } from "lucide-react";
|
||||||
import { smtpConfigService, type SmtpConfig } from '@/services/smtp-config-service';
|
import {
|
||||||
import { SmtpConfigModal } from '@/components/superadmin/SmtpConfigModal';
|
smtpConfigService,
|
||||||
import { showToast } from '@/utils/toast';
|
type SmtpConfig,
|
||||||
|
} from "@/services/smtp-config-service";
|
||||||
|
import { SmtpConfigModal } from "@/components/superadmin/SmtpConfigModal";
|
||||||
|
import { showToast } from "@/utils/toast";
|
||||||
|
|
||||||
const SmtpConfigPage = () => {
|
const SmtpConfigPage = () => {
|
||||||
const [configs, setConfigs] = useState<SmtpConfig[]>([]);
|
const [configs, setConfigs] = useState<SmtpConfig[]>([]);
|
||||||
@ -32,16 +35,16 @@ const SmtpConfigPage = () => {
|
|||||||
try {
|
try {
|
||||||
const res = await smtpConfigService.listAll({
|
const res = await smtpConfigService.listAll({
|
||||||
offset: (currentPage - 1) * limit,
|
offset: (currentPage - 1) * limit,
|
||||||
limit: limit
|
limit: limit,
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setConfigs(res.data);
|
setConfigs(res.data);
|
||||||
// Assuming the API would return total items if we had many,
|
// Assuming the API would return total items if we had many,
|
||||||
// for now let's just use the length or a fixed number
|
// for now let's just use the length or a fixed number
|
||||||
setTotalItems(res.data.length);
|
setTotalItems(res.data.length);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast.error('Failed to load SMTP configurations');
|
showToast.error("Failed to load SMTP configurations");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -65,12 +68,15 @@ const SmtpConfigPage = () => {
|
|||||||
if (!selectedConfig?.id) return;
|
if (!selectedConfig?.id) return;
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
await smtpConfigService.deleteConfig(selectedConfig.id, selectedConfig.tenant_id);
|
await smtpConfigService.deleteConfig(
|
||||||
showToast.success('Configuration deleted');
|
selectedConfig.id,
|
||||||
|
selectedConfig.tenant_id,
|
||||||
|
);
|
||||||
|
showToast.success("Configuration deleted");
|
||||||
setDeleteModalOpen(false);
|
setDeleteModalOpen(false);
|
||||||
fetchConfigs();
|
fetchConfigs();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast.error('Failed to delete configuration');
|
showToast.error("Failed to delete configuration");
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
@ -78,78 +84,89 @@ const SmtpConfigPage = () => {
|
|||||||
|
|
||||||
const columns: Column<SmtpConfig>[] = [
|
const columns: Column<SmtpConfig>[] = [
|
||||||
{
|
{
|
||||||
key: 'scope',
|
key: "scope",
|
||||||
label: 'Scope',
|
label: "Scope",
|
||||||
render: (config) => (
|
render: (config) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{config.scope === 'super_admin' ? (
|
{config.scope === "super_admin" ? (
|
||||||
<Globe className="w-4 h-4 text-blue-600" />
|
<Globe className="w-4 h-4 text-blue-600" />
|
||||||
) : (
|
) : (
|
||||||
<Building className="w-4 h-4 text-purple-600" />
|
<Building className="w-4 h-4 text-purple-600" />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-medium">
|
<span>
|
||||||
{config.scope === 'super_admin' ? 'Global' : config.tenant_name || 'Tenant'}
|
{config.scope === "super_admin"
|
||||||
|
? "Global"
|
||||||
|
: config.tenant_name || "Tenant"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'host',
|
key: "host",
|
||||||
label: 'Server',
|
label: "Server",
|
||||||
render: (config) => (
|
render: (config) => (
|
||||||
<div className="flex items-center gap-2">
|
<span>
|
||||||
<Server className="w-4 h-4 text-gray-400" />
|
{config.host}:{config.port}
|
||||||
<span className="text-sm">{config.host}:{config.port}</span>
|
</span>
|
||||||
</div>
|
),
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'from_email',
|
key: "from_email",
|
||||||
label: 'Sender',
|
label: "Sender",
|
||||||
render: (config) => (
|
render: (config) => (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium">{config.from_name || 'N/A'}</span>
|
<span className="text-sm font-medium">
|
||||||
<span className="text-xs text-gray-500">{config.from_email || 'N/A'}</span>
|
{config.from_name || "N/A"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{config.from_email || "N/A"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'is_active',
|
key: "is_active",
|
||||||
label: 'Status',
|
label: "Status",
|
||||||
render: (config) => (
|
render: (config) => (
|
||||||
<StatusBadge variant={config.is_active ? 'success' : 'failure'}>
|
<StatusBadge variant={config.is_active ? "success" : "failure"}>
|
||||||
{config.is_active ? 'Active' : 'Inactive'}
|
{config.is_active ? "Active" : "Inactive"}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: "actions",
|
||||||
label: 'Actions',
|
label: "Actions",
|
||||||
align: 'right',
|
align: "right",
|
||||||
render: (config) => (
|
render: (config) => (
|
||||||
<ActionDropdown
|
<ActionDropdown
|
||||||
onEdit={() => handleEdit(config)}
|
onEdit={() => handleEdit(config)}
|
||||||
onDelete={() => handleDelete(config)}
|
onDelete={() => handleDelete(config)}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Settings"
|
currentPage="Settings"
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: 'SMTP Configurations',
|
title: "SMTP Configurations",
|
||||||
description: 'Manage email delivery settings for the entire platform and individual tenants.',
|
description:
|
||||||
|
"Manage email delivery settings for the entire platform and individual tenants.",
|
||||||
action: (
|
action: (
|
||||||
<PrimaryButton onClick={() => { setSelectedConfig(null); setIsModalOpen(true); }}>
|
<PrimaryButton
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedConfig(null);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add Configuration
|
Add Configuration
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
)
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white border border-gray-100 rounded-lg shadow-sm overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={configs}
|
data={configs}
|
||||||
@ -157,7 +174,7 @@ const SmtpConfigPage = () => {
|
|||||||
keyExtractor={(item) => item.id!}
|
keyExtractor={(item) => item.id!}
|
||||||
emptyMessage="No SMTP configurations found"
|
emptyMessage="No SMTP configurations found"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{totalItems > limit && (
|
{totalItems > limit && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
@ -183,7 +200,11 @@ const SmtpConfigPage = () => {
|
|||||||
onConfirm={confirmDelete}
|
onConfirm={confirmDelete}
|
||||||
title="Delete SMTP Configuration"
|
title="Delete SMTP Configuration"
|
||||||
message="Are you sure you want to delete this SMTP configuration? This action cannot be undone."
|
message="Are you sure you want to delete this SMTP configuration? This action cannot be undone."
|
||||||
itemName={selectedConfig?.scope === 'super_admin' ? 'Global Config' : selectedConfig?.tenant_name || 'Tenant Config'}
|
itemName={
|
||||||
|
selectedConfig?.scope === "super_admin"
|
||||||
|
? "Global Config"
|
||||||
|
: selectedConfig?.tenant_name || "Tenant Config"
|
||||||
|
}
|
||||||
isLoading={isDeleting}
|
isLoading={isDeleting}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@ -235,7 +235,7 @@ const Tenants = (): ReactElement => {
|
|||||||
// {getTenantInitials(tenant.name)}
|
// {getTenantInitials(tenant.name)}
|
||||||
// </span>
|
// </span>
|
||||||
// </div>
|
// </div>
|
||||||
// <span className="text-sm font-normal text-[#0f1724]">
|
// <span className="">
|
||||||
// {tenant.name}
|
// {tenant.name}
|
||||||
// </span>
|
// </span>
|
||||||
// </div>
|
// </div>
|
||||||
@ -255,7 +255,7 @@ const Tenants = (): ReactElement => {
|
|||||||
key: "user_count",
|
key: "user_count",
|
||||||
label: "Users",
|
label: "Users",
|
||||||
render: (tenant) => (
|
render: (tenant) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="">
|
||||||
{tenant.user_count ?? 0}
|
{tenant.user_count ?? 0}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -264,7 +264,7 @@ const Tenants = (): ReactElement => {
|
|||||||
key: "subscription_tier",
|
key: "subscription_tier",
|
||||||
label: "Subscription Tier",
|
label: "Subscription Tier",
|
||||||
render: (tenant) => (
|
render: (tenant) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="">
|
||||||
{formatSubscriptionTier(tenant.subscription_tier)}
|
{formatSubscriptionTier(tenant.subscription_tier)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -273,7 +273,7 @@ const Tenants = (): ReactElement => {
|
|||||||
key: "module_count",
|
key: "module_count",
|
||||||
label: "Modules",
|
label: "Modules",
|
||||||
render: (tenant) => (
|
render: (tenant) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
<span className="">
|
||||||
{tenant.module_count ?? 0}
|
{tenant.module_count ?? 0}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -282,7 +282,7 @@ const Tenants = (): ReactElement => {
|
|||||||
key: "created_at",
|
key: "created_at",
|
||||||
label: "Joined Date",
|
label: "Joined Date",
|
||||||
render: (tenant) => (
|
render: (tenant) => (
|
||||||
<span className="text-sm font-normal text-[#6b7280]">
|
<span className="">
|
||||||
{formatDate(tenant.created_at)}
|
{formatDate(tenant.created_at)}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -373,9 +373,9 @@ const Tenants = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
{/* Table Header with Filters */}
|
{/* Table Header with Filters */}
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="pb-2 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
{/* Search & Filters */}
|
{/* Search & Filters */}
|
||||||
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
|
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto">
|
||||||
{/* Global Search */}
|
{/* Global Search */}
|
||||||
|
|||||||
@ -477,9 +477,9 @@ const AuditLogs = ({
|
|||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
{/* Table Header with Filters */}
|
{/* Table Header with Filters */}
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col gap-4">
|
<div className="pb-2 flex flex-col gap-4">
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
{/* Search and Filters */}
|
{/* Search and Filters */}
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
|||||||
@ -351,12 +351,8 @@ const CompletionHistory = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<section className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden">
|
<section className="overflow-hidden">
|
||||||
<div className="p-4 md:p-5 border-b border-[rgba(0,0,0,0.08)]">
|
<div className="pb-2 border-b border-[#D1D5DB]">
|
||||||
{/* <h3 className="text-sm md:text-base font-semibold text-[#0f1724] mb-3">
|
|
||||||
Completion List
|
|
||||||
</h3> */}
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-3">
|
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@ -436,7 +432,7 @@ const CompletionHistory = (): ReactElement => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={clearFilters}
|
onClick={clearFilters}
|
||||||
className="text-sm font-medium text-[#6b7280] hover:text-[#0f1724] transition-colors self-start lg:self-center"
|
className="text-[13px] font-medium text-[#6b7280] hover:text-[#94A3B8] transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Clear filters
|
Clear filters
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -47,7 +47,7 @@ const TaskCard = ({ task }: { task: WorkflowTask }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start self-stretch p-3 gap-2.5 rounded-[6px] border border-[#D1D5DB] bg-white hover:border-[#9CA3AF] transition-colors group">
|
<div className="flex flex-col items-start self-stretch p-3 gap-2.5 rounded-[6px] border border-[#D1D5DB] bg-[#F9F9F9] hover:border-[#9CA3AF] transition-colors group">
|
||||||
<div className="flex justify-between items-start self-stretch">
|
<div className="flex justify-between items-start self-stretch">
|
||||||
<span className="text-[10px] font-bold text-[#94A3B8] uppercase tracking-[0.05em] leading-none">
|
<span className="text-[10px] font-bold text-[#94A3B8] uppercase tracking-[0.05em] leading-none">
|
||||||
{task.entity.type}
|
{task.entity.type}
|
||||||
@ -82,7 +82,7 @@ const TaskCard = ({ task }: { task: WorkflowTask }) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleView}
|
onClick={handleView}
|
||||||
className="flex px-3 py-1.5 justify-center items-center rounded-[4px] border border-[#D1D5DB] bg-white text-[12px] font-bold text-[#1E293B] hover:bg-[#F9FAFB] hover:border-[#9CA3AF] transition-all shrink-0"
|
className="flex px-3 py-1.5 justify-center items-center rounded-[4px] border border-[#D1D5DB] bg-white text-[12px] font-bold text-[#1E293B] hover:bg-[#F9FAFB] hover:border-[#9CA3AF] transition-all shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</button>
|
</button>
|
||||||
@ -218,7 +218,7 @@ const Dashboard = (): ReactElement => {
|
|||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/tenant/workflows/tasks")}
|
onClick={() => navigate("/tenant/workflows/tasks")}
|
||||||
className="text-[11px] font-bold hover:underline"
|
className="text-[11px] font-bold hover:underline cursor-pointer"
|
||||||
style={{ color: primaryColor }}
|
style={{ color: primaryColor }}
|
||||||
>
|
>
|
||||||
View all
|
View all
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { useRef, type ReactElement } from 'react';
|
import { useRef, type ReactElement } from "react";
|
||||||
import { Layout } from '@/components/layout/Layout';
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import { DepartmentsTable, type DepartmentsTableRef } from '@/components/superadmin/DepartmentsTable';
|
import {
|
||||||
import { PrimaryButton } from '@/components/shared';
|
DepartmentsTable,
|
||||||
import { Plus } from 'lucide-react';
|
type DepartmentsTableRef,
|
||||||
import { usePermissions } from '@/hooks/usePermissions';
|
} from "@/components/superadmin/DepartmentsTable";
|
||||||
|
import { PrimaryButton } from "@/components/shared";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { usePermissions } from "@/hooks/usePermissions";
|
||||||
|
|
||||||
const Departments = (): ReactElement => {
|
const Departments = (): ReactElement => {
|
||||||
const tableRef = useRef<DepartmentsTableRef>(null);
|
const tableRef = useRef<DepartmentsTableRef>(null);
|
||||||
@ -13,8 +16,9 @@ const Departments = (): ReactElement => {
|
|||||||
<Layout
|
<Layout
|
||||||
currentPage="Departments"
|
currentPage="Departments"
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: 'Department Management',
|
title: "Department Management",
|
||||||
description: 'View and manage all departments within your organization.',
|
description:
|
||||||
|
"View and manage all departments within your organization.",
|
||||||
action: canCreate("departments") ? (
|
action: canCreate("departments") ? (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
size="default"
|
size="default"
|
||||||
|
|||||||
@ -1,17 +1,36 @@
|
|||||||
import { type ReactElement } from 'react';
|
import { useRef, type ReactElement } from "react";
|
||||||
import { Layout } from '@/components/layout/Layout';
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import { DesignationsTable } from '@/components/superadmin';
|
import {
|
||||||
|
DesignationsTable,
|
||||||
|
type DesignationsTableRef,
|
||||||
|
} from "@/components/superadmin";
|
||||||
|
import { PrimaryButton } from "@/components/shared";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { usePermissions } from "@/hooks/usePermissions";
|
||||||
|
|
||||||
const Designations = (): ReactElement => {
|
const Designations = (): ReactElement => {
|
||||||
|
const tableRef = useRef<DesignationsTableRef>(null);
|
||||||
|
const { canCreate } = usePermissions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Designations"
|
currentPage="Designations"
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: 'Designation Management',
|
title: "Designation Management",
|
||||||
description: 'View and manage all designations within your organization.',
|
description: "View and manage all designations within your organization.",
|
||||||
|
action: canCreate("designations") ? (
|
||||||
|
<PrimaryButton
|
||||||
|
size="default"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => tableRef.current?.openNewModal()}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>New Designation</span>
|
||||||
|
</PrimaryButton>
|
||||||
|
) : null,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DesignationsTable />
|
<DesignationsTable ref={tableRef} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,12 +14,14 @@ import {
|
|||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
DeleteConfirmationModal,
|
DeleteConfirmationModal,
|
||||||
type Column,
|
type Column,
|
||||||
|
SecondaryButton,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import { documentService } from "@/services/document-service";
|
import { documentService } from "@/services/document-service";
|
||||||
import type { DocumentCategory } from "@/types/document";
|
import type { DocumentCategory } from "@/types/document";
|
||||||
import { showToast } from "@/utils/toast";
|
import { showToast } from "@/utils/toast";
|
||||||
import { Plus, Eye, Edit, Trash2 } from "lucide-react";
|
import { Plus, Eye, Edit, Trash2 } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import CodeBadge from "@/components/shared/CodeBadge";
|
||||||
|
|
||||||
const categorySchema = z.object({
|
const categorySchema = z.object({
|
||||||
name: z.string().min(1, "Category name is required"),
|
name: z.string().min(1, "Category name is required"),
|
||||||
@ -139,18 +141,12 @@ const DocumentCategories = (): ReactElement => {
|
|||||||
{
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
label: "Name",
|
label: "Name",
|
||||||
render: (cat) => (
|
render: (cat) => <span className="">{cat.name}</span>,
|
||||||
<span className="text-[#0f1724] font-medium">{cat.name}</span>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "code",
|
key: "code",
|
||||||
label: "Code",
|
label: "Code",
|
||||||
render: (cat) => (
|
render: (cat) => <CodeBadge label={cat.code} />,
|
||||||
<span className="inline-flex items-center rounded-md bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-600 border border-blue-100">
|
|
||||||
{cat.code}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "review_frequency_months",
|
key: "review_frequency_months",
|
||||||
@ -293,7 +289,7 @@ const DocumentCategories = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={categories}
|
data={categories}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -317,9 +313,34 @@ const DocumentCategories = (): ReactElement => {
|
|||||||
: "Create Document Category"
|
: "Create Document Category"
|
||||||
}
|
}
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={() => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setEditingCategory(null);
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit(onFormSubmit)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-6"
|
||||||
|
>
|
||||||
|
{isSubmitting
|
||||||
|
? "Processing..."
|
||||||
|
: editingCategory
|
||||||
|
? "Update Category"
|
||||||
|
: "Create Category"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)} className="p-6 space-y-5">
|
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
|
||||||
<p className="text-sm text-gray-500 -mt-2">
|
<p className="text-sm text-gray-500">
|
||||||
Add a document category with review, retention, and training
|
Add a document category with review, retention, and training
|
||||||
requirements.
|
requirements.
|
||||||
</p>
|
</p>
|
||||||
@ -447,30 +468,6 @@ const DocumentCategories = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-100">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setIsModalOpen(false);
|
|
||||||
setEditingCategory(null);
|
|
||||||
}}
|
|
||||||
className="px-6 py-2 border border-gray-200 rounded-md text-sm font-bold text-[#0f1724] hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<PrimaryButton
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="px-6"
|
|
||||||
>
|
|
||||||
{isSubmitting
|
|
||||||
? "Processing..."
|
|
||||||
: editingCategory
|
|
||||||
? "Update Category"
|
|
||||||
: "Create Category"}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@ -484,7 +481,7 @@ const DocumentCategories = (): ReactElement => {
|
|||||||
title="Document Category Details"
|
title="Document Category Details"
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
>
|
>
|
||||||
<div className="p-6 space-y-6">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-gray-500 uppercase font-bold tracking-wider">
|
<label className="text-xs text-gray-500 uppercase font-bold tracking-wider">
|
||||||
|
|||||||
@ -35,9 +35,9 @@ const Documents = (): ReactElement => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [documents, setDocuments] = useState<DocumentSummary[]>([]);
|
const [documents, setDocuments] = useState<DocumentSummary[]>([]);
|
||||||
const [categories, setCategories] = useState<DocumentCategory[]>([]);
|
const [categories, setCategories] = useState<DocumentCategory[]>([]);
|
||||||
const [statuses, setStatuses] = useState<Array<{ code: string; name: string }>>(
|
const [statuses, setStatuses] = useState<
|
||||||
[],
|
Array<{ code: string; name: string }>
|
||||||
);
|
>([]);
|
||||||
const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]);
|
const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
||||||
@ -57,12 +57,13 @@ const Documents = (): ReactElement => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadDropdownData = async (): Promise<void> => {
|
const loadDropdownData = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const [categoriesRes, statusesRes, typesRes, modulesRes] = await Promise.all([
|
const [categoriesRes, statusesRes, typesRes, modulesRes] =
|
||||||
documentService.getCategories(),
|
await Promise.all([
|
||||||
documentService.getStatuses(),
|
documentService.getCategories(),
|
||||||
documentService.getTypes(),
|
documentService.getStatuses(),
|
||||||
moduleService.getAvailable(),
|
documentService.getTypes(),
|
||||||
]);
|
moduleService.getAvailable(),
|
||||||
|
]);
|
||||||
setCategories(categoriesRes.data || []);
|
setCategories(categoriesRes.data || []);
|
||||||
setStatuses(statusesRes.data || []);
|
setStatuses(statusesRes.data || []);
|
||||||
setTypes(typesRes.data || []);
|
setTypes(typesRes.data || []);
|
||||||
@ -101,7 +102,15 @@ const Documents = (): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void loadDocuments();
|
void loadDocuments();
|
||||||
}, [statusFilter, categoryFilter, typeFilter, moduleFilter, search, limit, offset]);
|
}, [
|
||||||
|
statusFilter,
|
||||||
|
categoryFilter,
|
||||||
|
typeFilter,
|
||||||
|
moduleFilter,
|
||||||
|
search,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
]);
|
||||||
|
|
||||||
const columns: Column<DocumentSummary>[] = useMemo(
|
const columns: Column<DocumentSummary>[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -111,7 +120,7 @@ const Documents = (): ReactElement => {
|
|||||||
render: (doc) => (
|
render: (doc) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="hover:underline transition-colors"
|
className="hover:underline transition-colors cursor-pointer"
|
||||||
style={{ color: primaryColor }}
|
style={{ color: primaryColor }}
|
||||||
onClick={() => navigate(`/tenant/documents/${doc.id}`)}
|
onClick={() => navigate(`/tenant/documents/${doc.id}`)}
|
||||||
>
|
>
|
||||||
@ -122,27 +131,32 @@ const Documents = (): ReactElement => {
|
|||||||
{
|
{
|
||||||
key: "title",
|
key: "title",
|
||||||
label: "Title",
|
label: "Title",
|
||||||
render: (doc) => <span className="text-[#0f1724]">{doc.title}</span>,
|
render: (doc) => <span className="">{doc.title}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "document_type",
|
key: "document_type",
|
||||||
label: "Type",
|
label: "Type",
|
||||||
render: (doc) => (
|
render: (doc) => (
|
||||||
<span className="text-[#0f1724]">{doc.document_type || "-"}</span>
|
<span className="">{doc.document_type || "-"}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "category",
|
key: "category",
|
||||||
label: "Category",
|
label: "Category",
|
||||||
render: (doc) => <span className="text-[#0f1724]">{doc.category || "-"}</span>,
|
render: (doc) => (
|
||||||
|
<span className="">{doc.category || "-"}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
label: "Status",
|
label: "Status",
|
||||||
render: (doc) => (
|
render: (doc) => (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center rounded-md px-2 py-1 text-[11px]"
|
className="inline-flex items-center rounded-md px-2 py-1 text-[11px]"
|
||||||
style={{ backgroundColor: `${primaryColor}1A`, color: primaryColor }}
|
style={{
|
||||||
|
backgroundColor: `${primaryColor}1A`,
|
||||||
|
color: primaryColor,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{toLabel(doc.status)}
|
{toLabel(doc.status)}
|
||||||
</span>
|
</span>
|
||||||
@ -152,21 +166,23 @@ const Documents = (): ReactElement => {
|
|||||||
key: "module_name",
|
key: "module_name",
|
||||||
label: "Module",
|
label: "Module",
|
||||||
render: (doc) => (
|
render: (doc) => (
|
||||||
<span className="text-[#0f1724]">{doc.module_name || "Platform"}</span>
|
<span className="">
|
||||||
|
{doc.module_name || "Platform"}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "current_version",
|
key: "current_version",
|
||||||
label: "Version",
|
label: "Version",
|
||||||
render: (doc) => (
|
render: (doc) => (
|
||||||
<span className="text-[#0f1724]">{doc.current_version || "-"}</span>
|
<span className="">{doc.current_version || "-"}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "updated_at",
|
key: "updated_at",
|
||||||
label: "Updated",
|
label: "Updated",
|
||||||
render: (doc) => (
|
render: (doc) => (
|
||||||
<span className="text-[#6b7280]">{formatDate(doc.updated_at)}</span>
|
<span className="">{formatDate(doc.updated_at)}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -214,100 +230,99 @@ const Documents = (): ReactElement => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-4">
|
<div className="flex flex-col md:flex-row items-center justify-between gap-3 pb-2">
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
{/* Left side: Search and Filters */}
|
||||||
{/* Left side: Search and Filters */}
|
<div className="flex flex-1 flex-wrap items-center gap-3">
|
||||||
<div className="flex flex-1 flex-wrap items-center gap-3">
|
{/* Search Bar */}
|
||||||
{/* Search Bar */}
|
<div className="relative w-full max-w-[280px]">
|
||||||
<div className="relative w-full max-w-[280px]">
|
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-gray-400">
|
||||||
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-gray-400">
|
<Search className="w-4 h-4" />
|
||||||
<Search className="w-4 h-4" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearch(e.target.value);
|
|
||||||
setCurrentPage(1);
|
|
||||||
}}
|
|
||||||
placeholder="Search by name, ID..."
|
|
||||||
className="h-10 w-full pl-9 pr-3 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-2 transition-all"
|
|
||||||
style={{
|
|
||||||
// @ts-ignore
|
|
||||||
'--tw-ring-color': `${primaryColor}33`,
|
|
||||||
borderColor: 'rgba(0,0,0,0.08)'
|
|
||||||
}}
|
|
||||||
onFocus={(e) => {
|
|
||||||
e.currentTarget.style.borderColor = primaryColor;
|
|
||||||
e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}1A`;
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
e.currentTarget.style.borderColor = 'rgba(0,0,0,0.08)';
|
|
||||||
e.currentTarget.style.boxShadow = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="Search by name, ID..."
|
||||||
|
className="h-10 w-full pl-9 pr-3 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-2 transition-all"
|
||||||
|
style={{
|
||||||
|
// @ts-ignore
|
||||||
|
"--tw-ring-color": `${primaryColor}33`,
|
||||||
|
borderColor: "rgba(0,0,0,0.08)",
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = primaryColor;
|
||||||
|
e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}1A`;
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = "rgba(0,0,0,0.08)";
|
||||||
|
e.currentTarget.style.boxShadow = "none";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FilterDropdown
|
<FilterDropdown
|
||||||
label="Status"
|
label="Status"
|
||||||
options={statuses.map((status) => ({
|
options={statuses.map((status) => ({
|
||||||
value: status.code,
|
value: status.code,
|
||||||
label: status.name,
|
label: status.name,
|
||||||
}))}
|
}))}
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setStatusFilter(value as string | null);
|
setStatusFilter(value as string | null);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
placeholder="All"
|
placeholder="All"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FilterDropdown
|
|
||||||
label="Category"
|
|
||||||
options={categories.map((category) => ({
|
|
||||||
value: category.id,
|
|
||||||
label: category.name,
|
|
||||||
}))}
|
|
||||||
value={categoryFilter}
|
|
||||||
onChange={(value) => {
|
|
||||||
setCategoryFilter(value as string | null);
|
|
||||||
setCurrentPage(1);
|
|
||||||
}}
|
|
||||||
placeholder="All"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterDropdown
|
<FilterDropdown
|
||||||
label="Type"
|
label="Category"
|
||||||
options={types.map((type) => ({
|
options={categories.map((category) => ({
|
||||||
value: type.code,
|
value: category.id,
|
||||||
label: type.name,
|
label: category.name,
|
||||||
}))}
|
}))}
|
||||||
value={typeFilter}
|
value={categoryFilter}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setTypeFilter(value as string | null);
|
setCategoryFilter(value as string | null);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
placeholder="All"
|
placeholder="All"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FilterDropdown
|
<FilterDropdown
|
||||||
label="Module"
|
label="Type"
|
||||||
options={modules.map((module) => ({
|
options={types.map((type) => ({
|
||||||
value: module.id,
|
value: type.code,
|
||||||
label: module.name,
|
label: type.name,
|
||||||
}))}
|
}))}
|
||||||
value={moduleFilter}
|
value={typeFilter}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setModuleFilter(value as string | null);
|
setTypeFilter(value as string | null);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
placeholder="All Modules"
|
placeholder="All"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <FilterDropdown
|
<FilterDropdown
|
||||||
|
label="Module"
|
||||||
|
options={modules.map((module) => ({
|
||||||
|
value: module.id,
|
||||||
|
label: module.name,
|
||||||
|
}))}
|
||||||
|
value={moduleFilter}
|
||||||
|
onChange={(value) => {
|
||||||
|
setModuleFilter(value as string | null);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="All Modules"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <FilterDropdown
|
||||||
label="Priority"
|
label="Priority"
|
||||||
options={[
|
options={[
|
||||||
{ value: "high", label: "High" },
|
{ value: "high", label: "High" },
|
||||||
@ -330,26 +345,25 @@ const Documents = (): ReactElement => {
|
|||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
placeholder="More"
|
placeholder="More"
|
||||||
/> */}
|
/> */}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Right side: Clear Filters */}
|
{/* Right side: Clear Filters */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearch("");
|
setSearch("");
|
||||||
setStatusFilter(null);
|
setStatusFilter(null);
|
||||||
setCategoryFilter(null);
|
setCategoryFilter(null);
|
||||||
setTypeFilter(null);
|
setTypeFilter(null);
|
||||||
setModuleFilter(null);
|
setModuleFilter(null);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="text-sm font-medium text-[#6b7280] hover:text-[#0f1724] transition-colors"
|
className="text-[13px] font-medium text-[#6b7280] hover:text-[#94A3B8] transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Clear filters
|
Clear filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -381,4 +395,3 @@ const Documents = (): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default Documents;
|
export default Documents;
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable";
|
import { FailedEmailsTable } from "@/components/shared/FailedEmailsTable";
|
||||||
import { Layout } from "@/components/layout/Layout";
|
import { Layout } from "@/components/layout/Layout";
|
||||||
|
|
||||||
export default function FailedEmails() {
|
export default function FailedEmails() {
|
||||||
|
const [resendAllButton, setResendAllButton] = useState<React.ReactNode>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Failed Emails"
|
currentPage="Failed Emails"
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: "Failed Emails Log",
|
title: "Failed Emails Log",
|
||||||
description: "View and resend failed system email dispatches and transaction logs for this tenant."
|
description: "View and resend failed system email dispatches and transaction logs for this tenant.",
|
||||||
|
action: resendAllButton
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FailedEmailsTable />
|
<FailedEmailsTable onRegisterResendAll={setResendAllButton} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -580,9 +580,9 @@ const FilesList = (): ReactElement => {
|
|||||||
) : null,
|
) : null,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
{/* Filter bar */}
|
{/* Filter bar */}
|
||||||
<div className="border-b border-[rgba(0,0,0,0.08)] px-5 py-3.5">
|
<div className="border-b border-[rgba(0,0,0,0.08)] pb-2">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<SearchBox
|
<SearchBox
|
||||||
|
|||||||
@ -87,7 +87,7 @@ const Modules = (): ReactElement => {
|
|||||||
key: 'module_id',
|
key: 'module_id',
|
||||||
label: 'Module ID',
|
label: 'Module ID',
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724] font-mono">{module.module_id}</span>
|
<span className="">{module.module_id}</span>
|
||||||
),
|
),
|
||||||
mobileLabel: 'ID',
|
mobileLabel: 'ID',
|
||||||
},
|
},
|
||||||
@ -95,14 +95,14 @@ const Modules = (): ReactElement => {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">{module.name}</span>
|
<span className="">{module.name}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'version',
|
key: 'version',
|
||||||
label: 'Version',
|
label: 'Version',
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">{module.version}</span>
|
<span className="">{module.version}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -118,7 +118,7 @@ const Modules = (): ReactElement => {
|
|||||||
key: 'frontend_base_url',
|
key: 'frontend_base_url',
|
||||||
label: 'Frontend URL',
|
label: 'Frontend URL',
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<span className="text-sm font-normal text-[#6b7280] font-mono truncate max-w-[200px]">
|
<span className="">
|
||||||
{module.frontend_base_url || 'N/A'}
|
{module.frontend_base_url || 'N/A'}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -194,7 +194,7 @@ const Modules = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Table Container */}
|
{/* Table Container */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden w-full max-w-full">
|
<div className="overflow-hidden">
|
||||||
{/* Data Table */}
|
{/* Data Table */}
|
||||||
<DataTable
|
<DataTable
|
||||||
data={modules}
|
data={modules}
|
||||||
|
|||||||
@ -214,7 +214,7 @@ const NotificationTemplates = (): ReactElement => {
|
|||||||
{
|
{
|
||||||
key: "code",
|
key: "code",
|
||||||
label: "Event",
|
label: "Event",
|
||||||
render: (t) => <span className="text-sm font-semibold">{t.code}</span>,
|
render: (t) => <span className="">{t.code}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "source",
|
key: "source",
|
||||||
@ -229,7 +229,7 @@ const NotificationTemplates = (): ReactElement => {
|
|||||||
key: "preview",
|
key: "preview",
|
||||||
label: "Title Preview",
|
label: "Title Preview",
|
||||||
render: (t) => (
|
render: (t) => (
|
||||||
<span className="text-xs truncate max-w-xs block text-gray-500">
|
<span className="">
|
||||||
{t.title_template}
|
{t.title_template}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -295,8 +295,8 @@ const NotificationTemplates = (): ReactElement => {
|
|||||||
"Customize the content and delivery of platform notifications for your organization.",
|
"Customize the content and delivery of platform notifications for your organization.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden flex flex-col min-h-[500px]">
|
<div className="overflow-hidden">
|
||||||
<div className="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/30 gap-4">
|
<div className="pb-2 flex flex-wrap justify-between items-center">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Building className="w-4 h-4 text-blue-500" />
|
<Building className="w-4 h-4 text-blue-500" />
|
||||||
<h2 className="text-sm font-semibold text-gray-700">
|
<h2 className="text-sm font-semibold text-gray-700">
|
||||||
@ -304,7 +304,7 @@ const NotificationTemplates = (): ReactElement => {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 min-w-[300px]">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2 text-xs font-medium text-gray-400 mr-1">
|
<div className="flex items-center gap-2 text-xs font-medium text-gray-400 mr-1">
|
||||||
<Filter className="w-3.5 h-3.5" /> Filter by Module
|
<Filter className="w-3.5 h-3.5" /> Filter by Module
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useEffect, useMemo, useState, type ReactElement } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
|
||||||
Copy,
|
Copy,
|
||||||
History,
|
History,
|
||||||
Trash2,
|
Trash2,
|
||||||
@ -17,6 +16,7 @@ import {
|
|||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
DeleteConfirmationModal,
|
DeleteConfirmationModal,
|
||||||
|
SearchBox,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import { aiService } from "@/services/ai-service";
|
import { aiService } from "@/services/ai-service";
|
||||||
import type { AIPrompt } from "@/types/ai";
|
import type { AIPrompt } from "@/types/ai";
|
||||||
@ -300,18 +300,14 @@ const PromptManagement = (): ReactElement => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="p-4 border-b border-[rgba(0,0,0,0.06)] bg-gray-50/50">
|
<div className="pb-2">
|
||||||
<div className="relative w-full md:w-80">
|
<SearchBox
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#94a3b8]" />
|
value={search}
|
||||||
<input
|
onChange={setSearch}
|
||||||
type="text"
|
placeholder="Search by name or description..."
|
||||||
value={search}
|
containerClassName="relative w-full md:w-80"
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
/>
|
||||||
placeholder="Search by name or description..."
|
|
||||||
className="w-full pl-9 pr-4 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-lg text-sm transition-all focus:outline-none focus:ring-2 focus:ring-[#112868]/10 focus:border-[#112868]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState, type ReactElement } from "react";
|
import { useEffect, useMemo, useState, type ReactElement } from "react";
|
||||||
import { useNavigate, useSearchParams, useParams } from "react-router-dom";
|
import { useNavigate, useSearchParams, useParams } from "react-router-dom";
|
||||||
import {
|
import { Plus, Play, Eye, Loader2 } from "lucide-react";
|
||||||
Plus,
|
|
||||||
Play,
|
|
||||||
Eye,
|
|
||||||
Loader2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Layout } from "@/components/layout/Layout";
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
@ -19,6 +14,7 @@ import { showToast } from "@/utils/toast";
|
|||||||
import { formatDate } from "@/utils/format-date";
|
import { formatDate } from "@/utils/format-date";
|
||||||
import { PromptTestCaseResultModal } from "@/components/tenant/PromptTestCaseResultModal";
|
import { PromptTestCaseResultModal } from "@/components/tenant/PromptTestCaseResultModal";
|
||||||
import { PromptTestCaseResultsListModal } from "@/components/tenant/PromptTestCaseResultsListModal";
|
import { PromptTestCaseResultsListModal } from "@/components/tenant/PromptTestCaseResultsListModal";
|
||||||
|
import CodeBadge from "@/components/shared/CodeBadge";
|
||||||
|
|
||||||
const PromptTestCases = (): ReactElement => {
|
const PromptTestCases = (): ReactElement => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -46,7 +42,8 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
|
|
||||||
const [selectedTestCaseId, setSelectedTestCaseId] = useState<string>("");
|
const [selectedTestCaseId, setSelectedTestCaseId] = useState<string>("");
|
||||||
const [selectedTestCaseName, setSelectedTestCaseName] = useState<string>("");
|
const [selectedTestCaseName, setSelectedTestCaseName] = useState<string>("");
|
||||||
const [isResultsListModalOpen, setIsResultsListModalOpen] = useState<boolean>(false);
|
const [isResultsListModalOpen, setIsResultsListModalOpen] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
const handleRunTestCase = async (testCaseId: string) => {
|
const handleRunTestCase = async (testCaseId: string) => {
|
||||||
setRunningCases((prev) => ({ ...prev, [testCaseId]: true }));
|
setRunningCases((prev) => ({ ...prev, [testCaseId]: true }));
|
||||||
@ -92,7 +89,8 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message =
|
const message =
|
||||||
(err as { response?: { data?: { error?: { message?: string } } } })
|
(err as { response?: { data?: { error?: { message?: string } } } })
|
||||||
?.response?.data?.error?.message || "Failed to load prompt test cases";
|
?.response?.data?.error?.message ||
|
||||||
|
"Failed to load prompt test cases";
|
||||||
setError(message);
|
setError(message);
|
||||||
showToast.error(message);
|
showToast.error(message);
|
||||||
} finally {
|
} finally {
|
||||||
@ -104,7 +102,6 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
void loadData();
|
void loadData();
|
||||||
}, [promptId]);
|
}, [promptId]);
|
||||||
|
|
||||||
|
|
||||||
const columns: Column<AIPromptTestCase>[] = useMemo(
|
const columns: Column<AIPromptTestCase>[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -144,14 +141,9 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
return <span className="text-xs text-[#94a3b8]">—</span>;
|
return <span className="text-xs text-[#94a3b8]">—</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-1.5 max-w-[150px]">
|
<div className="flex flex-wrap gap-1">
|
||||||
{tags.map((tag, index) => (
|
{tags.map((tag, index) => (
|
||||||
<span
|
<CodeBadge key={index} label={tag} />
|
||||||
key={index}
|
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded-md text-[11px] font-medium bg-blue-50 text-blue-600 border border-blue-100"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -161,7 +153,7 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
key: "updatedAt",
|
key: "updatedAt",
|
||||||
label: "Last Updated",
|
label: "Last Updated",
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<span className="text-xs text-[#64748b] select-none">
|
<span className="">
|
||||||
{formatDate(row.updated_at || row.created_at || "")}
|
{formatDate(row.updated_at || row.created_at || "")}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -220,10 +212,9 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[prompt, testCases, runningCases]
|
[prompt, testCases, runningCases],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Prompt Management"
|
currentPage="Prompt Management"
|
||||||
@ -236,7 +227,9 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
description: "Manage and execute test cases for your prompt templates.",
|
description: "Manage and execute test cases for your prompt templates.",
|
||||||
action: (
|
action: (
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={() => navigate(`/tenant/ai/prompts/${promptId}/test-cases/create`)}
|
onClick={() =>
|
||||||
|
navigate(`/tenant/ai/prompts/${promptId}/test-cases/create`)
|
||||||
|
}
|
||||||
className="flex items-center gap-2 h-10 shadow-sm bg-[#112868] text-white hover:bg-[#112868]/90"
|
className="flex items-center gap-2 h-10 shadow-sm bg-[#112868] text-white hover:bg-[#112868]/90"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
@ -246,8 +239,7 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
|
<div className="overflow-hidden select-none">
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none">
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={testCases}
|
data={testCases}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -266,19 +258,26 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
<span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none">
|
<span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none">
|
||||||
Input Variables
|
Input Variables
|
||||||
</span>
|
</span>
|
||||||
{item.input_variables && Object.keys(item.input_variables).length > 0 ? (
|
{item.input_variables &&
|
||||||
|
Object.keys(item.input_variables).length > 0 ? (
|
||||||
<div className="bg-white border border-slate-200/60 p-3 rounded-lg flex flex-col gap-2">
|
<div className="bg-white border border-slate-200/60 p-3 rounded-lg flex flex-col gap-2">
|
||||||
{Object.entries(item.input_variables).map(([key, value]) => (
|
{Object.entries(item.input_variables).map(
|
||||||
<div key={key} className="flex flex-col gap-0.5">
|
([key, value]) => (
|
||||||
<span className="text-xs font-bold text-slate-600">{key}:</span>
|
<div key={key} className="flex flex-col gap-0.5">
|
||||||
<span className="text-xs text-slate-700 font-mono bg-slate-50 p-1.5 rounded border border-slate-100/80 break-words whitespace-pre-wrap">
|
<span className="text-xs font-bold text-slate-600">
|
||||||
{String(value)}
|
{key}:
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span className="text-xs text-slate-700 font-mono bg-slate-50 p-1.5 rounded border border-slate-100/80 break-words whitespace-pre-wrap">
|
||||||
))}
|
{String(value)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-slate-400 italic">No input variables defined.</span>
|
<span className="text-slate-400 italic">
|
||||||
|
No input variables defined.
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -291,7 +290,9 @@ const PromptTestCases = (): ReactElement => {
|
|||||||
{item.expected_output}
|
{item.expected_output}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-slate-400 italic">No expected output defined.</span>
|
<span className="text-slate-400 italic">
|
||||||
|
No expected output defined.
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const Suppliers = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg bg-white overflow-hidden p-2 md:p-6">
|
{/* <div className="bg-white overflow-hidden"> */}
|
||||||
{/* <div className="flex flex-col gap-4"> */}
|
{/* <div className="flex flex-col gap-4"> */}
|
||||||
{/* <div className="flex items-center justify-between">
|
{/* <div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-semibold text-[#0f1724]">
|
<h2 className="text-lg font-semibold text-[#0f1724]">
|
||||||
@ -21,7 +21,7 @@ const Suppliers = (): ReactElement => {
|
|||||||
</div> */}
|
</div> */}
|
||||||
<SuppliersTable showHeader={true} compact={false} />
|
<SuppliersTable showHeader={true} compact={false} />
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
</div>
|
{/* </div> */}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
FilterDropdown,
|
FilterDropdown,
|
||||||
type Column,
|
type Column,
|
||||||
|
GradientStatCard,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import { workflowService } from "@/services/workflow-service";
|
import { workflowService } from "@/services/workflow-service";
|
||||||
import { moduleService } from "@/services/module-service";
|
import { moduleService } from "@/services/module-service";
|
||||||
@ -21,21 +22,11 @@ const formatDate = (value?: string | null): string => {
|
|||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit"
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatCard = ({ icon: Icon, label, value, color, style }: { icon: any, label: string, value: number, color?: string, style?: React.CSSProperties }) => (
|
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl p-4 flex items-center gap-4 shadow-sm">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<Icon className={cn("w-7 h-7", color)} style={style} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-bold text-gray-900">{value}</div>
|
|
||||||
<div className="text-sm font-medium text-gray-500">{label}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Tasks = (): ReactElement => {
|
const Tasks = (): ReactElement => {
|
||||||
const { primaryColor } = useAppTheme();
|
const { primaryColor } = useAppTheme();
|
||||||
@ -47,7 +38,7 @@ const Tasks = (): ReactElement => {
|
|||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
const [statusFilter, setStatusFilter] = useState<string | null>("pending");
|
const [statusFilter, setStatusFilter] = useState<string | null>("pending");
|
||||||
const [moduleFilter, setModuleFilter] = useState<string | null>(null);
|
const [moduleFilter, setModuleFilter] = useState<string | null>(null);
|
||||||
@ -61,7 +52,7 @@ const Tasks = (): ReactElement => {
|
|||||||
try {
|
try {
|
||||||
const res = await moduleService.getMyModules();
|
const res = await moduleService.getMyModules();
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setModules(res.data.map(m => ({ id: m.id, name: m.name })));
|
setModules(res.data.map((m) => ({ id: m.id, name: m.name })));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load modules", err);
|
console.error("Failed to load modules", err);
|
||||||
@ -75,20 +66,20 @@ const Tasks = (): ReactElement => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const [tasksRes, countsRes] = await Promise.all([
|
const [tasksRes, countsRes] = await Promise.all([
|
||||||
workflowService.listTasks({
|
workflowService.listTasks({
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
module_id: moduleFilter
|
module_id: moduleFilter,
|
||||||
}),
|
}),
|
||||||
workflowService.getTaskCounts({ module_id: moduleFilter })
|
workflowService.getTaskCounts({ module_id: moduleFilter }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (tasksRes.success) {
|
if (tasksRes.success) {
|
||||||
setTasks(tasksRes.data);
|
setTasks(tasksRes.data);
|
||||||
setTotal(tasksRes.pagination.total);
|
setTotal(tasksRes.pagination.total);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (countsRes.success) {
|
if (countsRes.success) {
|
||||||
setCounts(countsRes.data);
|
setCounts(countsRes.data);
|
||||||
}
|
}
|
||||||
@ -109,8 +100,12 @@ const Tasks = (): ReactElement => {
|
|||||||
label: "Entity",
|
label: "Entity",
|
||||||
render: (task) => (
|
render: (task) => (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-gray-900">{task.entity.name}</span>
|
<span className="font-medium text-gray-900">
|
||||||
<span className="text-[11px] text-gray-500 uppercase tracking-tight">{task.entity.type}</span>
|
{task.entity.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-gray-500 uppercase tracking-tight">
|
||||||
|
{task.entity.type}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -118,7 +113,7 @@ const Tasks = (): ReactElement => {
|
|||||||
key: "workflow_name",
|
key: "workflow_name",
|
||||||
label: "Workflow",
|
label: "Workflow",
|
||||||
render: (task) => (
|
render: (task) => (
|
||||||
<span className="text-gray-700">{task.workflow.name}</span>
|
<span className="">{task.workflow.name}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -126,9 +121,12 @@ const Tasks = (): ReactElement => {
|
|||||||
label: "Step",
|
label: "Step",
|
||||||
render: (task) => (
|
render: (task) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium"
|
className="inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium"
|
||||||
style={{ backgroundColor: `${primaryColor}1A`, color: primaryColor }}
|
style={{
|
||||||
|
backgroundColor: `${primaryColor}1A`,
|
||||||
|
color: primaryColor,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{task.step.name}
|
{task.step.name}
|
||||||
</span>
|
</span>
|
||||||
@ -142,8 +140,11 @@ const Tasks = (): ReactElement => {
|
|||||||
const user = task.assignment.assigned_to_name;
|
const user = task.assignment.assigned_to_name;
|
||||||
const roleIds = task.assignment.assigned_role_ids;
|
const roleIds = task.assignment.assigned_role_ids;
|
||||||
return (
|
return (
|
||||||
<span className="text-gray-600">
|
<span className="">
|
||||||
{user || (roleIds && roleIds.length > 0 ? `${roleIds.length} roles` : "-")}
|
{user ||
|
||||||
|
(roleIds && roleIds.length > 0
|
||||||
|
? `${roleIds.length} roles`
|
||||||
|
: "-")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -152,15 +153,25 @@ const Tasks = (): ReactElement => {
|
|||||||
key: "status",
|
key: "status",
|
||||||
label: "Status",
|
label: "Status",
|
||||||
render: (task) => {
|
render: (task) => {
|
||||||
const isOverdueActive = task.is_overdue && !["completed", "rejected", "cancelled"].includes(task.status.toLowerCase());
|
const isOverdueActive =
|
||||||
|
task.is_overdue &&
|
||||||
|
!["completed", "rejected", "cancelled"].includes(
|
||||||
|
task.status.toLowerCase(),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<span className={cn(
|
<span
|
||||||
"inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset",
|
className={cn(
|
||||||
isOverdueActive
|
"inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset",
|
||||||
? "bg-red-50 text-red-700 ring-red-600/10"
|
isOverdueActive
|
||||||
: "bg-green-50 text-green-700 ring-green-600/10"
|
? "bg-red-50 text-red-700 ring-red-600/10"
|
||||||
)}>
|
: "bg-green-50 text-green-700 ring-green-600/10",
|
||||||
{isOverdueActive ? "Overdue" : task.status.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())}
|
)}
|
||||||
|
>
|
||||||
|
{isOverdueActive
|
||||||
|
? "Overdue"
|
||||||
|
: task.status
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.replace(/\b\w/g, (l) => l.toUpperCase())}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -169,9 +180,17 @@ const Tasks = (): ReactElement => {
|
|||||||
key: "due_at",
|
key: "due_at",
|
||||||
label: "Due Date",
|
label: "Due Date",
|
||||||
render: (task) => {
|
render: (task) => {
|
||||||
const isOverdueActive = task.is_overdue && !["completed", "rejected", "cancelled"].includes(task.status.toLowerCase());
|
const isOverdueActive =
|
||||||
|
task.is_overdue &&
|
||||||
|
!["completed", "rejected", "cancelled"].includes(
|
||||||
|
task.status.toLowerCase(),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<span className={cn("text-sm", isOverdueActive ? "text-red-600 font-medium" : "text-gray-600")}>
|
<span
|
||||||
|
className={cn(
|
||||||
|
isOverdueActive ? "text-red-600 font-medium" : "text-gray-600",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{formatDate(task.due_at)}
|
{formatDate(task.due_at)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -183,11 +202,11 @@ const Tasks = (): ReactElement => {
|
|||||||
render: (task) => (
|
render: (task) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (task.entity.type.toLowerCase() === 'document') {
|
if (task.entity.type.toLowerCase() === "document") {
|
||||||
navigate(`/tenant/documents/${task.entity.id}`);
|
navigate(`/tenant/documents/${task.entity.id}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="font-bold text-sm transition-colors hover:opacity-80"
|
className="font-semibold text-sm transition-colors hover:opacity-80"
|
||||||
style={{ color: primaryColor }}
|
style={{ color: primaryColor }}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@ -206,49 +225,44 @@ const Tasks = (): ReactElement => {
|
|||||||
description: "Manage your pending workflow tasks and approvals.",
|
description: "Manage your pending workflow tasks and approvals.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-6 pb-8">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Count Stats Area */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<StatCard
|
<GradientStatCard
|
||||||
icon={Inbox}
|
icon={Inbox}
|
||||||
label="Pending Tasks"
|
label="Pending Tasks"
|
||||||
value={counts?.pending || 0}
|
value={counts?.pending || 0}
|
||||||
style={{ color: primaryColor }}
|
|
||||||
/>
|
/>
|
||||||
<StatCard
|
|
||||||
icon={Clock}
|
<GradientStatCard
|
||||||
label="Overdue"
|
icon={Clock}
|
||||||
value={counts?.overdue || 0}
|
label="Overdue"
|
||||||
color="text-red-500"
|
value={counts?.overdue || 0}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<GradientStatCard
|
||||||
icon={Calendar}
|
icon={Calendar}
|
||||||
label="Due Soon"
|
label="Due Soon"
|
||||||
value={counts?.due_soon || 0}
|
value={counts?.due_soon || 0}
|
||||||
color="text-yellow-500"
|
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<GradientStatCard
|
||||||
icon={CheckCircle2}
|
icon={CheckCircle2}
|
||||||
label="Completed (Week)"
|
label="Completed (Week)"
|
||||||
value={counts?.completed_this_week || 0}
|
value={counts?.completed_this_week || 0}
|
||||||
color="text-green-500"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Task Table Area */}
|
{/* Task Table Area */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="px-6 py-4 border-b border-gray-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div className="pb-2 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<h3 className="text-lg font-bold text-gray-900">
|
<h3 className="text-lg font-bold text-gray-900">
|
||||||
{statusFilter
|
{statusFilter
|
||||||
? `${statusFilter.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())} Tasks`
|
? `${statusFilter.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())} Tasks`
|
||||||
: "All Tasks"
|
: "All Tasks"}
|
||||||
}
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<FilterDropdown
|
<FilterDropdown
|
||||||
label="Module"
|
label="Module"
|
||||||
options={modules.map(m => ({ value: m.id, label: m.name }))}
|
options={modules.map((m) => ({ value: m.id, label: m.name }))}
|
||||||
value={moduleFilter}
|
value={moduleFilter}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setModuleFilter(val as string | null);
|
setModuleFilter(val as string | null);
|
||||||
@ -289,7 +303,7 @@ const Tasks = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={tasks}
|
data={tasks}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
DeleteConfirmationModal,
|
DeleteConfirmationModal,
|
||||||
|
StatusBadge,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import { Plus, Play, Loader2, Eye, Trash2 } from "lucide-react";
|
import { Plus, Play, Loader2, Eye, Trash2 } from "lucide-react";
|
||||||
import { aiService } from "@/services/ai-service";
|
import { aiService } from "@/services/ai-service";
|
||||||
@ -16,6 +17,7 @@ import type { TenantAIConfig } from "@/types/ai";
|
|||||||
import { showToast } from "@/utils/toast";
|
import { showToast } from "@/utils/toast";
|
||||||
import { formatDate } from "@/utils/format-date";
|
import { formatDate } from "@/utils/format-date";
|
||||||
import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal";
|
import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal";
|
||||||
|
import CodeBadge from "@/components/shared/CodeBadge";
|
||||||
|
|
||||||
export const TenantAIProviders = (): ReactElement => {
|
export const TenantAIProviders = (): ReactElement => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -25,8 +27,12 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const [statusFilter, setStatusFilter] = useState<string>("");
|
const [statusFilter, setStatusFilter] = useState<string>("");
|
||||||
|
|
||||||
const [testingProviders, setTestingProviders] = useState<Record<string, boolean>>({});
|
const [testingProviders, setTestingProviders] = useState<
|
||||||
const [selectedConfig, setSelectedConfig] = useState<TenantAIConfig | null>(null);
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
|
const [selectedConfig, setSelectedConfig] = useState<TenantAIConfig | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [isViewModalOpen, setIsViewModalOpen] = useState<boolean>(false);
|
const [isViewModalOpen, setIsViewModalOpen] = useState<boolean>(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
|
||||||
const [providerToDelete, setProviderToDelete] = useState<string | null>(null);
|
const [providerToDelete, setProviderToDelete] = useState<string | null>(null);
|
||||||
@ -39,7 +45,8 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
const data = await aiService.listConfigs();
|
const data = await aiService.listConfigs();
|
||||||
setConfigs(data || []);
|
setConfigs(data || []);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.error?.message || "Failed to load configs";
|
const msg =
|
||||||
|
err?.response?.data?.error?.message || "Failed to load configs";
|
||||||
setError(msg);
|
setError(msg);
|
||||||
showToast.error(msg);
|
showToast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
@ -57,13 +64,14 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
const resp = await aiService.testConfig(provider);
|
const resp = await aiService.testConfig(provider);
|
||||||
if (resp && resp.healthy) {
|
if (resp && resp.healthy) {
|
||||||
showToast.success(
|
showToast.success(
|
||||||
`Connection healthy for ${provider}! Latency: ${resp.latency_ms || "N/A"} ms`
|
`Connection healthy for ${provider}! Latency: ${resp.latency_ms || "N/A"} ms`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showToast.error(`Connection test failed for ${provider}.`);
|
showToast.error(`Connection test failed for ${provider}.`);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.error?.message || "Failed to test connection.";
|
const msg =
|
||||||
|
err?.response?.data?.error?.message || "Failed to test connection.";
|
||||||
showToast.error(msg);
|
showToast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setTestingProviders((prev) => ({ ...prev, [provider]: false }));
|
setTestingProviders((prev) => ({ ...prev, [provider]: false }));
|
||||||
@ -76,7 +84,9 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
setSelectedConfig(cfg);
|
setSelectedConfig(cfg);
|
||||||
setIsViewModalOpen(true);
|
setIsViewModalOpen(true);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const msg = err?.response?.data?.error?.message || "Failed to fetch AI Provider config details.";
|
const msg =
|
||||||
|
err?.response?.data?.error?.message ||
|
||||||
|
"Failed to fetch AI Provider config details.";
|
||||||
showToast.error(msg);
|
showToast.error(msg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -114,9 +124,15 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
return configs.filter((cfg) => {
|
return configs.filter((cfg) => {
|
||||||
const searchMatches =
|
const searchMatches =
|
||||||
!searchQuery.trim() ||
|
!searchQuery.trim() ||
|
||||||
(cfg.provider || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(cfg.provider || "")
|
||||||
(cfg.display_name || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
|
.toLowerCase()
|
||||||
(cfg.default_model || "").toLowerCase().includes(searchQuery.toLowerCase());
|
.includes(searchQuery.toLowerCase()) ||
|
||||||
|
(cfg.display_name || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchQuery.toLowerCase()) ||
|
||||||
|
(cfg.default_model || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchQuery.toLowerCase());
|
||||||
|
|
||||||
const statusMatches =
|
const statusMatches =
|
||||||
!statusFilter ||
|
!statusFilter ||
|
||||||
@ -149,24 +165,14 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
label: "Config Type",
|
label: "Config Type",
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
const type = row.config_type || "direct";
|
const type = row.config_type || "direct";
|
||||||
return (
|
return <CodeBadge className="uppercase" label={type} />;
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-md text-[11px] font-bold uppercase tracking-wider ${
|
|
||||||
type.toLowerCase() === "azure"
|
|
||||||
? "bg-purple-50 text-purple-600 border border-purple-100"
|
|
||||||
: "bg-blue-50 text-blue-600 border border-blue-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{type}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "default_model",
|
key: "default_model",
|
||||||
label: "Default Model",
|
label: "Default Model",
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<span className="text-xs text-slate-800 font-medium select-none">
|
<span className="">
|
||||||
{row.default_model || "—"}
|
{row.default_model || "—"}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -174,31 +180,17 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
{
|
{
|
||||||
key: "is_active",
|
key: "is_active",
|
||||||
label: "Status",
|
label: "Status",
|
||||||
render: (row) => {
|
render: (row) => (
|
||||||
const active = row.is_active;
|
<StatusBadge variant={row.is_active ? "success" : "failure"}>
|
||||||
return (
|
{row.is_active ? "Active" : "Disabled"}
|
||||||
<span
|
</StatusBadge>
|
||||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium w-fit ${
|
),
|
||||||
active
|
|
||||||
? "text-green-700 bg-green-50 border border-green-100"
|
|
||||||
: "text-gray-500 bg-gray-50 border border-gray-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`w-1.5 h-1.5 rounded-full ${
|
|
||||||
active ? "bg-green-500" : "bg-gray-400"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{active ? "Active" : "Disabled"}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "last_verified_at",
|
key: "last_verified_at",
|
||||||
label: "Last Verified",
|
label: "Last Verified",
|
||||||
render: (row) => (
|
render: (row) => (
|
||||||
<span className="text-xs text-[#64748b]">
|
<span className="">
|
||||||
{row.last_verified_at ? formatDate(row.last_verified_at) : "Never"}
|
{row.last_verified_at ? formatDate(row.last_verified_at) : "Never"}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
@ -232,7 +224,9 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
onClick: () => handleViewConfig(row.provider),
|
onClick: () => handleViewConfig(row.provider),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Trash2 className="w-3.5 h-3.5 text-red-500 shrink-0" />,
|
icon: (
|
||||||
|
<Trash2 className="w-3.5 h-3.5 text-red-500 shrink-0" />
|
||||||
|
),
|
||||||
label: "Delete Config",
|
label: "Delete Config",
|
||||||
onClick: () => handleDeleteConfig(row.provider),
|
onClick: () => handleDeleteConfig(row.provider),
|
||||||
},
|
},
|
||||||
@ -243,7 +237,7 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[testingProviders]
|
[testingProviders],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -263,7 +257,7 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-2">
|
||||||
{/* Subhead Toolbar matching Screenshot filter design */}
|
{/* Subhead Toolbar matching Screenshot filter design */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
@ -297,16 +291,14 @@ export const TenantAIProviders = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table list */}
|
{/* Table list */}
|
||||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none">
|
<DataTable
|
||||||
<DataTable
|
data={filteredConfigs}
|
||||||
data={filteredConfigs}
|
columns={columns}
|
||||||
columns={columns}
|
keyExtractor={(item) => item.id || item.provider}
|
||||||
keyExtractor={(item) => item.id || item.provider}
|
isLoading={isLoading}
|
||||||
isLoading={isLoading}
|
error={error}
|
||||||
error={error}
|
emptyMessage="No tenant AI providers configured."
|
||||||
emptyMessage="No tenant AI providers configured."
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ViewAIProviderModal
|
<ViewAIProviderModal
|
||||||
|
|||||||
@ -1,17 +1,33 @@
|
|||||||
import { type ReactElement } from "react";
|
import { useRef, type ReactElement } from "react";
|
||||||
import { Layout } from "@/components/layout/Layout";
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import { UsersTable } from "@/components/superadmin";
|
import { UsersTable, type UsersTableRef } from "@/components/superadmin";
|
||||||
|
import { PrimaryButton } from "@/components/shared";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { usePermissions } from "@/hooks/usePermissions";
|
||||||
|
|
||||||
const Users = (): ReactElement => {
|
const Users = (): ReactElement => {
|
||||||
|
const tableRef = useRef<UsersTableRef>(null);
|
||||||
|
const { canCreate } = usePermissions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Users"
|
currentPage="Users"
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: "User Management",
|
title: "User List",
|
||||||
description: "View and manage all users within your organization.",
|
description: "View and manage all users within your organization.",
|
||||||
|
action: canCreate("users") ? (
|
||||||
|
<PrimaryButton
|
||||||
|
size="default"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => tableRef.current?.openNewModal()}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>New User</span>
|
||||||
|
</PrimaryButton>
|
||||||
|
) : null,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UsersTable showHeader={true} />
|
<UsersTable ref={tableRef} showHeader={true} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1643,7 +1643,7 @@ const ViewDocument = (): ReactElement => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="p-5 space-y-3">
|
<div className="space-y-4">
|
||||||
{activeAction === "submit" && (
|
{activeAction === "submit" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<FormSelect
|
<FormSelect
|
||||||
@ -1786,7 +1786,7 @@ const ViewDocument = (): ReactElement => {
|
|||||||
description="Track the progress of this document's approval workflow."
|
description="Track the progress of this document's approval workflow."
|
||||||
maxWidth="2xl"
|
maxWidth="2xl"
|
||||||
>
|
>
|
||||||
<div className="p-6">
|
<div className="">
|
||||||
{isWorkflowLoading ? (
|
{isWorkflowLoading ? (
|
||||||
<div className="flex flex-col items-center justify-center py-10 space-y-3">
|
<div className="flex flex-col items-center justify-center py-10 space-y-3">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#112868]"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#112868]"></div>
|
||||||
|
|||||||
@ -1,24 +1,35 @@
|
|||||||
import { type ReactElement } from "react";
|
import { useRef, type ReactElement } from "react";
|
||||||
import { Layout } from "@/components/layout/Layout";
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import { WorkflowDefinitionsTable } from "@/components/shared";
|
import {
|
||||||
|
WorkflowDefinitionsTable,
|
||||||
|
type WorkflowDefinitionsTableRef,
|
||||||
|
PrimaryButton,
|
||||||
|
} from "@/components/shared";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
|
||||||
const WorkflowDefinationPage = (): ReactElement => {
|
const WorkflowDefinationPage = (): ReactElement => {
|
||||||
|
const tableRef = useRef<WorkflowDefinitionsTableRef>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Workflow Definitions"
|
currentPage="Workflow Definitions"
|
||||||
// breadcrumbs={[
|
pageHeader={{
|
||||||
// // { label: "Platform", path: "/tenant" },
|
title: "Workflow Definitions",
|
||||||
// { label: "Workflow Definitions" },
|
description: "Create and manage document approval workflow definitions.",
|
||||||
// ]}
|
action: (
|
||||||
|
<PrimaryButton
|
||||||
|
size="default"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => tableRef.current?.openNewModal()}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
<span>New Workflow</span>
|
||||||
|
</PrimaryButton>
|
||||||
|
),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex items-center justify-between">
|
<WorkflowDefinitionsTable ref={tableRef} compact={false} />
|
||||||
<h1 className="text-2xl font-bold text-[#0f1724] tracking-[-0.48px]">
|
|
||||||
Workflow Definitions
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<WorkflowDefinitionsTable compact={false} />
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user