initial commit - code moved to separate repo

This commit is contained in:
Dobromir Popov
2024-02-22 04:19:38 +02:00
commit 560d503219
240 changed files with 105125 additions and 0 deletions

45
pages/_app.tsx Normal file
View File

@ -0,0 +1,45 @@
import { SessionProvider } from "next-auth/react"
import "../styles/styles.css"
import "../styles/global.css"
import "tailwindcss/tailwind.css"
import type { AppProps } from "next/app";
import type { Session } from "next-auth";
import { useEffect } from "react"
// for fontawesome
import Head from 'next/head';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
// Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({
Component,
pageProps: { session, ...pageProps },
}: AppProps<{ session: Session }>) {
useEffect(() => {
const use = async () => {
(await import('tw-elements')).default;
};
use();
}, []);
return (
<>
<Head>
{/* Other tags */}
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
rel="stylesheet"
/>
</Head>
<SessionProvider session={session}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Component {...pageProps} />
</LocalizationProvider>
</SessionProvider>
</>
)
}

View File

@ -0,0 +1,200 @@
import NextAuth, { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import FacebookProvider from "next-auth/providers/facebook"
import GithubProvider from "next-auth/providers/github"
import TwitterProvider from "next-auth/providers/twitter"
import Auth0Provider from "next-auth/providers/auth0"
// import AppleProvider from "next-auth/providers/apple"
import EmailProvider from "next-auth/providers/email"
import CredentialsProvider from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
// https://next-auth.js.org/getting-started/client
const common = require("../../../src/helpers/common");
import { isLoggedIn, setAuthTokens, clearAuthTokens, getAccessToken, getRefreshToken } from 'axios-jwt'
// console.log(process.env.EMAIL_SERVER)
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export const authOptions: NextAuthOptions = {
// https://next-auth.js.org/configuration/providers/oauth
site: process.env.NEXTAUTH_URL,
secret: process.env.NEXTAUTH_SECRET, // Ensure you have this set in your .env file
//adapter: PrismaAdapter(prisma),
providers: [
// register new URL at https://console.cloud.google.com/apis/credentials/oauthclient/926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com?project=grand-forge-108716
//Request details: redirect_uri=http://20.101.62.76:8005/api/auth/callback/google https://s.mwhitnessing.com/
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code"
}
}
}),
CredentialsProvider({
// The name to display on the sign in form (e.g. 'Sign in with...')
name: 'Credentials',
credentials: {
username: { label: "Потребител", type: "text", placeholder: "Потребителско име" },
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" },
{ id: "2", name: "krasi", email: "krasi@example.com", password: "krasi123", role: "ADMIN" },
{ id: "3", name: "popov", email: "popov@example.com", password: "popov123", role: "ADMIN" }
];
// Check if a user with the given username and password exists
const user = users.find(user =>
user.name === credentials.username && user.password === credentials.password
);
// If a matching user is found, return the user data, otherwise return null
if (user) {
return user; //{ id: user.id, name: user.name, email: user.email };
}
return null;
}
}),
/*
EmailProvider({
server: {
host: "smtp.mailtrap.io",
port: 2525,
auth: {
user: "8ec69527ff2104",
pass: "c7bc05f171c96c"
}
},
// server: process.env.EMAIL_SERVER,
from: "noreply@example.com",
}),
// Temporarily removing the Apple provider from the demo site as the
// callback URL for it needs updating due to Vercel changing domains
/*
Providers.Apple({
clientId: process.env.APPLE_ID,
clientSecret: {
appleId: process.env.APPLE_ID,
teamId: process.env.APPLE_TEAM_ID,
privateKey: process.env.APPLE_PRIVATE_KEY,
keyId: process.env.APPLE_KEY_ID,
},
}),
*/
//d-popov@abv.bg
Auth0Provider({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
issuer: process.env.AUTH0_ISSUER,
}),
],
theme: {
colorScheme: "light",
},
session: {
strategy: "jwt"
},
callbacks: {
async signIn({ user, account, profile }) {
var prisma = common.getPrismaClient();
console.log("[nextauth] signIn:", account.provider, user.email)
if (account.provider === 'google') {
try {
// Check user in your database and assign roles
const dbUser = await prisma.publisher.findUnique({
where: { email: user.email }
});
if (dbUser) {
// Assign roles from your database to the session
user.role = dbUser.role;
user.id = dbUser.id;
//user.permissions = dbUser.permissions;
const session = { ...user };
return true; // Sign-in successful
} else {
// Optionally create a new user in your DB
// Or return false to deny access
return false;
}
} catch (e) {
console.log(e);
}
}
return true; // Allow other providers or default behavior
},
// Persist the OAuth access_token to the token right after signin
async jwt({ token, user, account, profile, isNewUser }) {
//!console.log("[nextauth] JWT", token, user)
//token.userRole = "adminer"
if (user) {
token.role = user.role;
token.id = user.id; //already done in session?
//token.name = user.name; already done in session (name, email, picture, sub)
}
if (account && user) {
token.accessToken = account.access_token; // Set the access token from the account object
token.provider = account.provider;
console.log("[nextauth] setting token.accessToken", token.accessToken);
setAuthTokens({
accessToken: account.accessToken,
refreshToken: account.refreshToken,
})
}
return token;
},
// Send properties to the client, like an access_token from a provider.
async session({ session, token, user }) {
//!console.log("[nextauth] session", token, user)
if (token) {
//session.user.role = token.role;
session.user.id = token.id;
session.user.role = token.role;
session.user.name = token.name || token.email;
}
// if (session?.user) {
// session.user.id = user.id; //duplicate
// }
return {
...session,
accessToken: token.accessToken
};
},
},
}
export default NextAuth(authOptions)

View File

@ -0,0 +1,68 @@
import NextCrud, { PrismaAdapter } from "@premieroctet/next-crud";
import { Prisma } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]";
// import { getToken } from "next-auth/jwt";
// import { getSession } from "next-auth/client";
const common = require("../../../src/helpers/common");
import jwt from 'jsonwebtoken';
import { decode } from 'next-auth/jwt';
// import { getToken } from "next-auth/jwt";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const prismaClient = common.getPrismaClient();
const nextCrudHandler = await NextCrud({
adapter: new PrismaAdapter({ prismaClient }),
models: {
[Prisma.ModelName.CartEvent]: { name: "cartevents" },
},
});
//1: check session
const session = await getServerSession(req, res, authOptions);
//console.log("Session:", session); // Log the session
const authHeader = req.headers.authorization || '';
//console.log('authHeader', authHeader);
if (session) {
return nextCrudHandler(req, res);
}
else {
console.log('[nextCrud]: No session');
}
//2: check jwt
const secret = process.env.NEXTAUTH_SECRET;
const bearerHeader = req.headers['authorization'];
if (bearerHeader) {
const token = bearerHeader.split(' ')[1]; // Assuming "Bearer <token>"
try {
const decoded = await decode({
token: token,
secret: process.env.NEXTAUTH_SECRET,
});
//console.log('Decoded JWT:');
} catch (err) {
console.error('[nextCrud]: Error decoding token:', err);
}
try {
const verified = jwt.verify(token, secret);
//console.log('Verified JWT:');
return nextCrudHandler(req, res);
} catch (err) {
console.error('[nextCrud]: Invalid token:', err);
}
}
//3. check X-From-Server header
const xFromServer = req.headers['x-from-server'];
if (xFromServer) {
return nextCrudHandler(req, res);
}
return res.status(401).json({ message: '[nextCrud]: Unauthorized' });
};
export default handler;

15
pages/api/data/content.ts Normal file
View File

@ -0,0 +1,15 @@
import path from 'path';
import { promises as fs } from 'fs';
export default async function handler(req, res) {
//Find the absolute path of the json directory and the requested file contents
const jsonDirectory = path.join(process.cwd(), 'content');
const requestedFile = req.query.nextcrud[0];
const fileContents = await fs.readFile(path.join(jsonDirectory, requestedFile), 'utf8');
// try to determine the content type from the file extension
const contentType = requestedFile.endsWith('.json') ? 'application/json' : 'text/plain';
// return the file contents with the appropriate content type
res.status(200).setHeader('Content-Type', contentType).end(fileContents);
}

15
pages/api/examples/jwt.ts Normal file
View File

@ -0,0 +1,15 @@
// This is an example of how to read a JSON Web Token from an API route
import { getToken } from "next-auth/jwt"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// If you don't have the NEXTAUTH_SECRET environment variable set,
// you will have to pass your secret as `secret` to `getToken`
const token = await getToken({ req })
console.log(token)
res.send(JSON.stringify(token, null, 2))
}

View File

@ -0,0 +1,19 @@
// This is an example of to protect an API route
import { getServerSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (session) {
return res.send({
content: "This is protected content. You can access this content because you are signed in.",
});
}
res.send({
error: "You must be signed in to view the protected content on this page.",
});
}

View File

@ -0,0 +1,10 @@
// This is an example of how to access a session from an API route
import { getServerSession } from "next-auth";
import { authOptions } from "../auth/[...nextauth]";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
res.send(JSON.stringify(session, null, 2));
}

643
pages/api/index.ts Normal file
View File

@ -0,0 +1,643 @@
import { getToken } from "next-auth/jwt";
import { NextApiRequest, NextApiResponse } from 'next'
import { DayOfWeek } from '@prisma/client';
const common = require('../../src/helpers/common');
const data = require('../../src/helpers/data');
import fs from 'fs';
import path from 'path';
/**
*
* @param req import { NextApiRequest, NextApiResponse } from 'next'
* @param res import { NextApiRequest, NextApiResponse } from 'next'
*/
export default async function handler(req, res) {
const prisma = common.getPrismaClient();
// Retrieve and validate the JWT token
const token = await getToken({ req: req });
if (!token) {
// If no token or invalid token, return unauthorized status
return res.status(401).json({ message: "Unauthorized to call this API endpoint" });
}
else {
// If token is valid, log the user
//console.log("JWT | User: " + token.email);
}
var action = req.query.action;
var filter = req.query.filter;
let date: Date;
if (req.query.date) {
date = new Date(req.query.date);
//date.setDate(date.getDate()); // Subtract one day to get the correct date, as calendar sends wrong date (one day ahead)
//date.setHours(0, 0, 0, 0);
}
if (req.query.filterDate) {
date = new Date(req.query.filterDate);
}
try {
switch (action) {
case "initDb":
// Read the SQL script from the file
const sqlFilePath = path.join(process.cwd(), 'prisma', 'data.sql');
const sql = fs.readFileSync(sqlFilePath, 'utf8');
// Execute the SQL script
await prisma.$executeRawUnsafe(sql);
res.status(200).json({ message: "SQL script executed successfully" });
break;
case "deleteAllPublishers":
//get filter and delete all publishers containing that in first name or last name
await prisma.publisher.deleteMany({
where: {
OR: [
{ firstName: { contains: filter } },
{ lastName: { contains: filter } },
],
},
});
res.status(200).json({ "message": "ok" });
break;
case "deleteAllAvailabilities":
//get filter and delete all publishers containing that in first name or last name
await prisma.availability.deleteMany({
where: filter ? {
OR: [
{ firstName: { contains: filter } },
{ lastName: { contains: filter } }
]
} : {}
});
res.status(200).json({ "message": "ok" });
break;
//gets publisher by names with availabilities and assignments
case "deleteAvailabilityForPublisher":
let publisherId = req.query.publisherId;
let dateFor, monthInfo;
if (req.query.date) {
dateFor = new Date(req.query.date);
//get month info from date
monthInfo = common.getMonthDatesInfo(dateFor);
}
const deleteFromPreviousAssignments = common.parseBool(req.query.deleteFromPreviousAssignments);
// if datefor is not null/undefined, delete availabilities for that month
try {
await prisma.availability.deleteMany({
where: {
publisherId: publisherId,
startTime: { gte: monthInfo?.firstMonday },
endTime: { lte: monthInfo?.lastSunday }
}
});
if (deleteFromPreviousAssignments) {
await prisma.availability.deleteMany({
where: {
publisherId: publisherId,
isFromPreviousAssignment: true
}
});
}
// await prisma.availability.deleteMany({
// where: {
// publisherId: publisherId
// }
// });
res.status(200).json({ "message": "ok" });
} catch (error) {
console.error("Error deleting availability for publisher: " + publisherId + " error: " + error);
res.status(500).json({ error });
}
break;
case "createAvailabilities": {
const availabilities = req.body;
//! console.log("createAvailabilities: " + JSON.stringify(availabilities));
try {
await prisma.availability.createMany({
data: availabilities
});
res.status(200).json({ "message": "ok" });
} catch (error) {
console.error("Error creating availabilities: " + error);
res.status(500).json({ error });
}
}
break;
case "getCalendarEvents":
let events = await getCalendarEvents(req.query.publisherId, date);
res.status(200).json(events);
case "getPublisherInfo":
let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", null, req.query.assignments || true, req.query.availabilities || true, false, req.query.id);
res.status(200).json(pubs[0]);
break;
case "getMonthlyStatistics":
let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", date);
res.status(200).json(allpubs);
break;
case "getUnassignedPublishers":
//let monthInfo = common.getMonthDatesInfo(date);
let allPubs = await filterPublishers("id,firstName,lastName,email,isactive".split(","), "", date, true, true, false);
let unassignedPubs = allPubs.filter(pub => pub.currentMonthAssignments == 0 && pub.availabilities.length > 0);
res.status(200).json(unassignedPubs);
break;
case "filterPublishers":
const searchText = req.query.searchText?.normalize('NFC');
const fetchAssignments = common.parseBool(req.query.assignments);
const fetchAvailabilities = common.parseBool(req.query.availabilities);
let publishers = await filterPublishers(req.query.select, searchText, date, fetchAssignments, fetchAvailabilities);
//!console.log("publishers: (" + publishers.length + ") " + JSON.stringify(publishers.map(pub => pub.firstName + " " + pub.lastName)));
res.status(200).json(publishers);
break;
// find publisher by full name or email
case "findPublisher":
const getAll = common.parseBool(req.query.all) || false;
let publisher = await data.findPublisher(filter, req.query.email, req.query.select, getAll);
res.status(200).json(publisher);
break;
case "getShiftsForDay":
// Setting the range for a day: starting from the beginning of the date and ending just before the next date.
let startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
let endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
let shiftsForDate = await prisma.shift.findMany({
where: {
startTime: {
gte: startOfDay,
lt: endOfDay
},
},
include: {
assignments: {
include: {
publisher: true,
},
},
},
});
console.log("shiftsForDate(" + date + ") - " + shiftsForDate.length + " : " + JSON.stringify(shiftsForDate.map(shift => shift.id)));
res.status(200).json(shiftsForDate);
break;
default:
res.status(200).json({ "message": "no action" });
break;
}
} catch (error) {
console.error("API: Error executing action: " + action + " with filter: " + filter + " error: " + error);
res.status(500).json({ error });
}
}
export async function getMonthlyStatistics(selectFields, filterDate) {
let publishers = [];
selectFields = selectFields?.split(",");
let selectBase = selectFields.reduce((acc, curr) => {
acc[curr] = true;
return acc;
}, {});
selectBase.assignments = {
select: {
id: true,
shift: {
select: {
id: true,
startTime: true,
endTime: true
}
}
}
};
let currentWeekStart: Date, currentWeekEnd: Date,
currentMonthStart: Date, currentMonthEnd: Date,
previousMonthStart: Date, previousMonthEnd: Date;
let date = new Date(filterDate);
date.setDate(filterDate.getDate());
currentWeekStart = common.getStartOfWeek(date);
currentWeekEnd = common.getEndOfWeek(date);
var monthInfo = common.getMonthDatesInfo(date);
currentMonthStart = monthInfo.firstMonday;
currentMonthEnd = monthInfo.lastSunday;
date.setMonth(date.getMonth() - 1);
monthInfo = common.getMonthDatesInfo(date);
previousMonthStart = monthInfo.firstMonday;
previousMonthEnd = monthInfo.lastSunday;
const prisma = common.getPrismaClient();
publishers = await prisma.publisher.findMany({
select: {
...selectBase,
}
});
publishers.forEach(pub => {
// Debug logs to help identify issues
pub.currentWeekAssignments = pub.assignments.filter(assignment => {
return assignment.shift.startTime >= currentWeekStart && assignment.shift.startTime <= currentWeekEnd;
}).length;
pub.currentMonthAssignments = pub.assignments.filter(assignment => {
return assignment.shift.startTime >= currentMonthStart && assignment.shift.startTime <= currentMonthEnd;
}).length;
pub.previousMonthAssignments = pub.assignments.filter(assignment => {
return assignment.shift.startTime >= previousMonthStart && assignment.shift.startTime <= previousMonthEnd;
}).length;
});
return publishers;
}
// availabilites filter:
// 1. if dayOfMonth is null, match by day of week (enum)
// 2. if dayOfMonth is not null, match by date
// 3. if date is 00:00:00, match by date only (without time)
// 4. if date is not 00:00:00, it should be in the range of start and end times
// this way we distinguish between weekly availabiillities (entered without dayOfMonth) and old availabilities from previous months (entered with dayOfMonth, but we set it to null),
// (To validate) we use useDateFilter in combination with the filterDate to get publishers without availabilities for the day:
// 1: useDateFilter = false, filterDate = null - get all publishers with availabilities for the current month
// 2: useDateFilter = false, filterDate = date - get all publishers with availabilities for the current month
// 3: useDateFilter = true, filterDate = null - get all publishers with availabilities for the current month
// 4: useDateFilter = true, filterDate = date - get all publishers with availabilities for the current month and filter by date
export async function filterPublishers(selectFields, searchText, filterDate, fetchAssignments: boolean = true, fetchAvailabilities: boolean = true, useDateFilter = true, id = null) {
let currentWeekStart: Date, currentWeekEnd: Date,
currentMonthStart: Date, currentMonthEnd: Date,
previousMonthStart: Date, previousMonthEnd: Date,
filterDateEnd: Date,
publishers = [];
if (!filterDate) {
useDateFilter = false;
}
else {
let date = new Date(filterDate.getTime());
//date.setDate(filterDate.getDate());
currentWeekStart = common.getStartOfWeek(date);
currentWeekEnd = common.getEndOfWeek(date);
var monthInfo = common.getMonthDatesInfo(date);
currentMonthStart = monthInfo.firstMonday;
currentMonthEnd = monthInfo.lastSunday;
date.setMonth(date.getMonth() - 1);
monthInfo = common.getMonthDatesInfo(date);
previousMonthStart = monthInfo.firstMonday;
previousMonthEnd = monthInfo.lastSunday;
filterDateEnd = new Date(filterDate);
filterDateEnd.setHours(23, 59, 59, 999);
}
let whereClause = {};
if (id) {
whereClause = {
id: String(id)
}
}
const searchTextString = String(searchText).trim();
if (searchTextString) {
whereClause = {
OR: [
{ firstName: { contains: searchTextString } },
{ lastName: { contains: searchTextString } },
],
};
}
// Base select fields
// Only attempt to split if selectFields is a string; otherwise, use it as it is.
selectFields = typeof selectFields === 'string' ? selectFields.split(",") : selectFields;
let selectBase = selectFields.reduce((acc, curr) => {
acc[curr] = true;
return acc;
}, {});
// If assignments flag is true, fetch assignments
if (fetchAssignments) {
//!! WORKING CODE, but heavy on the DB !!
selectBase.assignments = {
select: {
id: true,
shift: {
select: {
id: true,
startTime: true,
endTime: true
}
}
},
where: {
shift: {
OR: [
// {
// startTime: {
// gte: currentWeekStart,
// lte: currentWeekEnd
// }
// },
// {
// startTime: {
// gte: currentMonthStart,
// lte: currentMonthEnd
// }
// },
{
startTime: {
gte: previousMonthStart,
// lte: previousMonthEnd
}
},
]
}
}
};
//selectBase.assignments = true;
}
let dayOfWeekEnum: DayOfWeek
if (filterDate) {
// Determine day of week using common function
dayOfWeekEnum = common.getDayOfWeekNameEnEnum(filterDate);
if (filterDate.getHours() > 21 || filterDate.getHours() < 6) {
filterDate.setHours(0, 0, 0, 0); // Set to midnight
}
}
// console.log(`filterDate: ${filterDate}`);
// console.log(`filterDateEnd: ${filterDateEnd}`);
if (filterDate && useDateFilter) {
// Info, description and ToDo:
// We should distinguish between availabilities with dayOfMonth and without
// If dayOfMonth is null, we should match by day of week using the enum
// If dayOfMonth is not null, we should match by date.
// if date is 00:00:00, we should match by date only (without time)
// if date is not 00:00:00, it should be in the range of start and end times
// we should also include availabilities from previous assignments but not with preference - dayOfMonth is null. we shuold include them only if they match the day of week
// and distinguish between weekly availabiillities (entered without dayOfMonth) and old availabilities from previous months (entered with dayOfMonth, but we set it to null),
// which we count as weekly availabilities. We can use the type field for that
//console.log(`filterDate: ${filterDate}. date: ${filterDate.getDate()}. dayOfWeekEnum: ${dayOfWeekEnum}. useDateFilter: ${useDateFilter}`);
// we will have 3 cases: up-to date availabilities, old availabilities from previous months and availabilities from previous assignments but not with preference
// we will use the type field to distinguish between them
// up-to date availabilities will have type = 1
// old availabilities from previous months will have type = 2 - we want to drop that function to simplify the code and avoid confusion
// availabilities from previous assignments but not with preference will have type = 3
// also, permanent weekly availabilities will have dayOfMonth = null and type = 0
// for 0 we will match by dayOfWeekEnum and times
// for 1 we will match by exact date and times
// for 2 we will match by dayofweek, weeknr and times
// for 3 we will match by dayofweek, weeknr and times - this is the same as 2, but we will not count them as availabilities for the current month
// generaion of schedule:
/*
option 1: fill from blank - first two places per shift, then more if possible
option 2: fill from previous schedule , remove all assignments where new availabilities are not available
and permanent availabilities to make room for changes (we want to shuffle if possible??? do we?)
continue with option 1 from there
which one depends on if we prioritize empty shifts or making sure everyone has up to date availabilities
*/
//substract the time difference between from ISO string and local time
const offset = filterDate.getTimezoneOffset() * 60000; // offset in milliseconds
var dateAsISO = new Date(filterDate.getTime() + offset);
if (filterDate.getHours() == 0 || dateAsISO.getHours() == 0) {
whereClause["availabilities"] = {
some: {
OR: [
// Check only by date without considering time ( Assignments on specific days without time)
{
//AND: [{ startTime: { gte: filterDate } }, { startTime: { lte: filterDateEnd } }]
//dayOfMonth: filterDate.getDate(),
startTime: { gte: filterDate },
endTime: { lte: filterDateEnd },
// //dayofweek: dayOfWeekEnum,
}
,
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
// This includes availabilities from previous assignments but not with preference
{
dayOfMonth: null,
dayofweek: dayOfWeekEnum,
// ToDo: and weekNr
//startTime: { gte: currentMonthStart },
}
]
}
};
}
else {
whereClause["availabilities"] = {
some: {
OR: [
// Check if dayOfMonth is set and filterDate is between start and end dates (Assignments on specific days AND time)
{
dayOfMonth: filterDate.getDate(),
startTime: { lte: filterDate },
endTime: { gte: filterDate }
},
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
{
dayOfMonth: null,
dayofweek: dayOfWeekEnum,
}
]
}
};
}
} else { // we use month filter if date is passed and useDateFilter is false
if (fetchAvailabilities) {
// If no filter date, return all publishers's availabilities for currentMonthStart
whereClause["availabilities"] = {
some: {
OR: [
// Check if dayOfMonth is not null and startTime is after currentMonthStart (Assignments on specific days AND time)
{
dayOfMonth: { not: null },
startTime: { gte: currentMonthStart },
endTime: { lte: currentMonthEnd }
},
// Check if dayOfMonth is null and match by day of week using the enum (Assigments every week)
{
dayOfMonth: null,
}
]
}
};
//try here
// selectBase.аvailabilities = {
// select: {
// dayofweek: true,
// dayOfMonth: true,
// startTime: true,
// endTime: true,
// weekNr: true,
// type: true
// },
// where: {
// OR: [
// {
// startTime: { gte: currentMonthStart },
// endTime: { lte: currentMonthEnd }
// }
// ]
// }
// }
}
}
//include availabilities if flag is true
const prisma = common.getPrismaClient(); //why we need to get it again?
publishers = await prisma.publisher.findMany({
where: whereClause,
select: {
...selectBase,
...(fetchAvailabilities && { availabilities: true })
}
});
console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`);
if (filterDate) {
if (fetchAssignments) {
//get if publisher has assignments for current weekday, week, current month, previous month
publishers.forEach(pub => {
// Filter assignments for current day
pub.currentDayAssignments = pub.assignments?.filter(assignment => {
return assignment.shift.startTime >= filterDate && assignment.shift.startTime <= filterDateEnd;
}).length;
// Filter assignments for current week
pub.currentWeekAssignments = pub.assignments?.filter(assignment => {
return assignment.shift.startTime >= currentWeekStart && assignment.shift.startTime <= currentWeekEnd;
}).length;
// Filter assignments for current month
pub.currentMonthAssignments = pub.assignments?.filter(assignment => {
return assignment.shift.startTime >= currentMonthStart && assignment.shift.startTime <= currentMonthEnd;
}).length;
// Filter assignments for previous month
pub.previousMonthAssignments = pub.assignments?.filter(assignment => {
return assignment.shift.startTime >= previousMonthStart && assignment.shift.startTime <= previousMonthEnd;
}).length;
});
}
}
if (fetchAvailabilities) {
//get the availabilities for the day. Calcullate:
//1. how many days the publisher is available for the current month - only with dayOfMonth
//2. how many days the publisher is available without dayOfMonth (previous months count)
//3. how many hours in total the publisher is available for the current month
publishers.forEach(pub => {
pub.currentMonthAvailability = pub.availabilities?.filter(avail => {
// return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
return avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
})
pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length || 0;
// pub.currentMonthAvailabilityDaysCount += pub.availabilities.filter(avail => {
// return avail.dayOfMonth == null;
// }).length;
pub.currentMonthAvailabilityHoursCount = pub.currentMonthAvailability.reduce((acc, curr) => {
return acc + (curr.endTime.getTime() - curr.startTime.getTime()) / (1000 * 60 * 60);
}, 0);
//if pub has ever filled the form - if has availabilities which are not from previous assignments
pub.hasEverFilledForm = pub.availabilities?.some(avail => {
return avail.isFromPreviousAssignments == false;
});
//if pub has up-to-date availabilities (with dayOfMonth) for the current month
pub.hasUpToDateAvailabilities = pub.availabilities?.some(avail => {
return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd;
});
});
if (filterDate && useDateFilter) {
// Post filter for time if dayOfMonth is null
// Modify the availabilities array of the filtered publishers
publishers.forEach(pub => {
pub.availabilities = pub.availabilities?.filter(avail => matchesAvailability(avail, filterDate));
});
}
}
return publishers;
}
function matchesAvailability(avail, filterDate) {
// Setting the start and end time of the filterDate
filterDate.setHours(0, 0, 0, 0);
const filterDateEnd = new Date(filterDate);
filterDateEnd.setHours(23, 59, 59, 999);
// Return true if avail.startTime is between filterDate and filterDateEnd
return avail.startTime >= filterDate && avail.startTime <= filterDateEnd;
}
async function getCalendarEvents(publisherId, date, availabilities = true, assignments = true) {
const result = [];
let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", date, assignments, availabilities, date ? true : false, publisherId);
let publisher = pubs[0];
if (publisher) {
if (availabilities) {
publisher.availabilities?.forEach(item => {
result.push({
...item,
title: common.getTimeFomatted(new Date(item.startTime)) + "-" + common.getTimeFomatted(new Date(item.endTime)), //item.name,
date: new Date(item.startTime),
startTime: new Date(item.startTime),
endTime: new Date(item.endTime),
publisherId: publisher.id,
type: "availability",
isFromPreviousAssignment: item.isFromPreviousAssignment,
});
});
}
if (assignments) {
publisher.assignments?.forEach(item => {
result.push({
...item,
title: common.getTimeFomatted(new Date(item.shift.startTime)) + "-" + common.getTimeFomatted(new Date(item.shift.endTime)),
date: new Date(item.shift.startTime),
startTime: new Date(item.shift.startTime),
endTime: new Date(item.shift.endTime),
publisherId: item.publisherid,
type: "assignment",
});
});
}
}
return result;
}

187
pages/api/schedule.ts Normal file
View File

@ -0,0 +1,187 @@
// pages/api/shifts.ts
import axiosServer from '../../src/axiosServer';
import { getToken } from "next-auth/jwt";
import type { NextApiRequest, NextApiResponse } from "next";
import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client";
import { levenshteinEditDistance } from "levenshtein-edit-distance";
import { filterPublishers, /* other functions */ } from './index';
import CAL from "../../src/helpers/calendar";
//const common = require("@common");
import common from "../../src/helpers/common";
import { Axios } from 'axios';
const path = require("path");
const fs = require("fs");
const generateTemplateFile = async (data, templateSrc) => {
const handlebars = require("handlebars");
const htmlDocx = require("html-docx-js");
// Compile the Handlebars template
const template = handlebars.compile(templateSrc);
// Generate the HTML output using the template and the events data
const html = template(data);
return html;
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
console.log(req.url);
console.log(req.query);
const prisma = common.getPrismaClient();
// If you don't have the NEXTAUTH_SECRET environment variable set,
// you will have to pass your secret as `secret` to `getToken`
const axios = await axiosServer({ req: req, res: res });
const token = await getToken({ req: req });
if (!token) {
// If no token or invalid token, return unauthorized status
return res.status(401).json({ message: "Unauthorized" });
}
if (req.method === 'GET') {
const { year, month } = req.query;
let monthIndex = parseInt(month as string) - 1;
const monthInfo = common.getMonthDatesInfo(new Date(year, month, 1));
let fromDate = monthInfo.firstMonday;
const toDate = monthInfo.lastSunday;
// Ensure fromDate is not in the past
const today = new Date();
today.setHours(0, 0, 0, 0); // Set time to midnight for accurate comparison
if (fromDate < today) {
fromDate = today;
}
try {
const shifts = await prisma.shift.findMany({
where: {
isactive: true,
startTime: {
gte: fromDate,
lt: toDate,
},
},
include: {
assignments: {
where: {},
include: {
publisher: true,
},
},
cartEvent: {
include: {
location: true,
},
},
},
});
let json = JSON.stringify(shifts);
const groupedShifts = {};
const startDate = new Date(shifts[0].startTime);
const monthName = common.getMonthName(shifts[0].startTime.getMonth());
let i = 0;
try {
for (const shift of shifts) {
i++;
const date = new Date(shift.startTime);
const day = common.getISODateOnly(date)
const time = common.getTimeRange(shift.startTime, shift.endTime); //common.getLocalTime(date);
if (!groupedShifts[day]) {
groupedShifts[day] = {};
}
if (!groupedShifts[day][time]) {
groupedShifts[day][time] = [];
}
let shiftSchedule = {
date: date,
placeOfEvent: shift.cartEvent.location.name,
time: time,
//bold the text after - in the notes
notes: shift.notes?.substring(0, shift.notes.indexOf("-") + 1),
notes_bold: shift.notes?.substring(shift.notes.indexOf("-") + 1),
names: shift.assignments
.map((assignment) => {
return (
assignment.publisher.firstName +
" " +
assignment.publisher.lastName
);
})
.join(", "),
};
groupedShifts[day][time].push(shiftSchedule);
}
} catch (err) {
console.log(err + " " + JSON.stringify(shifts[i]));
}
// Create the output object in the format of the second JSON file
const monthlySchedule = {
month: monthName,
year: startDate.getFullYear(),
events: [],
};
for (const day in groupedShifts) {
var dayEvent = null;
for (const time in groupedShifts[day]) {
if (dayEvent == null) {
const shift = groupedShifts[day][time][0];
if (!shift) {
console.log("shift is null");
continue;
}
let weekday = common.getDayOfWeekName(shift.date);
weekday = weekday.charAt(0).toUpperCase() + weekday.slice(1);
let weekNr = common.getWeekNumber(shift.date);
console.log("weekday = " + weekday, " weekNr = " + weekNr);
dayEvent = {
week: weekNr,
dayOfWeek: weekday,
dayOfMonth: shift.date.getDate(),
placeOfEvent: shift.placeOfEvent,
shifts: [],
//transport: shift.notes,
};
}
dayEvent.shifts.push(...groupedShifts[day][time]);
}
monthlySchedule.events.push(dayEvent);
}
const outputPath = path.join(process.cwd(), 'public', 'content', 'output');
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
fs.writeFileSync(path.join(outputPath, `shifts ${year}.${month}.json`), JSON.stringify(monthlySchedule), 'utf8');
// Load the Handlebars template from a file
const template = fs.readFileSync("./src/templates/word.html", "utf8");
generateTemplateFile(monthlySchedule, template).then((result) => {
const filename = path.join(outputPath, `schedule ${year}.${month}.html`)
//fs.writeFileSync(filename, result, "utf8");
res.end(result);
}
);
}
catch (error) {
res.status(500).json({ error: "Internal Server Error" });
}
} else {
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

688
pages/api/shiftgenerate.ts Normal file
View File

@ -0,0 +1,688 @@
//import { getToken } from "next-auth/jwt";
import axiosServer from '../../src/axiosServer';
import { getToken } from "next-auth/jwt";
import type { NextApiRequest, NextApiResponse } from "next";
import { Prisma, PrismaClient, DayOfWeek, Publisher, Shift } from "@prisma/client";
import { levenshteinEditDistance } from "levenshtein-edit-distance";
import { filterPublishers, /* other functions */ } from './index';
import CAL from "../../src/helpers/calendar";
//const common = require("@common");
import common from "../../src/helpers/common";
import { Axios } from 'axios';
export default handler;
async function handler(req: NextApiRequest, res: NextApiResponse) {
console.log(req.url);
console.log(req.query);
const prisma = common.getPrismaClient();
// If you don't have the NEXTAUTH_SECRET environment variable set,
// you will have to pass your secret as `secret` to `getToken`
const axios = await axiosServer({ req: req, res: res });
const token = await getToken({ req: req });
if (!token) {
// If no token or invalid token, return unauthorized status
return res.status(401).json({ message: "Unauthorized" });
}
// const token = req.headers.authorization.split('Bearer ')[1]
// const { user } = await verify(token, process.env.NEXTAUTH_SECRET, {
// maxAge: 30 * 24 * 60 * 60, // 30 days
// })
// if (!user.roles.includes('admin')) {
// res.status(401).json({ message: 'Unauthorized' })
// return
// }
// // if (!user.role == "adminer") {
// if (token?.userRole !== "adminer") {
// res.status(401).json({ message: "Unauthorized" });
// console.log("not authorized");
// return;
// }
// var result = { error: "Not authorized" };
var action = req.query.action;
switch (action) {
case "generate":
var result = await GenerateSchedule(axios,
req.query.date?.toString() || common.getISODateOnly(new Date()),
common.parseBool(req.query.copyFromPreviousMonth),
common.parseBool(req.query.autoFill),
common.parseBool(req.query.forDay));
res.send(JSON.stringify(result.error?.toString()));
break;
case "delete":
result = await DeleteSchedule(axios, req.query.date, common.parseBool(req.query.forDay));
res.send("deleted"); // JSON.stringify(result, null, 2)
break;
case "createcalendarevent":
//CAL.GenerateICS();
result = await CreateCalendarForUser(req.query.id);
res.send(result); // JSON.stringify(result, null, 2)
break;
case "test":
var data = prisma.shift.findMany({
where: {
isactive: true
}
});
res.send({
action: "OK",
shifts: data,
locations: prisma.location.findMany({
take: 10, // Limit the number of records to 10
orderBy: {
name: 'asc' // Replace 'someField' with a field you want to sort by
},
})
});
break;
default:
res.send("Invalid action");
break;
}
}
// handle /api/data/schedule?date=2021-08-01&time=08:00:00&duration=60&service=1&provider=1
//Fix bugs in this code:
async function GenerateSchedule(axios: Axios, date: string, copyFromPreviousMonth: boolean = false, autoFill: boolean = false, forDay: Boolean) {
let missingPublishers: any[] = [];
let publishersWithChangedPref: any[] = [];
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));
//delete all shifts for this month
if (forDay) {
// Delete shifts only for the specific day
await DeleteShiftsForDay(monthInfo.date);
} else {
// Delete all shifts for the entire month
await DeleteShiftsForMonth(monthInfo);
}
console.log("finding shifts for previous 3 months for statistics (between " + new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 3, 1).toISOString() + " and " + monthInfo.firstDay.toISOString() + ")");
const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":{"$eq":true}}`);
//// let [shiftsLastMonth, publishers] = await getShiftsAndPublishersForPreviousMonths(lastMonthInfo);
//use filterPublishers from /pages/api/data/index.ts to get publishers with stats
let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo);
let publishers = await filterPublishers("id,firstName,lastName", null, lastMonthInfo.firstMonday, true, true, false);
//let publishersWithStatsNew = await filterPublishers("id,firstName,lastName", null, monthInfo.firstMonday, true, true, false);
//foreach day of the month check if there is an event for this day
//if there is an event, then generate shifts for this day based on shiftduration and event start and end time
//####################################################GPT###########################################################
let shiftAssignments = [];
let day = monthInfo.firstMonday; // Start from forDay if provided, otherwise start from first Monday
let endDate = monthInfo.lastSunday; // End at forDay + 1 day if provided, otherwise end at last Sunday
let dayNr = 1; // Start from the day number of forDay, or 1 for the entire month
let weekNr = 1; // Start from the week number of forDay, or 1 for the entire month
if (forDay) {
day = monthInfo.date;
endDate.setDate(monthInfo.date.getDate() + 1);
dayNr = monthInfo.date.getDate();
weekNr = common.getWeekNumber(monthInfo.date);
}
let publishersThisWeek: any[] = [];
console.log("\r\n");
console.log("###############################################");
console.log(" SHIFT GENERATION STARTED for " + common.getISODateOnly(monthInfo.date));
console.log("###############################################");
while (day < endDate) {
const dayOfM = day.getDate();
let dayName = common.DaysOfWeekArray[day.getDayEuropean()];
console.log("[day " + dayNr + "] " + dayName + " " + dayOfM);
//ToDo: rename event to cartEvent
const event = events.find((event: { dayofweek: string }) => {
return event.dayofweek == dayName;
});
if (!event) {
console.log("no event for " + dayName);
day.setDate(day.getDate() + 1);
continue;
}
event.startTime = new Date(event.startTime);
event.endTime = new Date(event.endTime);
var startTime = new Date(day);
startTime.setHours(event.startTime.getHours());
startTime.setMinutes(event.startTime.getMinutes());
var endTime = new Date(day);
endTime.setHours(event.endTime.getHours());
endTime.setMinutes(event.endTime.getMinutes());
var shiftStart = new Date(startTime);
var shiftEnd = new Date(startTime);
shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
var 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 = [];
console.log("[shift " + shiftNr + "] " + __shiftName);
if (autoFill || copyFromPreviousMonth) {
// ###########################################
// shift cache !!!
// ###########################################
// get last month attendance for this shift for each week, same day of the week and same shift
const shiftLastMonthSameDay = getShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr);
if (shiftLastMonthSameDay) {
console.log("shiftCache: loaded shifts from '" + shiftLastMonthSameDay.startTime + "' for: " + day);
//log shiftLastMonthSameDay.assignments.publisher names
console.log("last month attendance for shift " + shiftNr + " (" + __shiftName + ") : " + shiftLastMonthSameDay.assignments.map((a: { publisher: { firstName: string; lastName: string; }; }) => a.publisher.firstName + " " + a.publisher.lastName).join(", "));
for (var i = 0; i < shiftLastMonthSameDay.assignments.length; i++) {
let sameP = shiftLastMonthSameDay.assignments[i].publisher;
let name = sameP.firstName + " " + sameP.lastName;
console.log("shiftCache: considerig publisher: " + sameP.firstName + " " + sameP.lastName + ". Checking if he is available for this shift...");
//get availability for the same dayofweek and time (< startTime, > endTime) OR exact date (< startTime, > endTime)
// Query for exact date match
let availability = (await prisma.availability.findMany({
where: {
publisherId: sameP.id,
dayOfMonth: dayOfM,
startTime: {
lte: shiftStart,
},
endTime: {
gte: shiftEnd,
},
},
}))[0] || null;
if (copyFromPreviousMonth) {
//copy from previous month without checking availability
console.log("shiftCache: copy from previous month. Аvailability is " + (availability ? "available" : "not available")
+ ". Adding him to the new scedule as " + (availability ? "confirmed" : "tentative") + ".");
shiftAssignments.push({ publisherId: sameP.id, isConfirmed: availability ? false : true });
} else {
// check if the person filled the form this month
const allAvailabilities = await prisma.availability.findMany({
where: {
publisherId: sameP.id,
isFromPreviousAssignment: false,
},
});
// // ?? get the date on the same weeknr and dayofweek last month, and check if there is an availability for the same day of the week and required time
// if (!availability) {
// // check if there is an availability for the same day of the week and required time
// availability = allAvailabilities.filter((a: { dayofweek: any; startTime: Date; endTime: Date; }) => {
// return a.dayofweek === event.dayofweek && a.startTime <= startTime && a.endTime >= endTime;
// })[0] || null;
// }
// var availability = allAvailabilities.find((a) => {
// return (a.dayofweek === event.dayofweek && a.dayOfMonth == null) || a.dayOfMonth == dayOfM;
// });
//publishers not filled the form will not have an email with @, but rather as 'firstname.lastname'.
//We will add them to the schedule as manual override until they fill the form
//ToDo this logic is not valid in all cases.
if (!availability && sameP.email.includes("@")) {
if (!publishersWithChangedPref.includes(name)) {
//publishersWithChangedPref.push(name);
}
console.log("shiftCache: publisher is not available for this shift. Available days: " + allAvailabilities.filter((a: { dayOfMonth: any; }) => a.dayOfMonth === dayOfM).map((a) => a.dayofweek + " " + a.dayOfMonth).join(", "));
//continue;
}
if (availability) {
console.log("shiftCache: publisher is available for this shift. Available days: " + availability.dayofweek + " " + availability.dayOfMonth + " " + availability.startTime + " - " + availability.endTime);
console.log("shiftCache: publisher is available for this shift OR manual override is set. Adding him to the new scedule.");
shiftAssignments.push({ publisherId: sameP.id });
}
else {
// skip publishers without availability now
// console.warn("NO publisher availability found! for previous assignment for " + name + ". Assuming he does not have changes in his availability. !!! ADD !!! him to the new scedule but mark him as missing.");
// if (!missingPublishers.includes(name)) {
// missingPublishers.push(name);
// }
// try {
// console.log("shiftCache: publisher was last month assigned to this shift but he is not in the system. Adding him to the system with id: " + sameP.id);
// shiftAssignments.push({ publisherId: sameP.id, });
// } catch (e) {
// console.error(`shiftCache: error adding MANUAL publisher to the system(${sameP.email} ${sameP.firstName} ${sameP.lastName}): ` + e);
// }
}
}
}
// ###########################################
// shift CACHE END
// ###########################################
console.log("searching available publisher for " + dayName + " " + __shiftName);
if (!copyFromPreviousMonth) {
/* We chave the following data:
availabilities:(6) [{…}, {…}, {…}, {…}, {…}, {…}]
currentDayAssignments:0
currentMonthAssignments:2
currentMonthAvailability:(2) [{…}, {…}]
currentMonthAvailabilityDaysCount:2
currentMonthAvailabilityHoursCount:3
currentWeekAssignments:0
firstName:'Алесия'
id:'clqjtcrqj0008oio8kan5lkjn'
lastName:'Сейз'
previousMonthAssignments:2
*/
// until we reach event.numberOfPublishers, we will try to fill the shift with publishers from allAvailablePublishers with the following priority:
// do multiple passes, reecalculating availabilityIndex for each publisher after each pass.
// !!! Never assign the same publisher twice to the same day! (currentDayAssignments > 0)
// PASS 1: Prioritize publishers with little currentMonthAvailabilityHoursCount ( < 5 ), as they may not have another opportunity to serve this month
// PASS 2: try to fill normally based on availabilityIndex, excluding those who were assigned this week
// PASS 3: try to fill normally based on availabilityIndex, including those who were assigned this week and weighting the desiredShiftsPerMonth
// PASS 4: include those without availability this month - based on old availabilities and assignments for this day of the week.
// push found publisers to shiftAssignments with: .push({ publisherId: publisher.id }); and update publisher stats in new function: addAssignmentToPublisher(shiftAssignments, publisher)
// ---------------------------------- new code ---------------------------------- //
// get all publishers who are available for this SPECIFIC day and WEEKDAY
const queryParams = new URLSearchParams({
action: 'filterPublishers',
assignments: 'true',
availabilities: 'true',
date: common.getISODateOnly(shiftStart),
select: 'id,firstName,lastName,isactive,desiredShiftsPerMonth'
});
let allAvailablePublishers = (await axios.get(`/api/?${queryParams.toString()}`)).data;
let availablePublishers = allAvailablePublishers;
let publishersNeeded = Math.max(0, event.numberOfPublishers - shiftAssignments.length);
// LEVEL 1: Prioritize publishers with little currentMonthAvailabilityHoursCount ( < 5 ), as they may not have another opportunity to serve this month
// get publishers with little currentMonthAvailabilityHoursCount ( < 5 )
// let availablePublishers = allAvailablePublishers.filter((p: { currentMonthAvailabilityHoursCount: number; }) => p.currentMonthAvailabilityHoursCount < 5);
// // log all available publishers with their currentMonthAvailabilityHoursCount
// console.info("PASS 1: availablePublishers for this shift with currentMonthAvailabilityHoursCount < 5: " + availablePublishers.length + " (" + publishersNeeded + " needed)");
// availablePublishers.slice(0, publishersNeeded).forEach((p: { id: any; }) => { addAssignmentToPublisher(shiftAssignments, p); });
// publishersNeeded = Math.max(0, event.numberOfPublishers - shiftAssignments.length);
// LEVEL 2+3: try to fill normally based on availabilityIndex, excluding those who were assigned this week
// get candidates that are not assigned this week, and which have not been assigned this month as mutch as the last month.
// calculate availabilityIndex for each publisher based on various factors:
// 1. currentMonthAssignments - lastMonth (weight 50%)
// 2. desiredShiftsPerMonth (weight 30%)
// 3. publisher type (weight 20%) - regular, auxiliary, pioneer, special, bethel, etc.. (see publisherType in publisher model). exclude betelites who were assigned this month. (index =)
//calculate availabilityIndex:
allAvailablePublishers.forEach((p: { currentMonthAssignments: number; desiredShiftsPerMonth: number; publisherType: string; }) => {
// 1. currentMonthAssignments - lastMonth (weight 50%)
// 2. desiredShiftsPerMonth (weight 30%)
// 3. publisher type (weight 20%) - regular, auxiliary, pioneer, special, bethel, etc.. (see publisherType in publisher model). exclude betelites who were assigned this month. (index =)
p.availabilityIndex = Math.round(((p.currentMonthAssignments - p.previousMonthAssignments) * 0.5 + p.desiredShiftsPerMonth * 0.3 + (p.publisherType === "bethelite" ? 0 : 1) * 0.2) * 100) / 100;
});
// use the availabilityIndex to sort the publishers
// LEVEL 2: remove those who are already assigned this week (currentWeekAssignments > 0), order by !availabilityIndex
availablePublishers = allAvailablePublishers.filter((p: { currentWeekAssignments: number; }) => p.currentWeekAssignments === 0)
.sort((a: { availabilityIndex: number; }, b: { availabilityIndex: number; }) => a.availabilityIndex - b.availabilityIndex);
console.warn("PASS 2: availablePublishers for this shift after removing already assigned this week: " + availablePublishers.length + " (" + publishersNeeded + " needed)");
availablePublishers.slice(0, publishersNeeded).forEach((p: { id: any; }) => { addAssignmentToPublisher(shiftAssignments, p); });
publishersNeeded = Math.max(0, event.numberOfPublishers - shiftAssignments.length);
// LEVEL 3: order by !availabilityIndex
availablePublishers = allAvailablePublishers.sort((a: { availabilityIndex: number; }, b: { availabilityIndex: number; }) => a.availabilityIndex - b.availabilityIndex);
console.warn("PASS 3: availablePublishers for this shift including already assigned this week: " + availablePublishers.length + " (" + publishersNeeded + " needed)");
availablePublishers.slice(0, publishersNeeded).forEach((p: { id: any; }) => { addAssignmentToPublisher(shiftAssignments, p); });
publishersNeeded = Math.max(0, event.numberOfPublishers - shiftAssignments.length);
// LEVEL 4: include those without availability this month - based on old availabilities and assignments for this day of the week.
// get candidates that are not assigned this week, and which have not been assigned this month as mutch as the last month.
//query the api again for all publishers with assignments and availabilities for this day of the week including from old assignments (set filterPublishers to false)
availablePublishers = await filterPublishers("id,firstName,lastName", null, shiftStart, false, true, true);
console.warn("PASS 4: availablePublishers for this shift including weekly and old assignments: " + availablePublishers.length + " (" + publishersNeeded + " needed)");
function oldCode() {
// ---------------------------------- old code ---------------------------------- //
// console.warn("allAvailablePublishers: " + allAvailablePublishers.length);
// // remove those who are already assigned this week (currentWeekAssignments > 0)//, # OLD: order by !availabilityIndex
// let availablePublishers = allAvailablePublishers.filter((p: { currentWeekAssignments: number; }) => p.currentWeekAssignments === 0);
// console.warn("availablePublishers for this shift after removing already assigned this week: " + availablePublishers.length + " (" + (event.numberOfPublishers - shiftAssignments.length) + " needed)");
// if (availablePublishers.length === 0) {
// console.error(`------------------- no available publishers for ${dayName} ${dayOfM}!!! -------------------`);
// // Skipping the rest of the code execution
// //return;
// }
// let msg = `FOUND ${availablePublishers.length} publishers for ${dayName} ${dayOfM}, ${__shiftName} . ${event.numberOfPublishers - shiftAssignments.length} needed\r\n: `;
// msg += availablePublishers.map((p: { firstName: any; lastName: any; asignmentsThisMonth: any; availabilityIndex: any; }) => `${p.firstName} ${p.lastName} (${p.asignmentsThisMonth}:${p.availabilityIndex})`).join(", ");
// console.log(msg);
// // ---------------------------------- old code ---------------------------------- //
} // end of old code
}
}
}
//###############################################################################################################
// create shift assignmens
//###############################################################################################################
// using prisma client:
// https://stackoverflow.com/questions/65950407/prisma-many-to-many-relations-create-and-connect
// connect publishers to shift
const createdShift = await prisma.shift.create({
data: {
startTime: shiftStart,
endTime: shiftEnd,
name: event.dayofweek + " " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString(),
cartEvent: {
connect: {
id: event.id,
},
},
assignments: {
create: shiftAssignments.map((a) => {
return { publisher: { connect: { id: a.publisherId } }, isConfirmed: a.isConfirmed };
}),
},
},
});
shiftStart = new Date(shiftEnd);
shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration);
}
day.setDate(day.getDate() + 1);
dayNr++;
let weekDay = common.DaysOfWeekArray[day.getDayEuropean()]
if (weekDay == DayOfWeek.Sunday) {
weekNr++;
publishersThisWeek = [];
publishers.forEach((p: { currentWeekAssignments: number; }) => {
p.currentWeekAssignments = 0;
});
}
//the whole day is done, go to next day. break if we are generating for a specific day
if (forDay) {
break;
}
}
//###################################################GPT############################################################
if (!forDay) {
const fs = require("fs");
fs.writeFileSync("./content/publisherShiftStats.json", JSON.stringify(publishers, null, 2));
fs.writeFileSync("./content/publishersWithChangedPref.json", JSON.stringify(publishersWithChangedPref, null, 2));
fs.writeFileSync("./content/missingPublishers.json", JSON.stringify(missingPublishers, null, 2));
console.log("###############################################");
console.log(" DONE CREATING SCHEDULE FOR " + monthInfo.monthName + " " + monthInfo.year);
console.log("###############################################");
}
//create shifts using API
// const { data: createdShifts } = await axios.post(`${process.env.NEXTAUTH_URL}/api/data/shifts`, shiftsToCreate);
//const { data: allshifts } = await axios.get(`/api/data/shifts`);
return {}; //allshifts;
}
catch (error) {
console.log(error);
return { error: error };
}
}
function addAssignmentToPublisher(shiftAssignments: any[], publisher: Publisher) {
shiftAssignments.push({ publisherId: publisher.id });
publisher.currentWeekAssignments++ || 1;
publisher.currentDayAssignments++ || 1;
publisher.currentMonthAssignments++ || 1;
//console.log(`manual assignment: ${dayName} ${dayOfM} ${shiftStart}:${shiftEnd} ${p.firstName} ${p.lastName} ${p.availabilityIndex} ${p.currentMonthAssignments}`);
console.log(`manual assignment: ${publisher.firstName} ${publisher.lastName} ${publisher.currentMonthAssignments}`);
return publisher;
}
async function DeleteShiftsForMonth(monthInfo: any) {
try {
const prisma = common.getPrismaClient();
await prisma.shift.deleteMany({
where: {
startTime: {
gte: monthInfo.firstMonday,
lt: monthInfo.lastSunday,
},
},
});
} catch (e) {
console.log(e);
}
}
async function DeleteShiftsForDay(date: Date) {
const prisma = common.getPrismaClient();
try {
// Assuming shifts do not span multiple days, so equality comparison is used
await prisma.shift.deleteMany({
where: {
startTime: {
gte: date,
lt: new Date(date.getTime() + 86400000), // +1 day in milliseconds
},
},
});
} catch (e) {
console.log(e);
}
}
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 getShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr) {
let weekDay = common.DaysOfWeekArray[day.getDayEuropean()];
return shiftsLastMonth.find(s => {
return s.weekNr === weekNr &&
s.shiftNr === shiftNr &&
s.weekDay === weekDay;
});
}
/**
* Dangerous function that deletes all shifts and publishers.
* @param date
* @returns
*/
async function DeleteSchedule(axios: Axios, date: Date, forDay: Boolean | undefined) {
try {
let monthInfo = common.getMonthDatesInfo(new Date(date));
if (forDay) {
// Delete shifts only for the specific day
await DeleteShiftsForDay(monthInfo.date);
} else {
// Delete all shifts for the entire month
await DeleteShiftsForMonth(monthInfo);
}
} catch (error) {
console.log(error);
return { error: error };
}
}
async function CreateCalendarForUser(eventId: string | string[] | undefined) {
try {
CAL.authorizeNew();
CAL.createEvent(eventId);
} catch (error) {
console.log(error);
return { error: error };
}
}
/*
obsolete?
*/
async function ImportShiftsFromDocx(axios: Axios) {
try {
const { data: shifts } = await axios.get(`/api/data/shifts`);
shifts.forEach(async (shift: { id: any; }) => {
await axios.delete(`/api/data/shifts/${shift.id}`);
});
const { data: shiftsToCreate } = await axios.get(`/api/data/shiftsToCreate`);
shiftsToCreate.forEach(async (shift: any) => {
await axios.post(`/api/data/shifts`, shift);
});
} catch (error) {
console.log(error);
return { error: error };
}
}
/**
* Retrieves shifts and publishers for the previous months based on the given month information.
* @deprecated This function is deprecated and will be removed in future versions. Use `filterPublishers` from `/pages/api/data/index.ts` instead.
* @param monthInfo - An object containing information about the last month, including its first day and last Sunday.
* @returns A Promise that resolves to an array containing the publishers for the previous months.
*/
// async function getShiftsAndPublishersForPreviousMonths(monthInfo: { firstDay: any; lastSunday: any; firstMonday: any; nrOfWeeks: number; }) {
// const prisma = common.getPrismaClient(); //old: (global as any).prisma;
// const [shiftsLastMonth, initialPublishers] = await Promise.all([
// prisma.shift.findMany({
// where: {
// startTime: {
// gte: monthInfo.firstDay,
// lte: monthInfo.lastSunday,
// },
// },
// include: {
// assignments: {
// include: {
// publisher: true,
// },
// },
// },
// }),
// prisma.publisher.findMany({
// where: {
// isactive: true,
// },
// include: {
// availabilities: {
// where: {
// isactive: true,
// },
// },
// assignments: {
// include: {
// shift: true,
// },
// },
// },
// }),
// ]);
// // Group shifts by day
// function getDayFromDate(date: Date) {
// return date.toISO String().split('T')[0];
// }
// const groupedShifts = shiftsLastMonth.reduce((acc: { [x: string]: any[]; }, shift: { startTime: string | number | Date; }) => {
// const day = getDayFromDate(new Date(shift.startTime));
// if (!acc[day]) {
// acc[day] = [];
// }
// acc[day].push(shift);
// return acc;
// }, {});
// //temp fix - calculate shift.weekNr
// const updatedShiftsLastMonth = [];
// for (const day in groupedShifts) {
// const shifts = groupedShifts[day];
// for (let i = 0; i < shifts.length; i++) {
// const shift = shifts[i];
// updatedShiftsLastMonth.push({
// ...shift,
// weekNr: common.getWeekNumber(shift.startTime) + 1,
// shiftNr: i + 1 // The shift number for the day starts from 1
// });
// }
// }
// const publishers = initialPublishers.map((publisher: { assignments: any[]; desiredShiftsPerMonth: number; }) => {
// // const lastMonthStartDate = new Date(date.getFullYear(), date.getMonth() - 1, 1);
// // const last2MonthsStartDate = new Date(date.getFullYear(), date.getMonth() - 2, 1);
// const filterAssignmentsByDate = (startDate: any, endDate: any) =>
// publisher.assignments.filter((assignment: { shift: { startTime: string | number | Date; }; }) => isDateBetween(new Date(assignment.shift.startTime), startDate, endDate));
// const lastMonthAssignments = filterAssignmentsByDate(monthInfo.firstMonday, monthInfo.lastSunday);
// //const last2MonthsAssignments = filterAssignmentsByDate(last2MonthsStartDate, monthInfo.firstMonday);
// const desiredShifts = publisher.desiredShiftsPerMonth * (monthInfo.nrOfWeeks / 4);
// const availabilityIndex = Math.round((lastMonthAssignments.length / desiredShifts) * 100) / 100;
// return {
// ...publisher,
// availabilityIndex,
// currentWeekAssignments: 0,
// currentMonthAssignments: 0,
// assignmentsLastMonth: lastMonthAssignments.length,
// //assignmentsLast2Months: last2MonthsAssignments.length,
// };
// });
// return [updatedShiftsLastMonth, publishers];
// }
// *********************************************************************************************************************
//region helpers
// *********************************************************************************************************************

93
pages/api/upload.ts Normal file
View File

@ -0,0 +1,93 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { createRouter, expressWrapper } from "next-connect";
import multer from 'multer';
import excel from "../../src/helpers/excel";
import common from "../../src/helpers/common";
const upload = multer({
storage: multer.memoryStorage(),
});
const progressStore = {};
// Update the progressStore instead of the session
function updateProgress(fileId, progress) {
progressStore[fileId] = progress;
};
function getProgress(fileId) {
return progressStore[fileId] || 0;
};
const router = createRouter<NextApiRequest, NextApiResponse>();
router.use(expressWrapper(upload.single('file')))
.post(async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: 'No file uploaded' });
}
// Extract the action and date from query parameters
const { action, date } = req.query;
// Generate a unique upload ID
const fileId = new Date().getTime().toString();
// Initialize progress
updateProgress(fileId, 1);
if (action === 'readword') {
// Start file processing asynchronously
processWordFile(req.file.buffer, date, fileId, true)
.catch(error => {
// Handle any errors here
updateProgress(fileId, 0);
console.error('Грешка при обработката на файла:', error);
});
// Respond immediately
res.status(200).json({ message: 'Файла е качен. Започна обработката на данните.', fileId });
} else {
// Handle other actions or return an error
res.status(400).json({ message: 'Невалидно или неоточнено действие.' });
}
} catch (error) {
// Error handling
res.status(500).json({ message: 'Вътрешна грешка на сървъра', error: error.message });
}
})
.get((req, res) => {
console.log('Progress check handler');
const { fileId } = req.query;
var progress = getProgress(fileId);
res.status(200).json({ progress });
}
)
// Asynchronous file processing function
async function processWordFile(fileBuffer, dateString, fileId, createAvailabilities) {
const [year, month, day] = dateString.split('-');
await excel.ReadDocxFileForMonth(null, fileBuffer, month, year, (currentProgress) => {
updateProgress(fileId, currentProgress);
}, createAvailabilities);
}
// // Progress check handler - moved to server.js
// router.get('/progress/:id', (req, res) => {
// const { fileId } = req.query;
// var progress = getProgress(fileId);
// res.status(200).json({ progress });
// });
const handler = (req: NextApiRequest, res: NextApiResponse) => {
router.run(req, res);
};
export default handler;
export const config = {
api: {
bodyParser: false, // Necessary for file upload
},
};

View File

@ -0,0 +1,2 @@
import NewPage from "../new";
export default NewPage;

View File

@ -0,0 +1,158 @@
//next.js page to show all locatons in the database with a link to the location page
import { Availability, UserRole } from "@prisma/client";
import { format } from "date-fns";
import { useRouter } from "next/router";
import { useState } from 'react';
import Layout from "../../../components/layout";
import axiosInstance from '../../../src/axiosSecure';
import axiosServer from '../../../src/axiosServer';
import ProtectedRoute from '../../../components/protectedRoute';
import AvCalendar from '../../../components/calendar/avcalendar';
interface IProps {
initialItems: Availability[];
id: string;
}
// export default function AvPage({} : IProps) {
export default function AvPage({ initialItems, id }: IProps) {
const router = useRouter();
// items.forEach(item => {
// item.publisher = prisma.publisher.findUnique({where: {id: item.publisherId}});
// });
const [items, set] = useState(initialItems);
const events = initialItems?.map(item => ({
id: item.id,
title: item.name,
date: new Date(item.startTime),
start: new Date(item.startTime),
end: new Date(item.endTime),
isactive: item.isactive,
publisherId: item.publisher.id,
dayOfMonth: item.dayOfMonth,
dayOfWeek: item.dayOfWeek,
}));
const render = () => {
console.log("showing " + initialItems?.length + " availabilities");
if (initialItems?.length === 0) return <h1>No Items</h1>;
return ( //AvailabilityList(items));
<>
<table className="min-w-full">
<thead className="border-b">
<tr>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
#
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Publisher
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Name
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Weekday
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
From
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
To
</th>
</tr>
</thead>
<tbody>
{initialItems?.map((item: Availability) => (
<tr key={item.id} className={item.isactive ? "" : "text-gray-300"}>
<td className="px-6 py-4 whitespace-nowrap ">
{item.id} {item.isactive}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.publisher.lastName}, {item.publisher.firstName}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.name}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.dayofweek}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{format(new Date(item.startTime), "HH:mm")}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{format(new Date(item.endTime), "HH:mm")}
</td>
<td>
<button className="btn text-gray-700"
onClick={() => router.push(`/cart/availabilities/edit/${item.id}`)} >
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</>
)
};
return <Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN]}>
<AvCalendar publisherId={id} events={events} />
<div className="max-w-7xl mx-auto">
<div>
{render()}
</div>
{/* <div className="flex justify-center">
<a href="/cart/availabilities/new" className="btn">
New availability
</a>
</div> */}
</div></ProtectedRoute>
</Layout>
}
import { getSession } from "next-auth/react";
import { serverSideAuth } from '../../../components/protectedRoute'; // Adjust the path as needed
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
const auth = await serverSideAuth({
req: context.req,
allowedRoles: [/* ...allowed roles... */]
});
// const prisma = new PrismaClient()
//get current user role from session
const session = await getSession(context);
if (!session) { return { props: {} } }
const role = session?.user.role;
console.log("server role: " + role);
var queryUrl = process.env.NEXTAUTH_URL + "/api/data/availabilities?select=id,name,isactive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id";
if (role === UserRole.USER || context.query.my) {
queryUrl += `&where={"publisherId":"${session?.user.id}"}`;
} else if (role == UserRole.ADMIN) {
if (context.query.id) {
queryUrl += `&where={"publisherId":"${context.query.id}"}`;
} else {
queryUrl += `&where={"isactive":true}`;
}
}
var resp = await axios.get(
queryUrl
// process.env.NEXTAUTH_URL + "/api/data/availabilities?include=publisher",
, { decompress: true });
var items = resp.data;
console.log("got " + items.length + " availabilities");
return {
props: {
initialItems: items,
id: context.query.id || session?.user.id || null,
},
};
};

View File

@ -0,0 +1,42 @@
//next.js page to show all locatons in the database with a link to the location page
import { Availability } from "@prisma/client";
import AvailabilityForm from "../../../components/availability/AvailabilityForm";
import Layout from "../../../components/layout";
import axiosServer from '../../../src/axiosServer';
export default function NewPage(item: Availability) {
return (
<Layout>
<div className="h-5/6 grid place-items-center">
<AvailabilityForm id={item.id} publisherId={item.publisherId} />
</div>
</Layout>
);
}
//------------------pages\cart\availabilities\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
if (!context.query || !context.query.id) {
return {
props: {}
};
}
const { data: item } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/availabilities/" + context.params.id
);
return {
props: {
item: item,
},
};
};

View File

@ -0,0 +1,908 @@
import React, { useState, useEffect, use } from 'react';
import { useSession } from "next-auth/react"
import Link from 'next/link';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
import axiosInstance from '../../../src/axiosSecure';
import Layout from "../../../components/layout"
import Shift from '../../../components/calendar/ShiftComponent';
import { DayOfWeek, UserRole } from '@prisma/client';
import { env } from 'process'
import ShiftComponent from '../../../components/calendar/ShiftComponent';
//import { set } from 'date-fns';
const common = require('src/helpers/common');
import { toast } from 'react-toastify';
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
// import { useSession,} from 'next-auth/react';
// import { getToken } from "next-auth/jwt"
//define Shift type
interface Shift {
id: number;
startTime: Date;
endTime: Date;
cartEventId: number;
assignments: Assignment[];
}
interface Assignment {
id: number;
publisherId: number;
shiftId: number;
isConfirmed: boolean;
publisher: Publisher;
}
interface Publisher {
id: number;
firstName: string;
lastName: string;
isImported: boolean;
}
// https://www.npmjs.com/package/react-calendar
export default function CalendarPage({ initialEvents, initialShifts }) {
const { data: session } = useSession()
//if logged in, get the user's email
// var email = "";
// const [events, setEvents] = useState(initialEvents);
const events = initialEvents;
const [allShifts, setAllShifts] = useState(initialShifts);
const [value, onChange] = useState<Date>(new Date());
const [shifts, setShifts] = React.useState([]);
const [error, setError] = React.useState(null);
const [availablePubs, setAvailablePubs] = React.useState([]);
const [selectedShiftId, setSelectedShiftId] = useState(null);
const [isOperationInProgress, setIsOperationInProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [activeButton, setActiveButton] = useState(null);
const isLoading = (buttonId) => activeButton === buttonId;
// ------------------ MODAL ------------------
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalPub, setModalPub] = useState(null);
// ------------------ no assignments checkbox ------------------
const [isCheckboxChecked, setIsCheckboxChecked] = useState(false);
const handleCheckboxChange = (event) => {
setIsCheckboxChecked(!isCheckboxChecked); // Toggle the checkbox state
};
useEffect(() => {
console.log("checkbox checked: " + isCheckboxChecked);
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
}, [isCheckboxChecked]); // Dependency array
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
useEffect(() => {
const newMonth = value.getMonth();
if (newMonth !== selectedMonth) {
setSelectedMonth(newMonth);
}
}, [value, selectedMonth]);
const handleCalDateChange = async (selectedDate) => {
var date = new Date(common.getDateFromDateTime(selectedDate));//ToDo: check if seting the timezone affects the selectedDate?!
var dateStr = common.getISODateOnly(date);
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
if (isCheckboxChecked) {
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
setAvailablePubs(availablePubsForDate);
}
else {
console.log(`getting shifts for ${common.getISODateOnly(date)}`)
try {
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
setShifts(shiftsForDate);
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
//remove availabilities that are isFromPreviousAssignment or from previous month for each publisher
// availablePubsForDate = availablePubsForDate.map(pub => {
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
// return pub;
// });
//commented for now: remove unavailable publishers
// availablePubsForDate = availablePubsForDate.map(pub => {
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
// return pub;
// });
setAvailablePubs(availablePubsForDate);
console.log(`found shifts for ${dateStr}: ${shiftsForDate.length}`);
} catch (err) {
console.error("Error fetching shifts:", err);
setError(err);
}
onChange(selectedDate);
}
}
const handleShiftSelection = (selectedShift) => {
setSelectedShiftId(selectedShift.id);
const updatedPubs = availablePubs.map(pub => {
const isAvailableForShift = pub.availabilities.some(avail =>
avail.startTime <= selectedShift.startTime
&& avail.endTime >= selectedShift.endTime
&& avail.isFromPreviousAssignment == false
);
const isAvailableForShiftWithPrevious = pub.availabilities.some(avail =>
avail.startTime <= selectedShift.startTime
&& avail.endTime >= selectedShift.endTime
);
console.log(`Publisher ${pub.firstName} ${pub.lastName} is available for shift ${selectedShift.id}: ${isAvailableForShift}`);
// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + pub.availabilities.map(avail => avail.startTime + " - " + avail.endTime));
// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + stringify.join(', 'pub.availabilities.map(avail => avail.id)));
const availabilitiesIds = pub.availabilities.map(avail => avail.id).join(', ');
console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities with IDs: ${availabilitiesIds}`);
return { ...pub, isAvailableForShift, isAvailableForShiftWithPrevious, isSelected: pub.id === selectedShift.selectedPublisher?.id };
});
// Sort publishers based on their availability state. use currentDayAssignments, currentWeekAssignments,
// currentMonthAssignments and previousMonthAssignments properties
// Sort publishers based on availability and then by assignment counts.
const sortedPubs = updatedPubs.sort((a, b) => {
if (a.isactive !== b.isactive) {
return a.isactive ? -1 : 1;
}
// First, sort by isselected.
if (a.isSelected !== b.isSelected) {
return a.isSelected ? -1 : 1;
}
// Them, sort by availability.
if (a.isAvailableForShift !== b.isAvailableForShift) {
return a.isAvailableForShift ? -1 : 1;
}
// If both are available (or unavailable) for the shift, continue with the additional sorting logic.
// Prioritize those without currentDayAssignments.
if (!!a.currentDayAssignments !== !!b.currentDayAssignments) {
return a.currentDayAssignments ? 1 : -1;
}
// Then prioritize those without currentWeekAssignments.
if (!!a.currentWeekAssignments !== !!b.currentWeekAssignments) {
return a.currentWeekAssignments ? 1 : -1;
}
// Prioritize those with fewer currentMonthAvailabilityHoursCount.
if (a.currentMonthAvailabilityHoursCount !== b.currentMonthAvailabilityHoursCount) {
return a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount;
}
// Finally, sort by (currentMonthAssignments - previousMonthAssignments).
return (a.currentMonthAssignments - a.previousMonthAssignments) - (b.currentMonthAssignments - b.previousMonthAssignments);
});
setAvailablePubs(sortedPubs); // Assuming availablePubs is a state managed by useState
};
const handleSelectedPublisher = (publisher) => {
// Do something with the selected publisher
console.log("handle pub clicked:", publisher);
}
const handlePublisherModalOpen = async (publisher) => {
// Do something with the selected publisher
console.log("handle pub modal opened:", publisher.firstName + " " + publisher.lastName);
let date = new Date(value);
const { data: publisherInfo } = await axiosInstance.get(`/api/?action=getPublisherInfo&id=${publisher.id}&date=${common.getISODateOnly(date)}`);
publisher.assignments = publisherInfo.assignments;
publisher.availabilities = publisherInfo.availabilities;
publisher.email = publisherInfo.email;
setModalPub(publisher);
setIsModalOpen(true);
}
// file uploads
const [fileActionUrl, setFileActionUrl] = useState('');
const [file, setFile] = useState(null);
const handleFileUpload = async (event) => {
setIsOperationInProgress(true);
console.log('handleFileUpload(): Selected file:', event.target.files[0], 'actionUrl:', fileActionUrl);
setFile(event.target.files[0]);
if (!event.target.files[0]) {
toast.error('Моля, изберете файл!');
return;
}
uploadToServer(fileActionUrl, event.target.files[0]);
};
const uploadToServer = async (actionUrl, file) => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/' + actionUrl, {
method: 'POST',
body: formData,
});
const result = await response.json();
if (result.fileId) {
pollProgress(result.fileId);
}
console.log('Result from server-side API:', result);
toast.info(result.message || "Файла е качен! Започна обработката на данните...");
} catch (error) {
toast.error(error.message || "Възникна грешка при обработката на данните.");
} finally {
}
};
const pollProgress = (fileId: any) => {
fetch(`/api/upload?fileId=${fileId}`)
.then(response => response.json())
.then(data => {
updateProgressBar(data.progress); // Update the progress bar
if (data.progress < 98 && data.progress > 0) {
// Poll every second if progress is between 0 and 100
setTimeout(() => pollProgress(fileId), 1000);
} else if (data.progress === 0) {
// Handle error case
toast.error("Възникна грешка при обработката на данните.");
setIsOperationInProgress(false);
} else {
// Handle completion case
toast.success("Файла беше обработен успешно!");
setIsOperationInProgress(false);
}
})
.catch(error => {
console.error('Error polling for progress:', error);
toast.error("Грешка при обновяването на напредъка");
setIsOperationInProgress(false)
})
.finally();
};
const updateProgressBar = (progress: string) => {
// Implement the logic to update your progress bar based on the 'progress' value
// For example, updating the width of a progress bar element
const progressBar = document.getElementById('progress-bar');
if (progressBar) {
progressBar.style.width = progress + '%';
}
};
function getEventClassname(event, allShifts, date) {
if (event && allShifts) {
const matchingShifts = allShifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return shift.cartEventId === event.id && shiftDate.getDate() === date.getDate() && shiftDate.getMonth() === date.getMonth();
});
//get matching shifts with assignments using nextcrud
//const { data: withAss } = await axiosInstance.get(`/shifts?include=assignments&where={"id":{"$in":[${matchingShifts.map(shift => shift.id)}]}}`);
const minCount = Math.min(...matchingShifts.map(shift => shift.assignedCount)) || 0;
//const minCount = 4;
//console.log("matchingShifts: " + matchingShifts) + " for date " + date;
if (matchingShifts.length < 3) { return "text-gray"; }
else {
if (minCount === 0) return "text-red-700 font-bold ";
if (minCount === 1) return "text-brown-900 font-bold ";
if (minCount === 2) return "text-orange-500";
if (minCount === 3) return "text-yellow-500";
if (minCount >= 4) return "text-blue-500";
}
}
return "text-default"; // A default color in case none of the conditions are met.
}
const onTileContent = ({ date, view }) => {
// Add your logic here
var dayName = common.DaysOfWeekArray[date.getDayEuropean()];
var classname = "";
if (events == null) {
return <div>{" "}</div>;
}
const event = events.find((event) => {
return event.dayofweek == dayName;
});
if (event != null) {
const classname = getEventClassname(event, allShifts, date);
return <div className={classname}>
{new Date(event.startTime).getHours() + "-" + new Date(event.endTime).getHours()}ч.
</div>
}
return <div>{" "}</div>;
};
const addAssignment = async (publisher, shiftId) => {
try {
console.log(`new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`);
const newAssignment = {
publisher: { connect: { id: publisher.id } },
shift: { connect: { id: shiftId } },
isactive: true,
isConfirmed: true
};
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
// Update the 'publisher' property of the returned data with the full publisher object
data.publisher = publisher;
} catch (error) {
console.error("Error adding assignment:", error);
}
};
const removeAssignment = async (publisher, shiftId) => {
try {
const assignment = publisher.assignments.find(ass => ass.shift.id === shiftId);
console.log(`remove assignment for shift ${shiftId}`);
const { data } = await axiosInstance.delete(`/api/data/assignments/${assignment.id}`);
} catch (error) {
console.error("Error removing assignment:", error);
}
}
// ----------------------------------------------------------
// button handlers
// ----------------------------------------------------------
const importShifts = async () => {
try {
setActiveButton("importShifts");
setIsOperationInProgress(true);
let date = new Date(value);
date.setDate(date.getDate() + 1);
const dateString = common.getISODateOnly(date);
const fileInput = document.getElementById('fileInput');
// setFileActionUrl(`readword/${dateString.slice(0, 4)}/${dateString.slice(5, 7)}/${dateString.slice(8, 10)}?action=import`);
setFileActionUrl(`api/upload?action=readword&date=${dateString}`);
console.log('fileaction set to ' + fileActionUrl);
fileInput.click();
//handleFileUpload({ target: { files: [file] } });
fileInput.value = null;
} catch (error) {
toast.error(error);
} finally {
setIsOperationInProgress(false);
setActiveButton(null);
}
}
const fetchShifts = async () => {
try {
setActiveButton("fetchShifts");
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/api/data/shifts?include=assignments.publisher&where={"startTime":{"$and":[{"$gte":"2022-12-04T15:09:47.768Z","$lt":"2022-12-10T15:09:47.768Z"}]}}`);
setShifts(data);
toast.success('Готово!', { autoClose: 1000 });
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const generateShifts = async (buttonId, copyFromPrevious = false, autoFill = false, forDay?: Boolean | null) => {
try {
setActiveButton(buttonId);
const endpoint = `/api/shiftgenerate?action=generate&date=${common.getISODateOnly(value)}&copyFromPreviousMonth=${copyFromPrevious}&autoFill=${autoFill}&forDay=${forDay}`;
const { shifts } = await axiosInstance.get(endpoint);
toast.success('Готово!', { autoClose: 1000 });
setIsMenuOpen(false);
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const deleteShifts = async (buttonId, forDay: Boolean) => {
try {
setActiveButton(buttonId);
await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}&forDay=${forDay}`);
toast.success('Готово!', { autoClose: 1000 });
setIsMenuOpen(false);
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const sendMails = async () => {
try {
var month = new Date(value).getMonth() + 1;
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/sendmails/${new Date(value).getFullYear()}/${month}`);
} catch (error) {
console.log(error);
}
}
const generateXLS = async () => {
try {
var month = new Date(value).getMonth() + 1;
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/generatexcel/${new Date(value).getFullYear()}/${month}/2`);
} catch (error) {
console.log(error);
}
}
const generateDOCX = async () => {
try {
setActiveButton("generateDOCX");
var month = new Date(value).getMonth() + 1;
const response = await axiosInstance.get(`/getDocxFile/${new Date(value).getFullYear()}/${month}`, { responseType: 'blob' });
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `График 2023.${month}.docx`);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.log(error);
}
}
//get all publishers and create txt file with their names, current and previous month assignments count (getPublisherInfo)
//
const generateMonthlyStatistics = async () => {
try {
var month = new Date(value).getMonth() + 1;
let { data: allPublishersInfo } = await axiosInstance.get(`/api/?action=getMonthlyStatistics&date=${common.getISODateOnly(value)}`);
//order by name and generate the list
allPublishersInfo = allPublishersInfo.sort((a, b) => {
if (a.firstName !== b.firstName) {
return a.firstName < b.firstName ? -1 : 1;
} if (a.lastName !== b.lastName) {
return a.lastName < b.lastName ? -1 : 1;
}
return 0;
});
var list = "";
allPublishersInfo.forEach(pub => {
// list += `${pub.firstName} ${pub.lastName}\t ${pub.currentMonthAssignments} / ${pub.previousMonthAssignments}\n`;
list += `${pub.firstName} ${pub.lastName}\t ${pub.currentMonthAssignments}\n`;
});
//write to google sheets file
//download the file
const url = window.URL.createObjectURL(new Blob([list]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `Статистика 2023.${month}.txt`);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.log(error);
}
}
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false);
return (
<>
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
{/* Page Overlay */}
{isOperationInProgress && (
<div className="loading-overlay">
<div className="spinner"></div>
</div>
)}
<input id="fileInput" title="file input" type="file" onChange={handleFileUpload}
accept=".json, .doc, .docx, .xls, .xlsx" style={{ display: 'none' }}
/>
<div className="mb-4">
<button className="button m-2 bg-blue-800" onClick={importShifts}>
{isLoading('importShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fa fa-file-import"></i>)} Импорт от Word
</button>
<button className="button btn m-2 bg-blue-800" onClick={generateDOCX}>
{isLoading('generateDOCX') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fa fa-file-export"></i>)}Експорт в Word
</button>
<button className="button btn m-2 bg-yellow-500 hover:bg-yellow-600 text-white" onClick={() => { setActiveButton("sendEmails"); setConfirmModalOpen(true) }}>
{isLoading('sendEmails') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-envelope mr-2"></i>)} изпрати мейли!
</button>
<ConfirmationModal
isOpen={isConfirmModalOpen}
onClose={() => setConfirmModalOpen(false)}
onConfirm={() => {
toast.info("Вие потвърдихте!", { autoClose: 2000 });
setConfirmModalOpen(false);
sendMails()
}}
message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
/>
<div className="relative inline-block text-left">
<button
className={`button m-2 ${isMenuOpen ? 'bg-gray-400 border border-blue-500' : 'bg-gray-300'} hover:bg-gray-400`}
onClick={() => { setIsMenuOpen(!isMenuOpen) }}>
<i className="fa fa-ellipsis-h"></i> Още
</button>
{isMenuOpen && (
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
{/* Group 1: Daily actions */}
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmptyDay", false, false, true)}>
{isLoading('genEmptyDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
създай празни ({value.getDate()}-ти) </button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => generateShifts("genDay", false, true, true)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени ({value.getDate()}-ти) </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" onClick={() => { deleteShifts("deleteShiftsDay", true) }}>
{isLoading('deleteShiftsDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-trash-alt mr-2"></i>)}
изтрий смените ({value.getDate()}-ти)</button>
<hr className="my-1" />
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmpty", false, false)}>
{isLoading('genEmpty') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
създай празни </button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap" onClick={() => generateShifts("genCopy", true)}>
{isLoading('genCopy') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-copy mr-2"></i>)}
копирай от миналия месец</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)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" onClick={() => { deleteShifts("deleteShifts", false) }}>
{isLoading('deleteShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-trash-alt mr-2"></i>)}
изтрий смените</button>
<hr className="my-1" />
<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>
</div>
</div>
)}
{/* <button className={`button m-2 bg-blue-800 ${isOperationInProgress ? 'disabled' : ''}`} onClick={importShifts}>
{isOperationInProgress ? <div className="spinner"></div> : 'Import shifts (and missing Publishers) from WORD'}
</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts()}>Generate empty shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true)}>Copy last month shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true, true)}>Generate Auto shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(false, true, value)}>Generate Auto shifts DAY</button>
// <button className="button m-2" onClick={fetchShifts}>Fetch shifts</button>
// <button className="button m-2" onClick={sendMails}>Send mails</button>
// <button className="button m-2" onClick={generateXLS}>Generate XLSX</button>
// <button className="button m-2" onClick={async () => {
// await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}`);
// }
// }>Delete shifts (selected date's month)</button>
// <button className="button m-2" onClick={generateMonthlyStatistics}>Generate statistics</button>
*/}
</div>
</div>
{/* progress bar holder */}
{isOperationInProgress && (
<div id="progress" className="w-full h-2 bg-gray-300">
<div id="progress-bar" className="h-full bg-green-500" style={{ width: `${progress}%` }}></div>
</div>
)}
<div className="flex">
{/* Calendar section */}
<div className="flex-3">
<Calendar
className={['customCalendar']}
onChange={handleCalDateChange}
value={value}
tileContent={onTileContent}
locale="bg-BG"
/>
{/* ------------------------------- PUBLISHERS LIST ----------------------------------
list of publishers for the selected date with availabilities
------------------AVAILABLE PUBLISHERS LIST FOR THE SELECTED DATE0 ------------------ */}
<div className="flex flex-col items-center my-8 sticky top-0">
<h2 className="text-lg font-semibold mb-4">Достъпни за този ден: <span className="text-blue-600">{availablePubs.length}</span></h2>
<label className="toggle pb-3">
<input type="checkbox" className="toggle-checkbox" onChange={handleCheckboxChange} />
<span className="toggle-slider m-1">без назначения за месеца</span>
</label>
<ul className="w-full max-w-md">
{Array.isArray(availablePubs) && availablePubs?.map((pub, index) => {
// Determine background and border classes based on conditions
let bgAndBorderColorClass;
if (pub.isAvailableForShift) {
if (pub.currentDayAssignments === 0) {
const comparisonResultClass = pub.currentMonthAvailabilityDaysCount < pub.previousMonthAssignments ? 'bg-green-100' : 'bg-green-50';
bgAndBorderColorClass = `${comparisonResultClass} border-l-4 border-green-400`;
} else if (!pub.isSelected) {
bgAndBorderColorClass = 'bg-orange-50 border-l-4 border-orange-400';
}
} else {
if (pub.isAvailableForShiftWithPrevious) // add left orange border
{
bgAndBorderColorClass = 'border-l-4 border-orange-400';
}
else {
bgAndBorderColorClass = 'bg-white';
}
}
//tOdO: CHECK WHY THIS IS NOT WORKING
if (!pub.hasEverFilledForm) {
//bgAndBorderColorClass = 'border-t-2 border-yellow-400';
}
// Determine border class if selected
const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : '';
// Determine opacity class
const activeOpacityClass = pub.isactive ? '' : 'opacity-25';
return (
<li key={index}
className={`flex justify-between items-center p-4 rounded-lg shadow-sm mb-2
${bgAndBorderColorClass} ${selectedBorderClass} ${activeOpacityClass}`}
onDoubleClick={(handlePublisherModalOpen.bind(this, pub))}
>
<span className={`text-gray-700 ${pub.isAvailableForShift ? 'font-bold' : 'font-medium'} `}>
{pub.firstName} {pub.lastName}
</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`} >
{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>
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
<span tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
<button tooltip="желани участия този месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
{/* Shift list section */}
<div className="flex-grow mx-5">
<div className="flex-col" id="shiftlist">
{shifts.map((shift, index) => (
<ShiftComponent key={index} shift={shift}
onShiftSelect={handleShiftSelection} isSelected={shift.id == selectedShiftId}
onPublisherSelect={handleSelectedPublisher} showAllAuto={true}
allPublishersInfo={availablePubs} />
))}
</div>
</div>
</div>
<div>
{/* <CustomCalendar date={value} shifts={shifts} /> */}
</div>
{isModalOpen && <PublisherShiftsModal publisher={modalPub} shifts={allShifts} onClose={() => setIsModalOpen(false)} />}
</ProtectedRoute >
</Layout >
</>
);
function PublisherShiftsModal({ publisher, shifts, onClose }) {
const monthInfo = common.getMonthDatesInfo(new Date(value));
const monthShifts = shifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return shiftDate > monthInfo.firstDay && shiftDate < monthInfo.lastDay;
});
const weekShifts = monthShifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return common.getStartOfWeek(value) <= shiftDate && shiftDate <= common.getEndOfWeek(value);
});
const dayShifts = weekShifts.map(shift => {
const isAvailable = publisher.availabilities.some(avail =>
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
);
let color = isAvailable ? getColorForShift(shift) : 'bg-gray-300';
if (shift.isFromPreviousMonth) {
color += ' border-l-4 border-orange-500 ';
}
if (shift.isFromPreviousAssignment) {
color += ' border-l-4 border-red-500 ';
}
return { ...shift, isAvailable, color };
}).reduce((acc, shift) => {
const dayIndex = new Date(shift.startTime).getDay();
acc[dayIndex] = acc[dayIndex] || [];
acc[dayIndex].push(shift);
return acc;
}, {});
console.log("dayShifts:", dayShifts);
const hasAssignment = (shiftId) => {
return publisher.assignments.some(ass => ass.shift.id === shiftId);
};
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
console.log('ESC: closing modal.');
onClose(); // Call the onClose function when ESC key is pressed
}
};
// Add event listener
window.addEventListener('keydown', handleKeyDown);
// Remove event listener on cleanup
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]); // Include onClose in the dependency array
return (
<div className="fixed top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-50 z-50">
<div className="relative bg-white p-8 rounded-lg shadow-xl max-w-xl w-full h-auto overflow-y-auto">
<h2 className="text-xl font-semibold mb-4">График на <span title={publisher.email} className='publisher'>
<strong>{publisher.firstName} {publisher.lastName}</strong>
<span className="publisher-tooltip" onClick={common.copyToClipboard}>{publisher.email}</span>
</span> тази седмица:</h2>
{/* ... Display shifts in a calendar-like UI ... */}
<div className="grid grid-cols-6 gap-4 mb-4">
{Object.entries(dayShifts).map(([dayIndex, shiftsForDay]) => (
<div key={dayIndex} className="flex flex-col space-y-2 justify-end">
{/* Day header */}
<div className="text-center font-medium">{new Date(shiftsForDay[0].startTime).getDate()}-ти</div>
{shiftsForDay.map((shift, index) => {
const assignmentExists = hasAssignment(shift.id);
const availability = publisher.availabilities.find(avail =>
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
);
const isFromPrevMonth = availability && availability.isFromPreviousMonth;
return (
<div
key={index}
className={`text-sm text-white p-2 rounded-md ${isFromPrevMonth ? 'border-l-4 border-yellow-500' : ''} ${assignmentExists ? 'bg-blue-200' : shift.color} h-24 flex flex-col justify-center`}
>
{common.getTimeRange(shift.startTime, shift.endTime)}
{!assignmentExists && shift.isAvailable && (
<button onClick={() => { addAssignment(publisher, shift.id); onClose() }}
className="mt-2 bg-green-500 text-white p-1 rounded hover:bg-green-600 active:bg-green-700 focus:outline-none"
>
добави
</button>
)}
{assignmentExists && (
<button onClick={() => { removeAssignment(publisher, shift.id) }} // Implement the removeAssignment function
className="mt-2 bg-red-500 text-white p-1 rounded hover:bg-red-600 active:bg-red-700 focus:outline-none"
>
махни
</button>
)}
</div>
);
}
)}
</div>
))}
</div>
{/* Close button in the top right corner */}
<button
onClick={onClose}
className="absolute top-3 right-2 p-2 px-3 bg-red-500 text-white rounded-full hover:bg-red-600 active:bg-red-700 focus:outline-none"
>
&times;
</button>
{/* <Link href={`/cart/publishers/edit/${modalPub.id}`}
className="mt-2 bg-blue-500 text-white p-1 rounded hover:bg-blue-600 active:bg-blue-700 focus:outline-none">
<i className="fas fa-edit" />
</Link> */}
{/* Edit button in the top right corner, next to the close button */}
<Link href={`/cart/publishers/edit/${modalPub.id}`} className="absolute top-3 right-12 p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 active:bg-blue-700 focus:outline-none">
<i className="fas fa-edit" />
</Link>
</div>
</div >
);
}
function getColorForShift(shift) {
const assignedCount = shift.assignedCount || 0; // Assuming each shift has an assignedCount property
switch (assignedCount) {
case 0: return 'bg-blue-300';
case 1: return 'bg-green-300';
case 2: return 'bg-yellow-300';
case 3: return 'bg-orange-300';
case 4: return 'bg-red-200';
default: return 'bg-gray-300';
}
}
}
import axiosServer from '../../../src/axiosServer';
import { start } from 'repl';
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
const baseUrl = common.getBaseUrl();
console.log('runtime BaseUrl: ' + baseUrl);
console.log('runtime NEXTAUTH_URL: ' + process.env.NEXTAUTH_URL);
console.log('Runtime Axios Base URL:', axios.defaults.baseURL);
const currentDate = new Date();
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() - 3, 1);
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); // 0th day of the next month gives the last day of the current month
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
const prismaClient = common.getPrismaClient();
// let events = await prismaClient.cartEvent.findMany({ where: { isactive: true } });
// events = events.map(event => ({
// ...event,
// // Convert Date objects to ISO strings
// startTime: event.startTime.toISOString(),
// endTime: event.endTime.toISOString(),
// }));
const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":true}`);
//const { data: shifts } = await axios.get(url);
// get all shifts for the month, including assigments
let shifts = await prismaClient.shift.findMany({
where: {
isactive: true,
startTime: {
gte: firstDayOfMonth,
//lt: lastDayOfMonth
}
},
include: {
assignments: {
include: {
publisher: {
select: {
id: true,
}
}
}
}
}
});
//calculate assCount for each shift
shifts = shifts.map(shift => ({
...shift,
assignedCount: shift.assignments.length,
startTime: shift.startTime.toISOString(),
endTime: shift.endTime.toISOString(),
}));
return {
props: {
initialEvents: events,
initialShifts: shifts,
},
};
}

View File

@ -0,0 +1,47 @@
import React, { useState, useEffect, use } from 'react';
import { useSession } from "next-auth/react"
import Link from 'next/link';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
import axiosInstance from '../../../src/axiosSecure';
import Layout from "../../../components/layout"
import Shift from '../../../components/calendar/ShiftComponent';
import { DayOfWeek, UserRole } from '@prisma/client';
import { env } from 'process'
import ShiftComponent from '../../../components/calendar/ShiftComponent';
//import { set } from 'date-fns';
const common = require('src/helpers/common');
import { toast } from 'react-toastify';
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
const SchedulePage = () => {
const { data: session } = useSession();
const [htmlContent, setHtmlContent] = useState(""); // State to hold fetched HTML content
useEffect(() => {
// Define an async function to fetch the HTML content
const fetchHtmlContent = async () => {
try {
// Replace '/api/schedule' with your actual API endpoint
const response = await axiosInstance.get('/api/schedule?year=2024&month=1', { responseType: 'text' });
setHtmlContent(response.data); // Set the fetched HTML content in state
} catch (error) {
console.error("Failed to fetch schedule:", error);
// Handle error (e.g., display an error message)
}
};
fetchHtmlContent(); // Call the function to fetch HTML content
}, []); // Empty dependency array means this effect runs once on component mount
return (
<Layout>
<ProtectedRoute deniedMessage="">
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
</ProtectedRoute>
</Layout>
);
};
export default SchedulePage;

View File

@ -0,0 +1,25 @@
import NewPage from "../new";
import axiosServer from '../../../../src/axiosServer';
export default NewPage;
export const getServerSideProps = async (context) => {
console.log("edit page getServerSideProps");
const axios = await axiosServer(context);
const { id } = context.query;
const { data } = await axios.get(`${process.env.NEXTAUTH_URL}/api/data/cartevents/` + id);
const locations = await axios
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`)
.then((res) => {
console.log("locations: " + JSON.stringify(res.data));
return res.data;
});
return {
props: {
item: data,
locations: locations,
inline: false,
},
};
};

View File

@ -0,0 +1,119 @@
import { CartEvent, UserRole } from '@prisma/client';
import { useRouter } from "next/router";
import { useState } from "react";
import Layout from "../../../components/layout";
import common from 'src/helpers/common';
import ProtectedRoute from '../../../components/protectedRoute';
import CartEventForm from '../../../components/cartevent/CartEventForm';
// import IProps from '../../../components/cartevent/CartEventForm'
import axiosServer from '../../../src/axiosServer';
import { getServerSession } from "next-auth/next"
import { authOptions } from "../../../pages/api/auth/[...nextauth]"
// export default CartEventForm;
export interface ICartEventPageProps {
items: [CartEvent];
locations: [Location];
inline: false;
}
export default function CartEventPage({ items, locations }: ICartEventPageProps) {
const router = useRouter();
const [addnew, setAddNew] = useState(false);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="flex flex-col">
<h1>All cart events</h1>
<table className="min-w-full">
<thead className="border-b">
<tr>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
#
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Day of Week
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Time
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Shift Duration
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Active
</th>
</tr>
</thead>
<tbody>
{items.map((item: CartEvent, i) => (
<tr key={i} className="border-b">
<td className="px-6 py-4 whitespace-nowrap">
{item.id}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<strong>{common.dayOfWeekNames[common.getDayOfWeekIndex(item.dayofweek)]} </strong>
на {locations.find(l => l.id == item.locationId).name}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{new Date(item.startTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
+ " до " + new Date(item.endTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.shiftDuration}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.isactive ? "Yes" : "No"}
</td>
<td>
<button className="button bg-blue-500 hover:bg-blue-700"
onClick={() => router.push(`/cart/cartevents/edit/${item.id}`)}
>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
<button className="button bg-blue-500 hover:bg-blue-700"
onClick={() => setAddNew(!addnew)}
> {addnew ? "обратно" : "Добави нов"}</button>
{addnew && <CartEventForm locations={locations} />}
</div>
</ProtectedRoute>
</Layout>
)
}
export const getServerSideProps = async (context) => {
const session = await getServerSession(context.req, context.res, authOptions)
context.req.session = session;
const axios = await axiosServer(context);
const { data: items } = await axios.get("/api/data/cartevents");
console.log("gettnng locations from: " + "/api/data/locations?select=id,name");
const locations = await axios
.get(`/api/data/locations?select=id,name`)
.then((res) => {
console.log("locations: " + JSON.stringify(res.data));
return res.data;
});
return {
props: {
items,
locations: locations,
inline: false,
},
};
};

View File

@ -0,0 +1,55 @@
//next.js page to show all locatons in the database with a link to the location page
import CartEventForm from "../../../components/cartevent/CartEventForm";
import Layout from "../../../components/layout";
import axiosServer from '../../../src/axiosServer';
import { ICartEventPageProps } from "./index";
export default function NewPage(props: ICartEventPageProps) {
return (
<Layout>
<div className="h-5/6 grid place-items-center">
<CartEventForm props={props} />
{/*
<AvailabilityForm id={item.id} publisherId={item.publisherId} /> */}
</div>
</Layout>
);
}
//------------------pages\cart\availabilities\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
const axios = await axiosServer(context);
const locations = await axios
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`)
.then((res) => {
console.log("locations: " + JSON.stringify(res.data));
return res.data;
});
if (!context.query || !context.query.id) {
return {
props: {}
};
}
const { id } = context.query.id;
const { data: item } = await axiosInstance.get(
process.env.NEXTAUTH_URL + "/api/data/cartevents/" + context.params.id
);
return {
props: {
item: item,
locations: locations
},
};
};

View File

@ -0,0 +1,122 @@
import React, { useState, useEffect } from 'react';
import Layout from "../../../components/layout";
import { Carousel } from 'react-responsive-carousel';
import "react-responsive-carousel/lib/styles/carousel.min.css"; // requires a loader
import { GetServerSideProps } from 'next';
import { Location, UserRole } from "@prisma/client";
import axiosServer from '../../../src/axiosServer';
const ViewLocationPage: React.FC<ViewLocationPageProps> = ({ location }) => {
const [activeTab, setActiveTab] = useState('mainLocation');
const [activeImage, setActiveImage] = useState(0);
const [images, setImages] = useState([]);
const [mainLocationImageCount, setMainLocationImageCount] = useState(0);
useEffect(() => {
const mainLocationImages = [location.picture1, location.picture2, location.picture3].filter(Boolean);
const backupLocationImages = location.backupLocationImages?.filter(Boolean) ?? [];
setImages([...mainLocationImages, ...backupLocationImages]);
setMainLocationImageCount(mainLocationImages.length);
}, [location.picture1, location.picture2, location.picture3, location.backupLocationImages]);
const handleTabChange = (tab: string) => {
setActiveTab(tab);
//show the proper image in the carousel
if (tab === 'backupLocation') {
setActiveImage(mainLocationImageCount);
} else {
setActiveImage(0);
}
};
const handleCarouselChange = (index) => {
// Switch to backupLocation tab if the current carousel image index is from the backup location
if (index >= mainLocationImageCount) {
setActiveTab('backupLocation');
} else {
setActiveTab('mainLocation');
}
setActiveImage(index);
};
return (
<Layout>
<div className="view-location-page max-w-4xl mx-auto my-8">
{/* Tabs */}
<div className="tabs flex border-b">
{/* Main Location Tab */}
<button
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'mainLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
onClick={() => handleTabChange('mainLocation')}
>
{location.name}
</button>
{/* Backup Location Tab */}
<button
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'backupLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
onClick={() => handleTabChange('backupLocation')}
>
При лошо време: <strong>{location.backupLocationName}</strong>
</button>
</div>
{/* Carousel */}
{images.length > 0 && (
<Carousel showArrows={true}
autoPlay={false}
infiniteLoop={true}
showThumbs={false}
onChange={handleCarouselChange}
selectedItem={activeImage}
>
{images.map((src, index) => (
<div key={index}>
<img src={src} alt={`Slide ${index + 1}`} />
</div>
))}
</Carousel>
)}
{/* Tab Content */}
{(location.content || location.backupLocationContent) && (
<div className="tab-content mt-4">
{activeTab === 'mainLocation' && (
<div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.content }} />
)}
{activeTab === 'backupLocation' && location.backupLocationContent && (
<div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.backupLocationContent }} />
)}
</div>
)}
</div>
</Layout>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const axios = await axiosServer(context);
const { data: location } = await axios.get(
`${process.env.NEXTAUTH_URL}/api/data/locations/${context.params.id}`
);
if (location.backupLocationId !== null) {
const { data: backupLocation } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/locations/" + location.backupLocationId
);
location.backupLocationName = backupLocation.name;
location.backupLocationContent = backupLocation ? backupLocation.content : "";
location.backupLocationImages = backupLocation ? [backupLocation.picture1, backupLocation.picture2, backupLocation.picture3].filter(Boolean) : [];
}
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
return {
props: {
location: location,
},
};
};
export default ViewLocationPage;

View File

@ -0,0 +1,41 @@
//next.js page to show all locatons in the database with a link to the location page
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../../components/layout";
import LocationForm from "../../../../components/location/LocationForm";
import axiosServer from '../../../../src/axiosServer';
import ProtectedRoute from '../../../../components/protectedRoute';
function NewPage(item: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-center">
<LocationForm key={item.id} item={item} />
</div>
</ProtectedRoute>
</Layout>
);
}
export default NewPage;
//------------------pages\cart\locations\edit\[id].tsx------------------//
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
if (context.query.id === "new" || context.query.id === 0) {
return {
props: {}
};
}
const { data: item } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/locations/" + context.params.id
);
console.log(item) //this is the location object
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
return {
props: {
item: item,
},
};
};

View File

@ -0,0 +1,51 @@
//next.js page to show all locatons in the database with a link to the location page
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import LocationCard from "../../../components/location/LocationCard";
import axiosServer from '../../../src/axiosServer';
import ProtectedRoute from '../../../components/protectedRoute';
interface IProps {
item: Location;
}
function LocationsPage({ items = [] }: IProps) {
const renderLocations = () => {
if (!Array.isArray(items) || items.length === 0) return <h1>No Locations</h1>;
return items.map((item) => (
<LocationCard key={item.id} location={item} />
));
};
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="grid gap-4 grid-cols-1 md:grid-cols-4">
{renderLocations()}
</div>
{/* add location link */}
<div className="flex justify-center">
<a href="/cart/locations/new" className="btn">
Add Location
</a>
</div>
</ProtectedRoute>
</Layout>
);
}
export default LocationsPage;
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
const { data: items } = await axios.get("/api/data/locations");
//console.log('get server props - locations:' + items.length);
//console.log(items);
return {
props: {
items,
},
};
};

View File

@ -0,0 +1,44 @@
//next.js page to show all locatons in the database with a link to the location page
// import axios from "axios";
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import LocationForm from "../../../components/location/LocationForm";
import axiosServer from '../../../src/axiosServer';
import ProtectedRoute from '../../../components/protectedRoute';
function NewPage(loc: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-center">
<LocationForm key={loc.id} location={loc} />
</div></ProtectedRoute>
</Layout>
);
}
export default NewPage;
//------------------pages\cart\locations\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
//if query is undefined, then it is a new location
if (context.query.id === undefined) {
return {
props: {}
};
}
const { data: loc } = await axios.get(
`${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
);
console.log(location) //this is the location object
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
return {
props: {
location: loc,
},
};
};

View File

@ -0,0 +1,87 @@
import { useState } from 'react';
import Layout from "../../../components/layout";
import ProtectedRoute from '../../../components/protectedRoute';
import { UserRole } from '@prisma/client';
import axiosServer from '../../../src/axiosServer';
import common from '../../../src/helpers/common';
function ContactsPage({ publishers }) {
const [searchQuery, setSearchQuery] = useState('');
const filteredPublishers = publishers.filter((publisher) =>
publisher.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
publisher.lastName.toLowerCase().includes(searchQuery.toLowerCase()) ||
publisher.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
publisher.phone?.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
<div className="container mx-auto p-4">
<h1 className="text-xl font-semibold mb-4">Контакти</h1>
<input
type="text"
placeholder="Търси по име, имейл или телефон..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="border border-gray-300 rounded-md px-2 py-2 mb-4 w-full text-base md:text-sm"
/>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr>
<th className="border-b font-medium p-4 pl-8 pt-0 pb-3">Име</th>
<th className="border-b font-medium p-4 pt-0 pb-3">Имейл</th>
<th className="border-b font-medium p-4 pt-0 pb-3">Телефон</th>
</tr>
</thead>
<tbody>
{filteredPublishers.map((publisher) => (
<tr key={publisher.id}>
<td className="border-b p-4 pl-8">{publisher.firstName} {publisher.lastName}</td>
<td className="border-b p-4">
<a href={`mailto:${publisher.email}`} className="text-blue-500">{publisher.email}</a>
</td>
<td className="border-b p-4">
<div className="flex items-center justify-between">
<span className={common.isValidPhoneNumber(publisher.phone) ? '' : 'text-red-500'}>{publisher.phone}</span>
<div className="flex items-center">
<a href={`tel:${publisher.phone}`} className="inline-block p-2 mr-2">
<i className="fas fa-phone-alt text-blue-500 text-xl" title="Обаждане"></i>
</a>
<a href={`https://wa.me/${publisher.phone}`} className="inline-block p-2 mr-2">
<i className="fab fa-whatsapp text-green-500 text-xl" title="WhatsApp"></i>
</a>
{publisher.phone ? (
<a href={`viber://chat/?number=%2B${publisher.phone.startsWith('+') ? publisher.phone.substring(1) : publisher.phone}`} className="inline-block p-2">
<i className="fab fa-viber text-purple-500 text-xl" title="Viber"></i>
</a>
) : null}
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</ProtectedRoute>
</Layout>
);
}
export default ContactsPage;
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,phone');
return {
props: {
publishers,
},
};
};

View File

@ -0,0 +1,105 @@
import axiosServer from '../../../../src/axiosServer';
import NewPubPage from "../new";
export default NewPubPage;
import { Assignment, Shift, UserRole } from "prisma/prisma-client";
// import { monthNamesBG } from "~/src/helpers/const"
import { monthNamesBG } from "src/helpers/const";
function getShiftGroups(shifts: [Shift]) {
const groupedShifts = shifts.reduce((groups, shift) => {
// Extract the year and month from the shift date
const yearMonth = shift.startTime.substring(0, 7)
// Initialize the group for the year-month if it doesn't exist
if (!groups[yearMonth]) {
groups[yearMonth] = []
}
// Add the shift to the group
groups[yearMonth].push(shift)
// Return the updated groups object
return groups
}, {})
// Sort the groups by year-month
const sortedGroups = Object.keys(groupedShifts).sort((a, b) => {
// Compare the year-month strings lexicographically
if (a < b) return -1
if (a > b) return 1
return 0
}).reduce((result, key) => {
// Rebuild the object with the sorted keys
result[key] = groupedShifts[key]
return result
}, {})
return sortedGroups;
}
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
if (!context.query || !context.query.id) {
return {
props: {}
};
}
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift";
console.log("GET PUBLISHER FROM:" + url)
const { data: item } = await axios.get(url);
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);
//group shifts by month, remove duplicates
//sort availabilities by start time
// item.availabilities = item.availabilities
// .sort((a, b) => b.startTime - a.startTime);
item.assignments = item.assignments
.sort((a, b) => b.startTime - a.startTime)
.reduce((acc, assignment: Assignment) => {
const date = new Date(assignment.shift.startTime);
const year = date.getFullYear();
const month = date.getMonth();
const tabId = year + "-" + month;
const tabName = monthNamesBG[month] + " " + year;
const day = date.getDate();
// console.log("shift: year: " + year + " month: " + month + " day: " + day);
if (!acc.items[tabId]) {
acc.items[tabId] = [];
}
if (!acc.items[tabId][day]) {
acc.items[tabId][day] = [];
}
//remove duplicates
if (acc.items[tabId][day].find(s => s.id == assignment.shift.id)) {
return acc;
}
// acc.months = acc.months || [];
if (!acc.months[tabId]) {
acc.months[tabId] = [];
}
if (!acc.keys.includes(tabId)) {
acc.months[tabId] = tabName;
acc.keys.push(tabId);
}
acc.items[tabId][day].push({
start: assignment.shift.startTime,
end: assignment.shift.endTime,
id: assignment.id,
shiftId: assignment.shift.id,
isConfirmed: assignment.isConfirmed ? true : false,
});
return acc;
}, { items: {}, keys: [], months: {} });
// console.log("server item:");
// console.dir(item, { depth: null });
return {
props: {
item: item
},
};
};

View File

@ -0,0 +1,20 @@
// pages/me.jsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
export default function Me() {
const router = useRouter();
const { data: session, status } = useSession();
useEffect(() => {
if (status === 'authenticated') {
router.push(`/cart/publishers/edit/${session.user.id}?self=true`);
} else if (status === 'unauthenticated') {
router.push('/api/auth/signin');
}
}, [status, session, router]);
// You can add a fallback content or loader here if you want
return <div>Redirecting...</div>;
}

View File

@ -0,0 +1,629 @@
import { toast } from 'react-toastify';
import Layout from "../../../components/layout";
import { Publisher, Availability, AvailabilityType, DayOfWeek, UserRole } from "@prisma/client";
import ProtectedRoute from '../../../components/protectedRoute';
import axiosInstance from '../../../src/axiosSecure';
import { useState, useRef } from "react";
import * as XLSX from "xlsx";
// import { Table } from "react-bootstrap";
import { staticGenerationAsyncStorage } from "next/dist/client/components/static-generation-async-storage.external";
import moment from 'moment';
// import { DatePicker } from '@mui/x-date-pickers'; !! CAUSERS ERROR ???
// import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { set } from 'date-fns';
// import * as common from "src/helpers/common";
const common = require('../../../src/helpers/common');
export default function ImportPage() {
const [data, setData] = useState([])
const [rawData, setRawData] = useState([]);
const [status, setStatus] = useState({ status: 'idle', info: '' });
const MODE_PUBLISHERS1 = "PUBLISHERS1";
const MODE_PUBLISHERS2 = "PUBLISHERS2";
type ModeState = {
mainMode: typeof MODE_PUBLISHERS1 | typeof MODE_PUBLISHERS2;
schedule: boolean;
publishers2Import: boolean;
headerRow: number;
}
const [mode, setMode] = useState<ModeState>({
mainMode: MODE_PUBLISHERS1,
schedule: false,
publishers2Import: false,
headerRow: 0
});
const headerRef = useRef({
header: null,
dateIndex: -1,
emailIndex: -1,
nameIndex: -1,
phoneIndex: -1,
isTrainedIndex: -1,
desiredShiftsIndex: -1,
dataStartIndex: -1,
isActiveIndex: -1,
pubTypeIndex: -1
});
const handleFile = (e) => {
const [file] = e.target.files;
const reader = new FileReader();
reader.onload = (evt) => {
const bstr = evt.target.result;
const wb = XLSX.read(bstr, { type: "binary" });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const sheetData = XLSX.utils.sheet_to_json(ws, {
header: 1,
range: 0,
blankrows: false,
defval: '',
});
for (let i = 0; i < 5; i++) {
if (sheetData[i].includes('Имейл')) {
setMode({
mainMode: MODE_PUBLISHERS1,
schedule: false,
publishers2Import: false,
headerRow: i
});
headerRef.current.header = sheetData[i];
headerRef.current.dataStartIndex = i;
common.logger.debug("header at row " + i);
break;
}
// it seems we are processing availability sheet. import only publishers by default, and read only the first 3 columns as publisher data
if (sheetData[i].includes('Email Address')) {
setMode({
mainMode: MODE_PUBLISHERS2,
schedule: true,
publishers2Import: false,
headerRow: i
});
headerRef.current.header = sheetData[i];
headerRef.current.dataStartIndex = i;
break;
}
}
if (!headerRef.current.header) {
console.error("header not found in the first 5 rows!");
return;
}
const header = headerRef.current.header
headerRef.current.dateIndex = header.indexOf('Timestamp');
headerRef.current.emailIndex = header.indexOf('Имейл') !== -1 ? header.indexOf('Имейл') : header.indexOf('Email Address');
headerRef.current.nameIndex = header.indexOf('Име, Фамилия');
headerRef.current.phoneIndex = header.indexOf('Телефон');
headerRef.current.isTrainedIndex = header.indexOf('Обучен');
headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия');
headerRef.current.isActiveIndex = header.indexOf("Неактивен");
headerRef.current.pubTypeIndex = header.indexOf("Назначение");
const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => {
let date;
date = common.excelSerialDateToDate(row[headerRef.current.dateIndex]);
//substract 1 day, because excel serial date is 1 day ahead
date.setDate(date.getDate() - 1);
date = common.getDateFormated(date);
common.logger.debug(date);
return [date, row[headerRef.current.emailIndex], row[headerRef.current.nameIndex]];
});
setRawData(sheetData);
setData(filteredData);
setStatus({ status: 'зареден', info: `Заредени ${filteredData.length} от ${rawData.length} записа` });
};
reader.readAsBinaryString(file);
// Reset the file input value
e.target.value = null;
};
const handleSave = async () => {
try {
common.logger.debug("handleSave to: " + common.getBaseUrl());
const header = rawData[mode.headerRow];
for (let i = mode.headerRow + 1; i < rawData.length; i++) { //fullData.length; each publisher
//update status.info with current publisher
setStatus({ status: 'running', info: `Processing row ${i} of ${rawData.length}` });
//sleep for 300ms to allow the database to process the previous request
await new Promise(r => setTimeout(r, 100));
const row = rawData[i];
var email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = false;
//const date = new Date(row[0]).toISOS{tring().slice(0, 10);
if (mode.mainMode == MODE_PUBLISHERS1) {
email = row[headerRef.current.emailIndex];
phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace
// Remove any non-digit characters, except for the leading +
//phone = phone.replace(/(?!^\+)\D/g, '');
phone = phone.replace(/[^+\d]/g, '');
if (phone.startsWith('8') || phone.startsWith('9')) {
phone = '+359' + phone.substring(1); // Assumes all numbers starting with 8 are Bulgarian and should have +359
} else if (!phone.startsWith('+')) {
phone = '+' + phone; // Add + if it's missing, assuming the number is complete
}
names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/);
dateOfInput = importDate.value || new Date().toISOString();
// not empty == true
isTrained = row[headerRef.current.isTrainedIndex] !== '';
isActive = row[headerRef.current.isActiveIndex] == '';
desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4;
}
else {
dateOfInput = common.excelSerialDateToDate(row[0]);
email = row[1];
names = row[2].normalize('NFC').split(/[, ]+/);
}
//remove special characters from name
// let names = common.removeAccentsAndSpecialCharacters(row[2]).split(/[, ]+/);
let personId = '';
try {
try {
const select = "&select=id,firstName,lastName,phone,isTrained,desiredShiftsPerMonth,isactive,availabilities";
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
let existingPublisher = responseByName.data[0];
if (!existingPublisher) {
// If no match by name, check by email
const responseByEmail = await axiosInstance.get(`/api/?action=findPublisher&email=${email}${select}`);
if (responseByEmail.data.length > 0) {
// Iterate over all matches by email to find one with a matching or similar name
const fullName = names.join(' ').toLowerCase(); // Simplify comparison
existingPublisher = responseByEmail.data.find(publisher => {
const publisherFullName = (publisher.firstName + ' ' + publisher.lastName).toLowerCase();
return fullName === publisherFullName; // Consider expanding this comparison for typos
});
}
}
if (existingPublisher?.id) { // UPDATE
// Create a flag to check if update is needed
const updatedData = {};
personId = existingPublisher?.id;
let updateNeeded = false;
// Check for name update
const fullName = names.join(' ');
const existingFullName = existingPublisher.firstName + ' ' + existingPublisher.lastName;
if (fullName !== existingFullName) {
common.logger.debug(`Existing publisher '${existingFullName}' found for ${email} (ID:${personId})`);
updatedData.firstName = names[0];
updatedData.lastName = names.slice(1).join(' ');
data[i - mode.headerRow][4] = "name updated!";
updateNeeded = true;
} else {
data[i - mode.headerRow][4] = "existing";
}
// Log existing publisher
common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`);
// Check for other updates
const fieldsToUpdate = [
{ key: 'phone', value: phone },
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
{ key: 'isTrained', value: isTrained },
{ key: 'isactive', value: isActive }
];
fieldsToUpdate.forEach(({ key, value, parse }) => {
if (!existingPublisher[key] && value !== '' && value !== undefined) {
updatedData[key] = parse ? parse(value) : value;
updateNeeded = true;
}
});
// Update the record if needed and if MODE_PUBLISHERS1 (Import from List of Participants)
if (updateNeeded && (mode.publishers2Import || mode.mainMode == MODE_PUBLISHERS1)) {
try {
await axiosInstance.put(`/api/data/publishers/${personId}`, updatedData);
common.logger.debug(`Updated publisher ${personId}`);
data[i - mode.headerRow][4] = "updated";
} catch (error) {
console.error(`Failed to update publisher ${personId}`, error);
}
}
} else { // CREATE
// If no publisher with the email exists, create one
// const names = email.split('@')[0].split('.');\
//Save new publisher
if (mode.publishers2Import) {
const personResponse = await axiosInstance.post('/api/data/publishers', {
email,
phone,
firstName: names[0],
lastName: names[1],
isactive: isActive,
isTrained,
desiredShiftsPerMonth,
});
personId = personResponse.data.id;
data[i][4] = "new";
}
else
if (mode.mainMode == MODE_PUBLISHERS1) {
const firstname = names.length > 2 ? names.slice(0, -1).join(' ') : names[0];
const personResponse = await axiosInstance.post('/api/data/publishers', {
email,
phone,
firstName: firstname,
lastName: names[names.length - 1],
isactive: isActive,
isTrained,
desiredShiftsPerMonth
});
data[i - mode.headerRow][4] = "new";
} else {
data[i - mode.headerRow][4] = "import disabled";
}
common.logger.debug(`NEW Publisher ${personId} created for email ${email} (${names})`);
}
} catch (error) {
console.error(error);
data[i - mode.headerRow][4] = "error";
}
if (mode.schedule) {
// Parse the availability data from the Excel cell
//get days of the month and add up to the next full week
// Save availability records
const availabilities: Availability[] = [];
for (let j = 3; j < header.length; j++) {
const dayHeader = header[j];
const shifts = row[j];
if (!shifts || shifts === 'Не мога') {
continue;
}
// specific date: Седмица (17-23 април) [Четвъртък ]
// const regex = /^Седмица \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/;
// specific week: Седмица 1 <any character> [Четвъртък]
// const regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/;
//allow optional space before and after the brackets
// match both 'Седмица 3 (20-25 ноември) [пон]' and 'Седмица 4 (27 ноември - 2 декември) [четв]'
//const regex = /^\s*Седмица\s+(\d{1,2})\s+\((\d{1,2})\s+(\S+)(?:\s*-\s*(\d{1,2})\s+(\S+))?\)\s*\[\s*(\S+)\s*\]\s*$/;
//the original, but missing the two month names
let regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
regex = /^Седмица (\d{1,2}) \((\d{1,2})(?:\s*-\s*(\d{1,2}))? (\S+)(?:\s*-\s*(\d{1,2}) (\S+))?\)\s*\[(\S+)\s*\]\s*$/;
//const regex = /^Седмица (\d{1,2}) \((\d{1,2}(\s*\S*?))-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
//both Седмица 1 (6-11 ноември) [пон]
// Седмица 4 (27 ноември-2 декември) [пет]
//const regex = /^Седмица (\d{1,2}) \((\d{1,2} \S+)?-? ?(\d{1,2} \S+)\)\s*\[(\S+)\s*\]\s*$/;
//const regex = /^Седмица (\d{1,2}) \((\d{1,2} \S+)?-? ?(\d{1,2} \S+)\)\s*\[(\S+)\s*\]\s*$/;
// const regex = /^Седмица (\d{1,2}) \* \[(\S+)\]$/;
// const regex = /^Седмица (\d{1,2}) \[(\S+)\]$/;
// replace multiple spaces with single space
const normalizedHeader = dayHeader.replace(/\s+/g, ' ');
var match = normalizedHeader.match(regex);
if (!match) {
common.logger.debug("was not able to parse availability " + shifts + "trying again with different regex");
let regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
match = normalizedHeader.match(regex);
}
if (match) {
//ToDo: can't we use date.getDayEuropean() instead of this logic down?
const weekNr = parseInt(match[1]);
const weekStart = match[2];
// const endDate = match[2];
const month = match[4];
const dayOfWeekStr = match[7];
const dayOfWeek = common.getDayOfWeekIndex(dayOfWeekStr);
common.logger.debug("processing availability for week " + weekNr + ": " + weekStart + "." + month + "." + dayOfWeekStr)
// Create a new Date object for the start date of the range
const day = new Date();
day.setDate(1); // Set to the first day of the month to avoid overflow
//day.setMonth(day.getMonth() + 1); // Add one month to the date, because we assume we are p
day.setMonth(common.getMonthIndex(month));
day.setDate(parseInt(weekStart) + dayOfWeek);
day.setHours(0, 0, 0, 0);
common.logger.debug("processing availabilities for " + day.toLocaleDateString()); // Output: Sun Apr 17 2022 14:07:11 GMT+0300 (Eastern European Summer Time)
common.logger.debug("parsing availability input: " + shifts); // Output: 0 (Sunday)
const dayOfWeekName = common.getDayOfWeekNameEnEnum(day);
let dayOfMonth = day.getDate();
const name = `${names[0]} ${names[1]}`;
const intervals = shifts.split(",");
if (!oldAvDeleted && personId) {
if (mode.schedule && email) {
common.logger.debug(`Deleting existing availabilities for publisher ${personId} for date ${day}`);
try {
await axiosInstance.post(`/api/?action=deleteAvailabilityForPublisher&publisherId=${personId}&date=${day}&deleteFromPreviousAssignments=true`);
common.logger.debug(`Deleted all availabilities for publisher ${personId}`);
oldAvDeleted = true;
}
catch (error) {
console.error(`Failed to delete availabilities for publisher ${personId}`, error);
}
}
}
let parsedIntervals: { end: number; }[] = [];
intervals.forEach(interval => {
// Summer format: (12:00-14:00)
//if (!/\(\d{1,2}:\d{2}-\d{1,2}:\d{2}\)/.test(interval)) {
//winter regex:
//\d{1,2}-\d{1,2}:\d{2}/
// Regular expression to match both Summer format (12:00-14:00) and winter format '09-10:30'
const regex = /\d{1,2}(?::\d{2})?-\d{1,2}(?::\d{2})?/;
if (!regex.test(interval)) {
common.logger.debug(`Skipping invalid interval: ${interval}`);
return;
}
// If the interval matches the format, remove any non-essential characters
const cleanedInterval = interval.replace(/[()]/g, "");
// Extract start and end times from interval string
const [start, end] = cleanedInterval.split("-");
// Convert times like "12" to "12:00" for consistent parsing
const formattedStart = start.includes(":") ? start : start + ":00";
const formattedEnd = end.includes(":") ? end : end + ":00";
// Try to parse the times, and skip the interval if it can't be parsed
try {
const parsedStart = Number(formattedStart.split(":").join(""));
const parsedEnd = Number(formattedEnd.split(":").join(""));
// Store parsed interval
parsedIntervals.push({
start: parsedStart,
end: parsedEnd
});
} catch (error) {
common.logger.debug(`Error parsing interval: ${interval}`);
return;
}
});
// Sort intervals by start time
parsedIntervals.sort((a, b) => a.start - b.start);
// Initialize start and end times with the first interval
let minStartTime = parsedIntervals[0].start;
let maxEndTime = parsedIntervals[0].end;
let isOld = false;
// Calculate the total month difference by considering the year difference
let totalMonthDifference = (day.getFullYear() - dateOfInput.getFullYear()) * 12 + (day.getMonth() - dateOfInput.getMonth());
// If the total month difference is 2 or more, set isOld to true
if (totalMonthDifference >= 2) {
isOld = true;
}
// Iterate over intervals
for (let i = 1; i < parsedIntervals.length; i++) {
if (parsedIntervals[i].start > maxEndTime) {
availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld));
minStartTime = parsedIntervals[i].start;
maxEndTime = parsedIntervals[i].end;
} else {
maxEndTime = Math.max(maxEndTime, parsedIntervals[i].end);
}
}
// Add the last availability
availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld));
}
else {
common.logger.debug("availability not matched. header:" + dayHeader + " shifts:" + shifts);
}
}
common.logger.debug("availabilities to save for " + personId + ": " + availabilities.length);
// Send a single request to create all availabilities
axiosInstance.post('/api/?action=createAvailabilities', availabilities)
.then(response => common.logger.debug('Availabilities created:', response.data))
.catch(error => console.error('Error creating availabilities:', error));
// Experimental: add availabilities to all publishers with the same email
//check if more than one publisher has the same email, and add the availabilities to both
//check existing publishers with the same email
var sameNamePubs = axiosInstance.get(`/api/?action=findPublisher&all=true&email=${email}&select=id,firstName,lastName`);
sameNamePubs.then(function (response) {
common.logger.debug("same name pubs: " + response.data.length);
if (response.data.length > 1) {
response.data.forEach(pub => {
//check the publisher is not the same as the one we already added the availabilities to
if (pub.id != personId) {
//change the publisher id to the new one
availabilities.forEach(availability => {
availability.publisherId = pub.id;
}
);
//delete existing availabilities for the publisher
axiosInstance.post(`/api/?action=deleteAvailabilityForPublisher&publisherId=${pub.id}&date=${dateOfInput}`);
// Send a single request to create all availabilities
axiosInstance.post('/api/?action=createAvailabilities', availabilities)
.then(response => common.logger.debug('Availabilities created:', response.data))
.catch(error => console.error('Error creating availabilities:', error));
}
});
}
});
}
// await axios.post("/api/data/availabilities", availabilities);
} catch (error) {
console.error(error);
}
}
toast.info('Records saved successfully', { autoClose: 30000 });
} catch (error) {
console.error(error);
toast.error('An error occurred while saving records!');
}
};
// Function to create an availability object
function createAvailabilityObject(start: any, end: number, day: Date, dayOfWeekName: any, dayOfMonth: number, weekNr: number, personId: string, name: string, isFromPreviousMonth: boolean): Availability {
const formatTime = (time) => {
const paddedTime = String(time).padStart(4, '0');
return new Date(day.getFullYear(), day.getMonth(), day.getDate(), parseInt(paddedTime.substr(0, 2)), parseInt(paddedTime.substr(2, 4)));
};
const startTime = formatTime(start);
const endTime = formatTime(end);
return {
id: 0, // Add the missing 'id' property
publisherId: personId,
name,
dayofweek: dayOfWeekName,
dayOfMonth,
weekOfMonth: weekNr, // Add the missing 'weekOfMonth' property
startTime,
endTime,
isactive: true,
type: AvailabilityType.OneTime,
isWithTransportIn: false, // Add the missing 'isWithTransport' property
isWithTransportOut: false, // Add the missing 'isWithTransport' property
isFromPreviousAssignment: false, // Add the missing 'isFromPreviousAssignment' property
isFromPreviousMonth: isFromPreviousMonth // Add the missing 'isFromPreviousMonth' property
};
}
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="p-6">
<h1 className="text-3xl mb-4 font-semibold">Import Page</h1>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700" htmlFor="fileInput">Choose a file to import:</label>
<input type="file" id="fileInput" onChange={handleFile} className="mt-1 p-2 border rounded" placeholder="Choose a file" />
</div>
{/* <DatePicker label="Дата" value={date} onChange={setDate} /> */}
<div className="mb-4 space-y-2"> {/* Adjust this value as needed */}
<label className="flex items-center">
<input
type="radio"
value="PUBLISHERS1"
checked={mode.mainMode === "PUBLISHERS1"}
onChange={() => setMode({ mainMode: "PUBLISHERS1", schedule: false, publishers2Import: false })}
className="text-indigo-600"
/>
<span className="ml-2 space-x-4">Импортирай от Списък с участниците</span>
</label>
<label className="flex items-center">
<input
type="radio"
value="PUBLISHERS2"
checked={mode.mainMode === "PUBLISHERS2"}
onChange={() => setMode(prev => ({ ...prev, mainMode: "PUBLISHERS2" }))}
className="text-indigo-600"
/>
<span className="ml-2">Импортирай от Предпочитания за колички</span>
</label>
<label className="flex items-center">
{/* <DatePicker
label="Дата на импорт"
value={new Date()}
onChange={(date) => {
common.logger.debug("date changed to " + date);
}}
/> */}
{/* simple HTML datepicker for import date */}
<input type="date" id="importDate" name="importDate" />
</label>
{mode.mainMode === "PUBLISHERS2" && (
<div className="mt-2 space-y-1 pl-6">
<label className="flex items-center">
<input
type="checkbox"
checked={mode.schedule}
onChange={(e) => setMode(prev => ({ ...prev, schedule: e.target.checked }))}
className="text-indigo-600"
/>
<span className="ml-2">Предпочитания</span>
</label>
<label className="flex items-center ">
<input
type="checkbox"
checked={mode.publishers2Import}
onChange={(e) => setMode(prev => ({ ...prev, publishers2Import: e.target.checked }))}
className="text-indigo-600"
/>
<span className="ml-2">Вестители</span>
</label>
</div>
)}
</div>
<div className="mb-4">
<button onClick={handleSave} className="bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">Запази</button>
</div>
<div className="mb-4 flex items-center space-x-4">
<span className="text-gray-600">{data.length} вестители прочетени</span>
<span className="text-gray-600">{status.info}</span>
</div>
<table className="min-w-full border-collapse border border-gray-500">
<thead>
<tr>
{data.length > 0 &&
Object.keys(data[0]).map((key) => <th className="px-4 py-2 border-b font-medium" key={key}>{Object.values(data[0])[key]}</th>)}
</tr>
</thead>
<tbody>
{data.slice(1).map((row, index) => (
<tr key={index} className="even:bg-gray-100">
{Object.values(row).map((value, index) => (
<td key={index} className="border px-4 py-2">{value}</td>
))}
<td id={row[1]}>
<i className={`fa fa-circle ${status[row[3]] || 'text-gray-400'}`}></i>
</td>
</tr>
))}
</tbody>
</table>
</div>
</ProtectedRoute>
</Layout>
);
};

View File

@ -0,0 +1,214 @@
// Next.js page to show all locations in the database with a link to the location page
import { useSession } from "next-auth/react";
import { useEffect, useState, useRef, use } from "react";
// import { getSession } from 'next-auth/client'
// import { NextAuth } from 'next-auth/client'
import { Publisher, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import PublisherCard from "../../../components/publisher/PublisherCard";
import axiosInstance from "../../../src/axiosSecure";
import axiosServer from '../../../src/axiosServer';
import toast from "react-hot-toast";
import { levenshteinEditDistance } from "levenshtein-edit-distance";
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
interface IProps {
initialItems: Publisher[];
}
function PublishersPage({ publishers = [] }: IProps) {
const [shownPubs, setShownPubs] = useState(publishers);
const [filter, setFilter] = useState("");
const [filterIsImported, setFilterIsImported] = useState({
checked: false,
indeterminate: true,
});
const [showZeroShiftsOnly, setShowZeroShiftsOnly] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleDeleteAllVisible = async () => {
setIsDeleting(true);
for (const publisher of shownPubs) {
try {
await axiosInstance.delete(`/api/data/publishers/${publisher.id}`);
setShownPubs(shownPubs.filter(p => p.id !== publisher.id));
} catch (error) {
console.log(JSON.stringify(error));
}
}
setIsDeleting(false);
setIsModalOpen(false);
// After all publishers are deleted, you might want to refresh the list or make additional changes
};
useEffect(() => {
// const filteredPublishers = publishers.filter((publisher) => {
// return publisher.firstName.toLowerCase().includes(filter.toLowerCase())
// || publisher.lastName.toLowerCase().includes(filter.toLowerCase());
// });
//name filter
let filteredPublishersByName = publishers
.filter((publisher) => {
const fullName = publisher.firstName.toLowerCase() + " " + publisher.lastName.toLowerCase();
const distance = levenshteinEditDistance(fullName, filter.toLowerCase());
const lenDiff = Math.max(fullName.length, filter.length) - Math.min(fullName.length, filter.length);
let similarity;
if (distance === 0) {
similarity = 1; // Exact match
} else {
similarity = 1 - (distance - lenDiff) / distance;
}
console.log("distance: " + distance + "; lenDiff: " + lenDiff + " similarity: " + similarity + "; " + fullName + " =? " + filter + "")
return similarity >= 0.95;
});
// Email filter
let filteredPublishersByEmail = publishers.filter(publisher =>
publisher.email.toLowerCase().includes(filter.toLowerCase())
);
// Combine name and email filters, removing duplicates
let filteredPublishers = [...new Set([...filteredPublishersByName, ...filteredPublishersByEmail])];
// inactive publishers filter
filteredPublishers = showZeroShiftsOnly
? filteredPublishers.filter(p => p.assignments.length === 0)
: filteredPublishers;
setShownPubs(filteredPublishers);
}, [filter, showZeroShiftsOnly]);
const checkboxRef = useRef();
const renderPublishers = () => {
if (shownPubs.length === 0) {
return (
<div className="flex justify-center">
<a
className="btn"
href="javascript:void(0);"
onClick={() => {
setFilter("");
handleFilterChange({ target: { value: "" } });
}}
>
Clear filters
</a>
</div>
);
}
else {
return shownPubs.map((publisher) => (
<PublisherCard key={publisher.id} publisher={publisher} />
));
}
};
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = event.target;
// setFilter(event.target.value);
if (type === 'text') {
setFilter(value);
} else if (type === 'checkbox') {
// setFilterIsImported({ ...checkboxFilter, [name]: checked });
const { checked, indeterminate } = checkboxRef.current;
if (!checked && !indeterminate) {
// Checkbox was unchecked, set it to indeterminate state
checkboxRef.current.indeterminate = true;
setFilterIsImported({ checked: false, indeterminate: true });
} else if (!checked && indeterminate) {
// Checkbox was indeterminate, set it to checked state
checkboxRef.current.checked = true;
checkboxRef.current.indeterminate = false;
setFilterIsImported({ checked: true, indeterminate: false });
} else if (checked && !indeterminate) {
// Checkbox was checked, set it to unchecked state
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = false;
setFilterIsImported({ checked: false, indeterminate: false });
} else {
// Checkbox was checked and indeterminate (should not happen), set it to unchecked state
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = false;
setFilterIsImported({ checked: false, indeterminate: false });
}
}
};
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
<div className="">
<div className="flex items-center justify-center space-x-4 m-4">
<div className="flex justify-center m-4">
<a href="/cart/publishers/new" className="btn"> Добави вестител </a>
</div>
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpen(true)} disabled={isDeleting} >
{isDeleting ? "Изтриване..." : "Изтрий показаните вестители"}
</button>
<ConfirmationModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onConfirm={handleDeleteAllVisible}
message="Сигурни ли сте, че искате да изтриете всички показани в момента вестители?"
/>
<div className="flex justify-center m-4">
<a href="/cart/publishers/import" className="btn"> Import publishers </a>
</div>
</div>
<div name="filters" className="flex items-center justify-center space-x-4 m-4 sticky top-4 z-10 bg-gray-100 p-2">
<label htmlFor="filter">Filter:</label>
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
className="border border-gray-300 rounded-md px-2 py-1"
/>
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
className="form-checkbox text-indigo-600"
/>
<span className="ml-2">само без смени</span>
</label>
<span id="filter-info" className="ml-4">{publishers.length} от {publishers.length} вестителя</span>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-4 z-0">
{renderPublishers()}
</div>
</div>
</ProtectedRoute>
</Layout>
);
}
export default PublishersPage;
//import { set } from "date-fns";
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
//ToDo: refactor all axios calls to use axiosInstance and this URL
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isactive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect');
return {
props: {
publishers,
},
};
};

View File

@ -0,0 +1,59 @@
//next.js page to show all locatons in the database with a link to the location page
import { useState } from "react";
import { useRouter } from 'next/router';
import Layout from "../../../components/layout";
import axiosServer from '../../../src/axiosServer';
import { useSession } from 'next-auth/react';
import ProtectedRoute from '../../../components/protectedRoute';
import PublisherForm from "../../../components/publisher/PublisherForm";
import { Publisher, UserRole } from "@prisma/client";
export default function NewPubPage(item: Publisher) {
item = item.item;
const [publisher, setPublisher] = useState<Publisher>(item);
const router = useRouter();
const { id, self } = router.query;
const { data: session, status } = useSession();
const userRole = session?.user?.role as UserRole;
const userId = session?.user?.id;
// Check if the user is editing their own profile and adjust allowedRoles accordingly
let allowedRoles = [UserRole.POWERUSER, UserRole.ADMIN];
if (status === 'authenticated' && userId && userId === id) {
allowedRoles.push(userRole);
}
return (
<Layout>
<ProtectedRoute allowedRoles={allowedRoles}>
<div className="h-5/6 grid place-items-center">
<PublisherForm key={item?.id} item={item} me={self} />
</div>
</ProtectedRoute>
</Layout>
);
}
//------------------pages\cart\publishers\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
if (!context.query || !context.query.id) {
return {
props: {}
};
}
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts";
console.log("GET PUBLISHER FROM:" + url)
const { data } = await axios.get(url);
return {
props: {
data
},
};
};

View File

@ -0,0 +1,46 @@
//next.js page to show all locatons in the database with a link to the location page
// import axios from "axios";
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import ExperienceForm from "../../../components/reports/ExperienceForm";
import axiosInstance from '../../../src/axiosSecure';
import ProtectedRoute from '../../../components/protectedRoute';
function NewPage(loc: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<div className="h-5/6 grid place-items-center">
<ExperienceForm />
</div></ProtectedRoute>
</Layout>
);
}
export default NewPage;
//------------------pages\cart\locations\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
return {
props: {}
};
//if query is undefined, then it is a new location
// if (context.query.id === undefined) {
// return {
// props: {}
// };
// }
// const { data: loc } = await axiosInstance.get(
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
// );
// console.log(location) //this is the location object
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
// return {
// props: {
// location: loc,
// },
// };
};

130
pages/cart/reports/list.tsx Normal file
View File

@ -0,0 +1,130 @@
//page to show all repots in the database with a link to the report page
import axiosInstance from '../../../src/axiosSecure';
import { 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"
//const common = require('src/helpers/common');
import common from '../../../src/helpers/common';
import Layout from "../../../components/layout";
import ProtectedRoute from '../../../components/protectedRoute';
import { Location, UserRole } from "@prisma/client";
export default function Reports() {
const [reports, setReports] = useState([]);
const router = useRouter();
const { data: session } = useSession();
const deleteReport = (id) => {
axiosInstance
.delete(`api/data/reports/${id}`)
.then((res) => {
toast.success("Успешно изтрит отчет");
// router.push("/cart/reports/list");
setReports(reports.filter(report => report.id !== id));
})
.catch((err) => {
console.log(err);
});
};
const [locations, setLocations] = useState([]);
useEffect(() => {
const fetchLocations = async () => {
try {
console.log("fetching locations");
const { data } = await axiosInstance.get("/api/data/locations");
setLocations(data);
console.log(data);
axiosInstance.get(`api/data/reports`)
.then((res) => {
let reports = res.data;
reports.forEach((report) => {
report.location = data.find((loc) => loc.id === report.locationId);
});
setReports(res.data);
})
.catch((err) => {
console.log(err);
});
} catch (error) {
console.error(error);
}
};
if (!locations.length) {
fetchLocations();
}
}, []);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<div className="h-5/6 grid place-items-center">
<div className="flex flex-col w-full px-4">
<h1 className="text-2xl font-bold text-center">Отчети</h1>
<Link href="/cart/reports/report">
<button className="mt-4 bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
Добави нов отчет
</button>
</Link>
<div className="mt-4 w-full overflow-x-auto">
<table className="w-full table-auto">
<thead>
<tr>
<th className="px-4 py-2 text-left">Дата</th>
<th className="px-4 py-2 text-left" >Място</th>
<th className="px-4 py-2 text-left">Отчет</th>
<th className="px-4 py-2 text-left">Действия</th>
</tr>
</thead>
<tbody>
{reports.map((report) => (
<tr key={report.id}>
<td className="border px-4 py-2">{common.getDateFormated(new Date(report.date))}</td>
<td className="border px-4 py-2">{report.location?.name}</td>
<td className="border px-4 py-2">
{(report.experienceInfo === null || report.experienceInfo === "")
? (
<>
<div><strong>Отчет</strong></div>
Издания: {report.placementCount} <br />
Разговори: {report.conversationCount} <br />
Клипове: {report.videoCount} <br />
Адреси / Телефони: {report.returnVisitInfoCount} <br />
</>
) : (
<>
<div><strong>Случка</strong></div>
<div dangerouslySetInnerHTML={{ __html: report.experienceInfo }} />
</>
)}
</td>
<td className="border px-4 py-2">
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
onClick={() => deleteReport(report.id)}
>
Изтрий
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div >
</div >
</ProtectedRoute>
</Layout>
);
}

View File

@ -0,0 +1,45 @@
//next.js page to show all locatons in the database with a link to the location page
// import axios from "axios";
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import ReportForm from "../../../components/reports/ReportForm";
import axiosInstance from '../../../src/axiosSecure';
import ProtectedRoute from '../../../components/protectedRoute';
function NewPage(loc: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<div className="h-5/6 grid place-items-center">
<ReportForm />
</div></ProtectedRoute>
</Layout>
);
}
export default NewPage;
//------------------pages\cart\locations\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
return {
props: {}
};
//if query is undefined, then it is a new location
// if (context.query.id === undefined) {
// return {
// props: {}
// };
// }
// const { data: loc } = await axiosInstance.get(
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
// );
// console.log(location) //this is the location object
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
// return {
// props: {
// location: loc,
// },
// };
};

31
pages/contactUs.tsx Normal file
View File

@ -0,0 +1,31 @@
import React from 'react';
import Layout from "../components/layout";
const ContactsPage = () => {
return (
<Layout>
<div className="mx-auto my-8 p-6 max-w-4xl bg-white rounded-lg shadow-md">
<h1 className="text-2xl font-bold text-gray-800 mb-4">Специално свидетелстване на обществени места в София - Контакти</h1>
<ul className="list-disc pl-5">
<li className="text-gray-700 mb-2">Янко Ванчев - <a href="tel:+359878224467" className="text-blue-500 hover:text-blue-600">+359 878 22 44 67</a></li>
<li className="text-gray-700">Крейг Смит - <a href="tel:+359878994573" className="text-blue-500 hover:text-blue-600">+359 878 994 573</a></li>
</ul>
<div className="text-gray-700 pl-4 py-5">Електронна поща: <a href="mailto:specialnosvidetelstvanesofia@gmail.com" className="text-blue-500 hover:text-blue-600">specialnosvidetelstvanesofia@gmail.com</a></div>
{/* <div className="mt-6">
<h3 className="text-lg font-semibold text-gray-800 mb-3">Социални мрежи</h3>
<div className="flex space-x-4">
<a href="#" className="text-blue-500 hover:text-blue-600">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M24 4.557a9.83 9.83 0 0 1-2.828.775 4.932 4.932 0 0 0 2.165-2.723c-.951.564-2.005.974-3.127 1.195a4.916 4.916 0 0 0-8.384 4.482C7.69 7.88 4.067 5.794 1.64 2.905a4.822 4.822 0 0 0-.666 2.475c0 1.706.869 3.213 2.188 4.096a4.904 4.904 0 0 1-2.228-.616v.06a4.923 4.923 0 0 0 3.946 4.827 4.996 4.996 0 0 1-2.212.083 4.937 4.937 0 0 0 4.604 3.417A9.867 9.867 0 0 1 0 19.54a13.995 13.995 0 0 0 7.548 2.209c9.142 0 14.307-7.721 13.995-14.646A9.936 9.936 0 0 0 24 4.557z" /></svg>
</a>
<a href="#" className="text-blue-500 hover:text-blue-600">
</a>
</div>
</div > */
}
</div >
</Layout >
);
};
export default ContactsPage;

200
pages/dash.tsx Normal file
View File

@ -0,0 +1,200 @@
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
import AvCalendar from '../components/calendar/avcalendar';
import { getSession } from "next-auth/react";
import common from '../src/helpers/common';
import { Availability } from "@prisma/client";
import ProtectedRoute, { serverSideAuth } from "../components/protectedRoute";
import { UserRole } from "@prisma/client";
import React, { useState, useEffect } from 'react';
import axiosInstance from '../src/axiosSecure';
import { authOptions } from './api/auth/[...nextauth]'
import { getServerSession } from "next-auth/next"
import PublisherSearchBox from '../components/publisher/PublisherSearchBox';
import PublisherInlineForm from '../components/publisher/PublisherInlineForm';
interface IProps {
initialItems: Availability[];
initialUserId: string;
}
export default function IndexPage({ initialItems, initialUserId }: IProps) {
const { data: session } = useSession();
const [userName, setUserName] = useState(session?.user?.name);
const [userId, setUserId] = useState(initialUserId);
const [events, setEvents] = useState(initialItems?.map(item => ({
...item,
title: item.name,
date: new Date(item.startTime),
startTime: new Date(item.startTime),
endTime: new Date(item.endTime),
publisherId: item.publisherId,
})));
useEffect(() => {
if (session) {
setUserName(session.user.name);
setUserId(session.user.id);
//handleUserSelection({ id: session.user.id, firstName: session.user.name, lastName: '' });
}
}, [session]);
const handleUserSelection = async (publisher) => {
if (!publisher || publisher.id === undefined) return;
console.log("selecting publisher", publisher.id);
setUserName(publisher.firstName + " " + publisher.lastName);
setUserId(publisher.id);
try {
let events = await axiosInstance.get(`/api/?action=getCalendarEvents&publisherId=${publisher.id}`);
setEvents(events.data);
} catch (error) {
console.error("Error fetching publisher info:", error);
// Handle the error appropriately
}
};
return (
<Layout>
<ProtectedRoute deniedMessage="">
<h1 className="pt-2 pb-1 text-xl font-bold text-center">Графика на {userName}</h1>
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
<PublisherSearchBox selectedId={userId} infoText="" onChange={handleUserSelection} />
</ProtectedRoute>
<div className="flex flex-row md:flex-row mt-4 space-y-4 md:space-y-0 md:space-x-4 h-[calc(100vh-10rem)]">
<div className="flex-1">
<div className="text-center font-bold pb-3">
<PublisherInlineForm publisherId={userId} />
</div>
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} />
</div>
</div>
</ProtectedRoute>
</Layout>
);
}
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,
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.getTimeFomatted(item.startTime) + "-" + common.getTimeFomatted(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,
isTentative: 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.getTimeFomatted(new Date(item.shift.startTime)) + "-" + common.getTimeFomatted(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,
allowedRoles: [/* ...allowed roles... */]
});
const session = await getSession(context);
const sessionServer = await getServerSession(context.req, context.res, authOptions)
if (!session) { return { props: {} } }
const role = session?.user.role;
console.log("server role: " + role);
const userId = session?.user.id;
var items = await getAvailabilities(session.user.id);
return {
props: {
initialItems: items,
userId: session?.user.id,
},
};
}

19
pages/drive.tsx Normal file
View File

@ -0,0 +1,19 @@
import Layout from "../components/layout"
import GoogleDriveFolderPreview from "../components/board/GoogleDriveFolderPreview"
export default function IndexPage() {
return (
<Layout>
<h1>NextAuth.js Example</h1>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
</p>
<GoogleDriveFolderPreview folderId={"1DADj8OUWz2sMEmiW3sDETuihJvFS5x_p"}>
{/* https://drive.google.com/drive/folders/1DADj8OUWz2sMEmiW3sDETuihJvFS5x_p?usp=sharing */}
</GoogleDriveFolderPreview>
</Layout>
)
}

17
pages/examples/admin.tsx Normal file
View File

@ -0,0 +1,17 @@
import Layout from "../../components/layout"
export default function Page() {
return (
<Layout>
<h1>This page is protected by Middleware</h1>
<p>Only admin users can see this page.</p>
<p>
To learn more about the NextAuth middleware see&nbsp;
<a href="https://docs-git-misc-docs-nextauthjs.vercel.app/configuration/nextjs#middleware">
the docs
</a>
.
</p>
</Layout>
)
}

View File

@ -0,0 +1,19 @@
import Layout from "../../components/layout"
export default function ApiExamplePage() {
return (
<Layout>
<h1>API Example</h1>
<p>The examples below show responses from the example API endpoints.</p>
<p>
<em>You must be signed in to see responses.</em>
</p>
<h2>Session</h2>
<p>/api/examples/session</p>
<iframe src="/api/examples/session" />
<h2>JSON Web Token</h2>
<p>/api/examples/jwt</p>
<iframe src="/api/examples/jwt" />
</Layout>
)
}

27
pages/examples/client.tsx Normal file
View File

@ -0,0 +1,27 @@
import Layout from "../../components/layout"
export default function ClientPage() {
return (
<Layout>
<h1>Client Side Rendering</h1>
<p>
This page uses the <strong>useSession()</strong> React Hook in the{" "}
<strong>&lt;Header/&gt;</strong> component.
</p>
<p>
The <strong>useSession()</strong> React Hook is easy to use and allows
pages to render very quickly.
</p>
<p>
The advantage of this approach is that session state is shared between
pages by using the <strong>Provider</strong> in <strong>_app.js</strong>{" "}
so that navigation between pages using <strong>useSession()</strong> is
very fast.
</p>
<p>
The disadvantage of <strong>useSession()</strong> is that it requires
client side JavaScript.
</p>
</Layout>
)
}

32
pages/examples/policy.tsx Normal file
View File

@ -0,0 +1,32 @@
import Layout from "../../components/layout"
export default function PolicyPage() {
return (
<Layout>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
</p>
<h2>Terms of Service</h2>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
<h2>Privacy Policy</h2>
<p>
This site uses JSON Web Tokens and an in-memory database which resets
every ~2 hours.
</p>
<p>
Data provided to this site is exclusively used to support signing in and
is not passed to any third party services, other than via SMTP or OAuth
for the purposes of authentication.
</p>
</Layout>
)
}

View File

@ -0,0 +1,41 @@
import { useSession } from "next-auth/react"
import { useEffect, useState } from "react"
import AccessDenied from "../../components/access-denied"
import Layout from "../../components/layout"
export default function ProtectedPage() {
const { data: session } = useSession()
const [content, setContent] = useState()
// Fetch content from protected route
useEffect(() => {
const fetchData = async () => {
const res = await fetch("/api/examples/protected")
const json = await res.json()
if (json.content) {
setContent(json.content)
}
}
fetchData()
}, [session])
// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p>
<strong>{content ?? "\u00a0"}</strong>
</p>
</Layout>
)
}

47
pages/examples/server.tsx Normal file
View File

@ -0,0 +1,47 @@
import { getServerSession } from "next-auth/next"
import { authOptions } from "../api/auth/[...nextauth]"
import Layout from "../../components/layout"
import type { GetServerSidePropsContext } from "next"
import type { Session } from "next-auth"
export default function ServerSidePage({ session }: { session: Session }) {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
return (
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the <strong>getServerSession()</strong> method
in <strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>getServerSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is the recommended approach if you
need to support Server Side Rendering with authentication.
</p>
<p>
The advantage of Server Side Rendering is this page does not require
client side JavaScript.
</p>
<p>
The disadvantage of Server Side Rendering is that this page is slower to
render.
</p>
<pre>SESSION: {JSON.stringify(session, null, 2)}</pre>
</Layout>
)
}
// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
session: await getServerSession(
context.req,
context.res,
authOptions
),
},
}
}

0
pages/favicon.ico Normal file
View File

73
pages/guidelines.tsx Normal file
View File

@ -0,0 +1,73 @@
import React, { useState } from 'react';
import Layout from "../components/layout";
const PDFViewerPage = () => {
const [language, setLanguage] = useState('bg'); // default language is Bulgarian
// Determine the PDF file based on the selected language
const pdfFiles = {
en: '/content/guidelines/S-148_E.pdf',
bg: '/content/guidelines/S-148_BL.pdf',
ru: '/content/guidelines/S-148_U.pdf',
};
const languages = [
{ code: 'en', label: 'English' },
{ code: 'bg', label: 'Български' },
{ code: 'ru', label: 'Русский' },
];
const pdfFile = pdfFiles[language];
const toggleLanguage = () => {
const languages = Object.keys(pdfFiles);
const currentLangIndex = languages.indexOf(language);
const nextLangIndex = (currentLangIndex + 1) % languages.length;
setLanguage(languages[nextLangIndex]);
};
return (
<Layout>
<h1 className="text-3xl font-bold">Напътствия</h1>
<div className="guidelines-section mb-5 p-4 bg-gray-100 rounded-lg">
<h2 className="text-2xl font-semibold mb-3">Важни напътствия за службата</h2>
<ol className="list-decimal list-inside">
<li><strong>Щандове:</strong> Предлагаме следното:
<ul className="list-disc list-inside ml-4">
<li>Да има известно разстояние между нас и щандовете. Целта е да оставим хората свободно да се доближат до количките и ако някой прояви интерес може да се приближим.</li>
<li>Когато сме двама или трима може да стоим заедно. Ако сме четирима би било хубаво да се разделим по двама на количка и количките да са на известно разстояние една от друга.</li>
</ul>
</li>
<li><strong>Безопасност:</strong> Нека се страем зад нас винаги да има защита или препятствие за недобронамерени хора.</li>
<li><strong>Плакати:</strong> Моля при придвижване на количките да слагате плакатите така, че илюстрацията да се вижда, когато калъфа е сложен. Целта е снимките да не се търкат в количката, защото се повреждат.</li>
<li><strong>Литература:</strong> При проявен интерес на чужд език, използвайте списанията и трактатите на други езици в папките.</li>
<li><strong>График:</strong> Моля да ни изпратите вашите предпочитания до 23-то число на месеца.<a href='https://docs.google.com/forms/d/e/1FAIpQLSdLpuTi0o9laOYmenGJetY6MJ-CmphD9xFS5ZVz_7ipOxNGUQ/viewform?usp=sf_link'> Линк към анкетата</a></li>
<li><strong>Случки:</strong> Ако сте имали хубави случки на количките, моля пишете ни.</li>
</ol>
</div>
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
<div className="my-4 flex items-center">
{languages.map((lang, index) => (
<React.Fragment key={lang.code}>
{index > 0 && <div className="bg-gray-400 w-px h-6"></div>} {/* Vertical line separator */}
<button
onClick={() => setLanguage(lang.code)}
className={`text-lg py-2 px-4 ${language === lang.code ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800 hover:bg-blue-500 hover:text-white'} ${index === 0 ? 'rounded-l-full' : index === languages.length - 1 ? 'rounded-r-full' : ''}`}
>
{lang.label}
</button>
</React.Fragment>
))}
</div>
<div style={{ width: 'calc(100% - 1rem)', height: '100%', margin: '0 0' }}> {/* Center the PDF with 2rem margin */}
<object data={pdfFile} type="application/pdf" style={{ width: '100%', height: '100%' }}>
<p>Your browser does not support PDFs. Please download the PDF to view it: <a href={pdfFile}>Свали PDF файла</a>.</p>
<p>Вашият браузър не поддържа PDFs файлове. Моля свалете файла за да го разгледате: <a href={pdfFile}>Свали PDF файла</a>.</p>
</object>
</div>
</div>
</Layout >
);
};
export default PDFViewerPage;

49
pages/index.tsx Normal file
View File

@ -0,0 +1,49 @@
import Layout from "../components/layout"
import GoogleDriveFolderPreview from "../components/board/GoogleDriveFolderPreview"
import AvCalendar from '../components/calendar/avcalendar';
import { useEffect } from 'react';
import { getSession } from "next-auth/react";
import { useSession, signIn } from "next-auth/react";
import { serverSideAuth } from '../components/protectedRoute'; // Adjust the path as needed
import { useRouter } from 'next/router';
export default function IndexPage() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
// Redirect to /dash if user is logged in
if (session) {
router.push('/dash');
}
}, [session, router]);
if (status === "loading") {
return <p>Loading...</p>; // Or any other loading state
}
if (session) {
return null; // Or a loading indicator until the redirect happens
}
return (
<Layout>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Специално Свидетелстване София</h1>
<p className="mb-6">
Моля влезте в профила си.
</p>
{/* If GoogleDriveFolderPreview is a custom component, ensure it accepts and applies className props */}
<GoogleDriveFolderPreview folderId="1DADj8OUWz2sMEmiW3sDETuihJvFS5x_p" className="w-full max-w-lg">
{/* Content of the Google Drive Folder Preview */}
</GoogleDriveFolderPreview>
</div>
</div>
</Layout>
)
}

15
pages/privacy.tsx Normal file
View File

@ -0,0 +1,15 @@
// pages/PrivacyPolicyPage.jsx
import React from 'react';
import PrivacyPolicyContainer from '../components/privacy-policy/PrivacyPolicyContainer';
import Layout from "../components/layout"
function PrivacyPolicyPage() {
return (<Layout>
<div>
<PrivacyPolicyContainer />
</div>
</Layout>
);
}
export default PrivacyPolicyPage;