initial commit - code moved to separate repo
This commit is contained in:
42
src/axiosSecure.js
Normal file
42
src/axiosSecure.js
Normal file
@ -0,0 +1,42 @@
|
||||
import axios from "axios";
|
||||
import common from "../src/helpers/common";
|
||||
import { applyAuthTokenInterceptor } from 'axios-jwt';
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: common.getBaseUrl(),
|
||||
withCredentials: true,
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
});
|
||||
|
||||
// 2. Define token refresh function.
|
||||
const requestRefresh = (refresh) => {
|
||||
// Notice that this is the global axios instance, not the axiosInstance! <-- important
|
||||
return axios.post(`${common.getBaseUrl()}/auth/refresh_token`, { refresh })
|
||||
.then(response => response.data.access_token)
|
||||
};
|
||||
|
||||
// 3. Apply interceptor
|
||||
applyAuthTokenInterceptor(axiosInstance, { requestRefresh }); // Notice that this uses the axiosInstance instance. <-- important
|
||||
|
||||
// 4. Logging in
|
||||
const login = async (params) => {
|
||||
const response = await axiosInstance.post('/api/auth/signin', params)
|
||||
|
||||
// save tokens to storage
|
||||
setAuthTokens({
|
||||
accessToken: response.data.access_token,
|
||||
refreshToken: response.data.refresh_token
|
||||
})
|
||||
}
|
||||
|
||||
// 5. Logging out
|
||||
const logout = () => clearAuthTokens()
|
||||
|
||||
// Now just make all requests using your axiosInstance instance
|
||||
// axiosInstance.get('/api/data/locations').then(response => {
|
||||
// console.log(response.data) // <-- this will be the response from your API
|
||||
// })
|
||||
|
||||
export default axiosInstance;
|
40
src/axiosServer.js
Normal file
40
src/axiosServer.js
Normal file
@ -0,0 +1,40 @@
|
||||
import axios from 'axios';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import common from '../src/helpers/common';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { authOptions } from '../pages/api/auth/[...nextauth]';
|
||||
import { red } from '@mui/material/colors';
|
||||
|
||||
const axiosServer = async (context) => {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const session = await getServerSession(context.req || req, context.res || res, authOptions);
|
||||
context.req.session = session;
|
||||
|
||||
if (session) {
|
||||
const secret = process.env.NEXTAUTH_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error('NEXTAUTH_SECRET is not set');
|
||||
}
|
||||
|
||||
const token = jwt.sign({ ...session }, secret, { expiresIn: '1h' });
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
//headers['X-From-Server'] = token; // Note: Using the entire token as a header value might not be intended
|
||||
|
||||
return axios.create({
|
||||
baseURL: common.getBaseUrl(),
|
||||
withCredentials: true,
|
||||
headers: headers,
|
||||
});
|
||||
}
|
||||
else {
|
||||
//redirect to next-auth login page
|
||||
context.res.writeHead(302, { Location: '/api/auth/signin' });
|
||||
context.res.end();
|
||||
return { props: {} };
|
||||
}
|
||||
};
|
||||
|
||||
export default axiosServer;
|
480
src/helpers/calendar.js
Normal file
480
src/helpers/calendar.js
Normal file
@ -0,0 +1,480 @@
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
const process = require("process");
|
||||
const { google } = require("googleapis");
|
||||
const { OAuth2Client } = require("google-auth-library");
|
||||
const http = require("http");
|
||||
const url = require("url");
|
||||
|
||||
// const destroyer = require('server-destroy');
|
||||
|
||||
const CON = require("./const");
|
||||
|
||||
const TOKEN_PATH = path.join(process.cwd(), "content/token.json");
|
||||
const CREDENTIALS_PATH = path.join(
|
||||
process.cwd(),
|
||||
"content/client_secret_926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com.json"
|
||||
);
|
||||
|
||||
//generates iCalendar file for a single shift
|
||||
GenerateICS = function (shifts) {
|
||||
// https://stackoverflow.com/questions/3665115/create-a-file-in-memory-for-user-to-download-not-through-server
|
||||
|
||||
var content = "BEGIN:VCALENDAR\n";
|
||||
content += "VERSION:2.0\n";
|
||||
content += "PRODID:-//JW Cart//Shift Info//EN\n";
|
||||
content += "CALSCALE:GREGORIAN\n";
|
||||
content += "METHOD:PUBLISH\n";
|
||||
content += "X-WR-CALNAME:Колички София-Изток\n";
|
||||
content += "X-WR-TIMEZONE:Europe/Sofia\n";
|
||||
content += "BEGIN:VTIMEZONE\n";
|
||||
content += "TZID:Europe/Sofia\n";
|
||||
content += "BEGIN:DAYLIGHT\n";
|
||||
content += "TZOFFSETFROM:+0200\n";
|
||||
content += "TZOFFSETTO:+0300\n";
|
||||
content += "TZNAME:EEST\n";
|
||||
content += "DTSTART:19700329T020000\n";
|
||||
content += "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\n";
|
||||
content += "END:DAYLIGHT\n";
|
||||
content += "BEGIN:STANDARD\n";
|
||||
content += "TZOFFSETFROM:+0300\n";
|
||||
content += "TZOFFSETTO:+0200\n";
|
||||
content += "TZNAME:EET\n";
|
||||
content += "DTSTART:19701025T030000\n";
|
||||
content += "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\n";
|
||||
content += "END:STANDARD\n";
|
||||
content += "END:VTIMEZONE\n";
|
||||
content += shifts?.map((shift) => {
|
||||
var content = "BEGIN:VEVENT\n";
|
||||
content += "DTSTAMP:" + new Date().getTime() + "\n";
|
||||
content += "UID:" + shift.id + "\n";
|
||||
content +=
|
||||
"DTSTART;TZID=Europe/Sofia:" +
|
||||
CON.GetDateTimeShort(shift.startTime) +
|
||||
"\n";
|
||||
content +=
|
||||
"DTEND;TZID=Europe/Sofia:" +
|
||||
CON.GetDateTimeShort(shift.endTime.getTime()) +
|
||||
"\n";
|
||||
content += "LOCATION:" + shift.cartEvent.location.name + "\n";
|
||||
// content += "SUMMARY:" + shift.cartEvent.location.name + "\n";
|
||||
|
||||
// Adding a VALARM component for a reminder 1 day before the event
|
||||
content += "BEGIN:VALARM\n";
|
||||
content += "TRIGGER:-P1D\n"; // Trigger alarm 1 day before the event
|
||||
content += "ACTION:DISPLAY\n";
|
||||
content += "DESCRIPTION:Reminder\n";
|
||||
content += "END:VALARM\n";
|
||||
content += "END:VEVENT";
|
||||
return content;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
content += "END:VCALENDAR\n";
|
||||
//encode content in base64 to be send as attachment
|
||||
return Buffer.from(content).toString("base64");
|
||||
};
|
||||
|
||||
// /**
|
||||
// * Reads previously authorized credentials from the save file.
|
||||
// *
|
||||
// * @return {Promise<OAuth2Client|null>}
|
||||
// */
|
||||
// loadSavedCredentialsIfExist = async function loadSavedCredentialsIfExist() {
|
||||
// try {
|
||||
// const content = await fs.readFile(TOKEN_PATH);
|
||||
// const credentials = JSON.parse(content);
|
||||
// return google.auth.fromJSON(credentials);
|
||||
// } catch (err) {
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Serializes credentials to a file compatible with GoogleAUth.fromJSON.
|
||||
// *
|
||||
// * @param {OAuth2Client} client
|
||||
// * @return {Promise<void>}
|
||||
// */
|
||||
// saveCredentials = async function saveCredentials(client) {
|
||||
// const content = await fs.readFile(CREDENTIALS_PATH);
|
||||
// const keys = JSON.parse(content);
|
||||
// const key = keys.installed || keys.web;
|
||||
// const payload = JSON.stringify({
|
||||
// type: 'authorized_user',
|
||||
// client_id: key.client_id,
|
||||
// client_secret: key.client_secret,
|
||||
// refresh_token: client.credentials.refresh_token,
|
||||
// });
|
||||
// await fs.writeFile(TOKEN_PATH, payload);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Load or request or authorization to call APIs.
|
||||
// *
|
||||
// */
|
||||
// authorize = async function authorize() {
|
||||
// let client = await exports.loadSavedCredentialsIfExist();
|
||||
// if (client) {
|
||||
// return client;
|
||||
// }
|
||||
// client = await exports.authenticate({
|
||||
// scopes: SCOPES,
|
||||
// keyfilePath: CREDENTIALS_PATH,
|
||||
// });
|
||||
// if (client.credentials) {
|
||||
// await saveCredentials(client);
|
||||
// }
|
||||
// return client;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Lists the next 10 events on the user's primary calendar.
|
||||
// * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
|
||||
// */
|
||||
// listEvents = async function listEvents(auth) {
|
||||
// const calendar = google.calendar({ version: 'v3', auth });
|
||||
// const res = await calendar.events.list({
|
||||
// calendarId: 'primary',
|
||||
// timeMin: new Date().toISOString(),
|
||||
// maxResults: 10,
|
||||
// singleEvents: true,
|
||||
// orderBy: 'startTime',
|
||||
// });
|
||||
// const events = res.data.items;
|
||||
// if (!events || events.length === 0) {
|
||||
// console.log('No upcoming events found.');
|
||||
// return;
|
||||
// }
|
||||
// console.log('Upcoming 10 events:');
|
||||
// events.map((event, i) => {
|
||||
// const start = event.start.dateTime || event.start.date;
|
||||
// console.log(`${start} - ${event.summary}`);
|
||||
// });
|
||||
// }
|
||||
// //usage
|
||||
// GetEvents = async function GetEvents(dateTime = new Date()) {
|
||||
// var events = exports.authorize().then(exports.listEvents).catch(console.error);
|
||||
// }
|
||||
|
||||
// // const calendar = google.calendar({ version: 'v3', auth });
|
||||
|
||||
// const eventTest = {
|
||||
// summary: 'Test Event',
|
||||
// start: {
|
||||
// dateTime: '2023-01-01T09:00:00-07:00',
|
||||
// timeZone: 'Europe/Sofia',
|
||||
// },
|
||||
// end: {
|
||||
// dateTime: '2023-01-01T17:00:00-07:00',
|
||||
// timeZone: 'Europe/Sofia',
|
||||
// },
|
||||
// };
|
||||
|
||||
// const TOKEN_PATH = path.join(process.cwd(), 'content/token.json');
|
||||
// const CREDENTIALS_PATH = path.join(process.cwd(), 'content/client_secret_926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com.json');
|
||||
|
||||
createEvent = async function createEvent(id) {
|
||||
// var eventTest2 = {
|
||||
// 'summary': 'Google I/O 2015',
|
||||
// 'location': '800 Howard St., San Francisco, CA 94103',
|
||||
// 'description': 'A chance to hear more about Google\'s developer products.',
|
||||
// 'start': {
|
||||
// 'dateTime': '2015-05-28T09:00:00-07:00',
|
||||
// 'timeZone': 'America/Los_Angeles',
|
||||
// },
|
||||
// 'end': {
|
||||
// 'dateTime': '2015-05-28T17:00:00-07:00',
|
||||
// 'timeZone': 'America/Los_Angeles',
|
||||
// },
|
||||
// 'recurrence': [
|
||||
// 'RRULE:FREQ=DAILY;COUNT=2'
|
||||
// ],
|
||||
// 'attendees': [
|
||||
// { 'email': '' },
|
||||
|
||||
// ],
|
||||
// 'reminders': {
|
||||
// 'useDefault': false,
|
||||
// 'overrides': [
|
||||
// { 'method': 'email', 'minutes': 24 * 60 },
|
||||
// { 'method': 'popup', 'minutes': 10 },
|
||||
// ],
|
||||
// },
|
||||
// };
|
||||
// var errors;
|
||||
// var auth = await exports.authorize().then((auth) => {
|
||||
|
||||
// const calendar = google.calendar({ version: 'v3', auth });
|
||||
// const res = calendar.events.insert({
|
||||
// calendarId: 'primary',
|
||||
// resource: eventTest,
|
||||
|
||||
// }, (err, event) => {
|
||||
// if (err) {
|
||||
// console.log(`There was an error creating the event: ${err}`);
|
||||
// return;
|
||||
// }
|
||||
// console.log(`Event created: ${event.data.htmlLink}`);
|
||||
// });
|
||||
// }).catch(console.error).then((err) => {
|
||||
// errors = err;
|
||||
// });
|
||||
// return errors;
|
||||
// }
|
||||
|
||||
// authorizeNew = async function authorizeNew() {
|
||||
// let client = await loadSavedCredentialsIfExist();
|
||||
// if (client) {
|
||||
// return client;
|
||||
// }
|
||||
// // Set up the Google Calendar API client
|
||||
// const calendar = google.calendar({ version: 'v3', auth });
|
||||
|
||||
// // Load the client secrets from a JSON file
|
||||
|
||||
fs.readFile("./path/to/client_secret.json", (err, content) => {
|
||||
if (err) return console.error("Error loading client secret file:", err);
|
||||
|
||||
// Authorize a client with the loaded credentials
|
||||
authorizeOA(JSON.parse(content), calendar.calendarList.list);
|
||||
});
|
||||
if (client.credentials) {
|
||||
await saveCredentials(client);
|
||||
}
|
||||
return client;
|
||||
};
|
||||
|
||||
// var googletoken = axiosInstance.get("/content/google_token.json");
|
||||
// console.log("googletoken: " + googletoken);
|
||||
// if (!googletoken) {
|
||||
// toast.error("Google token not found. redirecting to google login");
|
||||
// const { data, error } = axiosInstance.get('/content/client_secret_926212607479-d3m8hm8f8esp3rf1639prskn445sa01v.apps.googleusercontent.com.json',
|
||||
// { headers: { 'Content-Type': 'application/json' } });
|
||||
// console.log("data: " + JSON.stringify(data));
|
||||
// console.log("error: " + JSON.stringify(error));
|
||||
// var secrets = data;
|
||||
// const { google } = await import('googleapis');
|
||||
// const { OAuth2 } = google.auth;
|
||||
// const oAuth2Client = new OAuth2(
|
||||
// secrets.web.client_id,
|
||||
// secrets.web.client_secret,
|
||||
// secrets.web.redirect_uris[0]
|
||||
// );
|
||||
// const authorizationUrl = oauth2Client.generateAuthUrl({
|
||||
// // 'online' (default) or 'offline' (gets refresh_token)
|
||||
// access_type: 'offline',
|
||||
// /** Pass in the scopes array defined above.
|
||||
// * Alternatively, if only one scope is needed, you can pass a scope URL as a string */
|
||||
// scope: scopes,
|
||||
// // Enable incremental authorization. Recommended as a best practice.
|
||||
// include_granted_scopes: true
|
||||
// });
|
||||
// return { redirect: authorizationUrl };
|
||||
|
||||
/**
|
||||
* Create an OAuth2 client with the given credentials, and then execute the
|
||||
* given callback function.
|
||||
* @param {Object} credentials The authorization client credentials.
|
||||
* @param {function} callback The callback to call with the authorized client.
|
||||
*/
|
||||
function authorizeOA(credentials, callback) {
|
||||
const { client_secret, client_id, redirect_uris } =
|
||||
credentials.installed || credentials.web;
|
||||
const oAuth2Client = new OAuth2Client(
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uris[0]
|
||||
);
|
||||
|
||||
// Check if we have previously stored a token
|
||||
fs.readFile(TOKEN_PATH, (err, token) => {
|
||||
if (err) return getAccessToken(oAuth2Client, callback);
|
||||
oAuth2Client.setCredentials(JSON.parse(token));
|
||||
callback(oAuth2Client);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and store new token after prompting for user authorization, and then
|
||||
* execute the given callback with the authorized OAuth2 client.
|
||||
* @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
|
||||
* @param {getEventsCallback} callback The callback for the authorized client.
|
||||
*/
|
||||
function getAccessToken(oAuth2Client, callback) {
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: ["https://www.googleapis.com/auth/calendar"],
|
||||
});
|
||||
console.log("Authorize this app by visiting this url:", authUrl);
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
rl.question("Enter the code from that page here: ", (code) => {
|
||||
rl.close();
|
||||
oAuth2Client.getToken(code, (err, token) => {
|
||||
if (err) return console.error("Error retrieving access token", err);
|
||||
oAuth2Client.setCredentials(token);
|
||||
// Store the token to disk for later program executions
|
||||
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
|
||||
if (err) return console.error(err);
|
||||
console.log("Token stored to", TOKEN_PATH);
|
||||
});
|
||||
callback(oAuth2Client);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// // /**
|
||||
// // * Lists the next 10 events on the user's primary calendar.
|
||||
// // * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
|
||||
// // * @param {getEventsCallback} callback The callback for the authorized client.
|
||||
// // *
|
||||
// // */
|
||||
// // function listEvents(auth) {
|
||||
// // const calendar = google.calendar({ version: 'v3', auth });
|
||||
// // calendar.events.list({
|
||||
// // calendarId: 'primary',
|
||||
// // timeMin: (new Date()).toISOString(),
|
||||
// // maxResults: 10,
|
||||
// // singleEvents: true,
|
||||
// // orderBy: 'startTime',
|
||||
// // }, (err, res) => {
|
||||
// // if (err) return console.error('The API returned an error: ' + err);
|
||||
// // const events = res.data.items;
|
||||
// // if (events.length) {
|
||||
// // console.log('Upcoming 10 events:');
|
||||
// // events.map((event, i) => {
|
||||
// // const start = event.start.dateTime || event.start.date;
|
||||
// // console.log(`${start} - ${event.summary}`);
|
||||
// // });
|
||||
// // } else {
|
||||
// // console.log('No upcoming events found.');
|
||||
// // }
|
||||
// // });
|
||||
// // }
|
||||
// // [END calendar_quickstart]
|
||||
// module.exports = {
|
||||
// GenerateICS,
|
||||
// createEvent,
|
||||
// authorize,
|
||||
// TOKEN_PATH,
|
||||
// CREDENTIALS_PATH
|
||||
// }
|
||||
|
||||
async function importModules() {
|
||||
const openModule = await import('open');
|
||||
const getPortModule = await import('get-port');
|
||||
return {
|
||||
open: openModule.default,
|
||||
getPort: getPortModule.default
|
||||
};
|
||||
}
|
||||
|
||||
createEvent = async (event) => {
|
||||
|
||||
const { open, getPort } = await importModules();
|
||||
|
||||
event = event || {
|
||||
summary: "Test Event",
|
||||
location: "Online",
|
||||
start: {
|
||||
dateTime: "2022-12-19T09:00:00Z",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
end: {
|
||||
dateTime: "2022-12-19T10:00:00Z",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const fc = await fs.readFile(CREDENTIALS_PATH);
|
||||
// const auth = new google.auth.fromClientSecret(fc);
|
||||
|
||||
const secrets = JSON.parse(fc);
|
||||
|
||||
const { google } = await import("googleapis");
|
||||
const { OAuth2 } = google.auth;
|
||||
var callbackUrl = secrets.web.redirect_uris[0];
|
||||
const freePort = await getPort();
|
||||
const url = new URL(callbackUrl);
|
||||
url.port = freePort;
|
||||
callbackUrl = url.toString();
|
||||
const oAuth2Client = new OAuth2(
|
||||
secrets.web.client_id,
|
||||
secrets.web.client_secret,
|
||||
callbackUrl
|
||||
);
|
||||
// Generate the url that will be used for the consent dialog.
|
||||
const authorizeUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: "https://www.googleapis.com/auth/userinfo.profile",
|
||||
});
|
||||
|
||||
let doAuth = await new Promise((resolve, reject) => {
|
||||
const server = http
|
||||
.createServer(async (req, res) => {
|
||||
try {
|
||||
if (req.url.indexOf("/oauth2callback") > -1) {
|
||||
// acquire the code from the querystring, and close the web server.
|
||||
const qs = new url.URL(req.url, "http://localhost:3003")
|
||||
.searchParams;
|
||||
const code = qs.get("code");
|
||||
console.log(`Code is ${code}`);
|
||||
res.end(
|
||||
"Authentication successful! Please return to the console."
|
||||
);
|
||||
server.destroy();
|
||||
|
||||
// Now that we have the code, use that to acquire tokens.
|
||||
const r = await oAuth2Client.getToken(code);
|
||||
// Make sure to set the credentials on the OAuth2 client.
|
||||
oAuth2Client.setCredentials(r.tokens);
|
||||
auth = oAuth2Client;
|
||||
console.info("Tokens acquired.");
|
||||
resolve(oAuth2Client);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
.listen(3003, () => {
|
||||
// open the browser to the authorize url to start the workflow
|
||||
open(authorizeUrl, { wait: false }).then((cp) => cp.unref());
|
||||
console.log(
|
||||
"Server started for auth flow to complete: " + authorizeUrl
|
||||
);
|
||||
});
|
||||
// destroyer(server);
|
||||
});
|
||||
|
||||
doAuth.then((auth) => {
|
||||
const calendar = google.calendar({ version: "v3", auth: auth });
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: auth,
|
||||
calendarId: "primary",
|
||||
resource: event,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.log(
|
||||
"There was an error contacting the Calendar service: " + err
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log("Event created: %s", event.htmlLink);
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.GenerateICS = GenerateICS;
|
||||
exports.createEvent = createEvent;
|
||||
|
||||
createEvent();
|
669
src/helpers/common.js
Normal file
669
src/helpers/common.js
Normal file
@ -0,0 +1,669 @@
|
||||
// import { format, startOfMonth, endOfMonth, eachDayOfInterval, startOfWeek, endOfWeek, isSameMonth } from 'date-fns';
|
||||
|
||||
|
||||
|
||||
const levenshtein = require('fastest-levenshtein');
|
||||
|
||||
const path = require("path");
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const DayOfWeek = require("@prisma/client").DayOfWeek;
|
||||
|
||||
const winston = require('winston');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info', // Set the default log level
|
||||
format: winston.format.json(), // Choose a log format
|
||||
transports: [
|
||||
// Define where logs should be output (e.g., to a file or the console)
|
||||
new winston.transports.Console(),
|
||||
],
|
||||
});
|
||||
|
||||
exports.logger = logger;
|
||||
|
||||
// import { DayOfWeek } from "@prisma/client";
|
||||
|
||||
//const UserRole = require("@prisma/client").UserRole; - does not allow usage of UserRole in code
|
||||
// import { UserRole } from '@prisma/client';
|
||||
|
||||
// const getConfig = require("next/config");
|
||||
// exports.nextconfig = getConfig();
|
||||
// //const { serverRuntimeConfig } = getConfig();
|
||||
|
||||
// const dotenv = require("dotenv");
|
||||
// dotenv.config();
|
||||
// // dotenv.config({ path: ".env.local" });
|
||||
|
||||
exports.isValidPhoneNumber = function (phone) {
|
||||
if (typeof phone !== 'string') {
|
||||
return false; // or handle as you see fit
|
||||
}
|
||||
// Remove spaces and dashes for validation
|
||||
const cleanedPhone = phone.replace(/\s|-/g, '');
|
||||
|
||||
// Check if the phone starts with '08' and has 10 digits
|
||||
if (cleanedPhone.startsWith('08') && cleanedPhone.length === 10) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the phone starts with '+359' and has the correct length
|
||||
if (cleanedPhone.startsWith('+359') && cleanedPhone.length === 12) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If neither condition is met, the phone number is invalid
|
||||
return false;
|
||||
}
|
||||
exports.getBaseUrl = function (relative = "") {
|
||||
const host = process.env.NEXT_PUBLIC_HOST || '127.0.0.1';
|
||||
const port = process.env.NEXT_PUBLIC_PORT ? `:${process.env.NEXT_PUBLIC_PORT}` : '';
|
||||
const protocol = process.env.NEXT_PUBLIC_PROTOCOL || "https"
|
||||
|
||||
//const url = `${protocol}://${host}${port}/${relative.replace(/^\/|\/$/g, '')}/`;
|
||||
const isRelativeEmpty = !relative || relative.trim() === '';
|
||||
const formattedRelative = !isRelativeEmpty ? '/' + relative.replace(/^\/|\/$/g, '') : '';
|
||||
const url = `${protocol}://${host}${port}${formattedRelative}`;
|
||||
|
||||
|
||||
logger.debug("NODE_ENV = ", process.env.NODE_ENV, "protocol:", protocol);
|
||||
logger.debug("getBaseURL = ", url);
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
let prisma;
|
||||
exports.getPrismaClient = function getPrismaClient() {
|
||||
if (!prisma) {
|
||||
logger.debug("getPrismaClient: process.env.DATABASE_URL = ", process.env.DATABASE_URL);
|
||||
prisma = new PrismaClient({
|
||||
// Optional: Enable logging
|
||||
// log: ['query', 'info', 'warn', 'error'],
|
||||
datasources: { db: { url: process.env.DATABASE_URL } },
|
||||
});
|
||||
}
|
||||
return prisma;
|
||||
}
|
||||
|
||||
|
||||
exports.removeAccentsAndSpecialCharacters = function (str) {
|
||||
return str.normalize("NFD") // split an accented letter in the base letter and the accent
|
||||
.replace(/[\u0300-\u036f]/g, "") // remove all previously split accents
|
||||
.replace(/[^a-zA-Z0-9]/g, ""); // remove all chars not letters and not digits
|
||||
}
|
||||
|
||||
Date.prototype.getDayEuropean = function () {
|
||||
const day = this.getDay();
|
||||
return (day === 0) ? 6 : day - 1; // Convert 0 (Sunday) to 6, and decrement other days by 1
|
||||
};
|
||||
|
||||
// Helper function to convert month name to 0-based index
|
||||
exports.getMonthIndex = function (monthName) {
|
||||
const monthNames = ["януари", "февруари", "март", "април", "май", "юни", "юли", "август", "септември", "октомври", "ноември", "декември"];
|
||||
return monthNames.indexOf(monthName.toLowerCase());
|
||||
};
|
||||
|
||||
exports.getMonthName = function (monthIndex) {
|
||||
const monthNames = ["януари", "февруари", "март", "април", "май", "юни", "юли", "август", "септември", "октомври", "ноември", "декември"];
|
||||
return monthNames[monthIndex];
|
||||
};
|
||||
|
||||
exports.getMonthNameEn = function (monthIndex) {
|
||||
const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
return monthNames[monthIndex];
|
||||
};
|
||||
|
||||
exports.dayOfWeekNames = ["понеделник", "вторник", "сряда", "четвъртък", "петък", "събота", "неделя"];
|
||||
exports.DaysOfWeekArray = [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday];
|
||||
|
||||
// Helper function to convert day of week name to 0-based index (Sunday = 0)
|
||||
exports.getDayOfWeekIndex = function (dayOfWeekName) {
|
||||
dayOfWeekName = dayOfWeekName.toLowerCase().trim();
|
||||
const abbreviatedDayOfWeekNames = ["пон", "вт", "ср", "четв", "пет", "съб", "нед"];
|
||||
const enDayOfWeekNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].map((d) => d.toLowerCase());
|
||||
|
||||
let index = exports.dayOfWeekNames.indexOf(dayOfWeekName);
|
||||
|
||||
// If not found in the full names, look in the abbreviated names
|
||||
if (index === -1) {
|
||||
index = abbreviatedDayOfWeekNames.indexOf(dayOfWeekName);
|
||||
}
|
||||
if (index === -1) {
|
||||
index = enDayOfWeekNames.indexOf(dayOfWeekName);
|
||||
}
|
||||
logger.debug("getDayOfWeekIndex: dayOfWeekName = ", dayOfWeekName, "index = ", index);
|
||||
return index;
|
||||
};
|
||||
|
||||
|
||||
exports.getDayOfWeekName = function (date) {
|
||||
const dayOfWeekIndex = date.getDayEuropean();
|
||||
return exports.dayOfWeekNames[dayOfWeekIndex];
|
||||
};
|
||||
|
||||
exports.getDayOfWeekNameEnEnum = function (date) {
|
||||
date = new Date(date);
|
||||
const dayOfWeekIndex = date.getDayEuropean();
|
||||
const dayOfWeekNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||
|
||||
//return enum instead of string
|
||||
// const dayOfWeekNames: Record<number, DayOfWeek> = {
|
||||
// 0: DayOfWeek.Monday,
|
||||
// 1: DayOfWeek.Tuesday,
|
||||
// 2: DayOfWeek.Wednesday,
|
||||
// 3: DayOfWeek.Thursday,
|
||||
// 4: DayOfWeek.Friday,
|
||||
// 5: DayOfWeek.Saturday,
|
||||
// 6: DayOfWeek.Sunday
|
||||
// };
|
||||
|
||||
return dayOfWeekNames[dayOfWeekIndex];
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
sets the date portion of the date object to the correct day of week, keeping the time portion
|
||||
*/
|
||||
exports.getDayOfWeekDate = function (dayOfWeekName, date = new Date()) {
|
||||
const targetDay = exports.DaysOfWeekArray.indexOf(dayOfWeekName); // 0 = Sunday, 1 = Monday, etc.
|
||||
if (targetDay === -1) {
|
||||
throw new Error('Invalid day of week name provided');
|
||||
}
|
||||
// Adjust currentDay to match our custom mapping
|
||||
let currentDay = date.getDayEuropean();
|
||||
const daysDifference = targetDay - currentDay;
|
||||
date.setDate(date.getDate() + daysDifference);
|
||||
return date;
|
||||
};
|
||||
//common.getWeekOfMonth(date)
|
||||
// exports.getWeekOfMonth = function (date) {
|
||||
// // Copy date so don't modify original
|
||||
// date = new Date(date);
|
||||
// // Adjust to Monday of this week
|
||||
// date.setDate(date.getDate() + 3 - (date.getDayEuropean() + 6) % 7);
|
||||
// // Return week number
|
||||
// const weekNumber = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000 / 7);
|
||||
// return weekNumber;
|
||||
// }
|
||||
|
||||
exports.getMonthDatesInfo = function (date) {
|
||||
// get first day of the month
|
||||
var firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
// get first day of next month
|
||||
var lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 1);
|
||||
// lastDay = new Date(lastDay.getTime() - 1);
|
||||
lastDay = new Date(lastDay.getTime() - 86400000); // 86400000 ms = 1 day
|
||||
|
||||
// // get the day of the week of the first day
|
||||
var firstDayOfWeek = firstDay.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
||||
// calculate the date of the first Monday
|
||||
var firstMonday = new Date(firstDay);
|
||||
if (firstDayOfWeek !== 1) { // Check if the first day is not already a Monday
|
||||
firstMonday.setDate(firstDay.getDate() + ((7 - firstDayOfWeek) % 7) + 1);
|
||||
}
|
||||
|
||||
// get last Monday
|
||||
var lastMonday = new Date(lastDay);
|
||||
if (lastDay.getDay() !== 0) { // Check if the last day is not already a Sunday
|
||||
lastMonday.setDate(lastDay.getDate() - ((lastDay.getDay() + 6) % 7));
|
||||
} else {
|
||||
lastMonday.setDate(lastDay.getDate() - 6);
|
||||
}
|
||||
|
||||
// get Sunday of the last week
|
||||
var lastSunday = new Date(lastMonday);
|
||||
lastSunday.setDate(lastMonday.getDate() + 6);
|
||||
var monthName = exports.getMonthName(date.getMonth());
|
||||
|
||||
|
||||
// var firstDayNextMonth = new Date(date.getFullYear(), date.getMonth() + 1, 1);
|
||||
// // Calculate the last Sunday of the current month
|
||||
// // .getDay() returns 0 for Sunday, 1 for Monday, ..., 6 for Saturday
|
||||
// var lastSunday = new Date(firstDayNextMonth);
|
||||
// lastSunday.setDate(firstDayNextMonth.getDate() - firstDayNextMonth.getDay());
|
||||
|
||||
//logger.debug("Last Sunday: ", lastSunday);
|
||||
|
||||
return {
|
||||
firstDay: firstDay,
|
||||
lastDay: lastDay,
|
||||
firstMonday: firstMonday,
|
||||
lastMonday: lastMonday,
|
||||
lastSunday: lastSunday,
|
||||
date: date,
|
||||
monthName: monthName,
|
||||
year: date.getFullYear(),
|
||||
nrOfWeeks: Math.ceil((lastMonday.getDate() - firstMonday.getDate()) / 7)
|
||||
};
|
||||
};
|
||||
|
||||
exports.getMonthlyScheduleRange = function (date) {
|
||||
let info = exports.getMonthDatesInfo(date);
|
||||
// get first day of the month
|
||||
var firstDay = info.firstDay;
|
||||
// get last sunday of the month
|
||||
var lastSunday = info.lastSunday;
|
||||
return {
|
||||
firstDay: firstDay,
|
||||
lastSunday: lastSunday
|
||||
};
|
||||
};
|
||||
|
||||
exports.getWeekNumber = function (date) {
|
||||
let info = exports.getMonthDatesInfo(date);
|
||||
|
||||
// If the date is before the first full week, return 0
|
||||
if (date < info.firstMonday) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate the week number based on the first full week
|
||||
return Math.ceil((date.getDate() - info.firstMonday.getDate() + 1) / 7);
|
||||
};
|
||||
|
||||
|
||||
|
||||
exports.getTimeRange = function (start, end) {
|
||||
start = new Date(start);
|
||||
end = new Date(end);
|
||||
const startTime = start.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Sofia' });
|
||||
const endTime = end.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Sofia' });
|
||||
return `${startTime}-${endTime}`;
|
||||
}
|
||||
|
||||
//get date in format сряда 1 юли 2020 г.
|
||||
exports.getDateFormated = function (date) {
|
||||
const dayOfWeekName = exports.getDayOfWeekName(date);
|
||||
const day = date.getDate();
|
||||
const monthName = exports.getMonthName(date.getMonth());
|
||||
const year = date.getFullYear();
|
||||
return `${dayOfWeekName} ${day} ${monthName} ${year} г.`;
|
||||
}
|
||||
|
||||
|
||||
exports.getTimeFomatted = function (date) {
|
||||
return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Sofia' });//timeZone: 'local'
|
||||
|
||||
}
|
||||
|
||||
/*Todo: remove:
|
||||
toISOString
|
||||
slice(0, 10)
|
||||
|
||||
getISODateFromDateTime > getISODateOnly
|
||||
|
||||
*/
|
||||
exports.toLocalISO = function (date) {
|
||||
// Adjust for timezone and get ISO string without 'Z'
|
||||
const offset = date.getTimezoneOffset() * 60000; // offset in milliseconds
|
||||
return new Date(date.getTime() - offset).toISOString().slice(0, -1);
|
||||
};
|
||||
|
||||
exports.getISODateOnly = function (date) {
|
||||
// Use toLocalISO and get the date part only
|
||||
return this.toLocalISO(date).split('T')[0];
|
||||
};
|
||||
|
||||
exports.getDateFromDateTime = function (day) {
|
||||
// Adjust for timezone and reset time to midnight
|
||||
var date = new Date(day);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
const offset = date.getTimezoneOffset() * 60000;
|
||||
return new Date(date.getTime() - offset);
|
||||
};
|
||||
|
||||
|
||||
// ###########################################
|
||||
// remove empty nodes from json
|
||||
|
||||
// Function to check if an object is empty or contains only empty nodes
|
||||
exports.isEmpty = function (obj) {
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === 'object') {
|
||||
if (!exports.isEmpty(obj[key])) {
|
||||
return false;
|
||||
}
|
||||
} else if (obj[key] !== '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Function to remove empty nodes from a JSON object
|
||||
exports.jsonRemoveEmptyNodes = function (obj) {
|
||||
const newObj = {};
|
||||
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === 'object') {
|
||||
const cleanedObj = exports.jsonRemoveEmptyNodes(obj[key]);
|
||||
if (!exports.isEmpty(cleanedObj)) {
|
||||
newObj[key] = cleanedObj;
|
||||
}
|
||||
} else if (obj[key] !== '') {
|
||||
newObj[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return newObj;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// #region string functions
|
||||
|
||||
//input:Светослав и Сириел Георгиеви
|
||||
//output: Светослав Георгиев, Сириел Георгиев
|
||||
//input: Игор и Галина Москвини
|
||||
//output: Игор Москвин, Галина Москвин
|
||||
exports.separateFamilyMemberNames = function (namesArray) {
|
||||
logger.debug("separateFamilyMemberNames: " + namesArray);
|
||||
const result = [];
|
||||
// itteraate over all names
|
||||
for (let i = 0; i < namesArray.trim().split(' '); i++) {
|
||||
const name = namesArray[i];
|
||||
|
||||
// if name contains " и " then split it into two names
|
||||
// else just add it to the result
|
||||
if (name.includes(" и ")) {
|
||||
const namePairs = name.split(" и ");
|
||||
const firstNames = namePairs.map((pair) => pair.split(" ")[0]);
|
||||
// last name is only one. and it is shared for both firstnames
|
||||
const lastName = namePairs[1].split(" ")[1];
|
||||
|
||||
for (let i = 0; i < namePairs.length; i++) {
|
||||
let firstName = firstNames[i];
|
||||
|
||||
if (i === namePairs.length - 1 && namePairs.length % 2 !== 0) {
|
||||
result.push(`${firstName} ${lastName}`);
|
||||
} else {
|
||||
result.push(`${firstName} ${lastName}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
//normalize names
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
var name = result[i].trim();
|
||||
//cut last letter of name if it is "a" or "и" (bulgarian feminine ending)
|
||||
if (name.endsWith("a") || name.endsWith("и")) {
|
||||
name = name.substring(0, name.length - 1);
|
||||
}
|
||||
result[i] = name;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.separateFamilyMemberNames2 = function (input) {
|
||||
const nameParts = input.trim().split(' ');
|
||||
let familyName = nameParts.pop();
|
||||
|
||||
const firstNames = nameParts.join(' ').split(' и ');
|
||||
const result = firstNames.map(firstName => `${firstName} ${familyName}`);
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
exports.fuzzySearch = function (publishers, searchQuery, distanceThreshold = 0.9) {
|
||||
const lowerCaseSearchQuery = searchQuery?.toLowerCase();
|
||||
|
||||
let results = publishers
|
||||
.filter((p) => {
|
||||
try {
|
||||
const fullName = p.firstName.toLowerCase() + " " + p.lastName.toLowerCase();
|
||||
const reversedFullName = p.lastName.toLowerCase() + " " + p.firstName.toLowerCase();
|
||||
|
||||
const distanceFullName = levenshtein.distance(fullName, lowerCaseSearchQuery);
|
||||
const distanceReversedFullName = levenshtein.distance(reversedFullName, lowerCaseSearchQuery);
|
||||
|
||||
const similarityFullName = 1 - distanceFullName / Math.max(fullName.length, lowerCaseSearchQuery.length);
|
||||
const similarityReversedFullName = 1 - distanceReversedFullName / Math.max(reversedFullName.length, lowerCaseSearchQuery.length);
|
||||
|
||||
// If total fullname length is less than 10 symbols, allow results that are 1 symbol difference
|
||||
if ((fullName.length < 10 || reversedFullName.length < 10) && (distanceFullName <= 1 || distanceReversedFullName <= 1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return similarityFullName >= distanceThreshold || similarityReversedFullName >= distanceThreshold
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => (a.similarity > b.similarity ? -1 : 1));
|
||||
|
||||
return (results && results.length > 0) ? results[0] : null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
exports.getCurrentNonthFormatted = function () {
|
||||
const getCurrentYearMonth = () => {
|
||||
const currentDate = new Date();
|
||||
const year = currentDate.getFullYear();
|
||||
const month = String(currentDate.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
|
||||
return `${year}-${month}`;
|
||||
}
|
||||
return getCurrentYearMonth();
|
||||
}
|
||||
|
||||
exports.getCurrentYearMonth = () => {
|
||||
const currentDate = new Date();
|
||||
const year = currentDate.getFullYear();
|
||||
const month = String(currentDate.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
|
||||
return `${year}-${month}`;
|
||||
}
|
||||
|
||||
// format date to 'HH:mm' time string required by the time picker
|
||||
exports.formatTimeHHmm = function (input) {
|
||||
// Check if the input is a string or a Date object
|
||||
const date = (typeof input === 'string') ? new Date(input) : input;
|
||||
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'Europe/Sofia'
|
||||
}).substring(0, 5);
|
||||
}
|
||||
|
||||
|
||||
//parse 'HH:mm' time string to date object
|
||||
exports.parseTimeHHmm = (timeString) => {
|
||||
// If timeString is already a date, return it as is
|
||||
if (timeString instanceof Date) {
|
||||
return timeString;
|
||||
}
|
||||
|
||||
const [hours, minutes] = timeString.split(':');
|
||||
const date = new Date();
|
||||
date.setHours(hours);
|
||||
date.setMinutes(minutes);
|
||||
return date;
|
||||
}
|
||||
|
||||
exports.setTimeHHmm = (date, timeStringOrHours) => {
|
||||
const newDate = new Date(date);
|
||||
|
||||
if (typeof timeStringOrHours === 'string' && timeStringOrHours.includes(':')) {
|
||||
// If hours is a string in "HH:mm" format
|
||||
const [h, m] = timeStringOrHours.split(':');
|
||||
newDate.setHours(parseInt(h, 10), parseInt(m, 10), 0, 0);
|
||||
} else {
|
||||
// If hours and minutes are provided separately
|
||||
newDate.setHours(parseInt(timeStringOrHours, 10), 0, 0, 0);
|
||||
}
|
||||
|
||||
return newDate;
|
||||
};
|
||||
|
||||
exports.getTimeInMinutes = (dateOrTimestamp) => {
|
||||
const date = new Date(dateOrTimestamp);
|
||||
logger.debug("getTimeInMinutes: date = ", date);
|
||||
return date.getHours() * 60 + date.getMinutes();
|
||||
};
|
||||
|
||||
exports.parseBool = function (value) {
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
value = value || "false";
|
||||
const truthyValues = ['1', 'y', 'yes', 'true', 't'];
|
||||
return truthyValues.includes(String(value).toLowerCase());
|
||||
}
|
||||
|
||||
|
||||
exports.getStartOfWeek = function (date) {
|
||||
const result = new Date(date); // create a copy of the input date
|
||||
// If the day is Sunday (0), we set it to -6, otherwise, subtract the current day of the week from 1 (for Monday)
|
||||
let daysToSubtract = result.getDay() === 0 ? 6 : result.getDay() - 1;
|
||||
result.setDate(result.getDate() - daysToSubtract);
|
||||
result.setHours(0, 0, 0, 0); // set time to midnight
|
||||
return result;
|
||||
}
|
||||
exports.getEndOfWeek = function (date) {
|
||||
const result = new Date(date);
|
||||
// If the day is Sunday (0 in `getDay()`), no addition is needed. Otherwise, add 7 minus the current day of the week.
|
||||
let daysToAdd = result.getDay() === 0 ? 0 : 7 - result.getDay();
|
||||
result.setDate(result.getDate() + daysToAdd);
|
||||
result.setHours(23, 59, 59, 999); // set time to the last millisecond of the day
|
||||
return result;
|
||||
}
|
||||
exports.getStartOfMonth = function (date) {
|
||||
date = new Date(date);
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
|
||||
}
|
||||
exports.getEndOfMonth = function (date) {
|
||||
date = new Date(date);
|
||||
const result = new Date(date.getFullYear(), date.getMonth() + 1, 0); // 0th day of the next month
|
||||
result.setHours(23, 59, 59, 999); // set time to the last millisecond of the day
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.excelSerialDateToDate = function (serial) {
|
||||
// Set base date as 1899-12-31 (Excel's zero date, ignoring leap year bug)
|
||||
let baseDate = new Date(1899, 11, 31);
|
||||
|
||||
// Extract date and time fractions
|
||||
let datePart = Math.floor(serial);
|
||||
let timePart = serial - datePart;
|
||||
|
||||
// Compute the complete date
|
||||
let dateInMs = baseDate.getTime() + datePart * 24 * 60 * 60 * 1000;
|
||||
let timeInMs = timePart * 24 * 60 * 60 * 1000;
|
||||
|
||||
// Construct and return the final date
|
||||
return new Date(dateInMs + timeInMs);
|
||||
}
|
||||
// Function to get a value from the session
|
||||
exports.getSessionValue = function (session, fieldName, defaultValue = null) {
|
||||
return session[fieldName] || defaultValue;
|
||||
};
|
||||
|
||||
// Function to update a value in the session
|
||||
exports.updateSessionValue = function (session, fieldName, value) {
|
||||
session[fieldName] = value;
|
||||
session.save(); // Save the session after updating
|
||||
};
|
||||
|
||||
|
||||
exports.copyToClipboard = function (event, text) {
|
||||
let contentToCopy;
|
||||
|
||||
if (event) {
|
||||
const spanElement = event.currentTarget;
|
||||
contentToCopy = spanElement.textContent;
|
||||
|
||||
// Animation logic
|
||||
const originalText = spanElement.textContent;
|
||||
const originalColor = spanElement.style.backgroundColor; // Store the original color
|
||||
|
||||
spanElement.textContent = "имейла копиран";
|
||||
spanElement.style.backgroundColor = "blue"; // Change color to "info" (blue in this case)
|
||||
|
||||
setTimeout(() => {
|
||||
spanElement.textContent = originalText;
|
||||
spanElement.style.backgroundColor = originalColor; // Reset the color back to original
|
||||
}, 3000); // Reset the text and color back to original after 3 seconds
|
||||
} else {
|
||||
// If event is null, use the provided text parameter
|
||||
if (!text) {
|
||||
console.error("No text provided to copy to clipboard");
|
||||
return; // Exit the function if no text is provided
|
||||
}
|
||||
contentToCopy = text;
|
||||
}
|
||||
|
||||
// Copy contentToCopy to clipboard
|
||||
const tempTextarea = document.createElement('textarea');
|
||||
tempTextarea.value = contentToCopy;
|
||||
document.body.appendChild(tempTextarea);
|
||||
tempTextarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(tempTextarea);
|
||||
}
|
||||
|
||||
|
||||
// User and auth functions
|
||||
// import { getSession } from "next-auth/react";
|
||||
// import { UserRole } from "@prisma/client";
|
||||
//convert to es6 import
|
||||
const { getSession } = require("next-auth/react");
|
||||
const { UserRole } = require("@prisma/client");
|
||||
|
||||
exports.getUser = async function (req) {
|
||||
// Use req if provided (server-side), otherwise call getSession without args (client-side)
|
||||
const session = req ? await getSession({ req }) : await getSession();
|
||||
return session?.user;
|
||||
}
|
||||
|
||||
exports.isUserInRole = async function (req, allowedRoles = []) {
|
||||
const user = await exports.getUser(req);
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no specific roles are required, return true as the user is authenticated
|
||||
if (allowedRoles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the user's role is among the allowed roles
|
||||
// Ensure role exists and is a valid UserRole
|
||||
const userRole = user.role;
|
||||
return allowedRoles.includes(userRole);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Utility functions for localStorage operations
|
||||
exports.setLocalStorage = function (key, value) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
exports.getLocalStorage = function (key, defaultValue) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = window.localStorage.getItem(key);
|
||||
if (stored) {
|
||||
try {
|
||||
// Attempt to parse the stored JSON data
|
||||
return JSON.parse(stored);
|
||||
} catch (error) {
|
||||
// Handle parsing error
|
||||
console.error(`Error parsing JSON from localStorage for key '${key}':`, error);
|
||||
// Return defaultValue or perform other error handling as needed
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
73
src/helpers/const.js
Normal file
73
src/helpers/const.js
Normal file
@ -0,0 +1,73 @@
|
||||
|
||||
const path = require("path");
|
||||
|
||||
// const getConfig = require("next/config");
|
||||
// exports.nextconfig = getConfig();
|
||||
// const { serverRuntimeConfig } = getConfig();
|
||||
// const { serverRuntimeConfig, publicRuntimeConfig } = require('next/config').default();
|
||||
|
||||
|
||||
exports.contentPath = path.join(__dirname, "../../content") + path.sep;
|
||||
|
||||
// ------------------ constants exports ------------------
|
||||
exports.monthNamesBG = [
|
||||
"Януари",
|
||||
"Февруари",
|
||||
"Март",
|
||||
"Април",
|
||||
"Май",
|
||||
"Юни",
|
||||
"Юли",
|
||||
"Август",
|
||||
"Септември",
|
||||
"Октомври",
|
||||
"Ноември",
|
||||
"Декември",
|
||||
];
|
||||
exports.weekdaysBG = [
|
||||
"Неделя",
|
||||
"Понеделник",
|
||||
"Вторник",
|
||||
"Сряда",
|
||||
"Четвъртък",
|
||||
"Петък",
|
||||
"Събота",
|
||||
];
|
||||
|
||||
exports.IsDateXMonthsAgo = function (date, months) {
|
||||
var date = new Date(date);
|
||||
var now = new Date();
|
||||
var diff = now.getTime() - date.getTime();
|
||||
var diffMonths = Math.floor(diff / (1000 * 60 * 60 * 24 * 30));
|
||||
return diffMonths >= months;
|
||||
};
|
||||
|
||||
|
||||
exports.GetDateFormat = function (datetime) {
|
||||
var datetime = new Date(datetime);
|
||||
return `${datetime.getDate()}.${datetime.getMonth()}.${datetime.getFullYear()}`;
|
||||
};
|
||||
|
||||
//Getdate in format 20221215T120000Z
|
||||
exports.GetDateTimeShort = function (datetime) {
|
||||
var datetime = new Date(datetime);
|
||||
return `${datetime.getFullYear()}${String(datetime.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}${String(datetime.getDate()).padStart(2, "0")}T${String(
|
||||
datetime.getHours()
|
||||
).padStart(2, "0")}${String(datetime.getMinutes()).padStart(2, "0")}${String(
|
||||
datetime.getSeconds()
|
||||
).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
//Getdate in format HH:mm
|
||||
exports.GetTimeFormat = function (datetime) {
|
||||
//get date in format HH:mm
|
||||
var datetime = new Date(datetime);
|
||||
return `${String(datetime.getHours()).padStart(2, "0")}:${String(
|
||||
datetime.getMinutes()
|
||||
).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
|
270
src/helpers/data.js
Normal file
270
src/helpers/data.js
Normal file
@ -0,0 +1,270 @@
|
||||
|
||||
const common = require('./common');
|
||||
|
||||
async function findPublisher(names, email, select, getAll = false) {
|
||||
// Normalize and split the name if provided
|
||||
let nameParts = names ? names.normalize('NFC').trim().split(/\s+/) : [];
|
||||
|
||||
// Construct the select clause
|
||||
let selectClause = select
|
||||
? select.split(",").reduce((acc, curr) => ({ ...acc, [curr]: true }), {})
|
||||
: { id: true, firstName: true, lastName: true };
|
||||
|
||||
|
||||
// Construct the where clause based on the provided data
|
||||
let whereClause = [];
|
||||
if (nameParts.length > 0) {
|
||||
if (nameParts.length === 1) {
|
||||
// If only one name part is provided, check it against both firstName and lastName
|
||||
whereClause.push({ OR: [{ firstName: nameParts[0] }, { lastName: nameParts[0] }] });
|
||||
} else if (nameParts.length === 2) {
|
||||
// If two name parts are provided, check combinations of firstName and lastName
|
||||
whereClause.push({ firstName: nameParts[0], lastName: nameParts[1] });
|
||||
whereClause.push({ firstName: nameParts[1], lastName: nameParts[0] });
|
||||
} else if (nameParts.length === 3) {
|
||||
// If three name parts are provided, consider the middle name part of first or last name
|
||||
whereClause.push({ firstName: `${nameParts[0]} ${nameParts[1]}`, lastName: nameParts[2] });
|
||||
whereClause.push({ firstName: nameParts[0], lastName: `${nameParts[1]} ${nameParts[2]}` });
|
||||
} else if (nameParts.length > 3) {
|
||||
// Join all parts except the last as the first name, and consider the last part as the last name
|
||||
const firstName = nameParts.slice(0, -1).join(' ');
|
||||
const lastName = nameParts.slice(-1).join(' ');
|
||||
whereClause.push({ firstName: firstName, lastName: lastName });
|
||||
|
||||
// Alternative case: First part as first name, and join the rest as the last name
|
||||
const altFirstName = nameParts.slice(0, 1).join(' ');
|
||||
const altLastName = nameParts.slice(1).join(' ');
|
||||
whereClause.push({ firstName: altFirstName, lastName: altLastName });
|
||||
}
|
||||
}
|
||||
|
||||
if (email) {
|
||||
whereClause.push({ email: email });
|
||||
}
|
||||
|
||||
if (whereClause.length === 0) {
|
||||
return null; // No search criteria provided
|
||||
}
|
||||
|
||||
|
||||
const prisma = common.getPrismaClient();
|
||||
// let publisher = await prisma.publisher.findFirst({
|
||||
// select: selectClause,
|
||||
// where: { OR: whereClause }
|
||||
// });
|
||||
// Retrieve all matching records
|
||||
try {
|
||||
let result = await prisma.publisher.findMany({
|
||||
select: selectClause,
|
||||
where: { OR: whereClause }
|
||||
});
|
||||
|
||||
// If getAll is false, take only the first record
|
||||
if (!getAll && result.length > 0) {
|
||||
result = result.length > 0 ? [result[0]] : [];
|
||||
}
|
||||
if (result.length === 0 && names) {
|
||||
console.log("No publisher found, trying fuzzy search for '" + names + "'- email: '" + email + "'");
|
||||
const publishers = await prisma.publisher.findMany();
|
||||
result = await common.fuzzySearch(publishers, names, 0.90);
|
||||
console.log("Fuzzy search result: " + result?.firstName + " " + result?.lastName + " - " + result?.email);
|
||||
}
|
||||
//always return an array
|
||||
result = result || [];
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error finding publisher: ", error);
|
||||
throw error; // Rethrow the error or handle it as needed
|
||||
}
|
||||
}
|
||||
|
||||
async function findPublisherAvailability(publisherId, date) {
|
||||
|
||||
const prisma = common.getPrismaClient();
|
||||
const dayOfWeek = common.getDayOfWeekNameEnEnum(date); // Assuming common.getDayOfWeekNameEnEnum returns the day of week
|
||||
//const weekOfMonth = common.getWeekOfMonth(date); // Assuming common.getWeekOfMonth returns the week of month
|
||||
date = new Date(date); // Convert to date object if not already
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
||||
const potentialAvailabilities = await prisma.availability.findMany({
|
||||
where: {
|
||||
publisherId: publisherId,
|
||||
OR: [
|
||||
{
|
||||
// Exact date match
|
||||
startTime: {
|
||||
gte: new Date(date.setHours(0, 0, 0, 0)),
|
||||
lt: new Date(date.setHours(23, 59, 59, 999))
|
||||
}
|
||||
},
|
||||
{
|
||||
// Correct day of week and before the date, with endDate consideration
|
||||
dayofweek: dayOfWeek,
|
||||
OR: [
|
||||
{
|
||||
endDate: null
|
||||
},
|
||||
{
|
||||
endDate: {
|
||||
gt: date
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Filter the results based on time and other criteria when not exact date match
|
||||
const availability = potentialAvailabilities.find(avail => {
|
||||
const availStartHours = avail.startTime.getHours();
|
||||
const availStartMinutes = avail.startTime.getMinutes();
|
||||
const availEndHours = avail.endTime.getHours();
|
||||
const availEndMinutes = avail.endTime.getMinutes();
|
||||
|
||||
const isAfterStartTime = hours > availStartHours || (hours === availStartHours && minutes >= availStartMinutes);
|
||||
const isBeforeEndTime = hours < availEndHours || (hours === availEndHours && minutes <= availEndMinutes);
|
||||
// check day of week if not null
|
||||
const isCorrectDayOfWeek = avail.repeatWeekly ? avail.startTime.getDay() === date.getDay() : true;
|
||||
const isExactDateMatch = avail.dayOfMonth ? avail.startTime.toDateString() === date.toDateString() : true;
|
||||
const isBeforeEndDate = avail.repeatWeekly ? true : avail.endTime > date;
|
||||
//const isCorrectWeekOfMonth = avail.repeatWeekly ? true : avail.weekOfMonth === weekOfMonth;
|
||||
|
||||
return isAfterStartTime && isBeforeEndTime && isCorrectDayOfWeek && isExactDateMatch && isBeforeEndDate;
|
||||
});
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function runSqlFile(filePath) {
|
||||
|
||||
const prisma = common.getPrismaClient();
|
||||
// Seed the database with some sample data
|
||||
try {
|
||||
// Read the SQL file
|
||||
const sql = fs.readFileSync(filePath, { encoding: 'utf-8' });
|
||||
|
||||
// Split the file content by semicolon and filter out empty statements
|
||||
const statements = sql.split(';').map(s => s.trim()).filter(s => s.length);
|
||||
|
||||
// Execute each statement
|
||||
for (const statement of statements) {
|
||||
await prisma.$executeRawUnsafe(statement);
|
||||
}
|
||||
console.log('SQL script executed successfully.');
|
||||
} catch (error) {
|
||||
console.error('Error executing SQL script:', error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findPublisher,
|
||||
findPublisherAvailability,
|
||||
runSqlFile,
|
||||
getAvailabilities
|
||||
};
|
162
src/helpers/email.js
Normal file
162
src/helpers/email.js
Normal file
@ -0,0 +1,162 @@
|
||||
// helper module to send emails with nodemailer
|
||||
|
||||
const fs = require("fs");
|
||||
const { MailtrapClient } = require("mailtrap");
|
||||
const nodemailer = require("nodemailer");
|
||||
const CON = require("./const");
|
||||
const CAL = require("./calendar");
|
||||
|
||||
// const { google } = require("googleapis");
|
||||
// const OAuth2 = google.auth.OAuth2;
|
||||
|
||||
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
||||
|
||||
const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad";
|
||||
const SENDER_EMAIL = "pw@d-popov.com";
|
||||
const sender = { name: "JW Cart: Shift Info", email: SENDER_EMAIL };
|
||||
const client = new MailtrapClient({ token: TOKEN });
|
||||
const mailtrapTestClient = new MailtrapClient({
|
||||
username: '8ec69527ff2104',//not working now
|
||||
password: 'c7bc05f171c96c'
|
||||
});
|
||||
|
||||
// ------------------ Email sending ------------------
|
||||
var lastResult = null;
|
||||
function setResult(result) {
|
||||
lastResult = result;
|
||||
}
|
||||
exports.GetLastResult = function () {
|
||||
return lastResult;
|
||||
};
|
||||
|
||||
exports.SendEmail = async function (to, subject, text, html) {
|
||||
const message = {
|
||||
from: sender,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
};
|
||||
};
|
||||
|
||||
exports.SendEmail_Test = async function (to, subject, text, html) {
|
||||
const message = {
|
||||
from: sender,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
};
|
||||
|
||||
await mailtrapTestClient
|
||||
.send(message)
|
||||
.then(console.log, console.error, setResult);
|
||||
}
|
||||
|
||||
|
||||
// https://mailtrap.io/blog/sending-emails-with-nodemailer/
|
||||
exports.SendTestEmail = async function (to) {
|
||||
// await client
|
||||
// .send({
|
||||
// from: sender,
|
||||
// to: [{ email: RECIPIENT_EMAIL }],
|
||||
// subject: "Hello from Mailtrap!",
|
||||
// text: "Welcome to Mailtrap Sending!",Shift Info"
|
||||
// })
|
||||
// .then(console.log, console.error, setResult);
|
||||
|
||||
// return lastResult;
|
||||
|
||||
const welcomeImage = fs.readFileSync(
|
||||
path.join(CON.contentPath, "welcome.png")
|
||||
);
|
||||
|
||||
await client
|
||||
.send({
|
||||
category: "test",
|
||||
custom_variables: {
|
||||
hello: "world",
|
||||
year: 2022,
|
||||
anticipated: true,
|
||||
},
|
||||
from: sender,
|
||||
to: [{ email: to }],
|
||||
subject: "Hello from Mailtrap!",
|
||||
html: `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body style="font-family: sans-serif;">
|
||||
<div style="display: block; margin: auto; max-width: 600px;" class="main">
|
||||
<h1 style="font-size: 18px; font-weight: bold; margin-top: 20px">Congrats for sending test email with Mailtrap!</h1>
|
||||
<p>Inspect it using the tabs you see above and learn how this email can be improved.</p>
|
||||
<img alt="Inspect with Tabs" src="cid:welcome.png" style="width: 100%;">
|
||||
<p>Now send your email using our fake SMTP server and integration of your choice!</p>
|
||||
<p>Good luck! Hope it works.</p>
|
||||
</div>
|
||||
<!-- Example of invalid for email html/css, will be detected by Mailtrap: -->
|
||||
<style>
|
||||
.main { background-color: white; }
|
||||
a:hover { border-left-width: 1em; min-height: 2em; }
|
||||
</style>
|
||||
</body>
|
||||
</html>`,
|
||||
attachments: [
|
||||
{
|
||||
filename: "welcome.png",
|
||||
content_id: "welcome.png",
|
||||
disposition: "inline",
|
||||
content: welcomeImage,
|
||||
},
|
||||
],
|
||||
})
|
||||
.then(console.log, console.error, setResult);
|
||||
};
|
||||
|
||||
|
||||
exports.SendEmail_NewShifts = async function (publisher, shifts) {
|
||||
if (shifts.length == 0) return;
|
||||
|
||||
var date = new Date(shifts[0].startTime);
|
||||
|
||||
//generate ICS calendar links for all shifts
|
||||
const icsLink = CAL.GenerateICS(shifts);
|
||||
|
||||
const shftStr = shifts
|
||||
.map((s) => {
|
||||
return ` ${CON.weekdaysBG[s.startTime.getDay()]
|
||||
} ${CON.GetDateFormat(s.startTime)} ${s.cartEvent.location.name
|
||||
} ${CON.GetTimeFormat(s.startTime)} - ${CON.GetTimeFormat(
|
||||
s.endTime
|
||||
)}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
await client.send({
|
||||
from: sender,
|
||||
to: [
|
||||
{
|
||||
email: "dobromir.popov@gmail.com",//publisher.email,
|
||||
name: publisher.firstName + " " + publisher.lastName,
|
||||
},
|
||||
],
|
||||
subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()],
|
||||
text:
|
||||
"Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" +
|
||||
"Вие сте регистриран да получавате известия за нови смени на количка.\n" +
|
||||
`За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` +
|
||||
` ${shftStr} \n\n\n` +
|
||||
"Поздрави,\n" +
|
||||
"Специално Свидетелстване София",
|
||||
attachments: [
|
||||
{
|
||||
filename: "calendar.ics",
|
||||
content_id: "calendar.ics",
|
||||
disposition: "inline",
|
||||
content: icsLink,
|
||||
},
|
||||
],
|
||||
})
|
||||
.then(console.log, console.error, setResult);
|
||||
};
|
822
src/helpers/excel.js
Normal file
822
src/helpers/excel.js
Normal file
@ -0,0 +1,822 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const dotenv = require("dotenv");
|
||||
dotenv.config();
|
||||
// dotenv.config({ path: ".env.local" });
|
||||
|
||||
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
||||
const CON = require("./const");
|
||||
const common = require("./common");
|
||||
const data = require("./data");
|
||||
//const { fi } = require("date-fns/locale");
|
||||
|
||||
//Works with nextjs, but fails with nodejs
|
||||
|
||||
// for nodejs
|
||||
//const api = require("./pages/api/index");
|
||||
|
||||
exports.GenerateExcel = async function (req, res) {
|
||||
|
||||
const prisma = common.getPrismaClient();
|
||||
const year = req.params.year;
|
||||
const month = parseInt(req.params.month) - 1;
|
||||
const fromDate = new Date(year, month, 1); // month is 0 based
|
||||
// to last day of the month. special case december
|
||||
const toDate = new Date(year, month + 1, 0); // month is 0 based
|
||||
// toDate.setMonth(fromDate.getMonth() + 1);
|
||||
|
||||
//get all shiifts for the month
|
||||
var shifts = await prisma.shift.findMany({
|
||||
where: {
|
||||
startTime: {
|
||||
gte: fromDate,
|
||||
lt: toDate,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
cartEvent: {
|
||||
include: {
|
||||
location: true,
|
||||
},
|
||||
},
|
||||
publishers: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
var filePath = path.join(CON.contentPath, "График КОЛИЧКИ.xlsx");
|
||||
const bеgin = new Date();
|
||||
|
||||
//----------------- exit "График КОЛИЧКИ.xlsx" with exceljs ----------------
|
||||
const ExcelJS = require("exceljs");
|
||||
const xjswb = new ExcelJS.Workbook();
|
||||
if (req.params.process == "1") {
|
||||
try {
|
||||
xjswb.xlsx
|
||||
.readFile(filePath)
|
||||
.then(function () {
|
||||
try {
|
||||
var worksheet = xjswb.getWorksheet(13);
|
||||
|
||||
//get row 2 with all the styles
|
||||
var weekHeader = worksheet.getRow(2);
|
||||
|
||||
var newWorksheet = xjswb.addWorksheet(
|
||||
CON.monthNamesBG[month]
|
||||
);
|
||||
newWorksheet.name = CON.monthNamesBG[month].toUpperCase();
|
||||
//copy each row from the template with all the styles
|
||||
worksheet.eachRow(
|
||||
{ includeEmpty: true },
|
||||
function (row, rowNumber) {
|
||||
var newRow = newWorksheet.getRow(rowNumber);
|
||||
newRow.height = row.height;
|
||||
row.eachCell(
|
||||
{ includeEmpty: true },
|
||||
function (cell, colNumber) {
|
||||
var newCell = newRow.getCell(colNumber);
|
||||
newCell.value = cell.value;
|
||||
newCell.font = cell.font;
|
||||
newCell.alignment = cell.alignment;
|
||||
newCell.border = cell.border;
|
||||
newCell.fill = cell.fill;
|
||||
newCell.numberFormat = cell.numberFormat;
|
||||
newCell.protection = cell.protection;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// worksheet.eachRow({ includeEmpty: true }, function (row, rowNumber) {
|
||||
// row.eachCell({ includeEmpty: true }, function (cell, colNumber) {
|
||||
// newWorksheet.getCell(rowNumber, colNumber).value = cell.value;
|
||||
// });
|
||||
// });
|
||||
for (let i = start.row; i <= end.row; i++) {
|
||||
const leftBorderCell = worksheet.getCell(i, start.col);
|
||||
|
||||
//hide original sheet
|
||||
worksheet.state = "hidden";
|
||||
//save file
|
||||
xjswb.xlsx.writeFile(
|
||||
path.join(
|
||||
contentPath,
|
||||
`График КОЛИЧКИ ${year}-${month + 1}.xlsx`
|
||||
)
|
||||
);
|
||||
//send the file to the client
|
||||
res.setHeader(
|
||||
"Content-Type",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=" +
|
||||
encodeURI(`График КОЛИЧКИ ${year}-${month + 1}.xlsx`)
|
||||
);
|
||||
xjswb.xlsx.write(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.end(
|
||||
"[" +
|
||||
new Date().toLocaleString() +
|
||||
"] (" +
|
||||
(new Date() - bеgin) +
|
||||
"ms) " +
|
||||
err
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
console.log("done");
|
||||
//show cyrillic text in response
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(
|
||||
"Генериране на График КОЛИЧКИ${year}-${month}.xlsx завършено успешно за " +
|
||||
(new Date() - bеgin) +
|
||||
"ms"
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.end(err.message);
|
||||
}
|
||||
}
|
||||
//----------------- exit "График КОЛИЧКИ.xlsx" with xlsx-style ----------------
|
||||
if (req.params.process == "2") {
|
||||
const XLSX = require('xlsx');
|
||||
const wb = XLSX.utils.book_new();
|
||||
wb.Props = {
|
||||
Title: "График КОЛИЧКИ",
|
||||
Subject: "График КОЛИЧКИ",
|
||||
};
|
||||
wb.SheetNames.push("График КОЛИЧКИ");
|
||||
const ws_data = [
|
||||
[1, 2, 3],
|
||||
[true, false, null, "sheetjs"],
|
||||
["foo", "bar", new Date("2014-02-19T14:30Z"), "0.3"],
|
||||
["baz", null, "qux"]
|
||||
];
|
||||
const ws = XLSX.utils.aoa_to_sheet(ws_data);
|
||||
wb.Sheets["График КОЛИЧКИ"] = ws;
|
||||
|
||||
const xlsxstyle = require("xlsx-style");
|
||||
try {
|
||||
const workbook = xlsxstyle.readFile(filePath);
|
||||
const sheetNames = workbook.SheetNames;
|
||||
//find the sheet with the name "Зима" in sheetNames
|
||||
const sheetName = sheetNames.find((name) => name === "Зима");
|
||||
|
||||
// Get the data of "Sheet1"
|
||||
const data = xlsxstyle.utils.sheet_to_json(workbook.Sheets[sheetNames[2]]);
|
||||
|
||||
var worksheet = workbook.Sheets[sheetName];
|
||||
//copy worksheet to new workbook
|
||||
//add new worksheet to new workbook with month name
|
||||
// var newWorksheet = wb.addWorksheet(CON.monthNamesBG[month]);
|
||||
var rows = xlsxstyle.utils.sheet_to_row_object_array(worksheet, {
|
||||
header: 1,
|
||||
});
|
||||
|
||||
XLSX.utils.book_append_sheet(wb, worksheet, "_" + CON.monthNamesBG[month]);
|
||||
|
||||
//save file
|
||||
XLSX.writeFile(wb, path.join(CON.contentPath, `_График КОЛИЧКИ ${year}-${month + 1}.xlsx`));
|
||||
|
||||
|
||||
//save file
|
||||
xlsxstyle.writeFile(
|
||||
newWorkbook,
|
||||
path.join(
|
||||
contentPath,
|
||||
`График КОЛИЧКИ ${year}-${month + 1}.xlsx`
|
||||
)
|
||||
);
|
||||
//send the file to the client
|
||||
res.setHeader(
|
||||
"Content-Type",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=" +
|
||||
encodeURI(`График КОЛИЧКИ ${year}-${month + 1}.xlsx`)
|
||||
);
|
||||
xlsxstyle.writeFile(newWorkbook, res);
|
||||
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(
|
||||
"Генериране на График КОЛИЧКИ${year}-${month}.xlsx завършено успешно за " +
|
||||
(new Date() - bеgin) +
|
||||
" ms!"
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.end("[" + new Date().toLocaleString() + "] " + err);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------- exit "График КОЛИЧКИ.xlsx" with node-excel-export ----------------
|
||||
if (req.params.process == "3") {
|
||||
// // uses xlsx-style in the background
|
||||
// // https://github.com/protobi/js-xlsx#cell-styles
|
||||
// // https://www.npmjs.com/package/node-excel-export
|
||||
// var excel = require('node-excel-export');
|
||||
// var rows = [[
|
||||
// { value: "EXTREMELY LONG TITLE 1", bold: 1, autoWidth: true },
|
||||
// { value: "TITLE2" },
|
||||
// { value: "TITLE3" }
|
||||
// ]];
|
||||
// var styles = {
|
||||
// headerHilight: {
|
||||
// fill: {
|
||||
// fgColor: {
|
||||
// rgb: 'FFE36600'
|
||||
// }
|
||||
// },
|
||||
// font: {
|
||||
// color: {
|
||||
// rgb: 'FFFFFFFF'
|
||||
// },
|
||||
// sz: 10,
|
||||
// bold: true,
|
||||
// // underline: true
|
||||
// }
|
||||
// },
|
||||
// cellOdd: {
|
||||
// fill: {
|
||||
// fgColor: {
|
||||
// rgb: 'FFF8F8F7'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// const heading = [
|
||||
// [{value: 'b1', style: styles.headerHilight},
|
||||
// {value: 'd1', style: styles.headerHilight},
|
||||
// {value: 'e1', style: styles.headerHilight}],
|
||||
// ['b2', 'd2', 'e2'] // <-- It can be only values
|
||||
// ];
|
||||
// var specification = {
|
||||
// "shiftTime": {
|
||||
// displayName: 'Смяна',
|
||||
// headerStyle: styles.headerHilight,
|
||||
// cellStyle: styles.cellOdd,
|
||||
// width: 60
|
||||
// },
|
||||
// "publisherName": {
|
||||
// "displayName": 'ПЛИСКА ПОНЕДЕЛНИК',
|
||||
// "headerStyle": styles.headerHilight,
|
||||
// "width": 250
|
||||
// },
|
||||
// "Col2": {
|
||||
// "displayName": 'СТАДИОН СРЯДА',
|
||||
// "headerStyle": styles.headerHilight,
|
||||
// "width": 215
|
||||
// },
|
||||
// "Col3": {
|
||||
// displayName: 'УНИВЕРСИТЕТ ЧЕТВЪРТЪК',
|
||||
// headerStyle: styles.headerHilight,
|
||||
// width: 150
|
||||
// }
|
||||
// }
|
||||
// var report = excel.buildExport(
|
||||
// [{
|
||||
// name: `График КОЛИЧКИ ${year}-${month + 1}.xlsx`,
|
||||
// specification: specification,
|
||||
// heading: heading, // <- Raw heading array (optional)
|
||||
// data: rows
|
||||
// }]);
|
||||
// //save file to disk
|
||||
// fs.writeFile(path.join(contentPath, `График КОЛИЧКИ ${year}-${month + 1}.xlsx`), report, 'binary', function (err) { });
|
||||
// res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
// res.end("Генериране на График КОЛИЧКИ${year}-${month}.xlsx завършено успешно за " + (new Date() - bеgin) + " ms!");
|
||||
// //send the file to the client
|
||||
// console.log("excel genarated in " + (new Date() - bеgin) + "ms");
|
||||
// // res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
// // res.setHeader("Content-Disposition", "attachment; filename=" + encodeURI(`График КОЛИЧКИ ${year}-${month + 1}.xlsx`));
|
||||
// // res.end(report);
|
||||
}
|
||||
}
|
||||
|
||||
exports.ImportFromExcel = function (req, res) {
|
||||
}
|
||||
|
||||
exports.processEvents = async function (events, year, monthNumber, progressCallback, createAvailabilities) {
|
||||
|
||||
const prisma = common.getPrismaClient();
|
||||
const d = new Date(year, monthNumber - 1);//month is 0 based in js
|
||||
const monthDatesInfo = common.getMonthDatesInfo(d); //CAL.GetMonthDatesInfo(date);
|
||||
try {
|
||||
await prisma.shift.deleteMany({
|
||||
where: {
|
||||
startTime: {
|
||||
gte: monthDatesInfo.firstMonday,
|
||||
lt: monthDatesInfo.lastSunday,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
var shifts = await prisma.shift.findMany({
|
||||
where: {
|
||||
isactive: true,
|
||||
startTime: {
|
||||
gte: monthDatesInfo.firstMonday,
|
||||
lt: monthDatesInfo.lastSunday,
|
||||
},
|
||||
}
|
||||
});
|
||||
var locations = await prisma.location.findMany({ where: { isactive: true, } });
|
||||
|
||||
var cartEvents = await prisma.cartEvent.findMany({ where: { isactive: true, } });
|
||||
|
||||
var publishers = await prisma.publisher.findMany({
|
||||
where: { isactive: true, },
|
||||
include: {
|
||||
availabilities: { where: { isactive: true, }, },
|
||||
assignments: { include: { shift: true, }, },
|
||||
},
|
||||
});
|
||||
|
||||
const totalEvents = events.length;
|
||||
|
||||
for (let i = 0; i < totalEvents; i++) {
|
||||
const event = events[i];
|
||||
const progress = (i / totalEvents) * 100;
|
||||
if (progress > 1) {
|
||||
progressCallback(progress);
|
||||
}
|
||||
try {
|
||||
const date = new Date(event.date);
|
||||
let startStr, endStr;
|
||||
if (event.time) {
|
||||
startStr = event.time.split("-")[0].trim();
|
||||
endStr = event.time.split("-")[1].trim();
|
||||
}
|
||||
else {
|
||||
//get the start event time and event.shiftNr and calculate start and end based on that
|
||||
const shift = shifts.find((s) => s.nr === event.shiftNr);
|
||||
if (!shift) {
|
||||
console.warn(`Could not find shift with nr '${event.shiftNr}'`);
|
||||
continue;
|
||||
}
|
||||
startStr = shift.startTime;
|
||||
endStr = shift.endTime;
|
||||
|
||||
}
|
||||
let st = new Date(event.date);
|
||||
st.setHours(startStr.split(":")[0]);
|
||||
st.setMinutes(startStr.split(":")[1]);
|
||||
const start = st;
|
||||
st = new Date(event.date);
|
||||
st.setHours(endStr.split(":")[0]);
|
||||
st.setMinutes(endStr.split(":")[1]);
|
||||
const end = st
|
||||
|
||||
|
||||
var location = locations.find((l) =>
|
||||
l.name.toLowerCase().includes(event.placeOfEvent.toLowerCase())
|
||||
);
|
||||
if (!location) {
|
||||
console.warn(`Could not find location with name '${event.placeOfEvent}'`);
|
||||
//await prisma.location.create({ data: { name: event.placeOfEvent } });
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var dayofWeek = common.getDayOfWeekNameEnEnum(date);
|
||||
const cartEvent = cartEvents.find(
|
||||
(ce) =>
|
||||
ce.locationId === location.id &&
|
||||
ce.dayofweek === dayofWeek
|
||||
);
|
||||
|
||||
if (!cartEvent) {
|
||||
console.warn(`Could not find cart event for date '${date}' and location '${event.placeOfEvent}'`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let shift = shifts.find((s) =>
|
||||
s.cartEventId === cartEvent.id &&
|
||||
new Date(s.startTime).getTime() === new Date(start).getTime()
|
||||
);
|
||||
|
||||
if (!shift) {
|
||||
//if shiftnr = 1, notes = "Докарва" + event.transport
|
||||
//if shiftnr = 8, notes = "Взема" + event.transport
|
||||
|
||||
|
||||
let note = event.shiftNr === 1 ? "Докарва количка от Люлин - " + event.transport :
|
||||
event.shiftNr === 6 ? "Прибира количка в Люлин - " + event.transport : "";
|
||||
const shiftEntity = await prisma.shift.create({
|
||||
data: {
|
||||
name: event.dayOfWeek + " " + event.dayOfMonth + ", " + start.toLocaleTimeString() + " - " + end.toLocaleTimeString(),
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
notes: note,
|
||||
cartEvent: {
|
||||
connect: {
|
||||
id: cartEvent.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
shift = shiftEntity;
|
||||
console.log(`Created shift with ID ${shiftEntity.id} for cart event with ID ${cartEvent.id} on ${date} from ${start.toLocaleTimeString()} to ${end.toLocaleTimeString()}`);
|
||||
|
||||
}
|
||||
|
||||
for (const nameOrFamily of event.names) {
|
||||
for (const name of common.separateFamilyMemberNames2(nameOrFamily)) {
|
||||
var publisher = null
|
||||
const pubs = await data.findPublisher(name, null, "id,email,firstName,lastName", true);
|
||||
publisher = pubs[0];
|
||||
|
||||
|
||||
if (!publisher) {
|
||||
const fuzzyPublisher = common.fuzzySearch(publishers, name);
|
||||
|
||||
if (fuzzyPublisher) {
|
||||
console.log(
|
||||
`Found publisher '${fuzzyPublisher.firstName} ${fuzzyPublisher.lastName}' through fuzzy search for '${name}'`
|
||||
);
|
||||
publisher = fuzzyPublisher;
|
||||
} else {
|
||||
console.warn(`NO publisher found! Could not find publisher with name '${name}'. Creating new publisher from known info.`);
|
||||
//continue;
|
||||
try {
|
||||
let firstname = name.substring(0, name.lastIndexOf(" ")).trim();
|
||||
let lastname = name.substring(name.lastIndexOf(" ") + 1).trim();
|
||||
// Remove the last letter if it is "а" or "и"
|
||||
// if (lastname.endsWith('а') || lastname.endsWith('и')) {
|
||||
if (lastname.endsWith('и')) {
|
||||
lastname = lastname.slice(0, -1);
|
||||
}
|
||||
//if any name is empty, skip this publisher
|
||||
if (firstname == "" || lastname == "") {
|
||||
console.warn(`NO publisher found! Could not find publisher with name '${name}', but we need both first and last name. Skipping this publisher.`);
|
||||
continue;
|
||||
}
|
||||
//var name = names[i].trim();
|
||||
// //cut last letter of name if it is "a" or "и" (bulgarian feminine ending)
|
||||
// if (name.endsWith("a") || name.endsWith("и")) {
|
||||
// name = name.substring(0, name.length - 1);
|
||||
// }
|
||||
|
||||
var manualPub = {
|
||||
email: name.toLowerCase().replace(/ /g, "."), // + "@gmail.com"
|
||||
firstName: firstname,
|
||||
lastName: lastname,
|
||||
isactive: true,
|
||||
isImported: true,
|
||||
// role: "EXTERNAL",
|
||||
};
|
||||
publisher = await prisma.publisher.create({ data: manualPub });
|
||||
console.log(`Created publisher with ID ${publisher.id} for name '${name}'`);
|
||||
|
||||
// create availability with the same date as the event.
|
||||
//ToDo: add parameter to control if we want to create availability for each event. can be done whe we import previous shifts.
|
||||
// if (createAvailabilities) {
|
||||
// const dayofWeek = common.getDayOfWeekNameEnEnum(date);
|
||||
// const availability = await prisma.availability.create({
|
||||
// data: {
|
||||
// publisherId: publisher.id,
|
||||
// //date: date,
|
||||
// dayofweek: dayofWeek,
|
||||
// startTime: startTime,
|
||||
// endTime: endTime,
|
||||
// name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`,
|
||||
// isFromPreviousAssignment: true,
|
||||
// isactive: true,
|
||||
// },
|
||||
// });
|
||||
// console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`);
|
||||
// }
|
||||
// const personResponse = await axiosInstance.post("/publishers", manualPub);
|
||||
// let personId = personResponse.data.id;
|
||||
|
||||
} catch (e) {
|
||||
console.error(`shiftCache: error adding MANUAL publisher to the system (${manualPub.email} ${manualPub.firstName} ${manualPub.lastName}): ` + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (location != null && publisher != null && shift != null) {
|
||||
const assignment = await prisma.assignment.create({
|
||||
data: {
|
||||
//publisherId: publisher.id,
|
||||
// shiftId: shift.id,
|
||||
publisher: {
|
||||
connect: {
|
||||
id: publisher.id,
|
||||
},
|
||||
},
|
||||
shift: {
|
||||
connect: {
|
||||
id: shift.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
//ToDo: fix findPublisherAvailability and creation of availabilities
|
||||
// check if there is an availability for this publisher on this date, and if not, create one
|
||||
// const availability = await data.findPublisherAvailability(publisher.id, start);
|
||||
// if (!availability && createAvailabilities) {
|
||||
// const dayofWeek = common.getDayOfWeekNameEnEnum(date);
|
||||
// const availability = await prisma.availability.create({
|
||||
// data: {
|
||||
// publisherId: publisher.id,
|
||||
// //date: date,
|
||||
// dayofweek: dayofWeek,
|
||||
// //weekOfMonth: common.getWeekOfMonth(date),
|
||||
// startTime: start,
|
||||
// endTime: end,
|
||||
// name: `от предишен график, ${publisher.firstName} ${publisher.lastName}`,
|
||||
// isFromPreviousAssignment: true,
|
||||
// },
|
||||
// });
|
||||
// console.log(`Created WEEKLY availability with ID ${availability.id} for date '${date.toDateString()}' and publisher '${publisher.firstName} ${publisher.lastName}'`);
|
||||
// }
|
||||
|
||||
console.log(`Created assignment with ID ${assignment.id} for date '${date.toDateString()}' and location '${event.placeOfEvent}'. publisher: ${publisher.firstName} ${publisher.lastName}}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
console.log("Done");
|
||||
}
|
||||
|
||||
//We used GPT to generate the file so far - day by day, by copy-pasting it in the chat. See PROMPTS.md
|
||||
exports.ReadDocxFileForMonth = async function (filePath, buffer, month, year, progressCallback, createAvailabilities) {
|
||||
try {
|
||||
const JSZip = require("jszip");
|
||||
const zip = new JSZip();
|
||||
//if filepath is not null read it. otherwise use buffer
|
||||
|
||||
if (filePath != null) {
|
||||
buffer = await fs.readFileSync(filePath);
|
||||
}
|
||||
const zipFile = await zip.loadAsync(buffer);
|
||||
const documentXml = await zipFile.file("word/document.xml").async("string");
|
||||
const xml2js = require("xml2js");
|
||||
const parser = new xml2js.Parser({
|
||||
explicitArray: false,
|
||||
ignoreAttrs: true,
|
||||
});
|
||||
const json = await parser.parseStringPromise(documentXml);
|
||||
//const tableData = parsedXml['w:document']['w:body']['w:tbl']['w:tr'][1]['w:tc']['w:p']['w:r']['w:t'];
|
||||
|
||||
// addParentReferences(json);
|
||||
// const xmlJs = require('xml-js');
|
||||
// const jsonstring = xmlJs.xml2json(json, { compact: true });
|
||||
|
||||
const cleanedJsonObj = common.jsonRemoveEmptyNodes(json);
|
||||
|
||||
//let filename = `График source ${year}-${month}.json`;
|
||||
//fs.writeFileSync("./content/temp/" + filename, JSON.stringify(cleanedJsonObj)); //initial json source for previous shifts
|
||||
|
||||
const extractedData = extractData(cleanedJsonObj, month, year);
|
||||
//console.log(extractedData);
|
||||
|
||||
//modify the file
|
||||
// try {
|
||||
// let filename = `График ${year}-${month}.json`;
|
||||
// fs.writeFileSync("./content/temp/" + filename, JSON.stringify(extractedData));
|
||||
// } catch (e) {
|
||||
// console.log(e);
|
||||
// }
|
||||
await exports.processEvents(extractedData, year, month, progressCallback, createAvailabilities);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
const weekNames = [
|
||||
"Понеделник",
|
||||
"Вторник",
|
||||
"Сряда",
|
||||
"Четвъртък",
|
||||
"Петък",
|
||||
"Събота",
|
||||
"Неделя",
|
||||
];
|
||||
|
||||
function findWeekNameNodes(obj, path = []) {
|
||||
let result = [];
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
const newPath = path.slice();
|
||||
newPath.push(i);
|
||||
result = result.concat(findWeekNameNodes(obj[i], newPath));
|
||||
}
|
||||
} else if (typeof obj === "object") {
|
||||
for (const key in obj) {
|
||||
const newPath = path.slice();
|
||||
newPath.push(key);
|
||||
result = result.concat(findWeekNameNodes(obj[key], newPath));
|
||||
}
|
||||
} else if (typeof obj === "string") {
|
||||
const matches = obj.match(/(\S+) (\d+)/);
|
||||
if (matches && weekNames.includes(matches[1])) {
|
||||
result.push({ weekName: matches[1], dayOfMonth: matches[2], path });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
function findShifts(node) {
|
||||
if (node === null || typeof node !== "object") return null;
|
||||
|
||||
if (node.hasOwnProperty("w:tbl")) {
|
||||
return node["w:tbl"];
|
||||
} else {
|
||||
return findShifts(node._parent);
|
||||
}
|
||||
}
|
||||
|
||||
function extractData(parsedJson, month, year) {
|
||||
const weekNameNodes = findWeekNameNodes(parsedJson);
|
||||
const data = [];
|
||||
let lastDay = 0;
|
||||
let monthOverflow = false;
|
||||
|
||||
|
||||
for (const node of weekNameNodes) {
|
||||
const { weekName, dayOfMonth, path } = node;
|
||||
let currentNode = parsedJson;
|
||||
let baseNode = null;
|
||||
let parentNode = null;
|
||||
const dom = parseInt(dayOfMonth);
|
||||
|
||||
if (lastDay > dom) {
|
||||
monthOverflow = true;
|
||||
}
|
||||
lastDay = dom;
|
||||
let date = new Date(year, month - (monthOverflow ? 0 : 1), dom);
|
||||
|
||||
for (const key of path) {
|
||||
if (currentNode[key] && currentNode[key]["w:tc"]) {
|
||||
parentNode = currentNode;
|
||||
baseNode = currentNode[key];
|
||||
}
|
||||
|
||||
currentNode = currentNode[key];
|
||||
}
|
||||
// for (const key of path) {
|
||||
// parentNode = currentNode;
|
||||
// currentNode = currentNode[key];
|
||||
// }
|
||||
console.log("Processing " + weekName + " " + dayOfMonth + " " + CON.monthNamesBG[date.getMonth()]);
|
||||
const dailyData = extractDataForDay(parentNode, weekName, date);
|
||||
|
||||
dailyData.forEach((item) => {
|
||||
if (!data.some((existingItem) =>
|
||||
existingItem.date === item.date &&
|
||||
existingItem.shiftNr === item.shiftNr &&
|
||||
existingItem.dayOfMonth === item.dayOfMonth
|
||||
)) {
|
||||
data.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function extractDataForDay(weeknameNode, weekName, date) {
|
||||
let result = [];
|
||||
weekName = weekName.split(" ")[0];
|
||||
let placeOfEvent = weeknameNode[0]["w:tc"]["w:p"][1]["w:r"]["w:t"] ?? weeknameNode[0]["w:tc"]["w:p"][1]["w:r"]["0"]["w:t"];
|
||||
|
||||
let names = [];
|
||||
let shiftNr = 0;
|
||||
let tbl = weeknameNode;
|
||||
|
||||
for (const trKey in tbl) {
|
||||
try {
|
||||
const weekNameNodes = findWeekNameNodes(tbl[trKey]);
|
||||
if (weekNameNodes.length > 0) {
|
||||
if (names && names.length > 0) {
|
||||
shiftNr = 0;
|
||||
}
|
||||
dayOfMonth = date.getDate(); //tbl[trKey]["w:tc"]["w:p"][0]["w:r"]["w:t"].match(/(\d+)/)[1];
|
||||
weekName = weekName; // tbl[trKey]["w:tc"]["w:p"][0]["w:r"]["w:t"].match(/(\S+) (\d+)/)[1];
|
||||
placeOfEvent = placeOfEvent.trim();//tbl[trKey]["w:tc"]["w:p"][1]["w:r"]["w:t"];
|
||||
continue;
|
||||
}
|
||||
const tr = tbl[trKey];
|
||||
console.log("Processin row: " + JSON.stringify(tr));
|
||||
|
||||
let time = tr["w:tc"]?.[1]?.["w:p"]?.["w:r"]?.["w:t"] ?? tr["w:tc"]?.[1]?.["w:p"]?.[0]?.["w:r"]?.["w:t"];
|
||||
let transport = tr["w:tc"]?.[3]?.["w:p"]?.["w:r"]?.[1]?.["w:t"];
|
||||
|
||||
|
||||
let namesPath = ["w:tc", 2, "w:p"];
|
||||
try {
|
||||
names = [getTextContent(safelyAccess(tr, namesPath))].join("").trim();
|
||||
} catch (e) {
|
||||
console.log("try to parse names:" + names + "; " + e + " " + JSON.stringify(tr["w:tc"] + " " + trKey) + e.stack);
|
||||
}
|
||||
//if starts with "Докарва" or empty - try the first cell instead of second
|
||||
if (names.startsWith("Докарва") || names.startsWith("Прибира ") || names === "") {
|
||||
transport = names;
|
||||
time = getTextContent(safelyAccess(tr, ["w:tc", 0, "w:p"]));
|
||||
namesPath = ["w:tc", 1, "w:p"];
|
||||
try {
|
||||
names = [getTextContent(safelyAccess(tr, namesPath))].join("").trim();
|
||||
} catch (e) {
|
||||
console.log("try to parse names:" + names + "; " + e + " " + JSON.stringify(tr["w:tc"] + " " + trKey) + e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
names = names.split(",").map((name) => name.trim()).filter((name) => name !== "");
|
||||
|
||||
shiftNr++;
|
||||
result.push({
|
||||
date,
|
||||
dayOfWeek: weekName,
|
||||
dayOfMonth: date.getDate(),
|
||||
placeOfEvent,
|
||||
shiftNr,
|
||||
time,
|
||||
names,
|
||||
transport,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("failed extracting data from node " + trKey + ": " + e + ": " + e.stack);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function safelyAccess(obj, path) {
|
||||
return path.reduce((acc, key) => (acc && key in acc) ? acc[key] : undefined, obj);
|
||||
}
|
||||
const getTextContent = (obj) => {
|
||||
let textContent = '';
|
||||
|
||||
const traverse = (node) => {
|
||||
if (typeof node === 'string') {
|
||||
textContent += node;
|
||||
} else if (Array.isArray(node)) {
|
||||
node.forEach((child) => traverse(child));
|
||||
} else if (typeof node === 'object') {
|
||||
Object.values(node).forEach((child) => traverse(child));
|
||||
}
|
||||
};
|
||||
|
||||
traverse(obj);
|
||||
return textContent;
|
||||
};
|
||||
|
||||
// ImportSchedule("./content/sources/march.json", 3);
|
||||
//
|
||||
function GenerateFlatJsonFile() {
|
||||
let data = JSON.parse(fs.readFileSync("./content/sources/test.json", "utf8"));
|
||||
let data_flat = transformJsonToFlat(data, 3);
|
||||
fs.writeFileSync(
|
||||
"./content/sources/march_flat.json",
|
||||
JSON.stringify(data_flat)
|
||||
);
|
||||
}
|
||||
|
||||
function transformJsonToFlat(inputJson, month) {
|
||||
const output = [];
|
||||
let dayNr = 0;
|
||||
|
||||
inputJson.events.forEach((event) => {
|
||||
const date = new Date(2023, month - 1, event.dayOfMonth + 1);
|
||||
dayNr++;
|
||||
let shiftNr = 1;
|
||||
event.shifts.forEach((shift) => {
|
||||
const shiftNames = shift.names.split(",").map((name) => name.trim());
|
||||
|
||||
// if (shift.transport !== null) {
|
||||
// shiftNames.push(shift.transport);
|
||||
// }
|
||||
|
||||
output.push({
|
||||
date: common.getISODateOnly(date),
|
||||
dayOfWeek: event.dayOfWeek,
|
||||
dayNr,
|
||||
shiftNr: shiftNr++,
|
||||
time: shift.time,
|
||||
names: shiftNames,
|
||||
transport: shift.transport,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
4
src/helpers/imports.js
Normal file
4
src/helpers/imports.js
Normal file
@ -0,0 +1,4 @@
|
||||
//??? can we consolidate all imports into one file?
|
||||
import ProtectedRoute from '../../../components/protectedRoute';
|
||||
import axiosInstance from '../../../src/axiosSecure';
|
||||
import Layout from "../../../components/layout";
|
16
src/middleware/errorHandler.js
Normal file
16
src/middleware/errorHandler.js
Normal file
@ -0,0 +1,16 @@
|
||||
export { errorHandler };
|
||||
|
||||
function errorHandler(err, res) {
|
||||
if (typeof (err) === 'string') {
|
||||
// custom application error
|
||||
return res.status(400).json({ message: err });
|
||||
}
|
||||
|
||||
if (err.name === 'UnauthorizedError') {
|
||||
// jwt authentication error
|
||||
return res.status(401).json({ message: 'Invalid Token' });
|
||||
}
|
||||
|
||||
// default to 500 server error
|
||||
return res.status(500).json({ message: err.message });
|
||||
}
|
18
src/middleware/jwtMiddleware.js
Normal file
18
src/middleware/jwtMiddleware.js
Normal file
@ -0,0 +1,18 @@
|
||||
const expressJwt = require('express-jwt');
|
||||
const util = require('util');
|
||||
import getConfig from 'next/config';
|
||||
|
||||
const { serverRuntimeConfig } = getConfig();
|
||||
|
||||
export { jwtMiddleware };
|
||||
|
||||
function jwtMiddleware(req, res) {
|
||||
const middleware = expressJwt({ secret: serverRuntimeConfig.secret, algorithms: ['HS256'] }).unless({
|
||||
path: [
|
||||
// public routes that don't require authentication
|
||||
'/api/users/authenticate'
|
||||
]
|
||||
});
|
||||
|
||||
return util.promisify(middleware)(req, res);
|
||||
}
|
46
src/sql/dev-test.sql
Normal file
46
src/sql/dev-test.sql
Normal file
@ -0,0 +1,46 @@
|
||||
DROP USER 'cart'@'%'
|
||||
|
||||
CREATE USER 'cart'@'%' IDENTIFIED BY PASSWORD 'cart2023';
|
||||
CREATE USER 'cart'@'%' IDENTIFIED BY PASSWORD '*DC4D6DF107711946088A6FA2ED3AD2074BC8F1BA';--/cart2023/
|
||||
|
||||
GRANT ALL PRIVILEGES ON cart.* TO 'cart'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
|
||||
SELECT DISTINCT Publisher.*
|
||||
FROM Publisher
|
||||
INNER JOIN Availability ON Availability.publisherId = Publisher.id
|
||||
WHERE
|
||||
Availability.isactive = true
|
||||
AND ( (
|
||||
Availability.dayOfMonth IS NOT NULL
|
||||
AND Availability.startTime <= '2023-03-30 13:00:00'
|
||||
AND Availability.endTime >= '2023-03-30 13:00:00'
|
||||
AND DAYOFMONTH(Availability.startTime) = DAYOFMONTH('2023-03-30')
|
||||
)
|
||||
OR (
|
||||
Availability.dayOfMonth IS NULL
|
||||
AND DAYOFWEEK(Availability.startTime) = DAYOFWEEK('2023-03-30')
|
||||
AND Availability.startTime <= '2023-03-30 13:00:00'
|
||||
AND Availability.endTime >= '2023-03-30 13:00:00'
|
||||
)
|
||||
);
|
||||
|
||||
SELECT DISTINCT Publisher.*
|
||||
FROM Publisher
|
||||
INNER JOIN Availability ON Availability.publisherId = Publisher.id
|
||||
WHERE
|
||||
Availability.isactive = true
|
||||
AND (Availability.dayOfMonth = 5) clfuyo33e005aknvchf1wm3bu All publishers: 121;
|
||||
|
||||
(118) unique,
|
||||
3 dupliates: clfuymbf6000sknvc9bvas2ee georgiggg @gmail.com,
|
||||
2;
|
||||
|
||||
clfuymcbu000yknvcmjlg27qn julanda_debora@yahoo.com,2;
|
||||
|
||||
clfuymbpn000uknvckz1g9cu5 my.room.is.a.relaxing.place @gmail.com,
|
||||
2
|
||||
SELECT DISTINCT Availability.*
|
||||
FROM Availability
|
||||
WHERE Availability.Pu
|
116
src/templates/word.html
Normal file
116
src/templates/word.html
Normal file
@ -0,0 +1,116 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Event Schedule</title>
|
||||
<style>
|
||||
/* template */
|
||||
/* Word-specific styles */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: none;
|
||||
margin-top: 20px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border: 1px solid black;
|
||||
padding: 4px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12.5pt;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid black;
|
||||
padding: 2px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
font-size: 12.5pt;
|
||||
}
|
||||
|
||||
|
||||
/* Word-specific styles */
|
||||
@media screen and (min-width: 0\0) {
|
||||
table {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
border: 1px solid #000000 !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* custom styles */
|
||||
.col1 {
|
||||
text-align: center;
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.col2 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
/* Define different header colors for each week */
|
||||
.week1 {
|
||||
background-color: #FFF766;
|
||||
}
|
||||
|
||||
.week2 {
|
||||
background-color: #B4C6E7;
|
||||
}
|
||||
|
||||
.week3 {
|
||||
background-color: #ED7D31;
|
||||
}
|
||||
|
||||
.week4 {
|
||||
background-color: #BFBFBF;
|
||||
}
|
||||
|
||||
.week5 {
|
||||
background-color: #C785C8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{#each events}}
|
||||
<div style="page-break-before: always;"></div>
|
||||
<table>
|
||||
<thead class="week{{week}}">
|
||||
<tr>
|
||||
<th colspan="3">{{dayOfWeek}} {{dayOfMonth}}<br />
|
||||
{{placeOfEvent}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each shifts}}
|
||||
<tr>
|
||||
<td class="col1">{{time}}</td>
|
||||
<td class="col2">{{names}}</td>
|
||||
<td class="col3">{{notes}}<strong>{{notes_bold}}</strong></td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="page-break-before: always;"></div>
|
||||
<div style="margin-bottom: 50px;"></div>
|
||||
{{#if @last}}
|
||||
<!-- Don't add margin after the last table -->
|
||||
{{else}}
|
||||
<!-- Add margin after the table -->
|
||||
<br><br><br>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</body>
|
||||
|
||||
</html>
|
Reference in New Issue
Block a user