This commit is contained in:
Dobromir Popov
2024-07-03 16:16:47 +03:00
73 changed files with 2052 additions and 1736 deletions

11
.vscode/launch.json vendored
View File

@ -60,6 +60,17 @@
"APP_ENV": "development.devserver"
}
},
{
"name": "!Run npm DEV (UI REDESIGN)",
"request": "launch",
"type": "node-terminal",
"cwd": "${workspaceFolder}",
"command": "npm run start-env",
"env": {
// "NODE_ENV": "test",
"APP_ENV": "development.devserver"
}
},
{
"name": "Run conda npm TEST",
"request": "launch",

View File

@ -259,7 +259,7 @@ in schedule admin - if a publisher is always pair & family is not in the shift -
[] fix transport UI
[] revise import/export to word
[] allow keyman/scheduler role
[] allow blocking of inputs (different from publishing)
[] allow blocking of inputs (different from publishing) TODO: fix to keep previous occurances when repeating evert week
[] user - add createdAt field
[] FIX insecure logins

View File

@ -0,0 +1,37 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
this.logger = null;
if (typeof window === 'undefined') {
this.logger = require('../src/logger');
}
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
// Log the error to an error reporting service
console.error(error, info);
if (this.logger) {
this.logger.error(`${error}: ${info.componentStack}`);
}
}
render() {
if (this.state.hasError) {
// Render any custom fallback UI
return <h1>Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи. </h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -35,6 +35,7 @@ function PwaManager({ subs }) {
useEffect(() => {
if (isSupported()) {
setNotificationPermission(Notification.permission);
getSubscriptionCount();
}
// Handle Push Notification Subscription
@ -77,6 +78,10 @@ function PwaManager({ subs }) {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.removeEventListener('appinstalled', handleAppInstalled);
};
}, []);
@ -127,6 +132,7 @@ function PwaManager({ subs }) {
throw new Error("Failed to fetch VAPID public key from server.");
}
}
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: common.base64ToUint8Array(vapidPublicKey)
@ -197,6 +203,19 @@ function PwaManager({ subs }) {
}
};
const getSubscriptionCount = async () => {
try {
const response = await fetch('/api/notify?id=' + session.user.id, { method: 'GET' });
if (!response.ok) {
throw new Error('Failed to fetch subscription data.');
}
const result = await response.json();
setSubs(result.subs);
} catch (error) {
console.error('Error fetching subscription data:', error);
}
};
// Function to request push notification permission
const requestNotificationPermission = async (e) => {
e.preventDefault();
@ -243,48 +262,56 @@ function PwaManager({ subs }) {
headers: {
'Content-Type': 'application/json'
},
//sends test notification to the current subscription
// body: JSON.stringify({ subscription })
//sends test notification to all subscriptions of this user
body: JSON.stringify({ id: session.user.id, title: "Тестово уведомление", message: "Това е тестово уведомление" })
body: JSON.stringify(
{
id: session.user.id,
title: "Тестово уведомление",
message: "Това е тестово уведомление",
actions: [{ action: 'test', title: 'Тест', icon: '✅' },
{ action: 'close', title: 'Затвори', icon: '❌' }]
})
});
};
async function sendTestReminder(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
event.preventDefault();
if (!subscription) {
console.error('Web push not subscribed');
return;
}
// async function sendTestReminder(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
// event.preventDefault();
// if (!subscription) {
// console.error('Web push not subscribed');
// return;
// }
await fetch('/api/notify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ broadcast: true, message: "Мили братя, искаме да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'" })
});
}
// await fetch('/api/notify', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({
// broadcast: true,
// message: "Мили братя, искаме да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'"
// })
// });
// }
async function sendTestCoverMe(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
event.preventDefault();
if (!subscription) {
console.error('Web push not subscribed');
return;
}
// async function sendTestCoverMe(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
// event.preventDefault();
// if (!subscription) {
// console.error('Web push not subscribed');
// return;
// }
await fetch('/api/notify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
broadcast: true, message: "Брат ТЕСТ търси заместник за 24-ти май от 10:00 ч. Можеш ли да го покриеш?",
//use fontawesome icons for actions
actions: [{ action: 'covermeaccepted', title: 'Да ', icon: '✅' }]
})
});
}
// await fetch('/api/notify', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({
// id: session.user.id,
// message: "Брат ТЕСТ търси заместник за 24-ти май от 10:00 ч. Можеш ли да го покриеш?",
// //use fontawesome icons for actions
// actions: [{ action: 'covermeaccepted', title: 'Да ', icon: '✅' }]
// })
// });
// }
async function deleteAllSubscriptions(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
event.preventDefault();
@ -358,22 +385,22 @@ function PwaManager({ subs }) {
</div>
{isAdmin &&
<div>
<button
{/* <button
onClick={sendTestReminder}
disabled={!isSubscribed}
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
}`}
>
Broadcast Reminder
</button>
<button
</button> */}
{/* <button
onClick={sendTestCoverMe}
disabled={!isSubscribed}
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${!isSubscribed ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-yellow-500 hover:bg-yellow-600 text-white'
}`}
>
Broadcast CoverMe
</button>
</button> */}
</div>
}
{notificationPermission !== "granted" && (

View File

@ -10,7 +10,7 @@ import { bgBG } from '../x-date-pickers/locales/bgBG';
import { ToastContainer } from 'react-toastify';
const common = require('src/helpers/common');
//todo import Availability type from prisma schema
import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns'; //ToDo obsolete
import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns'; //ToDo obsolete?
import { stat } from 'fs';
const { DateTime, FixedOffsetZone } = require('luxon');

View File

@ -17,11 +17,22 @@ export default function AvailabilityList({ publisher, showNew }) {
const [showAv, setShowAv] = useState(showNew || false);
const [selectedItem, setSelectedItem] = useState(null);
const [items, setItems] = useState(publisher.availabilities); // Convert items prop to state
const [blockedAvailabilityDate, setBlockedAvailabilityDate] = useState(null);
useEffect(() => {
console.log('items set to:', items?.map(item => item.id));
}, [items])
useEffect(() => {
axiosInstance.get(`/api/?action=settings&key=AvailabilityBlockDate`)
.then(({ data }) => {
setBlockedAvailabilityDate(new Date(data.value));
})
.catch(error => {
console.error("Error getting blocked date:", error);
});
}, []);
const toggleAv = () => setShowAv(!showAv);
const editAvailability = (item) => {
setSelectedItem(item);
@ -72,9 +83,16 @@ export default function AvailabilityList({ publisher, showNew }) {
{/* <button className="bg-blue-200 hover:bg-blue-300 text-blue-600 py-1 px-2 rounded inline-flex items-center" onClick={() => editAvailability(item)}>
<PencilSquareIcon className="h-6 w-6" />
</button> */}
<button className="bg-red-200 hover:bg-red-300 text-red-600 py-1 px-2 rounded ml-2 inline-flex items-center" onClick={() => deleteAvailability(item.id)}>
<TrashIcon className="h-6 w-6" />
</button>
{(blockedAvailabilityDate && new Date(item.startTime) < blockedAvailabilityDate) ? (
<button className="disabled bg-gray-200 hover:bg-gray-300 text-gray-600 py-1 px-2 rounded inline-flex items-center">
<TrashIcon className="h-6 w-6" />
</button>
) : (
<button className="bg-red-200 hover:bg-red-300 text-red-600 py-1 px-2 rounded ml-2 inline-flex items-center" onClick={() => deleteAvailability(item.id)}>
<TrashIcon className="h-6 w-6" />
</button>
)}
</td>
</tr>
))}

View File

@ -511,7 +511,8 @@ const AvCalendar = ({ publisherId, events, selectedDate, cartEvents, lastPublish
<> <div {...handlers} className="flex flex-col"
>
{/* достъпности на {publisherId} */}
<ToastContainer position="top-center" style={{ zIndex: 9999 }} />
{/* having multiple ToastContainers causes double rendering of toasts and all kind of problems */}
{/* <ToastContainer position="top-center" style={{ zIndex: 9999 }} /> */}
</div>
<Calendar
localizer={localizer}

View File

@ -10,6 +10,7 @@ import Body from 'next/document'
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { set } from "date-fns"
import ErrorBoundary from "./ErrorBoundary";
export default function Layout({ children }) {
const router = useRouter();
@ -61,7 +62,9 @@ export default function Layout({ children }) {
<Sidebar isSidebarOpen={isSidebarOpen} toggleSidebar={toggleSidebar} />
<main className={`flex-1 transition-all duration-300 ${marginLeftClass}`}>
<div className="">
{children}
<ErrorBoundary>
{children}
</ErrorBoundary>
</div>
<div id="modal-root"></div> {/* Modal container */}
</main>

View File

@ -46,7 +46,8 @@ const ProtectedRoute = ({ children, allowedRoles, deniedMessage, bypass = false,
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4 text-blue-500">{session?.user?.email},</h1>
<p className="mb-6">{`Нямате достъп до тази страница. Ако мислите, че това е грешка, моля, свържете се с администраторите`}</p>
<p className="mb-6">{`Нямате достъп до тази страница.`}</p>
<p className="mb-6">{`Ако мислите, че това е грешка, моля, свържете се с администраторите`}</p>
</div>
</div>
</>);

View File

@ -92,15 +92,15 @@ export default function PublisherForm({ item, me }) {
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 };
const familyHeadRelation = familyHeadId ? { connect: { id: familyHeadId } } : null;
const userRel = userId ? { connect: { id: userId } } : null;
const congregationRel = congregationId ? { connect: { id: parseInt(congregationId) } } : null;
// Return the new state without familyHeadId and with the correct familyHead relation
rest = {
...rest,
familyHead: familyHeadRelation,
user: userRel,
congregation: congregationRel
...(familyHeadRelation ? { familyHead: familyHeadRelation } : {}),
...(userRel ? { user: userRel } : {}),
...(congregationRel ? { congregation: congregationRel } : {}),
};
try {
@ -112,7 +112,7 @@ export default function PublisherForm({ item, me }) {
position: "bottom-center",
});
} else {
await axiosInstance.post(urls.apiUrl, publisher);
await axiosInstance.post(urls.apiUrl, rest);
toast.success("Task Saved", {
position: "bottom-center",
});

View File

@ -81,6 +81,7 @@ export default function ReportForm({ shiftId, existingItem, onDone }) {
};
fetchData();
}, [item.date, existingItem]);
const handleChange = ({ target }) => {
setItem({ ...item, [target.name]: target.value });
};

View File

@ -1,5 +1,9 @@
import { UserRole } from "@prisma/client";
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// import { faStar } from '@fortawesome/free-solid-svg-icons'; // Star icon
// import { faClipboardList } from '@fortawesome/free-solid-svg-icons'; // Clipboard icon
import { FaStar } from 'react-icons/fa'; // Import FontAwesome icons
const sidemenu = [
@ -103,6 +107,19 @@ const sidemenu = [
text: "Календар",
url: "/cart/calendar",
roles: [UserRole.ADMIN, UserRole.POWERUSER],
},
{
id: "surveys",
// text: "Анкети",
// add new icon before text
// text: "Анкети",
text: (
<span>
<FaStar className="inline-block mr-2" />
Анкети
</span>
),
url: "/cart/surveys",
}, {
id: "cart-reports",
text: "Отчети",

View File

@ -0,0 +1,354 @@
import axiosInstance from '../../src/axiosSecure';
import { use, useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useRouter } from "next/router";
import Link from "next/link";
import { useSession } from "next-auth/react"
import { MessageType, Message, Survey } from "@prisma/client";
// import { content } from 'googleapis/build/src/apis/content';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
// import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
// import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import dayjs from 'dayjs';
import { set } from 'lodash';
const common = require('src/helpers/common');
// ------------------ ------------------
// This component is used to create and edit
/* location model:
model Survey {
id Int @id @default(autoincrement())
content String
answers Json?
messages Message[]
publicFrom DateTime?
publicUntil DateTime?
}
model Message {
id Int @id @default(autoincrement())
publisher Publisher @relation(fields: [publisherId], references: [id])
publisherId String
date DateTime
content String
isRead Boolean @default(false)
isPublic Boolean @default(false)
type MessageType @default(Email)
publicUntil DateTime?
shownDate DateTime?
answer String?
answerDate DateTime?
Survey Survey? @relation(fields: [surveyId], references: [id])
surveyId Int?
}
*/
interface SurveyFormProps {
existingItem: Survey | null;
}
const SurveyForm: React.FC<SurveyFormProps> = ({ existingItem }) => {
const router = useRouter();
const [editMode, setEditMode] = useState(existingItem ? true : false);
const [pubs, setPubs] = useState([]);
const [item, setItem] = useState(existingItem || {
...existingItem,
content: existingItem?.content || "Нова анкета",
answers: existingItem?.answers.split(",") || [],
publicFrom: existingItem?.publicFrom ? dayjs(existingItem.publicFrom).toISOString() : new Date().toISOString(),
publicUntil: existingItem?.publicUntil ? dayjs(existingItem.publicUntil).toISOString() : new Date().toISOString(),
});
useEffect(() => {
let transformedItem = { ...existingItem };
transformedItem.answersCount = existingItem?.answers.split(",") || [];
setEditMode(existingItem ? true : false);
setItem(transformedItem);
}, [existingItem]);
useEffect(async () => {
const pubs = await axiosInstance.get("/api/data/publishers?select=id,firstName,lastName,email");
setPubs(pubs.data);
}, []);
const handleChange = ({ target }) => {
setItem({ ...item, [target.name]: target.value });
};
const handleDateChange = (fieldName, newDate) => {
setItem((prevItem) => ({
...prevItem,
[fieldName]: newDate
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
delete item.answersCount;
try {
if (editMode) {
delete item.messages;
const { data } = await axiosInstance.put(`/api/data/surveys/${existingItem.id}`, item);
toast.success("Анкетата е обновена успешно");
}
else {
//get all publisherIds and create a message for each
const messages = pubs.data.map(pub => {
return {
publisherId: pub.id,
content: JSON.stringify({ message: item.content, options: item.answers }),
date: new Date(),
isPublic: false,
type: MessageType.InApp,
publicUntil: item.publicUntil,
}
});
item.messages = { create: messages };
const { data } = await axiosInstance.post("/api/data/surveys", item);
toast.success("Анкетата е създадена успешно");
}
router.push("/cart/surveys");
} catch (error) {
toast.error("Възникна грешка при създаването на анкетата");
console.error("Error creating survey:", error);
}
}
const handleDelete = async (e) => {
e.preventDefault();
if (!editMode) return;
try {
await axiosInstance.delete(`/api/data/surveys/${existingItem.id}`);
toast.success("Записът изтрит", {
position: "bottom-center",
});
router.push("/cart/surveys");
} catch (error) {
//alert("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас");
console.log(JSON.stringify(error));
toast.error(error.response?.data?.message || "Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи.");
}
};
function handleDeleteAnswers(e: MouseEvent<HTMLButtonElement, MouseEvent>): void {
e.preventDefault();
if (!editMode) return;
Promise.all(existingItem.messages.map(message =>
axiosInstance.put(`/api/data/messages/${message.id}`, { answer: null })
))
.then(() => {
toast.success("Отговорите изтрити", {
position: "bottom-center",
});
})
.catch((error) => {
console.log(JSON.stringify(error));
toast.error(error.response?.data?.message || "Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи.");
});
}
const getNamesByIds = (ids) => {
return ids
.map((id) => {
const pub = pubs.find((p) => p.id === id);
return pub ? `${pub.firstName} ${pub.lastName}` : null;
})
.filter((name) => name !== null)
.join(", ");
};
const getIdsForAnswer = (answer) => {
return item.messages
.filter((message) => message.answer === answer)
.map((message) => message.publisherId);
};
const getIdsForAnswered = () => {
return item.messages
.filter((message) => message.answer)
.map((message) => message.publisherId);
};
const getIdsForUnanswered = () => {
return item.messages
.filter((message) => !message.answer)
.map((message) => message.publisherId);
};
// const copyToClipboard = (text) => {
// navigator.clipboard.writeText(text).then(
// () => toast.success('Copied to clipboard!'),
// (err) => toast.error('Failed to copy text: ', err)
// );
// };
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(
() => alert('Имената са копирани: ' + text),
(err) => alert('Не успяхме да копираме имената: ', err)
);
};
const sendIndividualNotification = async (id, message) => {
const response = await fetch('/api/notify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id,
title: 'Нямаме отговор',
message: `${message}`,
})
});
if (response.ok) {
console.log(`Notification sent successfully to ${name}`);
} else {
console.error(`Failed to send notification to ${name}`);
}
};
const handleSendNotificationsToAllUnanswered = async (message) => {
getIdsForUnanswered().forEach((id, index) => sendIndividualNotification(id, message));
};
return (
<div className="w-full max-w-md mx-auto" >
< form className="bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4" onSubmit={handleSubmit} >
<h1 className="text-2xl font-bold mb-8">Анкета {existingItem?.id}</h1>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="date">
Видима от
</label>
<DatePicker className="textbox form-input px-4 py-2 rounded" name="publicFrom" onChange={(newDate) => handleDateChange('publicFrom', newDate)} value={dayjs(item?.publicFrom)} />
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="date">
Видима до
</label>
<DatePicker className="textbox form-input px-4 py-2 rounded" name="publicUntil" onChange={(newDate) => handleDateChange('publicUntil', newDate)} value={dayjs(item?.publicUntil)} />
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="content">
Съдържание
</label>
<textarea className={`textbox form-input px-4 py-2 rounded ${editMode ? 'opacity-50 cursor-not-allowed' : ''}`} id="content" name="content" onChange={handleChange} value={item?.content} autoComplete="off" disabled={editMode} />
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="answers">
Отговори
</label>
<input className={`textbox form-input px-4 py-2 rounded ${editMode ? 'opacity-50 cursor-not-allowed' : ''}`} id="answers" name="answers" type="text" onChange={handleChange} value={item?.answers} autoComplete="off" disabled={editMode}
/>
</div>
{/* show count of each answer and the total answered/unanswered messages */}
{item?.answersCount?.length > 0 && (
<div className="mb-4">
<h3 className="text-lg font-semibold mb-2">Отговори:</h3>
{item.answersCount.map((answer, index) => {
const currentCount = item.messages ? item.messages.filter((message) => message.answer === answer).length : 0;
const totalCount = item.messages ? item.messages.length : 0;
const percentage = totalCount > 0 ? (currentCount / totalCount) * 100 : 0;
const ids = getIdsForAnswer(answer);
const names = getNamesByIds(ids);
return (
<div key={index} className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={`answer-${index}`}>
{answer}
</label>
<div className="relative h-6 w-full bg-gray-200 rounded" title={names}
onClick={() => copyToClipboard(names)} style={{ cursor: 'copy' }}>
<div className="absolute h-full bg-blue-600 rounded" style={{ width: `${percentage}%` }}></div>
<div className="absolute inset-0 flex items-center justify-center text-white font-bold">
{currentCount} ({percentage.toFixed(1)}%)
</div>
</div>
</div>
);
})}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">Общо отговорили</label>
<div className="relative h-6 w-full bg-gray-200 rounded" title={getNamesByIds(getIdsForAnswered())}
onClick={() => copyToClipboard(getNamesByIds(getIdsForAnswered()))} style={{ cursor: 'copy' }}
>
<div className="absolute h-full bg-green-600 rounded" style={{ width: `${item.messages ? (item.messages.filter((message) => message.answer).length / item.messages.length) * 100 : 0}%` }}></div>
<div className="absolute inset-0 flex items-center justify-center text-white font-bold">
{item.messages ? item.messages.filter((message) => message.answer).length : 0}
</div>
</div>
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">Общо неотговорили</label>
<div className="relative h-6 w-full bg-gray-200 rounded" title={getNamesByIds(getIdsForUnanswered())}
style={{ cursor: 'copy' }}
onClick={() => copyToClipboard(getNamesByIds(getIdsForUnanswered()))}>
<div className="absolute h-full bg-red-600 rounded" style={{ width: `${item.messages ? (item.messages.filter((message) => !message.answer).length / item.messages.length) * 100 : 0}%` }}></div>
<div className="absolute inset-0 flex items-center justify-center text-white font-bold">
{item.messages ? item.messages.filter((message) => !message.answer).length : 0}
</div>
</div>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => { handleSendNotificationsToAllUnanswered(item?.content) }}>
Подсети ВСИЧКИ неотховорили с нотификация
</button>
<div className="mt-2">
{getIdsForUnanswered().map((id) => {
const pub = pubs.find((p) => p.id === id);
const name = pub ? `${pub.firstName} ${pub.lastName}` : '???';
return (
<button
key={id}
// className="block mt-1 px-1 py-0.5 bg-orange-500 text-white rounded"
className="mt-1 px-2 py-1 bg-green-500 text-white rounded text-xs"
onClick={() => sendIndividualNotification(id, item?.content)}>
{name}
</button>
);
})}
</div>
</div>
</div>
)}
<div className="flex items-center justify-between">
{editMode && (<>
<button className="button btn-outline bg-red-500 hover:bg-red-700 focus:outline-none focus:shadow-outline" type="button" onClick={handleDelete}>
Изтрий
</button>
<button className="button btn-outline bg-red-500 hover:bg-red-700 focus:outline-none focus:shadow-outline" type="button" onClick={handleDeleteAnswers}>
Изтрий отговорите
</button>
</>)}
<button className="btn bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded transition duration-300" type="submit">
Запази
</button>
</div>
</form >
</div >
);
}
export default SurveyForm;

73
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "pwwa",
"version": "1.2.2",
"name": "smws",
"version": "1.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pwwa",
"version": "1.2.2",
"name": "smws",
"version": "1.2.4",
"dependencies": {
"@auth/prisma-adapter": "^1.4.0",
"@emotion/react": "^11.11.3",
@ -16,7 +16,7 @@
"@mui/material": "^5.15.10",
"@mui/x-date-pickers": "^6.19.4",
"@premieroctet/next-crud": "^3.0.0",
"@prisma/client": "^5.13.0",
"@prisma/client": "^5.15.0",
"@react-pdf/renderer": "^3.3.8",
"@tailwindcss/forms": "^0.5.7",
"@types/multer": "^1.4.11",
@ -49,6 +49,7 @@
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"levenshtein-edit-distance": "^3.0.1",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"mailtrap": "^3.3.0",
"module-alias": "^2.2.3",
@ -99,7 +100,7 @@
"devDependencies": {
"cross-env": "^7.0.3",
"depcheck": "^1.4.7",
"prisma": "^5.13.0"
"prisma": "^5.15.0"
}
},
"node_modules/@alloc/quick-lru": {
@ -3914,9 +3915,9 @@
}
},
"node_modules/@prisma/client": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.13.0.tgz",
"integrity": "sha512-uYdfpPncbZ/syJyiYBwGZS8Gt1PTNoErNYMuqHDa2r30rNSFtgTA/LXsSk55R7pdRTMi5pHkeP9B14K6nHmwkg==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.15.0.tgz",
"integrity": "sha512-wPTeTjbd2Q0abOeffN7zCDCbkp9C9cF+e9HPiI64lmpehyq2TepgXE+sY7FXr7Rhbb21prLMnhXX27/E11V09w==",
"hasInstallScript": true,
"engines": {
"node": ">=16.13"
@ -3931,39 +3932,39 @@
}
},
"node_modules/@prisma/debug": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.13.0.tgz",
"integrity": "sha512-699iqlEvzyCj9ETrXhs8o8wQc/eVW+FigSsHpiskSFydhjVuwTJEfj/nIYqTaWFYuxiWQRfm3r01meuW97SZaQ==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.15.0.tgz",
"integrity": "sha512-QpEAOjieLPc/4sMny/WrWqtpIAmBYsgqwWlWwIctqZO0AbhQ9QcT6x2Ut3ojbDo/pFRCCA1Z1+xm2MUy7fAkZA==",
"devOptional": true
},
"node_modules/@prisma/engines": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.13.0.tgz",
"integrity": "sha512-hIFLm4H1boj6CBZx55P4xKby9jgDTeDG0Jj3iXtwaaHmlD5JmiDkZhh8+DYWkTGchu+rRF36AVROLnk0oaqhHw==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.15.0.tgz",
"integrity": "sha512-hXL5Sn9hh/ZpRKWiyPA5GbvF3laqBHKt6Vo70hYqqOhh5e0ZXDzHcdmxNvOefEFeqxra2DMz2hNbFoPvqrVe1w==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/debug": "5.13.0",
"@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b",
"@prisma/fetch-engine": "5.13.0",
"@prisma/get-platform": "5.13.0"
"@prisma/debug": "5.15.0",
"@prisma/engines-version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022",
"@prisma/fetch-engine": "5.15.0",
"@prisma/get-platform": "5.15.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b.tgz",
"integrity": "sha512-AyUuhahTINGn8auyqYdmxsN+qn0mw3eg+uhkp8zwknXYIqoT3bChG4RqNY/nfDkPvzWAPBa9mrDyBeOnWSgO6A==",
"version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022.tgz",
"integrity": "sha512-3BEgZ41Qb4oWHz9kZNofToRvNeS4LZYaT9pienR1gWkjhky6t6K1NyeWNBkqSj2llgraUNbgMOCQPY4f7Qp5wA==",
"devOptional": true
},
"node_modules/@prisma/fetch-engine": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.13.0.tgz",
"integrity": "sha512-Yh4W+t6YKyqgcSEB3odBXt7QyVSm0OQlBSldQF2SNXtmOgMX8D7PF/fvH6E6qBCpjB/yeJLy/FfwfFijoHI6sA==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.15.0.tgz",
"integrity": "sha512-z6AY5yyXxc20Klj7wwnfGP0iIUkVKzybqapT02zLYR/nf9ynaeN8bq73WRmi1TkLYn+DJ5Qy+JGu7hBf1pE78A==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.13.0",
"@prisma/engines-version": "5.13.0-23.b9a39a7ee606c28e3455d0fd60e78c3ba82b1a2b",
"@prisma/get-platform": "5.13.0"
"@prisma/debug": "5.15.0",
"@prisma/engines-version": "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022",
"@prisma/get-platform": "5.15.0"
}
},
"node_modules/@prisma/generator-helper": {
@ -3980,12 +3981,12 @@
"integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA=="
},
"node_modules/@prisma/get-platform": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.13.0.tgz",
"integrity": "sha512-B/WrQwYTzwr7qCLifQzYOmQhZcFmIFhR81xC45gweInSUn2hTEbfKUPd2keAog+y5WI5xLAFNJ3wkXplvSVkSw==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.15.0.tgz",
"integrity": "sha512-1GULDkW4+/VQb73vihxCBSc4Chc2x88MA+O40tcZFjmBzG4/fF44PaXFxUqKSFltxU9L9GIMLhh0Gfkk/pUbtg==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.13.0"
"@prisma/debug": "5.15.0"
}
},
"node_modules/@prisma/internals": {
@ -15118,13 +15119,13 @@
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
},
"node_modules/prisma": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.13.0.tgz",
"integrity": "sha512-kGtcJaElNRAdAGsCNykFSZ7dBKpL14Cbs+VaQ8cECxQlRPDjBlMHNFYeYt0SKovAVy2Y65JXQwB3A5+zIQwnTg==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.15.0.tgz",
"integrity": "sha512-JA81ACQSCi3a7NUOgonOIkdx8PAVkO+HbUOxmd00Yb8DgIIEpr2V9+Qe/j6MLxIgWtE/OtVQ54rVjfYRbZsCfw==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "5.13.0"
"@prisma/engines": "5.15.0"
},
"bin": {
"prisma": "build/index.js"

View File

@ -1,6 +1,6 @@
{
"name": "smws",
"version": "1.2.4",
"version": "1.3.0",
"private": true,
"description": "SMWS | ССОМ | Специално Свидетелстване София",
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
@ -34,7 +34,7 @@
"@mui/material": "^5.15.10",
"@mui/x-date-pickers": "^6.19.4",
"@premieroctet/next-crud": "^3.0.0",
"@prisma/client": "^5.13.0",
"@prisma/client": "^5.15.0",
"@react-pdf/renderer": "^3.3.8",
"@tailwindcss/forms": "^0.5.7",
"@types/multer": "^1.4.11",
@ -67,6 +67,7 @@
"jsonwebtoken": "^9.0.2",
"jszip": "^3.10.1",
"levenshtein-edit-distance": "^3.0.1",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"mailtrap": "^3.3.0",
"module-alias": "^2.2.3",
@ -117,6 +118,6 @@
"devDependencies": {
"cross-env": "^7.0.3",
"depcheck": "^1.4.7",
"prisma": "^5.13.0"
"prisma": "^5.15.0"
}
}

View File

@ -86,10 +86,14 @@ function SmwsApp({ Component, pageProps, session, locale, messages }) {
// }, [locale]);
useEffect(() => {
const use = async () => {
(await import('tw-elements')).default;
};
use();
try {
const use = async () => {
(await import('tw-elements')).default;
};
use();
} catch (e) {
console.error('Error loading tw-elements:', e);
}
}, []);

View File

@ -64,6 +64,50 @@ export default async function handler(req, res) {
res.status(200).json({ message: "SQL script executed successfully" });
break;
case "settings":
try {
const key = req.query.key;
switch (req.method) {
case "LIST":
let s1 = await prisma.settings.findMany();
res.status(200).json(s1.map(setting => setting.key));
break;
case "GET":
const s2 = await prisma.settings.findUnique({
where: {
key: key
}
});
res.status(200).json(s2);
break;
case "PUT": //create or update
const value = req.query.value;
const results = await prisma.settings.upsert({
where: {
key: key
},
update: {
value: value
},
create: {
key: key,
value: value
}
});
res.status(200).json(results);
break;
default:
res.status(405).json({ message: "Method Not Allowed" });
break;
}
} catch (error) {
console.error("Error getting settings: " + error);
res.status(500).json({ error });
}
break;
case "deleteAllPublishers":
//get filter and delete all publishers containing that in first name or last name
await prisma.publisher.deleteMany({
@ -467,6 +511,7 @@ export async function getMonthlyStatistics(selectFields, filterDate) {
export async function filterPublishersNew_Available(selectFields, filterDate, isExactTime = false, isForTheMonth = false, isWithStats = true, includeOldAvailabilities = false) {
return dataHelper.filterPublishersNew(selectFields, filterDate, isExactTime, isForTheMonth, false, isWithStats, includeOldAvailabilities);
// async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false, id = null, filterAvailabilitiesByDate = true)
}
// availabilites filter:

View File

@ -33,19 +33,21 @@ const Notification = async (req, res) => {
select: { pushSubscription: true }
});
subs = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription.length : (publisher.pushSubscription ? 1 : 0);
res.send({ subs })
res.end()
return
} else {
// send the public key in the response headers
//res.setHeader('Content-Type', 'text/plain')
res.send({ pk: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, subs })
res.end()
}
// send the public key in the response headers
//res.setHeader('Content-Type', 'text/plain')
res.send({ pk: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, subs })
res.end()
}
if (req.method == 'PUT') {
// store the subscription object in the database
// publisher.pushSubscription = subscription
const prisma = common.getPrismaClient();
const { subscription, id } = req.body
const { subscription, id, name } = req.body
const publisher = await prisma.publisher.findUnique({
where: { id },
select: { pushSubscription: true }
@ -105,16 +107,21 @@ const Notification = async (req, res) => {
if (req.method == 'POST') {//title = "ССС", message = "Ще получите уведомление по този начин.")
const { subscription, id, broadcast, title = 'ССОМ', message = 'Ще получавате уведомления така.', actions } = req.body
const { subscription, id, ids, broadcast, title = 'ССОМ', message = 'Ще получавате уведомления така.', actions } = req.body
if (broadcast) {
await broadcastPush(title, message, actions)
res.statusCode = 200
res.end()
return
}
else if (id) {
} else if (ids && ids.length) {
console.log('Sending push notifications to publishers ', ids);
await Promise.all(ids.map(_id => sendPush(_id, title, message, actions)));
res.statusCode = 200;
res.end();
return;
} else if (id) {
console.log('Sending push notification to publisher ', id)
await sendPush(id, title, message.actions)
await sendPush(id, title, message, actions)
res.statusCode = 200
res.end()
return

File diff suppressed because it is too large Load Diff

View File

@ -62,6 +62,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
const [allShifts, setAllShifts] = useState(initialShifts);
const [isPublished, setIsPublished] = useState(() => initialShifts.some(shift => shift.isPublished));
const [value, onChange] = useState<Date>(new Date());
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
const [shifts, setShifts] = React.useState([]);
const [error, setError] = React.useState(null);
const [availablePubs, setAvailablePubs] = React.useState([]);
@ -89,7 +90,6 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
}, [filterShowWithoutAssignments]); // Dependency array
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
useEffect(() => {
const newMonth = value.getMonth();
if (newMonth !== selectedMonth) {
@ -607,6 +607,29 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
});
}
/*
model Settings {
id Int @id @default(autoincrement())
key String
value String
description String?
}
*/
async function setAvailabilityBlockDate(AvailabilityBlockDate: Date): Promise<void> {
// set AvailabilityBlockDate to the selected date
let monthInfo = common.getMonthInfo(value);
await axiosInstance.put(`/api/?action=settings&key=AvailabilityBlockDate&value=${common.getISODateOnly(monthInfo.lastSunday)}`)
.then((response) => {
console.log("AvailabilityBlockDate set to:", response.data);
// setShifts([...shifts, response.data]);
handleCalDateChange(value);
}
).catch((error) => {
console.error("Error setting AvailabilityBlockDate:", error);
});
}
return (
<>
<Layout>
@ -700,6 +723,9 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 1)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени 2 </button>
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 2)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени 3 </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100"
onClick={() => openConfirmModal(
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',
@ -715,8 +741,9 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateXLS}><i className="fas fa-file-excel mr-2"></i> Генерирай XLSX</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={fetchShifts}>
{isLoading('fetchShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-sync-alt mr-2"></i>)} презареди</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={copyOldAvailabilities}><i className="fas fa-copy mr-2"></i> Прехвърли предпочитанията</button>
{/* <button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={copyOldAvailabilities}><i className="fas fa-copy mr-2"></i> Прехвърли предпочитанията</button> */}
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={setAvailabilityBlockDate}><i className="fas fa-copy mr-2"></i> Блокирай предпочитанията до края на {selectedMonth + 1} м.</button>
</div>
</div>
)}
@ -795,7 +822,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
{pub.canTransport && (<LocalShippingIcon className="mx-2 text-gray-500" />)}
</span>
<div className="flex space-x-1 overflow-hidden">
<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`} >
<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>
<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>
@ -809,7 +836,27 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ broadcast: true, message: "Тестово съобщение", title: "Това е тестово съобщение от https://sofia.mwitnessing.com" })
body: JSON.stringify({
id: pub.id,
message: "Тестово съобщение",
title: "Това е тестово съобщение от https://sofia.mwitnessing.com",
actions: [
{ action: 'OK', title: 'OK', icon: '✅' },
{ action: 'close', title: 'Затвори', icon: '❌' }
]
// actions: [
// {
// title: 'Open URL',
// action: 'open_url',
// icon: '/images/open-url.png'
// },
// {
// title: 'Dismiss',
// action: 'dismiss',
// icon: '/images/dismiss.png'
// }
// ]
})
})
}}
>+</button>

View File

@ -253,7 +253,7 @@ function ContactsPage({ allPublishers }) {
<>
<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`}>
<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>}
@ -324,10 +324,22 @@ export default ContactsPage;
export const getServerSideProps = async (context) => {
const allPublishers = await data.getAllPublishersWithStatisticsMonth(new Date());
//merge first and last name
// 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

View File

@ -0,0 +1,86 @@
import React, { useState, useEffect } from 'react';
import Layout from "../../../components/layout";
import { GetServerSideProps } from 'next';
import { Location, UserRole } from "@prisma/client";
import axiosServer from '../../../src/axiosServer';
const common = require('../../../src/helpers/common');
// import * as common from '../../../src/helpers/common';
import SurveyForm from '../../../components/survey/SurveyForm';
import _ from 'lodash';
import ProtectedRoute from 'components/protectedRoute';
const SurveyPage = ({ serverSurveys }) => {
const [selectedSurvey, setSelectedSurvey] = useState(null);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
<div className="max-w-4xl mx-auto my-8 p-4 bg-white shadow-md rounded">
<h1 className="text-2xl font-bold mb-4">Анкети</h1>
<div className="flex flex-row justify-between">
<div className="w-1/2 pr-4">
<h2 className="text-xl font-semibold mb-4">Списък</h2>
<ul className="space-y-4">
{serverSurveys.map((survey) => (
<li key={survey.id} className="p-4 border rounded bg-gray-50 shadow-sm">
<p className="font-medium">{survey.id}: {survey.content}</p>
{/* <p className="text-gray-700">{survey.publicFrom} - {survey.publicUntil}</p> */}
<p className="mt-2"> [{survey.answers}] </p>
<div className="mt-2">
{Object.entries(_.groupBy(survey.messages, message => message.answer || "Без отговор")).map(([key, items]) => (
<div key={key} className="text-sm text-gray-700">
{key}: {items.length}
</div>
))}
</div>
<button
className="btn mt-2 bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded"
onClick={() => setSelectedSurvey(survey)}
>
Зареди детайли
</button>
</li>
))}
</ul>
<button
className="btn mt-4 bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
onClick={() => setSelectedSurvey(null)}
>
Нова анкета
</button>
</div>
<div className="w-1/2 pl-4">
<h2 className="text-xl font-semibold mb-4">Детайли</h2>
<SurveyForm existingItem={selectedSurvey} />
</div>
</div>
<div className="mt-8">
</div>
</div>
</ProtectedRoute>
</Layout>
);
};
export default SurveyPage;
export const getServerSideProps: GetServerSideProps = async (context) => {
const prisma = common.getPrismaClient();
let serverSurveys = await prisma.survey.findMany({
where: {
},
include: {
messages: true,
},
});
serverSurveys = common.convertDatesToISOStrings(serverSurveys);
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
return {
props: {
serverSurveys: serverSurveys
},
};
};

View File

@ -1,7 +1,8 @@
import { useSession } from "next-auth/react"
import { useRouter } from 'next/router';
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, use } from 'react';
import Layout from "../components/layout"
import { toast } from 'react-toastify';
import AvCalendar from '../components/calendar/avcalendar';
import { getSession } from "next-auth/react";
@ -25,9 +26,9 @@ interface IProps {
initialUserId: string;
cartEvents: any;
lastPublishedDate: Date;
messages: any;
}
export default function DashboardPage({ initialItems, initialUserId, cartEvents, lastPublishedDate }: IProps) {
export default function DashboardPage({ initialItems, initialUserId, cartEvents, lastPublishedDate, messages }: IProps) {
const router = useRouter();
const { newLogin } = router.query;
const { data: session } = useSession();
@ -51,17 +52,145 @@ export default function DashboardPage({ initialItems, initialUserId, cartEvents,
}, [session]);
// MESSAGES
//const [notificationsVisible, setNotificationsVisible] = useState(false);
useEffect(() => {
//if (newLogin === 'true')
{
// alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
const currentPath = router.pathname;
router.replace(currentPath, undefined, { shallow: true }); // Removes the query without affecting the history
}
}, []);// show the message every time we load the page
// useEffect(() => {
// //if (newLogin === 'true')
// {
// // alert("Мили братя, искаме само да ви напомним да ни изпратите вашите предпочитания за юни до 25-то число като използвате меню 'Възможности'. Ако имате проблем, моля пишете ни на 'specialnosvidetelstvanesofia@gmail.com'");
// const currentPath = router.pathname;
// router.replace(currentPath, undefined, { shallow: true }); // Removes the query without affecting the history
// }
// }, []);// show the message every time we load the page
// const [processedMessages, setProcessedMessages] = useState(new Set());
// useEffect(() => {
// if (messages && messages.length > 0) {
// const unprocessedMessages = messages.filter(message => !processedMessages.has(message.id));
// if (unprocessedMessages.length > 0) {
// showMessageToasts(unprocessedMessages);
// setProcessedMessages(new Set([...processedMessages, ...unprocessedMessages.map(msg => msg.id)]));
// }
// }
// }, [messages, processedMessages]);
useEffect(() => {
if (messages && messages.length > 0) {
showMessageToasts(messages);
}
}, [messages]);
const showMessageToasts = (messages) => {
const handleOptionClick = async (messageId, option, toastId) => {
try {
await axiosInstance.put(`/api/data/messages/${messageId}`, { answer: option });
handleClose(toastId);
} catch (error) {
console.error("Error updating message:", error);
toast.error("Error updating message. Please try again.");
}
};
const handleClose = (toastId) => {
toast.dismiss(toastId);
};
messages.forEach((message, messageIndex) => {
const toastId = `message-${message.id}-${messageIndex}`;
const content = (
<div>
<div>{message.content.message}</div>
<div>
{message.content.options?.map((option, index) => (
<button
key={index}
onClick={() => handleOptionClick(message.id, option, toastId)}
className="btn bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded m-1"
>
{option}
</button>
))}
</div>
</div>
);
toast(content, {
toastId,
autoClose: false,
closeButton: true,
onClose: () => handleClose(toastId),
});
});
};
// const showMessageToastNewModal = (messages, handleMessageOptionAnswer) => {
// let currentMessageIndex = 0;
// const showModal = () => {
// if (currentMessageIndex >= messages.length) {
// return; // All messages have been shown
// }
// const message = messages[currentMessageIndex];
// const content = (
// <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
// <div className="bg-white rounded shadow-lg p-4 max-w-lg w-full">
// <div className="text-right">
// <button
// className="text-gray-500 hover:text-gray-700"
// onClick={handleClose}
// >
// &times;
// </button>
// </div>
// <div className="mb-4">{message.content.message}</div>
// <div>
// {message.content.options?.map((option, index) => (
// <button
// key={index}
// onClick={() => handleOptionClick(message.id, option)}
// className="btn bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded m-1"
// >
// {option}
// </button>
// ))}
// </div>
// </div>
// </div>
// );
// toast(content, {
// autoClose: false, // Keep the toast open until manually closed
// closeButton: false,
// onClose: handleClose,
// //className: 'custom-toast', // Optional custom class for additional styling
// });
// };
// const handleOptionClick = async (messageId, option) => {
// try {
// await axiosInstance.put(`/api/data/messages/${messageId}`, { answer: option });
// toast.dismiss();
// currentMessageIndex++;
// showModal();
// } catch (error) {
// console.error("Error updating message:", error);
// toast.error("Error updating message. Please try again.");
// }
// };
// const handleClose = () => {
// toast.dismiss();
// };
// showModal();
// };
// FOR ADMINS ONLY
const handleUserSelection = async (publisher) => {
if (!publisher || publisher.id === undefined) return;
console.log("selecting publisher", publisher.id);
@ -107,107 +236,6 @@ export default function DashboardPage({ initialItems, initialUserId, cartEvents,
}
// async function getAvailabilities(userId) {
// const prismaClient = common.getPrismaClient();
// const items = await prismaClient.availability.findMany({
// where: {
// publisherId: userId,
// },
// select: {
// id: true,
// name: true,
// isActive: true,
// isFromPreviousAssignment: true,
// isFromPreviousMonth: true,
// dayofweek: true,
// dayOfMonth: true,
// startTime: true,
// endTime: true,
// repeatWeekly: true,
// endDate: true,
// publisher: {
// select: {
// firstName: true,
// lastName: true,
// id: true,
// },
// },
// },
// });
// // Convert Date objects to ISO strings
// const serializableItems = items.map(item => ({
// ...item,
// startTime: item.startTime.toISOString(),
// endTime: item.endTime.toISOString(),
// name: common.getTimeFormatted(item.startTime) + "-" + common.getTimeFormatted(item.endTime),
// //endDate can be null
// endDate: item.endDate ? item.endDate.toISOString() : null,
// type: 'availability',
// // Convert other Date fields similarly if they exist
// }));
// /*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
// isActive Boolean @default(true)
// isConfirmed Boolean @default(false)
// isWithTransport Boolean @default(false)
// Report Report[]
// }*/
// //get assignments for this user
// const assignments = await prismaClient.assignment.findMany({
// where: {
// publisherId: userId,
// },
// select: {
// id: true,
// isBySystem: true,
// isConfirmed: true,
// isWithTransport: true,
// shift: {
// select: {
// id: true,
// name: true,
// startTime: true,
// endTime: true,
// //select all assigned publishers names as name - comma separated
// assignments: {
// select: {
// publisher: {
// select: {
// firstName: true,
// lastName: true,
// }
// }
// }
// }
// }
// }
// }
// });
// const serializableAssignments = assignments.map(item => ({
// ...item,
// startTime: item.shift.startTime.toISOString(),
// endTime: item.shift.endTime.toISOString(),
// // name: item.shift.publishers.map(p => p.firstName + " " + p.lastName).join(", "),
// //name: item.shift.assignments.map(a => a.publisher.firstName[0] + " " + a.publisher.lastName).join(", "),
// name: common.getTimeFormatted(new Date(item.shift.startTime)) + "-" + common.getTimeFormatted(new Date(item.shift.endTime)),
// type: 'assignment',
// //delete shift object
// shift: null,
// publisher: { id: userId }
// }));
// serializableItems.push(...serializableAssignments);
// return serializableItems;
// }
export const getServerSideProps = async (context) => {
const auth = await serverSideAuth({
req: context.req,
@ -270,7 +298,7 @@ export const getServerSideProps = async (context) => {
}
});
cartEvents = common.convertDatesToISOStrings(cartEvents);
const lastPublishedDate = (await prisma.shift.findFirst({
let lastPublishedDate = (await prisma.shift.findFirst({
where: {
isPublished: true,
},
@ -280,7 +308,43 @@ export const getServerSideProps = async (context) => {
orderBy: {
endTime: 'desc'
}
})).endTime;
}))?.endTime || new Date();
let blockedDate = await prisma.settings.findUnique({
where: {
key: "AvailabilityBlockDate"
}
});
if (blockedDate) {
blockedDate.value = new Date(blockedDate.value);
lastPublishedDate = lastPublishedDate > blockedDate.value ? lastPublishedDate : blockedDate.value;
}
let messages = await prisma.message.findMany({
where: {
publisherId: userId,
isPublic: false,
answer: null,
},
include: {
Survey: true,
}
});
messages = messages.filter((message) => {
return (!message.Survey.publicFrom || message.Survey.publicFrom >= common.getStartOfDay(new Date()))
&& (!message.Survey.publicUntil || message.Survey.publicUntil <= common.getEndOfDay(new Date()))
});
messages = common.convertDatesToISOStrings(messages);
messages = messages.map(message => {
if (message.content) {
message.content = JSON.parse(message.content);
message.content.options = message.content.options?.split(",");
}
return message;
});
return {
props: {
@ -288,7 +352,7 @@ export const getServerSideProps = async (context) => {
userId: sessionServer?.user.id,
cartEvents: cartEvents,
lastPublishedDate: lastPublishedDate.toISOString(),
// messages: (await import(`../content/i18n/${context.locale}.json`)).default
messages: messages
},
};
}

View File

@ -1,80 +0,0 @@
-- CreateTable
CREATE TABLE `Publisher` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`firstName` VARCHAR(191) NOT NULL,
`lastName` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`phone` VARCHAR(191) NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`age` INTEGER NULL,
UNIQUE INDEX `Publisher_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Availability` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`publisherId` INTEGER NOT NULL,
`name` VARCHAR(191) NOT NULL,
`dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL,
`startTime` DATETIME(3) NOT NULL,
`endTime` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CartEvent` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`startTime` DATETIME(3) NOT NULL,
`endTime` DATETIME(3) NOT NULL,
`dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Shift` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`cartEventId` INTEGER NOT NULL,
`name` VARCHAR(191) NOT NULL,
`startTime` DATETIME(3) NOT NULL,
`endTime` DATETIME(3) NOT NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`requiresTransport` BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Location` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`address` VARCHAR(191) NOT NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `_PublisherToShift` (
`A` INTEGER NOT NULL,
`B` INTEGER NOT NULL,
UNIQUE INDEX `_PublisherToShift_AB_unique`(`A`, `B`),
INDEX `_PublisherToShift_B_index`(`B`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Availability` ADD CONSTRAINT `Availability_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Shift` ADD CONSTRAINT `Shift_cartEventId_fkey` FOREIGN KEY (`cartEventId`) REFERENCES `CartEvent`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_PublisherToShift` ADD CONSTRAINT `_PublisherToShift_A_fkey` FOREIGN KEY (`A`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_PublisherToShift` ADD CONSTRAINT `_PublisherToShift_B_fkey` FOREIGN KEY (`B`) REFERENCES `Shift`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,17 +0,0 @@
/*
Warnings:
- Added the required column `locationId` to the `CartEvent` table without a default value. This is not possible if the table is not empty.
- Added the required column `shiftDuration` to the `CartEvent` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE `CartEvent` ADD COLUMN `isActive` BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN `locationId` INTEGER NOT NULL,
ADD COLUMN `shiftDuration` INTEGER NOT NULL;
-- AddForeignKey
ALTER TABLE `CartEvent` ADD CONSTRAINT `CartEvent_locationId_fkey` FOREIGN KEY (`locationId`) REFERENCES `Location`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,12 +0,0 @@
/*
Warnings:
- You are about to drop the column `dayofweek` on the `Location` table. All the data in the column will be lost.
- Added the required column `date` to the `Shift` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE `Location` DROP COLUMN `dayofweek`;
-- AlterTable
ALTER TABLE `Shift` ADD COLUMN `date` DATETIME(3) NOT NULL;

View File

@ -1,8 +0,0 @@
/*
Warnings:
- You are about to drop the column `date` on the `Shift` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE `Shift` DROP COLUMN `date`;

View File

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE `CartEvent` ADD COLUMN `eventType` ENUM('PW_Cart', 'KH_Cleaning') NOT NULL DEFAULT 'PW_Cart';
-- AlterTable
ALTER TABLE `Shift` ADD COLUMN `isTentaive` BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,30 +0,0 @@
/*
Warnings:
- You are about to drop the `_PublisherToShift` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE `_PublisherToShift` DROP FOREIGN KEY `_PublisherToShift_A_fkey`;
-- DropForeignKey
ALTER TABLE `_PublisherToShift` DROP FOREIGN KEY `_PublisherToShift_B_fkey`;
-- DropTable
DROP TABLE `_PublisherToShift`;
-- CreateTable
CREATE TABLE `Assignment` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`shiftId` INTEGER NOT NULL,
`publisherId` INTEGER NOT NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Assignment` ADD CONSTRAINT `Assignment_shiftId_fkey` FOREIGN KEY (`shiftId`) REFERENCES `Shift`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Assignment` ADD CONSTRAINT `Assignment_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,11 +0,0 @@
/*
Warnings:
- You are about to drop the column `isTentaive` on the `Shift` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE `Assignment` ADD COLUMN `isTentaive` BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE `Shift` DROP COLUMN `isTentaive`;

View File

@ -1,9 +0,0 @@
/*
Warnings:
- You are about to drop the column `isTentaive` on the `Assignment` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE `Assignment` DROP COLUMN `isTentaive`,
ADD COLUMN `isTentative` BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,23 +0,0 @@
-- DropForeignKey
ALTER TABLE `Assignment` DROP FOREIGN KEY `Assignment_publisherId_fkey`;
-- DropForeignKey
ALTER TABLE `Assignment` DROP FOREIGN KEY `Assignment_shiftId_fkey`;
-- DropForeignKey
ALTER TABLE `Availability` DROP FOREIGN KEY `Availability_publisherId_fkey`;
-- DropForeignKey
ALTER TABLE `Shift` DROP FOREIGN KEY `Shift_cartEventId_fkey`;
-- AddForeignKey
ALTER TABLE `Availability` ADD CONSTRAINT `Availability_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Shift` ADD CONSTRAINT `Shift_cartEventId_fkey` FOREIGN KEY (`cartEventId`) REFERENCES `CartEvent`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Assignment` ADD CONSTRAINT `Assignment_shiftId_fkey` FOREIGN KEY (`shiftId`) REFERENCES `Shift`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Assignment` ADD CONSTRAINT `Assignment_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,75 +0,0 @@
/*
Warnings:
- The primary key for the `Publisher` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- DropForeignKey
ALTER TABLE `Assignment` DROP FOREIGN KEY `Assignment_publisherId_fkey`;
-- DropForeignKey
ALTER TABLE `Availability` DROP FOREIGN KEY `Availability_publisherId_fkey`;
-- AlterTable
ALTER TABLE `Assignment` MODIFY `publisherId` VARCHAR(191) NOT NULL;
-- AlterTable
ALTER TABLE `Availability` MODIFY `publisherId` VARCHAR(191) NOT NULL;
-- AlterTable
ALTER TABLE `Publisher` DROP PRIMARY KEY,
ADD COLUMN `emailVerified` DATETIME(3) NULL,
MODIFY `id` VARCHAR(191) NOT NULL,
ADD PRIMARY KEY (`id`);
-- CreateTable
CREATE TABLE `Account` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NOT NULL,
`type` VARCHAR(191) NOT NULL,
`provider` VARCHAR(191) NOT NULL,
`providerAccountId` VARCHAR(191) NOT NULL,
`refresh_token` VARCHAR(191) NULL,
`access_token` VARCHAR(191) NULL,
`expires_at` INTEGER NULL,
`token_type` VARCHAR(191) NULL,
`scope` VARCHAR(191) NULL,
`id_token` VARCHAR(191) NULL,
`session_state` VARCHAR(191) NULL,
UNIQUE INDEX `Account_provider_providerAccountId_key`(`provider`, `providerAccountId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Session` (
`id` VARCHAR(191) NOT NULL,
`sessionToken` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NOT NULL,
`expires` DATETIME(3) NOT NULL,
UNIQUE INDEX `Session_sessionToken_key`(`sessionToken`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `VerificationToken` (
`identifier` VARCHAR(191) NOT NULL,
`token` VARCHAR(191) NOT NULL,
`expires` DATETIME(3) NOT NULL,
UNIQUE INDEX `VerificationToken_token_key`(`token`),
UNIQUE INDEX `VerificationToken_identifier_token_key`(`identifier`, `token`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Availability` ADD CONSTRAINT `Availability_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Assignment` ADD CONSTRAINT `Assignment_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Account` ADD CONSTRAINT `Account_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,8 +0,0 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `dayOfMonth` INTEGER NULL;
-- AlterTable
ALTER TABLE `CartEvent` ADD COLUMN `numberOfPublishers` INTEGER NOT NULL DEFAULT 3;
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `role` ENUM('ADMIN', 'USER') NOT NULL DEFAULT 'USER';

View File

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `desiredShiftsPerMonth` INTEGER NOT NULL DEFAULT 4,
MODIFY `role` ENUM('ADMIN', 'USER', 'EXTERNAL') NOT NULL DEFAULT 'USER';

View File

@ -1,7 +0,0 @@
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `isMale` BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN `isNameForeign` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `parentId` VARCHAR(191) NULL;
-- AddForeignKey
ALTER TABLE `Publisher` ADD CONSTRAINT `Publisher_parentId_fkey` FOREIGN KEY (`parentId`) REFERENCES `Publisher`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `isImported` BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `Shift` ADD COLUMN `notes` VARCHAR(191) NULL;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `weekOfMonth` INTEGER NULL;

View File

@ -1,18 +0,0 @@
/*
Warnings:
- You are about to drop the column `parentId` on the `Publisher` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE `Publisher` DROP FOREIGN KEY `Publisher_parentId_fkey`;
-- AlterTable
ALTER TABLE `Assignment` ADD COLUMN `isWithTransport` BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE `Publisher` DROP COLUMN `parentId`,
ADD COLUMN `familyHeadId` VARCHAR(191) NULL;
-- AddForeignKey
ALTER TABLE `Publisher` ADD CONSTRAINT `Publisher_familyHeadId_fkey` FOREIGN KEY (`familyHeadId`) REFERENCES `Publisher`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,8 +0,0 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `isWithTransport` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `type` ENUM('Weekly', 'Monthly', 'OneTime', 'PreviousAssignment') NOT NULL DEFAULT 'Weekly';
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `comments` VARCHAR(191) NULL,
ADD COLUMN `town` VARCHAR(191) NULL,
ADD COLUMN `type` ENUM('Publisher', 'Bethelite', 'RegularPioneer', 'SpecialPioneer', 'Missionary', 'CircuitOverseer') NOT NULL DEFAULT 'Publisher';

View File

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `isFromPreviousAssignment` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `isFromPreviousMonth` BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,42 +0,0 @@
/*
Warnings:
- You are about to drop the column `emailVerified` on the `Publisher` table. All the data in the column will be lost.
- A unique constraint covering the columns `[userId]` on the table `Publisher` will be added. If there are existing duplicate values, this will fail.
*/
-- DropForeignKey
ALTER TABLE `Account` DROP FOREIGN KEY `Account_userId_fkey`;
-- DropForeignKey
ALTER TABLE `Session` DROP FOREIGN KEY `Session_userId_fkey`;
-- AlterTable
ALTER TABLE `Publisher` DROP COLUMN `emailVerified`,
ADD COLUMN `userId` VARCHAR(191) NULL;
-- CreateTable
CREATE TABLE `User` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NULL,
`email` VARCHAR(191) NULL,
`emailVerified` DATETIME(3) NULL,
`image` VARCHAR(191) NULL,
`publisherId` VARCHAR(191) NULL,
UNIQUE INDEX `User_email_key`(`email`),
UNIQUE INDEX `User_publisherId_key`(`publisherId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateIndex
CREATE UNIQUE INDEX `Publisher_userId_key` ON `Publisher`(`userId`);
-- AddForeignKey
ALTER TABLE `Publisher` ADD CONSTRAINT `Publisher_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Account` ADD CONSTRAINT `Account_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
-- This is an empty migration.
-- CREATE INDEX `Publisher_email_key` ON `Publisher` (email);

View File

@ -1,23 +0,0 @@
-- AlterTable
ALTER TABLE `Publisher` MODIFY `role` ENUM('ADMIN', 'POWERUSER', 'USER', 'EXTERNAL') NOT NULL DEFAULT 'USER';
-- CreateTable
CREATE TABLE `Report` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`date` DATETIME(3) NOT NULL,
`publisherId` VARCHAR(191) NOT NULL,
`assignmentId` INTEGER NOT NULL,
`placementCount` INTEGER NOT NULL,
`videoCount` INTEGER NOT NULL,
`returnVisitInfoCount` INTEGER NOT NULL,
`conversationCount` INTEGER NOT NULL,
`experienceInfo` VARCHAR(191) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Report` ADD CONSTRAINT `Report_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Report` ADD CONSTRAINT `Report_assignmentId_fkey` FOREIGN KEY (`assignmentId`) REFERENCES `Assignment`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,23 +0,0 @@
/*
Warnings:
- Added the required column `locationId` to the `Report` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE `Report` DROP FOREIGN KEY `Report_assignmentId_fkey`;
-- AlterTable
ALTER TABLE `Report` ADD COLUMN `locationId` INTEGER NOT NULL,
MODIFY `assignmentId` INTEGER NULL,
MODIFY `placementCount` INTEGER NULL,
MODIFY `videoCount` INTEGER NULL,
MODIFY `returnVisitInfoCount` INTEGER NULL,
MODIFY `conversationCount` INTEGER NULL,
MODIFY `experienceInfo` VARCHAR(191) NULL;
-- AddForeignKey
ALTER TABLE `Report` ADD CONSTRAINT `Report_locationId_fkey` FOREIGN KEY (`locationId`) REFERENCES `Location`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Report` ADD CONSTRAINT `Report_assignmentId_fkey` FOREIGN KEY (`assignmentId`) REFERENCES `Assignment`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `Report` MODIFY `experienceInfo` LONGTEXT NULL;

View File

@ -1,8 +0,0 @@
-- DropForeignKey
ALTER TABLE `Report` DROP FOREIGN KEY `Report_locationId_fkey`;
-- AlterTable
ALTER TABLE `Report` MODIFY `locationId` INTEGER NULL;
-- AddForeignKey
ALTER TABLE `Report` ADD CONSTRAINT `Report_locationId_fkey` FOREIGN KEY (`locationId`) REFERENCES `Location`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `endDate` DATETIME(3) NULL,
ADD COLUMN `repeatWeekly` BOOLEAN NULL;

View File

@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the column `isTentative` on the `assignment` table. All the data in the column will be lost.
- You are about to drop the column `assignmentId` on the `report` table. All the data in the column will be lost.
- A unique constraint covering the columns `[publicGuid]` on the table `Assignment` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[reportId]` on the table `Shift` will be added. If there are existing duplicate values, this will fail.
*/
-- DropForeignKey
ALTER TABLE `Report` DROP FOREIGN KEY `Report_assignmentId_fkey`;
-- AlterTable
ALTER TABLE `Assignment` DROP COLUMN `isTentative`,
ADD COLUMN `isConfirmed` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `isMailSent` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `publicGuid` VARCHAR(191) NULL;
-- AlterTable
ALTER TABLE `Report` DROP COLUMN `assignmentId`;
-- AlterTable
ALTER TABLE `Shift` ADD COLUMN `reportId` INTEGER NULL;
-- CreateIndex
CREATE UNIQUE INDEX `Assignment_publicGuid_key` ON `Assignment`(`publicGuid`);
-- CreateIndex
CREATE UNIQUE INDEX `Shift_reportId_key` ON `Shift`(`reportId`);
-- AddForeignKey
ALTER TABLE `Shift` ADD CONSTRAINT `Shift_reportId_fkey` FOREIGN KEY (`reportId`) REFERENCES `Report`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,9 +0,0 @@
-- AlterTable
ALTER TABLE `Location` ADD COLUMN `backupLocationId` INTEGER NULL,
ADD COLUMN `content` TEXT NULL;
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `isTrained` BOOLEAN NOT NULL DEFAULT false;
-- AddForeignKey
ALTER TABLE `Location` ADD CONSTRAINT `Location_backupLocationId_fkey` FOREIGN KEY (`backupLocationId`) REFERENCES `Location`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,12 +0,0 @@
/*
Warnings:
- You are about to drop the column `isActive` on the `assignment` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE `Assignment` DROP COLUMN `isActive`,
ADD COLUMN `isTentative` BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE `Location` MODIFY `content` LONGTEXT NULL;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `repeatFrequency` INTEGER NULL;

View File

@ -1,4 +0,0 @@
-- AlterTable
ALTER TABLE `Location` ADD COLUMN `picture1` VARCHAR(191) NULL,
ADD COLUMN `picture2` VARCHAR(191) NULL,
ADD COLUMN `picture3` VARCHAR(191) NULL;

View File

@ -1,15 +0,0 @@
/*
Warnings:
- You are about to drop the column `isWithTransport` on the `Availability` table. All the data in the column will be lost.
- The values [SpecialPioneer,Missionary,CircuitOverseer] on the enum `Publisher_type` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterTable
ALTER TABLE `Availability` DROP COLUMN `isWithTransport`,
ADD COLUMN `isWithTransportIn` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `isWithTransportOut` BOOLEAN NOT NULL DEFAULT false,
MODIFY `type` ENUM('Weekly', 'Monthly', 'OneTime', 'PreviousAssignment', 'ReplacementOnly') NOT NULL DEFAULT 'Weekly';
-- AlterTable
ALTER TABLE `Publisher` MODIFY `type` ENUM('Publisher', 'Bethelite', 'RegularPioneer', 'SpecialPioneer_Missionary') NOT NULL DEFAULT 'Publisher';

View File

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `alwaysAsFamily` BOOLEAN NULL DEFAULT false;
-- AlterTable
ALTER TABLE `Shift` ADD COLUMN `isPublished` BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,18 +0,0 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `dateOfEntry` DATETIME(3) NULL;
-- CreateTable
CREATE TABLE `Message` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`publisherId` VARCHAR(191) NOT NULL,
`date` DATETIME(3) NOT NULL,
`content` VARCHAR(191) NOT NULL,
`isRead` BOOLEAN NOT NULL DEFAULT false,
`isPublic` BOOLEAN NOT NULL DEFAULT false,
`type` ENUM('Email', 'SMS', 'Push', 'InApp') NOT NULL DEFAULT 'Email',
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Message` ADD CONSTRAINT `Message_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,21 +0,0 @@
/*
Warnings:
- You are about to drop the column `isTentative` on the `Assignment` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE `Assignment`
ADD COLUMN `isBySystem` BOOLEAN NOT NULL DEFAULT FALSE;
-- Depending on your DBMS, you might need to execute one statement at a time.
-- Especially, the UPDATE statement should be run separately.
UPDATE `Assignment` SET `isBySystem` = isTentative;
-- Drop the isTentative column
ALTER TABLE `Assignment` DROP COLUMN `isTentative`;
-- AlterTable
ALTER TABLE `Report`
ADD COLUMN `type` ENUM('ServiceReport', 'Experience', 'Feedback_Problem', 'Feedback_Suggestion', 'Feedback') NOT NULL DEFAULT 'ServiceReport';

View File

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE `Availability` ADD COLUMN `parentAvailabilityId` INTEGER NULL;
-- AddForeignKey
ALTER TABLE `Availability` ADD CONSTRAINT `Availability_parentAvailabilityId_fkey` FOREIGN KEY (`parentAvailabilityId`) REFERENCES `Availability`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `isSubscribedToCoverMe` BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN `isSubscribedToReminders` BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,20 +0,0 @@
-- CreateTable
CREATE TABLE `EventLog` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`date` DATETIME(3) NOT NULL,
`publisherId` VARCHAR(191) NULL,
`shiftId` INTEGER NULL,
`content` VARCHAR(5000) NOT NULL,
`type` ENUM('AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail') NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `EventLog`
ADD CONSTRAINT `EventLog_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `EventLog`
ADD CONSTRAINT `EventLog_shiftId_fkey` FOREIGN KEY (`shiftId`) REFERENCES `Shift` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `lastLogin` DATETIME(3) NULL;

View File

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE `User`
ADD COLUMN `passwordHashLocalAccount` VARCHAR(191) NULL;

View File

@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE `EventLog`
MODIFY `type` ENUM(
'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted'
) NOT NULL;

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `Publisher` ADD COLUMN `pushSubscription` JSON NULL;

View File

@ -1,33 +0,0 @@
-- 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;

View File

@ -1,19 +0,0 @@
-- 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);

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE `User` ADD COLUMN `emailVerifyToken` VARCHAR(191) NULL;

View File

@ -0,0 +1,313 @@
-- CreateTable
CREATE TABLE `Publisher` (
`id` VARCHAR(191) NOT NULL,
`firstName` VARCHAR(191) NOT NULL,
`lastName` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`phone` VARCHAR(191) NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`isImported` BOOLEAN NOT NULL DEFAULT false,
`isTrained` BOOLEAN NOT NULL DEFAULT false,
`age` INTEGER NULL,
`userId` VARCHAR(191) NULL,
`role` ENUM('ADMIN', 'POWERUSER', 'USER', 'EXTERNAL') NOT NULL DEFAULT 'USER',
`desiredShiftsPerMonth` INTEGER NOT NULL DEFAULT 4,
`isMale` BOOLEAN NOT NULL DEFAULT true,
`isNameForeign` BOOLEAN NOT NULL DEFAULT false,
`isSubscribedToCoverMe` BOOLEAN NOT NULL DEFAULT false,
`isSubscribedToReminders` BOOLEAN NOT NULL DEFAULT false,
`familyHeadId` VARCHAR(191) NULL,
`alwaysAsFamily` BOOLEAN NULL DEFAULT false,
`type` ENUM('Publisher', 'Bethelite', 'RegularPioneer', 'SpecialPioneer_Missionary') NOT NULL DEFAULT 'Publisher',
`town` VARCHAR(191) NULL,
`comments` VARCHAR(191) NULL,
`lastLogin` DATETIME(3) NULL,
`pushSubscription` JSON NULL,
`congregationId` INTEGER NULL,
`locale` VARCHAR(191) NULL DEFAULT 'bg',
UNIQUE INDEX `Publisher_email_key`(`email`),
UNIQUE INDEX `Publisher_userId_key`(`userId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 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;
-- CreateTable
CREATE TABLE `Availability` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`publisherId` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL,
`dayOfMonth` INTEGER NULL,
`weekOfMonth` INTEGER NULL,
`startTime` DATETIME(3) NOT NULL,
`endTime` DATETIME(3) NOT NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`type` ENUM('Weekly', 'Monthly', 'OneTime', 'PreviousAssignment', 'ReplacementOnly') NOT NULL DEFAULT 'Weekly',
`isWithTransportIn` BOOLEAN NOT NULL DEFAULT false,
`isWithTransportOut` BOOLEAN NOT NULL DEFAULT false,
`isFromPreviousAssignment` BOOLEAN NOT NULL DEFAULT false,
`isFromPreviousMonth` BOOLEAN NOT NULL DEFAULT false,
`repeatWeekly` BOOLEAN NULL,
`repeatFrequency` INTEGER NULL,
`endDate` DATETIME(3) NULL,
`dateOfEntry` DATETIME(3) NULL,
`parentAvailabilityId` INTEGER NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `CartEvent` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`startTime` DATETIME(3) NOT NULL,
`endTime` DATETIME(3) NOT NULL,
`shiftDuration` INTEGER NOT NULL,
`dayofweek` ENUM('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday') NOT NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`locationId` INTEGER NOT NULL,
`eventType` ENUM('PW_Cart', 'KH_Cleaning') NOT NULL DEFAULT 'PW_Cart',
`numberOfPublishers` INTEGER NOT NULL DEFAULT 3,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Shift` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`cartEventId` INTEGER NOT NULL,
`name` VARCHAR(191) NOT NULL,
`startTime` DATETIME(3) NOT NULL,
`endTime` DATETIME(3) NOT NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`requiresTransport` BOOLEAN NOT NULL DEFAULT false,
`notes` VARCHAR(191) NULL,
`reportId` INTEGER NULL,
`isPublished` BOOLEAN NOT NULL DEFAULT false,
UNIQUE INDEX `Shift_reportId_key`(`reportId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Assignment` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`shiftId` INTEGER NOT NULL,
`publisherId` VARCHAR(191) NOT NULL,
`isBySystem` BOOLEAN NOT NULL DEFAULT false,
`isConfirmed` BOOLEAN NOT NULL DEFAULT false,
`isWithTransport` BOOLEAN NOT NULL DEFAULT false,
`isMailSent` BOOLEAN NOT NULL DEFAULT false,
`publicGuid` VARCHAR(191) NULL,
`originalPublisherId` VARCHAR(191) NULL,
UNIQUE INDEX `Assignment_publicGuid_key`(`publicGuid`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Location` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`address` VARCHAR(191) NOT NULL,
`isActive` BOOLEAN NOT NULL DEFAULT true,
`content` LONGTEXT NULL,
`picture1` VARCHAR(191) NULL,
`picture2` VARCHAR(191) NULL,
`picture3` VARCHAR(191) NULL,
`backupLocationId` INTEGER NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Report` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`date` DATETIME(3) NOT NULL,
`publisherId` VARCHAR(191) NOT NULL,
`locationId` INTEGER NULL,
`placementCount` INTEGER NULL,
`videoCount` INTEGER NULL,
`returnVisitInfoCount` INTEGER NULL,
`conversationCount` INTEGER NULL,
`experienceInfo` LONGTEXT NULL,
`type` ENUM('ServiceReport', 'Experience', 'Feedback_Problem', 'Feedback_Suggestion', 'Feedback') NOT NULL DEFAULT 'ServiceReport',
`comments` VARCHAR(191) NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Survey` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`content` VARCHAR(191) NOT NULL,
`answers` JSON NULL,
`publicFrom` DATETIME(3) NULL,
`publicUntil` DATETIME(3) NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Message` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`publisherId` VARCHAR(191) NOT NULL,
`date` DATETIME(3) NOT NULL,
`content` VARCHAR(191) NOT NULL,
`isRead` BOOLEAN NOT NULL DEFAULT false,
`isPublic` BOOLEAN NOT NULL DEFAULT false,
`type` ENUM('Email', 'SMS', 'Push', 'InApp') NOT NULL DEFAULT 'Email',
`publicUntil` DATETIME(3) NULL,
`shownDate` DATETIME(3) NULL,
`answer` VARCHAR(191) NULL,
`answerDate` DATETIME(3) NULL,
`surveyId` INTEGER NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `EventLog` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`date` DATETIME(3) NOT NULL,
`publisherId` VARCHAR(191) NULL,
`shiftId` INTEGER NULL,
`content` VARCHAR(5000) NOT NULL,
`type` ENUM('AssignmentReplacementManual', 'AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail', 'PasswordResetRequested', 'PasswordResetEmailConfirmed', 'PasswordResetCompleted') NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `User` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NULL,
`email` VARCHAR(191) NULL,
`emailVerifyToken` VARCHAR(191) NULL,
`emailVerified` DATETIME(3) NULL,
`image` VARCHAR(191) NULL,
`passwordHashLocalAccount` VARCHAR(191) NULL,
`publisherId` VARCHAR(191) NULL,
UNIQUE INDEX `User_email_key`(`email`),
UNIQUE INDEX `User_publisherId_key`(`publisherId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Account` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NOT NULL,
`type` VARCHAR(191) NOT NULL,
`provider` VARCHAR(191) NOT NULL,
`providerAccountId` VARCHAR(191) NOT NULL,
`refresh_token` VARCHAR(191) NULL,
`access_token` VARCHAR(191) NULL,
`expires_at` INTEGER NULL,
`token_type` VARCHAR(191) NULL,
`scope` VARCHAR(191) NULL,
`id_token` VARCHAR(191) NULL,
`session_state` VARCHAR(191) NULL,
UNIQUE INDEX `Account_provider_providerAccountId_key`(`provider`, `providerAccountId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Session` (
`id` VARCHAR(191) NOT NULL,
`sessionToken` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NOT NULL,
`expires` DATETIME(3) NOT NULL,
UNIQUE INDEX `Session_sessionToken_key`(`sessionToken`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `VerificationToken` (
`identifier` VARCHAR(191) NOT NULL,
`token` VARCHAR(191) NOT NULL,
`expires` DATETIME(3) NOT NULL,
UNIQUE INDEX `VerificationToken_token_key`(`token`),
UNIQUE INDEX `VerificationToken_identifier_token_key`(`identifier`, `token`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Settings` (
`key` VARCHAR(191) NOT NULL,
`value` VARCHAR(191) NOT NULL,
`description` VARCHAR(191) NULL,
PRIMARY KEY (`key`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Publisher` ADD CONSTRAINT `Publisher_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Publisher` ADD CONSTRAINT `Publisher_familyHeadId_fkey` FOREIGN KEY (`familyHeadId`) REFERENCES `Publisher`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- 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 `Availability` ADD CONSTRAINT `Availability_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Availability` ADD CONSTRAINT `Availability_parentAvailabilityId_fkey` FOREIGN KEY (`parentAvailabilityId`) REFERENCES `Availability`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CartEvent` ADD CONSTRAINT `CartEvent_locationId_fkey` FOREIGN KEY (`locationId`) REFERENCES `Location`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Shift` ADD CONSTRAINT `Shift_cartEventId_fkey` FOREIGN KEY (`cartEventId`) REFERENCES `CartEvent`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Shift` ADD CONSTRAINT `Shift_reportId_fkey` FOREIGN KEY (`reportId`) REFERENCES `Report`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Assignment` ADD CONSTRAINT `Assignment_shiftId_fkey` FOREIGN KEY (`shiftId`) REFERENCES `Shift`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Assignment` ADD CONSTRAINT `Assignment_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE 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;
-- AddForeignKey
ALTER TABLE `Location` ADD CONSTRAINT `Location_backupLocationId_fkey` FOREIGN KEY (`backupLocationId`) REFERENCES `Location`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Report` ADD CONSTRAINT `Report_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Report` ADD CONSTRAINT `Report_locationId_fkey` FOREIGN KEY (`locationId`) REFERENCES `Location`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Message` ADD CONSTRAINT `Message_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Message` ADD CONSTRAINT `Message_surveyId_fkey` FOREIGN KEY (`surveyId`) REFERENCES `Survey`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `EventLog` ADD CONSTRAINT `EventLog_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `EventLog` ADD CONSTRAINT `EventLog_shiftId_fkey` FOREIGN KEY (`shiftId`) REFERENCES `Shift`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Account` ADD CONSTRAINT `Account_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -264,6 +264,15 @@ enum MessageType {
InApp
}
model Survey {
id Int @id @default(autoincrement())
content String
answers Json?
messages Message[]
publicFrom DateTime?
publicUntil DateTime?
}
model Message {
id Int @id @default(autoincrement())
publisher Publisher @relation(fields: [publisherId], references: [id])
@ -274,6 +283,12 @@ model Message {
isPublic Boolean @default(false)
type MessageType @default(Email)
publicUntil DateTime?
shownDate DateTime?
answer String?
answerDate DateTime?
Survey Survey? @relation(fields: [surveyId], references: [id], onDelete: Cascade)
surveyId Int?
}
enum EventLogType {
@ -348,3 +363,9 @@ model VerificationToken {
@@unique([identifier, token])
}
model Settings {
key String @id
value String
description String?
}

View File

@ -390,43 +390,7 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
///console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`);
// include repeating weekly availabilities. generate occurrences for the month
// convert matching weekly availabilities to availabilities for the day to make further processing easier on the client.
publishers.forEach(pub => {
pub.availabilities = pub.availabilities.map(avail => {
if (avail.dayOfMonth == null) {
if (filterAvailabilitiesByDate && !isForTheMonth) {
// filter out repeating availabilities when on other day of week
if (filterTimeFrom) {
if (avail.dayofweek != dayOfWeekEnum) {
return null;
}
}
}
let newStart = new Date(filterDate);
newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0);
let newEnd = new Date(filterDate);
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
return {
...avail,
startTime: newStart,
endTime: newEnd
}
}
else {
if (filterAvailabilitiesByDate && !isForTheMonth) {
if (avail.startTime >= filterTimeFrom && avail.startTime <= filterTimeTo) {
return avail;
}
return null;
}
return avail;
}
})
.filter(avail => avail !== null);
});
// ---------------------------------------------- statistics ----------------------------------------------
let currentWeekStart, currentWeekEnd;
if (isWithStats) {
@ -494,8 +458,45 @@ async function filterPublishersNew(selectFields, filterDate, isExactTime = false
return avail.startTime >= filterDate && avail.startTime <= filterTimeTo;
});
}
});
// ----------------------------------------------
// include repeating weekly availabilities. generate occurrences for the month
// convert matching weekly availabilities to availabilities for the day to make further processing easier on the client.
publishers.forEach(pub => {
pub.availabilities = pub.availabilities.map(avail => {
if (avail.dayOfMonth == null) {
if (filterAvailabilitiesByDate && !isForTheMonth) {
// filter out repeating availabilities when on other day of week
if (filterTimeFrom) {
if (avail.dayofweek != dayOfWeekEnum) {
return null;
}
}
}
let newStart = new Date(filterDate);
newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0);
let newEnd = new Date(filterDate);
newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0);
return {
...avail,
startTime: newStart,
endTime: newEnd
}
}
else {
if (filterAvailabilitiesByDate && !isForTheMonth) {
if (avail.startTime >= filterTimeFrom && avail.startTime <= filterTimeTo) {
return avail;
}
return null;
}
return avail;
}
})
.filter(avail => avail !== null);
});
// ToDo: test case/unit test
// ToDo: check and validate the filtering and calculations
if (isExactTime) {
@ -835,246 +836,302 @@ async function getCoverMePublisherEmails(shiftId) {
return { shift, availablePublishers: availablePublishers, subscribedPublishers };
}
// ### COPIED TO shift api (++) ###
// ### COPIED TO shift api (/shiftgenerate.ts) (++) ###
/** JSDoc
* Generates a schedule.
*
0. generate shifts and assign publishers from the previous month if still available
1. Make sure we always put people only when they are available.
2. First provision one male or two females that are available for transport in the first and last shifts.
3, Then gradually fill all other shifts with day by day troughout the whole month (monthInfo.firstMonday to .lastSunday) with first one, then two, then 3 and wherever possible more (up to CartEvent.numberOfPublishers number)
4. Some publishers are available only at specific time (somoetimes only once) and other are more available. if people are available only for this time, prioritize them so they are not left behind.
5. prioritize based on publisher's desiredShiftsPerMonth and previous months assignments.
6. Idealy noone should be more than once a week. disqualify publishers already on a shift this week. only assign them if there are no other options and we have less than 3 publishers on a specific shift.
*
* @param {Axios} axios Axios instance for making requests.
* @param {string} date The date for the schedule.
* @param {boolean} [copyFromPreviousMonth=false] Whether to copy from the previous month.
* @param {boolean} [autoFill=false] Whether to autofill data.
* @param {boolean} forDay Specific day flag.
*/
async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) {
let missingPublishers = [];
let publishersWithChangedPref = [];
// /** JSDoc
// * Generates a schedule.
// *
// 0. generate shifts and assign publishers from the previous month if still available
// 1. Make sure we always put people only when they are available.
// 2. First provision one male or two females that are available for transport in the first and last shifts.
// 3, Then gradually fill all other shifts with day by day troughout the whole month (monthInfo.firstMonday to .lastSunday) with first one, then two, then 3 and wherever possible more (up to CartEvent.numberOfPublishers number)
// 4. Some publishers are available only at specific time (somoetimes only once) and other are more available. if people are available only for this time, prioritize them so they are not left behind.
// 5. prioritize based on publisher's desiredShiftsPerMonth and previous months assignments.
// 6. Idealy noone should be more than once a week. disqualify publishers already on a shift this week. only assign them if there are no other options and we have less than 3 publishers on a specific shift.
// *
// * @param {Axios} axios Axios instance for making requests.
// * @param {string} date The date for the schedule.
// * @param {boolean} [copyFromPreviousMonth=false] Whether to copy from the previous month.
// * @param {boolean} [autoFill=false] Whether to autofill data.
// * @param {boolean} forDay Specific day flag.
// */
// async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) {
// let missingPublishers = [];
// let publishersWithChangedPref = [];
const prisma = common.getPrismaClient();
try {
const monthInfo = common.getMonthDatesInfo(new Date(date));
const lastMonthInfo = common.getMonthDatesInfo(new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 1, 1));
// const prisma = common.getPrismaClient();
// try {
// const monthInfo = common.getMonthDatesInfo(new Date(date));
// const lastMonthInfo = common.getMonthDatesInfo(new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 1, 1));
if (forDay) {
await DeleteShiftsForDay(monthInfo.date);
} else {
await DeleteShiftsForMonth(monthInfo);
}
// if (forDay) {
// await DeleteShiftsForDay(monthInfo.date);
// } else {
// await DeleteShiftsForMonth(monthInfo);
// }
const events = await prisma.cartEvent.findMany({
where: {
isActive: true
}
});
let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo);
let publishers = await getAllPublishersWithStatisticsMonth('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', date, false, true, false, true, true);
// const events = await prisma.cartEvent.findMany({
// where: {
// isActive: true
// }
// });
// let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo);
// let publishers = await getAllPublishersWithStatisticsMonth('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', date, false, true, false, true, true);
let shiftAssignments = [];
let day = new Date(monthInfo.firstMonday);
let endDate = monthInfo.lastSunday;
let dayNr = 1;
let weekNr = 1;
// let shiftAssignments = [];
// let day = new Date(monthInfo.firstMonday);
// let endDate = monthInfo.lastSunday;
// let dayNr = 1;
// let weekNr = 1;
if (forDay) {
day = monthInfo.date;
endDate.setDate(monthInfo.date.getDate() + 1);
dayNr = monthInfo.date.getDate();
weekNr = common.getWeekNumber(monthInfo.date);
}
// if (forDay) {
// day = monthInfo.date;
// endDate.setDate(monthInfo.date.getDate() + 1);
// dayNr = monthInfo.date.getDate();
// weekNr = common.getWeekNumber(monthInfo.date);
// }
let publishersThisWeek = [];
// let publishersThisWeek = [];
// 0. generate shifts and assign publishers from the previous month if still available
while (day < endDate) {
let availablePubsForTheDay = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, true);
console.log("passing schedule generation for " + day.toLocaleDateString());
const dayOfM = day.getDate();
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
let dayName = common.DaysOfWeekArray[day.getDayEuropean()];
const event = events.find((event) => event.dayofweek == dayName && (event.dayOfMonth == null || event.dayOfMonth == dayOfM));
// // 0. generate shifts and assign publishers from the previous month if still available
// while (day < endDate) {
// let availablePubsForTheDay = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, true);
// console.log("passing schedule generation for " + day.toLocaleDateString());
// const dayOfM = day.getDate();
// let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
// let dayName = common.DaysOfWeekArray[day.getDayEuropean()];
// const event = events.find((event) => event.dayofweek == dayName && (event.dayOfMonth == null || event.dayOfMonth == dayOfM));
if (!event) {
day.setDate(day.getDate() + 1);
continue;
}
// if (!event) {
// day.setDate(day.getDate() + 1);
// continue;
// }
event.startTime = new Date(event.startTime);
event.endTime = new Date(event.endTime);
// event.startTime = new Date(event.startTime);
// event.endTime = new Date(event.endTime);
let startTime = new Date(day);
startTime.setHours(event.startTime.getHours());
startTime.setMinutes(event.startTime.getMinutes());
let endTime = new Date(day);
endTime.setHours(event.endTime.getHours());
endTime.setMinutes(event.endTime.getMinutes());
// let startTime = new Date(day);
// startTime.setHours(event.startTime.getHours());
// startTime.setMinutes(event.startTime.getMinutes());
// let endTime = new Date(day);
// endTime.setHours(event.endTime.getHours());
// endTime.setMinutes(event.endTime.getMinutes());
let shiftStart = new Date(startTime);
let shiftEnd = new Date(startTime);
shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
// let shiftStart = new Date(startTime);
// let shiftEnd = new Date(startTime);
// shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
let shiftNr = 0;
while (shiftEnd <= endTime) {
shiftNr++;
const __shiftName = String(shiftStart.getHours()).padStart(2, "0") + ":" + String(shiftStart.getMinutes()).padStart(2, "0") + " - " + String(shiftEnd.getHours()).padStart(2, "0") + ":" + String(shiftEnd.getMinutes()).padStart(2, "0");
shiftAssignments = [];
let isTransportRequired = shiftNr == 1 || shiftEnd.getTime() == endTime.getTime();
// let shiftNr = 0;
// while (shiftEnd <= endTime) {
// shiftNr++;
// const __shiftName = String(shiftStart.getHours()).padStart(2, "0") + ":" + String(shiftStart.getMinutes()).padStart(2, "0") + " - " + String(shiftEnd.getHours()).padStart(2, "0") + ":" + String(shiftEnd.getMinutes()).padStart(2, "0");
// shiftAssignments = [];
// let isTransportRequired = shiftNr == 1 || shiftEnd.getTime() == endTime.getTime();
const shiftLastMonthSameDay = findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr);
// const shiftLastMonthSameDay = findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr);
if (shiftLastMonthSameDay) {
for (let assignment of shiftLastMonthSameDay.assignments) {
let publisher = assignment.publisher;
console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName);
let availability = await FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr);
console.log("availability " + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime));
// if (shiftLastMonthSameDay) {
// for (let assignment of shiftLastMonthSameDay.assignments) {
// let publisher = assignment.publisher;
// console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName);
// let availability = await FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr);
// console.log("availability " + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime));
if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) {
shiftAssignments.push({
publisherId: publisher.id,
isConfirmed: true,
isWithTransportIn: availability.isWithTransportIn,
isWithTransportOut: availability.isWithTransportOut
});
publishersThisWeek.push(publisher.id);
}
}
}
// if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) {
// shiftAssignments.push({
// publisherId: publisher.id,
// isConfirmed: true,
// isWithTransportIn: availability.isWithTransportIn,
// isWithTransportOut: availability.isWithTransportOut
// });
// publishersThisWeek.push(publisher.id);
// }
// }
// }
let publishersNeeded = event.numberOfPublishers - shiftAssignments.length;
//ToDo: check if getAvailablePublishersForShift is working correctly
let availablePublishers = await getAvailablePublishersForShift(shiftStart, shiftEnd, publishers, publishersThisWeek);
// let publishersNeeded = event.numberOfPublishers - shiftAssignments.length;
// //ToDo: check if getAvailablePublishersForShift is working correctly
// let availablePublishers = await getAvailablePublishersForShift(shiftStart, shiftEnd, publishers, publishersThisWeek);
console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + " for the day: " + availablePubsForTheDay.length);
// console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + " for the day: " + availablePubsForTheDay.length);
// Prioritize publishers with minimal availability
// SKIP ADDING PUBLISHERS FOR NOW
// availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount);
// // Prioritize publishers with minimal availability
// // SKIP ADDING PUBLISHERS FOR NOW
// // availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount);
// for (let i = 0; i < publishersNeeded; i++) {
// if (availablePublishers[i]) {
// shiftAssignments.push({ publisherId: availablePublishers[i].id });
// publishersThisWeek.push(availablePublishers[i].id);
// }
// }
// // for (let i = 0; i < publishersNeeded; i++) {
// // if (availablePublishers[i]) {
// // shiftAssignments.push({ publisherId: availablePublishers[i].id });
// // publishersThisWeek.push(availablePublishers[i].id);
// // }
// // }
const createdShift = await prisma.shift.create({
data: {
startTime: shiftStart,
endTime: shiftEnd,
name: event.dayofweek + " " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString(),
requiresTransport: isTransportRequired,
cartEvent: {
connect: {
id: event.id,
},
},
assignments: {
create: shiftAssignments.map((a) => {
return {
publisher: {
connect: { id: a.publisherId }
},
isConfirmed: a.isConfirmed,
isBySystem: true,
};
}),
},
},
});
// const createdShift = await prisma.shift.create({
// data: {
// startTime: shiftStart,
// endTime: shiftEnd,
// name: event.dayofweek + " " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString(),
// requiresTransport: isTransportRequired,
// cartEvent: {
// connect: {
// id: event.id,
// },
// },
// assignments: {
// create: shiftAssignments.map((a) => {
// return {
// publisher: {
// connect: { id: a.publisherId }
// },
// isConfirmed: a.isConfirmed,
// isBySystem: true,
// };
// }),
// },
// },
// });
shiftStart = new Date(shiftEnd);
shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
}
// shiftStart = new Date(shiftEnd);
// shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
// }
day.setDate(day.getDate() + 1);
dayNr++;
if (common.DaysOfWeekArray[day.getDayEuropean()] === DayOfWeek.Sunday) {
weekNr++;
publishersThisWeek = [];
publishers.forEach(p => p.currentWeekAssignments = 0);
}
if (forDay) break;
}
// day.setDate(day.getDate() + 1);
// dayNr++;
// if (common.DaysOfWeekArray[day.getDayEuropean()] === DayOfWeek.Sunday) {
// weekNr++;
// publishersThisWeek = [];
// publishers.forEach(p => p.currentWeekAssignments = 0);
// }
// if (forDay) break;
// }
let allShifts = await prisma.shift.findMany({
where: {
startTime: {
gte: monthInfo.firstMonday,
lt: monthInfo.lastSunday,
},
},
include: {
assignments: {
include: {
publisher: true,
},
},
},
});
// let allShifts = await prisma.shift.findMany({
// where: {
// startTime: {
// gte: monthInfo.firstMonday,
// lt: monthInfo.lastSunday,
// },
// },
// include: {
// assignments: {
// include: {
// publisher: true,
// },
// },
// },
// });
console.log(" second pass " + monthInfo.monthName + " " + monthInfo.year);
// 2. First pass - prioritize shifts with transport where it is needed
day = monthInfo.firstMonday;
dayNr = 1;
weekNr = 1;
while (day < endDate) {
let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
if (event) {
let availablePubsForTheDay = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, true);
// console.log(" second pass " + monthInfo.monthName + " " + monthInfo.year);
// // 2. First pass - prioritize shifts with transport where it is needed
// day = monthInfo.firstMonday;
// dayNr = 1;
// weekNr = 1;
// while (day < endDate) {
// let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day);
// let event = events.find((event) => event.dayofweek == common.DaysOfWeekArray[day.getDayEuropean()]);
// if (event) {
// let availablePubsForTheDay = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, true);
let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day));
let transportShifts = shifts.filter(s => s.requiresTransport);
transportShifts.forEach(shift => {
let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id));
availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount);
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
if (publishersNeeded > 0) {//get the beset match
if (availablePublishers[0]) {
shift.assignments.push({ publisherId: availablePublishers[i].id });
}
// let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day));
// let transportShifts = shifts.filter(s => s.requiresTransport);
// transportShifts.forEach(shift => {
// let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id));
// availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount);
// let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
// if (publishersNeeded > 0) {//get the beset match
// if (availablePublishers[0]) {
// shift.assignments.push({ publisherId: availablePublishers[i].id });
// }
}
});
// 3. Second pass - fill the rest of the shifts
let shiftsToFill = shifts.filter(s => !s.requiresTransport);
shiftsToFill.forEach(shift => {
let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id));
availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount);
let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
if (publishersNeeded > 0) {//get the beset match
if (availablePublishers[0]) {
shift.assignments.push({ publisherId: availablePublishers[i].id });
}
// }
// });
// // 3. Second pass - fill the rest of the shifts
// let shiftsToFill = shifts.filter(s => !s.requiresTransport);
// shiftsToFill.forEach(shift => {
// let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id));
// availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount);
// let publishersNeeded = event.numberOfPublishers - shift.assignments.length;
// if (publishersNeeded > 0) {//get the beset match
// if (availablePublishers[0]) {
// shift.assignments.push({ publisherId: availablePublishers[i].id });
// }
}
});
}
// }
// });
// }
day.setDate(day.getDate() + 1);
}
// day.setDate(day.getDate() + 1);
// }
if (!forDay) {
console.log("###############################################");
console.log(" DONE CREATING SCHEDULE FOR " + monthInfo.monthName + " " + monthInfo.year);
console.log("###############################################");
}
// if (!forDay) {
// console.log("###############################################");
// console.log(" DONE CREATING SCHEDULE FOR " + monthInfo.monthName + " " + monthInfo.year);
// console.log("###############################################");
// }
return {};
} catch (error) {
console.log(error);
return { error: error };
}
}
// return {};
// } catch (error) {
// console.log(error);
// return { error: error };
// }
// }
// async function getShiftsFromLastMonth(monthInfo) {
// const prisma = common.getPrismaClient();
// // Fetch shifts for the month
// const rawShifts = await prisma.shift.findMany({
// where: {
// startTime: {
// gte: monthInfo.firstMonday,
// lte: monthInfo.lastSunday,
// },
// },
// include: {
// assignments: {
// include: {
// publisher: true,
// },
// },
// },
// });
// // Process shifts to add weekNr and shiftNr
// return rawShifts.map(shift => ({
// ...shift,
// weekNr: common.getWeekNumber(new Date(shift.startTime)),
// shiftNr: rawShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(shift.startTime)).indexOf(shift) + 1,
// weekDay: common.DaysOfWeekArray[new Date(shift.startTime).getDayEuropean()],
// }));
// }
// function findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr) {
// let weekDay = common.DaysOfWeekArray[day.getDayEuropean()];
// return shiftsLastMonth.find(s => {
// return s.weekNr === weekNr &&
// s.shiftNr === shiftNr &&
// s.weekDay === weekDay;
// });
// }
// //ToDo use bulk find instead of loop
// async function getAvailablePublishersForShift(startTime, endTime, allPublishers, publishersThisWeek) {
// let availablePublishers = [];
// for (let publisher of allPublishers) {
// let availability = await FindPublisherAvailability(publisher.id, startTime, endTime);
// if (availability && !publishersThisWeek.includes(publisher.id)) {
// availablePublishers.push(publisher);
// }
// }
// return availablePublishers;
// }
// ### COPIED TO shift api (--) ###
async function DeleteShiftsForMonth(monthInfo) {
try {
@ -1109,57 +1166,6 @@ async function DeleteShiftsForDay(date) {
}
}
async function getShiftsFromLastMonth(monthInfo) {
const prisma = common.getPrismaClient();
// Fetch shifts for the month
const rawShifts = await prisma.shift.findMany({
where: {
startTime: {
gte: monthInfo.firstMonday,
lte: monthInfo.lastSunday,
},
},
include: {
assignments: {
include: {
publisher: true,
},
},
},
});
// Process shifts to add weekNr and shiftNr
return rawShifts.map(shift => ({
...shift,
weekNr: common.getWeekNumber(new Date(shift.startTime)),
shiftNr: rawShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(shift.startTime)).indexOf(shift) + 1,
weekDay: common.DaysOfWeekArray[new Date(shift.startTime).getDayEuropean()],
}));
}
function findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr) {
let weekDay = common.DaysOfWeekArray[day.getDayEuropean()];
return shiftsLastMonth.find(s => {
return s.weekNr === weekNr &&
s.shiftNr === shiftNr &&
s.weekDay === weekDay;
});
}
//ToDo use bulk find instead of loop
async function getAvailablePublishersForShift(startTime, endTime, allPublishers, publishersThisWeek) {
let availablePublishers = [];
for (let publisher of allPublishers) {
let availability = await FindPublisherAvailability(publisher.id, startTime, endTime);
if (availability && !publishersThisWeek.includes(publisher.id)) {
availablePublishers.push(publisher);
}
}
return availablePublishers;
}
async function FindPublisherAvailability(publisherId, startDate, endDate, dayOfWeekEnum, weekNr) {
const prisma = common.getPrismaClient();
const start = new Date(startDate);
@ -1219,9 +1225,6 @@ async function FindPublisherAvailability(publisherId, startDate, endDate, dayOfW
return exactAvailabilities.length > 0 ? exactAvailabilities[0] : repeatingAvailabilities.length > 0 ? repeatingAvailabilities[0] : null;
}
// ### COPIED TO shift api (--) ###
// function matchesAvailability(avail, filterDate) {
// // Setting the start and end time of the filterDate
// filterDate.setHours(0, 0, 0, 0);
@ -1267,7 +1270,7 @@ module.exports = {
getCoverMePublisherEmails,
getAllPublishersWithStatisticsMonth,
getCalendarEvents,
GenerateSchedule,
// GenerateSchedule,
DeleteShiftsForMonth,
DeleteShiftsForDay,
};

View File

@ -1,11 +1,22 @@
const winston = require('winston');
require('winston-daily-rotate-file');
const fs = require('fs');
const path = require('path');
// Define the logs directory path
const logDirectory = path.join(__dirname, '../logs');
// Ensure the logs directory exists
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory);
}
// Define the log configuration
const logConfiguration = {
'transports': [
transports: [
new winston.transports.DailyRotateFile({
filename: './logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD', // new file is created every hour: 'YYYY-MM-DD-HH'
filename: path.join(logDirectory, 'application-%DATE%.log'),
datePattern: 'YYYY-MM-DD', // new file is created every day
zippedArchive: true,
maxSize: '20m',
maxFiles: '90d',
@ -20,6 +31,7 @@ const logConfiguration = {
)
};
// Create the logger
const logger = winston.createLogger(logConfiguration);
module.exports = logger;

View File

@ -32,7 +32,8 @@
"components/location/LocationForm.js",
"pages/cart/locations/[id].tsx.old",
"components/publisher/ShiftsList.js",
"src/helpers/data.js"
"src/helpers/data.js",
"components/survey/SurveyForm.js"
],
"exclude": [
"node_modules"