Merge branch 'main' into production
This commit is contained in:
17
.env
17
.env
@ -48,25 +48,18 @@ GITHUB_SECRET=
|
|||||||
TWITTER_ID=
|
TWITTER_ID=
|
||||||
TWITTER_SECRET=
|
TWITTER_SECRET=
|
||||||
|
|
||||||
EMAIL_BYPASS_TO=mwitnessing@gmail.com
|
# EMAIL_BYPASS_TO=mwitnessing@gmail.com
|
||||||
EMAIL_SENDER='"Специално Свидетелстване София " <mwitnessing@gmail.com>'
|
EMAIL_SENDER='"ССС" <mwitnessing@gmail.com>'
|
||||||
# EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
# EMAIL_SERVER=smtp://8ec69527ff2104:c7bc05f171c96c@smtp.mailtrap.io:2525
|
||||||
EMAIL_FROM=noreply@mwitnessing.com
|
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_HOST=sandbox.smtp.mailtrap.io
|
||||||
|
MAILTRAP_PORT=2525
|
||||||
MAILTRAP_USER=8ec69527ff2104
|
MAILTRAP_USER=8ec69527ff2104
|
||||||
MAILTRAP_PASS=c7bc05f171c96c
|
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=false
|
||||||
TELEGRAM_BOT_TOKEN=7050075088:AAH6VRpNCyQd9x9sW6CLm6q0q4ibUgYBfnM
|
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@192.168.0.10:3306/cart_dev
|
||||||
DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
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_KEY=./certificates/localhost-key.pem
|
||||||
SSL_CERT=./certificates/localhost.pem
|
SSL_CERT=./certificates/localhost.pem
|
||||||
|
@ -9,8 +9,13 @@ NEXTAUTH_SECRET=1dd8a5457970d1dda50600be28e935ecc4513ff27c49c431849e6746f158d638
|
|||||||
DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
DATABASE=mysql://jwpwsofia:dwxhns9p9vp248V39xJyRthUsZ2gR9@mariadb:3306/jwpwsofia
|
||||||
# DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
# DATABASE=mysql://cart:cartpw@localhost:3306/cart
|
||||||
|
|
||||||
|
|
||||||
EMAIL_BYPASS_TO=
|
EMAIL_BYPASS_TO=
|
||||||
MAILTRAP_HOST_BULK=bulk.smtp.mailtrap.io
|
EMAIL_SENDER='"Специално Свидетелстване София" <mwitnessing@gmail.com>'
|
||||||
MAILTRAP_HOST=live.smtp.mailtrap.io
|
EMAIL_SERVICE=gmail
|
||||||
MAILTRAP_USER=api
|
EMAIL_GMAIL_USERNAME=mwitnessing
|
||||||
MAILTRAP_PASS=1cfe82e747b8dc3390ed08bb16e0f48d
|
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
|
store replacement
|
||||||
test email
|
test email
|
||||||
|
|
||||||
|
problem with my repeating availability3
|
||||||
|
@ -10,7 +10,7 @@ const common = require('src/helpers/common');
|
|||||||
|
|
||||||
|
|
||||||
function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) {
|
function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, allPublishersInfo }) {
|
||||||
|
const [isDeleted, setIsDeleted] = useState(false);
|
||||||
const [assignments, setAssignments] = useState(shift.assignments);
|
const [assignments, setAssignments] = useState(shift.assignments);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [useFilterDate, setUseFilterDate] = useState(true);
|
const [useFilterDate, setUseFilterDate] = useState(true);
|
||||||
@ -24,24 +24,14 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
}, [shift.assignments]);
|
}, [shift.assignments]);
|
||||||
|
|
||||||
const handleShiftClick = (shiftId) => {
|
const handleShiftClick = (shiftId) => {
|
||||||
// console.log("onShiftSelect prop:", onShiftSelect);
|
|
||||||
// console.log("Shift clicked:", shift);
|
|
||||||
//shift.selectedPublisher = selectedPublisher;
|
|
||||||
if (onShiftSelect) {
|
if (onShiftSelect) {
|
||||||
onShiftSelect(shift);
|
onShiftSelect(shift);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePublisherClick = (publisher) => {
|
const handlePublisherClick = (publisher) => {
|
||||||
|
|
||||||
//toggle selected
|
|
||||||
// if (selectedPublisher != null) {
|
|
||||||
// setSelectedPublisher(null);
|
|
||||||
// }
|
|
||||||
// else {
|
|
||||||
setSelectedPublisher(publisher);
|
setSelectedPublisher(publisher);
|
||||||
|
|
||||||
|
|
||||||
console.log("Publisher clicked:", publisher, "selected publisher:", selectedPublisher);
|
console.log("Publisher clicked:", publisher, "selected publisher:", selectedPublisher);
|
||||||
shift.selectedPublisher = publisher;
|
shift.selectedPublisher = publisher;
|
||||||
if (onShiftSelect) {
|
if (onShiftSelect) {
|
||||||
@ -54,6 +44,17 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
common.copyToClipboard(null, publisher.firstName + ' ' + publisher.lastName);
|
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) => {
|
const removeAssignment = async (id) => {
|
||||||
try {
|
try {
|
||||||
console.log("Removing assignment with id:", id);
|
console.log("Removing assignment with id:", id);
|
||||||
@ -100,137 +101,162 @@ function ShiftComponent({ shift, onShiftSelect, isSelected, onPublisherSelect, a
|
|||||||
} catch (error) { }
|
} 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 (
|
return (
|
||||||
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
|
<>{!isDeleted && (
|
||||||
onClick={handleShiftClick} onDoubleClick={copyAllPublisherNames}>
|
<div className={`flow w-full p-4 py-2 border-2 border-gray-300 rounded-md my-1 ${isSelected ? 'bg-gray-200' : ''}`}
|
||||||
{/* Time Window Header */}
|
onClick={handleShiftClick} onDoubleClick={copyAllPublisherNames}>
|
||||||
<div className="flex justify-between items-center mb-2 border-b pb-1">
|
{/* Time Window Header */}
|
||||||
<span className="text-lg font-semibold">
|
<div className="flex justify-between items-center mb-2 border-b pb-1">
|
||||||
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
|
<span className="flex text-lg font-semibold">
|
||||||
{/* {shift.requiresTransport && (<LocalShippingIcon />)} */}
|
{`${common.getTimeRange(new Date(shift.startTime), new Date(shift.endTime))}`}
|
||||||
</span>
|
{/* {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 */}
|
{/* Copy All Names Button */}
|
||||||
<button onClick={copyAllPublisherNames} className="bg-green-500 text-white py-1 px-2 text-sm rounded-md">
|
<button onClick={copyAllPublisherNames} className="bg-green-500 text-white py-1 px-2 text-sm rounded-md">
|
||||||
копирай имената {/* Placeholder for Copy icon */}
|
копирай имената {/* Placeholder for Copy icon */}
|
||||||
</button>
|
</button>
|
||||||
{/* Hint Message */}
|
{/* Hint Message */}
|
||||||
{showCopyHint && (
|
{showCopyHint && (
|
||||||
<div className="absolute top-0 right-0 p-2 bg-green-200 text-green-800 rounded">
|
<div className="absolute top-0 right-0 p-2 bg-green-200 text-green-800 rounded">
|
||||||
Имената са копирани
|
Имената са копирани
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Assignments */}
|
{/* Assignments */}
|
||||||
{assignments.map((ass, index) => {
|
{assignments.map((ass, index) => {
|
||||||
const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher;
|
const publisherInfo = allPublishersInfo.find(info => info?.id === ass.publisher.id) || ass.publisher;
|
||||||
|
|
||||||
// Determine border styles
|
// Determine border styles
|
||||||
let borderStyles = '';
|
let borderStyles = '';
|
||||||
let canTransport = false;
|
let canTransport = false;
|
||||||
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
|
if (selectedPublisher && selectedPublisher.id === ass.publisher.id) {
|
||||||
borderStyles += 'border-2 border-blue-300'; // Bottom border for selected publishers
|
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 ';
|
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
//if there is no publisherInfo - draw red border - publisher is no longer available for the day!
|
if (publisherInfo.availabilityCount == 0) //user has never the form
|
||||||
if (!publisherInfo.availabilities || publisherInfo.availabilities.length == 0) {
|
{
|
||||||
borderStyles = 'border-2 border-red-500 ';
|
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
|
return (
|
||||||
// if (publisherInfo.availabilities?.some(av =>
|
<div key={index}
|
||||||
// (!av.dayOfMonth || av.isFromPreviousMonth) &&
|
className={`flow space-x-2 rounded-md px-2 py-1 my-1 ${ass.isConfirmed ? 'bg-green-100' : 'bg-gray-100'} ${borderStyles}`}
|
||||||
// av.startTime <= ass.startTime &&
|
>
|
||||||
// av.endTime >= ass.endTime)) {
|
<div className="flex justify-between items-center" onClick={() => handlePublisherClick(ass.publisher)}>
|
||||||
// borderStyles += 'border-t-2 border-yellow-500 '; // Left border for specific availability conditions
|
<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>
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
||||||
</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">
|
<div className="flex space-x-2 items-center">
|
||||||
{/* Add Button */}
|
{/* Add Button */}
|
||||||
<button onClick={() => setIsModalOpen(true)} className="bg-blue-500 text-white p-2 py-1 rounded-md">
|
<button onClick={() => setIsModalOpen(true)} className="bg-blue-500 text-white p-2 py-1 rounded-md">
|
||||||
добави {/* Placeholder for Add icon */}
|
добави участник{/* Placeholder for Add icon */}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{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)}
|
forDate={new Date(shift.startTime)}
|
||||||
*/}
|
*/}
|
||||||
<Modal isOpen={isModalOpen}
|
<Modal isOpen={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
forDate={new Date(shift.startTime)}
|
forDate={new Date(shift.startTime)}
|
||||||
useFilterDate={useFilterDate}
|
useFilterDate={useFilterDate}
|
||||||
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
|
onUseFilterDateChange={(value) => setUseFilterDate(value)}>
|
||||||
|
|
||||||
<PublisherSearchBox
|
<PublisherSearchBox
|
||||||
selectedId={null}
|
selectedId={null}
|
||||||
isFocused={isModalOpen}
|
isFocused={isModalOpen}
|
||||||
filterDate={useFilterDate ? new Date(shift.startTime) : null}
|
filterDate={useFilterDate ? new Date(shift.startTime) : null}
|
||||||
onChange={(publisher) => {
|
onChange={(publisher) => {
|
||||||
// Add publisher as assignment logic
|
// Add publisher as assignment logic
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
addAssignment(publisher, shift.id);
|
addAssignment(publisher, shift.id);
|
||||||
}}
|
}}
|
||||||
showAllAuto={true}
|
showAllAuto={true}
|
||||||
showSearch={true}
|
showSearch={true}
|
||||||
showList={false}
|
showList={false}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div >
|
</div >
|
||||||
|
)}</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pwwa",
|
"name": "pwwa",
|
||||||
"version": "1.1.2",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "JW PW Web App",
|
"description": "JW PW Web App",
|
||||||
"repository": "http://git.d-popov.com/popov/next-cart-app.git",
|
"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 emailHelper = require('../../src/helpers/email');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const CON = require("../../src/helpers/const");
|
const CON = require("../../src/helpers/const");
|
||||||
|
import { EventLogType } from "@prisma/client";
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
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)}`;
|
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" });
|
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: {
|
where: {
|
||||||
email: token.email
|
email: token.email
|
||||||
}
|
}
|
||||||
@ -210,7 +220,7 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "sendCoverMeRequestByEmail":
|
case "sendCoverMeRequestByEmail":
|
||||||
// Send CoverMe request to the user
|
// Send CoverMe request to the users
|
||||||
//get from POST data: shiftId, assignmentId, date
|
//get from POST data: shiftId, assignmentId, date
|
||||||
//let shiftId = req.body.shiftId;
|
//let shiftId = req.body.shiftId;
|
||||||
let assignmentId = req.body.assignmentId;
|
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
|
// 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).
|
let pubsToSend = subscribedPublishers.concat(availablePublishers).
|
||||||
filter((item, index, self) =>
|
filter((item, index, self) =>
|
||||||
index === self.findIndex((t) => (
|
index === self.findIndex((t) => (
|
||||||
t.email === item.email //and exclude the user himself
|
t.email === item.email && item.email !== publisher.email//and exclude the user himself
|
||||||
)) //&& item.email !== user.email
|
))
|
||||||
);
|
);
|
||||||
console.log("Sending CoverMe request to " + pubsToSend.length + " publishers");
|
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
|
//send email to all subscribed publishers
|
||||||
for (let i = 0; i < pubsToSend.length; i++) {
|
for (let i = 0; i < pubsToSend.length; i++) {
|
||||||
|
|
||||||
//send email to subscribed publisher
|
//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;
|
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 = {
|
let model = {
|
||||||
user: user,
|
user: publisher,
|
||||||
shiftId: assignment.shiftId,
|
shiftId: assignment.shiftId,
|
||||||
acceptUrl: acceptUrl,
|
acceptUrl: acceptUrl,
|
||||||
prefix: user.isMale ? "Брат" : "Сестра",
|
|
||||||
firstName: pubsToSend[i].firstName,
|
firstName: pubsToSend[i].firstName,
|
||||||
lastName: pubsToSend[i].lastName,
|
lastName: pubsToSend[i].lastName,
|
||||||
email: pubsToSend[i].email,
|
email: pubsToSend[i].email,
|
||||||
|
@ -164,7 +164,7 @@ export default async function handler(req, res) {
|
|||||||
case "filterPublishersNew":
|
case "filterPublishersNew":
|
||||||
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
|
let includeOldAvailabilities = common.parseBool(req.query.includeOldAvailabilities);
|
||||||
let results = await filterPublishersNew_Available(req.query.select, day,
|
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);
|
res.status(200).json(results);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -140,6 +140,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
onChange(selectedDate);
|
onChange(selectedDate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShiftSelection = (selectedShift) => {
|
const handleShiftSelection = (selectedShift) => {
|
||||||
setSelectedShiftId(selectedShift.id);
|
setSelectedShiftId(selectedShift.id);
|
||||||
const updatedPubs = availablePubs.map(pub => {
|
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)}`);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -621,22 +654,6 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
{/* progress bar holder */}
|
{/* progress bar holder */}
|
||||||
@ -737,6 +754,12 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
|
|||||||
allPublishersInfo={availablePubs} />
|
allPublishersInfo={availablePubs} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
||||||
</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 url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
|
||||||
|
|
||||||
const prismaClient = common.getPrismaClient();
|
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: events } = await axios.get(`/api/data/cartevents?where={"isActive":true}`);
|
||||||
//const { data: shifts } = await axios.get(url);
|
//const { data: shifts } = await axios.get(url);
|
||||||
|
|
||||||
|
@ -51,7 +51,8 @@ export default function ImportPage() {
|
|||||||
desiredShiftsIndex: -1,
|
desiredShiftsIndex: -1,
|
||||||
dataStartIndex: -1,
|
dataStartIndex: -1,
|
||||||
isActiveIndex: -1,
|
isActiveIndex: -1,
|
||||||
pubTypeIndex: -1
|
pubTypeIndex: -1,
|
||||||
|
gender: -1
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFile = (e) => {
|
const handleFile = (e) => {
|
||||||
@ -111,6 +112,7 @@ export default function ImportPage() {
|
|||||||
headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия');
|
headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия');
|
||||||
headerRef.current.isActiveIndex = header.indexOf("Неактивен");
|
headerRef.current.isActiveIndex = header.indexOf("Неактивен");
|
||||||
headerRef.current.pubTypeIndex = header.indexOf("Назначение");
|
headerRef.current.pubTypeIndex = header.indexOf("Назначение");
|
||||||
|
headerRef.current.gender = header.indexOf("Пол");
|
||||||
|
|
||||||
const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => {
|
const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => {
|
||||||
let date;
|
let date;
|
||||||
@ -147,12 +149,16 @@ export default function ImportPage() {
|
|||||||
|
|
||||||
let isOld = false;
|
let isOld = false;
|
||||||
const row = rawData[i];
|
const row = rawData[i];
|
||||||
let email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = true, publisherType = PublisherType.Publisher;
|
let email, phone, names, dateOfInput, oldAvDeleted = false,
|
||||||
//const date = new Date(row[0]).toISOS{tring().slice(0, 10);
|
isTrained = false, desiredShiftsPerMonth = 4, isActive = true,
|
||||||
|
publisherType = PublisherType.Publisher,
|
||||||
|
isMale = 0
|
||||||
|
;
|
||||||
|
//ToDo: structure all vars above as single object:
|
||||||
|
|
||||||
if (mode.mainMode == MODE_PUBLISHERS1) {
|
if (mode.mainMode == MODE_PUBLISHERS1) {
|
||||||
|
|
||||||
email = row[headerRef.current.emailIndex];
|
email = row[headerRef.current.emailIndex];
|
||||||
|
|
||||||
phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace
|
phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace
|
||||||
// Remove any non-digit characters, except for the leading +
|
// Remove any non-digit characters, except for the leading +
|
||||||
//phone = phone.replace(/(?!^\+)\D/g, '');
|
//phone = phone.replace(/(?!^\+)\D/g, '');
|
||||||
@ -165,11 +171,11 @@ export default function ImportPage() {
|
|||||||
names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/);
|
names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/);
|
||||||
dateOfInput = importDate.value || new Date().toISOString();
|
dateOfInput = importDate.value || new Date().toISOString();
|
||||||
// not empty == true
|
// not empty == true
|
||||||
|
|
||||||
isTrained = row[headerRef.current.isTrainedIndex] !== '';
|
isTrained = row[headerRef.current.isTrainedIndex] !== '';
|
||||||
isActive = row[headerRef.current.isActiveIndex] == '';
|
isActive = row[headerRef.current.isActiveIndex] == '';
|
||||||
desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4;
|
desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4;
|
||||||
publisherType = row[headerRef.current.pubTypeIndex];
|
publisherType = row[headerRef.current.pubTypeIndex];
|
||||||
|
isMale = row[headerRef.current.gender].trim().toLowerCase() === 'брат';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dateOfInput = common.excelSerialDateToDate(row[0]);
|
dateOfInput = common.excelSerialDateToDate(row[0]);
|
||||||
@ -201,7 +207,7 @@ export default function ImportPage() {
|
|||||||
let personNames = names.join(' ');
|
let personNames = names.join(' ');
|
||||||
try {
|
try {
|
||||||
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}`);
|
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
|
||||||
let existingPublisher = responseByName.data[0];
|
let existingPublisher = responseByName.data[0];
|
||||||
if (!existingPublisher) {
|
if (!existingPublisher) {
|
||||||
@ -244,11 +250,8 @@ export default function ImportPage() {
|
|||||||
} else {
|
} else {
|
||||||
data[i - mode.headerRow][4] = "existing";
|
data[i - mode.headerRow][4] = "existing";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log existing publisher
|
|
||||||
common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`);
|
common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`);
|
||||||
|
|
||||||
|
|
||||||
// Check for other updates
|
// Check for other updates
|
||||||
const fieldsToUpdate = [
|
const fieldsToUpdate = [
|
||||||
{ key: 'email', value: email },
|
{ key: 'email', value: email },
|
||||||
@ -256,6 +259,7 @@ export default function ImportPage() {
|
|||||||
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
|
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
|
||||||
{ key: 'isTrained', value: isTrained },
|
{ key: 'isTrained', value: isTrained },
|
||||||
{ key: 'isActive', value: isActive },
|
{ key: 'isActive', value: isActive },
|
||||||
|
{ key: "isMale", value: isMale },
|
||||||
{ key: 'type', value: publisherType, parse: common.getPubTypeEnum }
|
{ 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)
|
data[i - mode.headerRow][4] = fieldsToUpdateString.substring(0, fieldsToUpdateString.length - 2)
|
||||||
+ " updated";
|
+ " updated";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
data[i - mode.headerRow][4] = "error updating!";
|
||||||
console.error(`Failed to update publisher ${personId} - Fields Attempted: ${fieldsToUpdateString}`, error);
|
console.error(`Failed to update publisher ${personId} - Fields Attempted: ${fieldsToUpdateString}`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,6 +314,7 @@ export default function ImportPage() {
|
|||||||
firstName: firstname,
|
firstName: firstname,
|
||||||
lastName: names[names.length - 1],
|
lastName: names[names.length - 1],
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
|
isMale: isMale,
|
||||||
isTrained,
|
isTrained,
|
||||||
desiredShiftsPerMonth
|
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 => {
|
const transformedAssignments = assignments?.map(assignment => {
|
||||||
if (assignment.shift && assignment.shift.startTime) {
|
if (assignment.shift && assignment.shift.startTime) {
|
||||||
|
@ -3,35 +3,61 @@ import Layout from "../components/layout";
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { url } from 'inspector';
|
import { url } from 'inspector';
|
||||||
|
import ProtectedRoute, { serverSideAuth } from "/components/protectedRoute";
|
||||||
|
import axiosInstance from '../src/axiosSecure';
|
||||||
|
|
||||||
|
|
||||||
const PDFViewerPage = ({ pdfFiles }) => {
|
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 (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<h1 className="text-3xl font-bold p-4 pt-8">Разрешителни</h1>
|
<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 */}
|
<ProtectedRoute>
|
||||||
{/* <p className="p-1">
|
<input type="file" onChange={handleFileUpload} className="mb-4" />
|
||||||
{pdfFiles.map((file, index) => (
|
{files.map((file, index) => (
|
||||||
<p className="p-2">
|
<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'>
|
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
||||||
Свали: {file.name}
|
{file.name}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
<button onClick={() => handleFileDelete(file.name)} className="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded">
|
||||||
))}
|
изтрий
|
||||||
</p> */}
|
</button>
|
||||||
{pdfFiles.map((file, index) => (
|
</div>
|
||||||
|
))}
|
||||||
|
</ProtectedRoute>
|
||||||
|
|
||||||
// <React.Fragment key={file.name}>
|
<div style={{ width: '100%', height: 'calc(100vh - 100px)' }}> {/* Adjust the 100px based on your header/footer size */}
|
||||||
// {index > 0 && <div className="bg-gray-400 w-px h-6"></div>} {/* Vertical line separator */}
|
{pdfFiles.map((file, index) => (
|
||||||
// <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>
|
|
||||||
<> <p className="pt-2">
|
<> <p className="pt-2">
|
||||||
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
<a href={file.url} className="text-blue-600 hover:text-blue-800 visited:text-purple-600 underline" target='_blank'>
|
||||||
Свали: {file.name}
|
Свали: {file.name}
|
||||||
|
@ -4,8 +4,8 @@ CREATE TABLE `EventLog` (
|
|||||||
`date` DATETIME(3) NOT NULL,
|
`date` DATETIME(3) NOT NULL,
|
||||||
`publisherId` VARCHAR(191) NULL,
|
`publisherId` VARCHAR(191) NULL,
|
||||||
`shiftId` INTEGER NULL,
|
`shiftId` INTEGER NULL,
|
||||||
`content` VARCHAR(191) NOT NULL,
|
`content` VARCHAR(5000) NOT NULL,
|
||||||
`type` ENUM('AssignnementReplacementRequested', 'AssignnementReplacement', 'SentEmail') NOT NULL,
|
`type` ENUM('AssignmentReplacementRequested', 'AssignmentReplacementAccepted', 'SentEmail') NOT NULL,
|
||||||
|
|
||||||
|
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
|
@ -259,8 +259,8 @@ model Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum EventLogType {
|
enum EventLogType {
|
||||||
AssignnementReplacementRequested
|
AssignmentReplacementRequested
|
||||||
AssignnementReplacement
|
AssignmentReplacementAccepted
|
||||||
SentEmail
|
SentEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +271,7 @@ model EventLog {
|
|||||||
publisher Publisher? @relation(fields: [publisherId], references: [id])
|
publisher Publisher? @relation(fields: [publisherId], references: [id])
|
||||||
shiftId Int?
|
shiftId Int?
|
||||||
shift Shift? @relation(fields: [shiftId], references: [id])
|
shift Shift? @relation(fields: [shiftId], references: [id])
|
||||||
content String
|
content String @db.VarChar(5000)
|
||||||
type EventLogType
|
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.DATABASE = ", process.env.DATABASE);
|
||||||
console.log("process.env.APPLE_APP_ID = ", process.env.APPLE_APP_ID);
|
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');
|
//require('module-alias/register');
|
||||||
|
|
||||||
//import helpers
|
//import helpers
|
||||||
|
@ -13,75 +13,30 @@ const Handlebars = require('handlebars');
|
|||||||
|
|
||||||
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
const { Shift, Publisher, PrismaClient } = require("@prisma/client");
|
||||||
const { env } = require("../../next.config");
|
const { env } = require("../../next.config");
|
||||||
|
const SMTPTransport = require("nodemailer/lib/smtp-transport");
|
||||||
|
|
||||||
// const TOKEN = process.env.TOKEN || "a7d7147a530235029d74a4c2f228e6ad";
|
var transporter;
|
||||||
// const SENDER_EMAIL = "sofia@mwitnessing.com";
|
if (process.env.EMAIL_SERVICE.toLowerCase() === "mailtrap") {
|
||||||
// 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"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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 ------------------
|
// ------------------ Email sending ------------------
|
||||||
@ -137,22 +92,11 @@ exports.SendEmail = async function (to, subject, text, html, attachments = []) {
|
|||||||
attachments
|
attachments
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mailtrapTestClient !== null) {
|
let result = await transporter
|
||||||
// Assuming mailtrapTestClient is correctly set up to send emails
|
.sendMail(message)
|
||||||
await mailtrapTestClient
|
.then(console.log)
|
||||||
.send(message)
|
.catch(console.error);
|
||||||
.then(console.log)
|
return result;
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
let result = await transporter
|
|
||||||
.sendMail(message)
|
|
||||||
.then(console.log)
|
|
||||||
.catch(console.error);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.SendEmailHandlebars = async function (to, templateName, model, attachments = []) {
|
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()],
|
// subject: "[CCC]: вашите смени през " + CON.monthNamesBG[date.getMonth()],
|
||||||
// text:
|
// text:
|
||||||
// "Здравейте, " + publisher.firstName + " " + publisher.lastName + "!\n\n" +
|
// "Здравей, " + publisher.firstName + " " + publisher.lastName + "!\n\n" +
|
||||||
// "Ти регистриран да получавате известия за нови смени на количка.\n" +
|
// "Ти регистриран да получавате известия за нови смени на количка.\n" +
|
||||||
// `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` +
|
// `За месец ${CON.monthNamesBG[date.getMonth()]} имате следните смени:\n` +
|
||||||
// ` ${shftStr} \n\n\n` +
|
// ` ${shftStr} \n\n\n` +
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{{!-- за смяна на {{placeName}} за {{dateStr}}! --}}
|
{{!-- за смяна на {{placeName}} за {{dateStr}}! --}}
|
||||||
</h3>
|
</h3>
|
||||||
<p>Здравей {{firstName}},</p>
|
<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><strong>Shift Details:</strong></p> --}}
|
||||||
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
||||||
<p>С натискането на бутона по-долу можеш да премеш да го заместваш.
|
<p>С натискането на бутона по-долу можеш да премеш да го заместваш.
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Промяна твоята смяна на {{placeName}} {{dateStr}} </h2>
|
<h2>Промяна твоята смяна на {{placeName}} {{dateStr}} </h2>
|
||||||
<p>Здравейте {{firstName}}, </p>
|
<p>Здравей {{firstName}}, </p>
|
||||||
<p>{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}</p>
|
<p>{{firstName}} {{lastName}} ще замести {{oldPubName}} на смяната ви в {{dateStr}} от {{time}}</p>
|
||||||
<p>Новаия списък с участници за тази смяна е:</p>
|
<p>Новаия списък с участници за тази смяна е:</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -4,7 +4,7 @@ text version. --}}
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3>Търси се зместник за смяна на {{placeName}} за {{dateStr}}!</h3>
|
<h3>Търси се зместник за смяна на {{placeName}} за {{dateStr}}!</h3>
|
||||||
<p>Здравейте,</p>
|
<p>Здравей,</p>
|
||||||
<p>{{prefix}} {{firstName}} {{lastName}} търси заместник.</p>
|
<p>{{prefix}} {{firstName}} {{lastName}} търси заместник.</p>
|
||||||
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
{{!-- <p><strong>Shift Details:</strong></p> --}}
|
||||||
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
<p>Дата: {{dateStr}} <br>Час: {{time}}<br>Място: {{placeName}}</p>
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer style="background-color: #f3f3f3; padding: 20px; text-align: center;">
|
<footer style="background-color: #f3f3f3; padding: 20px; text-align: center;">
|
||||||
© 2024 ССС. All rights reserved.
|
© 2024 ССС. Openly licensed.
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{!-- Subject: ССС: Нови назначени смени--}}
|
{{!-- Subject: ССС: Нови назначени смени--}}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Здравейте, {{publisherFirstName}} {{publisherLastName}}!</h2>
|
<h2>Здравей {{publisherFirstName}} {{publisherLastName}}!</h2>
|
||||||
<p>Ти регистриран да получавате известия за нови смени на количка.</p>
|
<p>Ти регистриран да получавате известия за нови смени на количка.</p>
|
||||||
<p>За месец {{month}} имате следните смени:</p>
|
<p>За месец {{month}} имате следните смени:</p>
|
||||||
<div>
|
<div>
|
||||||
|
Reference in New Issue
Block a user