Merge branch 'main' into feature-fixStats
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
103
pages/cart/publishers/congregationCRUD.tsx
Normal file
103
pages/cart/publishers/congregationCRUD.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user