Iman_AMS/asm_app/src/pages/PPMPlannerList.tsx-OLD

422 lines
19 KiB
Plaintext

import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { FaPlus, FaSearch, FaEdit, FaEye, FaTrash, FaEllipsisV, FaCalendarAlt, FaFilter, FaChevronDown, FaChevronUp, FaTimes } from 'react-icons/fa';
import { usePMSchedules, usePMScheduleMutations } from '../hooks/usePMSchedule';
import LinkField from '../components/LinkField';
const PPMPlannerList: React.FC = () => {
const navigate = useNavigate();
const [page, setPage] = useState(0);
const [searchTerm, setSearchTerm] = useState('');
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState<string | null>(null);
const [actionMenuOpen, setActionMenuOpen] = useState<string | null>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
// Filters
const [isFilterExpanded, setIsFilterExpanded] = useState(false);
const [filterHospital, setFilterHospital] = useState('');
const [filterModality, setFilterModality] = useState('');
const [filterPeriodicity, setFilterPeriodicity] = useState('');
const limit = 20;
// Build filters
const filters: Record<string, any> = {};
if (filterHospital) filters['hospital'] = filterHospital;
if (filterModality) filters['modality'] = filterModality;
if (filterPeriodicity) filters['periodicity'] = filterPeriodicity;
const { pmSchedules, totalCount, hasMore, loading, error, refetch } = usePMSchedules(
filters,
limit,
page * limit,
'creation desc'
);
const { deletePMSchedule, loading: mutationLoading } = usePMScheduleMutations();
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-planner/new');
};
const handleView = (scheduleName: string) => {
navigate(`/ppm-planner/${scheduleName}`);
};
const handleEdit = (scheduleName: string) => {
navigate(`/ppm-planner/${scheduleName}`);
};
const handleDelete = async (scheduleName: string) => {
try {
await deletePMSchedule(scheduleName);
refetch();
setDeleteConfirmOpen(null);
} catch (err) {
console.error('Error deleting PM Schedule:', err);
alert('Failed to delete PM Schedule');
}
};
const handleClearFilters = () => {
setFilterHospital('');
setFilterModality('');
setFilterPeriodicity('');
setPage(0);
};
const hasActiveFilters = filterHospital || filterModality || filterPeriodicity;
const activeFilterCount = [filterHospital, filterModality, filterPeriodicity].filter(Boolean).length;
// Filter schedules by search term
const filteredSchedules = pmSchedules.filter(schedule => {
if (!searchTerm) return true;
const term = searchTerm.toLowerCase();
return (
schedule.name?.toLowerCase().includes(term) ||
schedule.hospital?.toLowerCase().includes(term) ||
schedule.modality?.toLowerCase().includes(term) ||
schedule.maintenance_team?.toLowerCase().includes(term)
);
});
const totalPages = Math.ceil(totalCount / limit);
return (
<div className="flex flex-col h-screen bg-gray-50 dark:bg-gray-900">
{/* Header */}
<div className="flex-shrink-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3 lg:px-6">
<div className="flex justify-between items-center mb-3">
<div className="flex items-center gap-3">
<FaCalendarAlt className="text-blue-600 dark:text-blue-400" size={24} />
<div>
<h1 className="text-xl font-bold text-gray-800 dark:text-white">PPM Planners</h1>
<p className="text-xs text-gray-600 dark:text-gray-400">
Manage preventive maintenance schedules
</p>
</div>
</div>
<button
onClick={handleCreateNew}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors flex items-center gap-2 text-sm"
>
<FaPlus />
<span>Create PPM Planner</span>
</button>
</div>
{/* Search and Filters */}
<div className="flex gap-2">
<div className="flex-1 relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={14} />
<input
type="text"
placeholder="Search by name, hospital, modality..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<button
onClick={() => setIsFilterExpanded(!isFilterExpanded)}
className={`px-3 py-1.5 border rounded-lg transition-colors flex items-center gap-2 text-sm ${
hasActiveFilters
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
>
<FaFilter />
<span>Filters</span>
{activeFilterCount > 0 && (
<span className="bg-blue-600 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
{activeFilterCount}
</span>
)}
{isFilterExpanded ? <FaChevronUp /> : <FaChevronDown />}
</button>
</div>
{/* Filter Panel */}
{isFilterExpanded && (
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
{/* <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Hospital
</label> */}
<LinkField
label = "Hospital"
doctype="Company"
value={filterHospital}
onChange={setFilterHospital}
placeholder="All Hospitals"
filters={{ domain: "Healthcare" }}
/>
</div>
<div>
{/* <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Modality
</label> */}
<LinkField
label="Modality"
doctype="Modality"
value={filterModality}
onChange={setFilterModality}
placeholder="All Modalities"
filters={{}}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Periodicity
</label>
<select
value={filterPeriodicity}
onChange={(e) => setFilterPeriodicity(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option value="">All</option>
<option value="Daily">Daily</option>
<option value="Weekly">Weekly</option>
<option value="Monthly">Monthly</option>
<option value="Quarterly">Quarterly</option>
<option value="Half-yearly">Half-yearly</option>
<option value="Yearly">Yearly</option>
</select>
</div>
</div>
{hasActiveFilters && (
<div className="mt-4 flex justify-end">
<button
onClick={handleClearFilters}
className="px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 flex items-center gap-2"
>
<FaTimes />
Clear Filters
</button>
</div>
)}
</div>
)}
</div>
{/* Content */}
<div className="flex-1 overflow-auto p-4 lg:p-5">
{loading && page === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<span className="ml-3 text-gray-600 dark:text-gray-400">Loading PPM Planners...</span>
</div>
) : error ? (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p className="text-red-600 dark:text-red-400">{error}</p>
</div>
) : filteredSchedules.length === 0 ? (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-8 text-center">
<FaCalendarAlt className="mx-auto text-gray-400 mb-4" size={48} />
<h3 className="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">
No PPM Planners Found
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{searchTerm || hasActiveFilters
? 'Try adjusting your search or filters'
: 'Get started by creating your first PPM Planner'}
</p>
{!searchTerm && !hasActiveFilters && (
<button
onClick={handleCreateNew}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
Create PPM Planner
</button>
)}
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Hospital
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Modality
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Periodicity
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Due Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredSchedules.map((schedule) => (
<tr key={schedule.name} className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<button
onClick={() => handleView(schedule.name)}
className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
{schedule.name}
</button>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{schedule.hospital || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{schedule.modality || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{schedule.periodicity || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
{schedule.due_date ? new Date(schedule.due_date).toLocaleDateString() : '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs font-semibold rounded-full ${
schedule.docstatus === 1
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
: schedule.docstatus === 0
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400'
}`}>
{schedule.docstatus === 1 ? 'Submitted' : schedule.docstatus === 0 ? 'Draft' : 'Cancelled'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="relative inline-block" ref={actionMenuOpen === schedule.name ? dropdownRef : null}>
<button
onClick={() => setActionMenuOpen(actionMenuOpen === schedule.name ? null : schedule.name)}
className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 p-2"
>
<FaEllipsisV />
</button>
{actionMenuOpen === schedule.name && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 z-10">
<button
onClick={() => {
handleView(schedule.name);
setActionMenuOpen(null);
}}
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 flex items-center gap-2"
>
<FaEye /> View
</button>
<button
onClick={() => {
handleEdit(schedule.name);
setActionMenuOpen(null);
}}
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 flex items-center gap-2"
>
<FaEdit /> Edit
</button>
<button
onClick={() => {
setDeleteConfirmOpen(schedule.name);
setActionMenuOpen(null);
}}
className="w-full px-4 py-2 text-left text-sm text-red-600 dark:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-600 flex items-center gap-2"
>
<FaTrash /> Delete
</button>
</div>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
{totalPages > 1 && (
<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-600 dark:text-gray-400">
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 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Previous
</button>
<button
onClick={() => setPage(page + 1)}
disabled={!hasMore}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Next
</button>
</div>
</div>
)}
</div>
)}
</div>
{/* Delete Confirmation Modal */}
{deleteConfirmOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-md">
<h3 className="text-lg font-semibold text-gray-800 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 Planner? This action cannot be undone.
</p>
<div className="flex gap-3 justify-end">
<button
onClick={() => setDeleteConfirmOpen(null)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
disabled={mutationLoading}
>
Cancel
</button>
<button
onClick={() => handleDelete(deleteConfirmOpen)}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
disabled={mutationLoading}
>
{mutationLoading ? 'Deleting...' : 'Delete'}
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default PPMPlannerList;