initial commit - code moved to separate repo
This commit is contained in:
200
pages/api/auth/[...nextauth].ts
Normal file
200
pages/api/auth/[...nextauth].ts
Normal 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)
|
68
pages/api/data/[...nextcrud].ts
Normal file
68
pages/api/data/[...nextcrud].ts
Normal 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
15
pages/api/data/content.ts
Normal 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
15
pages/api/examples/jwt.ts
Normal 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))
|
||||
}
|
19
pages/api/examples/protected.ts
Normal file
19
pages/api/examples/protected.ts
Normal 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.",
|
||||
});
|
||||
}
|
10
pages/api/examples/session.ts
Normal file
10
pages/api/examples/session.ts
Normal 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
643
pages/api/index.ts
Normal 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
187
pages/api/schedule.ts
Normal 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
688
pages/api/shiftgenerate.ts
Normal 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
93
pages/api/upload.ts
Normal 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
|
||||
},
|
||||
};
|
Reference in New Issue
Block a user