refactor: update NotificationMaster code style, reorganize state management, and enhance pagination functionality
This commit is contained in:
parent
bc7972a92a
commit
3cf91ee256
@ -142,7 +142,7 @@ export const Pagination = ({
|
|||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
data-limit-dropdown="true"
|
data-limit-dropdown="true"
|
||||||
className="fixed border border-[rgba(0,0,0,0.2)] rounded-md shadow-lg z-[250] max-h-60 overflow-y-auto"
|
className="fixed bg-white border border-[rgba(0,0,0,0.15)] 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">
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef, type KeyboardEvent } from 'react';
|
import { useState, useEffect, useRef, type KeyboardEvent } from "react";
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from "react";
|
||||||
import { Layout } from '@/components/layout/Layout';
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { Layout } from "@/components/layout/Layout";
|
||||||
import {
|
import {
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
DataTable,
|
DataTable,
|
||||||
@ -14,25 +15,31 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
type Column,
|
type Column,
|
||||||
SearchBox,
|
SearchBox,
|
||||||
} from '@/components/shared';
|
} from "@/components/shared";
|
||||||
import { Plus, Code, 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";
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
const categorySchema = z.object({
|
const categorySchema = z.object({
|
||||||
name: z.string().min(1, 'Category name is required').max(100),
|
name: z.string().min(1, "Category name is required").max(100),
|
||||||
code: z.string().min(1, 'Category slug is required').regex(/^[a-z0-9_]+$/, 'Slug must be lowercase alphanumeric with underscores'),
|
code: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Category slug is required")
|
||||||
|
.regex(
|
||||||
|
/^[a-z0-9_]+$/,
|
||||||
|
"Slug must be lowercase alphanumeric with underscores",
|
||||||
|
),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
module_id: z.string().optional(),
|
module_id: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const codeSchema = z.object({
|
const codeSchema = z.object({
|
||||||
code: z.string().min(1, 'Code is required').max(50),
|
code: z.string().min(1, "Code is required").max(50),
|
||||||
name: z.string().min(1, 'Display name is required').max(100),
|
name: z.string().min(1, "Display name is required").max(100),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
default_channels: z.array(z.string()),
|
default_channels: z.array(z.string()),
|
||||||
default_priority: z.string(),
|
default_priority: z.string(),
|
||||||
@ -49,27 +56,28 @@ const VariableTagInput = ({
|
|||||||
variables: string[];
|
variables: string[];
|
||||||
onChange: (vars: string[]) => void;
|
onChange: (vars: string[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState("");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const addTag = (val: string) => {
|
const addTag = (val: string) => {
|
||||||
const tag = val.trim().replace(/\s+/g, '_').toLowerCase();
|
const tag = val.trim().replace(/\s+/g, "_").toLowerCase();
|
||||||
if (tag && !variables.includes(tag)) {
|
if (tag && !variables.includes(tag)) {
|
||||||
onChange([...variables, tag]);
|
onChange([...variables, tag]);
|
||||||
}
|
}
|
||||||
setInput('');
|
setInput("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter' || e.key === ',' || e.key === ' ') {
|
if (e.key === "Enter" || e.key === "," || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
addTag(input);
|
addTag(input);
|
||||||
} else if (e.key === 'Backspace' && !input && variables.length > 0) {
|
} else if (e.key === "Backspace" && !input && variables.length > 0) {
|
||||||
onChange(variables.slice(0, -1));
|
onChange(variables.slice(0, -1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTag = (tag: string) => onChange(variables.filter(v => v !== tag));
|
const removeTag = (tag: string) =>
|
||||||
|
onChange(variables.filter((v) => v !== tag));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -77,17 +85,26 @@ const VariableTagInput = ({
|
|||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<Tag className="w-3.5 h-3.5 text-indigo-500" />
|
<Tag className="w-3.5 h-3.5 text-indigo-500" />
|
||||||
Supported Variables
|
Supported Variables
|
||||||
<span className="text-gray-400 font-normal">(used in template placeholders)</span>
|
<span className="text-gray-400 font-normal">
|
||||||
|
(used in template placeholders)
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
className="flex flex-wrap gap-1.5 p-2 border border-gray-200 rounded-lg bg-white min-h-[42px] cursor-text focus-within:ring-2 focus-within:ring-indigo-500/20 focus-within:border-indigo-400 transition-all"
|
className="flex flex-wrap gap-1.5 p-2 border border-gray-200 rounded-lg bg-white min-h-[42px] cursor-text focus-within:ring-2 focus-within:ring-indigo-500/20 focus-within:border-indigo-400 transition-all"
|
||||||
onClick={() => inputRef.current?.focus()}
|
onClick={() => inputRef.current?.focus()}
|
||||||
>
|
>
|
||||||
{variables.map(v => (
|
{variables.map((v) => (
|
||||||
<span key={v} className="flex items-center gap-1 bg-indigo-50 border border-indigo-200 text-indigo-700 text-[11px] font-mono font-bold px-2 py-0.5 rounded-full">
|
<span
|
||||||
|
key={v}
|
||||||
|
className="flex items-center gap-1 bg-indigo-50 border border-indigo-200 text-indigo-700 text-[11px] font-mono font-bold px-2 py-0.5 rounded-full"
|
||||||
|
>
|
||||||
{`{{${v}}}`}
|
{`{{${v}}}`}
|
||||||
<button type="button" onClick={() => removeTag(v)} className="hover:text-red-600 transition-colors ml-0.5">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeTag(v)}
|
||||||
|
className="hover:text-red-600 transition-colors ml-0.5"
|
||||||
|
>
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@ -95,21 +112,27 @@ const VariableTagInput = ({
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={e => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={() => input && addTag(input)}
|
onBlur={() => input && addTag(input)}
|
||||||
placeholder={variables.length === 0 ? 'Type variable name, press Enter or comma...' : ''}
|
placeholder={
|
||||||
|
variables.length === 0
|
||||||
|
? "Type variable name, press Enter or comma..."
|
||||||
|
: ""
|
||||||
|
}
|
||||||
className="flex-1 min-w-[120px] text-xs outline-none bg-transparent text-gray-700 placeholder-gray-300"
|
className="flex-1 min-w-[120px] text-xs outline-none bg-transparent text-gray-700 placeholder-gray-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-gray-400 mt-1">
|
<p className="text-[10px] text-gray-400 mt-1">
|
||||||
These are the <code>{'{{variable}}'}</code> placeholders admins can use in templates for this event code.
|
These are the <code>{"{{variable}}"}</code> placeholders admins can use
|
||||||
|
in templates for this event code.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotificationMaster = (): ReactElement => {
|
const NotificationMaster = (): ReactElement => {
|
||||||
|
const location = useLocation();
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
const [categories, setCategories] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
@ -118,7 +141,7 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
const [limit, setLimit] = useState(10);
|
const [limit, setLimit] = useState(10);
|
||||||
const [totalItems, setTotalItems] = useState(0);
|
const [totalItems, setTotalItems] = useState(0);
|
||||||
const [totalPages, setTotalPages] = useState(0);
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState("");
|
||||||
const [moduleFilter, setModuleFilter] = useState<string | null>(null);
|
const [moduleFilter, setModuleFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
// Category Modal
|
// Category Modal
|
||||||
@ -134,11 +157,14 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
watch: watchCategory,
|
watch: watchCategory,
|
||||||
} = useForm<CategoryFormValues>({
|
} = useForm<CategoryFormValues>({
|
||||||
resolver: zodResolver(categorySchema),
|
resolver: zodResolver(categorySchema),
|
||||||
defaultValues: { name: '', code: '', description: '', module_id: '' }
|
defaultValues: { name: "", code: "", description: "", module_id: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Code Modal
|
// View Mode
|
||||||
const [codeModalOpen, setCodeModalOpen] = useState<boolean>(false);
|
const [viewMode, setViewMode] = useState<"categories" | "codes">(
|
||||||
|
"categories",
|
||||||
|
);
|
||||||
|
const [codeFormModalOpen, setCodeFormModalOpen] = useState<boolean>(false);
|
||||||
const [selectedCategory, setSelectedCategory] = useState<any>(null);
|
const [selectedCategory, setSelectedCategory] = useState<any>(null);
|
||||||
const [codes, setCodes] = useState<any[]>([]);
|
const [codes, setCodes] = useState<any[]>([]);
|
||||||
const [isCodesLoading, setIsCodesLoading] = useState<boolean>(false);
|
const [isCodesLoading, setIsCodesLoading] = useState<boolean>(false);
|
||||||
@ -147,6 +173,7 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
const [codePage, setCodePage] = useState(1);
|
const [codePage, setCodePage] = useState(1);
|
||||||
const [codeTotal, setCodeTotal] = useState(0);
|
const [codeTotal, setCodeTotal] = useState(0);
|
||||||
const [codePages, setCodePages] = useState(0);
|
const [codePages, setCodePages] = useState(0);
|
||||||
|
const [codeLimit, setCodeLimit] = useState(5);
|
||||||
|
|
||||||
const [editingCode, setEditingCode] = useState<any>(null);
|
const [editingCode, setEditingCode] = useState<any>(null);
|
||||||
const [codeVariables, setCodeVariables] = useState<string[]>([]);
|
const [codeVariables, setCodeVariables] = useState<string[]>([]);
|
||||||
@ -158,12 +185,22 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
formState: { errors: codeErrors },
|
formState: { errors: codeErrors },
|
||||||
} = useForm<CodeFormValues>({
|
} = useForm<CodeFormValues>({
|
||||||
resolver: zodResolver(codeSchema),
|
resolver: zodResolver(codeSchema),
|
||||||
defaultValues: { code: '', name: '', description: '', default_channels: ['in_app', 'email'], default_priority: 'normal' }
|
defaultValues: {
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
default_channels: ["in_app", "email"],
|
||||||
|
default_priority: "normal",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete Modal
|
// Delete Modal
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
|
||||||
const [deleteTarget, setDeleteTarget] = useState<{ id: string, name: string, type: 'category' | 'code' } | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: "category" | "code";
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const [modules, setModules] = useState<any[]>([]);
|
const [modules, setModules] = useState<any[]>([]);
|
||||||
|
|
||||||
@ -174,7 +211,7 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
limit,
|
limit,
|
||||||
offset: (currentPage - 1) * limit,
|
offset: (currentPage - 1) * limit,
|
||||||
search,
|
search,
|
||||||
module_id: moduleFilter || undefined
|
module_id: moduleFilter || undefined,
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setCategories(res.data);
|
setCategories(res.data);
|
||||||
@ -182,7 +219,7 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
setTotalPages(res.pagination?.pages || 1);
|
setTotalPages(res.pagination?.pages || 1);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showToast.error(err.message || 'Failed to fetch categories');
|
showToast.error(err.message || "Failed to fetch categories");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@ -193,7 +230,7 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
const res = await moduleService.getDropdown();
|
const res = await moduleService.getDropdown();
|
||||||
if (res.success) setModules(res.data);
|
if (res.success) setModules(res.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch modules', err);
|
console.error("Failed to fetch modules", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -202,12 +239,20 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
fetchModules();
|
fetchModules();
|
||||||
}, [currentPage, limit, search, moduleFilter]);
|
}, [currentPage, limit, search, moduleFilter]);
|
||||||
|
|
||||||
const fetchCodes = async (category: any, page: number = 1) => {
|
useEffect(() => {
|
||||||
|
setViewMode("categories");
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
const fetchCodes = async (
|
||||||
|
category: any,
|
||||||
|
page: number = 1,
|
||||||
|
currentLimit: number = codeLimit,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
setIsCodesLoading(true);
|
setIsCodesLoading(true);
|
||||||
const res = await notificationService.getCodesByCategory(category.id, {
|
const res = await notificationService.getCodesByCategory(category.id, {
|
||||||
limit: 5,
|
limit: currentLimit,
|
||||||
offset: (page - 1) * 5
|
offset: (page - 1) * currentLimit,
|
||||||
});
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setCodes(res.data);
|
setCodes(res.data);
|
||||||
@ -215,7 +260,7 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
setCodePages(res.pagination?.pages || 1);
|
setCodePages(res.pagination?.pages || 1);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showToast.error('Failed to fetch codes');
|
showToast.error("Failed to fetch codes");
|
||||||
} finally {
|
} finally {
|
||||||
setIsCodesLoading(false);
|
setIsCodesLoading(false);
|
||||||
}
|
}
|
||||||
@ -226,16 +271,22 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
setCodePage(1);
|
setCodePage(1);
|
||||||
setEditingCode(null);
|
setEditingCode(null);
|
||||||
setCodeVariables([]);
|
setCodeVariables([]);
|
||||||
resetCode({ code: '', name: '', description: '', default_channels: ['in_app', 'email'], default_priority: 'normal' });
|
resetCode({
|
||||||
await fetchCodes(category, 1);
|
code: "",
|
||||||
setCodeModalOpen(true);
|
name: "",
|
||||||
|
description: "",
|
||||||
|
default_channels: ["in_app", "email"],
|
||||||
|
default_priority: "normal",
|
||||||
|
});
|
||||||
|
await fetchCodes(category, 1, codeLimit);
|
||||||
|
setViewMode("codes");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedCategory && codeModalOpen) {
|
if (selectedCategory && viewMode === "codes") {
|
||||||
fetchCodes(selectedCategory, codePage);
|
fetchCodes(selectedCategory, codePage, codeLimit);
|
||||||
}
|
}
|
||||||
}, [codePage]);
|
}, [codePage, codeLimit, selectedCategory, viewMode]);
|
||||||
|
|
||||||
const onSaveCategory = async (data: CategoryFormValues) => {
|
const onSaveCategory = async (data: CategoryFormValues) => {
|
||||||
try {
|
try {
|
||||||
@ -244,16 +295,18 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
|
|
||||||
if (editingCategory) {
|
if (editingCategory) {
|
||||||
await notificationService.updateCategory(editingCategory.id, payload);
|
await notificationService.updateCategory(editingCategory.id, payload);
|
||||||
showToast.success('Category updated');
|
showToast.success("Category updated");
|
||||||
} else {
|
} else {
|
||||||
await notificationService.createCategory(payload);
|
await notificationService.createCategory(payload);
|
||||||
showToast.success('Category created');
|
showToast.success("Category created");
|
||||||
}
|
}
|
||||||
setCategoryModalOpen(false);
|
setCategoryModalOpen(false);
|
||||||
resetCategory();
|
resetCategory();
|
||||||
fetchCategories();
|
fetchCategories();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showToast.error(err.response?.data?.error?.message || err.message || 'Action failed');
|
showToast.error(
|
||||||
|
err.response?.data?.error?.message || err.message || "Action failed",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -262,46 +315,89 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
const payload = { ...data, variables: codeVariables };
|
const payload = { ...data, variables: codeVariables };
|
||||||
if (editingCode) {
|
if (editingCode) {
|
||||||
await notificationService.updateCode(editingCode.id, payload);
|
await notificationService.updateCode(editingCode.id, payload);
|
||||||
showToast.success('Code updated');
|
showToast.success("Code updated");
|
||||||
} else {
|
} else {
|
||||||
await notificationService.createCode(selectedCategory.id, payload);
|
await notificationService.createCode(selectedCategory.id, payload);
|
||||||
showToast.success('Code created');
|
showToast.success("Code created");
|
||||||
}
|
}
|
||||||
setEditingCode(null);
|
setEditingCode(null);
|
||||||
setCodeVariables([]);
|
setCodeVariables([]);
|
||||||
resetCode({ code: '', name: '', description: '', default_channels: ['in_app', 'email'], default_priority: 'normal' });
|
resetCode({
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
default_channels: ["in_app", "email"],
|
||||||
|
default_priority: "normal",
|
||||||
|
});
|
||||||
|
setCodeFormModalOpen(false);
|
||||||
fetchCodes(selectedCategory, codePage);
|
fetchCodes(selectedCategory, codePage);
|
||||||
|
fetchCategories();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showToast.error(err.response?.data?.error?.message || err.message || 'Action failed');
|
showToast.error(
|
||||||
|
err.response?.data?.error?.message || err.message || "Action failed",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
try {
|
try {
|
||||||
if (deleteTarget.type === 'category') {
|
if (deleteTarget.type === "category") {
|
||||||
await notificationService.deleteCategory(deleteTarget.id);
|
await notificationService.deleteCategory(deleteTarget.id);
|
||||||
fetchCategories();
|
fetchCategories();
|
||||||
} else {
|
} else {
|
||||||
await notificationService.deleteCode(deleteTarget.id);
|
await notificationService.deleteCode(deleteTarget.id);
|
||||||
fetchCodes(selectedCategory, codePage);
|
fetchCodes(selectedCategory, codePage);
|
||||||
|
fetchCategories();
|
||||||
}
|
}
|
||||||
setDeleteModalOpen(false);
|
setDeleteModalOpen(false);
|
||||||
showToast.success(`${deleteTarget.type === 'category' ? 'Category' : 'Code'} deleted`);
|
showToast.success(
|
||||||
|
`${deleteTarget.type === "category" ? "Category" : "Code"} deleted`,
|
||||||
|
);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showToast.error(err.message || 'Delete failed');
|
showToast.error(err.message || "Delete failed");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: Column<any>[] = [
|
const columns: Column<any>[] = [
|
||||||
{ key: 'name', label: 'Category Name', render: (c) => <span className="font-semibold text-[#0f1724]">{c.name}</span> },
|
|
||||||
{ key: 'code', label: 'Slug / Code', render: (c) => <code className="text-[10px] bg-gray-100 px-1.5 py-0.5 rounded font-mono text-gray-600">{c.code}</code> },
|
|
||||||
{ key: 'description', label: 'Description', render: (c) => <span className="text-xs text-gray-500 line-clamp-1">{c.description || '-'}</span> },
|
|
||||||
{ key: 'module_id', label: 'Assoc. Module', render: (c) => <span className="text-[10px] text-blue-500 font-bold uppercase">{c.module_code || 'System'}</span> },
|
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: "name",
|
||||||
label: 'Actions',
|
label: "Category Name",
|
||||||
align: 'right',
|
render: (c) => (
|
||||||
|
<span className="font-semibold text-[#0f1724]">{c.name}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "code",
|
||||||
|
label: "Slug / Code",
|
||||||
|
render: (c) => (
|
||||||
|
<code className="text-[10px] bg-gray-100 px-1.5 py-0.5 rounded font-mono text-gray-600">
|
||||||
|
{c.code}
|
||||||
|
</code>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "description",
|
||||||
|
label: "Description",
|
||||||
|
render: (c) => (
|
||||||
|
<span className="text-xs text-gray-500 line-clamp-1">
|
||||||
|
{c.description || "-"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "module_id",
|
||||||
|
label: "Assoc. Module",
|
||||||
|
render: (c) => (
|
||||||
|
<span className="text-[10px] text-blue-500 font-bold uppercase">
|
||||||
|
{c.module_code || "System"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
label: "Actions",
|
||||||
|
align: "right",
|
||||||
render: (c) => (
|
render: (c) => (
|
||||||
<div className="flex justify-end gap-3 items-center">
|
<div className="flex justify-end gap-3 items-center">
|
||||||
<button
|
<button
|
||||||
@ -314,39 +410,166 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
<ActionDropdown
|
<ActionDropdown
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
setEditingCategory(c);
|
setEditingCategory(c);
|
||||||
resetCategory({ name: c.name, code: c.code, description: c.description || '', module_id: c.module_id || '' });
|
resetCategory({
|
||||||
|
name: c.name,
|
||||||
|
code: c.code,
|
||||||
|
description: c.description || "",
|
||||||
|
module_id: c.module_id || "",
|
||||||
|
});
|
||||||
setCategoryModalOpen(true);
|
setCategoryModalOpen(true);
|
||||||
}}
|
}}
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
setDeleteTarget({ id: c.id, name: c.name, type: 'category' });
|
setDeleteTarget({ id: c.id, name: c.name, type: "category" });
|
||||||
setDeleteModalOpen(true);
|
setDeleteModalOpen(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const codeColumns: Column<any>[] = [
|
||||||
|
{
|
||||||
|
key: "code",
|
||||||
|
label: "Trigger Code",
|
||||||
|
render: (c) => (
|
||||||
|
<code className="text-[11px] font-mono font-bold text-indigo-600 bg-indigo-50 px-1.5 py-0.5 rounded border border-indigo-100">
|
||||||
|
{c.code}
|
||||||
|
</code>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "Name",
|
||||||
|
render: (c) => (
|
||||||
|
<span className="font-semibold text-[#0f1724]">{c.name}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "variables",
|
||||||
|
label: "Variables",
|
||||||
|
render: (c) => (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{Array.isArray(c.variables) && c.variables.length > 0 ? (
|
||||||
|
c.variables.map((v: string) => (
|
||||||
|
<span
|
||||||
|
key={v}
|
||||||
|
className="text-[10px] font-mono bg-indigo-50 text-indigo-600 border border-indigo-100 px-1.5 py-0.5 rounded-full"
|
||||||
|
>
|
||||||
|
{`{{${v}}}`}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-[10px] text-gray-300 italic">none</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
label: "Actions",
|
||||||
|
align: "right",
|
||||||
|
render: (c) => (
|
||||||
|
<div className="flex justify-end gap-3 whitespace-nowrap">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingCode(c);
|
||||||
|
setCodeVariables(Array.isArray(c.variables) ? c.variables : []);
|
||||||
|
resetCode({
|
||||||
|
code: c.code,
|
||||||
|
name: c.name,
|
||||||
|
description: c.description || "",
|
||||||
|
default_channels: c.default_channels,
|
||||||
|
default_priority: c.default_priority,
|
||||||
|
});
|
||||||
|
setCodeFormModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="text-blue-600 hover:text-blue-800 font-bold text-[11px] uppercase"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteTarget({
|
||||||
|
id: c.id,
|
||||||
|
name: c.code,
|
||||||
|
type: "code",
|
||||||
|
});
|
||||||
|
setDeleteModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="text-red-500 hover:text-red-700 font-bold text-[11px] uppercase"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const breadcrumbs =
|
||||||
|
viewMode === "codes"
|
||||||
|
? [
|
||||||
|
{ label: "Notification Master", path: "/notification-master" },
|
||||||
|
{ label: `Event Codes (${selectedCategory?.name})` },
|
||||||
|
]
|
||||||
|
: [{ label: "Notification Master" }];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="Notification Master"
|
currentPage="Notification Master"
|
||||||
|
breadcrumbs={breadcrumbs}
|
||||||
pageHeader={{
|
pageHeader={{
|
||||||
title: 'Notification Master Management',
|
title:
|
||||||
description: 'Manage notification categories and event codes across the platform.',
|
viewMode === "codes"
|
||||||
|
? `Event Codes: ${selectedCategory?.name}`
|
||||||
|
: "Notification Master Management",
|
||||||
|
description:
|
||||||
|
viewMode === "codes"
|
||||||
|
? `Manage event trigger codes and variables for the ${selectedCategory?.name} category.`
|
||||||
|
: "Manage notification categories and event codes across the platform.",
|
||||||
|
action:
|
||||||
|
viewMode === "codes" ? (
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("categories")}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 border border-gray-200 rounded-lg text-xs font-semibold text-gray-600 bg-white hover:bg-gray-50 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
← Back to Categories
|
||||||
|
</button>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={() => {
|
||||||
|
setEditingCode(null);
|
||||||
|
setCodeVariables([]);
|
||||||
|
resetCode({
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
default_channels: ["in_app", "email"],
|
||||||
|
default_priority: "normal",
|
||||||
|
});
|
||||||
|
setCodeFormModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" /> New Code
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
) : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="overflow-hidden flex flex-col min-h-[500px]">
|
{viewMode === "categories" ? (
|
||||||
<div className="pb-2 flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
|
<div className="overflow-hidden flex flex-col min-h-[500px]">
|
||||||
<div className="flex items-center gap-4 flex-1">
|
<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">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setSearch(val);
|
setSearch(val);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
placeholder="Search categories..."
|
placeholder="Search categories..."
|
||||||
containerClassName="relative flex-1 max-w-sm"
|
containerClassName="relative flex-1 max-w-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FilterDropdown
|
<FilterDropdown
|
||||||
label="Module"
|
label="Module"
|
||||||
@ -359,154 +582,198 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
placeholder="All Modules"
|
placeholder="All Modules"
|
||||||
isSearchable
|
isSearchable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PrimaryButton onClick={() => {
|
<PrimaryButton
|
||||||
setEditingCategory(null);
|
onClick={() => {
|
||||||
resetCategory({ name: '', code: '', description: '', module_id: '' });
|
setEditingCategory(null);
|
||||||
setCategoryModalOpen(true);
|
resetCategory({
|
||||||
}} className="flex gap-2">
|
name: "",
|
||||||
<Plus className="w-4 h-4" /> New Category
|
code: "",
|
||||||
</PrimaryButton>
|
description: "",
|
||||||
</div>
|
module_id: "",
|
||||||
|
});
|
||||||
|
setCategoryModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" /> New Category
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
{/* <div className="flex-1"> */}
|
||||||
<DataTable columns={columns} data={categories} isLoading={isLoading} keyExtractor={(c) => c.id} />
|
<DataTable
|
||||||
</div>
|
columns={columns}
|
||||||
|
data={categories}
|
||||||
|
isLoading={isLoading}
|
||||||
|
keyExtractor={(c) => c.id}
|
||||||
|
/>
|
||||||
|
{/* </div> */}
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
totalItems={totalItems}
|
totalItems={totalItems}
|
||||||
limit={limit}
|
limit={limit}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
onLimitChange={(l) => { setLimit(l); setCurrentPage(1); }}
|
onLimitChange={(l) => {
|
||||||
/>
|
setLimit(l);
|
||||||
</div>
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<DataTable
|
||||||
|
columns={codeColumns}
|
||||||
|
data={codes}
|
||||||
|
isLoading={isCodesLoading}
|
||||||
|
keyExtractor={(c) => c.id}
|
||||||
|
emptyMessage="No codes registered for this category."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
currentPage={codePage}
|
||||||
|
totalPages={codePages}
|
||||||
|
totalItems={codeTotal}
|
||||||
|
limit={codeLimit}
|
||||||
|
onPageChange={setCodePage}
|
||||||
|
onLimitChange={(l) => {
|
||||||
|
setCodeLimit(l);
|
||||||
|
setCodePage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Category Modal */}
|
{/* Category Modal */}
|
||||||
<Modal isOpen={categoryModalOpen} onClose={() => setCategoryModalOpen(false)} title={editingCategory ? "Edit Category" : "New Category"} maxWidth="lg">
|
<Modal
|
||||||
<div className="space-y-4 p-6">
|
isOpen={categoryModalOpen}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
onClose={() => setCategoryModalOpen(false)}
|
||||||
<FormField label="Category Name" required placeholder="e.g. Workflow" error={categoryErrors.name?.message} {...registerCategory('name')} />
|
title={editingCategory ? "Edit Category" : "New Category"}
|
||||||
<FormField label="Slug (Code)" required placeholder="e.g. workflow" error={categoryErrors.code?.message} {...registerCategory('code')} />
|
maxWidth="lg"
|
||||||
</div>
|
footer={
|
||||||
<FormTextArea label="Description" rows={2} error={categoryErrors.description?.message} {...registerCategory('description')} />
|
<>
|
||||||
<FormSelect
|
<button
|
||||||
label="Associated Module"
|
onClick={() => setCategoryModalOpen(false)}
|
||||||
value={watchCategory('module_id')}
|
className="px-4 py-2 text-sm text-gray-600"
|
||||||
options={[
|
>
|
||||||
{ value: '', label: 'System (No Module)' },
|
Cancel
|
||||||
...modules.map(m => ({ value: m.id, label: m.name }))
|
</button>
|
||||||
]}
|
<PrimaryButton onClick={handleCategorySubmit(onSaveCategory)}>
|
||||||
onValueChange={(val) => setCategoryValue('module_id', val, { shouldValidate: true })}
|
{editingCategory ? "Update" : "Create"}
|
||||||
placeholder="Select a module"
|
</PrimaryButton>
|
||||||
error={categoryErrors.module_id?.message}
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
label="Category Name"
|
||||||
|
required
|
||||||
|
placeholder="e.g. Workflow"
|
||||||
|
error={categoryErrors.name?.message}
|
||||||
|
{...registerCategory("name")}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Slug (Code)"
|
||||||
|
required
|
||||||
|
placeholder="e.g. workflow"
|
||||||
|
error={categoryErrors.code?.message}
|
||||||
|
{...registerCategory("code")}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-3 mt-6 pt-4 border-t">
|
|
||||||
<button onClick={() => setCategoryModalOpen(false)} className="px-4 py-2 text-sm text-gray-600">Cancel</button>
|
|
||||||
<PrimaryButton onClick={handleCategorySubmit(onSaveCategory)}>{editingCategory ? 'Update' : 'Create'}</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<FormTextArea
|
||||||
|
label="Description"
|
||||||
|
rows={2}
|
||||||
|
error={categoryErrors.description?.message}
|
||||||
|
{...registerCategory("description")}
|
||||||
|
/>
|
||||||
|
<FormSelect
|
||||||
|
label="Associated Module"
|
||||||
|
value={watchCategory("module_id")}
|
||||||
|
options={[
|
||||||
|
{ value: "", label: "System (No Module)" },
|
||||||
|
...modules.map((m) => ({ value: m.id, label: m.name })),
|
||||||
|
]}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
setCategoryValue("module_id", val, { shouldValidate: true })
|
||||||
|
}
|
||||||
|
placeholder="Select a module"
|
||||||
|
error={categoryErrors.module_id?.message}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Codes Modal */}
|
{/* Code Form Modal */}
|
||||||
<Modal isOpen={codeModalOpen} onClose={() => setCodeModalOpen(false)} title={`Event Codes: ${selectedCategory?.name}`} maxWidth="2xl">
|
<Modal
|
||||||
<div className="p-6 space-y-6">
|
isOpen={codeFormModalOpen}
|
||||||
{/* ── Add / Edit Code Form ─────────────────────────────────── */}
|
onClose={() => {
|
||||||
<div className="bg-gray-50 p-5 rounded-xl border border-gray-200 shadow-inner">
|
setCodeFormModalOpen(false);
|
||||||
<h3 className="text-[11px] font-bold uppercase text-gray-500 tracking-wider font-mono flex items-center gap-2 pb-4">
|
setEditingCode(null);
|
||||||
{editingCode ? 'Edit Event Code' : 'Add New Event Trigger'}
|
setCodeVariables([]);
|
||||||
</h3>
|
resetCode({
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
default_channels: ["in_app", "email"],
|
||||||
|
default_priority: "normal",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title={editingCode ? "Edit Event Code" : "Add New Event Trigger"}
|
||||||
|
maxWidth="lg"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCodeFormModalOpen(false);
|
||||||
|
setEditingCode(null);
|
||||||
|
setCodeVariables([]);
|
||||||
|
resetCode({
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
default_channels: ["in_app", "email"],
|
||||||
|
default_priority: "normal",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<PrimaryButton onClick={handleCodeSubmit(onSaveCode)}>
|
||||||
|
{editingCode ? "Update Code" : "Add Code"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
label="Code (e.g. task_assigned)"
|
||||||
|
required
|
||||||
|
error={codeErrors.code?.message}
|
||||||
|
disabled={!!editingCode}
|
||||||
|
{...registerCode("code")}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Display Name"
|
||||||
|
required
|
||||||
|
error={codeErrors.name?.message}
|
||||||
|
{...registerCode("name")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormTextArea
|
||||||
|
label="Description"
|
||||||
|
rows={2}
|
||||||
|
error={codeErrors.description?.message}
|
||||||
|
{...registerCode("description")}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{/* Variables tag input */}
|
||||||
<FormField label="Code (e.g. task_assigned)" required error={codeErrors.code?.message} disabled={!!editingCode} {...registerCode('code')} />
|
<VariableTagInput
|
||||||
<FormField label="Display Name" required error={codeErrors.name?.message} {...registerCode('name')} />
|
variables={codeVariables}
|
||||||
</div>
|
onChange={setCodeVariables}
|
||||||
<FormTextArea label="Description" rows={2} error={codeErrors.description?.message} {...registerCode('description')} />
|
/>
|
||||||
|
|
||||||
{/* Variables tag input */}
|
<div className="text-[10px] text-gray-400 font-medium">
|
||||||
<VariableTagInput variables={codeVariables} onChange={setCodeVariables} />
|
Auto-populates default channels (In-App, Email)
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-2">
|
|
||||||
<div className="text-[10px] text-gray-400 font-medium">Auto-populates default channels (In-App, Email)</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
{editingCode && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setEditingCode(null);
|
|
||||||
setCodeVariables([]);
|
|
||||||
resetCode({ code: '', name: '', description: '', default_channels: ['in_app', 'email'], default_priority: 'normal' });
|
|
||||||
}}
|
|
||||||
className="text-xs text-gray-500 hover:text-gray-800 underline transition-colors"
|
|
||||||
>
|
|
||||||
Cancel Edit
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<PrimaryButton onClick={handleCodeSubmit(onSaveCode)} size="default">
|
|
||||||
{editingCode ? 'Update Code' : 'Add Code'}
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Registered Codes Table ───────────────────────────────── */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h4 className="text-[11px] font-bold text-gray-400 uppercase tracking-widest px-1">Registered Codes</h4>
|
|
||||||
<div className="border border-gray-100 rounded-xl overflow-hidden shadow-sm">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-[#f8fafc] border-b border-gray-100">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Trigger Code</th>
|
|
||||||
<th className="px-4 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Name</th>
|
|
||||||
<th className="px-4 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Variables</th>
|
|
||||||
<th className="px-4 py-3 text-right text-[11px] font-bold text-gray-500 uppercase tracking-wider">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-50 text-[13px]">
|
|
||||||
{isCodesLoading ? (
|
|
||||||
<tr><td colSpan={4} className="px-4 py-10 text-center text-gray-400 animate-pulse">Loading codes...</td></tr>
|
|
||||||
) : codes.length === 0 ? (
|
|
||||||
<tr><td colSpan={4} className="px-4 py-10 text-center text-gray-400 italic">No codes registered for this category.</td></tr>
|
|
||||||
) : codes.map(c => (
|
|
||||||
<tr key={c.id} className="hover:bg-gray-50/50 transition-colors">
|
|
||||||
<td className="px-4 py-3"><code className="text-[11px] font-mono font-bold text-indigo-600 bg-indigo-50 px-1.5 py-0.5 rounded border border-indigo-100">{c.code}</code></td>
|
|
||||||
<td className="px-4 py-3 font-medium text-gray-700">{c.name}</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{Array.isArray(c.variables) && c.variables.length > 0
|
|
||||||
? c.variables.map((v: string) => (
|
|
||||||
<span key={v} className="text-[10px] font-mono bg-indigo-50 text-indigo-600 border border-indigo-100 px-1.5 py-0.5 rounded-full">{`{{${v}}}`}</span>
|
|
||||||
))
|
|
||||||
: <span className="text-[10px] text-gray-300 italic">none</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-right">
|
|
||||||
<button onClick={() => {
|
|
||||||
setEditingCode(c);
|
|
||||||
setCodeVariables(Array.isArray(c.variables) ? c.variables : []);
|
|
||||||
resetCode({ code: c.code, name: c.name, description: c.description || '', default_channels: c.default_channels, default_priority: c.default_priority });
|
|
||||||
}} className="text-blue-600 hover:text-blue-800 font-bold text-[11px] uppercase mr-3">Edit</button>
|
|
||||||
<button onClick={() => {
|
|
||||||
setDeleteTarget({ id: c.id, name: c.code, type: 'code' });
|
|
||||||
setDeleteModalOpen(true);
|
|
||||||
}} className="text-red-500 hover:text-red-700 font-bold text-[11px] uppercase">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
currentPage={codePage}
|
|
||||||
totalPages={codePages}
|
|
||||||
totalItems={codeTotal}
|
|
||||||
limit={5}
|
|
||||||
onPageChange={setCodePage}
|
|
||||||
onLimitChange={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@ -516,7 +783,7 @@ const NotificationMaster = (): ReactElement => {
|
|||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
title={`Delete ${deleteTarget?.type}`}
|
title={`Delete ${deleteTarget?.type}`}
|
||||||
message={`Are you sure you want to delete ${deleteTarget?.name}? This action cannot be undone.`}
|
message={`Are you sure you want to delete ${deleteTarget?.name}? This action cannot be undone.`}
|
||||||
itemName={deleteTarget?.name || ''}
|
itemName={deleteTarget?.name || ""}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user