Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Usamaliaquat123/4fcb854047b28396d8fe1a655483d50a to your computer and use it in GitHub Desktop.
Save Usamaliaquat123/4fcb854047b28396d8fe1a655483d50a to your computer and use it in GitHub Desktop.
tablecomponent
import React, { useState, useEffect } from 'react';
import {
Box,
CircularProgress,
Menu,
MenuItem,
IconButton,
TextField,
Button,
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import SearchIcon from '@mui/icons-material/Search';
import PageLoader from 'layouts/components/common/PageLoader';
import JobDetailsModal from './components/JobDetailsModal';
import { fetchStaffsByDepartment } from 'layouts/TasksBoard/functions/firestoreUtils';
import {
doc,
updateDoc,
collection,
query,
where,
getDocs,
orderBy,
startAfter,
limit,
getCountFromServer,
onSnapshot,
Timestamp,
} from 'firebase/firestore';
import ReasonModal from './components/ReasonModal';
import CreateTechniciaJobModal from './components/CreateJobModal';
import UserDetailsSidebar from './UserDetailsSidebar';
import { db } from 'Utils/firebase';
import { toast } from 'react-toastify';
import TechnicianAssignModal from './components/TechnicianAssignModal';
export default function TasksTable({ title }) {
const [loading, setLoading] = useState(true);
const [tasks, setTasks] = useState([]);
const [tabsTotalCount, setTabsTotalCount] = useState({
new: 0,
followUp: 0,
dispatched: 0,
rejected: 0,
accepted: 0,
userCalled: 0,
onRoute: 0,
arrived: 0,
jobStarted: 0,
jobDone: 0,
declined: 0,
unfinished: 0,
});
const [showLoader, setShowLoader] = useState(false);
const [lastDocs, setLastDocs] = useState({});
const [anchorEl, setAnchorEl] = useState(null);
const [openMenuId, setOpenMenuId] = useState(null);
const [openModal, setOpenModal] = useState(false);
const [selectedJob, setSelectedJob] = useState(null);
const [openReasonModal, setOpenReasonUpModal] = useState(false);
const [actionForReasonModal, setActionForReasonModal] = useState('');
const [openAssignModal, setOpenAssignModal] = useState(false);
const [taskDetails, setTaskDetails] = useState(null);
const [staffs, setStaffs] = useState([]);
const [editModal, setEditModal] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [selectedUserData, setSelectedUserData] = useState(null);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [filteredTasks, setFilteredTasks] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const [tab, setTab] = useState(0);
const [subTab, setSubTab] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [number, setNumber] = useState(0);
const recordsPerPage = 15;
const [lastRec, setLastRec] = useState(null);
const [allRealtimeTasks, setAllRealtimeTasks] = useState([]);
const [sucessFullyAssigned, setSucessFullyAssigned] = useState(false);
const tasksCol = collection(db, 'TechnicianJobs');
const subTabs = {
0: [
{ index: 0, label: 'Progress track', key: 'new' },
{ index: 1, label: 'Follow Up', key: 'followUp' },
{ index: 2, label: 'Dispatched', key: 'dispatched' },
{ index: 3, label: 'Rejected', key: 'rejected' },
{ index: 4, label: 'Accepted', key: 'accepted' },
{ index: 5, label: 'User Called', key: 'userCalled' },
{ index: 6, label: 'On Route', key: 'onRoute' },
{ index: 7, label: 'Arrived', key: 'arrived' },
{ index: 8, label: 'Job Started', key: 'jobStarted' },
{ index: 9, label: 'Done', key: 'jobDone' },
{ index: 10, label: 'Declined', key: 'declined' },
],
1: [{ index: 0, label: 'Job Done', key: 'jobDone-History' }],
3: [{ index: 0, label: 'Declined', key: 'declined-History' }],
2: [{ index: 0, label: 'Unfinished', key: 'unfinished' }],
};
// Helper function to get timestamp for 2 days ago
const get24HoursAgo = () => {
const twoDaysAgo = new Date();
twoDaysAgo.setDate(twoDaysAgo.getDate() - 1);
return Timestamp.fromDate(twoDaysAgo);
};
useEffect(() => {
const fetchUnfinishedCount = async () => {
try {
const twoDaysAgo = get24HoursAgo();
const unfinishedQuery = query(
tasksCol,
where('dateTimestamp', '<', twoDaysAgo),
where('statuses', 'array-contains-any', ['unFinished']),
orderBy('dateTimestamp', 'desc'),
);
const countSnapshot = await getCountFromServer(unfinishedQuery);
console.log('---?', unfinishedQuery, countSnapshot.data());
setTabsTotalCount(prev => ({
...prev,
unfinished: countSnapshot.data().count,
}));
} catch (error) {
console.error('Error fetching unfinished count:', error);
}
};
fetchUnfinishedCount();
}, []);
// Fetch historical counts only once or when needed
useEffect(() => {
const fetchHistoricalCounts = async () => {
try {
// Remove jobDone and declined from historical counts since they're now in real-time
const historicalStatuses = ['jobDone-History', 'declined-History'];
const counts = {};
for (const status of historicalStatuses) {
const countQuery = query(tasksCol, where('status', '==', `${status.split('-')[0]}`));
const snapshot = await getCountFromServer(countQuery);
counts[status] = snapshot.data().count;
}
setTabsTotalCount(prev => ({
...prev,
...counts,
}));
setLoading(false);
} catch (error) {
setLoading(false);
console.error('Error fetching historical counts:', error);
}
};
fetchHistoricalCounts();
}, []);
useEffect(() => {
const twoDaysAgo = get24HoursAgo();
const recentTasksQuery = query(
tasksCol,
where('dateTimestamp', '>=', twoDaysAgo),
orderBy('dateTimestamp', 'desc'),
);
const unsubscribe = onSnapshot(recentTasksQuery, snapshot => {
const newCounts = {
new: 0,
followUp: 0,
dispatched: 0,
rejected: 0,
accepted: 0,
userCalled: 0,
onRoute: 0,
arrived: 0,
jobStarted: 0,
jobDone: 0,
declined: 0,
};
const allTasks = snapshot.docs.map(doc => {
const data = doc.data();
if (newCounts.hasOwnProperty(data.status)) {
newCounts[data.status]++;
}
return { id: doc.id, data };
});
setAllRealtimeTasks(allTasks);
setTabsTotalCount(prev => ({
...prev,
...newCounts,
}));
if (tab === 0) {
const filteredTasks = allTasks.filter(task => task.data.status === subTabs[0][subTab].key);
setNumber(filteredTasks.length);
setTasks(filteredTasks);
setFilteredTasks(
filteredTasks.slice((currentPage - 1) * recordsPerPage, currentPage * recordsPerPage),
);
}
});
return () => unsubscribe();
}, [tab, currentPage]);
// Handle tab changes
const handleTabChange = async (newTab, newSubTab = 0) => {
setShowLoader(true);
try {
setTab(newTab);
setSubTab(newSubTab);
setCurrentPage(1);
setIsSearching(false);
if (newTab === 0) {
// For progress track tabs, filter from real-time data
const filteredTasks = allRealtimeTasks.filter(
task => task.data.status === subTabs[0][newSubTab].key,
);
setTasks(filteredTasks);
setNumber(filteredTasks.length);
setFilteredTasks(filteredTasks.slice(0, recordsPerPage));
} else {
// For historical tabs, fetch from Firestore
const statusKey = subTabs[newTab][newSubTab].key;
const historicalQuery = query(
tasksCol,
where('status', '==', `${statusKey.split('-')[0]}`),
orderBy('dateTimestamp', 'desc'),
limit(recordsPerPage),
);
const snapshot = await getDocs(historicalQuery);
const historicalTasks = snapshot.docs.map(doc => ({
id: doc.id,
data: doc.data(),
}));
setTasks(historicalTasks);
setFilteredTasks(historicalTasks);
setNumber(tabsTotalCount[statusKey] || 0);
if (snapshot.docs.length > 0) {
setLastRec(snapshot.docs[snapshot.docs.length - 1]);
}
}
} catch (error) {
console.error('Error changing tab:', error);
toast.error('Error loading data');
} finally {
setShowLoader(false);
}
};
// Handle pagination
const handleNextPage = async () => {
if (currentPage * recordsPerPage >= number) return;
setShowLoader(true);
try {
if (tab === 0) {
// For progress track tabs, paginate from allRealtimeTasks
const filteredTasks = allRealtimeTasks.filter(
task => task.data.status === subTabs[0][subTab].key,
);
setFilteredTasks(
filteredTasks.slice(currentPage * recordsPerPage, (currentPage + 1) * recordsPerPage),
);
} else {
// For historical tabs, fetch next page from Firestore
const statusKey = subTabs[tab][subTab].key;
const paginationQuery = query(
tasksCol,
where('status', '==', `${statusKey.split('-')[0]}`),
orderBy('dateTimestamp', 'desc'),
startAfter(lastRec),
limit(recordsPerPage),
);
const snapshot = await getDocs(paginationQuery);
const newTasks = snapshot.docs.map(doc => ({
id: doc.id,
data: doc.data(),
}));
setTasks(prevTasks => [...prevTasks, ...newTasks]);
setFilteredTasks(prevFiltered => [...prevFiltered, ...newTasks]);
if (snapshot.docs.length > 0) {
setLastRec(snapshot.docs[snapshot.docs.length - 1]);
}
}
setCurrentPage(prev => prev + 1);
} catch (error) {
console.error('Error fetching next page:', error);
} finally {
setShowLoader(false);
}
};
const handlePrevPage = () => {
if (currentPage === 1) return;
const newPage = currentPage - 1;
setCurrentPage(newPage);
if (tab === 0) {
// For progress track tabs, paginate from allRealtimeTasks
const filteredTasks = allRealtimeTasks.filter(
task => task.data.status === subTabs[0][subTab].key,
);
setFilteredTasks(
filteredTasks.slice((newPage - 1) * recordsPerPage, newPage * recordsPerPage),
);
} else {
// For historical tabs, show previous page from already fetched tasks
setFilteredTasks(tasks.slice((newPage - 1) * recordsPerPage, newPage * recordsPerPage));
}
};
const handleClick = (event, id) => {
setAnchorEl(event.currentTarget);
setOpenMenuId(id);
};
const handleCloseMenu = () => {
setAnchorEl(null);
setOpenMenuId(null);
};
const handleUserClick = async phoneNumber => {
try {
setShowLoader(true);
const q = query(collection(db, 'Users'), where('phoneNumber', '==', phoneNumber));
const userQuery = await getDocs(q);
if (!userQuery.empty) {
const user = userQuery.docs[0];
setSelectedUserData(user.data());
setSidebarOpen(true);
} else {
toast.error('User not found');
}
} catch (error) {
console.error('Failed to fetch user details:', error);
} finally {
setShowLoader(false);
}
};
const handleCloseSidebar = () => {
setSidebarOpen(false);
setSelectedUserData(null);
};
const handleOpenModal = jobData => {
setSelectedJob(jobData);
setOpenModal(true);
};
const handleOpenEditModal = jobData => {
setSelectedJob(jobData);
setEditModal(true);
};
const handleCloseModal = () => {
setOpenModal(false);
setSelectedJob(null);
};
const handleReasonModal = (action, task) => {
setTaskDetails(task);
setOpenReasonUpModal(true);
setActionForReasonModal(action === 'followUp' ? 'followUp' : 'decline');
};
const handleCloseFollowUpModal = () => {
setOpenReasonUpModal(false);
};
const handleConfirmFollowUp = async (reason, taskId) => {
try {
if (actionForReasonModal === 'followUp') {
await updateDoc(doc(db, 'TechnicianJobs', taskId), {
status: 'followUp',
followUpReason: reason,
});
} else {
await updateDoc(doc(db, 'TechnicianJobs', taskId), {
status: 'declined',
declineReason: reason,
assignedTechnicianID: '',
assignedTechnician: '',
});
}
} catch (error) {
console.error('Failed to update document:', error);
}
};
const handleAssignTechnician = async taskDetails => {
setOpenAssignModal(true);
setTaskDetails(taskDetails);
console.log('Assigning technicians..., quuried');
try {
if (staffs?.length > 0) {
return;
}
setStaffs([]);
const staffArr = await fetchStaffsByDepartment('Technicians');
setStaffs(staffArr);
} catch (error) {
console.error('Error fetching technicians:', error.message);
}
};
const handleSearch = async () => {
if (!searchTerm.trim()) {
setFilteredTasks(tasks.slice(0, recordsPerPage));
setIsSearching(false);
return;
}
setShowLoader(true);
setIsSearching(true);
try {
const searchQuery = query(tasksCol, where('phoneNumber', '==', searchTerm));
const querySnapshot = await getDocs(searchQuery);
const searchResults = querySnapshot.docs.map(doc => ({
id: doc.id,
data: doc.data(),
}));
setFilteredTasks(searchResults);
} catch (error) {
console.error('Error performing search:', error.message);
} finally {
setShowLoader(false);
}
};
const handleResetSearch = () => {
setSearchTerm('');
setFilteredTasks(tasks.slice(0, recordsPerPage));
setIsSearching(false);
};
// Update filtered tasks when tasks or pagination changes
useEffect(() => {
if (!isSearching) {
setFilteredTasks(
tasks.slice((currentPage - 1) * recordsPerPage, currentPage * recordsPerPage),
);
}
}, [tasks, currentPage, isSearching]);
return loading ? (
<PageLoader />
) : (
<div className="mt-1">
{/* Main Tabs */}
{!isSearching && (
<div className="mb-4 mb-9 border-b border-gray-200 pt-1">
<ul className="-mb-px flex w-full flex-wrap justify-between text-center text-sm font-medium text-gray-500">
{Object.keys(subTabs).map(mainTabKey => (
<li key={mainTabKey} className="flex-1">
<a
href="#"
className={`${
tab === parseInt(mainTabKey)
? 'bg-white-100 border-b-2 border-gray-800 text-gray-800'
: 'border-transparent bg-white text-gray-500'
} group inline-flex w-full items-center justify-center py-3`}
onClick={() => handleTabChange(parseInt(mainTabKey))}
>
{subTabs[mainTabKey][0].label} ({tabsTotalCount[subTabs[mainTabKey][0].key] || 0})
</a>
</li>
))}
</ul>
</div>
)}
{/* Search Bar */}
<div className={`relative mb-4 mr-2 flex justify-center ${isSearching ? 'mt-15' : ''}`}>
<TextField
label="Search by Phone Number"
variant="outlined"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter') handleSearch();
}}
InputProps={{
style: {
backgroundColor: 'white',
fontWeight: 'bold',
},
endAdornment: (
<>
<IconButton onClick={handleSearch}>
<SearchIcon />
</IconButton>
<Button
onClick={handleResetSearch}
variant="outlined"
size="small"
style={{ marginLeft: '10px', color: '#000' }}
>
Reset
</Button>
</>
),
}}
InputLabelProps={{
style: {
fontWeight: 'bold',
},
}}
className={`bg-white p-2 text-gray-900 shadow-md ${isSearching ? 'mb-8' : ''}`}
style={{
position: 'absolute',
top: '-1rem',
zIndex: 1,
right: 35,
}}
/>
</div>
<div className="relative mb-9 mr-2 rounded-lg bg-white">
<div className="absolute -top-9 left-0 right-0 mx-7 rounded-md bg-red-500 py-3 pl-3 text-white">
{isSearching ? `Search Results for: ${searchTerm}` : title}
</div>
<div className="mb-2 py-4"></div>
{/* Sub Tabs */}
{!isSearching && (
<div className="mb-4 border-b border-gray-200 bg-slate-300 pt-2">
<ul className="-mb-px flex flex-wrap text-center text-sm font-medium text-gray-500">
{(subTabs[tab] || []).map(({ index, label, key }) => (
<li key={index} className="me-2">
<a
href="#"
className={`${subTab === index ? 'border-b-2 border-red-500 text-red-800' : 'text-gray-950'} group inline-flex items-center justify-center rounded-t-lg p-4`}
onClick={() => handleTabChange(tab, index)}
>
{label} ({tabsTotalCount[key] || 0})
</a>
</li>
))}
</ul>
</div>
)}
{/* Table */}
<div className="overflow-x-auto pb-6 pl-4 pr-8 shadow-xl">
<table className={`min-w-full ${isSearching ? 'mt-8' : ''}`}>
<thead className="border-b bg-slate-100">
<tr>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">
Customer
</th>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">Mobile</th>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">Service</th>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">Branch</th>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">
Date/Time
</th>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">
Created By
</th>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">
Assigned To
</th>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">Status</th>
<th className="px-4 py-2 text-left text-base font-medium text-gray-900">Actions</th>
</tr>
</thead>
<tbody>
{filteredTasks.length > 0 ? (
filteredTasks.map(task => (
<tr
key={task.id}
className="border-b bg-white transition duration-300 ease-in-out hover:cursor-pointer hover:bg-gray-100"
>
<td onClick={() => handleUserClick(task?.data?.phoneNumber)}>
<p className="whitespace-nowrap px-4 py-2 text-base text-blue-800">
{task?.data?.customerName || 'N/A'}
</p>
</td>
<td className="whitespace-nowrap px-4 py-2 text-base text-gray-900">
{task?.data?.phoneNumber || 'N/A'}
</td>
<td className="whitespace-nowrap px-4 py-2 text-base text-gray-900">
{task?.data?.service || 'N/A'}
</td>
<td className="whitespace-nowrap px-4 py-2 text-base text-gray-900">
{task?.data?.branch || 'N/A'}
</td>
<td className="whitespace-nowrap px-4 py-2 text-base text-gray-900">{`${task?.data?.bookedTime} - ${task?.data?.bookedDate}`}</td>
<td className="whitespace-nowrap px-4 py-2 text-base text-gray-900">
{task?.data?.createdBy?.displayName || 'N/A'}
</td>
<td className="whitespace-nowrap px-4 py-2 text-base text-gray-900">
{task?.data?.assignedTechnician || 'N/A'}
</td>
<td className="whitespace-nowrap px-4 py-2 text-base text-gray-900">
{task?.data?.status || 'N/A'}
</td>
<td className="flex items-center justify-center gap-2 pt-2">
<IconButton
aria-label="more"
aria-controls={`long-menu-${task.id}`}
aria-haspopup="true"
onClick={event => handleClick(event, task.id)}
>
<MoreVertIcon />
</IconButton>
{openMenuId === task.id && (
<Menu
id={`long-menu-${task.id}`}
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl) && openMenuId === task.id}
onClose={handleCloseMenu}
>
<MenuItem
onClick={() => {
handleCloseMenu();
handleOpenModal(task.data);
}}
>
View Details
</MenuItem>
<MenuItem
onClick={() => {
handleCloseMenu();
handleOpenEditModal({ ...task.data, taskId: task.id });
}}
>
Edit Details
</MenuItem>
{task?.data?.status === 'jobStarted' ||
task?.data?.status === 'jobDone' ? null : (
<MenuItem
onClick={() => {
handleCloseMenu();
handleAssignTechnician({ ...task.data, taskId: task.id });
}}
>
Assign Technician
</MenuItem>
)}
<MenuItem
onClick={() =>
handleReasonModal('followUp', { ...task.data, taskId: task.id })
}
>
Add to Follow Up
</MenuItem>
<MenuItem
onClick={() =>
handleReasonModal('decline', { ...task.data, taskId: task.id })
}
>
Decline
</MenuItem>
</Menu>
)}
</td>
</tr>
))
) : (
<tr>
<td colSpan="9" className="py-4 text-center text-gray-500">
No Tasks Available
</td>
</tr>
)}
</tbody>
</table>
{showLoader && (
<Box sx={{ display: 'flex', position: 'absolute', top: '50%', left: '50%' }}>
<CircularProgress />
</Box>
)}
</div>
</div>
{!isSearching && (
<div className="mr-36 mt-12 flex items-center justify-end space-x-4">
<div>
<span className="text-base">
Showing items {(currentPage - 1) * recordsPerPage + 1} -
{currentPage * recordsPerPage > number ? number : currentPage * recordsPerPage} out of{' '}
{number}
</span>
</div>
<div className="flex items-center gap-x-4 rounded-lg bg-blue-500 px-2 py-2 text-base text-white">
<button
type="button"
onClick={handlePrevPage}
disabled={currentPage === 1}
className={`${currentPage === 1 ? 'opacity-50 hover:cursor-not-allowed' : ''} flex items-center hover:cursor-pointer hover:text-white/80`}
>
<svg
className="mr-0 h-5 w-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span>Previous</span>
</button>
<button
type="button"
onClick={handleNextPage}
disabled={currentPage * recordsPerPage >= number}
className={`${currentPage * recordsPerPage >= number ? 'opacity-50 hover:cursor-not-allowed' : 'hover:bg-blue-500'} flex items-center hover:cursor-pointer hover:text-white/80`}
>
<span>Next</span>
<svg
className="h-5 w-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
)}
{/* Modals and Sidebars */}
{sidebarOpen && selectedUserData && (
<UserDetailsSidebar userData={selectedUserData} onClose={handleCloseSidebar} />
)}
{openAssignModal && (
<TechnicianAssignModal
staffs={staffs}
taskId={taskDetails?.taskId}
closeStaffModal={() => setOpenAssignModal(false)}
role={'Technicians'}
setSucessFullyAssigned={setSucessFullyAssigned}
taskDetails={taskDetails}
/>
)}
<ReasonModal
open={openReasonModal}
handleClose={handleCloseFollowUpModal}
onConfirm={handleConfirmFollowUp}
heading={`Reason for ${actionForReasonModal === 'followUp' ? 'Follow Up' : 'Decline'}`}
actionForReasonModal={actionForReasonModal}
taskId={taskDetails?.taskId}
/>
{editModal && (
<CreateTechniciaJobModal
openModal={editModal}
setOpenModal={setEditModal}
dontReload={true}
job={selectedJob}
/>
)}
<JobDetailsModal open={openModal} handleClose={handleCloseModal} jobData={selectedJob} />
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment