diff --git a/.env b/.env index 61f85aa..c3383e9 100644 --- a/.env +++ b/.env @@ -48,25 +48,18 @@ GITHUB_SECRET= TWITTER_ID= TWITTER_SECRET= -EMAIL_BYPASS_TO=mwitnessing@gmail.com -EMAIL_SENDER='"Специално Свидетелстване София " ' +# EMAIL_BYPASS_TO=mwitnessing@gmail.com +EMAIL_SENDER='"ССС" ' # EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525 EMAIL_FROM=noreply@mwitnessing.com -MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io +EMAIL_SERVICE=mailtrap +# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io MAILTRAP_HOST=sandbox.smtp.mailtrap.io +MAILTRAP_PORT=2525 MAILTRAP_USER=8ec69527ff2104 MAILTRAP_PASS=c7bc05f171c96c -MAILERSEND_TOKEN=mlsn.27d1a8120e120e147e1bb9c6345739faf3a03688bd9bf1b34f797d08b0f9fc26 -MAILERSEND_SERVER=smtp.mailersend.net -MAILERSEND_PORT=587 -MAILERSEND_USER=MS_bL93ka@mwitnessing.com -MAILERSEND_PASS=v23Z2XrDSNjHJxgo - -EMAIL_GMAIL_USERNAME=mwitnessing -EMAIL_GMAIL_APP_PASS="acys uzsp eere qzyh" - TELEGRAM_BOT=false TELEGRAM_BOT_TOKEN=7050075088:AAH6VRpNCyQd9x9sW6CLm6q0q4ibUgYBfnM diff --git a/.env.development b/.env.development index 40806ac..9202801 100644 --- a/.env.development +++ b/.env.development @@ -7,5 +7,12 @@ NEXT_PUBLIC_PUBLIC_URL=https://localhost:3003 # DATABASE=mysql://cart:cartpw@192.168.0.10:3306/cart_dev DATABASE=mysql://cart:cartpw@localhost:3306/cart + +EMAIL_SENDER='"ССС [ТЕСТ] " ' +# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io +# MAILTRAP_HOST=sandbox.smtp.mailtrap.io +# MAILTRAP_USER=8ec69527ff2104 +# MAILTRAP_PASS=c7bc05f171c96c + SSL_KEY=./certificates/localhost-key.pem SSL_CERT=./certificates/localhost.pem diff --git a/.env.production b/.env.production index 8e9e8f4..5431048 100644 --- a/.env.production +++ b/.env.production @@ -9,8 +9,13 @@ NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638 DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia # DATABASE=mysql://cart:cartpw@localhost:3306/cart + EMAIL_BYPASS_TO= -MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io -MAILTRAP_HOST=live.smtp.mailtrap.io -MAILTRAP_USER=api -MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d \ No newline at end of file +EMAIL_SENDER='"Специално Свидетелстване София" ' +EMAIL_SERVICE=gmail +EMAIL_GMAIL_USERNAME=mwitnessing +EMAIL_GMAIL_APP_PASS="acys uzsp eere qzyh" +# MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io +# MAILTRAP_HOST=live.smtp.mailtrap.io +# MAILTRAP_USER=api +# MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d \ No newline at end of file diff --git a/_doc/ToDo.md b/_doc/ToDo.md index e615914..9c1c7b1 100644 --- a/_doc/ToDo.md +++ b/_doc/ToDo.md @@ -207,4 +207,4 @@ push notifications store replacement test email - + problem with my repeating availability3 diff --git a/components/calendar/ShiftComponent.tsx b/components/calendar/ShiftComponent.tsx index a50db70..1d694dd 100644 --- a/components/calendar/ShiftComponent.tsx +++ b/components/calendar/ShiftComponent.tsx @@ -10,7 +10,7 @@ const common = require('src/helpers/common'); function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) { - + const [isDeleted, setIsDeleted] = useState(false); const [assignments, setAssignments] = useState(shift.assignments); const [isModalOpen, setIsModalOpen] = useState(false); const [useFilterDate, setUseFilterDate] = useState(true); @@ -24,24 +24,14 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a }, [shift.assignments]); const handleShiftClick = (shiftId) => { - // console.log("onShiftSelect prop:", onShiftSelect); - // console.log("Shift clicked:", shift); - //shift.selectedPublisher = selectedPublisher; if (onShiftSelect) { onShiftSelect(shift); } }; const handlePublisherClick = (publisher) => { - - //toggle selected - // if (selectedPublisher != null) { - // setSelectedPublisher(null); - // } - // else { setSelectedPublisher(publisher); - console.log("Publisher clicked:", publisher, "selected publisher:", selectedPublisher); shift.selectedPublisher = publisher; if (onShiftSelect) { @@ -54,6 +44,17 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a common.copyToClipboard(null, publisher.firstName + ' ' + publisher.lastName); } + const deleteShift = async (id) => { + try { + console.log("Removing shift with id:", id); + await axiosInstance.delete("/api/data/shifts/" + id); + setIsDeleted(true); + } catch (error) { + console.error("Error removing shift:", error); + } + } + + const removeAssignment = async (id) => { try { console.log("Removing assignment with id:", id); @@ -100,137 +101,162 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a } catch (error) { } } + async function toggleRequiresTransport(shiftId): Promise { + try { + shift.requiresTransport = !shift.requiresTransport; + const { data } = await axiosInstance.put("/api/data/shifts/" + shiftId, + { requiresTransport: shift.requiresTransport }) + .then(() => { + console.log("shift '" + shiftId + "' transport required:" + shift.requiresTransport); + // setTransportProvided(assignments.some(ass => ass.isWithTransport)) + }); + } catch (error) { } + } + return ( -
- {/* Time Window Header */} -
- - {`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`} - {/* {shift.requiresTransport && ()} */} - + <>{!isDeleted && ( +
+ {/* Time Window Header */} +
+ + {`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`} + {/* {shift.requiresTransport && ()} */} + {/* Toggle for Transport Requirement */} + + - {/* Copy All Names Button */} - - {/* Hint Message */} - {showCopyHint && ( -
- Имената са копирани -
- )} -
+ {/* Copy All Names Button */} + + {/* Hint Message */} + {showCopyHint && ( +
+ Имената са копирани +
+ )} +
- {/* Assignments */} - {assignments.map((ass, index) => { - const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher; + {/* Assignments */} + {assignments.map((ass, index) => { + const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher; - // Determine border styles - let borderStyles = ''; - let canTransport = false; - if (selectedPublisher && selectedPublisher.id === ass.publisher.id) { - borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers - } - else { - if (publisherInfo.availabilityCount == 0) //user has never the form - { - borderStyles = 'border-2 border-orange-300 '; + // Determine border styles + let borderStyles = ''; + let canTransport = false; + if (selectedPublisher && selectedPublisher.id === ass.publisher.id) { + borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers } - else - //if there is no publisherInfo - draw red border - publisher is no longer available for the day! - if (!publisherInfo.availabilities || publisherInfo.availabilities.length == 0) { - borderStyles = 'border-2 border-red-500 '; + else { + if (publisherInfo.availabilityCount == 0) //user has never the form + { + borderStyles = 'border-2 border-orange-300 '; } - else { + else + //if there is no publisherInfo - draw red border - publisher is no longer available for the day! + if (!publisherInfo.availabilities || publisherInfo.availabilities.length == 0) { + borderStyles = 'border-2 border-red-500 '; + } + else { + + // checkig if the publisher is available for this assignment + const av = publisherInfo.availabilities?.find(av => + av.startTime <= shift.startTime && av.endTime >= shift.endTime + ); + if (av) { + borderStyles += 'border-l-2 border-blue-500 '; // Left border for specific availability conditions + ass.canTransport = av.isWithTransportIn || av.isWithTransportOut; + } + + if (publisherInfo.hasUpToDateAvailabilities) { + //add green right border + borderStyles += 'border-r-2 border-green-300'; + } + + //the pub is the same time as last month + // if (publisherInfo.availabilities?.some(av => + // (!av.dayOfMonth || av.isFromPreviousMonth) && + // av.startTime <= ass.startTime && + // av.endTime >= ass.endTime)) { + // borderStyles += 'border-t-2 border-yellow-500 '; // Left border for specific availability conditions + // } - // checkig if the publisher is available for this assignment - const av = publisherInfo.availabilities?.find(av => - av.startTime <= shift.startTime && av.endTime >= shift.endTime - ); - if (av) { - borderStyles += 'border-l-2 border-blue-500 '; // Left border for specific availability conditions - ass.canTransport = av.isWithTransportIn || av.isWithTransportOut; } - if (publisherInfo.hasUpToDateAvailabilities) { - //add green right border - borderStyles += 'border-r-2 border-green-300'; - } + } - //the pub is the same time as last month - // if (publisherInfo.availabilities?.some(av => - // (!av.dayOfMonth || av.isFromPreviousMonth) && - // av.startTime <= ass.startTime && - // av.endTime >= ass.endTime)) { - // borderStyles += 'border-t-2 border-yellow-500 '; // Left border for specific availability conditions - // } + return ( +
+
handlePublisherClick(ass.publisher)}> + {publisherInfo.firstName} {publisherInfo.lastName} +
+ {/* //if shift.isWithTransport, add trnsport button toggle, which sets ass.isWithTransportIn */} + {shift.requiresTransport && ( + toggleTransport(ass) : undefined} + className={`material-icons ${ass.isWithTransport ? 'text-green-500 font-bold' : (transportProvided ? 'text-gray-400 ' : 'text-orange-400 font-bold')} ${ass.canTransport ? ' cursor-pointer' : 'cursor-not-allowed'} px-3 py-1 ml-2 rounded-md`} + > + {ass.isWithTransport ? "транспорт" : ass.canTransport ? "може транспорт" : "без транспорт"} + + )} + - } - - } - - return ( -
-
handlePublisherClick(ass.publisher)}> - {publisherInfo.firstName} {publisherInfo.lastName} -
- {/* //if shift.isWithTransport, add trnsport button toggle, which sets ass.isWithTransportIn */} - {shift.requiresTransport && ( - toggleTransport(ass) : undefined} - className={`material-icons ${ass.isWithTransport ? 'text-green-500 font-bold' : (transportProvided ? 'text-gray-400 ' : 'text-orange-400 font-bold')} ${ass.canTransport ? ' cursor-pointer' : 'cursor-not-allowed'} px-3 py-1 ml-2 rounded-md`} - > - {ass.isWithTransport ? "транспорт" : ass.canTransport ? "може транспорт" : "без транспорт"} - - )} - +
-
-
- ); - })} + ); + })} - {/* This is a placeholder for the dropdown to add a publisher. You'll need to implement or integrate a dropdown component */} + {/* This is a placeholder for the dropdown to add a publisher. You'll need to implement or integrate a dropdown component */} -
- {/* Add Button */} - -
+
+ {/* Add Button */} + + {assignments.length == 0 && ( + + )} +
- {/* Modal for Publisher Search + {/* Modal for Publisher Search forDate={new Date(shift.startTime)} */} - setIsModalOpen(false)} - forDate={new Date(shift.startTime)} - useFilterDate={useFilterDate} - onUseFilterDateChange={(value) => setUseFilterDate(value)}> + setIsModalOpen(false)} + forDate={new Date(shift.startTime)} + useFilterDate={useFilterDate} + onUseFilterDateChange={(value) => setUseFilterDate(value)}> - { - // Add publisher as assignment logic - setIsModalOpen(false); - addAssignment(publisher, shift.id); - }} - showAllAuto={true} - showSearch={true} - showList={false} - /> - -
+ { + // Add publisher as assignment logic + setIsModalOpen(false); + addAssignment(publisher, shift.id); + }} + showAllAuto={true} + showSearch={true} + showList={false} + /> + +
+ )} ); } diff --git a/package.json b/package.json index d602472..a9e5e94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pwwa", - "version": "1.1.2", + "version": "1.2.0", "private": true, "description": "JW PW Web App", "repository": "http://git.d-popov.com/popov/next-cart-app.git", diff --git a/pages/api/content.ts b/pages/api/content.ts new file mode 100644 index 0000000..e69de29 diff --git a/pages/api/content/[subfolder].ts b/pages/api/content/[subfolder].ts new file mode 100644 index 0000000..2984aa9 --- /dev/null +++ b/pages/api/content/[subfolder].ts @@ -0,0 +1,145 @@ +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'; + +const handler = nc({ + onError: (err, req, res, next) => { + console.error(err.stack); + res.status(500).end('Something broke!'); + }, + onNoMatch: (req, res) => { + res.status(404).end('Page is not found'); + } +}); + +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.'); + } +} \ No newline at end of file diff --git a/pages/api/data/content.ts b/pages/api/data/content.ts deleted file mode 100644 index 30f67d7..0000000 --- a/pages/api/data/content.ts +++ /dev/null @@ -1,15 +0,0 @@ -import path from 'path'; -import { promises as fs } from 'fs'; - -export default async function handler(req, res) { - //Find the absolute path of the json directory and the requested file contents - const jsonDirectory = path.join(process.cwd(), 'content'); - const requestedFile = req.query.nextcrud[0]; - const fileContents = await fs.readFile(path.join(jsonDirectory, requestedFile), 'utf8'); - // try to determine the content type from the file extension - const contentType = requestedFile.endsWith('.json') ? 'application/json' : 'text/plain'; - // return the file contents with the appropriate content type - res.status(200).setHeader('Content-Type', contentType).end(fileContents); - - -} diff --git a/pages/api/email.ts b/pages/api/email.ts index 4e9fa3d..facba07 100644 --- a/pages/api/email.ts +++ b/pages/api/email.ts @@ -8,6 +8,7 @@ const data = require('../../src/helpers/data'); const emailHelper = require('../../src/helpers/email'); const { v4: uuidv4 } = require('uuid'); const CON = require("../../src/helpers/const"); +import { EventLogType } from "@prisma/client"; import fs from 'fs'; import path from 'path'; @@ -128,6 +129,15 @@ export default async function handler(req, res) { } } }); + await prisma.eventLog.create({ + data: { + date: new Date(), + publisher: { connect: { id: publisher.id } }, + shift: { connect: { id: assignment.shiftId } }, + type: EventLogType.AssignmentReplacementAccepted, + content: "Заявка за заместване приета от " + publisher.firstName + " " + publisher.lastName + } + }); const shiftStr = `${CON.weekdaysBG[assignment.shift.startTime.getDay()]} ${CON.GetDateFormat(assignment.shift.startTime)} at ${assignment.shift.cartEvent.location.name} from ${CON.GetTimeFormat(assignment.shift.startTime)} to ${CON.GetTimeFormat(assignment.shift.endTime)}`; @@ -202,7 +212,7 @@ export default async function handler(req, res) { return res.status(401).json({ message: "Unauthorized to call this API endpoint" }); } - const user = await prisma.publisher.findUnique({ + const publisher = await prisma.publisher.findUnique({ where: { email: token.email } @@ -210,7 +220,7 @@ export default async function handler(req, res) { switch (action) { case "sendCoverMeRequestByEmail": - // Send CoverMe request to the user + // Send CoverMe request to the users //get from POST data: shiftId, assignmentId, date //let shiftId = req.body.shiftId; let assignmentId = req.body.assignmentId; @@ -235,7 +245,7 @@ export default async function handler(req, res) { } } }); - console.log("User: " + user.email + " sent a 'CoverMe' request for his assignment " + assignmentId + " - " + assignment.shift.cartEvent.location.name + " " + assignment.shift.startTime.toISOString()); + console.log("User: " + publisher.email + " sent a 'CoverMe' request for his assignment " + assignmentId + " - " + assignment.shift.cartEvent.location.name + " " + assignment.shift.startTime.toISOString()); // update the assignment. generate new publicGuid, isConfirmed to false @@ -269,22 +279,33 @@ export default async function handler(req, res) { let pubsToSend = subscribedPublishers.concat(availablePublishers). filter((item, index, self) => index === self.findIndex((t) => ( - t.email === item.email //and exclude the user himself - )) //&& item.email !== user.email + t.email === item.email && item.email !== publisher.email//and exclude the user himself + )) ); console.log("Sending CoverMe request to " + pubsToSend.length + " publishers"); + await prisma.eventLog.create({ + data: { + date: new Date(), + publisher: { connect: { id: publisher.id } }, + shift: { connect: { id: assignment.shiftId } }, + type: EventLogType.AssignmentReplacementRequested, + content: "Заявка за заместване от " + publisher.firstName + " " + publisher.lastName + + "до: " + pubsToSend.map(p => p.firstName + " " + p.lastName + "<" + p.email + ">").join(", "), + } + }); + //send email to all subscribed publishers for (let i = 0; i < pubsToSend.length; i++) { //send email to subscribed publisher let acceptUrl = process.env.NEXTAUTH_URL + "/api/email?action=email_response&emailaction=coverMeAccept&userId=" + pubsToSend[i].id + "&shiftId=" + assignment.shiftId + "&assignmentPID=" + newPublicGuid; + publisher.prefix = publisher.isMale ? "Брат" : "Сестра"; let model = { - user: user, + user: publisher, shiftId: assignment.shiftId, acceptUrl: acceptUrl, - prefix: user.isMale ? "Брат" : "Сестра", firstName: pubsToSend[i].firstName, lastName: pubsToSend[i].lastName, email: pubsToSend[i].email, diff --git a/pages/api/index.ts b/pages/api/index.ts index 3349dad..9df5799 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -164,7 +164,7 @@ export default async function handler(req, res) { case "filterPublishersNew": let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities); let results = await filterPublishersNew_Available(req.query.select, day, - common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), includeOldAvailabilities); + common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth), true, includeOldAvailabilities); res.status(200).json(results); break; diff --git a/pages/cart/calendar/index.tsx b/pages/cart/calendar/index.tsx index 6bdacb6..6ad9127 100644 --- a/pages/cart/calendar/index.tsx +++ b/pages/cart/calendar/index.tsx @@ -140,6 +140,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) { onChange(selectedDate); } } + const handleShiftSelection = (selectedShift) => { setSelectedShiftId(selectedShift.id); const updatedPubs = availablePubs.map(pub => { @@ -535,6 +536,38 @@ export default function CalendarPage({ initialEvents, initialShifts }) { await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`); } + async function handleCreateNewShift(event: MouseEvent): Promise { + //get last shift end time + let lastShift = shifts.sort((a, b) => new Date(b.endTime).getTime() - new Date(a.endTime).getTime())[0]; + //default to 9:00 if no shifts + if (!lastShift) { + //get cart event id + var dayName = common.DaysOfWeekArray[value.getDayEuropean()]; + const cartEvent = events.find(event => event.dayofweek == dayName); + lastShift = { + endTime: new Date(value.setHours(9, 0, 0, 0)), + cartEventId: cartEvent.id + }; + } + const lastShiftEndTime = new Date(lastShift.endTime); + //add 90 minutes + const newShiftEndTime = new Date(lastShiftEndTime.getTime() + 90 * 60000); + await axiosInstance.post(`/api/data/shifts`, { + name: "Нова смяна", + startTime: lastShiftEndTime, + endTime: newShiftEndTime, + isPublished: false, + cartEvent: { connect: { id: lastShift.cartEventId } } + }).then((response) => { + console.log("New shift created:", response.data); + // setShifts([...shifts, response.data]); + handleCalDateChange(value); + } + ).catch((error) => { + console.error("Error creating new shift:", error); + }); + } + return ( <> @@ -621,22 +654,6 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
)} - {/* - // - // - // - // - // - // - // - // - // - */} {/* progress bar holder */} @@ -737,6 +754,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) { allPublishersInfo={availablePubs} /> ))} + @@ -907,13 +930,6 @@ export const getServerSideProps = async (context) => { const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`; const prismaClient = common.getPrismaClient(); - // let events = await prismaClient.cartEvent.findMany({ where: { isActive: true } }); - // events = events.map(event => ({ - // ...event, - // // Convert Date objects to ISO strings - // startTime: event.startTime.toISOString(), - // endTime: event.endTime.toISOString(), - // })); const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":true}`); //const { data: shifts } = await axios.get(url); diff --git a/pages/cart/publishers/import.tsx b/pages/cart/publishers/import.tsx index 81d3824..0bf0a3c 100644 --- a/pages/cart/publishers/import.tsx +++ b/pages/cart/publishers/import.tsx @@ -51,7 +51,8 @@ export default function ImportPage() { desiredShiftsIndex: -1, dataStartIndex: -1, isActiveIndex: -1, - pubTypeIndex: -1 + pubTypeIndex: -1, + gender: -1 }); const handleFile = (e) => { @@ -111,6 +112,7 @@ export default function ImportPage() { headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия'); headerRef.current.isActiveIndex = header.indexOf("Неактивен"); headerRef.current.pubTypeIndex = header.indexOf("Назначение"); + headerRef.current.gender = header.indexOf("Пол"); const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => { let date; @@ -147,12 +149,16 @@ export default function ImportPage() { let isOld = false; const row = rawData[i]; - let email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = true, publisherType = PublisherType.Publisher; - //const date = new Date(row[0]).toISOS{tring().slice(0, 10); + let email, phone, names, dateOfInput, oldAvDeleted = false, + isTrained = false, desiredShiftsPerMonth = 4, isActive = true, + publisherType = PublisherType.Publisher, + isMale = 0 + ; + //ToDo: structure all vars above as single object: + if (mode.mainMode == MODE_PUBLISHERS1) { email = row[headerRef.current.emailIndex]; - phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace // Remove any non-digit characters, except for the leading + //phone = phone.replace(/(?!^\+)\D/g, ''); @@ -165,11 +171,11 @@ export default function ImportPage() { names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/); dateOfInput = importDate.value || new Date().toISOString(); // not empty == true - isTrained = row[headerRef.current.isTrainedIndex] !== ''; isActive = row[headerRef.current.isActiveIndex] == ''; desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4; publisherType = row[headerRef.current.pubTypeIndex]; + isMale = row[headerRef.current.gender].trim().toLowerCase() === 'брат'; } else { dateOfInput = common.excelSerialDateToDate(row[0]); @@ -201,7 +207,7 @@ export default function ImportPage() { let personNames = names.join(' '); try { try { - const select = "&select=id,firstName,lastName,email,phone,isTrained,desiredShiftsPerMonth,isActive,type,availabilities"; + const select = "&select=id,firstName,lastName,email,phone,isTrained,desiredShiftsPerMonth,isActive,isMale,type,availabilities"; const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`); let existingPublisher = responseByName.data[0]; if (!existingPublisher) { @@ -244,11 +250,8 @@ export default function ImportPage() { } else { data[i - mode.headerRow][4] = "existing"; } - - // Log existing publisher common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`); - // Check for other updates const fieldsToUpdate = [ { key: 'email', value: email }, @@ -256,6 +259,7 @@ export default function ImportPage() { { key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt }, { key: 'isTrained', value: isTrained }, { key: 'isActive', value: isActive }, + { key: "isMale", value: isMale }, { key: 'type', value: publisherType, parse: common.getPubTypeEnum } ]; @@ -277,6 +281,7 @@ export default function ImportPage() { data[i - mode.headerRow][4] = fieldsToUpdateString.substring(0, fieldsToUpdateString.length - 2) + " updated"; } catch (error) { + data[i - mode.headerRow][4] = "error updating!"; console.error(`Failed to update publisher ${personId} - Fields Attempted: ${fieldsToUpdateString}`, error); } } @@ -309,6 +314,7 @@ export default function ImportPage() { firstName: firstname, lastName: names[names.length - 1], isActive: isActive, + isMale: isMale, isTrained, desiredShiftsPerMonth }); diff --git a/pages/cart/publishers/myschedule.tsx b/pages/cart/publishers/myschedule.tsx index 017b21c..b074440 100644 --- a/pages/cart/publishers/myschedule.tsx +++ b/pages/cart/publishers/myschedule.tsx @@ -205,7 +205,7 @@ export const getServerSideProps = async (context) => { }, }); - const assignments = publisher?.assignments || []; + const assignments = publisher?.assignments.filter(assignment => assignment.shift.startTime >= lastSunday) || []; const transformedAssignments = assignments?.map(assignment => { if (assignment.shift && assignment.shift.startTime) { diff --git a/pages/permits.tsx b/pages/permits.tsx index 2ffced7..2029885 100644 --- a/pages/permits.tsx +++ b/pages/permits.tsx @@ -3,35 +3,61 @@ import Layout from "../components/layout"; import fs from 'fs'; import path from 'path'; import { url } from 'inspector'; +import ProtectedRoute, { serverSideAuth } from "/components/protectedRoute"; +import axiosInstance from '../src/axiosSecure'; + const PDFViewerPage = ({ pdfFiles }) => { + const [files, setFiles] = useState(pdfFiles); + + const handleFileDelete = async (fileName) => { + const subfolder = 'permits'; // Change this as needed based on your subfolder structure + try { + await axiosInstance.delete(`/api/content/${subfolder}?file=${fileName}`); + setFiles(files.filter(file => file.name !== fileName)); + } catch (error) { + console.error('Error deleting file:', error); + } + }; + + const handleFileUpload = async (event) => { + const file = event.target.files[0]; + const formData = new FormData(); + formData.append('file', file); + + 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' + } + }); + setFiles([...files, response.data]); + } catch (error) { + console.error('Error uploading file:', error); + } + }; return (

Разрешителни

-
{/* Adjust the 100px based on your header/footer size */} - {/*

- {pdfFiles.map((file, index) => ( -

- - Свали: {file.name} - -

- ))} -

*/} - {pdfFiles.map((file, index) => ( + + + {files.map((file, index) => ( +
+ + {file.name} + + +
+ ))} +
- // - // {index > 0 &&
} {/* Vertical line separator */} - // - // {file.name} - // - //
+
{/* Adjust the 100px based on your header/footer size */} + {pdfFiles.map((file, index) => ( <>

Свали: {file.name} diff --git a/prisma/migrations/20240418092928_add_event_log_table/migration.sql b/prisma/migrations/20240418092928_add_event_log_table/migration.sql index 52dd62f..2f4ef0d 100644 --- a/prisma/migrations/20240418092928_add_event_log_table/migration.sql +++ b/prisma/migrations/20240418092928_add_event_log_table/migration.sql @@ -4,8 +4,8 @@ CREATE TABLE `EventLog` ( `date` DATETIME(3) NOT NULL, `publisherId` VARCHAR(191) NULL, `shiftId` INTEGER NULL, - `content` VARCHAR(191) NOT NULL, - `type` ENUM('AssignnementReplacementRequested', 'AssignnementReplacement', 'SentEmail') NOT NULL, + `content` VARCHAR(5000) NOT NULL, + `type` ENUM('AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail') NOT NULL, PRIMARY KEY (`id`) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cbb1d77..5b91e47 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -259,8 +259,8 @@ model Message { } enum EventLogType { - AssignnementReplacementRequested - AssignnementReplacement + AssignmentReplacementRequested + AssignmentReplacementAccepted SentEmail } @@ -271,7 +271,7 @@ model EventLog { publisher Publisher? @relation(fields: [publisherId], references: [id]) shiftId Int? shift Shift? @relation(fields: [shiftId], references: [id]) - content String + content String @db.VarChar(5000) type EventLogType } diff --git a/public/content/permits/Разрешително за Април - 24г..pdf b/public/content/permits/Разрешително за Април - 24г..pdf new file mode 100644 index 0000000..0c32d80 Binary files /dev/null and b/public/content/permits/Разрешително за Април - 24г..pdf differ diff --git a/public/content/permits/Разрешително за Март 24г.-промяна (1).pdf b/public/content/permits/Разрешително за Март 24г.-промяна (1).pdf deleted file mode 100644 index 9fdf45c..0000000 Binary files a/public/content/permits/Разрешително за Март 24г.-промяна (1).pdf and /dev/null differ diff --git a/public/content/permits/Разрешително за Март 24г..pdf b/public/content/permits/Разрешително за Март 24г..pdf deleted file mode 100644 index 281f391..0000000 Binary files a/public/content/permits/Разрешително за Март 24г..pdf and /dev/null differ diff --git a/server.js b/server.js index 4d6534d..561691f 100644 --- a/server.js +++ b/server.js @@ -42,6 +42,19 @@ console.log("process.env.DATABASE_URL = ", process.env.DATABASE_URL); console.log("process.env.DATABASE = ", process.env.DATABASE); console.log("process.env.APPLE_APP_ID = ", process.env.APPLE_APP_ID); + +// update GIT_COMMIT_ID +const { exec } = require("child_process"); +exec("git rev-parse HEAD", (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + process.env.GIT_COMMIT_ID = "unknown"; + return; + } + process.env.GIT_COMMIT_ID = stdout.trim(); + console.log("GIT_COMMIT_ID = ", process.env.GIT_COMMIT_ID); +}); + //require('module-alias/register'); //import helpers diff --git a/src/helpers/email.js b/src/helpers/email.js index 64efd24..80eef96 100644 --- a/src/helpers/email.js +++ b/src/helpers/email.js @@ -13,75 +13,30 @@ const Handlebars = require('handlebars'); const { Shift, Publisher, PrismaClient } = require("@prisma/client"); const { env } = require("../../next.config"); +const SMTPTransport = require("nodemailer/lib/smtp-transport"); -// const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad"; -// const SENDER_EMAIL = "sofia@mwitnessing.com"; -// const sender = { name: "Специално Свидетелстване София", email: SENDER_EMAIL }; -// const client = new MailtrapClient({ token: TOKEN }); - -let mailtrapTestClient = null; -// const mailtrapTestClient = new MailtrapClient({ -// username: '8ec69527ff2104',//not working now -// password: 'c7bc05f171c96c' -// }); - -//MAILTRAP -var transporterMT = nodemailer.createTransport({ - host: process.env.MAILTRAP_HOST || "sandbox.smtp.mailtrap.io", - port: 2525, - auth: { - user: process.env.MAILTRAP_USER, - pass: process.env.MAILTRAP_PASS - } -}); - -//PROD GMAIL -// const oauth2Client = new OAuth2( -// process.env.CLIENT_ID, -// process.env.CLIENT_SECRET, -// "https://developers.google.com/oauthplayground" -// ); -// var transporterGmail = nodemailer.createTransport({ -// service: "gmail", -// auth: { -// type: "OAuth2", -// user: process.env.GMAIL_USER, -// clientId: process.env.CLIENT_ID, -// clientSecret: process.env.CLIENT_SECRET, -// refreshToken: process.env.REFRESH_TOKEN, -// accessToken: process.env.ACCESS_TOKEN -// } -// }); -//-------------- -var transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: process.env.EMAIL_GMAIL_USERNAME, - pass: process.env.EMAIL_GMAIL_APP_PASS - } -}); - - -//PROD MAILERSEND -// var transporter = nodemailer.createTransport({ -// host: process.env.MAILERSEND_SERVER, -// port: process.env.MAILERSEND_PORT, -// auth: { -// user: process.env.MAILERSEND_USER, -// pass: process.env.MAILERSEND_PASS -// } -// }); - -var transporterBulk = nodemailer.createTransport({ - host: "bulk.smtp.mailtrap.io", - port: 587, - auth: { - user: "api", - pass: "1cfe82e747b8dc3390ed08bb16e0f48d" - } -}); +var transporter; +if (process.env.EMAIL_SERVICE.toLowerCase() === "mailtrap") { + transporter = nodemailer.createTransport({ + host: process.env.MAILTRAP_HOST || "sandbox.smtp.mailtrap.io", + port: process.env.MAILTRAP_PORT || 2525, + auth: { + user: process.env.MAILTRAP_USER, + pass: process.env.MAILTRAP_PASS + } + }); +} +if (process.env.EMAIL_SERVICE.toLowerCase() === "gmail") { + transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_GMAIL_USERNAME, + pass: process.env.EMAIL_GMAIL_APP_PASS + } + }); +} // ------------------ Email sending ------------------ @@ -137,22 +92,11 @@ exports.SendEmail = async function (to, subject, text, html, attachments = []) { attachments }; - if (mailtrapTestClient !== null) { - // Assuming mailtrapTestClient is correctly set up to send emails - await mailtrapTestClient - .send(message) - .then(console.log) - .catch(console.error); - - } else { - - let result = await transporter - .sendMail(message) - .then(console.log) - .catch(console.error); - return result; - } - + let result = await transporter + .sendMail(message) + .then(console.log) + .catch(console.error); + return result; }; exports.SendEmailHandlebars = async function (to, templateName, model, attachments = []) { @@ -266,7 +210,7 @@ exports.SendEmail_NewShifts = async function (publisher, shifts) { // ], // subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()], // text: -// "Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" + +// "Здравей, " + publisher.firstName + " " + publisher.lastName + "!\n\n" + // "Ти регистриран да получавате известия за нови смени на количка.\n" + // `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` + // ` ${shftStr} \n\n\n` + diff --git a/src/templates/emails/coverMe.hbs b/src/templates/emails/coverMe.hbs index 2855a11..e96a471 100644 --- a/src/templates/emails/coverMe.hbs +++ b/src/templates/emails/coverMe.hbs @@ -5,7 +5,7 @@ {{!-- за смяна на {{placeName}} за {{dateStr}}! --}}

Здравей {{firstName}},

-

{{prefix}} {{user.firstName}} {{user.lastName}} търси заместник.

+

{{user.prefix}} {{user.firstName}} {{user.lastName}} търси заместник.

{{!--

Shift Details:

--}}

Дата: {{dateStr}}
Час: {{time}}
Място: {{placeName}}

С натискането на бутона по-долу можеш да премеш да го заместваш. diff --git a/src/templates/emails/coverMeAccepted.hbs b/src/templates/emails/coverMeAccepted.hbs index 16bb198..a29a086 100644 --- a/src/templates/emails/coverMeAccepted.hbs +++ b/src/templates/emails/coverMeAccepted.hbs @@ -2,7 +2,7 @@

Промяна твоята смяна на {{placeName}} {{dateStr}}

-

Здравейте {{firstName}},

+

Здравей {{firstName}},

{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}

Новаия списък с участници за тази смяна е:

    diff --git a/src/templates/emails/example.hbs b/src/templates/emails/example.hbs index e300167..4c872f9 100644 --- a/src/templates/emails/example.hbs +++ b/src/templates/emails/example.hbs @@ -4,7 +4,7 @@ text version. --}}

    Търси се зместник за смяна на {{placeName}} за {{dateStr}}!

    -

    Здравейте,

    +

    Здравей,

    {{prefix}} {{firstName}} {{lastName}} търси заместник.

    {{!--

    Shift Details:

    --}}

    Дата: {{dateStr}}
    Час: {{time}}
    Място: {{placeName}}

    diff --git a/src/templates/emails/main.hbs b/src/templates/emails/main.hbs index 1669557..cd6ef07 100644 --- a/src/templates/emails/main.hbs +++ b/src/templates/emails/main.hbs @@ -18,7 +18,7 @@
    - © 2024 ССС. All rights reserved. + © 2024 ССС. Openly licensed.
    diff --git a/src/templates/emails/newShifts.hbs b/src/templates/emails/newShifts.hbs index 5f52bc8..a49bc72 100644 --- a/src/templates/emails/newShifts.hbs +++ b/src/templates/emails/newShifts.hbs @@ -1,7 +1,7 @@ {{!-- Subject: ССС: Нови назначени смени--}}
    -

    Здравейте, {{publisherFirstName}} {{publisherLastName}}!

    +

    Здравей {{publisherFirstName}} {{publisherLastName}}!

    Ти регистриран да получавате известия за нови смени на количка.

    За месец {{month}} имате следните смени: