From db17572ea6cce2b675af844e0c8fbfdaf289e0fb Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Thu, 2 May 2024 01:50:09 +0300 Subject: [PATCH 01/10] add statsFilters --- pages/cart/publishers/stats.tsx | 297 ++++++++++++++++---------------- 1 file changed, 144 insertions(+), 153 deletions(-) diff --git a/pages/cart/publishers/stats.tsx b/pages/cart/publishers/stats.tsx index 2e8b558..70d503d 100644 --- a/pages/cart/publishers/stats.tsx +++ b/pages/cart/publishers/stats.tsx @@ -1,62 +1,173 @@ -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import Layout from "../../../components/layout"; import ProtectedRoute from '../../../components/protectedRoute'; -import { Prisma, UserRole } from '@prisma/client'; +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'; -// const data = require('../../src/helpers/data'); +function ContactsPage({ allPublishers }) { + + const currentMonth = new Date().getMonth(); + const [selectedMonth, setSelectedMonth] = useState(""); + const isMounted = useRef(false); -function ContactsPage({ publishers, allPublishers }) { const [searchQuery, setSearchQuery] = useState(''); + const [publisherType, setPublisherType] = useState(''); + const [publishers, setPublishers] = useState(allPublishers); + const [pubWithAssignmentsCount, setPubWithAssignmentsCount] = useState(0); + const [filteredPublishers, setFilteredPublishers] = useState(allPublishers); - const filteredPublishers = allPublishers.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()) - ); + const [sortField, setSortField] = useState('firstName'); + const [sortOrder, setSortOrder] = useState('asc'); + + const months = common.getMonthNames(); + const subsetMonths = Array.from({ length: 7 }, (_, i) => { + const monthIndex = (currentMonth - 3 + i + 12) % 12; // Adjust for year wrap-around + return { + name: months[monthIndex], + index: monthIndex + 1 + }; + }); + const datesOn15th = Array.from({ length: 7 }, (_, i) => new Date(new Date().getFullYear(), new Date().getMonth() - 3 + i, 15)) + .map(date => date.toISOString().split('T')[0]); + + + function handleSort(field) { + const order = sortField === field && sortOrder === 'asc' ? 'desc' : 'asc'; + setSortField(field); + setSortOrder(order); + } + + useEffect(() => { + let filtered = allPublishers.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) + ); + + 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); + }, [searchQuery, publisherType, sortField, sortOrder, allPublishers]); + + 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()}`); + setPublishers(response.data); + setFilteredPublishers(response.data); + + setPubWithAssignmentsCount(response.data.filter(publisher => publisher.currentMonthAvailabilityHoursCount && publisher.currentMonthAvailabilityHoursCount > 0).length); + } 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 (

Статистика

-
{publishers.length} участника с предпочитания за месеца (от {allPublishers.length} )
- setSearchQuery(e.target.value)} - className="border border-gray-300 rounded-md px-2 py-2 mb-4 w-full text-base md:text-sm" - /> +
{pubWithAssignmentsCount} участника с предпочитания за месеца (от {allPublishers.length} )
+
+ 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 */} + + {/* Publisher type dropdown */} + +
- - - - + + + + - {filteredPublishers.map((allPub) => { + {filteredPublishers.map((pub) => { // Find the publisher in the publishers collection to access statistics - const pub = publishers.find(publisher => publisher.id === allPub.id); + //const pub = publishers.find(publisher => publisher.id === allPub.id); return ( - - + + {/* Display statistics if publisher is found */} {pub ? ( <> - {filteredPublishers.map((pub) => { - // Find the publisher in the publishers collection to access statistics - //const pub = publishers.find(publisher => publisher.id === allPub.id); - + {filteredPublishers.map((pub, i) => { return ( - + {/* Display statistics if publisher is found */} {pub ? ( <> @@ -184,7 +181,6 @@ function ContactsPage({ allPublishers }) { ) : ( <>
ИмеВъзможностиУчастияПоследно влизане handleSort('firstName')}> + Име{renderSortArrow('firstName')} + handleSort('currentMonthAvailabilityDaysCount')}> + Възможности{renderSortArrow('currentMonthAvailabilityDaysCount')} + handleSort('currentMonthAssignments')}> + Участия{renderSortArrow('currentMonthAssignments')} + handleSort('lastLogin')}> + Последно влизане{renderSortArrow('lastLogin')} +
{allPub.firstName} {allPub.lastName}
{pub.firstName} {pub.lastName} - - {pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0} - + {pub.availabilities.length > 0 ? ( + + {pub.currentMonthAvailabilityDaysCount} | {pub.currentMonthAvailabilityHoursCount} + + ) : 0}
@@ -79,10 +190,10 @@ function ContactsPage({ publishers, allPublishers }) {
- {allPub.currentMonthAssignments || 0} + {pub.currentMonthAssignments} - {allPub.previousMonthAssignments || 0} + {pub.previousMonthAssignments}
@@ -106,132 +217,12 @@ function ContactsPage({ publishers, allPublishers }) { export default ContactsPage; -// Helper functions ToDo: move them to common and replace all implementations with the common ones -function countAssignments(assignments, startTime, endTime) { - return assignments.filter(assignment => - assignment.shift.startTime >= startTime && assignment.shift.startTime <= endTime - ).length; -} - -function convertShiftDates(assignments) { - assignments.forEach(assignment => { - if (assignment.shift && assignment.shift.startTime) { - assignment.shift.startTime = new Date(assignment.shift.startTime).toISOString(); - assignment.shift.endTime = new Date(assignment.shift.endTime).toISOString(); - } - }); -} export const getServerSideProps = async (context) => { - - const prisma = common.getPrismaClient(); - const dateStr = new Date().toISOString().split('T')[0]; - - let publishers = await data.filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin', dateStr, false, true, true, true, true); - - // const axios = await axiosServer(context); - // const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`); - - // api/index?action=filterPublishers&assignments=true&availabilities=true&date=2024-03-14&select=id%2CfirstName%2ClastName%2CisActive%2CdesiredShiftsPerMonth - publishers.forEach(publisher => { - publisher.desiredShiftsPerMonth = publisher.desiredShiftsPerMonth || 0; - publisher.assignments = publisher.assignments || []; - publisher.availabilities = publisher.availabilities || []; - publisher.lastUpdate = publisher.availabilities.reduce((acc, curr) => curr.dateOfEntry > acc ? curr.dateOfEntry : acc, null); - if (publisher.lastUpdate) { - publisher.lastUpdate = common.getDateFormated(publisher.lastUpdate); - } - else { - publisher.lastUpdate = "Няма данни"; - } - //serialize dates in publisher.assignments and publisher.availabilities - publisher.assignments.forEach(assignment => { - if (assignment.shift && assignment.shift.startTime) { - assignment.shift.startTime = assignment.shift.startTime.toISOString(); - assignment.shift.endTime = assignment.shift.endTime.toISOString(); - } - }); - publisher.availabilities.forEach(availability => { - if (availability.startTime) { - availability.startTime = availability.startTime.toISOString(); - availability.endTime = availability.endTime.toISOString(); - if (availability.dateOfEntry) { - availability.dateOfEntry = availability.dateOfEntry.toISOString(); - } - } - }); - publisher.lastLogin = publisher.lastLogin ? publisher.lastLogin.toISOString() : null; - //remove availabilities that isFromPreviousAssignment - publisher.availabilities = publisher.availabilities.filter(availability => !availability.isFromPreviousAssignment); - - - }); - //remove publishers without availabilities - publishers = publishers.filter(publisher => publisher.availabilities.length > 0); - - let allPublishers = await prisma.publisher.findMany({ - select: { - id: true, - firstName: true, - lastName: true, - email: true, - phone: true, - isActive: true, - desiredShiftsPerMonth: true, - lastLogin: true, - assignments: { - select: { - id: true, - shift: { - select: { - startTime: true, - endTime: true, - }, - }, - }, - }, - }, - }); - - - - - let monthInfo, - currentMonthStart, currentMonthEnd, - previousMonthStart, previousMonthEnd; - - monthInfo = common.getMonthDatesInfo(new Date()); - currentMonthStart = monthInfo.firstMonday; - currentMonthEnd = monthInfo.lastSunday; - let prevMnt = new Date(); - prevMnt.setMonth(prevMnt.getMonth() - 1); - monthInfo = common.getMonthDatesInfo(prevMnt); - previousMonthStart = monthInfo.firstMonday; - previousMonthEnd = monthInfo.lastSunday; - - - allPublishers.forEach(publisher => { - // Use helper functions to calculate and assign assignment counts - publisher.currentMonthAssignments = countAssignments(publisher.assignments, currentMonthStart, currentMonthEnd); - publisher.previousMonthAssignments = countAssignments(publisher.assignments, previousMonthStart, previousMonthEnd); - - publisher.lastLogin = publisher.lastLogin ? publisher.lastLogin.toISOString() : null; - // Convert date formats within the same iteration - convertShiftDates(publisher.assignments); - }); - - // Optionally, if you need a transformed list or additional properties, map the publishers - allPublishers = allPublishers.map(publisher => ({ - ...publisher, - // Potentially add more computed properties or transformations here if needed - })); - allPublishers.sort((a, b) => a.firstName.localeCompare(b.firstName) || a.lastName.localeCompare(b.lastName)); - - + const allPublishers = await data.getAllPublishersWithStatistics(new Date()); return { props: { - publishers, - allPublishers, + allPublishers }, }; }; From 17b8adbab893c3906c98b1f6f8a22b4d8b29ae96 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Fri, 3 May 2024 01:41:18 +0300 Subject: [PATCH 02/10] add numbers to stats list --- pages/cart/publishers/stats.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pages/cart/publishers/stats.tsx b/pages/cart/publishers/stats.tsx index 70d503d..b5fa9a0 100644 --- a/pages/cart/publishers/stats.tsx +++ b/pages/cart/publishers/stats.tsx @@ -152,13 +152,10 @@ function ContactsPage({ allPublishers }) {
{pub.firstName} {pub.lastName}{i + 1}. {pub.firstName} {pub.lastName} -
From c7980f46bb35438d4a5b833e1e8f4437b5581bb6 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 4 May 2024 14:30:20 +0300 Subject: [PATCH 03/10] wip stats --- pages/api/index.ts | 3 ++- pages/cart/publishers/stats.tsx | 5 +++-- src/helpers/data.js | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pages/api/index.ts b/pages/api/index.ts index 704e0bc..6dbe2d5 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -355,7 +355,8 @@ export default async function handler(req, res) { res.status(200).json(data); break; case "getAllPublishersWithStatistics": - res.status(200).json(await dataHelper.getAllPublishersWithStatistics(day)); + let noEndDate = common.parseBool(req.query.noEndDate); + res.status(200).json(await dataHelper.getAllPublishersWithStatistics(day, noEndDate)); default: res.status(200).json({ diff --git a/pages/cart/publishers/stats.tsx b/pages/cart/publishers/stats.tsx index b5fa9a0..38e58d4 100644 --- a/pages/cart/publishers/stats.tsx +++ b/pages/cart/publishers/stats.tsx @@ -67,6 +67,7 @@ function ContactsPage({ allPublishers }) { }); } setFilteredPublishers(filtered); + setPubWithAssignmentsCount(filtered.filter(publisher => publisher.currentMonthAvailabilityHoursCount && publisher.currentMonthAvailabilityHoursCount > 0).length); }, [searchQuery, publisherType, sortField, sortOrder, allPublishers]); useEffect(() => { @@ -75,7 +76,7 @@ function ContactsPage({ allPublishers }) { 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()}`); + const response = await axiosInstance.get(`/api/?action=getAllPublishersWithStatistics&date=${filterDate.toISOString()}&noEndDate=false`); setPublishers(response.data); setFilteredPublishers(response.data); @@ -101,7 +102,7 @@ function ContactsPage({ allPublishers }) {

Статистика

-
{pubWithAssignmentsCount} участника с предпочитания за месеца (от {allPublishers.length} )
+
{pubWithAssignmentsCount} участника с предпочитания за месеца (от {filteredPublishers.length} )
Date: Sat, 11 May 2024 16:49:32 +0300 Subject: [PATCH 04/10] language select works in pub form --- _doc/ToDo.md | 3 +++ components/publisher/PublisherForm.js | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/_doc/ToDo.md b/_doc/ToDo.md index d111925..abcbef7 100644 --- a/_doc/ToDo.md +++ b/_doc/ToDo.md @@ -238,3 +238,6 @@ in schedule admin - if a publisher is always pair & family is not in the shift - [] new page to show EventLog (substitutions) [] fix "login as" [] list with open shift replacements (coverMe requests) +[] fix statistics +[] add notification to statistics info +[] fix logins (apple/azure) diff --git a/components/publisher/PublisherForm.js b/components/publisher/PublisherForm.js index fa81b47..51ae652 100644 --- a/components/publisher/PublisherForm.js +++ b/components/publisher/PublisherForm.js @@ -221,6 +221,15 @@ export default function PublisherForm({ item, me }) {
+ {/* language preference */} +
+ + +
From ffe68b492b3b7a9f777074b6587cd82ef7b5ebaa Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 11 May 2024 17:48:42 +0300 Subject: [PATCH 05/10] fix migration (again) --- .../migrations/20240511112711_aded_enum_options/migration.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/migrations/20240511112711_aded_enum_options/migration.sql b/prisma/migrations/20240511112711_aded_enum_options/migration.sql index c8d2dd1..2769eb0 100644 --- a/prisma/migrations/20240511112711_aded_enum_options/migration.sql +++ b/prisma/migrations/20240511112711_aded_enum_options/migration.sql @@ -1,5 +1,5 @@ -- AlterTable -ALTER TABLE `eventlog` +ALTER TABLE `EventLog` MODIFY `type` ENUM( 'AssignmentReplacementManual', 'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted' ) NOT NULL; From e29807540f0077e8a0565dbea736afb46c4137c7 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 11 May 2024 17:57:33 +0300 Subject: [PATCH 06/10] fix server session problem; optimize --- pages/api/auth/[...nextauth].ts | 17 ++----------- pages/cart/publishers/index.tsx | 36 +++++++++++++++++++++++++--- pages/cart/publishers/myschedule.tsx | 4 +++- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 098b95b..6020c99 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -70,21 +70,6 @@ export const authOptions: NextAuthOptions = { password: { label: "Парола", type: "password" } }, async authorize(credentials, req) { - //const user = { id: "1", name: "Администратора", email: "jsmith@example.com" } - //return user - // const res = await fetch("/your/endpoint", { - // method: 'POST', - // body: JSON.stringify(credentials), - // headers: { "Content-Type": "application/json" } - // }) - // const user = await res.json() - - // // If no error and we have user data, return it - // if (res.ok && user) { - // return user - // } - // // Return null if user data could not be retrieved - // return null const users = [ { id: "1", name: "admin", email: "admin@example.com", password: "admin123", role: "ADMIN", static: true }, { id: "2", name: "krasi", email: "krasi@example.com", password: "krasi123", role: "ADMIN", static: true }, @@ -272,6 +257,8 @@ export const authOptions: NextAuthOptions = { verifyRequest: "/auth/verify-request", // (used for check email message) newUser: null // If set, new users will be directed here on first sign in }, + + debug: process.env.NODE_ENV === 'development', } export default NextAuth(authOptions) \ No newline at end of file diff --git a/pages/cart/publishers/index.tsx b/pages/cart/publishers/index.tsx index 896ec40..57d9d35 100644 --- a/pages/cart/publishers/index.tsx +++ b/pages/cart/publishers/index.tsx @@ -8,6 +8,7 @@ import Layout from "../../../components/layout"; import PublisherCard from "../../../components/publisher/PublisherCard"; import axiosInstance from "../../../src/axiosSecure"; import axiosServer from '../../../src/axiosServer'; +const common = require("../../../src/helpers/common"); import toast from "react-hot-toast"; import { levenshteinEditDistance } from "levenshtein-edit-distance"; @@ -226,9 +227,38 @@ export default PublishersPage; //import { set } from "date-fns"; export const getServerSideProps = async (context) => { - const axios = await axiosServer(context); - //ToDo: refactor all axios calls to use axiosInstance and this URL - const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isActive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect'); + // const axios = await axiosServer(context); + // //ToDo: refactor all axios calls to use axiosInstance and this URL + // const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isActive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect'); + //use prisma instead of axios + const prisma = common.getPrismaClient(); + let publishers = await prisma.publisher.findMany({ + select: { + id: true, + firstName: true, + lastName: true, + email: true, + isActive: true, + isTrained: true, + isImported: true, + assignments: { + select: { + shift: { + select: { + startTime: true, + }, + }, + }, + }, + availabilities: { + select: { + startTime: true, + }, + }, + } + }); + publishers = JSON.parse(JSON.stringify(publishers)); + return { props: { diff --git a/pages/cart/publishers/myschedule.tsx b/pages/cart/publishers/myschedule.tsx index 952d9aa..bda1015 100644 --- a/pages/cart/publishers/myschedule.tsx +++ b/pages/cart/publishers/myschedule.tsx @@ -14,6 +14,8 @@ import { useSession, getSession } from 'next-auth/react'; import axiosInstance from 'src/axiosSecure'; import { toast } from 'react-toastify'; import LocalShippingIcon from '@mui/icons-material/LocalShipping'; +import { getServerSession } from 'next-auth'; +import { authOptions } from 'pages/api/auth/[...nextauth]'; export default function MySchedulePage({ assignments }) { @@ -160,7 +162,7 @@ export default function MySchedulePage({ assignments }) { //get future assignments for the current user (session.user.id) export const getServerSideProps = async (context) => { - const session = await getSession(context); + const session = await getServerSession(context.req, context.res, authOptions) context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate"); From e987b0028c496417b4ca7633a661e5b684c2bc87 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 11 May 2024 22:26:29 +0300 Subject: [PATCH 07/10] fix confirmation modal on top of other confirmation modal --- components/ConfirmationModal.tsx | 4 ++-- pages/api/index.ts | 10 ++++------ pages/cart/reports/coverMe.tsx | 32 +++++++++++++++++++++++++------- styles/styles.css | 22 ++++++++++++++++++++-- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/components/ConfirmationModal.tsx b/components/ConfirmationModal.tsx index 787be29..1900155 100644 --- a/components/ConfirmationModal.tsx +++ b/components/ConfirmationModal.tsx @@ -5,8 +5,8 @@ export default function ConfirmationModal({ isOpen, onClose, onConfirm, message if (!isOpen) return null; return ( -
-
+
+

{message}