Iman_AMS/asm_app/src/pages/PPMList.tsx

444 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePPMs, usePPMMutations } from '../hooks/usePPM';
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaCopy, FaEllipsisV, FaFileExport, FaCalendarCheck, FaBuilding } from 'react-icons/fa';
const PPMList: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [page, setPage] = useState(0);
const [searchTerm, setSearchTerm] = useState('');
const [companyFilter, setCompanyFilter] = useState<string>('');
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>(null);
const [actionMenuOpen, setActionMenuOpen] = useState<string | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const limit = 20;
const filters = companyFilter ? { company: companyFilter } : {};
const { ppms, totalCount, hasMore, loading, error, refetch } = usePPMs(
filters,
limit,
page * limit,
'creation desc'
);
const { deletePPM, loading: mutationLoading } = usePPMMutations();
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setActionMenuOpen(null);
}
};
if (actionMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [actionMenuOpen]);
const handleCreateNew = () => {
navigate('/ppm/new');
};
const handleView = (ppmName: string) => {
navigate(`/ppm/${ppmName}`);
};
const handleEdit = (ppmName: string) => {
navigate(`/ppm/${ppmName}`);
};
const handleDelete = async (ppmName: string) => {
try {
await deletePPM(ppmName);
setDeleteConfirmOpen(null);
refetch();
alert('PPM schedule deleted successfully!');
} catch (err) {
alert(`Failed to delete: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
};
const handleDuplicate = (ppmName: string) => {
navigate(`/ppm/new?duplicate=${ppmName}`);
};
const handleExport = (ppm: any) => {
const dataStr = JSON.stringify(ppm, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `ppm_${ppm.name}.json`;
link.click();
URL.revokeObjectURL(url);
};
const handleExportAll = () => {
const headers = ['PPM ID', 'Company', 'Asset', 'Asset Type', 'Frequency', 'No. of PMs', 'Total Amount'];
const csvContent = [
headers.join(','),
...ppms.map(ppm => [
ppm.name,
ppm.company || '',
ppm.asset_name || '',
ppm.custom_asset_type || '',
ppm.custom_frequency || '',
ppm.custom_no_of_pms || '',
ppm.custom_total_amount || ''
].join(','))
].join('\n');
const dataBlob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `ppm_schedules_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
URL.revokeObjectURL(url);
};
if (loading && page === 0) {
return (
<div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">{t('listPages.loading')}</p>
</div>
</div>
);
}
if (error) {
return (
<div className="p-4 sm:p-6 bg-gray-50 dark:bg-gray-900 min-h-screen min-w-0 overflow-x-hidden">
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
<h2 className="text-xl font-bold text-yellow-800 dark:text-yellow-300 mb-4"> PPM API Not Available</h2>
<div className="text-yellow-700 dark:text-yellow-400 space-y-3">
<p><strong>The PPM API endpoint is not deployed yet.</strong></p>
<div className="mt-4 flex gap-3">
<button
onClick={() => navigate('/ppm/new')}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"
>
Try Creating New (Demo)
</button>
<button
onClick={refetch}
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded"
>
Try Again
</button>
</div>
</div>
<div className="mt-4 p-4 bg-white dark:bg-gray-800 rounded border border-yellow-300 dark:border-yellow-700">
<p className="text-sm text-gray-600 dark:text-gray-400">
<strong>Technical Error:</strong> {error}
</p>
</div>
</div>
</div>
);
}
const filteredPPMs = ppms.filter(ppm =>
ppm.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
ppm.asset_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
ppm.company?.toLowerCase().includes(searchTerm.toLowerCase()) ||
ppm.custom_asset_type?.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="p-4 sm:p-6 bg-gray-50 dark:bg-gray-900 min-h-screen min-w-0 overflow-x-hidden">
{/* Header */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between lg:items-center">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-800 dark:text-white break-words">{t('ppm.title')}</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Total: {totalCount} PPM schedule{totalCount !== 1 ? 's' : ''}
</p>
</div>
<div className="flex gap-3">
<button
onClick={handleExportAll}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-3 rounded-lg flex items-center gap-2 shadow transition-all"
disabled={ppms.length === 0}
>
<FaFileExport />
<span className="font-medium">{t('listPages.export')}</span>
</button>
<button
onClick={handleCreateNew}
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg flex items-center gap-2 shadow-lg transition-all hover:shadow-xl"
>
<FaPlus />
<span className="font-medium">{t('ppm.addPPM')}</span>
</button>
</div>
</div>
{/* Filters Bar */}
<div className="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="flex items-center gap-2 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 bg-white dark:bg-gray-700">
<FaSearch className="text-gray-400 dark:text-gray-500" />
<input
type="text"
placeholder="Search by ID, asset, company..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 outline-none text-gray-700 dark:text-gray-200 bg-transparent"
/>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<input
type="text"
placeholder="Filter by Company"
value={companyFilter}
onChange={(e) => {
setCompanyFilter(e.target.value);
setPage(0);
}}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{/* PPM Schedules Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-100 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
PPM ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Company
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Asset
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Asset Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Frequency
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
No. of PMs
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Total Amount
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredPPMs.length === 0 ? (
<tr>
<td colSpan={8} className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
<div className="flex flex-col items-center">
<FaSearch className="text-4xl text-gray-300 dark:text-gray-600 mb-2" />
<p>No PPM schedules found</p>
<button
onClick={handleCreateNew}
className="mt-4 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
>
Create your first PPM schedule
</button>
</div>
</td>
</tr>
) : (
filteredPPMs.map((ppm) => (
<tr
key={ppm.name}
className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer"
onClick={() => handleView(ppm.name)}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{ppm.name}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
<FaBuilding className="text-gray-400" />
<span className="text-sm text-gray-700 dark:text-gray-300">
{ppm.company || '-'}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{ppm.asset_name || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{ppm.custom_asset_type || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
<FaCalendarCheck className="text-blue-500" />
<span className="text-sm text-gray-700 dark:text-gray-300">
{ppm.custom_frequency || '-'}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-700 dark:text-gray-300">
{ppm.custom_no_of_pms || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{ppm.custom_total_amount ? `$${ppm.custom_total_amount.toLocaleString()}` : '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="relative" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => setActionMenuOpen(actionMenuOpen === ppm.name ? null : ppm.name)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
>
<FaEllipsisV />
</button>
{actionMenuOpen === ppm.name && (
<div
ref={dropdownRef}
className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg z-10 border border-gray-200 dark:border-gray-700"
>
<button
onClick={() => {
handleView(ppm.name);
setActionMenuOpen(null);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
>
<FaEye />
View
</button>
<button
onClick={() => {
handleEdit(ppm.name);
setActionMenuOpen(null);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
>
<FaEdit />
Edit
</button>
<button
onClick={() => {
handleDuplicate(ppm.name);
setActionMenuOpen(null);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
>
<FaCopy />
Duplicate
</button>
<button
onClick={() => {
handleExport(ppm);
setActionMenuOpen(null);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
>
<FaFileExport />
Export
</button>
<div className="border-t border-gray-200 dark:border-gray-700"></div>
<button
onClick={() => {
setDeleteConfirmOpen(ppm.name);
setActionMenuOpen(null);
}}
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2"
>
<FaTrash />
Delete
</button>
</div>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{(hasMore || page > 0) && (
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div className="text-sm text-gray-700 dark:text-gray-300">
Showing {page * limit + 1} to {Math.min((page + 1) * limit, totalCount)} of {totalCount} results
</div>
<div className="flex gap-2">
<button
onClick={() => setPage(Math.max(0, page - 1))}
disabled={page === 0}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => setPage(page + 1)}
disabled={!hasMore}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</div>
{/* Delete Confirmation Modal */}
{deleteConfirmOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-4">Confirm Delete</h3>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Are you sure you want to delete this PPM schedule? This action cannot be undone.
</p>
<div className="flex gap-3 justify-end">
<button
onClick={() => setDeleteConfirmOpen(null)}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600"
>
Cancel
</button>
<button
onClick={() => handleDelete(deleteConfirmOpen)}
disabled={mutationLoading}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
>
{mutationLoading ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default PPMList;