Merge branch 'main' into feature-fixStats

This commit is contained in:
Dobromir Popov
2024-05-11 16:33:41 +03:00
67 changed files with 2703 additions and 881 deletions

View File

@@ -26,9 +26,47 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
// appleWebApp: true,
// }
// (custom) Service worker registration and push notification logic
// function registerServiceWorkerAndPushNotifications() {
// useEffect(() => {
// const registerServiceWorker = async () => {
// if ('serviceWorker' in navigator) {
// try {
// const registration = await navigator.serviceWorker.register('/worker/index.js')
// .then((registration) => console.log('reg: ', registration));
// } catch (error) {
// console.log('Service Worker registration failed:', error);
// }
// }
// };
// const askForNotificationPermission = async () => {
// if ('serviceWorker' in navigator && 'PushManager' in window) {
// try {
// const permission = await Notification.requestPermission();
// if (permission === 'granted') {
// console.log('Notification permission granted.');
// // TODO: Subscribe the user to push notifications here
// } else {
// console.log('Notification permission not granted.');
// }
// } catch (error) {
// console.error('Error during service worker registration:', error);
// }
// } else {
// console.log('Service Worker or Push notifications not supported in this browser.');
// }
// };
// registerServiceWorker();
// askForNotificationPermission();
// }, []);
// }
//function SmwsApp({ Component, pageProps: { locale, messages, session, ...pageProps }, }: AppProps<{ session: Session }>) {
function SmwsApp({ Component, pageProps, session, locale, messages }) {
//registerServiceWorkerAndPushNotifications();
// dynamic locale loading using our API endpoint
// const [locale, setLocale] = useState(_locale);
// const [messages, setMessages] = useState(_messages);

View File

@@ -14,8 +14,7 @@ class MyDocument extends Document {
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="CCOM" />
<link rel="apple-touch-icon" href="/old-192x192.png"></link>
<link rel="apple-touch-icon" href="/favicon.ico"></link>
</Head>
<body>
<Main />

View File

@@ -45,15 +45,22 @@ export const authOptions: NextAuthOptions = {
}
}
}),
// AppleProvider({
// clientId: process.env.APPLE_APP_ID,
// clientSecret: process.env.APPLE_SECRET
// }),
// AzureADProvider({
// clientId: process.env.AZURE_AD_CLIENT_ID,
// clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
// tenantId: process.env.AZURE_AD_TENANT_ID,
// }),
AppleProvider({
// clientId: process.env.APPLE_APP_ID,
// clientSecret: process.env.APPLE_SECRET
clientId: process.env.APPLE_APP_ID,
clientSecret: {
appleId: process.env.APPLE_APP_ID,
teamId: process.env.APPLE_TEAM_ID,
privateKey: process.env.APPLE_PK,
keyId: process.env.APPLE_KEY_ID,
}
}),
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
}),
CredentialsProvider({
id: 'credentials',
// The name to display on the sign in form (e.g. 'Sign in with...')
@@ -79,9 +86,9 @@ export const authOptions: NextAuthOptions = {
// // Return null if user data could not be retrieved
// return null
const users = [
{ id: "1", name: "admin", email: "admin@example.com", password: "admin123", role: "ADMIN" },
{ id: "2", name: "krasi", email: "krasi@example.com", password: "krasi123", role: "ADMIN" },
{ id: "3", name: "popov", email: "popov@example.com", password: "popov123", role: "ADMIN" }
{ id: "1", name: "admin", email: "admin@example.com", password: "admin123", role: "ADMIN", static: true },
{ id: "2", name: "krasi", email: "krasi@example.com", password: "krasi123", role: "ADMIN", static: true },
{ id: "3", name: "popov", email: "popov@example.com", password: "popov123", role: "ADMIN", static: true }
];
const user = users.find(user =>
@@ -167,6 +174,10 @@ export const authOptions: NextAuthOptions = {
callbacks: {
// https://codevoweb.com/implement-authentication-with-nextauth-in-nextjs-14/
async signIn({ user, account, profile }) {
if (account.provider === 'credentials' && user?.static) {
return true;
}
var prisma = common.getPrismaClient();
console.log("[nextauth] signIn:", account.provider, user.email)
@@ -240,7 +251,10 @@ export const authOptions: NextAuthOptions = {
session.user.role = token.role;
session.user.name = token.name || token.email;
}
if (user?.impersonating) {
// Add flag to session if user is being impersonated
session.user.impersonating = true;
}
// if (session?.user) {
// session.user.id = user.id; //duplicate
// }

View File

@@ -27,6 +27,8 @@ export default async function handler(req, res) {
impersonating: true, // flag to indicate impersonation
originalUser: session.user // save the original user for later
};
// Log the event (simplified example)
console.log(`Admin ${session.user} impersonated user ${userToImpersonate.email} on ${new Date().toISOString()}`);
// Here you would typically use some method to create a session server-side
// For this example, we'll just send the impersonated session as a response

View File

@@ -1,12 +1,77 @@
import path from 'path';
import { promises as fs } from 'fs';
import express from 'express';
import type { NextApiRequest, NextApiResponse } from 'next';
import nc from 'next-connect';
import { promises as fs } from 'fs';
import path from 'path';
import multer from 'multer';
import sharp from 'sharp';
import { createRouter } from 'next-connect';
const handler = nc({
onError: (err, req, res, next) => {
// Generalized Multer configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const subfolder = req.query.subfolder ? decodeURIComponent(req.query.subfolder as string) : 'default';
const uploadPath = path.join(process.cwd(), 'public/content', subfolder);
fs.mkdir(uploadPath, { recursive: true })
.then(() => cb(null, uploadPath))
.catch(cb);
},
filename: (req, file, cb) => {
const filename = decodeURIComponent(file.originalname); // Ensure the filename is correctly decoded
const prefix = req.body.prefix ? decodeURIComponent(req.body.prefix) : path.parse(filename).name;
cb(null, `${prefix}${path.extname(filename)}`);
}
});
const fileFilter = (req, file, cb) => {
// Accept PDFs only
if (file.mimetype === 'application/pdf') {
cb(null, true);
} else {
cb(new Error('Only PDF files are allowed!'), false);
}
};
const upload = multer({ storage, fileFilter });
const router = createRouter<NextApiRequest, NextApiResponse>();
router.use(upload.array('file'));
router.post((req, res) => {
console.log(req.files); // Log files to see if PDFs are included
if (req.files.length === 0) {
return res.status(400).json({ error: 'No files were uploaded.' });
}
// Process uploaded files, assume images are being resized and saved
res.json({ message: 'Files uploaded successfully', files: req.files });
});
router.get(async (req, res) => {
// Implement functionality to list files
const directory = path.join(process.cwd(), 'public/content', req.query.subfolder as string);
try {
const files = await fs.readdir(directory);
const imageUrls = files.map(file => `/content/${req.query.subfolder}/${file}`);
res.json({ imageUrls });
} catch (err) {
res.status(500).json({ error: 'Internal Server Error', details: err.message });
}
});
router.delete(async (req, res) => {
// Implement functionality to delete a file
const filePath = path.join(process.cwd(), 'public/content', req.query.subfolder as string, req.query.file as string);
try {
await fs.unlink(filePath);
res.send('File deleted successfully.');
} catch (error) {
res.status(500).send('Failed to delete the file.');
}
});
export default router.handler({
onError: (err, req, res) => {
console.error(err.stack);
res.status(500).end('Something broke!');
},
@@ -15,131 +80,8 @@ const handler = nc({
}
});
handler.use((req: NextApiRequest, res: NextApiResponse, next) => {
const subfolder = req.query.subfolder as string;
const upload = createUploadMiddleware(subfolder).array('image');
upload(req, res, (err) => {
if (err) {
return res.status(500).json({ error: 'Failed to upload files.', details: err.message });
}
next();
});
});
handler.post((req: NextApiRequest, res: NextApiResponse) => {
// Process uploaded files
// Example response
res.json({ message: 'Files uploaded successfully', files: req.files });
});
handler.get((req: NextApiRequest, res: NextApiResponse) => {
// Handle listing files
//listFiles(req, res, req.subfolder);
listFiles(req, res, req.query.subfolder as string);
});
handler.delete((req: NextApiRequest, res: NextApiResponse) => {
// Handle deleting files
deleteFile(req, res, req.query.subfolder as string);
});
export const config = {
api: {
bodyParser: false,
},
};
export default handler;
// ------------------------------------------------------------
//handling file uploads
import multer from 'multer';
import sharp from 'sharp';
// Generalized Multer configuration
export const createUploadMiddleware = (folder: string) => {
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadPath = path.join(process.cwd(), 'public/content', folder);
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
cb(null, uploadPath);
},
filename: (req, file, cb) => {
const prefix = req.body.prefix || path.parse(file.originalname).name;
cb(null, `${prefix}${path.extname(file.originalname)}`);
}
});
return multer({ storage });
};
async function processFiles(req, res, folder) {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No files uploaded.' });
}
const uploadDir = path.join(process.cwd(), 'public/content', folder);
const thumbDir = path.join(uploadDir, "thumb");
if (!fs.existsSync(thumbDir)) {
fs.mkdirSync(thumbDir, { recursive: true });
}
try {
const processedFiles = await Promise.all(req.files.map(async (file) => {
const originalPath = path.join(uploadDir, file.filename);
const thumbPath = path.join(thumbDir, file.filename);
await sharp(file.path)
.resize({ width: 1920, fit: sharp.fit.inside, withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(originalPath);
await sharp(file.path)
.resize(320, 320, { fit: sharp.fit.inside, withoutEnlargement: true })
.toFile(thumbPath);
fs.unlinkSync(file.path); // Remove temp file
return {
originalUrl: `/content/${folder}/${file.filename}`,
thumbUrl: `/content/${folder}/thumb/${file.filename}`
};
}));
res.json(processedFiles);
} catch (error) {
console.error('Error processing files:', error);
res.status(500).json({ error: 'Error processing files.' });
}
}
// List files in a directory
async function listFiles(req, res, folder) {
const directory = path.join(process.cwd(), 'public/content', folder);
try {
const files = await fs.promises.readdir(directory);
const imageUrls = files.map(file => `${req.protocol}://${req.get('host')}/content/${folder}/${file}`);
res.json({ imageUrls });
} catch (err) {
console.error('Error reading uploads directory:', err);
res.status(500).json({ error: 'Internal Server Error' });
}
}
// Delete a file
async function deleteFile(req, res, folder) {
const filename = req.query.file;
if (!filename) {
return res.status(400).send('Filename is required.');
}
try {
const filePath = path.join(process.cwd(), 'public/content', folder, filename);
await fs.unlink(filePath);
res.status(200).send('File deleted successfully.');
} catch (error) {
res.status(500).send('Failed to delete the file.');
}
}

View File

@@ -106,6 +106,7 @@ export default async function handler(req, res) {
},
data: {
publisherId: userId,
originalPublisherId: originalPublisher.id,
publicGuid: null, // if this exists, we consider the request open
isConfirmed: true
}
@@ -161,7 +162,7 @@ export default async function handler(req, res) {
newPubs: newPubs,
placeName: assignment.shift.cartEvent.location.name,
dateStr: common.getDateFormated(assignment.shift.startTime),
time: common.formatTimeHHmm(assignment.shift.startTime),
time: common.getTimeFormatted(assignment.shift.startTime),
sentDate: common.getDateFormated(new Date())
};
@@ -383,7 +384,7 @@ export default async function handler(req, res) {
email: pubsToSend[i].email,
placeName: assignment.shift.cartEvent.location.name,
dateStr: common.getDateFormated(assignment.shift.startTime),
time: common.formatTimeHHmm(assignment.shift.startTime),
time: common.getTimeFormatted(assignment.shift.startTime),
sentDate: common.getDateFormated(new Date())
};
let results = emailHelper.SendEmailHandlebars(

View File

@@ -1,6 +1,8 @@
import { getToken } from "next-auth/jwt";
import { authOptions } from './auth/[...nextauth]'
import { getServerSession } from "next-auth/next"
import { NextApiRequest, NextApiResponse } from 'next'
import { DayOfWeek, AvailabilityType } from '@prisma/client';
import { DayOfWeek, AvailabilityType, UserRole, EventLogType } from '@prisma/client';
const common = require('../../src/helpers/common');
const dataHelper = require('../../src/helpers/data');
const subq = require('../../prisma/bl/subqueries');
@@ -9,6 +11,7 @@ import { addMinutes } from 'date-fns';
import fs from 'fs';
import path from 'path';
import { all } from "axios";
import { logger } from "src/helpers/common";
/**
*
@@ -46,6 +49,9 @@ export default async function handler(req, res) {
let monthInfo = common.getMonthDatesInfo(day);
const searchText = req.query.searchText?.normalize('NFC');
const sessionServer = await getServerSession(req, res, authOptions)
var isAdmin = sessionServer?.user.role == UserRole.ADMIN
try {
switch (action) {
case "initDb":
@@ -137,7 +143,7 @@ export default async function handler(req, res) {
break;
case "getCalendarEvents":
let events = await dataHelper.getCalendarEvents(req.query.publisherId, day);
let events = await dataHelper.getCalendarEvents(req.query.publisherId, true, true, isAdmin);
res.status(200).json(events);
case "getPublisherInfo":
@@ -817,10 +823,70 @@ async function replaceInAssignment(oldPublisherId, newPublisherId, shiftId) {
},
data: {
publisherId: newPublisherId,
originalPublisherId: oldPublisherId,
isConfirmed: false,
isBySystem: true,
isMailSent: false
}
});
// log the event
let shift = await prisma.shift.findUnique({
where: {
id: shiftId
},
select: {
startTime: true,
cartEvent: {
select: {
location: {
select: {
name: true
}
}
}
}
},
include: {
assignments: {
include: {
publisher: {
select: {
firstName: true,
lastName: true,
email: true
}
}
}
}
}
});
let publishers = await prisma.publisher.findMany({
where: {
id: { in: [oldPublisherId, newPublisherId] }
},
select: {
id: true,
firstName: true,
lastName: true,
email: true
}
});
let originalPublisher = publishers.find(p => p.id == oldPublisherId);
let newPublisher = publishers.find(p => p.id == newPublisherId);
let eventLog = await prisma.eventLog.create({
data: {
date: new Date(),
publisher: { connect: { id: oldPublisherId } },
shift: { connect: { id: shiftId } },
type: EventLogType.AssignmentReplacementManual,
content: "Въведено заместване от " + originalPublisher.firstName + " " + originalPublisher.lastName + ". Ще го замества " + newPublisher.firstName + " " + newPublisher.lastName + "."
}
});
logger.info("User: " + originalPublisher.email + " replaced his assignment for " + shift.cartEvent.location.name + " " + shift.startTime.toISOString() + " with " + newPublisher.firstName + " " + newPublisher.lastName + "<" + newPublisher.email + ">. EventLogId: " + eventLog.id + "");
return result;
}

View File

@@ -1,33 +1,139 @@
const webPush = require('web-push')
import common from '../../src/helpers/common';
//generate and store VAPID keys in .env.local if not already done
if (!process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY || !process.env.WEB_PUSH_PRIVATE_KEY) {
const { publicKey, privateKey } = webPush.generateVAPIDKeys()
console.log('VAPID keys generated:')
console.log('Public key:', publicKey)
console.log('Private key:', privateKey)
console.log('Store these keys in your .env.local file:')
console.log('NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY=', publicKey)
console.log('WEB_PUSH_PRIVATE_KEY=', privateKey)
process.exit(0)
}
webPush.setVapidDetails(
`mailto:${process.env.WEB_PUSH_EMAIL}`,
process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY,
process.env.WEB_PUSH_PRIVATE_KEY
)
const Notification = (req, res) => {
if (req.method == 'POST') {
const { subscription } = req.body
const Notification = async (req, res) => {
if (req.method == 'GET') {
res.statusCode = 200
res.setHeader('Allow', 'POST')
let subs = 0
if (req.query && req.query.id) {
const prisma = common.getPrismaClient();
const publisher = await prisma.publisher.findUnique({
where: { id: req.query.id },
select: { pushSubscription: true }
});
subs = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription.length : (publisher.pushSubscription ? 1 : 0);
res.end()
return
}
// send the public key in the response headers
//res.setHeader('Content-Type', 'text/plain')
res.send({ pk: process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY, subs })
res.end()
}
if (req.method == 'PUT') {
// store the subscription object in the database
// publisher.pushSubscription = subscription
const prisma = common.getPrismaClient();
const { subscription, id } = req.body
const publisher = await prisma.publisher.findUnique({
where: { id },
select: { pushSubscription: true }
});
webPush
.sendNotification(
subscription,
JSON.stringify({ title: 'Hello Web Push', message: 'Your web push notification is here!' })
)
.then(response => {
res.writeHead(response.statusCode, response.headers).end(response.body)
})
.catch(err => {
if ('statusCode' in err) {
res.writeHead(err.statusCode, err.headers).end(err.body)
} else {
console.error(err)
res.statusCode = 500
res.end()
}
})
let subscriptions = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription : (publisher.pushSubscription ? [publisher.pushSubscription] : []);
const index = subscriptions.findIndex(sub => sub.endpoint === subscription.endpoint);
if (index !== -1) {
subscriptions[index] = subscription; // Update existing subscription
} else {
subscriptions.push(subscription); // Add new subscription
}
await prisma.publisher.update({
where: { id },
data: { pushSubscription: subscriptions }
});
console.log('Subscription for publisher', id, 'updated:', subscription)
res.send({ subs: subscriptions.length })
res.statusCode = 200
res.end()
}
if (req.method == 'DELETE') {
// remove the subscription object from the database
// publisher.pushSubscription = null
const prisma = common.getPrismaClient();
const { subscriptionId, id } = req.body;
const publisher = await prisma.publisher.findUnique({
where: { id },
select: { pushSubscription: true }
});
let subscriptions = Array.isArray(publisher.pushSubscription) ? publisher.pushSubscription : (publisher.pushSubscription ? [publisher.pushSubscription] : []);
try {
subscriptions = subscriptionId ? subscriptions.filter(sub => sub.endpoint !== subscriptionId) : [];
await prisma.publisher.update({
where: { id },
data: { pushSubscription: subscriptions }
});
} catch (e) {
console.log(e)
await prisma.publisher.update({
where: { id },
data: { pushSubscription: null }
});
}
console.log('Subscription for publisher', id, 'deleted')
res.send({ subs: subscriptions.length })
res.statusCode = 200
res.end()
}
if (req.method == 'POST') {//title = "ССС", message = "Ще получите уведомление по този начин.")
const { subscription, id, broadcast, title = 'ССОМ', message = 'Ще получавате уведомления така.', actions } = req.body
if (broadcast) {
await broadcastPush(title, message, actions)
res.statusCode = 200
res.end()
return
}
else if (id) {
await sendPush(id, title, message.actions)
res.statusCode = 200
res.end()
return
} else if (subscription) {
await webPush
.sendNotification(
subscription,
JSON.stringify({ title, message, actions })
)
.then(response => {
res.writeHead(response.statusCode, response.headers).end(response.body)
})
.catch(err => {
if ('statusCode' in err) {
res.writeHead(err.statusCode, err.headers).end(err.body)
} else {
console.error(err)
res.statusCode = 500
res.end()
}
})
}
} else {
res.statusCode = 405
res.end()
@@ -35,3 +141,54 @@ const Notification = (req, res) => {
}
export default Notification
//export pushNotification(userId or email) for use in other files
export const sendPush = async (id, title, message, actions) => {
const prisma = common.getPrismaClient();
const publisher = await prisma.publisher.findUnique({
where: { id }
})
if (!publisher.pushSubscription) {
console.log('No push subscription found for publisher', id)
return
}
await webPush
.sendNotification(
publisher.pushSubscription,
JSON.stringify({ title, message, actions })
)
.then(response => {
console.log('Push notification sent to publisher', id)
})
.catch(err => {
console.error('Error sending push notification to publisher', id, ':', err)
})
}
//export breoadcastNotification for use in other files
export const broadcastPush = async (title, message, actions) => {
const prisma = common.getPrismaClient();
const publishers = await prisma.publisher.findMany({
where: { pushSubscription: { not: null } }
})
for (const publisher of publishers) {
if (Array.isArray(publisher.pushSubscription) && publisher.pushSubscription.length) {
for (const subscription of publisher.pushSubscription) {
await webPush.sendNotification(
subscription, // Here subscription is each individual subscription object
JSON.stringify({ title, message, actions })
)
.then(response => {
console.log('Push notification sent to device', subscription.endpoint, 'of publisher', publisher.id);
})
.catch(err => {
console.error('Error sending push notification to device', subscription.endpoint, 'of publisher', publisher.id, ':', err);
// Optionally handle failed subscriptions, e.g., remove outdated or invalid subscriptions
});
}
} else {
console.log('No valid subscriptions found for publisher', publisher.id);
}
}
}

View File

@@ -58,10 +58,7 @@ export default function SignIn({ csrfToken }) {
<div className="page">
<div className="signin">
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-100">
{/* Page Title */}
<h1 className="text-2xl font-bold text-gray-900 mt-6">Вход</h1>
{/* Section for Social Sign-On Providers */}
<div className="mt-8 w-full max-w-md px-4 py-8 bg-white shadow rounded-lg">
{/* <h2 className="text-center text-lg font-semibold text-gray-900 mb-4">Sign in with a Social Media Account</h2> */}
<button onClick={() => signIn('google', { callbackUrl: '/' })}
@@ -70,22 +67,24 @@ export default function SignIn({ csrfToken }) {
src="https://authjs.dev/img/providers/google.svg" className="mr-2" />
Влез чрез Google
</button>
{/* Add more buttons for other SSO providers here in similar style */}
{/* Apple Sign-In Button */}
{/* <button onClick={() => signIn('apple', { callbackUrl: '/' })}
className="mt-4 flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<img loading="lazy" height="24" width="24" alt="Apple logo"
src="https://authjs.dev/img/providers/apple.svg" className="mr-2" />
Влез чрез Apple
</button> */}
{/* microsoft */}
{/* <button onClick={() => signIn('azure-ad', { callbackUrl: '/' })}
className="mt-4 flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<img loading="lazy" height="24" width="24" alt="Microsoft logo"
src="https://authjs.dev/img/providers/azure-ad.svg" className="mr-2" />
Влез чрез Microsoft
</button> */}
</div>
{/* Apple Sign-In Button */}
<button onClick={() => signIn('apple', { callbackUrl: '/' })}
className="mt-4 flex items-center justify-center w-full py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<img loading="lazy" height="24" width="24" alt="Apple logo"
src="https://authjs.dev/img/providers/apple.svg" className="mr-2" />
Влез чрез Apple
</button>
{/* Divider (Optional) */}
<div className="w-full max-w-xs mt-8 mb-8">
<hr className="border-t border-gray-300" />
</div>
{/* Local Account Email and Password Form */}
<div className="w-full max-w-md mt-8 mb-8 px-4 py-8 bg-white shadow rounded-lg">
<h2 className="text-center text-lg font-semibold text-gray-900 mb-4">Влез с локален акаунт</h2>
<form onSubmit={handleSubmit}>
@@ -131,9 +130,11 @@ export default function SignIn({ csrfToken }) {
// This gets called on every request
export async function getServerSideProps(context) {
const csrfToken = await getCsrfToken(context);
return {
props: {
csrfToken: await getCsrfToken(context),
...(csrfToken ? { csrfToken } : {}),
},
};
}

View File

@@ -15,6 +15,9 @@ import { toast } from 'react-toastify';
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
// import notify api
import { sendPush, broadcastPush } from '../../api/notify';
const { DateTime } = require('luxon');
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
@@ -544,7 +547,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
var dayName = common.DaysOfWeekArray[value.getDayEuropean()];
const cartEvent = events.find(event => event.dayofweek == dayName);
lastShift = {
endTime: new Date(value.setHours(9, 0, 0, 0)),
endTime: DateTime.fromJSDate(value).setZone('Europe/Sofia', { keepLocalTime: true }).set({ hour: 9 }).toJSDate(),
cartEventId: cartEvent.id
};
}
@@ -733,7 +736,19 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
<span tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
<button tooltip="желани участия този месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
<button tooltip="желани участия на месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
<button tooltip="push" title="push" className={`badge py-1 px-2 rounded-md text-xs bg-red-100`}
onClick={async () => {
await fetch('/api/notify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ broadcast: true, message: "Тестово съобщение", title: "Това е тестово съобщение от https://sofia.mwitnessing.com" })
})
}}
>+</button>
</div>
</li>
);

View File

@@ -4,6 +4,7 @@ import Layout from "../../../components/layout";
import LocationCard from "../../../components/location/LocationCard";
import axiosServer from '../../../src/axiosServer';
import ProtectedRoute from '../../../components/protectedRoute';
import CongregationCRUD from "../publishers/congregationCRUD";
interface IProps {
item: Location;
}
@@ -32,6 +33,7 @@ function LocationsPage({ items = [] }: IProps) {
</a>
</div>
</ProtectedRoute>
<CongregationCRUD />
</Layout>
);
}

View File

@@ -0,0 +1,103 @@
// a simple CRUD componenet for congregations for admins
import { useEffect, useState } from 'react';
import axiosInstance from '../../../src/axiosSecure';
import toast from 'react-hot-toast';
import Layout from '../../../components/layout';
import ProtectedRoute from '../../../components/protectedRoute';
import { UserRole } from '@prisma/client';
import { useRouter } from 'next/router';
export default function CongregationCRUD() {
const [congregations, setCongregations] = useState([]);
const [newCongregation, setNewCongregation] = useState('');
const router = useRouter();
const fetchCongregations = async () => {
try {
const { data: congregationsData } = await axiosInstance.get(`/api/data/congregations`);
setCongregations(congregationsData);
} catch (error) {
console.error(error);
}
};
const addCongregation = async () => {
try {
await axiosInstance.post(`/api/data/congregations`, { name: newCongregation, address: "" });
toast.success('Успешно добавен сбор');
setNewCongregation('');
fetchCongregations();
} catch (error) {
console.error(error);
}
};
const deleteCongregation = async (id) => {
try {
await axiosInstance.delete(`/api/data/congregations/${id}`);
toast.success('Успешно изтрит сбор');
fetchCongregations();
} catch (error) {
console.error(error);
}
};
useEffect(() => {
fetchCongregations();
}, []);
return (
<ProtectedRoute allowedRoles={[UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-start px-4 pt-8">
<div className="flex flex-col w-full px-4">
<h1 className="text-2xl font-bold text-center">Сборове</h1>
<div className="flex gap-2 mb-4">
<input
type="text"
value={newCongregation}
onChange={(e) => setNewCongregation(e.target.value)}
placeholder="Име на сбор"
className="px-4 py-2 rounded-md border border-gray-300"
/>
<button
onClick={addCongregation}
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
>
Добави
</button>
</div>
<table className="w-full">
<thead>
<tr>
<th>Име</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{congregations.map((congregation) => (
<tr key={congregation.id}>
<td>{congregation.name}</td>
<td className='right'>
{/* <button
onClick={() => router.push(`/cart/publishers/congregation/${congregation.id}`)}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Преглед
</button> */}
<button
onClick={() => deleteCongregation(congregation.id)}
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Изтрий
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</ProtectedRoute>
);
}

View File

@@ -64,13 +64,23 @@ export const getServerSideProps = async (context) => {
}
});
if (!item) {
const user = context.req.session.user;
const user = context.req.session?.user;
if (!user) {
return {
// redirect to '/auth/signin'. assure it is not relative path
redirect: {
destination: process.env.NEXT_PUBLIC_PUBLIC_URL + "/auth/signin",
permanent: false,
}
}
}
const message = encodeURIComponent(`Този имейл (${user?.email}) не е регистриран. Моля свържете се с администратора.`);
return {
redirect: {
destination: '/message?message=Този имейл (' + user.email + ') не е регистриран. Моля свържете се с администратора.',
destination: `/message?message=${message}`,
permanent: false,
},
}
};
}
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);

View File

@@ -11,7 +11,6 @@ import * as XLSX from "xlsx";
// import { Table } from "react-bootstrap";
import { staticGenerationAsyncStorage } from "next/dist/client/components/static-generation-async-storage.external";
import moment from 'moment';
// import { DatePicker } from '@mui/x-date-pickers'; !! CAUSERS ERROR ???
// import { DatePicker } from '@mui/x-date-pickers/DatePicker';

View File

@@ -60,7 +60,7 @@ export default function MySchedulePage({ assignments }) {
<div className="container ">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-center my-4">Моите смени</h1>
<div className="space-y-4">
{assignments && assignments.map((assignment) => (
{assignments && assignments.length > 0 ? (assignments.map((assignment) => (
<div key={assignment.dateStr + assignments.indexOf(assignment)} className="bg-white shadow overflow-hidden rounded-lg">
<div className="px-4 py-5 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">{assignment.dateStr}</h3>
@@ -117,7 +117,13 @@ export default function MySchedulePage({ assignments }) {
</dl>
</div>
</div>
))}
))) :
<div className="bg-white shadow overflow-hidden rounded-lg">
<div className="px-4 py-5 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">За сега нямате бъдещи назначени смени. Можете да проверите дали вашите възножности са актуални.</h3>
</div>
</div>
}
</div>
</div>
<Modal isOpen={isModalOpen}
@@ -168,10 +174,12 @@ export const getServerSideProps = async (context) => {
}
const prisma = common.getPrismaClient();
const monthInfo = common.getMonthInfo(new Date());
//minus 1 day from the firstMonday to get the last Sunday
const lastSunday = new Date(monthInfo.firstMonday);
lastSunday.setDate(lastSunday.getDate() - 1);
let today = new Date();
today.setHours(0, 0, 0, 0);
// const monthInfo = common.getMonthInfo(today);
// //minus 1 day from the firstMonday to get the last Sunday
// const lastSunday = new Date(monthInfo.firstMonday);
// lastSunday.setDate(lastSunday.getDate() - 1);
const publisher = await prisma.publisher.findUnique({
where: {
id: session.user.id,
@@ -179,7 +187,7 @@ export const getServerSideProps = async (context) => {
some: {
shift: {
startTime: {
gte: lastSunday,
gte: today,
},
},
},
@@ -208,7 +216,7 @@ export const getServerSideProps = async (context) => {
},
});
const assignments = publisher?.assignments.filter(a => a.shift.startTime >= lastSunday && a.shift.isPublished) || [];
const assignments = publisher?.assignments.filter(a => a.shift.startTime >= today && a.shift.isPublished) || [];
const transformedAssignments = assignments?.sort((a, b) => a.shift.startTime - b.shift.startTime)

View File

@@ -99,7 +99,7 @@ function ContactsPage({ allPublishers }) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
<div className="container mx-auto p-4">
<h1 className="text-xl font-semibold mb-4">Статистика </h1>
<h5 className="text-lg font-semibold mb-4">{pubWithAssignmentsCount} участника с предпочитания за месеца (от {filteredPublishers.length} )</h5>

View File

@@ -42,7 +42,7 @@ export default function EventLogList() {
}, []);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-start px-4 pt-8">
<div className="flex flex-col w-full px-4">

View File

@@ -9,7 +9,7 @@ import ProtectedRoute from '../../../components/protectedRoute';
function NewPage(loc: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-center">
<ExperienceForm />
</div></ProtectedRoute>

View File

@@ -83,7 +83,7 @@ export default function Reports() {
}, []);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-start px-4 pt-8">
<div className="flex flex-col w-full px-4">

View File

@@ -9,7 +9,7 @@ import ProtectedRoute from '../../../components/protectedRoute';
function NewPage(loc: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-center">
<ReportForm />
</div></ProtectedRoute>

View File

@@ -15,13 +15,16 @@ import { getServerSession } from "next-auth/next"
import PublisherSearchBox from '../components/publisher/PublisherSearchBox';
import PublisherInlineForm from '../components/publisher/PublisherInlineForm';
import CartEventForm from "components/cartevent/CartEventForm";
interface IProps {
initialItems: Availability[];
initialUserId: string;
cartEvents: any;
lastPublishedDate: Date;
}
export default function IndexPage({ initialItems, initialUserId }: IProps) {
export default function IndexPage({ initialItems, initialUserId, cartEvents, lastPublishedDate }: IProps) {
const { data: session } = useSession();
const [userName, setUserName] = useState(session?.user?.name);
const [userId, setUserId] = useState(initialUserId);
@@ -68,7 +71,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
return (
<Layout>
<ProtectedRoute deniedMessage="">
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER, UserRole.EXTERNAL]} deniedMessage="">
<h1 className="pt-2 pb-1 text-xl font-bold text-center">Графика на {userName}</h1>
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
<PublisherSearchBox selectedId={userId} infoText="" onChange={handleUserSelection} />
@@ -78,7 +81,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
<div className="text-center font-bold pb-3 xs:pb-1">
<PublisherInlineForm publisherId={userId} />
</div>
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} />
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} cartEvents={cartEvents} lastPublishedDate={lastPublishedDate} />
</div>
</div>
</ProtectedRoute>
@@ -119,7 +122,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
// ...item,
// startTime: item.startTime.toISOString(),
// endTime: item.endTime.toISOString(),
// name: common.getTimeFomatted(item.startTime) + "-" + common.getTimeFomatted(item.endTime),
// name: common.getTimeFormatted(item.startTime) + "-" + common.getTimeFormatted(item.endTime),
// //endDate can be null
// endDate: item.endDate ? item.endDate.toISOString() : null,
// type: 'availability',
@@ -175,7 +178,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
// 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)),
// name: common.getTimeFormatted(new Date(item.shift.startTime)) + "-" + common.getTimeFormatted(new Date(item.shift.endTime)),
// type: 'assignment',
// //delete shift object
// shift: null,
@@ -193,29 +196,84 @@ export const getServerSideProps = async (context) => {
req: context.req,
allowedRoles: [/* ...allowed roles... */]
});
const session = await getSession(context);
// const session = await getSession(context);
const sessionServer = await getServerSession(context.req, context.res, authOptions)
if (!session) { return { props: {} } }
const role = session?.user.role;
console.log("server role: " + role);
const userId = session?.user.id;
if (!sessionServer) {
return {
redirect: {
destination: '/auth/signin',
permanent: false,
},
};
}
var items = await dataHelper.getCalendarEvents(session.user.id);
const role = sessionServer?.user.role;
console.log("server role: " + role);
const userId = sessionServer?.user.id;
var isAdmin = sessionServer?.user.role == UserRole.ADMIN;//role.localeCompare(UserRole.ADMIN) === 0;
var items = await dataHelper.getCalendarEvents(userId, true, true, isAdmin);
// common.convertDatesToISOStrings(items);
//serializable dates
items = items.map(item => ({
...item,
startTime: item.startTime.toISOString(),
endTime: item.endTime.toISOString(),
date: item.date.toISOString(),
}));
items = items.map(item => {
const updatedItem = {
...item,
startTime: item.startTime.toISOString(),
endTime: item.endTime.toISOString(),
date: item.date.toISOString(),
name: common.getTimeFormatted(item.startTime) + "-" + common.getTimeFormatted(item.endTime)
};
if (updatedItem.shift) {
updatedItem.shift = {
...updatedItem.shift,
startTime: updatedItem.shift.startTime.toISOString(),
endTime: updatedItem.shift.endTime.toISOString()
};
updatedItem.isPublished = updatedItem.shift.isPublished;
}
return updatedItem;
});
// log first availability startTime to verify timezone and UTC conversion
console.log("First availability startTime: " + items[0]?.startTime);
console.log("First availability startTime: " + items[0]?.startTime.toLocaleString());
const prisma = common.getPrismaClient();
let cartEvents = await prisma.cartEvent.findMany({
where: {
isActive: true,
},
select: {
id: true,
startTime: true,
endTime: true,
dayofweek: true,
shiftDuration: true,
}
});
cartEvents = common.convertDatesToISOStrings(cartEvents);
const lastPublishedDate = (await prisma.shift.findFirst({
where: {
isPublished: true,
},
select: {
endTime: true,
},
orderBy: {
endTime: 'desc'
}
})).endTime;
return {
props: {
initialItems: items,
userId: session?.user.id,
userId: sessionServer?.user.id,
cartEvents: cartEvents,
lastPublishedDate: lastPublishedDate.toISOString(),
// messages: (await import(`../content/i18n/${context.locale}.json`)).default
},
};

View File

@@ -5,6 +5,7 @@ import path from 'path';
import { url } from 'inspector';
import ProtectedRoute, { serverSideAuth } from "/components/protectedRoute";
import axiosInstance from '../src/axiosSecure';
import { UserRole } from "@prisma/client";
const PDFViewerPage = ({ pdfFiles }) => {
@@ -22,17 +23,23 @@ const PDFViewerPage = ({ pdfFiles }) => {
const handleFileUpload = async (event) => {
const file = event.target.files[0];
//utf-8 encoding
// const formData = new FormData();
const formData = new FormData();
formData.append('file', file);
// formData.append('file', file);
const newFile = new File([file], encodeURI(file.name), { type: file.type });
formData.append('file', newFile);
const subfolder = 'permits'; // Change this as needed based on your subfolder structure
try {
const response = await axiosInstance.post(`/api/content/${subfolder}`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
'Content-Type': 'multipart/form-data',
// 'Content-Encoding': 'utf-8'
}
});
setFiles([...files, response.data]);
const newFiles = response.data.files.map(file => ({ name: decodeURIComponent(file.originalname), url: file.path }));
setFiles([...files, ...newFiles]);
} catch (error) {
console.error('Error uploading file:', error);
}
@@ -42,18 +49,36 @@ const PDFViewerPage = ({ pdfFiles }) => {
return (
<Layout>
<h1 className="text-3xl font-bold p-4 pt-8">Разрешителни</h1>
<ProtectedRoute>
<input type="file" onChange={handleFileUpload} className="mb-4" />
{files.map((file, index) => (
<div key={file.name} className="py-2">
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
{file.name}
</a>
<button onClick={() => handleFileDelete(file.name)} className="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">
изтрий
</button>
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage="">
<div className="border border-blue-500 p-4 rounded shadow-md">
<div className="mb-6">
<p className="text-lg mb-2">За да качите файл кликнете на бутона по-долу и изберете файл от вашия компютър.</p>
<input type="file" onChange={handleFileUpload} className="block w-full text-sm text-gray-600
file:mr-4 file:py-2 file:px-4
file:border-0
file:text-sm file:font-semibold
file:bg-blue-500 file:text-white
hover:file:bg-blue-600"/>
</div>
))}
<div>
<h3 className="text-lg font-semibold mb-2">Съществуващи файлове:</h3>
{files.length > 0 ? (
files.map((file, index) => (
<div key={index} className="flex items-center justify-between mb-2 p-2 hover:bg-blue-50 rounded">
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target="_blank" rel="noopener noreferrer">
{file.name}
</a>
<button onClick={() => handleFileDelete(file.name)} className="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50">
изтрий
</button>
</div>
))
) : (
<p className="text-gray-500">Няма качени файлове.</p>
)}
</div>
</div>
</ProtectedRoute>
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
@@ -102,5 +127,4 @@ export const getServerSideProps = async (context) => {
}
};
}
// export const getServerSideProps = async (context) => {