diff --git a/.vscode/launch.json b/.vscode/launch.json
index ac9398c..ed8ac12 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -55,7 +55,7 @@
"request": "launch",
"type": "node-terminal",
"cwd": "${workspaceFolder}",
- "command": "conda activate node && npm run start-env",
+ "command": "conda activate node && npm install && npm run start-env",
"env": {
"APP_ENV": "development.devserver"
}
diff --git a/_doc/ToDo.md b/_doc/ToDo.md
index d111925..abcbef7 100644
--- a/_doc/ToDo.md
+++ b/_doc/ToDo.md
@@ -238,3 +238,6 @@ in schedule admin - if a publisher is always pair & family is not in the shift -
[] new page to show EventLog (substitutions)
[] fix "login as"
[] list with open shift replacements (coverMe requests)
+[] fix statistics
+[] add notification to statistics info
+[] fix logins (apple/azure)
diff --git a/components/ConfirmationModal.tsx b/components/ConfirmationModal.tsx
index 787be29..ddf646b 100644
--- a/components/ConfirmationModal.tsx
+++ b/components/ConfirmationModal.tsx
@@ -1,29 +1,29 @@
import zIndex from "@mui/material/styles/zIndex";
+import ReactDOM from 'react-dom';
export default function ConfirmationModal({ isOpen, onClose, onConfirm, message }) {
//export default function ConfirmationModal({ isOpen, onClose, onConfirm, message })
if (!isOpen) return null;
- return (
-
-
+ const modalContent = (
+
+
{message}
-
);
+
+ return ReactDOM.createPortal(
+ modalContent,
+ document.getElementById('modal-root')
+ );
};
// const CustomCalendar = ({ month, year, shifts }) => {
// export default function CustomCalendar({ date, shifts }: CustomCalendarProps) {
\ No newline at end of file
diff --git a/components/calendar/avcalendar.tsx b/components/calendar/avcalendar.tsx
index d6497b5..cfac429 100644
--- a/components/calendar/avcalendar.tsx
+++ b/components/calendar/avcalendar.tsx
@@ -48,9 +48,19 @@ const messages = {
// Any other labels you want to translate...
};
-const AvCalendar = ({ publisherId, events, selectedDate, cartEvents }) => {
-
- const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
+const AvCalendar = ({ publisherId, events, selectedDate, cartEvents, lastPublishedDate }) => {
+ const [editLockedBefore, setEditLockedBefore] = useState(new Date(lastPublishedDate));
+ const [isAdmin, setIsAdmin] = useState(false);
+ useEffect(() => {
+ (async () => {
+ try {
+ setIsAdmin(await ProtectedRoute.IsInRole(UserRole.ADMIN));
+ } catch (error) {
+ console.error("Failed to check admin role:", error);
+ }
+ })();
+ }, []);
+ //const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN);
const [date, setDate] = useState(new Date());
//ToDo: see if we can optimize this
@@ -227,6 +237,12 @@ const AvCalendar = ({ publisherId, events, selectedDate, cartEvents }) => {
//readonly for past dates (ToDo: if not admin)
if (!isAdmin) {
if (startdate < new Date() || end < new Date() || startdate > end) return;
+ //or if schedule is published (lastPublishedDate)
+ if (editLockedBefore && startdate < editLockedBefore) {
+ toast.error(`Не можете да променяте предпочитанията си за дати преди ${common.getDateFormatedShort(editLockedBefore)}.`, { autoClose: 5000 });
+ return;
+
+ }
}
// Check if start and end are on the same day
if (startdate.toDateString() !== enddate.toDateString()) {
diff --git a/components/layout.tsx b/components/layout.tsx
index 7c5639b..77f1073 100644
--- a/components/layout.tsx
+++ b/components/layout.tsx
@@ -63,6 +63,7 @@ export default function Layout({ children }) {
{children}
+
{/* Modal container */}
diff --git a/components/publisher/PublisherForm.js b/components/publisher/PublisherForm.js
index e851008..51ae652 100644
--- a/components/publisher/PublisherForm.js
+++ b/components/publisher/PublisherForm.js
@@ -19,34 +19,6 @@ import { useSession } from "next-auth/react"
// import { Tabs, List } from 'tw-elements'
-// model Publisher {
-// id String @id @default(cuid())
-// firstName String
-// lastName String
-// email String @unique
-// phone String?
-// isActive Boolean @default(true)
-// isImported Boolean @default(false)
-// age Int?
-// availabilities Availability[]
-// assignments Assignment[]
-
-// emailVerified DateTime?
-// accounts Account[]
-// sessions Session[]
-// role UserRole @default(USER)
-// desiredShiftsPerMonth Int @default(4)
-// isMale Boolean @default(true)
-// isNameForeign Boolean @default(false)
-
-// familyHeadId String? // Optional familyHeadId for each family member
-// familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id])
-// familyMembers Publisher[] @relation("FamilyMember")
-// type PublisherType @default(Publisher)
-// Town String?
-// Comments String?
-// }
-
Array.prototype.groupBy = function (prop) {
return this.reduce(function (groups, item) {
const val = item[prop]
@@ -59,9 +31,11 @@ Array.prototype.groupBy = function (prop) {
export default function PublisherForm({ item, me }) {
const router = useRouter();
const { data: session } = useSession()
+ const [congregations, setCongregations] = useState([]);
const urls = {
apiUrl: "/api/data/publishers/",
+ congregationsUrl: "/api/data/congregations",
indexUrl: session?.user?.role == UserRole.ADMIN ? "/cart/publishers" : "/dash"
}
console.log("urls.indexUrl: " + urls.indexUrl);
@@ -72,6 +46,9 @@ export default function PublisherForm({ item, me }) {
const h = (await import("../../src/helpers/const.js")).default;
//console.log("fetchModules: " + JSON.stringify(h));
setHelper(h);
+
+ const response = await axiosInstance.get(urls.congregationsUrl);
+ setCongregations(response.data);
}
useEffect(() => {
fetchModules();
@@ -113,15 +90,17 @@ export default function PublisherForm({ item, me }) {
publisher.availabilities = undefined;
publisher.assignments = undefined;
- let { familyHeadId, userId, ...rest } = publisher;
+ let { familyHeadId, userId, congregationId, ...rest } = publisher;
// Set the familyHead relation based on the selected head
const familyHeadRelation = familyHeadId ? { connect: { id: familyHeadId } } : { disconnect: true };
const userRel = userId ? { connect: { id: userId } } : { disconnect: true };
+ const congregationRel = congregationId ? { connect: { id: parseInt(congregationId) } } : { disconnect: true };
// Return the new state without familyHeadId and with the correct familyHead relation
rest = {
...rest,
familyHead: familyHeadRelation,
- user: userRel
+ user: userRel,
+ congregation: congregationRel
};
try {
@@ -242,10 +221,32 @@ export default function PublisherForm({ item, me }) {
+ {/* language preference */}
+
+
+
+
+
+
+
+
+
+
{/* notifications */}
diff --git a/components/publisher/PublisherSearchBox.js b/components/publisher/PublisherSearchBox.js
index 7ad9925..18085c7 100644
--- a/components/publisher/PublisherSearchBox.js
+++ b/components/publisher/PublisherSearchBox.js
@@ -127,7 +127,7 @@ function PublisherSearchBox({ id, selectedId, onChange, isFocused, filterDate, s
) : null}
{showList ? (
// Display only clickable list of all publishers
-
+
{publishers.map((publisher) => (
-
))}
- ) : null}
-
+ ) : null
+ }
+
);
}
diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts
index 8cb4f4d..6020c99 100644
--- a/pages/api/auth/[...nextauth].ts
+++ b/pages/api/auth/[...nextauth].ts
@@ -56,11 +56,11 @@ export const authOptions: NextAuthOptions = {
keyId: process.env.APPLE_KEY_ID,
}
}),
- // AzureADProvider({
- // clientId: process.env.AZURE_AD_CLIENT_ID,
- // clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
- // tenantId: process.env.AZURE_AD_TENANT_ID,
- // }),
+ AzureADProvider({
+ clientId: process.env.AZURE_AD_CLIENT_ID,
+ clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
+ tenantId: process.env.AZURE_AD_TENANT_ID,
+ }),
CredentialsProvider({
id: 'credentials',
// The name to display on the sign in form (e.g. 'Sign in with...')
@@ -70,21 +70,6 @@ export const authOptions: NextAuthOptions = {
password: { label: "Парола", type: "password" }
},
async authorize(credentials, req) {
- //const user = { id: "1", name: "Администратора", email: "jsmith@example.com" }
- //return user
- // const res = await fetch("/your/endpoint", {
- // method: 'POST',
- // body: JSON.stringify(credentials),
- // headers: { "Content-Type": "application/json" }
- // })
- // const user = await res.json()
-
- // // If no error and we have user data, return it
- // if (res.ok && user) {
- // return user
- // }
- // // Return null if user data could not be retrieved
- // return null
const users = [
{ id: "1", name: "admin", email: "admin@example.com", password: "admin123", role: "ADMIN", static: true },
{ id: "2", name: "krasi", email: "krasi@example.com", password: "krasi123", role: "ADMIN", static: true },
@@ -272,6 +257,8 @@ export const authOptions: NextAuthOptions = {
verifyRequest: "/auth/verify-request", // (used for check email message)
newUser: null // If set, new users will be directed here on first sign in
},
+
+ debug: process.env.NODE_ENV === 'development',
}
export default NextAuth(authOptions)
\ No newline at end of file
diff --git a/pages/api/email.ts b/pages/api/email.ts
index b8c2161..14ee1de 100644
--- a/pages/api/email.ts
+++ b/pages/api/email.ts
@@ -106,6 +106,7 @@ export default async function handler(req, res) {
},
data: {
publisherId: userId,
+ originalPublisherId: originalPublisher.id,
publicGuid: null, // if this exists, we consider the request open
isConfirmed: true
}
diff --git a/pages/api/index.ts b/pages/api/index.ts
index 736fdda..330c407 100644
--- a/pages/api/index.ts
+++ b/pages/api/index.ts
@@ -2,7 +2,7 @@ import { getToken } from "next-auth/jwt";
import { authOptions } from './auth/[...nextauth]'
import { getServerSession } from "next-auth/next"
import { NextApiRequest, NextApiResponse } from 'next'
-import { DayOfWeek, AvailabilityType, UserRole } from '@prisma/client';
+import { DayOfWeek, AvailabilityType, UserRole, EventLogType } from '@prisma/client';
const common = require('../../src/helpers/common');
const dataHelper = require('../../src/helpers/data');
const subq = require('../../prisma/bl/subqueries');
@@ -11,6 +11,7 @@ import { addMinutes } from 'date-fns';
import fs from 'fs';
import path from 'path';
import { all } from "axios";
+import { logger } from "src/helpers/common";
/**
*
@@ -360,7 +361,8 @@ export default async function handler(req, res) {
res.status(200).json(data);
break;
case "getAllPublishersWithStatistics":
- res.status(200).json(await dataHelper.getAllPublishersWithStatistics(day));
+ let noEndDate = common.parseBool(req.query.noEndDate);
+ res.status(200).json(await dataHelper.getAllPublishersWithStatistics(day, noEndDate));
default:
res.status(200).json({
@@ -821,10 +823,68 @@ async function replaceInAssignment(oldPublisherId, newPublisherId, shiftId) {
},
data: {
publisherId: newPublisherId,
+ originalPublisherId: oldPublisherId,
isConfirmed: false,
isBySystem: true,
isMailSent: false
}
});
+
+ // log the event
+ let shift = await prisma.shift.findUnique({
+ where: {
+ id: shiftId
+ },
+
+ include: {
+ cartEvent: {
+ select: {
+ location: {
+ select: {
+ name: true
+ }
+ }
+ }
+ },
+ assignments: {
+ include: {
+ publisher: {
+ select: {
+ firstName: true,
+ lastName: true,
+ email: true
+ }
+ }
+ }
+ }
+ }
+ });
+ let publishers = await prisma.publisher.findMany({
+ where: {
+ id: { in: [oldPublisherId, newPublisherId] }
+ },
+ select: {
+ id: true,
+ firstName: true,
+ lastName: true,
+ email: true
+ }
+ });
+ let originalPublisher = publishers.find(p => p.id == oldPublisherId);
+ let newPublisher = publishers.find(p => p.id == newPublisherId);
+ let eventLog = await prisma.eventLog.create({
+ data: {
+ date: new Date(),
+ publisher: { connect: { id: oldPublisherId } },
+ shift: { connect: { id: shiftId } },
+ type: EventLogType.AssignmentReplacementManual,
+ content: "Заместване въведено от " + originalPublisher.firstName + " " + originalPublisher.lastName + ". Ще го замества " + newPublisher.firstName + " " + newPublisher.lastName + "."
+
+ }
+ });
+
+ logger.info("User: " + originalPublisher.email + " replaced his assignment for " + shift.cartEvent.location.name + " " + shift.startTime.toISOString() + " with " + newPublisher.firstName + " " + newPublisher.lastName + "<" + newPublisher.email + ">. EventLogId: " + eventLog.id + "");
+
+
return result;
}
\ No newline at end of file
diff --git a/pages/auth/signin.tsx b/pages/auth/signin.tsx
index 9904a52..ceccfdd 100644
--- a/pages/auth/signin.tsx
+++ b/pages/auth/signin.tsx
@@ -74,6 +74,13 @@ export default function SignIn({ csrfToken }) {
src="https://authjs.dev/img/providers/apple.svg" className="mr-2" />
Влез чрез Apple
*/}
+ {/* microsoft */}
+ {/* signIn('azure-ad', { callbackUrl: '/' })}
+ className="mt-4 flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
+
+ Влез чрез Microsoft
+ */}
diff --git a/pages/cart/locations/index.tsx b/pages/cart/locations/index.tsx
index 716993a..e6b5b44 100644
--- a/pages/cart/locations/index.tsx
+++ b/pages/cart/locations/index.tsx
@@ -4,6 +4,7 @@ import Layout from "../../../components/layout";
import LocationCard from "../../../components/location/LocationCard";
import axiosServer from '../../../src/axiosServer';
import ProtectedRoute from '../../../components/protectedRoute';
+import CongregationCRUD from "../publishers/congregationCRUD";
interface IProps {
item: Location;
}
@@ -32,6 +33,7 @@ function LocationsPage({ items = [] }: IProps) {
+
);
}
diff --git a/pages/cart/publishers/congregationCRUD.tsx b/pages/cart/publishers/congregationCRUD.tsx
new file mode 100644
index 0000000..95d0e43
--- /dev/null
+++ b/pages/cart/publishers/congregationCRUD.tsx
@@ -0,0 +1,103 @@
+// a simple CRUD componenet for congregations for admins
+
+import { useEffect, useState } from 'react';
+import axiosInstance from '../../../src/axiosSecure';
+import toast from 'react-hot-toast';
+import Layout from '../../../components/layout';
+import ProtectedRoute from '../../../components/protectedRoute';
+import { UserRole } from '@prisma/client';
+import { useRouter } from 'next/router';
+
+export default function CongregationCRUD() {
+ const [congregations, setCongregations] = useState([]);
+ const [newCongregation, setNewCongregation] = useState('');
+ const router = useRouter();
+
+ const fetchCongregations = async () => {
+ try {
+ const { data: congregationsData } = await axiosInstance.get(`/api/data/congregations`);
+ setCongregations(congregationsData);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const addCongregation = async () => {
+ try {
+ await axiosInstance.post(`/api/data/congregations`, { name: newCongregation, address: "" });
+ toast.success('Успешно добавен сбор');
+ setNewCongregation('');
+ fetchCongregations();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const deleteCongregation = async (id) => {
+ try {
+ await axiosInstance.delete(`/api/data/congregations/${id}`);
+ toast.success('Успешно изтрит сбор');
+ fetchCongregations();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+ useEffect(() => {
+ fetchCongregations();
+ }, []);
+
+ return (
+
+
+
+
Сборове
+
+ setNewCongregation(e.target.value)}
+ placeholder="Име на сбор"
+ className="px-4 py-2 rounded-md border border-gray-300"
+ />
+
+ Добави
+
+
+
+
+
+ Име |
+ Действия |
+
+
+
+ {congregations.map((congregation) => (
+
+ {congregation.name} |
+
+ {/* router.push(`/cart/publishers/congregation/${congregation.id}`)}
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
+ >
+ Преглед
+ */}
+ deleteCongregation(congregation.id)}
+ className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
+ >
+ Изтрий
+
+ |
+
+ ))}
+
+
+
+
+
+ );
+}
+
diff --git a/pages/cart/publishers/index.tsx b/pages/cart/publishers/index.tsx
index 896ec40..493fe3e 100644
--- a/pages/cart/publishers/index.tsx
+++ b/pages/cart/publishers/index.tsx
@@ -8,11 +8,13 @@ import Layout from "../../../components/layout";
import PublisherCard from "../../../components/publisher/PublisherCard";
import axiosInstance from "../../../src/axiosSecure";
import axiosServer from '../../../src/axiosServer';
+const common = require("../../../src/helpers/common");
import toast from "react-hot-toast";
import { levenshteinEditDistance } from "levenshtein-edit-distance";
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
+import { relative } from "path";
@@ -164,7 +166,7 @@ function PublishersPage({ publishers = [] }: IProps) {
return (
-
+
Добави вестител
@@ -195,23 +197,24 @@ function PublishersPage({ publishers = [] }: IProps) {
-
-
-
-
-