initial commit - code moved to separate repo
This commit is contained in:
45
pages/_app.tsx
Normal file
45
pages/_app.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
import "../styles/styles.css"
|
||||
import "../styles/global.css"
|
||||
import "tailwindcss/tailwind.css"
|
||||
|
||||
import type { AppProps } from "next/app";
|
||||
import type { Session } from "next-auth";
|
||||
import { useEffect } from "react"
|
||||
// for fontawesome
|
||||
import Head from 'next/head';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
||||
// Use of the <SessionProvider> is mandatory to allow components that call
|
||||
// `useSession()` anywhere in your application to access the `session` object.
|
||||
|
||||
export default function App({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
}: AppProps<{ session: Session }>) {
|
||||
|
||||
useEffect(() => {
|
||||
const use = async () => {
|
||||
(await import('tw-elements')).default;
|
||||
};
|
||||
use();
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{/* Other tags */}
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<SessionProvider session={session}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<Component {...pageProps} />
|
||||
</LocalizationProvider>
|
||||
</SessionProvider>
|
||||
</>
|
||||
)
|
||||
}
|
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
|
||||
},
|
||||
};
|
2
pages/cart/availabilities/edit/[id].tsx
Normal file
2
pages/cart/availabilities/edit/[id].tsx
Normal file
@ -0,0 +1,2 @@
|
||||
import NewPage from "../new";
|
||||
export default NewPage;
|
158
pages/cart/availabilities/index.tsx
Normal file
158
pages/cart/availabilities/index.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
import { Availability, UserRole } from "@prisma/client";
|
||||
import { format } from "date-fns";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from 'react';
|
||||
import Layout from "../../../components/layout";
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import AvCalendar from '../../../components/calendar/avcalendar';
|
||||
|
||||
|
||||
interface IProps {
|
||||
initialItems: Availability[];
|
||||
id: string;
|
||||
}
|
||||
|
||||
// export default function AvPage({} : IProps) {
|
||||
export default function AvPage({ initialItems, id }: IProps) {
|
||||
const router = useRouter();
|
||||
// items.forEach(item => {
|
||||
// item.publisher = prisma.publisher.findUnique({where: {id: item.publisherId}});
|
||||
// });
|
||||
|
||||
const [items, set] = useState(initialItems);
|
||||
|
||||
const events = initialItems?.map(item => ({
|
||||
id: item.id,
|
||||
title: item.name,
|
||||
date: new Date(item.startTime),
|
||||
start: new Date(item.startTime),
|
||||
end: new Date(item.endTime),
|
||||
isactive: item.isactive,
|
||||
publisherId: item.publisher.id,
|
||||
dayOfMonth: item.dayOfMonth,
|
||||
dayOfWeek: item.dayOfWeek,
|
||||
}));
|
||||
|
||||
const render = () => {
|
||||
console.log("showing " + initialItems?.length + " availabilities");
|
||||
if (initialItems?.length === 0) return <h1>No Items</h1>;
|
||||
return ( //AvailabilityList(items));
|
||||
<>
|
||||
<table className="min-w-full">
|
||||
<thead className="border-b">
|
||||
<tr>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
#
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
Publisher
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
Weekday
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
From
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
To
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{initialItems?.map((item: Availability) => (
|
||||
<tr key={item.id} className={item.isactive ? "" : "text-gray-300"}>
|
||||
<td className="px-6 py-4 whitespace-nowrap ">
|
||||
{item.id} {item.isactive}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{item.publisher.lastName}, {item.publisher.firstName}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{item.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{item.dayofweek}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{format(new Date(item.startTime), "HH:mm")}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{format(new Date(item.endTime), "HH:mm")}
|
||||
</td>
|
||||
<td>
|
||||
<button className="btn text-gray-700"
|
||||
onClick={() => router.push(`/cart/availabilities/edit/${item.id}`)} >
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
return <Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]}>
|
||||
|
||||
<AvCalendar publisherId={id} events={events} />
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div>
|
||||
{render()}
|
||||
</div>
|
||||
{/* <div className="flex justify-center">
|
||||
<a href="/cart/availabilities/new" className="btn">
|
||||
New availability
|
||||
</a>
|
||||
</div> */}
|
||||
</div></ProtectedRoute>
|
||||
</Layout>
|
||||
}
|
||||
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { serverSideAuth } from '../../../components/protectedRoute'; // Adjust the path as needed
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
const auth = await serverSideAuth({
|
||||
req: context.req,
|
||||
allowedRoles: [/* ...allowed roles... */]
|
||||
});
|
||||
// const prisma = new PrismaClient()
|
||||
//get current user role from session
|
||||
const session = await getSession(context);
|
||||
if (!session) { return { props: {} } }
|
||||
|
||||
const role = session?.user.role;
|
||||
console.log("server role: " + role);
|
||||
var queryUrl = process.env.NEXTAUTH_URL + "/api/data/availabilities?select=id,name,isactive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id";
|
||||
if (role === UserRole.USER || context.query.my) {
|
||||
queryUrl += `&where={"publisherId":"${session?.user.id}"}`;
|
||||
} else if (role == UserRole.ADMIN) {
|
||||
if (context.query.id) {
|
||||
queryUrl += `&where={"publisherId":"${context.query.id}"}`;
|
||||
} else {
|
||||
queryUrl += `&where={"isactive":true}`;
|
||||
}
|
||||
}
|
||||
var resp = await axios.get(
|
||||
queryUrl
|
||||
// process.env.NEXTAUTH_URL + "/api/data/availabilities?include=publisher",
|
||||
, { decompress: true });
|
||||
var items = resp.data;
|
||||
console.log("got " + items.length + " availabilities");
|
||||
return {
|
||||
props: {
|
||||
initialItems: items,
|
||||
id: context.query.id || session?.user.id || null,
|
||||
},
|
||||
};
|
||||
};
|
42
pages/cart/availabilities/new.tsx
Normal file
42
pages/cart/availabilities/new.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
|
||||
|
||||
|
||||
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
import { Availability } from "@prisma/client";
|
||||
import AvailabilityForm from "../../../components/availability/AvailabilityForm";
|
||||
import Layout from "../../../components/layout";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
|
||||
|
||||
export default function NewPage(item: Availability) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="h-5/6 grid place-items-center">
|
||||
<AvailabilityForm id={item.id} publisherId={item.publisherId} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
//------------------pages\cart\availabilities\edit\[id].tsx------------------
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
|
||||
const axios = await axiosServer(context);
|
||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
if (!context.query || !context.query.id) {
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
}
|
||||
const { data: item } = await axios.get(
|
||||
process.env.NEXTAUTH_URL + "/api/data/availabilities/" + context.params.id
|
||||
);
|
||||
|
||||
return {
|
||||
props: {
|
||||
item: item,
|
||||
},
|
||||
};
|
||||
};
|
908
pages/cart/calendar/index.tsx
Normal file
908
pages/cart/calendar/index.tsx
Normal file
@ -0,0 +1,908 @@
|
||||
import React, { useState, useEffect, use } from 'react';
|
||||
import { useSession } from "next-auth/react"
|
||||
import Link from 'next/link';
|
||||
import Calendar from 'react-calendar';
|
||||
import 'react-calendar/dist/Calendar.css';
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import Layout from "../../../components/layout"
|
||||
import Shift from '../../../components/calendar/ShiftComponent';
|
||||
import { DayOfWeek, UserRole } from '@prisma/client';
|
||||
import { env } from 'process'
|
||||
import ShiftComponent from '../../../components/calendar/ShiftComponent';
|
||||
//import { set } from 'date-fns';
|
||||
const common = require('src/helpers/common');
|
||||
import { toast } from 'react-toastify';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import ConfirmationModal from '../../../components/ConfirmationModal';
|
||||
|
||||
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
|
||||
|
||||
|
||||
|
||||
// import { useSession,} from 'next-auth/react';
|
||||
// import { getToken } from "next-auth/jwt"
|
||||
|
||||
//define Shift type
|
||||
interface Shift {
|
||||
id: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
cartEventId: number;
|
||||
assignments: Assignment[];
|
||||
}
|
||||
interface Assignment {
|
||||
id: number;
|
||||
publisherId: number;
|
||||
shiftId: number;
|
||||
isConfirmed: boolean;
|
||||
publisher: Publisher;
|
||||
}
|
||||
interface Publisher {
|
||||
id: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isImported: boolean;
|
||||
}
|
||||
|
||||
// https://www.npmjs.com/package/react-calendar
|
||||
export default function CalendarPage({ initialEvents, initialShifts }) {
|
||||
|
||||
const { data: session } = useSession()
|
||||
//if logged in, get the user's email
|
||||
// var email = "";
|
||||
// const [events, setEvents] = useState(initialEvents);
|
||||
|
||||
const events = initialEvents;
|
||||
|
||||
|
||||
const [allShifts, setAllShifts] = useState(initialShifts);
|
||||
const [value, onChange] = useState<Date>(new Date());
|
||||
const [shifts, setShifts] = React.useState([]);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [availablePubs, setAvailablePubs] = React.useState([]);
|
||||
|
||||
const [selectedShiftId, setSelectedShiftId] = useState(null);
|
||||
|
||||
const [isOperationInProgress, setIsOperationInProgress] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const [activeButton, setActiveButton] = useState(null);
|
||||
const isLoading = (buttonId) => activeButton === buttonId;
|
||||
|
||||
// ------------------ MODAL ------------------
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [modalPub, setModalPub] = useState(null);
|
||||
|
||||
// ------------------ no assignments checkbox ------------------
|
||||
const [isCheckboxChecked, setIsCheckboxChecked] = useState(false);
|
||||
const handleCheckboxChange = (event) => {
|
||||
setIsCheckboxChecked(!isCheckboxChecked); // Toggle the checkbox state
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("checkbox checked: " + isCheckboxChecked);
|
||||
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
|
||||
}, [isCheckboxChecked]); // Dependency array
|
||||
|
||||
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
|
||||
useEffect(() => {
|
||||
const newMonth = value.getMonth();
|
||||
if (newMonth !== selectedMonth) {
|
||||
setSelectedMonth(newMonth);
|
||||
}
|
||||
}, [value, selectedMonth]);
|
||||
|
||||
|
||||
const handleCalDateChange = async (selectedDate) => {
|
||||
var date = new Date(common.getDateFromDateTime(selectedDate));//ToDo: check if seting the timezone affects the selectedDate?!
|
||||
var dateStr = common.getISODateOnly(date);
|
||||
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
|
||||
if (isCheckboxChecked) {
|
||||
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
|
||||
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
|
||||
setAvailablePubs(availablePubsForDate);
|
||||
}
|
||||
else {
|
||||
console.log(`getting shifts for ${common.getISODateOnly(date)}`)
|
||||
try {
|
||||
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
|
||||
setShifts(shiftsForDate);
|
||||
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
|
||||
//remove availabilities that are isFromPreviousAssignment or from previous month for each publisher
|
||||
// availablePubsForDate = availablePubsForDate.map(pub => {
|
||||
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
|
||||
// return pub;
|
||||
// });
|
||||
|
||||
|
||||
|
||||
//commented for now: remove unavailable publishers
|
||||
// availablePubsForDate = availablePubsForDate.map(pub => {
|
||||
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
|
||||
// return pub;
|
||||
// });
|
||||
|
||||
setAvailablePubs(availablePubsForDate);
|
||||
console.log(`found shifts for ${dateStr}: ${shiftsForDate.length}`);
|
||||
} catch (err) {
|
||||
console.error("Error fetching shifts:", err);
|
||||
setError(err);
|
||||
}
|
||||
onChange(selectedDate);
|
||||
}
|
||||
}
|
||||
const handleShiftSelection = (selectedShift) => {
|
||||
setSelectedShiftId(selectedShift.id);
|
||||
const updatedPubs = availablePubs.map(pub => {
|
||||
const isAvailableForShift = pub.availabilities.some(avail =>
|
||||
avail.startTime <= selectedShift.startTime
|
||||
&& avail.endTime >= selectedShift.endTime
|
||||
&& avail.isFromPreviousAssignment == false
|
||||
);
|
||||
const isAvailableForShiftWithPrevious = pub.availabilities.some(avail =>
|
||||
avail.startTime <= selectedShift.startTime
|
||||
&& avail.endTime >= selectedShift.endTime
|
||||
);
|
||||
console.log(`Publisher ${pub.firstName} ${pub.lastName} is available for shift ${selectedShift.id}: ${isAvailableForShift}`);
|
||||
// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + pub.availabilities.map(avail => avail.startTime + " - " + avail.endTime));
|
||||
// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + stringify.join(', 'pub.availabilities.map(avail => avail.id)));
|
||||
|
||||
const availabilitiesIds = pub.availabilities.map(avail => avail.id).join(', ');
|
||||
console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities with IDs: ${availabilitiesIds}`);
|
||||
return { ...pub, isAvailableForShift, isAvailableForShiftWithPrevious, isSelected: pub.id === selectedShift.selectedPublisher?.id };
|
||||
});
|
||||
|
||||
// Sort publishers based on their availability state. use currentDayAssignments, currentWeekAssignments,
|
||||
// currentMonthAssignments and previousMonthAssignments properties
|
||||
// Sort publishers based on availability and then by assignment counts.
|
||||
const sortedPubs = updatedPubs.sort((a, b) => {
|
||||
if (a.isactive !== b.isactive) {
|
||||
return a.isactive ? -1 : 1;
|
||||
}
|
||||
// First, sort by isselected.
|
||||
if (a.isSelected !== b.isSelected) {
|
||||
return a.isSelected ? -1 : 1;
|
||||
}
|
||||
|
||||
// Them, sort by availability.
|
||||
if (a.isAvailableForShift !== b.isAvailableForShift) {
|
||||
return a.isAvailableForShift ? -1 : 1;
|
||||
}
|
||||
|
||||
// If both are available (or unavailable) for the shift, continue with the additional sorting logic.
|
||||
|
||||
// Prioritize those without currentDayAssignments.
|
||||
if (!!a.currentDayAssignments !== !!b.currentDayAssignments) {
|
||||
return a.currentDayAssignments ? 1 : -1;
|
||||
}
|
||||
|
||||
// Then prioritize those without currentWeekAssignments.
|
||||
if (!!a.currentWeekAssignments !== !!b.currentWeekAssignments) {
|
||||
return a.currentWeekAssignments ? 1 : -1;
|
||||
}
|
||||
// Prioritize those with fewer currentMonthAvailabilityHoursCount.
|
||||
if (a.currentMonthAvailabilityHoursCount !== b.currentMonthAvailabilityHoursCount) {
|
||||
return a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount;
|
||||
}
|
||||
// Finally, sort by (currentMonthAssignments - previousMonthAssignments).
|
||||
return (a.currentMonthAssignments - a.previousMonthAssignments) - (b.currentMonthAssignments - b.previousMonthAssignments);
|
||||
});
|
||||
setAvailablePubs(sortedPubs); // Assuming availablePubs is a state managed by useState
|
||||
};
|
||||
|
||||
const handleSelectedPublisher = (publisher) => {
|
||||
// Do something with the selected publisher
|
||||
console.log("handle pub clicked:", publisher);
|
||||
}
|
||||
const handlePublisherModalOpen = async (publisher) => {
|
||||
// Do something with the selected publisher
|
||||
console.log("handle pub modal opened:", publisher.firstName + " " + publisher.lastName);
|
||||
let date = new Date(value);
|
||||
const { data: publisherInfo } = await axiosInstance.get(`/api/?action=getPublisherInfo&id=${publisher.id}&date=${common.getISODateOnly(date)}`);
|
||||
publisher.assignments = publisherInfo.assignments;
|
||||
publisher.availabilities = publisherInfo.availabilities;
|
||||
publisher.email = publisherInfo.email;
|
||||
setModalPub(publisher);
|
||||
setIsModalOpen(true);
|
||||
|
||||
}
|
||||
|
||||
// file uploads
|
||||
const [fileActionUrl, setFileActionUrl] = useState('');
|
||||
const [file, setFile] = useState(null);
|
||||
const handleFileUpload = async (event) => {
|
||||
setIsOperationInProgress(true);
|
||||
console.log('handleFileUpload(): Selected file:', event.target.files[0], 'actionUrl:', fileActionUrl);
|
||||
setFile(event.target.files[0]);
|
||||
|
||||
if (!event.target.files[0]) {
|
||||
toast.error('Моля, изберете файл!');
|
||||
return;
|
||||
}
|
||||
|
||||
uploadToServer(fileActionUrl, event.target.files[0]);
|
||||
};
|
||||
|
||||
|
||||
const uploadToServer = async (actionUrl, file) => {
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
|
||||
const response = await fetch('/' + actionUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.fileId) {
|
||||
pollProgress(result.fileId);
|
||||
}
|
||||
console.log('Result from server-side API:', result);
|
||||
toast.info(result.message || "Файла е качен! Започна обработката на данните...");
|
||||
|
||||
} catch (error) {
|
||||
toast.error(error.message || "Възникна грешка при обработката на данните.");
|
||||
} finally {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
const pollProgress = (fileId: any) => {
|
||||
fetch(`/api/upload?fileId=${fileId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateProgressBar(data.progress); // Update the progress bar
|
||||
|
||||
if (data.progress < 98 && data.progress > 0) {
|
||||
// Poll every second if progress is between 0 and 100
|
||||
setTimeout(() => pollProgress(fileId), 1000);
|
||||
} else if (data.progress === 0) {
|
||||
// Handle error case
|
||||
toast.error("Възникна грешка при обработката на данните.");
|
||||
setIsOperationInProgress(false);
|
||||
} else {
|
||||
// Handle completion case
|
||||
toast.success("Файла беше обработен успешно!");
|
||||
setIsOperationInProgress(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error polling for progress:', error);
|
||||
toast.error("Грешка при обновяването на напредъка");
|
||||
setIsOperationInProgress(false)
|
||||
})
|
||||
.finally();
|
||||
|
||||
};
|
||||
|
||||
const updateProgressBar = (progress: string) => {
|
||||
// Implement the logic to update your progress bar based on the 'progress' value
|
||||
// For example, updating the width of a progress bar element
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = progress + '%';
|
||||
}
|
||||
};
|
||||
|
||||
function getEventClassname(event, allShifts, date) {
|
||||
if (event && allShifts) {
|
||||
const matchingShifts = allShifts.filter(shift => {
|
||||
const shiftDate = new Date(shift.startTime);
|
||||
return shift.cartEventId === event.id && shiftDate.getDate() === date.getDate() && shiftDate.getMonth() === date.getMonth();
|
||||
});
|
||||
|
||||
//get matching shifts with assignments using nextcrud
|
||||
//const { data: withAss } = await axiosInstance.get(`/shifts?include=assignments&where={"id":{"$in":[${matchingShifts.map(shift => shift.id)}]}}`);
|
||||
const minCount = Math.min(...matchingShifts.map(shift => shift.assignedCount)) || 0;
|
||||
//const minCount = 4;
|
||||
//console.log("matchingShifts: " + matchingShifts) + " for date " + date;
|
||||
if (matchingShifts.length < 3) { return "text-gray"; }
|
||||
else {
|
||||
if (minCount === 0) return "text-red-700 font-bold ";
|
||||
if (minCount === 1) return "text-brown-900 font-bold ";
|
||||
if (minCount === 2) return "text-orange-500";
|
||||
if (minCount === 3) return "text-yellow-500";
|
||||
if (minCount >= 4) return "text-blue-500";
|
||||
}
|
||||
}
|
||||
return "text-default"; // A default color in case none of the conditions are met.
|
||||
}
|
||||
|
||||
|
||||
const onTileContent = ({ date, view }) => {
|
||||
// Add your logic here
|
||||
var dayName = common.DaysOfWeekArray[date.getDayEuropean()];
|
||||
var classname = "";
|
||||
|
||||
if (events == null) {
|
||||
return <div>{" "}</div>;
|
||||
}
|
||||
|
||||
const event = events.find((event) => {
|
||||
return event.dayofweek == dayName;
|
||||
});
|
||||
if (event != null) {
|
||||
const classname = getEventClassname(event, allShifts, date);
|
||||
return <div className={classname}>
|
||||
{new Date(event.startTime).getHours() + "-" + new Date(event.endTime).getHours()}ч.
|
||||
</div>
|
||||
|
||||
}
|
||||
return <div>{" "}</div>;
|
||||
};
|
||||
|
||||
const addAssignment = async (publisher, shiftId) => {
|
||||
try {
|
||||
console.log(`new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`);
|
||||
const newAssignment = {
|
||||
publisher: { connect: { id: publisher.id } },
|
||||
shift: { connect: { id: shiftId } },
|
||||
isactive: true,
|
||||
isConfirmed: true
|
||||
};
|
||||
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
|
||||
// Update the 'publisher' property of the returned data with the full publisher object
|
||||
data.publisher = publisher;
|
||||
} catch (error) {
|
||||
console.error("Error adding assignment:", error);
|
||||
}
|
||||
};
|
||||
const removeAssignment = async (publisher, shiftId) => {
|
||||
try {
|
||||
const assignment = publisher.assignments.find(ass => ass.shift.id === shiftId);
|
||||
console.log(`remove assignment for shift ${shiftId}`);
|
||||
const { data } = await axiosInstance.delete(`/api/data/assignments/${assignment.id}`);
|
||||
} catch (error) {
|
||||
console.error("Error removing assignment:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// button handlers
|
||||
// ----------------------------------------------------------
|
||||
const importShifts = async () => {
|
||||
try {
|
||||
setActiveButton("importShifts");
|
||||
setIsOperationInProgress(true);
|
||||
let date = new Date(value);
|
||||
date.setDate(date.getDate() + 1);
|
||||
const dateString = common.getISODateOnly(date);
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
// setFileActionUrl(`readword/${dateString.slice(0, 4)}/${dateString.slice(5, 7)}/${dateString.slice(8, 10)}?action=import`);
|
||||
setFileActionUrl(`api/upload?action=readword&date=${dateString}`);
|
||||
console.log('fileaction set to ' + fileActionUrl);
|
||||
|
||||
fileInput.click();
|
||||
//handleFileUpload({ target: { files: [file] } });
|
||||
fileInput.value = null;
|
||||
} catch (error) {
|
||||
toast.error(error);
|
||||
} finally {
|
||||
setIsOperationInProgress(false);
|
||||
setActiveButton(null);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchShifts = async () => {
|
||||
try {
|
||||
setActiveButton("fetchShifts");
|
||||
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
|
||||
const { data } = await axiosInstance.get(`/api/data/shifts?include=assignments.publisher&where={"startTime":{"$and":[{"$gte":"2022-12-04T15:09:47.768Z","$lt":"2022-12-10T15:09:47.768Z"}]}}`);
|
||||
setShifts(data);
|
||||
toast.success('Готово!', { autoClose: 1000 });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setActiveButton(null);
|
||||
}
|
||||
}
|
||||
const generateShifts = async (buttonId, copyFromPrevious = false, autoFill = false, forDay?: Boolean | null) => {
|
||||
try {
|
||||
setActiveButton(buttonId);
|
||||
const endpoint = `/api/shiftgenerate?action=generate&date=${common.getISODateOnly(value)}©FromPreviousMonth=${copyFromPrevious}&autoFill=${autoFill}&forDay=${forDay}`;
|
||||
const { shifts } = await axiosInstance.get(endpoint);
|
||||
toast.success('Готово!', { autoClose: 1000 });
|
||||
setIsMenuOpen(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setActiveButton(null);
|
||||
}
|
||||
}
|
||||
const deleteShifts = async (buttonId, forDay: Boolean) => {
|
||||
try {
|
||||
setActiveButton(buttonId);
|
||||
await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}&forDay=${forDay}`);
|
||||
toast.success('Готово!', { autoClose: 1000 });
|
||||
setIsMenuOpen(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setActiveButton(null);
|
||||
}
|
||||
}
|
||||
const sendMails = async () => {
|
||||
try {
|
||||
var month = new Date(value).getMonth() + 1;
|
||||
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
|
||||
const { data } = await axiosInstance.get(`/sendmails/${new Date(value).getFullYear()}/${month}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
const generateXLS = async () => {
|
||||
try {
|
||||
var month = new Date(value).getMonth() + 1;
|
||||
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
|
||||
const { data } = await axiosInstance.get(`/generatexcel/${new Date(value).getFullYear()}/${month}/2`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
const generateDOCX = async () => {
|
||||
try {
|
||||
setActiveButton("generateDOCX");
|
||||
var month = new Date(value).getMonth() + 1;
|
||||
const response = await axiosInstance.get(`/getDocxFile/${new Date(value).getFullYear()}/${month}`, { responseType: 'blob' });
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `График 2023.${month}.docx`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
//get all publishers and create txt file with their names, current and previous month assignments count (getPublisherInfo)
|
||||
//
|
||||
const generateMonthlyStatistics = async () => {
|
||||
try {
|
||||
var month = new Date(value).getMonth() + 1;
|
||||
let { data: allPublishersInfo } = await axiosInstance.get(`/api/?action=getMonthlyStatistics&date=${common.getISODateOnly(value)}`);
|
||||
//order by name and generate the list
|
||||
allPublishersInfo = allPublishersInfo.sort((a, b) => {
|
||||
if (a.firstName !== b.firstName) {
|
||||
return a.firstName < b.firstName ? -1 : 1;
|
||||
} if (a.lastName !== b.lastName) {
|
||||
return a.lastName < b.lastName ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
var list = "";
|
||||
allPublishersInfo.forEach(pub => {
|
||||
// list += `${pub.firstName} ${pub.lastName}\t ${pub.currentMonthAssignments} / ${pub.previousMonthAssignments}\n`;
|
||||
list += `${pub.firstName} ${pub.lastName}\t ${pub.currentMonthAssignments}\n`;
|
||||
});
|
||||
|
||||
//write to google sheets file
|
||||
|
||||
|
||||
//download the file
|
||||
const url = window.URL.createObjectURL(new Blob([list]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `Статистика 2023.${month}.txt`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
|
||||
{/* Page Overlay */}
|
||||
{isOperationInProgress && (
|
||||
<div className="loading-overlay">
|
||||
<div className="spinner"></div>
|
||||
</div>
|
||||
)}
|
||||
<input id="fileInput" title="file input" type="file" onChange={handleFileUpload}
|
||||
accept=".json, .doc, .docx, .xls, .xlsx" style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
<div className="mb-4">
|
||||
|
||||
<button className="button m-2 bg-blue-800" onClick={importShifts}>
|
||||
{isLoading('importShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fa fa-file-import"></i>)} Импорт от Word
|
||||
</button>
|
||||
<button className="button btn m-2 bg-blue-800" onClick={generateDOCX}>
|
||||
{isLoading('generateDOCX') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fa fa-file-export"></i>)}Експорт в Word
|
||||
</button>
|
||||
<button className="button btn m-2 bg-yellow-500 hover:bg-yellow-600 text-white" onClick={() => { setActiveButton("sendEmails"); setConfirmModalOpen(true) }}>
|
||||
{isLoading('sendEmails') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-envelope mr-2"></i>)} изпрати мейли!
|
||||
</button>
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmModalOpen}
|
||||
onClose={() => setConfirmModalOpen(false)}
|
||||
onConfirm={() => {
|
||||
toast.info("Вие потвърдихте!", { autoClose: 2000 });
|
||||
setConfirmModalOpen(false);
|
||||
sendMails()
|
||||
}}
|
||||
message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
|
||||
/>
|
||||
<div className="relative inline-block text-left">
|
||||
<button
|
||||
className={`button m-2 ${isMenuOpen ? 'bg-gray-400 border border-blue-500' : 'bg-gray-300'} hover:bg-gray-400`}
|
||||
onClick={() => { setIsMenuOpen(!isMenuOpen) }}>
|
||||
<i className="fa fa-ellipsis-h"></i> Още
|
||||
</button>
|
||||
{isMenuOpen && (
|
||||
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
|
||||
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
{/* Group 1: Daily actions */}
|
||||
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmptyDay", false, false, true)}>
|
||||
{isLoading('genEmptyDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
|
||||
създай празни ({value.getDate()}-ти) </button>
|
||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => generateShifts("genDay", false, true, true)}>
|
||||
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
|
||||
Генерирай смени ({value.getDate()}-ти) </button>
|
||||
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" onClick={() => { deleteShifts("deleteShiftsDay", true) }}>
|
||||
{isLoading('deleteShiftsDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-trash-alt mr-2"></i>)}
|
||||
изтрий смените ({value.getDate()}-ти)</button>
|
||||
|
||||
<hr className="my-1" />
|
||||
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmpty", false, false)}>
|
||||
{isLoading('genEmpty') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
|
||||
създай празни </button>
|
||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap" onClick={() => generateShifts("genCopy", true)}>
|
||||
{isLoading('genCopy') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-copy mr-2"></i>)}
|
||||
копирай от миналия месец</button>
|
||||
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true)}>
|
||||
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
|
||||
Генерирай смени </button>
|
||||
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" onClick={() => { deleteShifts("deleteShifts", false) }}>
|
||||
{isLoading('deleteShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-trash-alt mr-2"></i>)}
|
||||
изтрий смените</button>
|
||||
|
||||
|
||||
<hr className="my-1" />
|
||||
|
||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateXLS}><i className="fas fa-file-excel mr-2"></i> Генерирай XLSX</button>
|
||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={fetchShifts}>
|
||||
{isLoading('fetchShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-sync-alt mr-2"></i>)} презареди</button>
|
||||
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <button className={`button m-2 bg-blue-800 ${isOperationInProgress ? 'disabled' : ''}`} onClick={importShifts}>
|
||||
{isOperationInProgress ? <div className="spinner"></div> : 'Import shifts (and missing Publishers) from WORD'}
|
||||
</button>
|
||||
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts()}>Generate empty shifts</button>
|
||||
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true)}>Copy last month shifts</button>
|
||||
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true, true)}>Generate Auto shifts</button>
|
||||
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(false, true, value)}>Generate Auto shifts DAY</button>
|
||||
// <button className="button m-2" onClick={fetchShifts}>Fetch shifts</button>
|
||||
// <button className="button m-2" onClick={sendMails}>Send mails</button>
|
||||
// <button className="button m-2" onClick={generateXLS}>Generate XLSX</button>
|
||||
// <button className="button m-2" onClick={async () => {
|
||||
// await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}`);
|
||||
// }
|
||||
// }>Delete shifts (selected date's month)</button>
|
||||
// <button className="button m-2" onClick={generateMonthlyStatistics}>Generate statistics</button>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
{/* progress bar holder */}
|
||||
{isOperationInProgress && (
|
||||
<div id="progress" className="w-full h-2 bg-gray-300">
|
||||
<div id="progress-bar" className="h-full bg-green-500" style={{ width: `${progress}%` }}></div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
{/* Calendar section */}
|
||||
<div className="flex-3">
|
||||
<Calendar
|
||||
className={['customCalendar']}
|
||||
onChange={handleCalDateChange}
|
||||
value={value}
|
||||
tileContent={onTileContent}
|
||||
locale="bg-BG"
|
||||
/>
|
||||
{/* ------------------------------- PUBLISHERS LIST ----------------------------------
|
||||
|
||||
list of publishers for the selected date with availabilities
|
||||
|
||||
------------------AVAILABLE PUBLISHERS LIST FOR THE SELECTED DATE0 ------------------ */}
|
||||
<div className="flex flex-col items-center my-8 sticky top-0">
|
||||
|
||||
<h2 className="text-lg font-semibold mb-4">Достъпни за този ден: <span className="text-blue-600">{availablePubs.length}</span></h2>
|
||||
<label className="toggle pb-3">
|
||||
<input type="checkbox" className="toggle-checkbox" onChange={handleCheckboxChange} />
|
||||
<span className="toggle-slider m-1">без назначения за месеца</span>
|
||||
|
||||
</label>
|
||||
<ul className="w-full max-w-md">
|
||||
{Array.isArray(availablePubs) && availablePubs?.map((pub, index) => {
|
||||
// Determine background and border classes based on conditions
|
||||
let bgAndBorderColorClass;
|
||||
if (pub.isAvailableForShift) {
|
||||
if (pub.currentDayAssignments === 0) {
|
||||
const comparisonResultClass = pub.currentMonthAvailabilityDaysCount < pub.previousMonthAssignments ? 'bg-green-100' : 'bg-green-50';
|
||||
bgAndBorderColorClass = `${comparisonResultClass} border-l-4 border-green-400`;
|
||||
} else if (!pub.isSelected) {
|
||||
bgAndBorderColorClass = 'bg-orange-50 border-l-4 border-orange-400';
|
||||
}
|
||||
} else {
|
||||
if (pub.isAvailableForShiftWithPrevious) // add left orange border
|
||||
{
|
||||
bgAndBorderColorClass = 'border-l-4 border-orange-400';
|
||||
}
|
||||
else {
|
||||
bgAndBorderColorClass = 'bg-white';
|
||||
}
|
||||
}
|
||||
//tOdO: CHECK WHY THIS IS NOT WORKING
|
||||
if (!pub.hasEverFilledForm) {
|
||||
//bgAndBorderColorClass = 'border-t-2 border-yellow-400';
|
||||
}
|
||||
|
||||
// Determine border class if selected
|
||||
const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : '';
|
||||
// Determine opacity class
|
||||
const activeOpacityClass = pub.isactive ? '' : 'opacity-25';
|
||||
|
||||
|
||||
return (
|
||||
<li key={index}
|
||||
className={`flex justify-between items-center p-4 rounded-lg shadow-sm mb-2
|
||||
${bgAndBorderColorClass} ${selectedBorderClass} ${activeOpacityClass}`}
|
||||
onDoubleClick={(handlePublisherModalOpen.bind(this, pub))}
|
||||
>
|
||||
<span className={`text-gray-700 ${pub.isAvailableForShift ? 'font-bold' : 'font-medium'} `}>
|
||||
{pub.firstName} {pub.lastName}
|
||||
</span>
|
||||
<div className="flex space-x-1 overflow-hidden">
|
||||
<span title="Достъпност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
|
||||
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
|
||||
</span>
|
||||
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
|
||||
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
|
||||
<span tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
|
||||
<button tooltip="желани участия този месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shift list section */}
|
||||
<div className="flex-grow mx-5">
|
||||
<div className="flex-col" id="shiftlist">
|
||||
{shifts.map((shift, index) => (
|
||||
<ShiftComponent key={index} shift={shift}
|
||||
onShiftSelect={handleShiftSelection} isSelected={shift.id == selectedShiftId}
|
||||
onPublisherSelect={handleSelectedPublisher} showAllAuto={true}
|
||||
allPublishersInfo={availablePubs} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* <CustomCalendar date={value} shifts={shifts} /> */}
|
||||
</div>
|
||||
{isModalOpen && <PublisherShiftsModal publisher={modalPub} shifts={allShifts} onClose={() => setIsModalOpen(false)} />}
|
||||
</ProtectedRoute >
|
||||
</Layout >
|
||||
</>
|
||||
);
|
||||
|
||||
function PublisherShiftsModal({ publisher, shifts, onClose }) {
|
||||
const monthInfo = common.getMonthDatesInfo(new Date(value));
|
||||
const monthShifts = shifts.filter(shift => {
|
||||
const shiftDate = new Date(shift.startTime);
|
||||
return shiftDate > monthInfo.firstDay && shiftDate < monthInfo.lastDay;
|
||||
});
|
||||
const weekShifts = monthShifts.filter(shift => {
|
||||
const shiftDate = new Date(shift.startTime);
|
||||
return common.getStartOfWeek(value) <= shiftDate && shiftDate <= common.getEndOfWeek(value);
|
||||
});
|
||||
const dayShifts = weekShifts.map(shift => {
|
||||
const isAvailable = publisher.availabilities.some(avail =>
|
||||
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
|
||||
);
|
||||
let color = isAvailable ? getColorForShift(shift) : 'bg-gray-300';
|
||||
if (shift.isFromPreviousMonth) {
|
||||
color += ' border-l-4 border-orange-500 ';
|
||||
}
|
||||
if (shift.isFromPreviousAssignment) {
|
||||
color += ' border-l-4 border-red-500 ';
|
||||
}
|
||||
return { ...shift, isAvailable, color };
|
||||
}).reduce((acc, shift) => {
|
||||
const dayIndex = new Date(shift.startTime).getDay();
|
||||
acc[dayIndex] = acc[dayIndex] || [];
|
||||
acc[dayIndex].push(shift);
|
||||
return acc;
|
||||
}, {});
|
||||
console.log("dayShifts:", dayShifts);
|
||||
|
||||
const hasAssignment = (shiftId) => {
|
||||
return publisher.assignments.some(ass => ass.shift.id === shiftId);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
console.log('ESC: closing modal.');
|
||||
onClose(); // Call the onClose function when ESC key is pressed
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listener
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// Remove event listener on cleanup
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [onClose]); // Include onClose in the dependency array
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||||
<div className="relative bg-white p-8 rounded-lg shadow-xl max-w-xl w-full h-auto overflow-y-auto">
|
||||
<h2 className="text-xl font-semibold mb-4">График на <span title={publisher.email} className='publisher'>
|
||||
<strong>{publisher.firstName} {publisher.lastName}</strong>
|
||||
<span className="publisher-tooltip" onClick={common.copyToClipboard}>{publisher.email}</span>
|
||||
</span> тази седмица:</h2>
|
||||
|
||||
{/* ... Display shifts in a calendar-like UI ... */}
|
||||
<div className="grid grid-cols-6 gap-4 mb-4">
|
||||
{Object.entries(dayShifts).map(([dayIndex, shiftsForDay]) => (
|
||||
<div key={dayIndex} className="flex flex-col space-y-2 justify-end">
|
||||
{/* Day header */}
|
||||
<div className="text-center font-medium">{new Date(shiftsForDay[0].startTime).getDate()}-ти</div>
|
||||
|
||||
{shiftsForDay.map((shift, index) => {
|
||||
const assignmentExists = hasAssignment(shift.id);
|
||||
const availability = publisher.availabilities.find(avail =>
|
||||
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
|
||||
);
|
||||
const isFromPrevMonth = availability && availability.isFromPreviousMonth;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`text-sm text-white p-2 rounded-md ${isFromPrevMonth ? 'border-l-4 border-yellow-500' : ''} ${assignmentExists ? 'bg-blue-200' : shift.color} h-24 flex flex-col justify-center`}
|
||||
>
|
||||
{common.getTimeRange(shift.startTime, shift.endTime)}
|
||||
|
||||
{!assignmentExists && shift.isAvailable && (
|
||||
<button onClick={() => { addAssignment(publisher, shift.id); onClose() }}
|
||||
className="mt-2 bg-green-500 text-white p-1 rounded hover:bg-green-600 active:bg-green-700 focus:outline-none"
|
||||
>
|
||||
добави
|
||||
</button>
|
||||
)}
|
||||
{assignmentExists && (
|
||||
<button onClick={() => { removeAssignment(publisher, shift.id) }} // Implement the removeAssignment function
|
||||
className="mt-2 bg-red-500 text-white p-1 rounded hover:bg-red-600 active:bg-red-700 focus:outline-none"
|
||||
>
|
||||
махни
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Close button in the top right corner */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-3 right-2 p-2 px-3 bg-red-500 text-white rounded-full hover:bg-red-600 active:bg-red-700 focus:outline-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
{/* <Link href={`/cart/publishers/edit/${modalPub.id}`}
|
||||
className="mt-2 bg-blue-500 text-white p-1 rounded hover:bg-blue-600 active:bg-blue-700 focus:outline-none">
|
||||
<i className="fas fa-edit" />
|
||||
</Link> */}
|
||||
{/* Edit button in the top right corner, next to the close button */}
|
||||
<Link href={`/cart/publishers/edit/${modalPub.id}`} className="absolute top-3 right-12 p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 active:bg-blue-700 focus:outline-none">
|
||||
<i className="fas fa-edit" />
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
function getColorForShift(shift) {
|
||||
const assignedCount = shift.assignedCount || 0; // Assuming each shift has an assignedCount property
|
||||
switch (assignedCount) {
|
||||
case 0: return 'bg-blue-300';
|
||||
case 1: return 'bg-green-300';
|
||||
case 2: return 'bg-yellow-300';
|
||||
case 3: return 'bg-orange-300';
|
||||
case 4: return 'bg-red-200';
|
||||
default: return 'bg-gray-300';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import { start } from 'repl';
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
const baseUrl = common.getBaseUrl();
|
||||
console.log('runtime BaseUrl: ' + baseUrl);
|
||||
console.log('runtime NEXTAUTH_URL: ' + process.env.NEXTAUTH_URL);
|
||||
console.log('Runtime Axios Base URL:', axios.defaults.baseURL);
|
||||
|
||||
const currentDate = new Date();
|
||||
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() - 3, 1);
|
||||
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); // 0th day of the next month gives the last day of the current month
|
||||
|
||||
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
|
||||
|
||||
const prismaClient = common.getPrismaClient();
|
||||
// let events = await prismaClient.cartEvent.findMany({ where: { isactive: true } });
|
||||
// events = events.map(event => ({
|
||||
// ...event,
|
||||
// // Convert Date objects to ISO strings
|
||||
// startTime: event.startTime.toISOString(),
|
||||
// endTime: event.endTime.toISOString(),
|
||||
// }));
|
||||
const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":true}`);
|
||||
//const { data: shifts } = await axios.get(url);
|
||||
|
||||
// get all shifts for the month, including assigments
|
||||
let shifts = await prismaClient.shift.findMany({
|
||||
where: {
|
||||
isactive: true,
|
||||
startTime: {
|
||||
gte: firstDayOfMonth,
|
||||
//lt: lastDayOfMonth
|
||||
}
|
||||
},
|
||||
include: {
|
||||
assignments: {
|
||||
include: {
|
||||
publisher: {
|
||||
select: {
|
||||
id: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//calculate assCount for each shift
|
||||
shifts = shifts.map(shift => ({
|
||||
...shift,
|
||||
assignedCount: shift.assignments.length,
|
||||
startTime: shift.startTime.toISOString(),
|
||||
endTime: shift.endTime.toISOString(),
|
||||
}));
|
||||
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialEvents: events,
|
||||
initialShifts: shifts,
|
||||
},
|
||||
};
|
||||
|
||||
}
|
47
pages/cart/calendar/schedule.tsx
Normal file
47
pages/cart/calendar/schedule.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { useState, useEffect, use } from 'react';
|
||||
import { useSession } from "next-auth/react"
|
||||
import Link from 'next/link';
|
||||
import Calendar from 'react-calendar';
|
||||
import 'react-calendar/dist/Calendar.css';
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import Layout from "../../../components/layout"
|
||||
import Shift from '../../../components/calendar/ShiftComponent';
|
||||
import { DayOfWeek, UserRole } from '@prisma/client';
|
||||
import { env } from 'process'
|
||||
import ShiftComponent from '../../../components/calendar/ShiftComponent';
|
||||
//import { set } from 'date-fns';
|
||||
const common = require('src/helpers/common');
|
||||
import { toast } from 'react-toastify';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import ConfirmationModal from '../../../components/ConfirmationModal';
|
||||
|
||||
const SchedulePage = () => {
|
||||
const { data: session } = useSession();
|
||||
const [htmlContent, setHtmlContent] = useState(""); // State to hold fetched HTML content
|
||||
|
||||
useEffect(() => {
|
||||
// Define an async function to fetch the HTML content
|
||||
const fetchHtmlContent = async () => {
|
||||
try {
|
||||
// Replace '/api/schedule' with your actual API endpoint
|
||||
const response = await axiosInstance.get('/api/schedule?year=2024&month=1', { responseType: 'text' });
|
||||
setHtmlContent(response.data); // Set the fetched HTML content in state
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch schedule:", error);
|
||||
// Handle error (e.g., display an error message)
|
||||
}
|
||||
};
|
||||
|
||||
fetchHtmlContent(); // Call the function to fetch HTML content
|
||||
}, []); // Empty dependency array means this effect runs once on component mount
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute deniedMessage="">
|
||||
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchedulePage;
|
25
pages/cart/cartevents/edit/[id].tsx
Normal file
25
pages/cart/cartevents/edit/[id].tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import NewPage from "../new";
|
||||
|
||||
import axiosServer from '../../../../src/axiosServer';
|
||||
export default NewPage;
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
console.log("edit page getServerSideProps");
|
||||
const axios = await axiosServer(context);
|
||||
const { id } = context.query;
|
||||
const { data } = await axios.get(`${process.env.NEXTAUTH_URL}/api/data/cartevents/` + id);
|
||||
const locations = await axios
|
||||
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`)
|
||||
.then((res) => {
|
||||
console.log("locations: " + JSON.stringify(res.data));
|
||||
return res.data;
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
item: data,
|
||||
locations: locations,
|
||||
inline: false,
|
||||
},
|
||||
};
|
||||
};
|
119
pages/cart/cartevents/index.tsx
Normal file
119
pages/cart/cartevents/index.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { CartEvent, UserRole } from '@prisma/client';
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import Layout from "../../../components/layout";
|
||||
|
||||
import common from 'src/helpers/common';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
|
||||
|
||||
import CartEventForm from '../../../components/cartevent/CartEventForm';
|
||||
// import IProps from '../../../components/cartevent/CartEventForm'
|
||||
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "../../../pages/api/auth/[...nextauth]"
|
||||
|
||||
// export default CartEventForm;
|
||||
export interface ICartEventPageProps {
|
||||
items: [CartEvent];
|
||||
locations: [Location];
|
||||
inline: false;
|
||||
}
|
||||
|
||||
export default function CartEventPage({ items, locations }: ICartEventPageProps) {
|
||||
|
||||
const router = useRouter();
|
||||
const [addnew, setAddNew] = useState(false);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||
<div className="flex flex-col">
|
||||
<h1>All cart events</h1>
|
||||
<table className="min-w-full">
|
||||
<thead className="border-b">
|
||||
<tr>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
#
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
Day of Week
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
Time
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
Shift Duration
|
||||
</th>
|
||||
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
|
||||
Active
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item: CartEvent, i) => (
|
||||
<tr key={i} className="border-b">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{item.id}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<strong>{common.dayOfWeekNames[common.getDayOfWeekIndex(item.dayofweek)]} </strong>
|
||||
на {locations.find(l => l.id == item.locationId).name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{new Date(item.startTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
||||
+ " до " + new Date(item.endTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{item.shiftDuration}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{item.isactive ? "Yes" : "No"}
|
||||
</td>
|
||||
<td>
|
||||
<button className="button bg-blue-500 hover:bg-blue-700"
|
||||
onClick={() => router.push(`/cart/cartevents/edit/${item.id}`)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="button bg-blue-500 hover:bg-blue-700"
|
||||
onClick={() => setAddNew(!addnew)}
|
||||
> {addnew ? "обратно" : "Добави нов"}</button>
|
||||
{addnew && <CartEventForm locations={locations} />}
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
|
||||
|
||||
const session = await getServerSession(context.req, context.res, authOptions)
|
||||
context.req.session = session;
|
||||
const axios = await axiosServer(context);
|
||||
|
||||
const { data: items } = await axios.get("/api/data/cartevents");
|
||||
console.log("gettnng locations from: " + "/api/data/locations?select=id,name");
|
||||
const locations = await axios
|
||||
.get(`/api/data/locations?select=id,name`)
|
||||
.then((res) => {
|
||||
console.log("locations: " + JSON.stringify(res.data));
|
||||
return res.data;
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
items,
|
||||
locations: locations,
|
||||
inline: false,
|
||||
},
|
||||
};
|
||||
};
|
55
pages/cart/cartevents/new.tsx
Normal file
55
pages/cart/cartevents/new.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
|
||||
|
||||
|
||||
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
import CartEventForm from "../../../components/cartevent/CartEventForm";
|
||||
import Layout from "../../../components/layout";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import { ICartEventPageProps } from "./index";
|
||||
|
||||
|
||||
export default function NewPage(props: ICartEventPageProps) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="h-5/6 grid place-items-center">
|
||||
<CartEventForm props={props} />
|
||||
{/*
|
||||
<AvailabilityForm id={item.id} publisherId={item.publisherId} /> */}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
//------------------pages\cart\availabilities\edit\[id].tsx------------------
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
|
||||
const axios = await axiosServer(context);
|
||||
const locations = await axios
|
||||
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`)
|
||||
.then((res) => {
|
||||
console.log("locations: " + JSON.stringify(res.data));
|
||||
return res.data;
|
||||
});
|
||||
|
||||
|
||||
if (!context.query || !context.query.id) {
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
}
|
||||
|
||||
const { id } = context.query.id;
|
||||
const { data: item } = await axiosInstance.get(
|
||||
process.env.NEXTAUTH_URL + "/api/data/cartevents/" + context.params.id
|
||||
);
|
||||
|
||||
return {
|
||||
props: {
|
||||
item: item,
|
||||
locations: locations
|
||||
},
|
||||
};
|
||||
};
|
122
pages/cart/locations/[id].tsx
Normal file
122
pages/cart/locations/[id].tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Layout from "../../../components/layout";
|
||||
import { Carousel } from 'react-responsive-carousel';
|
||||
import "react-responsive-carousel/lib/styles/carousel.min.css"; // requires a loader
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
|
||||
const ViewLocationPage: React.FC<ViewLocationPageProps> = ({ location }) => {
|
||||
const [activeTab, setActiveTab] = useState('mainLocation');
|
||||
const [activeImage, setActiveImage] = useState(0);
|
||||
|
||||
const [images, setImages] = useState([]);
|
||||
const [mainLocationImageCount, setMainLocationImageCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const mainLocationImages = [location.picture1, location.picture2, location.picture3].filter(Boolean);
|
||||
|
||||
const backupLocationImages = location.backupLocationImages?.filter(Boolean) ?? [];
|
||||
setImages([...mainLocationImages, ...backupLocationImages]);
|
||||
setMainLocationImageCount(mainLocationImages.length);
|
||||
}, [location.picture1, location.picture2, location.picture3, location.backupLocationImages]);
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
//show the proper image in the carousel
|
||||
if (tab === 'backupLocation') {
|
||||
setActiveImage(mainLocationImageCount);
|
||||
} else {
|
||||
setActiveImage(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCarouselChange = (index) => {
|
||||
// Switch to backupLocation tab if the current carousel image index is from the backup location
|
||||
if (index >= mainLocationImageCount) {
|
||||
setActiveTab('backupLocation');
|
||||
} else {
|
||||
setActiveTab('mainLocation');
|
||||
}
|
||||
setActiveImage(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="view-location-page max-w-4xl mx-auto my-8">
|
||||
{/* Tabs */}
|
||||
<div className="tabs flex border-b">
|
||||
{/* Main Location Tab */}
|
||||
<button
|
||||
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'mainLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
|
||||
onClick={() => handleTabChange('mainLocation')}
|
||||
>
|
||||
{location.name}
|
||||
</button>
|
||||
{/* Backup Location Tab */}
|
||||
<button
|
||||
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'backupLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
|
||||
onClick={() => handleTabChange('backupLocation')}
|
||||
>
|
||||
При лошо време: <strong>{location.backupLocationName}</strong>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Carousel */}
|
||||
{images.length > 0 && (
|
||||
<Carousel showArrows={true}
|
||||
autoPlay={false}
|
||||
infiniteLoop={true}
|
||||
showThumbs={false}
|
||||
onChange={handleCarouselChange}
|
||||
selectedItem={activeImage}
|
||||
>
|
||||
{images.map((src, index) => (
|
||||
<div key={index}>
|
||||
<img src={src} alt={`Slide ${index + 1}`} />
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
)}
|
||||
|
||||
{/* Tab Content */}
|
||||
{(location.content || location.backupLocationContent) && (
|
||||
<div className="tab-content mt-4">
|
||||
{activeTab === 'mainLocation' && (
|
||||
<div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.content }} />
|
||||
)}
|
||||
{activeTab === 'backupLocation' && location.backupLocationContent && (
|
||||
<div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.backupLocationContent }} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
|
||||
const { data: location } = await axios.get(
|
||||
`${process.env.NEXTAUTH_URL}/api/data/locations/${context.params.id}`
|
||||
);
|
||||
if (location.backupLocationId !== null) {
|
||||
const { data: backupLocation } = await axios.get(
|
||||
process.env.NEXTAUTH_URL + "/api/data/locations/" + location.backupLocationId
|
||||
);
|
||||
location.backupLocationName = backupLocation.name;
|
||||
location.backupLocationContent = backupLocation ? backupLocation.content : "";
|
||||
location.backupLocationImages = backupLocation ? [backupLocation.picture1, backupLocation.picture2, backupLocation.picture3].filter(Boolean) : [];
|
||||
}
|
||||
|
||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
|
||||
return {
|
||||
props: {
|
||||
location: location,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default ViewLocationPage;
|
41
pages/cart/locations/edit/[id].tsx
Normal file
41
pages/cart/locations/edit/[id].tsx
Normal file
@ -0,0 +1,41 @@
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import Layout from "../../../../components/layout";
|
||||
import LocationForm from "../../../../components/location/LocationForm";
|
||||
import axiosServer from '../../../../src/axiosServer';
|
||||
import ProtectedRoute from '../../../../components/protectedRoute';
|
||||
|
||||
function NewPage(item: Location) {
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||
<div className="h-5/6 grid place-items-center">
|
||||
<LocationForm key={item.id} item={item} />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPage;
|
||||
|
||||
//------------------pages\cart\locations\edit\[id].tsx------------------//
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
if (context.query.id === "new" || context.query.id === 0) {
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
}
|
||||
const { data: item } = await axios.get(
|
||||
process.env.NEXTAUTH_URL + "/api/data/locations/" + context.params.id
|
||||
);
|
||||
console.log(item) //this is the location object
|
||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
return {
|
||||
props: {
|
||||
item: item,
|
||||
},
|
||||
};
|
||||
};
|
51
pages/cart/locations/index.tsx
Normal file
51
pages/cart/locations/index.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import Layout from "../../../components/layout";
|
||||
import LocationCard from "../../../components/location/LocationCard";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
interface IProps {
|
||||
item: Location;
|
||||
}
|
||||
|
||||
function LocationsPage({ items = [] }: IProps) {
|
||||
const renderLocations = () => {
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) return <h1>No Locations</h1>;
|
||||
|
||||
return items.map((item) => (
|
||||
<LocationCard key={item.id} location={item} />
|
||||
));
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-4">
|
||||
{renderLocations()}
|
||||
</div>
|
||||
{/* add location link */}
|
||||
<div className="flex justify-center">
|
||||
<a href="/cart/locations/new" className="btn">
|
||||
Add Location
|
||||
</a>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default LocationsPage;
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
const { data: items } = await axios.get("/api/data/locations");
|
||||
//console.log('get server props - locations:' + items.length);
|
||||
//console.log(items);
|
||||
return {
|
||||
props: {
|
||||
items,
|
||||
},
|
||||
};
|
||||
};
|
44
pages/cart/locations/new.tsx
Normal file
44
pages/cart/locations/new.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
// import axios from "axios";
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import Layout from "../../../components/layout";
|
||||
import LocationForm from "../../../components/location/LocationForm";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
|
||||
function NewPage(loc: Location) {
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||
<div className="h-5/6 grid place-items-center">
|
||||
<LocationForm key={loc.id} location={loc} />
|
||||
</div></ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPage;
|
||||
|
||||
//------------------pages\cart\locations\edit\[id].tsx------------------
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
|
||||
//if query is undefined, then it is a new location
|
||||
if (context.query.id === undefined) {
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
}
|
||||
|
||||
const { data: loc } = await axios.get(
|
||||
`${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
|
||||
);
|
||||
console.log(location) //this is the location object
|
||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
return {
|
||||
props: {
|
||||
location: loc,
|
||||
},
|
||||
};
|
||||
};
|
87
pages/cart/publishers/contacts.tsx
Normal file
87
pages/cart/publishers/contacts.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { useState } from 'react';
|
||||
import Layout from "../../../components/layout";
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import { UserRole } from '@prisma/client';
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import common from '../../../src/helpers/common';
|
||||
|
||||
function ContactsPage({ publishers }) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredPublishers = publishers.filter((publisher) =>
|
||||
publisher.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
publisher.lastName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
publisher.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
publisher.phone?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-xl font-semibold mb-4">Контакти</h1>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Търси по име, имейл или телефон..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="border border-gray-300 rounded-md px-2 py-2 mb-4 w-full text-base md:text-sm"
|
||||
/>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border-b font-medium p-4 pl-8 pt-0 pb-3">Име</th>
|
||||
<th className="border-b font-medium p-4 pt-0 pb-3">Имейл</th>
|
||||
<th className="border-b font-medium p-4 pt-0 pb-3">Телефон</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredPublishers.map((publisher) => (
|
||||
<tr key={publisher.id}>
|
||||
<td className="border-b p-4 pl-8">{publisher.firstName} {publisher.lastName}</td>
|
||||
<td className="border-b p-4">
|
||||
<a href={`mailto:${publisher.email}`} className="text-blue-500">{publisher.email}</a>
|
||||
</td>
|
||||
<td className="border-b p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={common.isValidPhoneNumber(publisher.phone) ? '' : 'text-red-500'}>{publisher.phone}</span>
|
||||
<div className="flex items-center">
|
||||
<a href={`tel:${publisher.phone}`} className="inline-block p-2 mr-2">
|
||||
<i className="fas fa-phone-alt text-blue-500 text-xl" title="Обаждане"></i>
|
||||
</a>
|
||||
<a href={`https://wa.me/${publisher.phone}`} className="inline-block p-2 mr-2">
|
||||
<i className="fab fa-whatsapp text-green-500 text-xl" title="WhatsApp"></i>
|
||||
</a>
|
||||
{publisher.phone ? (
|
||||
<a href={`viber://chat/?number=%2B${publisher.phone.startsWith('+') ? publisher.phone.substring(1) : publisher.phone}`} className="inline-block p-2">
|
||||
<i className="fab fa-viber text-purple-500 text-xl" title="Viber"></i>
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default ContactsPage;
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,phone');
|
||||
|
||||
return {
|
||||
props: {
|
||||
publishers,
|
||||
},
|
||||
};
|
||||
};
|
105
pages/cart/publishers/edit/[id].tsx
Normal file
105
pages/cart/publishers/edit/[id].tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import axiosServer from '../../../../src/axiosServer';
|
||||
import NewPubPage from "../new";
|
||||
export default NewPubPage;
|
||||
|
||||
import { Assignment, Shift, UserRole } from "prisma/prisma-client";
|
||||
// import { monthNamesBG } from "~/src/helpers/const"
|
||||
import { monthNamesBG } from "src/helpers/const";
|
||||
|
||||
function getShiftGroups(shifts: [Shift]) {
|
||||
|
||||
const groupedShifts = shifts.reduce((groups, shift) => {
|
||||
// Extract the year and month from the shift date
|
||||
const yearMonth = shift.startTime.substring(0, 7)
|
||||
// Initialize the group for the year-month if it doesn't exist
|
||||
if (!groups[yearMonth]) {
|
||||
groups[yearMonth] = []
|
||||
}
|
||||
// Add the shift to the group
|
||||
groups[yearMonth].push(shift)
|
||||
// Return the updated groups object
|
||||
return groups
|
||||
}, {})
|
||||
|
||||
// Sort the groups by year-month
|
||||
const sortedGroups = Object.keys(groupedShifts).sort((a, b) => {
|
||||
// Compare the year-month strings lexicographically
|
||||
if (a < b) return -1
|
||||
if (a > b) return 1
|
||||
return 0
|
||||
}).reduce((result, key) => {
|
||||
// Rebuild the object with the sorted keys
|
||||
result[key] = groupedShifts[key]
|
||||
return result
|
||||
}, {})
|
||||
return sortedGroups;
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
|
||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
if (!context.query || !context.query.id) {
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
}
|
||||
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift";
|
||||
console.log("GET PUBLISHER FROM:" + url)
|
||||
const { data: item } = await axios.get(url);
|
||||
|
||||
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);
|
||||
|
||||
//group shifts by month, remove duplicates
|
||||
//sort availabilities by start time
|
||||
// item.availabilities = item.availabilities
|
||||
// .sort((a, b) => b.startTime - a.startTime);
|
||||
|
||||
item.assignments = item.assignments
|
||||
.sort((a, b) => b.startTime - a.startTime)
|
||||
.reduce((acc, assignment: Assignment) => {
|
||||
|
||||
const date = new Date(assignment.shift.startTime);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const tabId = year + "-" + month;
|
||||
const tabName = monthNamesBG[month] + " " + year;
|
||||
const day = date.getDate();
|
||||
// console.log("shift: year: " + year + " month: " + month + " day: " + day);
|
||||
if (!acc.items[tabId]) {
|
||||
acc.items[tabId] = [];
|
||||
}
|
||||
if (!acc.items[tabId][day]) {
|
||||
acc.items[tabId][day] = [];
|
||||
}
|
||||
//remove duplicates
|
||||
if (acc.items[tabId][day].find(s => s.id == assignment.shift.id)) {
|
||||
return acc;
|
||||
}
|
||||
// acc.months = acc.months || [];
|
||||
if (!acc.months[tabId]) {
|
||||
acc.months[tabId] = [];
|
||||
}
|
||||
|
||||
if (!acc.keys.includes(tabId)) {
|
||||
acc.months[tabId] = tabName;
|
||||
acc.keys.push(tabId);
|
||||
}
|
||||
|
||||
acc.items[tabId][day].push({
|
||||
start: assignment.shift.startTime,
|
||||
end: assignment.shift.endTime,
|
||||
id: assignment.id,
|
||||
shiftId: assignment.shift.id,
|
||||
isConfirmed: assignment.isConfirmed ? true : false,
|
||||
});
|
||||
return acc;
|
||||
}, { items: {}, keys: [], months: {} });
|
||||
// console.log("server item:");
|
||||
// console.dir(item, { depth: null });
|
||||
return {
|
||||
props: {
|
||||
item: item
|
||||
},
|
||||
};
|
||||
};
|
20
pages/cart/publishers/edit/me.tsx
Normal file
20
pages/cart/publishers/edit/me.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
// pages/me.jsx
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export default function Me() {
|
||||
const router = useRouter();
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'authenticated') {
|
||||
router.push(`/cart/publishers/edit/${session.user.id}?self=true`);
|
||||
} else if (status === 'unauthenticated') {
|
||||
router.push('/api/auth/signin');
|
||||
}
|
||||
}, [status, session, router]);
|
||||
|
||||
// You can add a fallback content or loader here if you want
|
||||
return <div>Redirecting...</div>;
|
||||
}
|
629
pages/cart/publishers/import.tsx
Normal file
629
pages/cart/publishers/import.tsx
Normal file
@ -0,0 +1,629 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import Layout from "../../../components/layout";
|
||||
import { Publisher, Availability, AvailabilityType, DayOfWeek, UserRole } from "@prisma/client";
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import { useState, useRef } from "react";
|
||||
|
||||
|
||||
import * as XLSX from "xlsx";
|
||||
// import { Table } from "react-bootstrap";
|
||||
import { staticGenerationAsyncStorage } from "next/dist/client/components/static-generation-async-storage.external";
|
||||
|
||||
import moment from 'moment';
|
||||
// import { DatePicker } from '@mui/x-date-pickers'; !! CAUSERS ERROR ???
|
||||
|
||||
// import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { set } from 'date-fns';
|
||||
|
||||
// import * as common from "src/helpers/common";
|
||||
const common = require('../../../src/helpers/common');
|
||||
|
||||
export default function ImportPage() {
|
||||
const [data, setData] = useState([])
|
||||
const [rawData, setRawData] = useState([]);
|
||||
const [status, setStatus] = useState({ status: 'idle', info: '' });
|
||||
|
||||
const MODE_PUBLISHERS1 = "PUBLISHERS1";
|
||||
const MODE_PUBLISHERS2 = "PUBLISHERS2";
|
||||
type ModeState = {
|
||||
mainMode: typeof MODE_PUBLISHERS1 | typeof MODE_PUBLISHERS2;
|
||||
schedule: boolean;
|
||||
publishers2Import: boolean;
|
||||
headerRow: number;
|
||||
}
|
||||
|
||||
const [mode, setMode] = useState<ModeState>({
|
||||
mainMode: MODE_PUBLISHERS1,
|
||||
schedule: false,
|
||||
publishers2Import: false,
|
||||
headerRow: 0
|
||||
});
|
||||
|
||||
const headerRef = useRef({
|
||||
header: null,
|
||||
dateIndex: -1,
|
||||
emailIndex: -1,
|
||||
nameIndex: -1,
|
||||
phoneIndex: -1,
|
||||
isTrainedIndex: -1,
|
||||
desiredShiftsIndex: -1,
|
||||
dataStartIndex: -1,
|
||||
isActiveIndex: -1,
|
||||
pubTypeIndex: -1
|
||||
});
|
||||
|
||||
const handleFile = (e) => {
|
||||
const [file] = e.target.files;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (evt) => {
|
||||
const bstr = evt.target.result;
|
||||
const wb = XLSX.read(bstr, { type: "binary" });
|
||||
const wsname = wb.SheetNames[0];
|
||||
const ws = wb.Sheets[wsname];
|
||||
const sheetData = XLSX.utils.sheet_to_json(ws, {
|
||||
header: 1,
|
||||
range: 0,
|
||||
blankrows: false,
|
||||
defval: '',
|
||||
});
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (sheetData[i].includes('Имейл')) {
|
||||
setMode({
|
||||
mainMode: MODE_PUBLISHERS1,
|
||||
schedule: false,
|
||||
publishers2Import: false,
|
||||
headerRow: i
|
||||
});
|
||||
|
||||
headerRef.current.header = sheetData[i];
|
||||
headerRef.current.dataStartIndex = i;
|
||||
common.logger.debug("header at row " + i);
|
||||
break;
|
||||
}
|
||||
// it seems we are processing availability sheet. import only publishers by default, and read only the first 3 columns as publisher data
|
||||
if (sheetData[i].includes('Email Address')) {
|
||||
setMode({
|
||||
mainMode: MODE_PUBLISHERS2,
|
||||
schedule: true,
|
||||
publishers2Import: false,
|
||||
headerRow: i
|
||||
});
|
||||
headerRef.current.header = sheetData[i];
|
||||
headerRef.current.dataStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!headerRef.current.header) {
|
||||
console.error("header not found in the first 5 rows!");
|
||||
return;
|
||||
}
|
||||
|
||||
const header = headerRef.current.header
|
||||
headerRef.current.dateIndex = header.indexOf('Timestamp');
|
||||
headerRef.current.emailIndex = header.indexOf('Имейл') !== -1 ? header.indexOf('Имейл') : header.indexOf('Email Address');
|
||||
headerRef.current.nameIndex = header.indexOf('Име, Фамилия');
|
||||
headerRef.current.phoneIndex = header.indexOf('Телефон');
|
||||
headerRef.current.isTrainedIndex = header.indexOf('Обучен');
|
||||
headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия');
|
||||
headerRef.current.isActiveIndex = header.indexOf("Неактивен");
|
||||
headerRef.current.pubTypeIndex = header.indexOf("Назначение");
|
||||
|
||||
const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => {
|
||||
let date;
|
||||
date = common.excelSerialDateToDate(row[headerRef.current.dateIndex]);
|
||||
//substract 1 day, because excel serial date is 1 day ahead
|
||||
date.setDate(date.getDate() - 1);
|
||||
date = common.getDateFormated(date);
|
||||
common.logger.debug(date);
|
||||
return [date, row[headerRef.current.emailIndex], row[headerRef.current.nameIndex]];
|
||||
});
|
||||
|
||||
setRawData(sheetData);
|
||||
setData(filteredData);
|
||||
setStatus({ status: 'зареден', info: `Заредени ${filteredData.length} от ${rawData.length} записа` });
|
||||
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
|
||||
// Reset the file input value
|
||||
e.target.value = null;
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
common.logger.debug("handleSave to: " + common.getBaseUrl());
|
||||
const header = rawData[mode.headerRow];
|
||||
for (let i = mode.headerRow + 1; i < rawData.length; i++) { //fullData.length; each publisher
|
||||
//update status.info with current publisher
|
||||
setStatus({ status: 'running', info: `Processing row ${i} of ${rawData.length}` });
|
||||
//sleep for 300ms to allow the database to process the previous request
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
const row = rawData[i];
|
||||
var email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = false;
|
||||
//const date = new Date(row[0]).toISOS{tring().slice(0, 10);
|
||||
if (mode.mainMode == MODE_PUBLISHERS1) {
|
||||
|
||||
email = row[headerRef.current.emailIndex];
|
||||
|
||||
phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace
|
||||
// Remove any non-digit characters, except for the leading +
|
||||
//phone = phone.replace(/(?!^\+)\D/g, '');
|
||||
phone = phone.replace(/[^+\d]/g, '');
|
||||
if (phone.startsWith('8') || phone.startsWith('9')) {
|
||||
phone = '+359' + phone.substring(1); // Assumes all numbers starting with 8 are Bulgarian and should have +359
|
||||
} else if (!phone.startsWith('+')) {
|
||||
phone = '+' + phone; // Add + if it's missing, assuming the number is complete
|
||||
}
|
||||
names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/);
|
||||
dateOfInput = importDate.value || new Date().toISOString();
|
||||
// not empty == true
|
||||
isTrained = row[headerRef.current.isTrainedIndex] !== '';
|
||||
isActive = row[headerRef.current.isActiveIndex] == '';
|
||||
desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4;
|
||||
}
|
||||
else {
|
||||
dateOfInput = common.excelSerialDateToDate(row[0]);
|
||||
email = row[1];
|
||||
names = row[2].normalize('NFC').split(/[, ]+/);
|
||||
}
|
||||
|
||||
//remove special characters from name
|
||||
|
||||
// let names = common.removeAccentsAndSpecialCharacters(row[2]).split(/[, ]+/);
|
||||
|
||||
let personId = '';
|
||||
try {
|
||||
try {
|
||||
const select = "&select=id,firstName,lastName,phone,isTrained,desiredShiftsPerMonth,isactive,availabilities";
|
||||
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
|
||||
let existingPublisher = responseByName.data[0];
|
||||
if (!existingPublisher) {
|
||||
// If no match by name, check by email
|
||||
const responseByEmail = await axiosInstance.get(`/api/?action=findPublisher&email=${email}${select}`);
|
||||
if (responseByEmail.data.length > 0) {
|
||||
// Iterate over all matches by email to find one with a matching or similar name
|
||||
const fullName = names.join(' ').toLowerCase(); // Simplify comparison
|
||||
existingPublisher = responseByEmail.data.find(publisher => {
|
||||
const publisherFullName = (publisher.firstName + ' ' + publisher.lastName).toLowerCase();
|
||||
return fullName === publisherFullName; // Consider expanding this comparison for typos
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (existingPublisher?.id) { // UPDATE
|
||||
// Create a flag to check if update is needed
|
||||
const updatedData = {};
|
||||
personId = existingPublisher?.id;
|
||||
let updateNeeded = false;
|
||||
|
||||
// Check for name update
|
||||
const fullName = names.join(' ');
|
||||
const existingFullName = existingPublisher.firstName + ' ' + existingPublisher.lastName;
|
||||
if (fullName !== existingFullName) {
|
||||
common.logger.debug(`Existing publisher '${existingFullName}' found for ${email} (ID:${personId})`);
|
||||
updatedData.firstName = names[0];
|
||||
updatedData.lastName = names.slice(1).join(' ');
|
||||
data[i - mode.headerRow][4] = "name updated!";
|
||||
updateNeeded = true;
|
||||
} else {
|
||||
data[i - mode.headerRow][4] = "existing";
|
||||
}
|
||||
|
||||
// Log existing publisher
|
||||
common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`);
|
||||
|
||||
|
||||
// Check for other updates
|
||||
const fieldsToUpdate = [
|
||||
{ key: 'phone', value: phone },
|
||||
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
|
||||
{ key: 'isTrained', value: isTrained },
|
||||
{ key: 'isactive', value: isActive }
|
||||
];
|
||||
|
||||
fieldsToUpdate.forEach(({ key, value, parse }) => {
|
||||
if (!existingPublisher[key] && value !== '' && value !== undefined) {
|
||||
updatedData[key] = parse ? parse(value) : value;
|
||||
updateNeeded = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update the record if needed and if MODE_PUBLISHERS1 (Import from List of Participants)
|
||||
if (updateNeeded && (mode.publishers2Import || mode.mainMode == MODE_PUBLISHERS1)) {
|
||||
try {
|
||||
await axiosInstance.put(`/api/data/publishers/${personId}`, updatedData);
|
||||
common.logger.debug(`Updated publisher ${personId}`);
|
||||
data[i - mode.headerRow][4] = "updated";
|
||||
} catch (error) {
|
||||
console.error(`Failed to update publisher ${personId}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
} else { // CREATE
|
||||
|
||||
// If no publisher with the email exists, create one
|
||||
// const names = email.split('@')[0].split('.');\
|
||||
|
||||
//Save new publisher
|
||||
if (mode.publishers2Import) {
|
||||
const personResponse = await axiosInstance.post('/api/data/publishers', {
|
||||
email,
|
||||
phone,
|
||||
firstName: names[0],
|
||||
lastName: names[1],
|
||||
isactive: isActive,
|
||||
isTrained,
|
||||
desiredShiftsPerMonth,
|
||||
});
|
||||
personId = personResponse.data.id;
|
||||
data[i][4] = "new";
|
||||
}
|
||||
else
|
||||
if (mode.mainMode == MODE_PUBLISHERS1) {
|
||||
const firstname = names.length > 2 ? names.slice(0, -1).join(' ') : names[0];
|
||||
const personResponse = await axiosInstance.post('/api/data/publishers', {
|
||||
email,
|
||||
phone,
|
||||
firstName: firstname,
|
||||
lastName: names[names.length - 1],
|
||||
isactive: isActive,
|
||||
isTrained,
|
||||
desiredShiftsPerMonth
|
||||
});
|
||||
data[i - mode.headerRow][4] = "new";
|
||||
} else {
|
||||
data[i - mode.headerRow][4] = "import disabled";
|
||||
}
|
||||
|
||||
common.logger.debug(`NEW Publisher ${personId} created for email ${email} (${names})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
data[i - mode.headerRow][4] = "error";
|
||||
}
|
||||
|
||||
if (mode.schedule) {
|
||||
// Parse the availability data from the Excel cell
|
||||
//get days of the month and add up to the next full week
|
||||
// Save availability records
|
||||
const availabilities: Availability[] = [];
|
||||
|
||||
for (let j = 3; j < header.length; j++) {
|
||||
const dayHeader = header[j];
|
||||
|
||||
const shifts = row[j];
|
||||
if (!shifts || shifts === 'Не мога') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// specific date: Седмица (17-23 април) [Четвъртък ]
|
||||
// const regex = /^Седмица \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/;
|
||||
// specific week: Седмица 1 <any character> [Четвъртък]
|
||||
// const regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/;
|
||||
//allow optional space before and after the brackets
|
||||
// match both 'Седмица 3 (20-25 ноември) [пон]' and 'Седмица 4 (27 ноември - 2 декември) [четв]'
|
||||
//const regex = /^\s*Седмица\s+(\d{1,2})\s+\((\d{1,2})\s+(\S+)(?:\s*-\s*(\d{1,2})\s+(\S+))?\)\s*\[\s*(\S+)\s*\]\s*$/;
|
||||
//the original, but missing the two month names
|
||||
let regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
|
||||
regex = /^Седмица (\d{1,2}) \((\d{1,2})(?:\s*-\s*(\d{1,2}))? (\S+)(?:\s*-\s*(\d{1,2}) (\S+))?\)\s*\[(\S+)\s*\]\s*$/;
|
||||
//const regex = /^Седмица (\d{1,2}) \((\d{1,2}(\s*\S*?))-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
|
||||
|
||||
//both Седмица 1 (6-11 ноември) [пон]
|
||||
// Седмица 4 (27 ноември-2 декември) [пет]
|
||||
//const regex = /^Седмица (\d{1,2}) \((\d{1,2} \S+)?-? ?(\d{1,2} \S+)\)\s*\[(\S+)\s*\]\s*$/;
|
||||
|
||||
//const regex = /^Седмица (\d{1,2}) \((\d{1,2} \S+)?-? ?(\d{1,2} \S+)\)\s*\[(\S+)\s*\]\s*$/;
|
||||
|
||||
|
||||
// const regex = /^Седмица (\d{1,2}) \* \[(\S+)\]$/;
|
||||
// const regex = /^Седмица (\d{1,2}) \[(\S+)\]$/;
|
||||
|
||||
// replace multiple spaces with single space
|
||||
const normalizedHeader = dayHeader.replace(/\s+/g, ' ');
|
||||
var match = normalizedHeader.match(regex);
|
||||
|
||||
if (!match) {
|
||||
common.logger.debug("was not able to parse availability " + shifts + "trying again with different regex");
|
||||
let regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
|
||||
match = normalizedHeader.match(regex);
|
||||
}
|
||||
if (match) {
|
||||
//ToDo: can't we use date.getDayEuropean() instead of this logic down?
|
||||
const weekNr = parseInt(match[1]);
|
||||
const weekStart = match[2];
|
||||
// const endDate = match[2];
|
||||
const month = match[4];
|
||||
const dayOfWeekStr = match[7];
|
||||
const dayOfWeek = common.getDayOfWeekIndex(dayOfWeekStr);
|
||||
common.logger.debug("processing availability for week " + weekNr + ": " + weekStart + "." + month + "." + dayOfWeekStr)
|
||||
// Create a new Date object for the start date of the range
|
||||
const day = new Date();
|
||||
day.setDate(1); // Set to the first day of the month to avoid overflow
|
||||
//day.setMonth(day.getMonth() + 1); // Add one month to the date, because we assume we are p
|
||||
day.setMonth(common.getMonthIndex(month));
|
||||
day.setDate(parseInt(weekStart) + dayOfWeek);
|
||||
day.setHours(0, 0, 0, 0);
|
||||
|
||||
common.logger.debug("processing availabilities for " + day.toLocaleDateString()); // Output: Sun Apr 17 2022 14:07:11 GMT+0300 (Eastern European Summer Time)
|
||||
common.logger.debug("parsing availability input: " + shifts); // Output: 0 (Sunday)
|
||||
const dayOfWeekName = common.getDayOfWeekNameEnEnum(day);
|
||||
|
||||
let dayOfMonth = day.getDate();
|
||||
const name = `${names[0]} ${names[1]}`;
|
||||
const intervals = shifts.split(",");
|
||||
|
||||
if (!oldAvDeleted && personId) {
|
||||
if (mode.schedule && email) {
|
||||
common.logger.debug(`Deleting existing availabilities for publisher ${personId} for date ${day}`);
|
||||
try {
|
||||
await axiosInstance.post(`/api/?action=deleteAvailabilityForPublisher&publisherId=${personId}&date=${day}&deleteFromPreviousAssignments=true`);
|
||||
common.logger.debug(`Deleted all availabilities for publisher ${personId}`);
|
||||
oldAvDeleted = true;
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to delete availabilities for publisher ${personId}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
let parsedIntervals: { end: number; }[] = [];
|
||||
intervals.forEach(interval => {
|
||||
// Summer format: (12:00-14:00)
|
||||
//if (!/\(\d{1,2}:\d{2}-\d{1,2}:\d{2}\)/.test(interval)) {
|
||||
//winter regex:
|
||||
//\d{1,2}-\d{1,2}:\d{2}/
|
||||
// Regular expression to match both Summer format (12:00-14:00) and winter format '09-10:30'
|
||||
const regex = /\d{1,2}(?::\d{2})?-\d{1,2}(?::\d{2})?/;
|
||||
if (!regex.test(interval)) {
|
||||
common.logger.debug(`Skipping invalid interval: ${interval}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the interval matches the format, remove any non-essential characters
|
||||
const cleanedInterval = interval.replace(/[()]/g, "");
|
||||
|
||||
// Extract start and end times from interval string
|
||||
const [start, end] = cleanedInterval.split("-");
|
||||
|
||||
// Convert times like "12" to "12:00" for consistent parsing
|
||||
const formattedStart = start.includes(":") ? start : start + ":00";
|
||||
const formattedEnd = end.includes(":") ? end : end + ":00";
|
||||
|
||||
// Try to parse the times, and skip the interval if it can't be parsed
|
||||
try {
|
||||
const parsedStart = Number(formattedStart.split(":").join(""));
|
||||
const parsedEnd = Number(formattedEnd.split(":").join(""));
|
||||
// Store parsed interval
|
||||
parsedIntervals.push({
|
||||
start: parsedStart,
|
||||
end: parsedEnd
|
||||
});
|
||||
} catch (error) {
|
||||
common.logger.debug(`Error parsing interval: ${interval}`);
|
||||
return;
|
||||
}
|
||||
|
||||
});
|
||||
// Sort intervals by start time
|
||||
parsedIntervals.sort((a, b) => a.start - b.start);
|
||||
|
||||
// Initialize start and end times with the first interval
|
||||
let minStartTime = parsedIntervals[0].start;
|
||||
let maxEndTime = parsedIntervals[0].end;
|
||||
|
||||
let isOld = false;
|
||||
// Calculate the total month difference by considering the year difference
|
||||
let totalMonthDifference = (day.getFullYear() - dateOfInput.getFullYear()) * 12 + (day.getMonth() - dateOfInput.getMonth());
|
||||
// If the total month difference is 2 or more, set isOld to true
|
||||
if (totalMonthDifference >= 2) {
|
||||
isOld = true;
|
||||
}
|
||||
|
||||
// Iterate over intervals
|
||||
for (let i = 1; i < parsedIntervals.length; i++) {
|
||||
if (parsedIntervals[i].start > maxEndTime) {
|
||||
availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld));
|
||||
minStartTime = parsedIntervals[i].start;
|
||||
maxEndTime = parsedIntervals[i].end;
|
||||
} else {
|
||||
maxEndTime = Math.max(maxEndTime, parsedIntervals[i].end);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last availability
|
||||
availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld));
|
||||
|
||||
}
|
||||
else {
|
||||
common.logger.debug("availability not matched. header:" + dayHeader + " shifts:" + shifts);
|
||||
}
|
||||
}
|
||||
|
||||
common.logger.debug("availabilities to save for " + personId + ": " + availabilities.length);
|
||||
// Send a single request to create all availabilities
|
||||
axiosInstance.post('/api/?action=createAvailabilities', availabilities)
|
||||
.then(response => common.logger.debug('Availabilities created:', response.data))
|
||||
.catch(error => console.error('Error creating availabilities:', error));
|
||||
|
||||
// Experimental: add availabilities to all publishers with the same email
|
||||
//check if more than one publisher has the same email, and add the availabilities to both
|
||||
//check existing publishers with the same email
|
||||
var sameNamePubs = axiosInstance.get(`/api/?action=findPublisher&all=true&email=${email}&select=id,firstName,lastName`);
|
||||
sameNamePubs.then(function (response) {
|
||||
common.logger.debug("same name pubs: " + response.data.length);
|
||||
if (response.data.length > 1) {
|
||||
response.data.forEach(pub => {
|
||||
//check the publisher is not the same as the one we already added the availabilities to
|
||||
if (pub.id != personId) {
|
||||
//change the publisher id to the new one
|
||||
availabilities.forEach(availability => {
|
||||
availability.publisherId = pub.id;
|
||||
}
|
||||
);
|
||||
//delete existing availabilities for the publisher
|
||||
axiosInstance.post(`/api/?action=deleteAvailabilityForPublisher&publisherId=${pub.id}&date=${dateOfInput}`);
|
||||
// Send a single request to create all availabilities
|
||||
axiosInstance.post('/api/?action=createAvailabilities', availabilities)
|
||||
.then(response => common.logger.debug('Availabilities created:', response.data))
|
||||
.catch(error => console.error('Error creating availabilities:', error));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// await axios.post("/api/data/availabilities", availabilities);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
toast.info('Records saved successfully', { autoClose: 30000 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('An error occurred while saving records!');
|
||||
}
|
||||
};
|
||||
|
||||
// Function to create an availability object
|
||||
function createAvailabilityObject(start: any, end: number, day: Date, dayOfWeekName: any, dayOfMonth: number, weekNr: number, personId: string, name: string, isFromPreviousMonth: boolean): Availability {
|
||||
const formatTime = (time) => {
|
||||
const paddedTime = String(time).padStart(4, '0');
|
||||
return new Date(day.getFullYear(), day.getMonth(), day.getDate(), parseInt(paddedTime.substr(0, 2)), parseInt(paddedTime.substr(2, 4)));
|
||||
};
|
||||
|
||||
const startTime = formatTime(start);
|
||||
const endTime = formatTime(end);
|
||||
|
||||
return {
|
||||
id: 0, // Add the missing 'id' property
|
||||
publisherId: personId,
|
||||
name,
|
||||
dayofweek: dayOfWeekName,
|
||||
dayOfMonth,
|
||||
weekOfMonth: weekNr, // Add the missing 'weekOfMonth' property
|
||||
startTime,
|
||||
endTime,
|
||||
isactive: true,
|
||||
type: AvailabilityType.OneTime,
|
||||
isWithTransportIn: false, // Add the missing 'isWithTransport' property
|
||||
isWithTransportOut: false, // Add the missing 'isWithTransport' property
|
||||
isFromPreviousAssignment: false, // Add the missing 'isFromPreviousAssignment' property
|
||||
isFromPreviousMonth: isFromPreviousMonth // Add the missing 'isFromPreviousMonth' property
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
|
||||
|
||||
<div className="p-6">
|
||||
<h1 className="text-3xl mb-4 font-semibold">Import Page</h1>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700" htmlFor="fileInput">Choose a file to import:</label>
|
||||
<input type="file" id="fileInput" onChange={handleFile} className="mt-1 p-2 border rounded" placeholder="Choose a file" />
|
||||
</div>
|
||||
|
||||
{/* <DatePicker label="Дата" value={date} onChange={setDate} /> */}
|
||||
|
||||
<div className="mb-4 space-y-2"> {/* Adjust this value as needed */}
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
value="PUBLISHERS1"
|
||||
checked={mode.mainMode === "PUBLISHERS1"}
|
||||
onChange={() => setMode({ mainMode: "PUBLISHERS1", schedule: false, publishers2Import: false })}
|
||||
className="text-indigo-600"
|
||||
/>
|
||||
<span className="ml-2 space-x-4">Импортирай от Списък с участниците</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
value="PUBLISHERS2"
|
||||
checked={mode.mainMode === "PUBLISHERS2"}
|
||||
onChange={() => setMode(prev => ({ ...prev, mainMode: "PUBLISHERS2" }))}
|
||||
className="text-indigo-600"
|
||||
/>
|
||||
<span className="ml-2">Импортирай от Предпочитания за колички</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
|
||||
{/* <DatePicker
|
||||
label="Дата на импорт"
|
||||
value={new Date()}
|
||||
onChange={(date) => {
|
||||
common.logger.debug("date changed to " + date);
|
||||
}}
|
||||
/> */}
|
||||
|
||||
{/* simple HTML datepicker for import date */}
|
||||
<input type="date" id="importDate" name="importDate" />
|
||||
</label>
|
||||
|
||||
{mode.mainMode === "PUBLISHERS2" && (
|
||||
<div className="mt-2 space-y-1 pl-6">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mode.schedule}
|
||||
onChange={(e) => setMode(prev => ({ ...prev, schedule: e.target.checked }))}
|
||||
className="text-indigo-600"
|
||||
/>
|
||||
<span className="ml-2">Предпочитания</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center ">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mode.publishers2Import}
|
||||
onChange={(e) => setMode(prev => ({ ...prev, publishers2Import: e.target.checked }))}
|
||||
className="text-indigo-600"
|
||||
/>
|
||||
<span className="ml-2">Вестители</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<button onClick={handleSave} className="bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">Запази</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<span className="text-gray-600">{data.length} вестители прочетени</span>
|
||||
<span className="text-gray-600">{status.info}</span>
|
||||
</div>
|
||||
|
||||
<table className="min-w-full border-collapse border border-gray-500">
|
||||
<thead>
|
||||
<tr>
|
||||
{data.length > 0 &&
|
||||
Object.keys(data[0]).map((key) => <th className="px-4 py-2 border-b font-medium" key={key}>{Object.values(data[0])[key]}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.slice(1).map((row, index) => (
|
||||
<tr key={index} className="even:bg-gray-100">
|
||||
{Object.values(row).map((value, index) => (
|
||||
<td key={index} className="border px-4 py-2">{value}</td>
|
||||
))}
|
||||
<td id={row[1]}>
|
||||
<i className={`fa fa-circle ${status[row[3]] || 'text-gray-400'}`}></i>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
214
pages/cart/publishers/index.tsx
Normal file
214
pages/cart/publishers/index.tsx
Normal file
@ -0,0 +1,214 @@
|
||||
// Next.js page to show all locations in the database with a link to the location page
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useState, useRef, use } from "react";
|
||||
// import { getSession } from 'next-auth/client'
|
||||
// import { NextAuth } from 'next-auth/client'
|
||||
import { Publisher, UserRole } from "@prisma/client";
|
||||
import Layout from "../../../components/layout";
|
||||
import PublisherCard from "../../../components/publisher/PublisherCard";
|
||||
import axiosInstance from "../../../src/axiosSecure";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { levenshteinEditDistance } from "levenshtein-edit-distance";
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import ConfirmationModal from '../../../components/ConfirmationModal';
|
||||
|
||||
|
||||
|
||||
interface IProps {
|
||||
initialItems: Publisher[];
|
||||
}
|
||||
|
||||
function PublishersPage({ publishers = [] }: IProps) {
|
||||
const [shownPubs, setShownPubs] = useState(publishers);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [filterIsImported, setFilterIsImported] = useState({
|
||||
checked: false,
|
||||
indeterminate: true,
|
||||
});
|
||||
const [showZeroShiftsOnly, setShowZeroShiftsOnly] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleDeleteAllVisible = async () => {
|
||||
setIsDeleting(true);
|
||||
|
||||
for (const publisher of shownPubs) {
|
||||
try {
|
||||
await axiosInstance.delete(`/api/data/publishers/${publisher.id}`);
|
||||
setShownPubs(shownPubs.filter(p => p.id !== publisher.id));
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify(error));
|
||||
}
|
||||
}
|
||||
|
||||
setIsDeleting(false);
|
||||
setIsModalOpen(false);
|
||||
// After all publishers are deleted, you might want to refresh the list or make additional changes
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// const filteredPublishers = publishers.filter((publisher) => {
|
||||
// return publisher.firstName.toLowerCase().includes(filter.toLowerCase())
|
||||
// || publisher.lastName.toLowerCase().includes(filter.toLowerCase());
|
||||
// });
|
||||
|
||||
//name filter
|
||||
let filteredPublishersByName = publishers
|
||||
.filter((publisher) => {
|
||||
const fullName = publisher.firstName.toLowerCase() + " " + publisher.lastName.toLowerCase();
|
||||
const distance = levenshteinEditDistance(fullName, filter.toLowerCase());
|
||||
const lenDiff = Math.max(fullName.length, filter.length) - Math.min(fullName.length, filter.length);
|
||||
|
||||
let similarity;
|
||||
if (distance === 0) {
|
||||
similarity = 1; // Exact match
|
||||
} else {
|
||||
similarity = 1 - (distance - lenDiff) / distance;
|
||||
}
|
||||
|
||||
console.log("distance: " + distance + "; lenDiff: " + lenDiff + " similarity: " + similarity + "; " + fullName + " =? " + filter + "")
|
||||
return similarity >= 0.95;
|
||||
});
|
||||
|
||||
// Email filter
|
||||
let filteredPublishersByEmail = publishers.filter(publisher =>
|
||||
publisher.email.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
|
||||
// Combine name and email filters, removing duplicates
|
||||
let filteredPublishers = [...new Set([...filteredPublishersByName, ...filteredPublishersByEmail])];
|
||||
|
||||
// inactive publishers filter
|
||||
filteredPublishers = showZeroShiftsOnly
|
||||
? filteredPublishers.filter(p => p.assignments.length === 0)
|
||||
: filteredPublishers;
|
||||
|
||||
setShownPubs(filteredPublishers);
|
||||
}, [filter, showZeroShiftsOnly]);
|
||||
|
||||
const checkboxRef = useRef();
|
||||
|
||||
const renderPublishers = () => {
|
||||
if (shownPubs.length === 0) {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<a
|
||||
className="btn"
|
||||
href="javascript:void(0);"
|
||||
onClick={() => {
|
||||
setFilter("");
|
||||
handleFilterChange({ target: { value: "" } });
|
||||
}}
|
||||
>
|
||||
Clear filters
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return shownPubs.map((publisher) => (
|
||||
<PublisherCard key={publisher.id} publisher={publisher} />
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value, type, checked } = event.target;
|
||||
|
||||
// setFilter(event.target.value);
|
||||
if (type === 'text') {
|
||||
setFilter(value);
|
||||
} else if (type === 'checkbox') {
|
||||
// setFilterIsImported({ ...checkboxFilter, [name]: checked });
|
||||
const { checked, indeterminate } = checkboxRef.current;
|
||||
if (!checked && !indeterminate) {
|
||||
// Checkbox was unchecked, set it to indeterminate state
|
||||
checkboxRef.current.indeterminate = true;
|
||||
setFilterIsImported({ checked: false, indeterminate: true });
|
||||
} else if (!checked && indeterminate) {
|
||||
// Checkbox was indeterminate, set it to checked state
|
||||
checkboxRef.current.checked = true;
|
||||
checkboxRef.current.indeterminate = false;
|
||||
setFilterIsImported({ checked: true, indeterminate: false });
|
||||
} else if (checked && !indeterminate) {
|
||||
// Checkbox was checked, set it to unchecked state
|
||||
checkboxRef.current.checked = false;
|
||||
checkboxRef.current.indeterminate = false;
|
||||
setFilterIsImported({ checked: false, indeterminate: false });
|
||||
} else {
|
||||
// Checkbox was checked and indeterminate (should not happen), set it to unchecked state
|
||||
checkboxRef.current.checked = false;
|
||||
checkboxRef.current.indeterminate = false;
|
||||
setFilterIsImported({ checked: false, indeterminate: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
|
||||
<div className="">
|
||||
<div className="flex items-center justify-center space-x-4 m-4">
|
||||
<div className="flex justify-center m-4">
|
||||
<a href="/cart/publishers/new" className="btn"> Добави вестител </a>
|
||||
</div>
|
||||
|
||||
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpen(true)} disabled={isDeleting} >
|
||||
{isDeleting ? "Изтриване..." : "Изтрий показаните вестители"}
|
||||
</button>
|
||||
<ConfirmationModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onConfirm={handleDeleteAllVisible}
|
||||
message="Сигурни ли сте, че искате да изтриете всички показани в момента вестители?"
|
||||
/>
|
||||
<div className="flex justify-center m-4">
|
||||
<a href="/cart/publishers/import" className="btn"> Import publishers </a>
|
||||
</div>
|
||||
</div>
|
||||
<div name="filters" className="flex items-center justify-center space-x-4 m-4 sticky top-4 z-10 bg-gray-100 p-2">
|
||||
<label htmlFor="filter">Filter:</label>
|
||||
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
|
||||
className="border border-gray-300 rounded-md px-2 py-1"
|
||||
/>
|
||||
|
||||
|
||||
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
|
||||
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
|
||||
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
|
||||
className="form-checkbox text-indigo-600"
|
||||
/>
|
||||
<span className="ml-2">само без смени</span>
|
||||
</label>
|
||||
|
||||
<span id="filter-info" className="ml-4">{publishers.length} от {publishers.length} вестителя</span>
|
||||
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-4 z-0">
|
||||
{renderPublishers()}
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublishersPage;
|
||||
|
||||
//import { set } from "date-fns";
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
//ToDo: refactor all axios calls to use axiosInstance and this URL
|
||||
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isactive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect');
|
||||
|
||||
return {
|
||||
props: {
|
||||
publishers,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
59
pages/cart/publishers/new.tsx
Normal file
59
pages/cart/publishers/new.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
import { useState } from "react";
|
||||
import { useRouter } from 'next/router';
|
||||
import Layout from "../../../components/layout";
|
||||
import axiosServer from '../../../src/axiosServer';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import PublisherForm from "../../../components/publisher/PublisherForm";
|
||||
import { Publisher, UserRole } from "@prisma/client";
|
||||
|
||||
|
||||
export default function NewPubPage(item: Publisher) {
|
||||
item = item.item;
|
||||
const [publisher, setPublisher] = useState<Publisher>(item);
|
||||
const router = useRouter();
|
||||
const { id, self } = router.query;
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
const userRole = session?.user?.role as UserRole;
|
||||
const userId = session?.user?.id;
|
||||
|
||||
// Check if the user is editing their own profile and adjust allowedRoles accordingly
|
||||
let allowedRoles = [UserRole.POWERUSER, UserRole.ADMIN];
|
||||
if (status === 'authenticated' && userId && userId === id) {
|
||||
allowedRoles.push(userRole);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={allowedRoles}>
|
||||
<div className="h-5/6 grid place-items-center">
|
||||
<PublisherForm key={item?.id} item={item} me={self} />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
//------------------pages\cart\publishers\edit\[id].tsx------------------
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
const axios = await axiosServer(context);
|
||||
|
||||
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
if (!context.query || !context.query.id) {
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
}
|
||||
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts";
|
||||
console.log("GET PUBLISHER FROM:" + url)
|
||||
const { data } = await axios.get(url);
|
||||
|
||||
return {
|
||||
props: {
|
||||
data
|
||||
},
|
||||
};
|
||||
};
|
46
pages/cart/reports/experience.tsx
Normal file
46
pages/cart/reports/experience.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
// import axios from "axios";
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import Layout from "../../../components/layout";
|
||||
import ExperienceForm from "../../../components/reports/ExperienceForm";
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
|
||||
function NewPage(loc: Location) {
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
||||
<div className="h-5/6 grid place-items-center">
|
||||
<ExperienceForm />
|
||||
</div></ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPage;
|
||||
|
||||
//------------------pages\cart\locations\edit\[id].tsx------------------
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
//if query is undefined, then it is a new location
|
||||
// if (context.query.id === undefined) {
|
||||
// return {
|
||||
// props: {}
|
||||
// };
|
||||
// }
|
||||
// const { data: loc } = await axiosInstance.get(
|
||||
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
|
||||
// );
|
||||
|
||||
// console.log(location) //this is the location object
|
||||
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
// return {
|
||||
// props: {
|
||||
// location: loc,
|
||||
|
||||
// },
|
||||
// };
|
||||
};
|
130
pages/cart/reports/list.tsx
Normal file
130
pages/cart/reports/list.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
//page to show all repots in the database with a link to the report page
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { useSession } from "next-auth/react"
|
||||
//const common = require('src/helpers/common');
|
||||
import common from '../../../src/helpers/common';
|
||||
import Layout from "../../../components/layout";
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
|
||||
|
||||
export default function Reports() {
|
||||
const [reports, setReports] = useState([]);
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
|
||||
|
||||
|
||||
|
||||
const deleteReport = (id) => {
|
||||
axiosInstance
|
||||
.delete(`api/data/reports/${id}`)
|
||||
.then((res) => {
|
||||
toast.success("Успешно изтрит отчет");
|
||||
// router.push("/cart/reports/list");
|
||||
setReports(reports.filter(report => report.id !== id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const [locations, setLocations] = useState([]);
|
||||
useEffect(() => {
|
||||
const fetchLocations = async () => {
|
||||
try {
|
||||
console.log("fetching locations");
|
||||
const { data } = await axiosInstance.get("/api/data/locations");
|
||||
setLocations(data);
|
||||
console.log(data);
|
||||
axiosInstance.get(`api/data/reports`)
|
||||
.then((res) => {
|
||||
let reports = res.data;
|
||||
reports.forEach((report) => {
|
||||
report.location = data.find((loc) => loc.id === report.locationId);
|
||||
});
|
||||
setReports(res.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
if (!locations.length) {
|
||||
fetchLocations();
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
||||
|
||||
<div className="h-5/6 grid place-items-center">
|
||||
<div className="flex flex-col w-full px-4">
|
||||
<h1 className="text-2xl font-bold text-center">Отчети</h1>
|
||||
<Link href="/cart/reports/report">
|
||||
<button className="mt-4 bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
|
||||
Добави нов отчет
|
||||
</button>
|
||||
</Link>
|
||||
<div className="mt-4 w-full overflow-x-auto">
|
||||
<table className="w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left">Дата</th>
|
||||
<th className="px-4 py-2 text-left" >Място</th>
|
||||
<th className="px-4 py-2 text-left">Отчет</th>
|
||||
<th className="px-4 py-2 text-left">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reports.map((report) => (
|
||||
<tr key={report.id}>
|
||||
<td className="border px-4 py-2">{common.getDateFormated(new Date(report.date))}</td>
|
||||
<td className="border px-4 py-2">{report.location?.name}</td>
|
||||
<td className="border px-4 py-2">
|
||||
{(report.experienceInfo === null || report.experienceInfo === "")
|
||||
? (
|
||||
<>
|
||||
<div><strong>Отчет</strong></div>
|
||||
Издания: {report.placementCount} <br />
|
||||
Разговори: {report.conversationCount} <br />
|
||||
Клипове: {report.videoCount} <br />
|
||||
Адреси / Телефони: {report.returnVisitInfoCount} <br />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div><strong>Случка</strong></div>
|
||||
<div dangerouslySetInnerHTML={{ __html: report.experienceInfo }} />
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<td className="border px-4 py-2">
|
||||
|
||||
<button
|
||||
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
|
||||
onClick={() => deleteReport(report.id)}
|
||||
>
|
||||
Изтрий
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
45
pages/cart/reports/report.tsx
Normal file
45
pages/cart/reports/report.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
//next.js page to show all locatons in the database with a link to the location page
|
||||
// import axios from "axios";
|
||||
import { Location, UserRole } from "@prisma/client";
|
||||
import Layout from "../../../components/layout";
|
||||
import ReportForm from "../../../components/reports/ReportForm";
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
|
||||
function NewPage(loc: Location) {
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
|
||||
<div className="h-5/6 grid place-items-center">
|
||||
<ReportForm />
|
||||
</div></ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPage;
|
||||
|
||||
//------------------pages\cart\locations\edit\[id].tsx------------------
|
||||
|
||||
export const getServerSideProps = async (context) => {
|
||||
return {
|
||||
props: {}
|
||||
};
|
||||
//if query is undefined, then it is a new location
|
||||
// if (context.query.id === undefined) {
|
||||
// return {
|
||||
// props: {}
|
||||
// };
|
||||
// }
|
||||
|
||||
// const { data: loc } = await axiosInstance.get(
|
||||
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
|
||||
// );
|
||||
// console.log(location) //this is the location object
|
||||
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
|
||||
// return {
|
||||
// props: {
|
||||
// location: loc,
|
||||
// },
|
||||
// };
|
||||
};
|
31
pages/contactUs.tsx
Normal file
31
pages/contactUs.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import Layout from "../components/layout";
|
||||
|
||||
const ContactsPage = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="mx-auto my-8 p-6 max-w-4xl bg-white rounded-lg shadow-md">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-4">Специално свидетелстване на обществени места в София - Контакти</h1>
|
||||
<ul className="list-disc pl-5">
|
||||
<li className="text-gray-700 mb-2">Янко Ванчев - <a href="tel:+359878224467" className="text-blue-500 hover:text-blue-600">+359 878 22 44 67</a></li>
|
||||
<li className="text-gray-700">Крейг Смит - <a href="tel:+359878994573" className="text-blue-500 hover:text-blue-600">+359 878 994 573</a></li>
|
||||
</ul>
|
||||
<div className="text-gray-700 pl-4 py-5">Електронна поща: <a href="mailto:specialnosvidetelstvanesofia@gmail.com" className="text-blue-500 hover:text-blue-600">specialnosvidetelstvanesofia@gmail.com</a></div>
|
||||
|
||||
{/* <div className="mt-6">
|
||||
<h3 className="text-lg font-semibold text-gray-800 mb-3">Социални мрежи</h3>
|
||||
<div className="flex space-x-4">
|
||||
<a href="#" className="text-blue-500 hover:text-blue-600">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M24 4.557a9.83 9.83 0 0 1-2.828.775 4.932 4.932 0 0 0 2.165-2.723c-.951.564-2.005.974-3.127 1.195a4.916 4.916 0 0 0-8.384 4.482C7.69 7.88 4.067 5.794 1.64 2.905a4.822 4.822 0 0 0-.666 2.475c0 1.706.869 3.213 2.188 4.096a4.904 4.904 0 0 1-2.228-.616v.06a4.923 4.923 0 0 0 3.946 4.827 4.996 4.996 0 0 1-2.212.083 4.937 4.937 0 0 0 4.604 3.417A9.867 9.867 0 0 1 0 19.54a13.995 13.995 0 0 0 7.548 2.209c9.142 0 14.307-7.721 13.995-14.646A9.936 9.936 0 0 0 24 4.557z" /></svg>
|
||||
</a>
|
||||
<a href="#" className="text-blue-500 hover:text-blue-600">
|
||||
</a>
|
||||
</div>
|
||||
</div > */
|
||||
}
|
||||
</div >
|
||||
</Layout >
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactsPage;
|
200
pages/dash.tsx
Normal file
200
pages/dash.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import { useSession } from "next-auth/react"
|
||||
import Layout from "../components/layout"
|
||||
import AvCalendar from '../components/calendar/avcalendar';
|
||||
import { getSession } from "next-auth/react";
|
||||
import common from '../src/helpers/common';
|
||||
import { Availability } from "@prisma/client";
|
||||
import ProtectedRoute, { serverSideAuth } from "../components/protectedRoute";
|
||||
import { UserRole } from "@prisma/client";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axiosInstance from '../src/axiosSecure';
|
||||
|
||||
import { authOptions } from './api/auth/[...nextauth]'
|
||||
import { getServerSession } from "next-auth/next"
|
||||
|
||||
import PublisherSearchBox from '../components/publisher/PublisherSearchBox';
|
||||
import PublisherInlineForm from '../components/publisher/PublisherInlineForm';
|
||||
|
||||
|
||||
interface IProps {
|
||||
initialItems: Availability[];
|
||||
initialUserId: string;
|
||||
}
|
||||
export default function IndexPage({ initialItems, initialUserId }: IProps) {
|
||||
const { data: session } = useSession();
|
||||
const [userName, setUserName] = useState(session?.user?.name);
|
||||
const [userId, setUserId] = useState(initialUserId);
|
||||
const [events, setEvents] = useState(initialItems?.map(item => ({
|
||||
...item,
|
||||
title: item.name,
|
||||
date: new Date(item.startTime),
|
||||
startTime: new Date(item.startTime),
|
||||
endTime: new Date(item.endTime),
|
||||
publisherId: item.publisherId,
|
||||
})));
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
setUserName(session.user.name);
|
||||
setUserId(session.user.id);
|
||||
//handleUserSelection({ id: session.user.id, firstName: session.user.name, lastName: '' });
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const handleUserSelection = async (publisher) => {
|
||||
if (!publisher || publisher.id === undefined) return;
|
||||
console.log("selecting publisher", publisher.id);
|
||||
setUserName(publisher.firstName + " " + publisher.lastName);
|
||||
setUserId(publisher.id);
|
||||
|
||||
try {
|
||||
let events = await axiosInstance.get(`/api/?action=getCalendarEvents&publisherId=${publisher.id}`);
|
||||
setEvents(events.data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching publisher info:", error);
|
||||
// Handle the error appropriately
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<ProtectedRoute deniedMessage="">
|
||||
<h1 className="pt-2 pb-1 text-xl font-bold text-center">Графика на {userName}</h1>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
|
||||
<PublisherSearchBox selectedId={userId} infoText="" onChange={handleUserSelection} />
|
||||
</ProtectedRoute>
|
||||
<div className="flex flex-row md:flex-row mt-4 space-y-4 md:space-y-0 md:space-x-4 h-[calc(100vh-10rem)]">
|
||||
<div className="flex-1">
|
||||
<div className="text-center font-bold pb-3">
|
||||
<PublisherInlineForm publisherId={userId} />
|
||||
</div>
|
||||
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} />
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async function getAvailabilities(userId) {
|
||||
const prismaClient = common.getPrismaClient();
|
||||
const items = await prismaClient.availability.findMany({
|
||||
where: {
|
||||
publisherId: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
isactive: true,
|
||||
isFromPreviousAssignment: true,
|
||||
dayofweek: true,
|
||||
dayOfMonth: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
repeatWeekly: true,
|
||||
endDate: true,
|
||||
publisher: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// Convert Date objects to ISO strings
|
||||
const serializableItems = items.map(item => ({
|
||||
...item,
|
||||
startTime: item.startTime.toISOString(),
|
||||
endTime: item.endTime.toISOString(),
|
||||
name: common.getTimeFomatted(item.startTime) + "-" + common.getTimeFomatted(item.endTime),
|
||||
//endDate can be null
|
||||
endDate: item.endDate ? item.endDate.toISOString() : null,
|
||||
type: 'availability',
|
||||
// Convert other Date fields similarly if they exist
|
||||
}));
|
||||
|
||||
/*model Assignment {
|
||||
id Int @id @default(autoincrement())
|
||||
shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade)
|
||||
shiftId Int
|
||||
publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade)
|
||||
publisherId String
|
||||
isactive Boolean @default(true)
|
||||
isConfirmed Boolean @default(false)
|
||||
isWithTransport Boolean @default(false)
|
||||
Report Report[]
|
||||
}*/
|
||||
//get assignments for this user
|
||||
const assignments = await prismaClient.assignment.findMany({
|
||||
where: {
|
||||
publisherId: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
isTentative: true,
|
||||
isConfirmed: true,
|
||||
isWithTransport: true,
|
||||
shift: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
//select all assigned publishers names as name - comma separated
|
||||
assignments: {
|
||||
select: {
|
||||
publisher: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const serializableAssignments = assignments.map(item => ({
|
||||
...item,
|
||||
startTime: item.shift.startTime.toISOString(),
|
||||
endTime: item.shift.endTime.toISOString(),
|
||||
// name: item.shift.publishers.map(p => p.firstName + " " + p.lastName).join(", "),
|
||||
//name: item.shift.assignments.map(a => a.publisher.firstName[0] + " " + a.publisher.lastName).join(", "),
|
||||
name: common.getTimeFomatted(new Date(item.shift.startTime)) + "-" + common.getTimeFomatted(new Date(item.shift.endTime)),
|
||||
type: 'assignment',
|
||||
//delete shift object
|
||||
shift: null,
|
||||
publisher: { id: userId }
|
||||
}));
|
||||
|
||||
serializableItems.push(...serializableAssignments);
|
||||
|
||||
return serializableItems;
|
||||
|
||||
}
|
||||
export const getServerSideProps = async (context) => {
|
||||
const auth = await serverSideAuth({
|
||||
req: context.req,
|
||||
allowedRoles: [/* ...allowed roles... */]
|
||||
});
|
||||
const session = await getSession(context);
|
||||
const sessionServer = await getServerSession(context.req, context.res, authOptions)
|
||||
|
||||
if (!session) { return { props: {} } }
|
||||
const role = session?.user.role;
|
||||
console.log("server role: " + role);
|
||||
const userId = session?.user.id;
|
||||
|
||||
var items = await getAvailabilities(session.user.id);
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialItems: items,
|
||||
userId: session?.user.id,
|
||||
},
|
||||
};
|
||||
}
|
19
pages/drive.tsx
Normal file
19
pages/drive.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import Layout from "../components/layout"
|
||||
import GoogleDriveFolderPreview from "../components/board/GoogleDriveFolderPreview"
|
||||
|
||||
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>NextAuth.js Example</h1>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use{" "}
|
||||
<a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
<GoogleDriveFolderPreview folderId={"1DADj8OUWz2sMEmiW3sDETuihJvFS5x_p"}>
|
||||
{/* https://drive.google.com/drive/folders/1DADj8OUWz2sMEmiW3sDETuihJvFS5x_p?usp=sharing */}
|
||||
|
||||
</GoogleDriveFolderPreview>
|
||||
</Layout>
|
||||
)
|
||||
}
|
17
pages/examples/admin.tsx
Normal file
17
pages/examples/admin.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import Layout from "../../components/layout"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>This page is protected by Middleware</h1>
|
||||
<p>Only admin users can see this page.</p>
|
||||
<p>
|
||||
To learn more about the NextAuth middleware see
|
||||
<a href="https://docs-git-misc-docs-nextauthjs.vercel.app/configuration/nextjs#middleware">
|
||||
the docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
19
pages/examples/api-example.tsx
Normal file
19
pages/examples/api-example.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import Layout from "../../components/layout"
|
||||
|
||||
export default function ApiExamplePage() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>API Example</h1>
|
||||
<p>The examples below show responses from the example API endpoints.</p>
|
||||
<p>
|
||||
<em>You must be signed in to see responses.</em>
|
||||
</p>
|
||||
<h2>Session</h2>
|
||||
<p>/api/examples/session</p>
|
||||
<iframe src="/api/examples/session" />
|
||||
<h2>JSON Web Token</h2>
|
||||
<p>/api/examples/jwt</p>
|
||||
<iframe src="/api/examples/jwt" />
|
||||
</Layout>
|
||||
)
|
||||
}
|
27
pages/examples/client.tsx
Normal file
27
pages/examples/client.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import Layout from "../../components/layout"
|
||||
|
||||
export default function ClientPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Client Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the <strong>useSession()</strong> React Hook in the{" "}
|
||||
<strong><Header/></strong> component.
|
||||
</p>
|
||||
<p>
|
||||
The <strong>useSession()</strong> React Hook is easy to use and allows
|
||||
pages to render very quickly.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of this approach is that session state is shared between
|
||||
pages by using the <strong>Provider</strong> in <strong>_app.js</strong>{" "}
|
||||
so that navigation between pages using <strong>useSession()</strong> is
|
||||
very fast.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of <strong>useSession()</strong> is that it requires
|
||||
client side JavaScript.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
32
pages/examples/policy.tsx
Normal file
32
pages/examples/policy.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import Layout from "../../components/layout"
|
||||
|
||||
export default function PolicyPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use{" "}
|
||||
<a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
<h2>Terms of Service</h2>
|
||||
<p>
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
</p>
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>
|
||||
This site uses JSON Web Tokens and an in-memory database which resets
|
||||
every ~2 hours.
|
||||
</p>
|
||||
<p>
|
||||
Data provided to this site is exclusively used to support signing in and
|
||||
is not passed to any third party services, other than via SMTP or OAuth
|
||||
for the purposes of authentication.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
41
pages/examples/protected.tsx
Normal file
41
pages/examples/protected.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useEffect, useState } from "react"
|
||||
import AccessDenied from "../../components/access-denied"
|
||||
import Layout from "../../components/layout"
|
||||
|
||||
export default function ProtectedPage() {
|
||||
const { data: session } = useSession()
|
||||
const [content, setContent] = useState()
|
||||
|
||||
// Fetch content from protected route
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const res = await fetch("/api/examples/protected")
|
||||
const json = await res.json()
|
||||
if (json.content) {
|
||||
setContent(json.content)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [session])
|
||||
|
||||
|
||||
// If no session exists, display access denied message
|
||||
if (!session) {
|
||||
return (
|
||||
<Layout>
|
||||
<AccessDenied />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p>
|
||||
<strong>{content ?? "\u00a0"}</strong>
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
47
pages/examples/server.tsx
Normal file
47
pages/examples/server.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "../api/auth/[...nextauth]"
|
||||
import Layout from "../../components/layout"
|
||||
|
||||
import type { GetServerSidePropsContext } from "next"
|
||||
import type { Session } from "next-auth"
|
||||
|
||||
export default function ServerSidePage({ session }: { session: Session }) {
|
||||
// As this page uses Server Side Rendering, the `session` will be already
|
||||
// populated on render without needing to go through a loading stage.
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Server Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the <strong>getServerSession()</strong> method
|
||||
in <strong>getServerSideProps()</strong>.
|
||||
</p>
|
||||
<p>
|
||||
Using <strong>getServerSession()</strong> in{" "}
|
||||
<strong>getServerSideProps()</strong> is the recommended approach if you
|
||||
need to support Server Side Rendering with authentication.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of Server Side Rendering is this page does not require
|
||||
client side JavaScript.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of Server Side Rendering is that this page is slower to
|
||||
render.
|
||||
</p>
|
||||
<pre>SESSION: {JSON.stringify(session, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// Export the `session` prop to use sessions with Server Side Rendering
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: {
|
||||
session: await getServerSession(
|
||||
context.req,
|
||||
context.res,
|
||||
authOptions
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
0
pages/favicon.ico
Normal file
0
pages/favicon.ico
Normal file
73
pages/guidelines.tsx
Normal file
73
pages/guidelines.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { useState } from 'react';
|
||||
import Layout from "../components/layout";
|
||||
|
||||
const PDFViewerPage = () => {
|
||||
const [language, setLanguage] = useState('bg'); // default language is Bulgarian
|
||||
|
||||
// Determine the PDF file based on the selected language
|
||||
const pdfFiles = {
|
||||
en: '/content/guidelines/S-148_E.pdf',
|
||||
bg: '/content/guidelines/S-148_BL.pdf',
|
||||
ru: '/content/guidelines/S-148_U.pdf',
|
||||
};
|
||||
const languages = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'bg', label: 'Български' },
|
||||
{ code: 'ru', label: 'Русский' },
|
||||
];
|
||||
|
||||
const pdfFile = pdfFiles[language];
|
||||
const toggleLanguage = () => {
|
||||
const languages = Object.keys(pdfFiles);
|
||||
const currentLangIndex = languages.indexOf(language);
|
||||
const nextLangIndex = (currentLangIndex + 1) % languages.length;
|
||||
setLanguage(languages[nextLangIndex]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1 className="text-3xl font-bold">Напътствия</h1>
|
||||
<div className="guidelines-section mb-5 p-4 bg-gray-100 rounded-lg">
|
||||
<h2 className="text-2xl font-semibold mb-3">Важни напътствия за службата</h2>
|
||||
<ol className="list-decimal list-inside">
|
||||
<li><strong>Щандове:</strong> Предлагаме следното:
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
<li>Да има известно разстояние между нас и щандовете. Целта е да оставим хората свободно да се доближат до количките и ако някой прояви интерес може да се приближим.</li>
|
||||
<li>Когато сме двама или трима може да стоим заедно. Ако сме четирима би било хубаво да се разделим по двама на количка и количките да са на известно разстояние една от друга.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Безопасност:</strong> Нека се страем зад нас винаги да има защита или препятствие за недобронамерени хора.</li>
|
||||
<li><strong>Плакати:</strong> Моля при придвижване на количките да слагате плакатите така, че илюстрацията да се вижда, когато калъфа е сложен. Целта е снимките да не се търкат в количката, защото се повреждат.</li>
|
||||
<li><strong>Литература:</strong> При проявен интерес на чужд език, използвайте списанията и трактатите на други езици в папките.</li>
|
||||
<li><strong>График:</strong> Моля да ни изпратите вашите предпочитания до 23-то число на месеца.<a href='https://docs.google.com/forms/d/e/1FAIpQLSdLpuTi0o9laOYmenGJetY6MJ-CmphD9xFS5ZVz_7ipOxNGUQ/viewform?usp=sf_link'> Линк към анкетата</a></li>
|
||||
<li><strong>Случки:</strong> Ако сте имали хубави случки на количките, моля пишете ни.</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
||||
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
|
||||
<div className="my-4 flex items-center">
|
||||
{languages.map((lang, index) => (
|
||||
<React.Fragment key={lang.code}>
|
||||
{index > 0 && <div className="bg-gray-400 w-px h-6"></div>} {/* Vertical line separator */}
|
||||
<button
|
||||
onClick={() => setLanguage(lang.code)}
|
||||
className={`text-lg py-2 px-4 ${language === lang.code ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800 hover:bg-blue-500 hover:text-white'} ${index === 0 ? 'rounded-l-full' : index === languages.length - 1 ? 'rounded-r-full' : ''}`}
|
||||
>
|
||||
{lang.label}
|
||||
</button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ width: 'calc(100% - 1rem)', height: '100%', margin: '0 0' }}> {/* Center the PDF with 2rem margin */}
|
||||
<object data={pdfFile} type="application/pdf" style={{ width: '100%', height: '100%' }}>
|
||||
<p>Your browser does not support PDFs. Please download the PDF to view it: <a href={pdfFile}>Свали PDF файла</a>.</p>
|
||||
<p>Вашият браузър не поддържа PDFs файлове. Моля свалете файла за да го разгледате: <a href={pdfFile}>Свали PDF файла</a>.</p>
|
||||
</object>
|
||||
</div>
|
||||
</div>
|
||||
</Layout >
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFViewerPage;
|
49
pages/index.tsx
Normal file
49
pages/index.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import Layout from "../components/layout"
|
||||
import GoogleDriveFolderPreview from "../components/board/GoogleDriveFolderPreview"
|
||||
import AvCalendar from '../components/calendar/avcalendar';
|
||||
import { useEffect } from 'react';
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { useSession, signIn } from "next-auth/react";
|
||||
import { serverSideAuth } from '../components/protectedRoute'; // Adjust the path as needed
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function IndexPage() {
|
||||
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Redirect to /dash if user is logged in
|
||||
if (session) {
|
||||
router.push('/dash');
|
||||
}
|
||||
}, [session, router]);
|
||||
|
||||
if (status === "loading") {
|
||||
return <p>Loading...</p>; // Or any other loading state
|
||||
}
|
||||
|
||||
if (session) {
|
||||
return null; // Or a loading indicator until the redirect happens
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Специално Свидетелстване София</h1>
|
||||
<p className="mb-6">
|
||||
Моля влезте в профила си.
|
||||
</p>
|
||||
|
||||
{/* If GoogleDriveFolderPreview is a custom component, ensure it accepts and applies className props */}
|
||||
<GoogleDriveFolderPreview folderId="1DADj8OUWz2sMEmiW3sDETuihJvFS5x_p" className="w-full max-w-lg">
|
||||
{/* Content of the Google Drive Folder Preview */}
|
||||
</GoogleDriveFolderPreview>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
15
pages/privacy.tsx
Normal file
15
pages/privacy.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
// pages/PrivacyPolicyPage.jsx
|
||||
import React from 'react';
|
||||
import PrivacyPolicyContainer from '../components/privacy-policy/PrivacyPolicyContainer';
|
||||
import Layout from "../components/layout"
|
||||
|
||||
function PrivacyPolicyPage() {
|
||||
return (<Layout>
|
||||
<div>
|
||||
<PrivacyPolicyContainer />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default PrivacyPolicyPage;
|
Reference in New Issue
Block a user