2026-02-02 17:40:13 +05:30
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
import { Plus, Edit, Trash2, Settings, Shield, Eye, EyeOff } from 'lucide-react';
|
|
|
|
|
import { useModuleApi } from '../hooks/useModuleApi';
|
|
|
|
|
import type { Module } from '../AdminModuleTypes';
|
2026-04-09 11:26:50 +05:30
|
|
|
import { adminModuleApi } from '../AdminModuleApi';
|
2026-02-02 17:40:13 +05:30
|
|
|
import ModuleForm from './ModuleForm';
|
|
|
|
|
import CustomConfirmationModal from '../../../../components/custom/CustomConfirmationModal';
|
|
|
|
|
import CustomButton from '../../../../components/custom/CustomButton';
|
|
|
|
|
|
|
|
|
|
const ModuleList = () => {
|
|
|
|
|
const navigate = useNavigate();
|
2026-02-02 23:06:31 +05:30
|
|
|
const { t } = useTranslation(['modules', 'common']);
|
2026-04-09 11:26:50 +05:30
|
|
|
const { listModules, deleteModule, updateModule, loading } = useModuleApi();
|
2026-02-02 17:40:13 +05:30
|
|
|
const [modules, setModules] = useState<Module[]>([]);
|
|
|
|
|
const [showForm, setShowForm] = useState(false);
|
|
|
|
|
const [selectedModule, setSelectedModule] = useState<Module | null>(null);
|
|
|
|
|
const [moduleToDelete, setModuleToDelete] = useState<Module | null>(null);
|
2026-04-09 11:26:50 +05:30
|
|
|
const [togglingId, setTogglingId] = useState<string | null>(null);
|
2026-02-02 17:40:13 +05:30
|
|
|
|
|
|
|
|
const fetchModules = async () => {
|
|
|
|
|
const data = await listModules();
|
|
|
|
|
if (data) {
|
|
|
|
|
setModules(data);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchModules();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleCreate = () => {
|
|
|
|
|
setSelectedModule(null);
|
|
|
|
|
setShowForm(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEdit = (module: Module) => {
|
|
|
|
|
setSelectedModule(module);
|
|
|
|
|
setShowForm(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const confirmDelete = (module: Module) => {
|
|
|
|
|
setModuleToDelete(module);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = async () => {
|
|
|
|
|
if (!moduleToDelete) return;
|
|
|
|
|
const success = await deleteModule(moduleToDelete.id);
|
|
|
|
|
if (success) {
|
|
|
|
|
fetchModules();
|
|
|
|
|
}
|
|
|
|
|
setModuleToDelete(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFormClose = (saved: boolean) => {
|
|
|
|
|
setShowForm(false);
|
|
|
|
|
setSelectedModule(null);
|
|
|
|
|
if (saved) {
|
|
|
|
|
fetchModules();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (loading && modules.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
<div className="animate-pulse space-y-4">
|
|
|
|
|
<div className="h-8 bg-gray-300 rounded w-1/4"></div>
|
|
|
|
|
<div className="h-64 bg-gray-300 rounded"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6 space-y-6">
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-bold text-(--text-primary)">{t('registry.title')}</h1>
|
|
|
|
|
<p className="text-(--text-secondary) mt-1">{t('registry.subtitle')}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<CustomButton
|
|
|
|
|
onClick={handleCreate}
|
|
|
|
|
leftIcon={<Plus size={20} />}
|
|
|
|
|
>
|
|
|
|
|
{t('registry.create_button')}
|
|
|
|
|
</CustomButton>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
|
|
{modules.map((module) => (
|
|
|
|
|
<div
|
|
|
|
|
key={module.id}
|
|
|
|
|
className="bg-(--card-bg) border border-(--card-border) rounded-lg p-6 space-y-4 shadow-sm"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-start justify-between">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
{module.icon_url ? (
|
|
|
|
|
<img src={module.icon_url} alt={module.module_name} className="w-12 h-12 rounded-lg" />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
|
|
|
<Settings className="w-6 h-6 text-blue-600" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-(--text-primary)">{module.module_name}</h3>
|
|
|
|
|
<p className="text-sm text-(--text-secondary)">{module.module_id}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-09 11:26:50 +05:30
|
|
|
<div
|
|
|
|
|
className={`flex items-center gap-1 transition-opacity ${
|
|
|
|
|
togglingId === module.id
|
|
|
|
|
? 'opacity-50 cursor-not-allowed'
|
|
|
|
|
: 'cursor-pointer hover:text-blue-600'
|
|
|
|
|
}`}
|
|
|
|
|
title={module.status === 'active' ? 'Deactivate' : 'Activate'}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
if (togglingId) return;
|
|
|
|
|
try {
|
|
|
|
|
setTogglingId(module.id);
|
|
|
|
|
const updated = await adminModuleApi.updateModule(module.id, {
|
|
|
|
|
status: module.status === 'active' ? 'inactive' : 'active'
|
|
|
|
|
});
|
|
|
|
|
if (updated) {
|
|
|
|
|
setModules(prev => prev.map(m =>
|
|
|
|
|
m.id === module.id ? { ...m, status: updated.status } : m
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Toggle failed:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setTogglingId(null);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-02-02 17:40:13 +05:30
|
|
|
{module.status === 'active' ? (
|
|
|
|
|
<Eye className="w-5 h-5 text-green-600" />
|
|
|
|
|
) : (
|
2026-04-09 11:26:50 +05:30
|
|
|
<EyeOff className="w-5 h-5 text-gray-600" />
|
2026-02-02 17:40:13 +05:30
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{module.description && (
|
|
|
|
|
<p className="text-sm text-(--text-secondary) line-clamp-2">{module.description}</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2 pt-4 border-t border-(--card-border)">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => navigate(`/admin/modules/${module.id}/environments`)}
|
|
|
|
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm text-(--text-primary)"
|
|
|
|
|
>
|
|
|
|
|
<Settings size={16} />
|
2026-02-02 23:06:31 +05:30
|
|
|
{t('registry.card.environments')}
|
2026-02-02 17:40:13 +05:30
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => navigate(`/admin/modules/${module.id}/permissions`)}
|
|
|
|
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm text-(--text-primary)"
|
|
|
|
|
>
|
|
|
|
|
<Shield size={16} />
|
2026-02-02 23:06:31 +05:30
|
|
|
{t('registry.card.permissions')}
|
2026-02-02 17:40:13 +05:30
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleEdit(module)}
|
|
|
|
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-blue-50 hover:bg-blue-100 text-blue-600 rounded-lg transition-colors text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Edit size={16} />
|
2026-02-02 23:06:31 +05:30
|
|
|
{t('common:actions.edit')}
|
2026-02-02 17:40:13 +05:30
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => confirmDelete(module)}
|
|
|
|
|
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-red-50 hover:bg-red-100 text-red-600 rounded-lg transition-colors text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 size={16} />
|
2026-02-02 23:06:31 +05:30
|
|
|
{t('common:actions.delete')}
|
2026-02-02 17:40:13 +05:30
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{modules.length === 0 && !loading && (
|
|
|
|
|
<div className="bg-(--card-bg) border border-(--card-border) rounded-lg p-12 text-center">
|
|
|
|
|
<Settings className="w-16 h-16 text-(--text-secondary) mx-auto mb-4" />
|
|
|
|
|
<h3 className="text-lg font-semibold text-(--text-primary) mb-2">{t('registry.empty_state.title')}</h3>
|
|
|
|
|
<p className="text-(--text-secondary) mb-4">{t('registry.empty_state.subtitle')}</p>
|
|
|
|
|
<CustomButton onClick={handleCreate}>
|
|
|
|
|
{t('registry.create_button')}
|
|
|
|
|
</CustomButton>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showForm && (
|
|
|
|
|
<ModuleForm
|
|
|
|
|
module={selectedModule}
|
|
|
|
|
onClose={handleFormClose}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<CustomConfirmationModal
|
|
|
|
|
isOpen={!!moduleToDelete}
|
|
|
|
|
onClose={() => setModuleToDelete(null)}
|
|
|
|
|
onConfirm={handleDelete}
|
|
|
|
|
title={t('delete_modal.title')}
|
|
|
|
|
description={t('delete_modal.description', { name: moduleToDelete?.module_name })}
|
|
|
|
|
confirmText={t('delete_modal.confirm')}
|
|
|
|
|
cancelText={t('delete_modal.cancel')}
|
|
|
|
|
variant="danger"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ModuleList;
|