import React, { memo, useState, useEffect } from 'react'; import { Space, ConfigProvider, Button, Row, Col, Card, Input, Typography, Divider, Checkbox, Select, } from 'antd'; import { PlusOutlined, SearchOutlined, EyeOutlined, EditOutlined, DeleteOutlined, } from '@ant-design/icons'; import { NotifAlert, NotifOk, NotifConfirmDialog } from '../../../components/Global/ToastNotif'; import { useNavigate } from 'react-router-dom'; import { getAllJadwalShift, deleteJadwalShift, updateJadwalShift } from '../../../api/jadwal-shift'; import { getAllShift } from '../../../api/master-shift'; const { Title, Text } = Typography; const ListJadwalShift = memo(function ListJadwalShift(props) { const [trigerFilter, setTrigerFilter] = useState(false); const defaultFilter = { criteria: '' }; const [formDataFilter, setFormDataFilter] = useState(defaultFilter); const [groupedSchedules, setGroupedSchedules] = useState({}); const [loading, setLoading] = useState(false); const [searchValue, setSearchValue] = useState(''); const [selectedSchedules, setSelectedSchedules] = useState([]); const [editingShift, setEditingShift] = useState(null); const [pendingChanges, setPendingChanges] = useState({}); const [employeeOptions, setEmployeeOptions] = useState([]); const [shiftOptions, setShiftOptions] = useState([]); const navigate = useNavigate(); const formatRelativeTimestamp = (timestamp) => { const now = new Date(); const date = new Date(timestamp); const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const startOfYesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1); let dayString; if (date >= startOfToday) { dayString = 'Hari ini'; } else if (date >= startOfYesterday) { dayString = 'Kemarin'; } else { dayString = date.toLocaleDateString('id-ID', { day: 'numeric', month: 'long' }); } const timeString = date .toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false }) .replace('.', ':'); return `${dayString}, ${timeString}`; }; const fetchData = async () => { setLoading(true); try { const paging = { page: 1, limit: 1000, }; const params = new URLSearchParams({ ...paging, ...formDataFilter }); // Fetch both schedules and shifts data const [schedulesResponse, shiftsResponse] = await Promise.all([ getAllJadwalShift(params), getAllShift(params), ]); // Handle nested data structure from backend const rawData = schedulesResponse?.data || schedulesResponse || []; const shifts = shiftsResponse?.data || shiftsResponse || []; setShiftOptions(shifts); // Parse backend response structure: [{ shift: { shift_id, shift_name, users: [...] } }] const grouped = {}; const allUsers = []; rawData.forEach((item) => { if (item.shift && item.shift.shift_name) { const shift = item.shift; const shiftName = shift.shift_name.toUpperCase().trim(); // Initialize shift group if (!grouped[shiftName]) { grouped[shiftName] = { shift_id: shift.shift_id, users: [], lastUpdate: { user: 'N/A', timestamp: '1970-01-01T00:00:00Z' }, }; } // Process users in this shift if (shift.users && Array.isArray(shift.users)) { shift.users.forEach((user) => { const normalizedUser = { id: user.user_schedule_id, user_schedule_id: user.user_schedule_id, user_id: user.user_id, shift_id: shift.shift_id, shift_name: shift.shift_name, nama_employee: user.user_fullname || user.user_name || 'Unknown', whatsapp: user.user_phone || '-', user_fullname: user.user_fullname, user_name: user.user_name, user_phone: user.user_phone, updated_at: user.updated_at, created_at: user.created_at, updated_by: user.updated_by, }; grouped[shiftName].users.push(normalizedUser); allUsers.push(normalizedUser); // Update last update timestamp const currentUpdate = new Date( user.updated_at || user.created_at || new Date() ); const lastUpdate = new Date(grouped[shiftName].lastUpdate.timestamp); if (currentUpdate > lastUpdate) { grouped[shiftName].lastUpdate = { user: user.updated_by || 'N/A', timestamp: currentUpdate.toISOString(), }; } }); } } }); setEmployeeOptions(allUsers); // Add empty shifts that don't have users yet shifts.forEach((shift) => { const shiftName = shift.shift_name.toUpperCase().trim(); if (!grouped[shiftName]) { grouped[shiftName] = { shift_id: shift.shift_id, users: [], lastUpdate: { user: 'N/A', timestamp: new Date().toISOString() }, }; } }); setGroupedSchedules(grouped); } catch (error) { NotifAlert({ icon: 'error', title: 'Gagal Memuat Data', message: 'Terjadi kesalahan saat memuat data jadwal shift.', }); } finally { setLoading(false); } }; useEffect(() => { const token = localStorage.getItem('token'); if (token) { if (props.actionMode === 'list') { setFormDataFilter(defaultFilter); doFilter(); } } else { navigate('/signin'); } }, [props.actionMode]); useEffect(() => { if (props.actionMode === 'list') { fetchData(); } }, [trigerFilter]); const doFilter = () => { setTrigerFilter((prev) => !prev); }; const handleSearch = () => { setFormDataFilter({ criteria: searchValue }); setTrigerFilter((prev) => !prev); }; const handleSearchClear = () => { setSearchValue(''); setFormDataFilter({ criteria: '' }); setTrigerFilter((prev) => !prev); }; const showPreviewModal = (param) => { props.setSelectedData(param); props.setActionMode('preview'); }; const showEditModal = (param = null) => { props.setSelectedData(param); props.setActionMode('edit'); }; const showAddModal = (param = null) => { props.setSelectedData(param); props.setActionMode('add'); }; const showDeleteDialog = (param) => { NotifConfirmDialog({ icon: 'question', title: 'Konfirmasi Hapus', message: `Jadwal untuk karyawan "${param.nama_employee}" akan dihapus?`, onConfirm: () => handleDelete(param.id), onCancel: () => props.setSelectedData(null), }); }; const handleDelete = async (id) => { const response = await deleteJadwalShift(id); if (response.statusCode === 200) { NotifAlert({ icon: 'success', title: 'Berhasil', message: 'Jadwal berhasil dihapus.', }); doFilter(); } else { NotifAlert({ icon: 'error', title: 'Gagal', message: response?.message || 'Gagal menghapus jadwal.', }); } }; const handleShiftEditMode = (shiftName) => { setEditingShift(shiftName); setPendingChanges({}); // Clear pending changes setSelectedSchedules([]); // Clear selections when entering a new edit mode }; const cancelShiftEditMode = () => { setEditingShift(null); setPendingChanges({}); setSelectedSchedules([]); }; const handleSelectSchedule = (id, isChecked) => { if (isChecked) { setSelectedSchedules((prev) => [...prev, id]); } else { setSelectedSchedules((prev) => prev.filter((scheduleId) => scheduleId !== id)); } }; const handleBulkUpdateChange = (scheduleId, field, value) => { setPendingChanges((prev) => ({ ...prev, [scheduleId]: { ...prev[scheduleId], [field]: value, }, })); }; const handleBulkSave = async () => { if (Object.keys(pendingChanges).length === 0) { NotifAlert({ icon: 'info', title: 'Tidak Ada Perubahan', message: 'Tidak ada perubahan untuk disimpan.', }); cancelShiftEditMode(); return; } const updatePromises = Object.keys(pendingChanges).map((id) => { const originalSchedule = groupedSchedules[editingShift].users.find( (u) => u.id.toString() === id ); const changes = pendingChanges[id]; // Build payload according to backend schema const payload = { user_id: changes.user_id || originalSchedule.user_id, shift_id: changes.shift_id || originalSchedule.shift_id, }; if (originalSchedule.schedule_id) { payload.schedule_id = originalSchedule.schedule_id; } return updateJadwalShift(id, payload); }); try { await Promise.all(updatePromises); NotifOk({ icon: 'success', title: 'Berhasil', message: 'Semua perubahan berhasil disimpan.', }); doFilter(); cancelShiftEditMode(); } catch (error) { NotifAlert({ icon: 'error', title: 'Gagal', message: 'Gagal menyimpan beberapa perubahan.', }); } }; const handleBulkDelete = () => { if (selectedSchedules.length === 0) { NotifAlert({ icon: 'warning', title: 'Perhatian', message: 'Pilih setidaknya satu jadwal untuk dihapus.', }); return; } NotifConfirmDialog({ icon: 'question', title: `Konfirmasi Hapus`, message: `Anda yakin ingin menghapus ${selectedSchedules.length} jadwal yang dipilih?`, onConfirm: async () => { await Promise.all(selectedSchedules.map((id) => deleteJadwalShift(id))); NotifOk({ icon: 'success', title: 'Berhasil', message: `${selectedSchedules.length} jadwal berhasil dihapus.`, }); doFilter(); setEditingShift(null); setSelectedSchedules([]); }, }); }; return ( Jadwal Shift { const value = e.target.value; setSearchValue(value); if (value === '') { handleSearchClear(); } }} onSearch={handleSearch} allowClear enterButton={ ) : ( )} {/* Horizontal scrollable container for employee cards */}
{groupedSchedules[shiftName].users.length > 0 ? ( groupedSchedules[shiftName].users.map((user) => ( {editingShift === shiftName ? ( // EDIT MODE VIEW
handleSelectSchedule( user.id, e.target.checked ) } style={{ transform: 'scale(1.4)', }} />
KARYAWAN
NO. TELEPON emp.user_id === pendingChanges[ user .id ]?.user_id )?.user_phone || user.whatsapp : user.whatsapp } readOnly style={{ backgroundColor: '#f5f5f5', color: '#595959', cursor: 'not-allowed', }} size="large" />
SHIFT
) : ( // NORMAL VIEW
{user.nama_employee} {user.whatsapp}
Terakhir diperbarui {' '}
{formatRelativeTimestamp( user.updated_at || user.created_at || new Date() )}{' '}
oleh {user.updated_by || 'N/A'}
)}
)) ) : ( Tidak ada karyawan yang dijadwalkan untuk shift ini. )}
)) )}
); }); export default ListJadwalShift;