Merge branch 'main' into production
This commit is contained in:
17
.env
17
.env
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -207,4 +207,4 @@ push notifications
|
||||
store replacement
|
||||
test email
|
||||
|
||||
|
||||
problem with my repeating availability3
|
||||
|
@ -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 >
|
||||
)}</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
0
pages/api/content.ts
Normal file
145
pages/api/content/[subfolder].ts
Normal file
145
pages/api/content/[subfolder].ts
Normal 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.');
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
|
@ -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`)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
BIN
public/content/permits/Разрешително за Април - 24г..pdf
Normal file
BIN
public/content/permits/Разрешително за Април - 24г..pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
13
server.js
13
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
|
||||
|
@ -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` +
|
||||
|
@ -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>С натискането на бутона по-долу можеш да премеш да го заместваш.
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -18,7 +18,7 @@
|
||||
</main>
|
||||
|
||||
<footer style="background-color: #f3f3f3; padding: 20px; text-align: center;">
|
||||
© 2024 ССС. All rights reserved.
|
||||
© 2024 ССС. Openly licensed.
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{{!-- Subject: ССС: Нови назначени смени--}}
|
||||
|
||||
<section>
|
||||
<h2>Здравейте, {{publisherFirstName}} {{publisherLastName}}!</h2>
|
||||
<h2>Здравей {{publisherFirstName}} {{publisherLastName}}!</h2>
|
||||
<p>Ти регистриран да получавате известия за нови смени на количка.</p>
|
||||
<p>За месец {{month}} имате следните смени:</p>
|
||||
<div>
|
||||
|
Reference in New Issue
Block a user