Merge branch 'main' into production

This commit is contained in:
Dobromir Popov
2024-04-20 01:52:03 +03:00
27 changed files with 496 additions and 309 deletions

17
.env
View File

@ -48,25 +48,18 @@ GITHUB_SECRET=
TWITTER_ID=
TWITTER_SECRET=
EMAIL_BYPASS_TO=mwitnessing@gmail.com
EMAIL_SENDER='"Специално Свидетелстване София " <mwitnessing@gmail.com>'
# EMAIL_BYPASS_TO=mwitnessing@gmail.com
EMAIL_SENDER='"ССС" <mwitnessing@gmail.com>'
# 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

View File

@ -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='"ССС [ТЕСТ] " <mwitnessing@gmail.com>'
# 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

View File

@ -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
EMAIL_SENDER='"Специално Свидетелстване София" <mwitnessing@gmail.com>'
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

View File

@ -207,4 +207,4 @@ push notifications
store replacement
test email
problem with my repeating availability3

View File

@ -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<void> {
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 (
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
onClick={handleShiftClick} onDoubleClick={copyAllPublisherNames}>
{/* Time Window Header */}
<div className="flex justify-between items-center mb-2 border-b pb-1">
<span className="text-lg font-semibold">
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
{/* {shift.requiresTransport && (<LocalShippingIcon />)} */}
</span>
<>{!isDeleted && (
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
onClick={handleShiftClick} onDoubleClick={copyAllPublisherNames}>
{/* Time Window Header */}
<div className="flex justify-between items-center mb-2 border-b pb-1">
<span className="flex text-lg font-semibold">
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
{/* {shift.requiresTransport && (<LocalShippingIcon />)} */}
{/* Toggle for Transport Requirement */}
<label className="ml-4 flex items-center">
<input type="checkbox" checked={shift.requiresTransport}
onChange={() => toggleRequiresTransport(shift.id)}
className="form-checkbox h-5 w-5 text-green-600" />
<span className="ml-2 text-sm text-gray-700">транспорт</span>
</label>
</span>
{/* Copy All Names Button */}
<button onClick={copyAllPublisherNames} className="bg-green-500 text-white py-1 px-2 text-sm rounded-md">
копирай имената {/* Placeholder for Copy icon */}
</button>
{/* Hint Message */}
{showCopyHint && (
<div className="absolute top-0 right-0 p-2 bg-green-200 text-green-800 rounded">
Имената са копирани
</div>
)}
</div>
{/* Copy All Names Button */}
<button onClick={copyAllPublisherNames} className="bg-green-500 text-white py-1 px-2 text-sm rounded-md">
копирай имената {/* Placeholder for Copy icon */}
</button>
{/* Hint Message */}
{showCopyHint && (
<div className="absolute top-0 right-0 p-2 bg-green-200 text-green-800 rounded">
Имената са копирани
</div>
)}
</div>
{/* 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 (
<div key={index}
className={`flow space-x-2 rounded-md px-2 py-1 my-1 ${ass.isConfirmed ? 'bg-green-100' : 'bg-gray-100'} ${borderStyles}`}
>
<div className="flex justify-between items-center" onClick={() => handlePublisherClick(ass.publisher)}>
<span className="text-gray-700">{publisherInfo.firstName} {publisherInfo.lastName}</span>
<div className="flex items-left" >
{/* //if shift.isWithTransport, add trnsport button toggle, which sets ass.isWithTransportIn */}
{shift.requiresTransport && (
<span
onClick={ass.canTransport ? () => 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 ? "може транспорт" : "без транспорт"} <LocalShippingIcon />
</span>
)}
<button onClick={() => removeAssignment(ass.id)} className="text-white bg-red-500 hover:bg-red-600 px-3 py-1 ml-2 rounded-md" >
махни
</button>
}
}
return (
<div key={index}
className={`flow space-x-2 rounded-md px-2 py-1 my-1 ${ass.isConfirmed ? 'bg-green-100' : 'bg-gray-100'} ${borderStyles}`}
>
<div className="flex justify-between items-center" onClick={() => handlePublisherClick(ass.publisher)}>
<span className="text-gray-700">{publisherInfo.firstName} {publisherInfo.lastName}</span>
<div className="flex items-left" >
{/* //if shift.isWithTransport, add trnsport button toggle, which sets ass.isWithTransportIn */}
{shift.requiresTransport && (
<span
onClick={ass.canTransport ? () => 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 ? "може транспорт" : "без транспорт"} <LocalShippingIcon />
</span>
)}
<button onClick={() => removeAssignment(ass.id)} className="text-white bg-red-500 hover:bg-red-600 px-3 py-1 ml-2 rounded-md" >
махни
</button>
</div>
</div>
</div>
</div>
);
})}
);
})}
{/* 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 */}
<div className="flex space-x-2 items-center">
{/* Add Button */}
<button onClick={() => setIsModalOpen(true)} className="bg-blue-500 text-white p-2 py-1 rounded-md">
добави {/* Placeholder for Add icon */}
</button>
</div>
<div className="flex space-x-2 items-center">
{/* Add Button */}
<button onClick={() => setIsModalOpen(true)} className="bg-blue-500 text-white p-2 py-1 rounded-md">
добави участник{/* Placeholder for Add icon */}
</button>
{assignments.length == 0 && (
<button onClick={() => deleteShift(shift.id)} className="bg-red-500 text-white p-2 py-1 rounded-md"
>изтрий смяната</button>
)}
</div>
{/* Modal for Publisher Search
{/* Modal for Publisher Search
forDate={new Date(shift.startTime)}
*/}
<Modal isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
forDate={new Date(shift.startTime)}
useFilterDate={useFilterDate}
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
<Modal isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
forDate={new Date(shift.startTime)}
useFilterDate={useFilterDate}
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
<PublisherSearchBox
selectedId={null}
isFocused={isModalOpen}
filterDate={useFilterDate ? new Date(shift.startTime) : null}
onChange={(publisher) => {
// Add publisher as assignment logic
setIsModalOpen(false);
addAssignment(publisher, shift.id);
}}
showAllAuto={true}
showSearch={true}
showList={false}
/>
</Modal>
</div >
<PublisherSearchBox
selectedId={null}
isFocused={isModalOpen}
filterDate={useFilterDate ? new Date(shift.startTime) : null}
onChange={(publisher) => {
// Add publisher as assignment logic
setIsModalOpen(false);
addAssignment(publisher, shift.id);
}}
showAllAuto={true}
showSearch={true}
showList={false}
/>
</Modal>
</div >
)}</>
);
}

View File

@ -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",

0
pages/api/content.ts Normal file
View File

View File

@ -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.');
}
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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;

View File

@ -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<HTMLButtonElement, MouseEvent>): Promise<void> {
//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 (
<>
<Layout>
@ -621,22 +654,6 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
</div>
</div>
)}
{/* <button className={`button m-2 bg-blue-800 ${isOperationInProgress ? 'disabled' : ''}`} onClick={importShifts}>
{isOperationInProgress ? <div className="spinner"></div> : 'Import shifts (and missing Publishers) from WORD'}
</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts()}>Generate empty shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true)}>Copy last month shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true, true)}>Generate Auto shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(false, true, value)}>Generate Auto shifts DAY</button>
// <button className="button m-2" onClick={fetchShifts}>Fetch shifts</button>
// <button className="button m-2" onClick={sendMails}>Send mails</button>
// <button className="button m-2" onClick={generateXLS}>Generate XLSX</button>
// <button className="button m-2" onClick={async () => {
// await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}`);
// }
// }>Delete shifts (selected date's month)</button>
// <button className="button m-2" onClick={generateMonthlyStatistics}>Generate statistics</button>
*/}
</div>
</div>
{/* progress bar holder */}
@ -737,6 +754,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
allPublishersInfo={availablePubs} />
))}
</div>
<button
className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={handleCreateNewShift}
>
Добави нова смяна
</button>
</div>
</div>
@ -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);

View File

@ -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
});

View File

@ -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) {

View File

@ -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 (
<Layout>
<h1 className="text-3xl font-bold p-4 pt-8">Разрешителни</h1>
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
{/* <p className="p-1">
{pdfFiles.map((file, index) => (
<p className="p-2">
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
Свали: {file.name}
</a>
</p>
))}
</p> */}
{pdfFiles.map((file, index) => (
<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>
</div>
))}
</ProtectedRoute>
// <React.Fragment key={file.name}>
// {index > 0 && <div className="bg-gray-400 w-px h-6"></div>} {/* Vertical line separator */}
// <a
// href={file.url}
// target="_blank"
// className={`text-lg py-2 px-4 bg-gray-200 text-gray-800 hover:bg-blue-500 hover:text-white ${index === 0 ? 'rounded-l-full' : index === pdfFiles.length - 1 ? 'rounded-r-full' : ''}`}
// >
// {file.name}
// </a>
// </React.Fragment>
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
{pdfFiles.map((file, index) => (
<> <p className="pt-2">
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
Свали: {file.name}

View File

@ -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`)

View File

@ -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
}

View File

@ -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

View File

@ -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` +

View File

@ -5,7 +5,7 @@
{{!-- за смяна на {{placeName}} за {{dateStr}}! --}}
</h3>
<p>Здравей {{firstName}},</p>
<p>{{prefix}} {{user.firstName}} {{user.lastName}} търси заместник.</p>
<p>{{user.prefix}} {{user.firstName}} {{user.lastName}} търси заместник.</p>
{{!-- <p><strong>Shift Details:</strong></p> --}}
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
<p>С натискането на бутона по-долу можеш да премеш да го заместваш.

View File

@ -2,7 +2,7 @@
<section>
<h2>Промяна твоята смяна на {{placeName}} {{dateStr}} </h2>
<p>Здравейте {{firstName}}, </p>
<p>Здравей {{firstName}}, </p>
<p>{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}</p>
<p>Новаия списък с участници за тази смяна е:</p>
<ul>

View File

@ -4,7 +4,7 @@ text version. --}}
<section>
<h3>Търси се зместник за смяна на {{placeName}} за {{dateStr}}!</h3>
<p>Здравейте,</p>
<p>Здравей,</p>
<p>{{prefix}} {{firstName}} {{lastName}} търси заместник.</p>
{{!-- <p><strong>Shift Details:</strong></p> --}}
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>

View File

@ -18,7 +18,7 @@
</main>
<footer style="background-color: #f3f3f3; padding: 20px; text-align: center;">
© 2024 ССС. All rights reserved.
© 2024 ССС. Openly licensed.
</footer>
</body>

View File

@ -1,7 +1,7 @@
{{!-- Subject: ССС: Нови назначени смени--}}
<section>
<h2>Здравейте, {{publisherFirstName}} {{publisherLastName}}!</h2>
<h2>Здравей {{publisherFirstName}} {{publisherLastName}}!</h2>
<p>Ти регистриран да получавате известия за нови смени на количка.</p>
<p>За месец {{month}} имате следните смени:</p>
<div>