Merge commit 'dcf42df20bebc491183f2c069c65536853a84999' into production
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -55,7 +55,7 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"command": "conda activate node && npm run start-env",
|
||||
"command": "conda activate node && npm install && npm run start-env",
|
||||
"env": {
|
||||
"APP_ENV": "development.devserver"
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -1,29 +1,29 @@
|
||||
import zIndex from "@mui/material/styles/zIndex";
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export default function ConfirmationModal({ isOpen, onClose, onConfirm, message }) {
|
||||
//export default function ConfirmationModal({ isOpen, onClose, onConfirm, message })
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="opacity-100 fixed inset-0 flex items-center justify-center z-1002" >
|
||||
<div className="bg-white p-4 rounded-md shadow-lg modal-content" style={{ zIndex: 1002 }}>
|
||||
const modalContent = (
|
||||
<div className="modal-container opacity-100 inset-0 flex items-center justify-center z-1152">
|
||||
<div className="bg-white p-4 rounded-md shadow-lg modal-content">
|
||||
<p className="mb-4">{message}</p>
|
||||
<button
|
||||
className="bg-red-500 text-white px-4 py-2 rounded mr-2"
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<button className="bg-red-500 text-white px-4 py-2 rounded mr-2" onClick={onConfirm}>
|
||||
Потвтрждавам
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-300 px-4 py-2 rounded"
|
||||
onClick={onClose}
|
||||
>
|
||||
<button className="bg-gray-300 px-4 py-2 rounded" onClick={onClose}>
|
||||
Отказвам
|
||||
</button>
|
||||
</div>
|
||||
<div className="fixed inset-0 bg-black opacity-50 modal-overlay" onClick={onClose}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
modalContent,
|
||||
document.getElementById('modal-root')
|
||||
);
|
||||
};
|
||||
// const CustomCalendar = ({ month, year, shifts }) => {
|
||||
// export default function CustomCalendar({ date, shifts }: CustomCalendarProps) {
|
@ -48,9 +48,19 @@ const messages = {
|
||||
// Any other labels you want to translate...
|
||||
};
|
||||
|
||||
const AvCalendar = ({ publisherId, events, selectedDate, cartEvents }) => {
|
||||
|
||||
const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
|
||||
const AvCalendar = ({ publisherId, events, selectedDate, cartEvents, lastPublishedDate }) => {
|
||||
const [editLockedBefore, setEditLockedBefore] = useState(new Date(lastPublishedDate));
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setIsAdmin(await ProtectedRoute.IsInRole(UserRole.ADMIN));
|
||||
} catch (error) {
|
||||
console.error("Failed to check admin role:", error);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
//const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
|
||||
|
||||
const [date, setDate] = useState(new Date());
|
||||
//ToDo: see if we can optimize this
|
||||
@ -227,6 +237,12 @@ const AvCalendar = ({ publisherId, events, selectedDate, cartEvents }) => {
|
||||
//readonly for past dates (ToDo: if not admin)
|
||||
if (!isAdmin) {
|
||||
if (startdate < new Date() || end < new Date() || startdate > end) return;
|
||||
//or if schedule is published (lastPublishedDate)
|
||||
if (editLockedBefore && startdate < editLockedBefore) {
|
||||
toast.error(`Не можете да променяте предпочитанията си за дати преди ${common.getDateFormatedShort(editLockedBefore)}.`, { autoClose: 5000 });
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
// Check if start and end are on the same day
|
||||
if (startdate.toDateString() !== enddate.toDateString()) {
|
||||
|
@ -63,6 +63,7 @@ export default function Layout({ children }) {
|
||||
<div className="">
|
||||
{children}
|
||||
</div>
|
||||
<div id="modal-root"></div> {/* Modal container */}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,34 +19,6 @@ import { useSession } from "next-auth/react"
|
||||
|
||||
// import { Tabs, List } from 'tw-elements'
|
||||
|
||||
// model Publisher {
|
||||
// id String @id @default(cuid())
|
||||
// firstName String
|
||||
// lastName String
|
||||
// email String @unique
|
||||
// phone String?
|
||||
// isActive Boolean @default(true)
|
||||
// isImported Boolean @default(false)
|
||||
// age Int?
|
||||
// availabilities Availability[]
|
||||
// assignments Assignment[]
|
||||
|
||||
// emailVerified DateTime?
|
||||
// accounts Account[]
|
||||
// sessions Session[]
|
||||
// role UserRole @default(USER)
|
||||
// desiredShiftsPerMonth Int @default(4)
|
||||
// isMale Boolean @default(true)
|
||||
// isNameForeign Boolean @default(false)
|
||||
|
||||
// familyHeadId String? // Optional familyHeadId for each family member
|
||||
// familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id])
|
||||
// familyMembers Publisher[] @relation("FamilyMember")
|
||||
// type PublisherType @default(Publisher)
|
||||
// Town String?
|
||||
// Comments String?
|
||||
// }
|
||||
|
||||
Array.prototype.groupBy = function (prop) {
|
||||
return this.reduce(function (groups, item) {
|
||||
const val = item[prop]
|
||||
@ -59,9 +31,11 @@ Array.prototype.groupBy = function (prop) {
|
||||
export default function PublisherForm({ item, me }) {
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession()
|
||||
const [congregations, setCongregations] = useState([]);
|
||||
|
||||
const urls = {
|
||||
apiUrl: "/api/data/publishers/",
|
||||
congregationsUrl: "/api/data/congregations",
|
||||
indexUrl: session?.user?.role == UserRole.ADMIN ? "/cart/publishers" : "/dash"
|
||||
}
|
||||
console.log("urls.indexUrl: " + urls.indexUrl);
|
||||
@ -72,6 +46,9 @@ export default function PublisherForm({ item, me }) {
|
||||
const h = (await import("../../src/helpers/const.js")).default;
|
||||
//console.log("fetchModules: " + JSON.stringify(h));
|
||||
setHelper(h);
|
||||
|
||||
const response = await axiosInstance.get(urls.congregationsUrl);
|
||||
setCongregations(response.data);
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchModules();
|
||||
@ -113,15 +90,17 @@ export default function PublisherForm({ item, me }) {
|
||||
publisher.availabilities = undefined;
|
||||
publisher.assignments = undefined;
|
||||
|
||||
let { familyHeadId, userId, ...rest } = publisher;
|
||||
let { familyHeadId, userId, congregationId, ...rest } = publisher;
|
||||
// Set the familyHead relation based on the selected head
|
||||
const familyHeadRelation = familyHeadId ? { connect: { id: familyHeadId } } : { disconnect: true };
|
||||
const userRel = userId ? { connect: { id: userId } } : { disconnect: true };
|
||||
const congregationRel = congregationId ? { connect: { id: parseInt(congregationId) } } : { disconnect: true };
|
||||
// Return the new state without familyHeadId and with the correct familyHead relation
|
||||
rest = {
|
||||
...rest,
|
||||
familyHead: familyHeadRelation,
|
||||
user: userRel
|
||||
user: userRel,
|
||||
congregation: congregationRel
|
||||
};
|
||||
|
||||
try {
|
||||
@ -242,10 +221,32 @@ export default function PublisherForm({ item, me }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* language preference */}
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="locale">Език (в разработка)</label>
|
||||
<select id="locale" name="locale" value={publisher.locale} onChange={handleChange} className="select textbox" placeholder="Език" autoFocus >
|
||||
<option value="bg" >Български</option>
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Руски</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="town">Град</label>
|
||||
<input type="text" id="town" name="town" value={publisher.town} onChange={handleChange} className="textbox" placeholder="Град" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="congregationId">Сбор</label>
|
||||
<select id="congregationId" name="congregationId" value={publisher.congregationId} onChange={handleChange} className="select textbox" placeholder="Община" autoFocus >
|
||||
<option value="">Избери сбор</option>
|
||||
{congregations.map((congregation) => (
|
||||
<option key={congregation.id} value={congregation.id}>
|
||||
{congregation.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* notifications */}
|
||||
<div className="mb-6 p-4 border border-gray-300 rounded-lg">
|
||||
|
@ -127,7 +127,7 @@ function PublisherSearchBox({ id, selectedId, onChange, isFocused, filterDate, s
|
||||
) : null}
|
||||
{showList ? (
|
||||
// Display only clickable list of all publishers
|
||||
<ul className="absolute bg-white border border-gray-300 w-full z-10">
|
||||
<ul className="absolute bg-white border border-gray-300 w-full z-10 overflow-y-auto">
|
||||
{publishers.map((publisher) => (
|
||||
<li key={publisher.id}
|
||||
className="p-2 cursor-pointer hover:bg-gray-200"
|
||||
@ -136,8 +136,9 @@ function PublisherSearchBox({ id, selectedId, onChange, isFocused, filterDate, s
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -56,11 +56,11 @@ export const authOptions: NextAuthOptions = {
|
||||
keyId: process.env.APPLE_KEY_ID,
|
||||
}
|
||||
}),
|
||||
// AzureADProvider({
|
||||
// clientId: process.env.AZURE_AD_CLIENT_ID,
|
||||
// clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
|
||||
// tenantId: process.env.AZURE_AD_TENANT_ID,
|
||||
// }),
|
||||
AzureADProvider({
|
||||
clientId: process.env.AZURE_AD_CLIENT_ID,
|
||||
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
|
||||
tenantId: process.env.AZURE_AD_TENANT_ID,
|
||||
}),
|
||||
CredentialsProvider({
|
||||
id: 'credentials',
|
||||
// The name to display on the sign in form (e.g. 'Sign in with...')
|
||||
@ -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)
|
@ -106,6 +106,7 @@ export default async function handler(req, res) {
|
||||
},
|
||||
data: {
|
||||
publisherId: userId,
|
||||
originalPublisherId: originalPublisher.id,
|
||||
publicGuid: null, // if this exists, we consider the request open
|
||||
isConfirmed: true
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { getToken } from "next-auth/jwt";
|
||||
import { authOptions } from './auth/[...nextauth]'
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { DayOfWeek, AvailabilityType, UserRole } from '@prisma/client';
|
||||
import { DayOfWeek, AvailabilityType, UserRole, EventLogType } from '@prisma/client';
|
||||
const common = require('../../src/helpers/common');
|
||||
const dataHelper = require('../../src/helpers/data');
|
||||
const subq = require('../../prisma/bl/subqueries');
|
||||
@ -11,6 +11,7 @@ import { addMinutes } from 'date-fns';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { all } from "axios";
|
||||
import { logger } from "src/helpers/common";
|
||||
|
||||
/**
|
||||
*
|
||||
@ -360,7 +361,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({
|
||||
@ -821,10 +823,68 @@ async function replaceInAssignment(oldPublisherId, newPublisherId, shiftId) {
|
||||
},
|
||||
data: {
|
||||
publisherId: newPublisherId,
|
||||
originalPublisherId: oldPublisherId,
|
||||
isConfirmed: false,
|
||||
isBySystem: true,
|
||||
isMailSent: false
|
||||
}
|
||||
});
|
||||
|
||||
// log the event
|
||||
let shift = await prisma.shift.findUnique({
|
||||
where: {
|
||||
id: shiftId
|
||||
},
|
||||
|
||||
include: {
|
||||
cartEvent: {
|
||||
select: {
|
||||
location: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
assignments: {
|
||||
include: {
|
||||
publisher: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let publishers = await prisma.publisher.findMany({
|
||||
where: {
|
||||
id: { in: [oldPublisherId, newPublisherId] }
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true
|
||||
}
|
||||
});
|
||||
let originalPublisher = publishers.find(p => p.id == oldPublisherId);
|
||||
let newPublisher = publishers.find(p => p.id == newPublisherId);
|
||||
let eventLog = await prisma.eventLog.create({
|
||||
data: {
|
||||
date: new Date(),
|
||||
publisher: { connect: { id: oldPublisherId } },
|
||||
shift: { connect: { id: shiftId } },
|
||||
type: EventLogType.AssignmentReplacementManual,
|
||||
content: "Заместване въведено от " + originalPublisher.firstName + " " + originalPublisher.lastName + ". Ще го замества " + newPublisher.firstName + " " + newPublisher.lastName + "."
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("User: " + originalPublisher.email + " replaced his assignment for " + shift.cartEvent.location.name + " " + shift.startTime.toISOString() + " with " + newPublisher.firstName + " " + newPublisher.lastName + "<" + newPublisher.email + ">. EventLogId: " + eventLog.id + "");
|
||||
|
||||
|
||||
return result;
|
||||
}
|
@ -74,6 +74,13 @@ export default function SignIn({ csrfToken }) {
|
||||
src="https://authjs.dev/img/providers/apple.svg" className="mr-2" />
|
||||
Влез чрез Apple
|
||||
</button> */}
|
||||
{/* microsoft */}
|
||||
{/* <button onClick={() => signIn('azure-ad', { callbackUrl: '/' })}
|
||||
className="mt-4 flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
<img loading="lazy" height="24" width="24" alt="Microsoft logo"
|
||||
src="https://authjs.dev/img/providers/azure-ad.svg" className="mr-2" />
|
||||
Влез чрез Microsoft
|
||||
</button> */}
|
||||
</div>
|
||||
<div className="w-full max-w-xs mt-8 mb-8">
|
||||
<hr className="border-t border-gray-300" />
|
||||
|
@ -4,6 +4,7 @@ import Layout from "../../../components/layout";
|
||||
import LocationCard from "../../../components/location/LocationCard";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import CongregationCRUD from "../publishers/congregationCRUD";
|
||||
interface IProps {
|
||||
item: Location;
|
||||
}
|
||||
@ -32,6 +33,7 @@ function LocationsPage({ items = [] }: IProps) {
|
||||
</a>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
<CongregationCRUD />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
103
pages/cart/publishers/congregationCRUD.tsx
Normal file
103
pages/cart/publishers/congregationCRUD.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
// a simple CRUD componenet for congregations for admins
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import toast from 'react-hot-toast';
|
||||
import Layout from '../../../components/layout';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import { UserRole } from '@prisma/client';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function CongregationCRUD() {
|
||||
const [congregations, setCongregations] = useState([]);
|
||||
const [newCongregation, setNewCongregation] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
const fetchCongregations = async () => {
|
||||
try {
|
||||
const { data: congregationsData } = await axiosInstance.get(`/api/data/congregations`);
|
||||
setCongregations(congregationsData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const addCongregation = async () => {
|
||||
try {
|
||||
await axiosInstance.post(`/api/data/congregations`, { name: newCongregation, address: "" });
|
||||
toast.success('Успешно добавен сбор');
|
||||
setNewCongregation('');
|
||||
fetchCongregations();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCongregation = async (id) => {
|
||||
try {
|
||||
await axiosInstance.delete(`/api/data/congregations/${id}`);
|
||||
toast.success('Успешно изтрит сбор');
|
||||
fetchCongregations();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchCongregations();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]}>
|
||||
<div className="h-5/6 grid place-items-start px-4 pt-8">
|
||||
<div className="flex flex-col w-full px-4">
|
||||
<h1 className="text-2xl font-bold text-center">Сборове</h1>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={newCongregation}
|
||||
onChange={(e) => setNewCongregation(e.target.value)}
|
||||
placeholder="Име на сбор"
|
||||
className="px-4 py-2 rounded-md border border-gray-300"
|
||||
/>
|
||||
<button
|
||||
onClick={addCongregation}
|
||||
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Добави
|
||||
</button>
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Име</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{congregations.map((congregation) => (
|
||||
<tr key={congregation.id}>
|
||||
<td>{congregation.name}</td>
|
||||
<td className='right'>
|
||||
{/* <button
|
||||
onClick={() => router.push(`/cart/publishers/congregation/${congregation.id}`)}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Преглед
|
||||
</button> */}
|
||||
<button
|
||||
onClick={() => deleteCongregation(congregation.id)}
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Изтрий
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
@ -8,11 +8,13 @@ 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";
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import ConfirmationModal from '../../../components/ConfirmationModal';
|
||||
import { relative } from "path";
|
||||
|
||||
|
||||
|
||||
@ -164,7 +166,7 @@ function PublishersPage({ publishers = [] }: IProps) {
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
|
||||
<div className="">
|
||||
<div className="mx-auto ">
|
||||
<div className="flex items-center justify-center space-x-4 m-4">
|
||||
<div className="flex justify-center m-4">
|
||||
<a href="/cart/publishers/new" className="btn">Добави вестител</a>
|
||||
@ -195,23 +197,24 @@ function PublishersPage({ publishers = [] }: IProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div name="filters" className="flex items-center justify-center space-x-4 m-4 sticky top-4 z-10 bg-gray-100 p-2">
|
||||
<label htmlFor="filter">Filter:</label>
|
||||
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
|
||||
className="border border-gray-300 rounded-md px-2 py-1"
|
||||
/>
|
||||
|
||||
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
|
||||
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
|
||||
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
|
||||
className="form-checkbox text-indigo-600"
|
||||
<div className="z-60 sticky top-0" style={{ zIndex: 60, position: relative }}>
|
||||
<div name="filters" className="flex items-center justify-center space-x-4 m-4 bg-gray-100 p-2" >
|
||||
<label htmlFor="filter">Filter:</label>
|
||||
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
|
||||
className="border border-gray-300 rounded-md px-2 py-1"
|
||||
/>
|
||||
<span className="ml-2">само без смени</span>
|
||||
</label>
|
||||
|
||||
<span id="filter-info" className="ml-4">{publishers.length} от {publishers.length} вестителя</span>
|
||||
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
|
||||
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
|
||||
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
|
||||
className="form-checkbox text-indigo-600"
|
||||
/>
|
||||
<span className="ml-2">само без смени</span>
|
||||
</label>
|
||||
|
||||
<span id="filter-info" className="ml-4">{publishers.length} от {publishers.length} вестителя</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-4 z-0">
|
||||
{renderPublishers()}
|
||||
</div>
|
||||
@ -226,9 +229,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: {
|
||||
|
@ -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");
|
||||
|
||||
|
@ -1,62 +1,262 @@
|
||||
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';
|
||||
|
||||
function ContactsPage({ allPublishers }) {
|
||||
|
||||
const currentMonth = new Date().getMonth();
|
||||
const [selectedMonth, setSelectedMonth] = useState(currentMonth + 1);
|
||||
const isMounted = useRef(false);
|
||||
|
||||
// const data = require('../../src/helpers/data');
|
||||
|
||||
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('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">{publishers.length} участника с предпочитания за месеца (от {allPublishers.length} )</h5>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Търси по име, имейл или телефон..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="border border-gray-300 rounded-md px-2 py-2 mb-4 w-full text-base md:text-sm"
|
||||
/>
|
||||
<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">Име</th>
|
||||
<th className="border-b font-medium p-4 pt-0 pb-3">Възможности</th>
|
||||
<th className="border-b font-medium p-4 pt-0 pb-3">Участия</th>
|
||||
<th className="border-b font-medium p-4 pt-0 pb-3">Последно влизане</th>
|
||||
<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((allPub) => {
|
||||
// Find the publisher in the publishers collection to access statistics
|
||||
const pub = publishers.find(publisher => publisher.id === allPub.id);
|
||||
|
||||
{filteredPublishers.map((pub, i) => {
|
||||
return (
|
||||
<tr key={allPub.id}>
|
||||
<td className="border-b p-4 pl-8" title={allPub.lastUpdate}>{allPub.firstName} {allPub.lastName}</td>
|
||||
<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">
|
||||
<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 || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
|
||||
</span>
|
||||
{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">
|
||||
@ -73,16 +273,15 @@ function ContactsPage({ publishers, allPublishers }) {
|
||||
) : (
|
||||
<>
|
||||
<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="участия този месец">
|
||||
{allPub.currentMonthAssignments || 0}
|
||||
{pub.currentMonthAssignments}
|
||||
</span>
|
||||
<span className="badge py-1 px-2 rounded-md text-xs bg-gray-300 text-gray-500" title="участия миналия месец">
|
||||
{allPub.previousMonthAssignments || 0}
|
||||
{pub.previousMonthAssignments}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -90,6 +289,22 @@ function ContactsPage({ publishers, allPublishers }) {
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
@ -106,132 +321,16 @@ 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;
|
||||
|
||||
|
||||
const allPublishers = await data.getAllPublishersWithStatistics(new Date());
|
||||
//merge first and last name
|
||||
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);
|
||||
publisher.name = `${publisher.firstName} ${publisher.lastName}`;
|
||||
});
|
||||
|
||||
// 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));
|
||||
|
||||
|
||||
return {
|
||||
props: {
|
||||
publishers,
|
||||
allPublishers,
|
||||
allPublishers
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -25,7 +25,17 @@ export default function EventLogList() {
|
||||
useEffect(() => {
|
||||
const fetchLocations = async () => {
|
||||
try {
|
||||
const { data: eventLogsData } = await axiosInstance.get(`/api/data/prisma/eventLog?where={"type":"${EventLogType.AssignmentReplacementAccepted}"}&include={"publisher":{"select":{"firstName":true,"lastName":true}},"shift":{"include":{"assignments":{"include":{"publisher":{"select":{"firstName":true,"lastName":true}}}}}}}`);
|
||||
const { data: eventLogsDataold } = await axiosInstance.get(`/api/data/prisma/eventLog?where={"type":"${EventLogType.AssignmentReplacementAccepted}"}&include={"publisher":{"select":{"firstName":true,"lastName":true}},"shift":{"include":{"assignments":{"include":{"publisher":{"select":{"firstName":true,"lastName":true}}}}}}}`);
|
||||
|
||||
// const where = encodeURIComponent(`{OR: [{type: "${EventLogType.AssignmentReplacementAccepted}"}, {type: "${EventLogType.AssignmentReplacementManual}"}]}`);
|
||||
const where = encodeURIComponent(JSON.stringify({
|
||||
OR: [
|
||||
{ type: EventLogType.AssignmentReplacementAccepted },
|
||||
{ type: EventLogType.AssignmentReplacementManual }
|
||||
]
|
||||
}));
|
||||
|
||||
const { data: eventLogsData } = await axiosInstance.get(`/api/data/prisma/eventLog?where=${where}&include={"publisher":{"select":{"firstName":true,"lastName":true}},"shift":{"include":{"assignments":{"include":{"publisher":{"select":{"firstName":true,"lastName":true}}}}}}}`);
|
||||
|
||||
setEventLog(eventLogsData);
|
||||
|
||||
@ -40,6 +50,9 @@ export default function EventLogList() {
|
||||
fetchLocations();
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||
@ -67,15 +80,18 @@ export default function EventLogList() {
|
||||
<table className="w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left">На</th>
|
||||
<th className="px-4 py-2 text-left">От</th>
|
||||
<th className="px-4 py-2 text-left">Дата</th>
|
||||
<th className="px-4 py-2 text-left">Смяна</th>
|
||||
<th className="px-4 py-2 text-left">Действия</th>
|
||||
<th className="px-4 py-2 text-left">Събитие</th>
|
||||
{/* <th className="px-4 py-2 text-left">Действия</th> */}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!showOpenRequests && (eventLogs.map((event) => (
|
||||
<tr key={event.id}>
|
||||
<td className="border px-2 py-2">{new Date(event.date).toLocaleString('bg')}</td>
|
||||
<td className="border px-2 py-2">{event.publisher.firstName + " " + event.publisher.lastName}</td>
|
||||
<td className="border px-2 py-2">{new Date(event.shift?.startTime).toLocaleString('bg')}</td>
|
||||
<td className="border px-2 py-2">
|
||||
@ -86,17 +102,18 @@ export default function EventLogList() {
|
||||
<td className="border px-2 py-2">
|
||||
{event.content}
|
||||
</td>
|
||||
<td className="border px-4 py-2">
|
||||
{/* <td className="border px-4 py-2">
|
||||
<button
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
|
||||
Изтрий
|
||||
</button>
|
||||
</td>
|
||||
</td> */}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
{showOpenRequests && (requestedAssignments.map((assignment) => (
|
||||
<tr key={assignment.id}>
|
||||
<td className="border px-2 py-2">{new Date(assignment.date).toLocaleString('bg')}</td>
|
||||
<td className="border px-2 py-2">{assignment.publisher.firstName + " " + assignment.publisher.lastName}</td>
|
||||
<td className="border px-2 py-2">{new Date(assignment.shift.startTime).toLocaleString('bg')}</td>
|
||||
<td className="border px-2 py-2">
|
||||
@ -104,12 +121,13 @@ export default function EventLogList() {
|
||||
<div key={ass.id}>{ass.publisher.firstName + " " + ass.publisher.lastName}</div>
|
||||
))}
|
||||
</td>
|
||||
<td className="border px-4 py-2">
|
||||
{/* <td className="border px-4 py-2">
|
||||
<button
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Изтрий
|
||||
</button>
|
||||
</td>
|
||||
</td> */}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
|
@ -21,8 +21,10 @@ import CartEventForm from "components/cartevent/CartEventForm";
|
||||
interface IProps {
|
||||
initialItems: Availability[];
|
||||
initialUserId: string;
|
||||
cartEvents: any;
|
||||
lastPublishedDate: Date;
|
||||
}
|
||||
export default function IndexPage({ initialItems, initialUserId, cartEvents }: IProps) {
|
||||
export default function IndexPage({ initialItems, initialUserId, cartEvents, lastPublishedDate }: IProps) {
|
||||
const { data: session } = useSession();
|
||||
const [userName, setUserName] = useState(session?.user?.name);
|
||||
const [userId, setUserId] = useState(initialUserId);
|
||||
@ -79,7 +81,7 @@ export default function IndexPage({ initialItems, initialUserId, cartEvents }: I
|
||||
<div className="text-center font-bold pb-3 xs:pb-1">
|
||||
<PublisherInlineForm publisherId={userId} />
|
||||
</div>
|
||||
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} cartEvents={cartEvents} />
|
||||
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} cartEvents={cartEvents} lastPublishedDate={lastPublishedDate} />
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
@ -229,6 +231,7 @@ export const getServerSideProps = async (context) => {
|
||||
startTime: updatedItem.shift.startTime.toISOString(),
|
||||
endTime: updatedItem.shift.endTime.toISOString()
|
||||
};
|
||||
updatedItem.isPublished = updatedItem.shift.isPublished;
|
||||
}
|
||||
|
||||
return updatedItem;
|
||||
@ -239,6 +242,7 @@ export const getServerSideProps = async (context) => {
|
||||
console.log("First availability startTime: " + items[0]?.startTime);
|
||||
console.log("First availability startTime: " + items[0]?.startTime.toLocaleString());
|
||||
|
||||
|
||||
const prisma = common.getPrismaClient();
|
||||
let cartEvents = await prisma.cartEvent.findMany({
|
||||
where: {
|
||||
@ -253,11 +257,23 @@ export const getServerSideProps = async (context) => {
|
||||
}
|
||||
});
|
||||
cartEvents = common.convertDatesToISOStrings(cartEvents);
|
||||
const lastPublishedDate = (await prisma.shift.findFirst({
|
||||
where: {
|
||||
isPublished: true,
|
||||
},
|
||||
select: {
|
||||
endTime: true,
|
||||
},
|
||||
orderBy: {
|
||||
endTime: 'desc'
|
||||
}
|
||||
})).endTime;
|
||||
return {
|
||||
props: {
|
||||
initialItems: items,
|
||||
userId: sessionServer?.user.id,
|
||||
cartEvents: cartEvents,
|
||||
lastPublishedDate: lastPublishedDate.toISOString(),
|
||||
// messages: (await import(`../content/i18n/${context.locale}.json`)).default
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,33 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `Assignment`
|
||||
ADD COLUMN `originalPublisherId` VARCHAR(191) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Message` ADD COLUMN `publicUntil` DATETIME(3) NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Publisher`
|
||||
ADD COLUMN `congregationId` INTEGER NULL,
|
||||
ADD COLUMN `locale` VARCHAR(191) NULL DEFAULT 'bg';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `Report` ADD COLUMN `comments` VARCHAR(191) NULL;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `Congregation` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`address` VARCHAR(191) NOT NULL,
|
||||
`isActive` BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Publisher`
|
||||
ADD CONSTRAINT `Publisher_congregationId_fkey` FOREIGN KEY (`congregationId`) REFERENCES `Congregation` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `Assignment`
|
||||
ADD CONSTRAINT `Assignment_originalPublisherId_fkey` FOREIGN KEY (`originalPublisherId`) REFERENCES `Publisher` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -0,0 +1,19 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `EventLog`
|
||||
MODIFY `type` ENUM(
|
||||
'AssignmentReplacementManual', 'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted'
|
||||
) NOT NULL;
|
||||
|
||||
INSERT INTO
|
||||
`Congregation`
|
||||
VALUES (1, 'Перник', '', 1),
|
||||
(2, 'София Люлин', '', 1),
|
||||
(3, 'София Юг', '', 1),
|
||||
(4, 'София Надежда', '', 1),
|
||||
(5, 'София Руски', '', 1),
|
||||
(6, 'София Факултета', '', 1),
|
||||
(7, 'София Изток', '', 1),
|
||||
(8, 'София Младост', '', 1),
|
||||
(9, 'София Английски', '', 1),
|
||||
(10, 'Ботевград', '', 1),
|
||||
(11, 'София Дружба', '', 1);
|
@ -124,6 +124,18 @@ model Publisher {
|
||||
EventLog EventLog[]
|
||||
lastLogin DateTime?
|
||||
pushSubscription Json?
|
||||
originalAssignments Assignment[] @relation("OriginalPublisher")
|
||||
congregation Congregation? @relation(fields: [congregationId], references: [id])
|
||||
congregationId Int?
|
||||
locale String? @default("bg")
|
||||
}
|
||||
|
||||
model Congregation {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
address String
|
||||
isActive Boolean @default(true)
|
||||
publishers Publisher[]
|
||||
}
|
||||
|
||||
model Availability {
|
||||
@ -181,23 +193,25 @@ model Shift {
|
||||
//date DateTime
|
||||
reportId Int? @unique
|
||||
Report Report? @relation(fields: [reportId], references: [id])
|
||||
isPublished Boolean @default(false) //NEW v1.0.1
|
||||
isPublished Boolean @default(false)
|
||||
EventLog EventLog[]
|
||||
|
||||
@@map("Shift")
|
||||
}
|
||||
|
||||
model Assignment {
|
||||
id Int @id @default(autoincrement())
|
||||
shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade)
|
||||
shiftId Int
|
||||
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
||||
publisherId String
|
||||
isBySystem Boolean @default(false) // if no availability for it, when importing previous schedules
|
||||
isConfirmed Boolean @default(false)
|
||||
isWithTransport Boolean @default(false)
|
||||
isMailSent Boolean @default(false)
|
||||
publicGuid String? @unique
|
||||
id Int @id @default(autoincrement())
|
||||
shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade)
|
||||
shiftId Int
|
||||
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
||||
publisherId String
|
||||
isBySystem Boolean @default(false) // if no availability for it, when importing previous schedules
|
||||
isConfirmed Boolean @default(false)
|
||||
isWithTransport Boolean @default(false)
|
||||
isMailSent Boolean @default(false)
|
||||
publicGuid String? @unique
|
||||
originalPublisherId String? // New field to store the original publisher id when the assignment is replaced
|
||||
originalPublisher Publisher? @relation("OriginalPublisher", fields: [originalPublisherId], references: [id])
|
||||
|
||||
@@map("Assignment")
|
||||
}
|
||||
@ -237,6 +251,7 @@ model Report {
|
||||
|
||||
experienceInfo String? @db.LongText
|
||||
type ReportType @default(ServiceReport)
|
||||
comments String?
|
||||
|
||||
@@map("Report")
|
||||
}
|
||||
@ -258,9 +273,11 @@ model Message {
|
||||
isRead Boolean @default(false)
|
||||
isPublic Boolean @default(false)
|
||||
type MessageType @default(Email)
|
||||
publicUntil DateTime?
|
||||
}
|
||||
|
||||
enum EventLogType {
|
||||
AssignmentReplacementManual
|
||||
AssignmentReplacementRequested
|
||||
AssignmentReplacementAccepted
|
||||
SentEmail
|
||||
|
@ -346,7 +346,6 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log(`getting publishers for date: ${filterDate}, isExactTime: ${isExactTime}, isForTheMonth: ${isForTheMonth}`);
|
||||
console.log`whereClause: ${JSON.stringify(whereClause)}`
|
||||
//include availabilities if flag is true
|
||||
@ -420,7 +419,8 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
|
||||
if (isWithStats) {
|
||||
pub.currentMonthAvailability = pub.availabilities?.filter(avail => {
|
||||
// return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= monthInfo.lastSunday;
|
||||
return avail.startTime >= monthInfo.firstMonday && (noEndDateFilter || avail.startTime <= monthInfo.lastSunday);
|
||||
return (avail.startTime >= monthInfo.firstMonday && (noEndDateFilter || avail.startTime <= monthInfo.lastSunday))
|
||||
|| (avail.dayOfMonth == null); // include repeating availabilities
|
||||
})
|
||||
pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length;
|
||||
// pub.currentMonthAvailabilityDaysCount += pub.availabilities.filter(avail => {
|
||||
@ -472,13 +472,13 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
|
||||
}
|
||||
|
||||
//ToDo: refactor this function
|
||||
async function getAllPublishersWithStatistics(filterDate) {
|
||||
async function getAllPublishersWithStatistics(filterDate, noEndDateFilter = false) {
|
||||
|
||||
const prisma = common.getPrismaClient();
|
||||
const monthInfo = common.getMonthDatesInfo(new Date(filterDate));
|
||||
const dateStr = new Date(monthInfo.firstMonday).toISOString().split('T')[0];
|
||||
|
||||
let publishers = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', dateStr, false, true, true, true, true);
|
||||
let publishers = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', dateStr, false, true, noEndDateFilter, 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`);
|
||||
@ -531,6 +531,7 @@ async function getAllPublishersWithStatistics(filterDate) {
|
||||
desiredShiftsPerMonth: true,
|
||||
lastLogin: true,
|
||||
type: true,
|
||||
pushSubscription: true,
|
||||
assignments: {
|
||||
select: {
|
||||
id: true,
|
||||
@ -557,6 +558,14 @@ async function getAllPublishersWithStatistics(filterDate) {
|
||||
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
firstName: 'asc', // or 'desc' if you want descending order
|
||||
},
|
||||
{
|
||||
lastName: 'asc', // or 'desc' if you want descending order
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -566,6 +575,8 @@ async function getAllPublishersWithStatistics(filterDate) {
|
||||
|
||||
|
||||
allPublishers.forEach(publisher => {
|
||||
publisher.isPushActive = publisher.pushSubscription ? true : false;
|
||||
delete publisher.pushSubscription
|
||||
// Use helper functions to calculate and assign assignment counts
|
||||
publisher.currentMonthAssignments = countAssignments(publisher.assignments, monthInfo.firstMonday, monthInfo.lastSunday);
|
||||
publisher.previousMonthAssignments = countAssignments(publisher.assignments, prevMonthInfo.firstMonday, prevMonthInfo.lastSunday);
|
||||
@ -577,6 +588,11 @@ async function getAllPublishersWithStatistics(filterDate) {
|
||||
// common.convertDatesToISOStrings(publisher.availabilities); //ToDo fix the function to work with this sctucture and use it
|
||||
});
|
||||
|
||||
//debug
|
||||
const pubsWithAvailabilities = allPublishers.filter(publisher => publisher.availabilities.length > 0);
|
||||
const pubsWithAvailabilities2 = publishers.filter(publisher => publisher.currentMonthAvailabilityHoursCount > 0);
|
||||
console.log(`publishers: ${allPublishers.length}, publishers with availabilities: ${pubsWithAvailabilities.length}, publishers with availabilities2: ${pubsWithAvailabilities2.length}`);
|
||||
|
||||
|
||||
//merge allPublishers with publishers
|
||||
allPublishers = allPublishers.map(pub => {
|
||||
|
@ -37,12 +37,30 @@ iframe {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse; /* Newest first if new elements are prepended */
|
||||
}
|
||||
.modal-content {
|
||||
z-index: 1002; /* or a higher value if necessary */
|
||||
/* z-index: 1002; or a higher value if necessary */
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%); /* Center the modal */
|
||||
z-index: 1051; /* Higher z-index than overlay */
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
z-index: 1051; /* High z-index */
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
z-index: 1001;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
|
||||
z-index: 1050; /* High z-index */
|
||||
}
|
||||
.publisher {
|
||||
position: relative;
|
||||
|
Reference in New Issue
Block a user