258 lines
11 KiB
TypeScript
258 lines
11 KiB
TypeScript
import React, { useState, useEffect, useMemo } from 'react';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { FaFilter, FaChevronDown, FaChevronUp, FaTimes, FaCalendarAlt, FaMap } from 'react-icons/fa';
|
|
import MaintenanceCalendar from '../components/MaintenanceCalendar';
|
|
import LinkField from '../components/LinkField';
|
|
import useUserHospitalFilter from '../hooks/useUserHospitalFilter';
|
|
|
|
const MaintenanceCalendarPage: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
const [searchParams] = useSearchParams();
|
|
const { filterHospital, filtersLocked, handleHospitalChange } = useUserHospitalFilter();
|
|
|
|
// Filter states
|
|
const [filterDepartment, setFilterDepartment] = useState('');
|
|
const [filterStatus, setFilterStatus] = useState('');
|
|
const [filterAssignTo, setFilterAssignTo] = useState('');
|
|
|
|
// Load filters from URL on mount (only when hospital is not locked to user profile)
|
|
useEffect(() => {
|
|
if (filtersLocked) return;
|
|
|
|
const hospital = searchParams.get('hospital');
|
|
const status = searchParams.get('status');
|
|
|
|
if (hospital) handleHospitalChange(hospital);
|
|
if (status) setFilterStatus(status);
|
|
}, [searchParams, filtersLocked, handleHospitalChange]);
|
|
|
|
// View type states
|
|
const [viewType, setViewType] = useState<'maintenance-log' | 'ppm-planner'>('maintenance-log');
|
|
|
|
// UI states
|
|
const [isFilterExpanded, setIsFilterExpanded] = useState(false);
|
|
const [activeFilterCount, setActiveFilterCount] = useState(0);
|
|
|
|
// Update active filter count
|
|
useEffect(() => {
|
|
const count = [filterHospital, filterDepartment, filterStatus, filterAssignTo].filter(Boolean).length;
|
|
setActiveFilterCount(count);
|
|
}, [filterHospital, filterDepartment, filterStatus, filterAssignTo]);
|
|
|
|
const handleClearFilters = () => {
|
|
if (!filtersLocked) {
|
|
handleHospitalChange('');
|
|
}
|
|
setFilterDepartment('');
|
|
setFilterStatus('');
|
|
setFilterAssignTo('');
|
|
};
|
|
|
|
const hasActiveFilters = filterHospital || filterDepartment || filterStatus || filterAssignTo;
|
|
|
|
// Build filters for calendar - memoized to prevent object reference changes
|
|
const calendarFilters: Record<string, any> = useMemo(() => {
|
|
const filters: Record<string, any> = {};
|
|
if (filterHospital) filters['company'] = filterHospital;
|
|
if (filterDepartment) filters['department'] = filterDepartment;
|
|
if (filterStatus) filters['maintenance_status'] = filterStatus;
|
|
if (filterAssignTo) filters['assign_to_name'] = filterAssignTo;
|
|
return filters;
|
|
}, [filterHospital, filterDepartment, filterStatus, filterAssignTo]);
|
|
|
|
return (
|
|
<div className="flex flex-col h-screen bg-gray-50 dark:bg-gray-900 overflow-hidden">
|
|
{/* Header - Single Row on Desktop/Laptop */}
|
|
<div className="flex-shrink-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-2.5 lg:px-6">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 md:gap-4">
|
|
{/* Left Side - Title */}
|
|
<div className="flex items-center gap-2.5 min-w-0">
|
|
<FaCalendarAlt className="text-blue-600 dark:text-blue-400 flex-shrink-0" size={22} />
|
|
<div className="min-w-0">
|
|
<h1 className="text-lg md:text-xl font-bold text-gray-800 dark:text-white whitespace-nowrap">
|
|
Maintenance Calendar
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side - All Controls in Single Row */}
|
|
<div className="flex items-end gap-2 md:gap-3 flex-wrap md:flex-nowrap">
|
|
{/* View Type Dropdown */}
|
|
<div className="relative">
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
View Type
|
|
</label>
|
|
<select
|
|
value={viewType}
|
|
onChange={(e) => setViewType(e.target.value as 'maintenance-log' | 'ppm-planner')}
|
|
className="px-2.5 md:px-3 py-1.5 text-xs md: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"
|
|
>
|
|
<option value="maintenance-log">Maintenance Log</option>
|
|
<option value="ppm-planner">PPM Planner</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Filters Button */}
|
|
<div className="relative">
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 invisible">
|
|
Filters
|
|
</label>
|
|
<button
|
|
onClick={() => setIsFilterExpanded(!isFilterExpanded)}
|
|
className={`px-3 md:px-4 py-1.5 md:py-2 border rounded-lg transition-colors flex items-center gap-1.5 md:gap-2 text-xs md: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 size={14} />
|
|
<span className="hidden sm:inline">Filters</span>
|
|
{activeFilterCount > 0 && (
|
|
<span className="bg-blue-600 text-white rounded-full w-4 h-4 md:w-5 md:h-5 flex items-center justify-center text-[10px] md:text-xs">
|
|
{activeFilterCount}
|
|
</span>
|
|
)}
|
|
{isFilterExpanded ? <FaChevronUp size={12} /> : <FaChevronDown size={12} />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Yearly PPM Planner Button */}
|
|
{viewType === 'ppm-planner' && (
|
|
<div className="relative">
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 invisible">
|
|
Yearly Map
|
|
</label>
|
|
<button
|
|
onClick={() => navigate('/yearly-ppm-planner')}
|
|
className="px-3 md:px-4 py-1.5 md:py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors flex items-center gap-1.5 md:gap-2 text-xs md:text-sm font-medium whitespace-nowrap"
|
|
title="View Yearly PPM Planner Map"
|
|
>
|
|
<FaMap size={14} />
|
|
<span className="hidden sm:inline">Yearly Map</span>
|
|
<span className="sm:hidden">Map</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter Panel */}
|
|
{isFilterExpanded && (
|
|
<div className="mt-2.5 md:mt-3 p-3 md:p-4 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-2 lg:grid-cols-4 gap-3">
|
|
{/* Hospital */}
|
|
<div className="relative z-[60]">
|
|
<LinkField
|
|
label="Hospital"
|
|
doctype="Company"
|
|
value={filterHospital}
|
|
onChange={handleHospitalChange}
|
|
placeholder="Select Hospital"
|
|
compact={true}
|
|
disabled={filtersLocked}
|
|
/>
|
|
{filterHospital && !filtersLocked && (
|
|
<button
|
|
onClick={() => handleHospitalChange('')}
|
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
|
>
|
|
<FaTimes size={10} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Department */}
|
|
<div className="relative z-[55]">
|
|
<LinkField
|
|
label="Department"
|
|
doctype="Department"
|
|
value={filterDepartment}
|
|
onChange={(val) => setFilterDepartment(val)}
|
|
placeholder="All Departments"
|
|
compact={true}
|
|
/>
|
|
{filterDepartment && (
|
|
<button
|
|
onClick={() => setFilterDepartment('')}
|
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
|
>
|
|
<FaTimes size={10} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Status */}
|
|
<div className="relative">
|
|
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Status
|
|
</label>
|
|
<select
|
|
value={filterStatus}
|
|
onChange={(e) => setFilterStatus(e.target.value)}
|
|
className="w-full px-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"
|
|
>
|
|
<option value="">All Statuses</option>
|
|
<option value="Planned">Planned</option>
|
|
<option value="Completed">Completed</option>
|
|
<option value="Overdue">Overdue</option>
|
|
<option value="Cancelled">Cancelled</option>
|
|
</select>
|
|
{filterStatus && (
|
|
<button
|
|
onClick={() => setFilterStatus('')}
|
|
className="absolute right-8 top-7 text-gray-400 hover:text-red-500 transition-colors"
|
|
>
|
|
<FaTimes size={10} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Assigned To */}
|
|
<div className="relative z-[50]">
|
|
<LinkField
|
|
label="Assigned To"
|
|
doctype="User"
|
|
value={filterAssignTo}
|
|
onChange={(val) => setFilterAssignTo(val)}
|
|
placeholder="All Technicians"
|
|
compact={true}
|
|
/>
|
|
{filterAssignTo && (
|
|
<button
|
|
onClick={() => setFilterAssignTo('')}
|
|
className="absolute right-2 top-6 text-gray-400 hover:text-red-500 transition-colors z-10"
|
|
>
|
|
<FaTimes size={10} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{hasActiveFilters && (
|
|
<div className="mt-3 flex justify-end">
|
|
<button
|
|
onClick={handleClearFilters}
|
|
className="px-3 py-1.5 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>
|
|
|
|
{/* Calendar */}
|
|
<div className="flex-1 overflow-hidden px-3 pb-3 lg:px-4 lg:pb-4">
|
|
<MaintenanceCalendar
|
|
filters={calendarFilters}
|
|
viewType={viewType}
|
|
timeView="day-month"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MaintenanceCalendarPage;
|