Files
saas_frontend/src/application/modules/admin/components/ModuleList.tsx
T
2026-04-09 11:26:50 +05:30

217 lines
9.9 KiB
TypeScript

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';
import { adminModuleApi } from '../AdminModuleApi';
import ModuleForm from './ModuleForm';
import CustomConfirmationModal from '../../../../components/custom/CustomConfirmationModal';
import CustomButton from '../../../../components/custom/CustomButton';
const ModuleList = () => {
const navigate = useNavigate();
const { t } = useTranslation(['modules', 'common']);
const { listModules, deleteModule, updateModule, loading } = useModuleApi();
const [modules, setModules] = useState<Module[]>([]);
const [showForm, setShowForm] = useState(false);
const [selectedModule, setSelectedModule] = useState<Module | null>(null);
const [moduleToDelete, setModuleToDelete] = useState<Module | null>(null);
const [togglingId, setTogglingId] = useState<string | null>(null);
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>
<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);
}
}}
>
{module.status === 'active' ? (
<Eye className="w-5 h-5 text-green-600" />
) : (
<EyeOff className="w-5 h-5 text-gray-600" />
)}
</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} />
{t('registry.card.environments')}
</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} />
{t('registry.card.permissions')}
</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} />
{t('common:actions.edit')}
</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} />
{t('common:actions.delete')}
</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;