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

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
},
};