349 lines
22 KiB
TypeScript
349 lines
22 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
||
import Layout from "../../../components/layout";
|
||
import ProtectedRoute from '../../../components/protectedRoute';
|
||
import { Prisma, UserRole, PublisherType } from '@prisma/client';
|
||
import axiosServer from '../../../src/axiosServer';
|
||
import common from '../../../src/helpers/common';
|
||
// import { filterPublishers, /* other functions */ } from '../../api/index';
|
||
import data from '../../../src/helpers/data';
|
||
import axiosInstance from '../../../src/axiosSecure';
|
||
import { setFlagsFromString } from 'v8';
|
||
import { set } from 'date-fns';
|
||
|
||
function ContactsPage({ allPublishers }) {
|
||
|
||
const currentMonth = new Date().getMonth();
|
||
const [selectedMonth, setSelectedMonth] = useState(currentMonth + 1);
|
||
const isMounted = useRef(false);
|
||
|
||
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [publisherType, setPublisherType] = useState('');
|
||
const [publishers, setPublishers] = useState(allPublishers);
|
||
const [pubWithAssignmentsCount, setPubWithAssignmentsCount] = useState(0);
|
||
const [filteredPublishers, setFilteredPublishers] = useState(allPublishers);
|
||
|
||
const [sortField, setSortField] = useState('name');
|
||
const [sortOrder, setSortOrder] = useState('asc');
|
||
|
||
const months = common.getMonthNames();
|
||
const subsetMonths = Array.from({ length: 9 }, (_, i) => {
|
||
const monthIndex = (currentMonth - 3 + i + 12) % 12; // Adjust for year wrap-around
|
||
return {
|
||
name: months[monthIndex],
|
||
index: monthIndex + 1
|
||
};
|
||
});
|
||
|
||
const [hideEmptyFields, setHideEmptyFields] = useState({
|
||
availability: 'off', // 'on', 'off', 'onlyEmpty'
|
||
assignments: 'off', // 'on', 'off', 'onlyEmpty'
|
||
lastLogin: false,
|
||
notifiications: false
|
||
});
|
||
|
||
const availabilityRef = useRef(null);
|
||
const assignmentsRef = useRef(null);
|
||
useEffect(() => {
|
||
if (availabilityRef.current) {
|
||
availabilityRef.current.indeterminate = hideEmptyFields.availability === 'off';
|
||
}
|
||
}, [hideEmptyFields.availability]);
|
||
useEffect(() => {
|
||
if (assignmentsRef.current) {
|
||
assignmentsRef.current.indeterminate = hideEmptyFields.assignments === 'off';
|
||
}
|
||
}, [hideEmptyFields.assignments]);
|
||
|
||
|
||
const getCheckboxState = (field) => {
|
||
switch (hideEmptyFields[field]) {
|
||
case 'on':
|
||
return true;
|
||
case 'onlyEmpty':
|
||
return false;
|
||
default:
|
||
return undefined; // this will be used to set indeterminate
|
||
}
|
||
};
|
||
const getCheckboxTooltip = (field, label) => {
|
||
switch (hideEmptyFields[field]) {
|
||
case 'on':
|
||
return 'Само със ' + label;
|
||
case 'onlyEmpty':
|
||
return 'Само без ' + label;
|
||
default:
|
||
return 'Всички ' + label;
|
||
}
|
||
}
|
||
|
||
function handleSort(field) {
|
||
const order = sortField === field && sortOrder === 'asc' ? 'desc' : 'asc';
|
||
setSortField(field);
|
||
setSortOrder(order);
|
||
}
|
||
|
||
useEffect(() => {
|
||
let filtered = publishers.filter(publisher =>
|
||
(publisher.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
publisher.lastName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
publisher.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
(publisher.phone?.toLowerCase().includes(searchQuery.toLowerCase())))
|
||
&& (publisherType ? publisher.type === publisherType : true)
|
||
// && (!hideEmptyFields.availability || publisher.currentMonthAvailabilityDaysCount > 0)
|
||
// && (!hideEmptyFields.assignments || publisher.currentMonthAssignments > 0)
|
||
&& (hideEmptyFields.availability === 'on' ? publisher.currentMonthAvailabilityDaysCount > 0 :
|
||
hideEmptyFields.availability === 'onlyEmpty' ? !publisher.currentMonthAvailabilityDaysCount || publisher.currentMonthAvailabilityDaysCount === 0 : true)
|
||
&& (hideEmptyFields.assignments === 'on' ? publisher.currentMonthAssignments > 0 :
|
||
hideEmptyFields.assignments === 'onlyEmpty' ? publisher.currentMonthAssignments === 0 : true)
|
||
&& (!hideEmptyFields.lastLogin || publisher.lastLogin)
|
||
&& (!hideEmptyFields.notifiications || publisher.isPushActive)
|
||
);
|
||
|
||
if (sortField) {
|
||
filtered.sort((a, b) => {
|
||
// Check for undefined or null values and treat them as "larger" when sorting ascending
|
||
const aValue = a[sortField] || 0; // Treat undefined, null as 0
|
||
const bValue = b[sortField] || 0; // Treat undefined, null as 0
|
||
|
||
if (aValue === 0 && bValue !== 0) return 1; // aValue is falsy, push it to end if asc
|
||
if (bValue === 0 && aValue !== 0) return -1; // bValue is falsy, push it to end if asc
|
||
|
||
if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1;
|
||
if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1;
|
||
return 0;
|
||
});
|
||
}
|
||
setFilteredPublishers(filtered);
|
||
setPubWithAssignmentsCount(filtered.filter(publisher => publisher.currentMonthAvailabilityHoursCount && publisher.currentMonthAvailabilityHoursCount > 0).length);
|
||
}, [searchQuery, publisherType, sortField, sortOrder, allPublishers, hideEmptyFields, selectedMonth]);
|
||
|
||
useEffect(() => {
|
||
if (isMounted.current) {
|
||
const fetchData = async () => {
|
||
const month = parseInt(selectedMonth);
|
||
const filterDate = new Date(new Date().getFullYear(), month - 1, 15);
|
||
try {
|
||
const response = await axiosInstance.get(`/api/?action=getAllPublishersWithStatistics&date=${filterDate.toISOString()}&noEndDate=false`);
|
||
setPublishers(response.data);
|
||
setFilteredPublishers(response.data);
|
||
setPubWithAssignmentsCount(response.data.filter(publisher => publisher.currentMonthAvailabilityHoursCount && publisher.currentMonthAvailabilityHoursCount > 0).length);
|
||
setHideEmptyFields({ availability: 'off', assignments: 'off', lastLogin: false, notifiications: false })
|
||
} catch (error) {
|
||
console.error('Failed to fetch publishers data:', error);
|
||
// Optionally, handle errors more gracefully here
|
||
}
|
||
};
|
||
fetchData();
|
||
} else {
|
||
// Set the ref to true after the initial render
|
||
isMounted.current = true;
|
||
}
|
||
}, [selectedMonth]); // Dependency array includes only selectedMonth
|
||
|
||
function renderSortArrow(field) {
|
||
return sortField === field ? sortOrder === 'asc' ? ' ↑' : ' ↓' : '';
|
||
}
|
||
|
||
return (
|
||
<Layout>
|
||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
|
||
<div className="container mx-auto p-4">
|
||
<h1 className="text-xl font-semibold mb-4">Статистика </h1>
|
||
<h5 className="text-lg font-semibold mb-4">{pubWithAssignmentsCount} участника с предпочитания за месеца (от {filteredPublishers.length} )</h5>
|
||
<div className="mb-4 flex justify-between items-center">
|
||
<input name="filterText"
|
||
type="text"
|
||
placeholder="Търси по име, имейл или телефон..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="border border-gray-300 rounded-md px-2 py-2 text-base md:text-sm flex-grow mr-2"
|
||
/>
|
||
{/* Month dropdown */}
|
||
<select name="filterMonth"
|
||
className="border border-gray-300 rounded-md px-2 py-2 text-base md:text-sm"
|
||
value={selectedMonth}
|
||
onChange={(e) => setSelectedMonth(e.target.value)}
|
||
>
|
||
<option value="">избери месец</option>
|
||
{subsetMonths.map((month) => (
|
||
<option key={month.index} value={month.index}>{month.name}</option>
|
||
))}
|
||
</select>
|
||
{/* Publisher type dropdown */}
|
||
<select name="filterType"
|
||
className="border border-gray-300 rounded-md px-2 py-2 text-base md:text-sm"
|
||
value={publisherType}
|
||
onChange={(e) => setPublisherType(e.target.value)}
|
||
>
|
||
<option value="">Всички типове</option>
|
||
{Object.keys(PublisherType).map((type) => (
|
||
<option key={type} value={PublisherType[type]}>{PublisherType[type]}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-left border-collapse">
|
||
<thead>
|
||
<tr>
|
||
<th className="border-b font-medium p-4 pl-8 pt-0 pb-3 cursor-pointer" onClick={() => handleSort('name')}>
|
||
Име{renderSortArrow('name')}
|
||
</th>
|
||
<th className="border-b font-medium p-4 pt-0 pb-3 cursor-pointer" >
|
||
<p className="inline-block" onClick={() => handleSort('currentMonthAvailabilityDaysCount')}>
|
||
Възможности{renderSortArrow('currentMonthAvailabilityDaysCount')}
|
||
</p>
|
||
<input
|
||
type="checkbox" className='ml-2'
|
||
ref={availabilityRef}
|
||
checked={getCheckboxState('availability') ?? false}
|
||
onChange={() => {
|
||
const newState = hideEmptyFields.availability === 'on' ? 'onlyEmpty' : (hideEmptyFields.availability === 'onlyEmpty' ? 'off' : 'on');
|
||
setHideEmptyFields({ ...hideEmptyFields, availability: newState });
|
||
}}
|
||
title={getCheckboxTooltip('availability', "възможности")}
|
||
/>
|
||
</th>
|
||
<th className="border-b font-medium p-4 pt-0 pb-3 cursor-pointer" >
|
||
<p className="inline-block" onClick={() => handleSort('currentMonthAssignments')}>
|
||
Участия{renderSortArrow('currentMonthAssignments')}
|
||
</p>
|
||
<input
|
||
type="checkbox" className='ml-2'
|
||
ref={assignmentsRef}
|
||
checked={getCheckboxState('assignments') ?? false}
|
||
onChange={() => {
|
||
const newState = hideEmptyFields.assignments === 'on' ? 'onlyEmpty' : (hideEmptyFields.assignments === 'onlyEmpty' ? 'off' : 'on');
|
||
setHideEmptyFields({ ...hideEmptyFields, assignments: newState });
|
||
}}
|
||
title={getCheckboxTooltip('assignments', "участия")}
|
||
/>
|
||
</th>
|
||
<th className="border-b font-medium p-4 pt-0 pb-3 cursor-pointer" >
|
||
<p className="inline-block" onClick={() => handleSort('lastLogin')}>
|
||
Последно влизане{renderSortArrow('lastLogin')}
|
||
</p>
|
||
<input
|
||
type="checkbox" className='ml-2'
|
||
checked={hideEmptyFields.lastLogin}
|
||
onChange={() => setHideEmptyFields({ ...hideEmptyFields, lastLogin: !hideEmptyFields.lastLogin })}
|
||
title="Скрий редове без нотификации"
|
||
/>
|
||
</th>
|
||
<th className="border-b font-medium p-4 pt-0 pb-3 cursor-pointer">
|
||
<p className="inline-block" >
|
||
Нотификации
|
||
</p>
|
||
<input
|
||
type="checkbox" className='ml-2'
|
||
checked={hideEmptyFields.notifiications}
|
||
onChange={() => setHideEmptyFields({ ...hideEmptyFields, notifiications: !hideEmptyFields.notifiications })}
|
||
title="Скрий редове без последно влизане"
|
||
/>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filteredPublishers.map((pub, i) => {
|
||
return (
|
||
<tr key={pub.id}>
|
||
<td className="border-b p-4 pl-8" title={pub.lastUpdate}>{i + 1}. {pub.firstName} {pub.lastName}</td>
|
||
{/* Display statistics if publisher is found */}
|
||
{pub ? (
|
||
<>
|
||
<td className="border-b p-4">
|
||
{pub.availabilities.length > 0 ? (
|
||
<span title="Възможност: дни | часове" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`}>
|
||
{pub.currentMonthAvailabilityDaysCount} | {pub.currentMonthAvailabilityHoursCount}
|
||
</span>
|
||
) : <span title="Няма възможности" className="badge py-1 px-2 rounded-md text-xs bg-gray-300 text-gray-500">0</span>}
|
||
</td>
|
||
<td className="border-b p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center">
|
||
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
|
||
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
|
||
<span title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
|
||
<button title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="border-b p-4">{pub.lastLogin ? new Date(pub.lastLogin).toLocaleString("bg") : ""}</td>
|
||
</>
|
||
) : (
|
||
<>
|
||
<td className="border-b p-4">
|
||
</td>
|
||
<td className="border-b p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center">
|
||
<span className="badge py-1 px-2 rounded-md text-xs bg-gray-300 text-gray-500" title="участия този месец">
|
||
{pub.currentMonthAssignments}
|
||
</span>
|
||
<span className="badge py-1 px-2 rounded-md text-xs bg-gray-300 text-gray-500" title="участия миналия месец">
|
||
{pub.previousMonthAssignments}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="border-b p-4"></td> {/* Empty cell for alignment */}
|
||
</>
|
||
)}
|
||
<td className="border-b p-4 text-center">
|
||
{pub.isPushActive ? (
|
||
<span className="text-green-500 flex items-center justify-center" aria-label="Notifications Enabled">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 28 28" stroke="currentColor" className="w-7 h-7" title="Notifications are active">
|
||
<circle cx="13" cy="13" r="12" fill="white" stroke="currentColor" stroke-width="2" />
|
||
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 16l2.5 2.5 7-10" /> </svg>
|
||
</span>
|
||
) : (
|
||
<span className="text-gray-400">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" className="w-4 h-4 inline-block">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</ProtectedRoute>
|
||
</Layout>
|
||
);
|
||
}
|
||
|
||
|
||
export default ContactsPage;
|
||
|
||
|
||
|
||
export const getServerSideProps = async (context) => {
|
||
const allPublishers = await data.getAllPublishersWithStatisticsMonth(new Date());
|
||
// Merge first and last name and serialize Date objects
|
||
allPublishers.forEach(publisher => {
|
||
publisher.name = `${publisher.firstName} ${publisher.lastName}`;
|
||
|
||
if (publisher.currentMonthAvailability) {
|
||
publisher.currentMonthAvailability = publisher.currentMonthAvailability.map(availability => {
|
||
return {
|
||
...availability,
|
||
startTime: availability.startTime instanceof Date ? availability.startTime.toISOString() : availability.startTime,
|
||
endTime: availability.endTime instanceof Date ? availability.endTime.toISOString() : availability.endTime,
|
||
dateOfEntry: availability.dateOfEntry instanceof Date ? availability.dateOfEntry.toISOString() : availability.dateOfEntry,
|
||
};
|
||
});
|
||
}
|
||
});
|
||
|
||
return {
|
||
props: {
|
||
allPublishers
|
||
},
|
||
};
|
||
};
|