Merge branch 'production'

This commit is contained in:
Dobromir Popov
2024-07-06 19:18:22 +03:00
12 changed files with 120 additions and 218 deletions

View File

@ -9,6 +9,7 @@ services:
# - "3000:3000" # - "3000:3000"
volumes: volumes:
- /mnt/docker_volumes/pw/app/public/content/uploads/:/app/public/content/uploads - /mnt/docker_volumes/pw/app/public/content/uploads/:/app/public/content/uploads
- /mnt/docker_volumes/pw/app/logs:/app/logs
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- TZ=Europe/Sofia - TZ=Europe/Sofia
@ -19,7 +20,7 @@ services:
- GIT_USERNAME=deploy - GIT_USERNAME=deploy
- GIT_PASSWORD=L3Kr2R438u4F7 - GIT_PASSWORD=L3Kr2R438u4F7
- ADMIN_PASSWORD=changeme - ADMIN_PASSWORD=changeme
command: sh -c " cd /app && npm install && npm run prod; tail -f /dev/null" command: sh -c " cd /app && npm install && npx next build && npm run prod; tail -f /dev/null"
#command: sh -c " cd /app && tail -f /dev/null" #command: sh -c " cd /app && tail -f /dev/null"
tty: true tty: true
stdin_open: true stdin_open: true
@ -56,15 +57,15 @@ services:
networks: networks:
- infrastructure_default - infrastructure_default
command: | command: |
apk update && \ "apk update && \
apk add --no-cache mariadb-client mariadb-connector-c && \ apk add --no-cache mariadb-client mariadb-connector-c && \
echo '0 2 * * * mysqldump -h $$MYSQL_HOST -P 3306 -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE > /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql' > /etc/crontabs/root && \ echo '0 2 * * * mysqldump -h $$MYSQL_HOST -P 3306 -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE > /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql' > /etc/crontabs/root && \
echo '0 7 * * * rclone sync /backup nextcloud:/mwitnessing' >> /etc/crontabs/root && \ crond -f -d 8"
crond -f -d 8
# wget -q https://github.com/prasmussen/gdrive/releases/download/2.1.0/gdrive-linux-x64 -O /usr/bin/gdrive && \ # wget -q https://github.com/prasmussen/gdrive/releases/download/2.1.0/gdrive-linux-x64 -O /usr/bin/gdrive && \
# chmod +x /usr/bin/gdrive && \ # chmod +x /usr/bin/gdrive && \
# gdrive about --service-account /root/.gdrive_service_account.json && \ # gdrive about --service-account /root/.gdrive_service_account.json && \
# echo '0 * * * * /usr/bin/mysqldump -h $$MYSQL_HOST -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE | gzip > /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql.gz && gdrive upload --parent $$GOOGLE_DRIVE_FOLDER_ID --service-account /root/.gdrive_service_account.json /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql.gz' > /etc/crontabs/root && crond -f -d 8" # echo '0 * * * * /usr/bin/mysqldump -h $$MYSQL_HOST -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE | gzip > /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql.gz && gdrive upload --parent $$GOOGLE_DRIVE_FOLDER_ID --service-account /root/.gdrive_service_account.json /backup/$$(date +\\%Y-\\%m-\\%d-\\%H\\%M\\%S)-$$MYSQL_DATABASE.sql.gz' > /etc/crontabs/root && crond -f -d 8 \
# echo '0 7 * * * rclone sync /backup nextcloud:/mwitnessing' >> /etc/crontabs/root && \"
networks: networks:
infrastructure_default: infrastructure_default:
external: true external: true

View File

@ -12,8 +12,9 @@ if [ "$UPDATE_CODE_FROM_GIT" = "true" ]; then
# Clone the repository # Clone the repository
git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwitnessing.git /tmp/clone || exit 1 git clone -b ${GIT_BRANCH:-main} --depth 1 https://$GIT_USERNAME:${GIT_PASSWORD//@/%40}@git.d-popov.com/popov/mwitnessing.git /tmp/clone || exit 1
# Synchronize all files except package.json and package-lock.json to /app # Synchronize all files except package.json and package-lock.json to /app. alo exclude '/app/public/content/uploads' to avoid deleting uploaded files
rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files" rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' --exclude '/app/public/content/uploads'
/tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files"
# Determine if package.json or package-lock.json has changed # Determine if package.json or package-lock.json has changed
PACKAGE_CHANGE=0 PACKAGE_CHANGE=0

View File

@ -262,4 +262,12 @@ in schedule admin - if a publisher is always pair & family is not in the shift -
[] allow blocking of inputs (different from publishing) TODO: fix to keep previous occurances when repeating evert week [] allow blocking of inputs (different from publishing) TODO: fix to keep previous occurances when repeating evert week
[] user - add createdAt field [] user - add createdAt field
[] FIX insecure logins [x] FIX insecure logins
[] nove push to form, - reorganize pWAManager to have session, role, subscriptions, etc...
[] add shift name in calendar/ show in schedule if no assignments.
[] show unpublished schedule if admin

View File

@ -27,7 +27,7 @@ class ErrorBoundary extends React.Component {
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
// Render any custom fallback UI // Render any custom fallback UI
return <h1>Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи. </h1>; return <h1>Нещо се обърка. Моля, опитай отново и се свържете с нас ако проблема продължи. </h1>;
} }
return this.props.children; return this.props.children;

View File

@ -6,7 +6,7 @@ import e from 'express';
import ProtectedRoute from './protectedRoute'; import ProtectedRoute from './protectedRoute';
import { UserRole } from '@prisma/client'; import { UserRole } from '@prisma/client';
function PwaManager({ subs }) { function PwaManager({ userId, subs }) {
//ToDo: for iOS, try to use apn? https://github.com/node-apn/node-apn/blob/master/doc/apn.markdown //ToDo: for iOS, try to use apn? https://github.com/node-apn/node-apn/blob/master/doc/apn.markdown
const isSupported = () => const isSupported = () =>
'Notification' in window && 'Notification' in window &&
@ -271,6 +271,37 @@ function PwaManager({ subs }) {
{ action: 'close', title: 'Затвори', icon: '❌' }] { action: 'close', title: 'Затвори', icon: '❌' }]
}) })
}); });
/*
await fetch('/api/notify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: pub.id,
message: "Тестово съобщение",
title: "Това е тестово съобщение от https://sofia.mwitnessing.com",
actions: [
{ action: 'OK', title: 'OK', icon: '✅' },
{ action: 'close', title: 'Затвори', icon: '❌' }
]
// actions: [
// {
// title: 'Open URL',
// action: 'open_url',
// icon: '/images/open-url.png'
// },
// {
// title: 'Dismiss',
// action: 'dismiss',
// icon: '/images/dismiss.png'
// }
// ]
})
})
*/
}; };
// async function sendTestReminder(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> { // async function sendTestReminder(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
@ -382,7 +413,7 @@ function PwaManager({ subs }) {
> >
Тестово уведомление Тестово уведомление
</button> </button>
</div> </div >
{isAdmin && {isAdmin &&
<div> <div>
{/* <button {/* <button
@ -403,28 +434,31 @@ function PwaManager({ subs }) {
</button> */} </button> */}
</div> </div>
} }
{notificationPermission !== "granted" && ( {
<button notificationPermission !== "granted" && (
onClick={togglePushNotifications} <button
className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${notificationPermission === "denied" ? 'bg-red-500 hover:bg-red-700 text-white' : 'bg-green-500 hover:bg-green-700 text-white' onClick={togglePushNotifications}
}`} className={`text-xs py-1 px-2 rounded-full focus:outline-none transition duration-150 ease-in-out ${notificationPermission === "denied" ? 'bg-red-500 hover:bg-red-700 text-white' : 'bg-green-500 hover:bg-green-700 text-white'
> }`}
{notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'} >
</button> {notificationPermission === "denied" ? 'Notifications Denied!' : 'Enable Notifications'}
)} </button>
)
}
{isAdmin && <div> {
<div> isAdmin && <div>
<a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank"> <div>
<img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" /> <a href="https://t.me/mwhitnessing_bot" className="inline-flex items-center ml-4" target="_blank">
<span className="align-middle">Телеграм</span> <img src="/content/icons/telegram-svgrepo-com.svg" alt="Телеграм" width="32" height="32" className="align-middle" />
</a> <span className="align-middle">Телеграм</span>
</a>
<a href="/api/auth/apple-signin" className="inline-flex items-center ml-4 bg-gray-100 button" target="_blank"> <a href="/api/auth/apple-signin" className="inline-flex items-center ml-4 bg-gray-100 button" target="_blank">
<span className="align-middle">Apple sign-in</span> <span className="align-middle">Apple sign-in</span>
</a> </a>
</div>
</div> </div>
</div>
} }
</> </>
); );

View File

@ -303,7 +303,8 @@ export default function PublisherForm({ item, me }) {
{/* In-App notifications group */} {/* In-App notifications group */}
<div className="mb-4"> <div className="mb-4">
<h3 className="text-md font-semibold mb-2">Известия в приложението</h3> <h3 className="text-md font-semibold mb-2">Известия в приложението</h3>
<PwaManager /> <PwaManager userId={publisher.userId || session.user.id} />
</div> </div>
</fieldset> </fieldset>
</div> </div>

View File

@ -185,5 +185,4 @@ const removeAssignment = async (publisher, shiftId) => {
} }
} }
export default PublisherShiftsModal; export default PublisherShiftsModal;

View File

@ -62,8 +62,8 @@ const SurveyForm: React.FC<SurveyFormProps> = ({ existingItem }) => {
...existingItem, ...existingItem,
content: existingItem?.content || "Нова анкета", content: existingItem?.content || "Нова анкета",
answers: existingItem?.answers.split(",") || [], answers: existingItem?.answers.split(",") || [],
publicFrom: existingItem?.publicFrom ? dayjs(existingItem.publicFrom).toISOString() : new Date().toISOString(), publicFrom: existingItem?.publicFrom ? dayjs(existingItem.publicFrom).toISOString() : '',
publicUntil: existingItem?.publicUntil ? dayjs(existingItem.publicUntil).toISOString() : new Date().toISOString(), publicUntil: existingItem?.publicUntil ? dayjs(existingItem.publicUntil).toISOString() : null,
}); });
@ -104,7 +104,7 @@ const SurveyForm: React.FC<SurveyFormProps> = ({ existingItem }) => {
} }
else { else {
//get all publisherIds and create a message for each //get all publisherIds and create a message for each
const messages = pubs.data.map(pub => { const messages = pubs.map(pub => {
return { return {
publisherId: pub.id, publisherId: pub.id,
content: JSON.stringify({ message: item.content, options: item.answers }), content: JSON.stringify({ message: item.content, options: item.answers }),
@ -236,13 +236,16 @@ const SurveyForm: React.FC<SurveyFormProps> = ({ existingItem }) => {
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="date"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="date">
Видима от Видима от
</label> </label>
<DatePicker className="textbox form-input px-4 py-2 rounded" name="publicFrom" onChange={(newDate) => handleDateChange('publicFrom', newDate)} value={dayjs(item?.publicFrom)} /> <DatePicker className="textbox form-input px-4 py-2 rounded" name="publicFrom" onChange={(newDate) => handleDateChange('publicFrom', newDate)}
value={item && item.publicFrom ? dayjs(item.publicFrom) : null} />
</div> </div>
<div className="mb-4"> <div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="date"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="date">
Видима до Видима до
</label> </label>
<DatePicker className="textbox form-input px-4 py-2 rounded" name="publicUntil" onChange={(newDate) => handleDateChange('publicUntil', newDate)} value={dayjs(item?.publicUntil)} /> <DatePicker className="textbox form-input px-4 py-2 rounded" name="publicUntil" onChange={(newDate) => handleDateChange('publicUntil', newDate)}
value={item && item.publicUntil ? dayjs(item.publicUntil) : null}
/>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="content"> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="content">

View File

@ -94,6 +94,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
where: { where: {
isActive: true, isActive: true,
isPublished: true, isPublished: true,
// OR: [
// { isPublished: true },
// { user: { role: 'admin' } } // Todo: example. fix this
// ],
startTime: { startTime: {
gte: fromDate, gte: fromDate,
//lt: toDate, //lt: toDate,
@ -152,15 +156,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
//bold the text after - in the notes //bold the text after - in the notes
notes: notes, notes: notes,
notes_bold: notes_bold, notes_bold: notes_bold,
names: shift.assignments names: shift.assignments.length > 0
.map((assignment) => { ? shift.assignments
return ( .map((assignment) => {
assignment.publisher.firstName + return (
" " + assignment.publisher.firstName +
assignment.publisher.lastName " " +
); assignment.publisher.lastName
}) );
.join(", "), })
.join(", ")
: shift.name,
}; };
if (shiftSchedule.names.length > 0) { if (shiftSchedule.names.length > 0) {
@ -246,6 +252,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
); );
} }
catch (error) { catch (error) {
console.log(error);
res.status(500).json({ error: "Internal Server Error" }); res.status(500).json({ error: "Internal Server Error" });
} }
} else { } else {

View File

@ -9,6 +9,7 @@ import Shift from '../../../components/calendar/ShiftComponent';
import { DayOfWeek, UserRole } from '@prisma/client'; import { DayOfWeek, UserRole } from '@prisma/client';
import { env } from 'process' import { env } from 'process'
import ShiftComponent from '../../../components/calendar/ShiftComponent'; import ShiftComponent from '../../../components/calendar/ShiftComponent';
import PublisherShiftsModal from '../../../components/publisher/PublisherShiftsModal';
//import { set } from 'date-fns'; //import { set } from 'date-fns';
const common = require('src/helpers/common'); const common = require('src/helpers/common');
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -66,6 +67,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
const [shifts, setShifts] = React.useState([]); const [shifts, setShifts] = React.useState([]);
const [error, setError] = React.useState(null); const [error, setError] = React.useState(null);
const [availablePubs, setAvailablePubs] = React.useState([]); const [availablePubs, setAvailablePubs] = React.useState([]);
const [selectedPublisher, setSelectedPublisher] = React.useState(null);
const [selectedShiftId, setSelectedShiftId] = useState(null); const [selectedShiftId, setSelectedShiftId] = useState(null);
@ -214,8 +216,8 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
}; };
const handleSelectedPublisher = (publisher) => { const handleSelectedPublisher = (publisher) => {
// Do something with the selected publisher
console.log("handle pub clicked:", publisher); console.log("handle pub clicked:", publisher);
setSelectedPublisher(publisher);
} }
const handlePublisherModalOpen = async (publisher) => { const handlePublisherModalOpen = async (publisher) => {
// Do something with the selected publisher // Do something with the selected publisher
@ -355,7 +357,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
} }
return <div>{" "}</div>; return <div>{" "}</div>;
}; };
//ToDo: DRY - move to common
const addAssignment = async (publisher, shiftId) => { const addAssignment = async (publisher, shiftId) => {
try { try {
console.log(`calendar.idx: new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`); console.log(`calendar.idx: new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`);
@ -365,6 +367,10 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
isConfirmed: true isConfirmed: true
}; };
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment); const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
if (selectedShiftId == shiftId) {
handleShiftSelection(shifts.find(shift => shift.id === shiftId));
}
// Update the 'publisher' property of the returned data with the full publisher object // Update the 'publisher' property of the returned data with the full publisher object
data.publisher = publisher; data.publisher = publisher;
data.shift = shifts.find(shift => shift.id === shiftId); data.shift = shifts.find(shift => shift.id === shiftId);
@ -779,7 +785,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
<input type="checkbox" className="toggle-checkbox" id="filterIncludeOldAvailabilities" onChange={handleCheckboxChange} /> <input type="checkbox" className="toggle-checkbox" id="filterIncludeOldAvailabilities" onChange={handleCheckboxChange} />
<span className="toggle-slider m-1">със стари предпочитания</span> <span className="toggle-slider m-1">със стари предпочитания</span>
</label> </label>
<ul className="w-full max-w-md"> <ul className="w-full max-w-md" id="availablePubsList" name="availablePubsList">
{Array.isArray(availablePubs) && availablePubs?.map((pub, index) => { {Array.isArray(availablePubs) && availablePubs?.map((pub, index) => {
// Determine background and border classes based on conditions // Determine background and border classes based on conditions
let bgAndBorderColorClass; let bgAndBorderColorClass;
@ -814,8 +820,8 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
<li key={index} <li key={index}
className={`flex justify-between items-center p-4 sm:py-2 rounded-lg shadow-sm mb-2 className={`flex justify-between items-center p-4 sm:py-2 rounded-lg shadow-sm mb-2
${bgAndBorderColorClass} ${selectedBorderClass} ${activeOpacityClass} ${bgAndBorderColorClass} ${selectedBorderClass} ${activeOpacityClass}
${pub.currentMonthAssignments >= pub.desiredShiftsPerMonth ? 'text-gray-400' : 'text-gray-800'}`} ${pub.currentMonthAssignments === pub.desiredShiftsPerMonth ? 'text-gray-400' : pub.currentMonthAssignments > pub.desiredShiftsPerMonth ? 'text-orange-300' : 'text-gray-800'}`}
onDoubleClick={(handlePublisherModalOpen.bind(this, pub))} onDoubleClick={() => handlePublisherModalOpen(pub)}
> >
<span className={`${pub.isAvailableForShift ? 'font-bold' : 'font-medium'} `}> <span className={`${pub.isAvailableForShift ? 'font-bold' : 'font-medium'} `}>
{pub.firstName} {pub.lastName} {pub.firstName} {pub.lastName}
@ -831,36 +837,10 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
<button tooltip="желани участия на месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button> <button tooltip="желани участия на месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
<button tooltip="push" title="push" className={`badge py-1 px-2 rounded-md text-xs bg-red-100`} <button tooltip="push" title="push" className={`badge py-1 px-2 rounded-md text-xs bg-red-100`}
onClick={async () => { onClick={async () => {
await fetch('/api/notify', { handleSelectedPublisher(pub);
method: 'POST', addAssignment(pub, selectedShiftId);
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: pub.id,
message: "Тестово съобщение",
title: "Това е тестово съобщение от https://sofia.mwitnessing.com",
actions: [
{ action: 'OK', title: 'OK', icon: '✅' },
{ action: 'close', title: 'Затвори', icon: '❌' }
]
// actions: [
// {
// title: 'Open URL',
// action: 'open_url',
// icon: '/images/open-url.png'
// },
// {
// title: 'Dismiss',
// action: 'dismiss',
// icon: '/images/dismiss.png'
// }
// ]
})
})
}} }}
>+</button> >+</button>
</div> </div>
</li> </li>
); );
@ -913,145 +893,8 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
</> </>
); );
function PublisherShiftsModal({ publisher, shifts, onClose }) {
const monthInfo = common.getMonthDatesInfo(new Date(value));
const monthShifts = shifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return shiftDate > monthInfo.firstDay && shiftDate < monthInfo.lastDay;
});
const weekShifts = monthShifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return common.getStartOfWeek(value) <= shiftDate && shiftDate <= common.getEndOfWeek(value);
});
const dayShifts = weekShifts.map(shift => {
const isAvailable = publisher.availabilities?.some(avail =>
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
);
let color = isAvailable ? getColorForShift(shift) : 'bg-gray-300';
if (shift.isFromPreviousMonth) {
color += ' border-l-4 border-orange-500 ';
}
if (shift.isFromPreviousAssignment) {
color += ' border-l-4 border-red-500 ';
}
return { ...shift, isAvailable, color };
}).reduce((acc, shift) => {
const dayIndex = new Date(shift.startTime).getDay();
acc[dayIndex] = acc[dayIndex] || [];
acc[dayIndex].push(shift);
return acc;
}, {});
console.log("dayShifts:", dayShifts);
const hasAssignment = (shiftId) => {
// return publisher.assignments.some(ass => ass.shift.id == shiftId);
return publisher.assignments?.some(ass => {
//console.log(`Comparing: ${ass.shift.id} to ${shiftId}: ${ass.shift.id === shiftId}`);
return ass.shift.id === shiftId;
});
};
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
console.log('ESC: closing modal.');
onClose(); // Call the onClose function when ESC key is pressed
}
};
// Add event listener
window.addEventListener('keydown', handleKeyDown);
// Remove event listener on cleanup
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]); // Include onClose in the dependency array
return (
<div className="fixed top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-50 z-50">
<div className="relative bg-white p-8 rounded-lg shadow-xl max-w-xl w-full h-auto overflow-y-auto">
<h2 className="text-xl font-semibold mb-4">График на <span title={publisher.email} className='publisher'>
<strong>{publisher.firstName} {publisher.lastName}</strong>
<span className="publisher-tooltip" onClick={common.copyToClipboard}>{publisher.email}</span>
</span> тази седмица:</h2>
{/* ... Display shifts in a calendar-like UI ... */}
<div className="grid grid-cols-6 gap-4 mb-4">
{Object.entries(dayShifts).map(([dayIndex, shiftsForDay]) => (
<div key={dayIndex} className="flex flex-col space-y-2 justify-end">
{/* Day header */}
<div className="text-center font-medium">{new Date(shiftsForDay[0].startTime).getDate()}-ти</div>
{shiftsForDay.map((shift, index) => {
const assignmentExists = hasAssignment(shift.id);
const availability = publisher.availabilities.find(avail =>
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
);
const isFromPrevMonth = availability && availability.isFromPreviousMonth;
return (
<div
key={index}
className={`text-sm text-white p-2 rounded-md ${isFromPrevMonth ? 'border-l-6 border-black-500' : ''} ${shift.color} ${assignmentExists ? 'border-2 border-blue-500' : ""} h-24 flex flex-col justify-center`}
>
{common.getTimeRange(shift.startTime, shift.endTime)} {shift.id}
{!assignmentExists && shift.isAvailable && (
<button onClick={() => { addAssignment(publisher, shift.id); }}
className="mt-2 bg-green-500 text-white p-1 rounded hover:bg-green-600 active:bg-green-700 focus:outline-none"
>
добави
</button>
)}
{assignmentExists && (
<button onClick={() => { removeAssignment(publisher, shift.id) }} // Implement the removeAssignment function
className="mt-2 bg-red-500 text-white p-1 rounded hover:bg-red-600 active:bg-red-700 focus:outline-none"
>
махни
</button>
)}
</div>
);
}
)}
</div>
))}
</div>
{/* Close button in the top right corner */}
<button
onClick={onClose}
className="absolute top-3 right-2 p-2 px-3 bg-red-500 text-white rounded-full hover:bg-red-600 active:bg-red-700 focus:outline-none"
>
&times;
</button>
{/* <Link href={`/cart/publishers/edit/${modalPub.id}`}
className="mt-2 bg-blue-500 text-white p-1 rounded hover:bg-blue-600 active:bg-blue-700 focus:outline-none">
<i className="fas fa-edit" />
</Link> */}
{/* Edit button in the top right corner, next to the close button */}
<Link href={`/cart/publishers/edit/${modalPub.id}`} className="absolute top-3 right-12 p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 active:bg-blue-700 focus:outline-none">
<i className="fas fa-edit" />
</Link>
</div>
</div >
);
}
function getColorForShift(shift) {
const assignedCount = shift.assignedCount || 0; // Assuming each shift has an assignedCount property
switch (assignedCount) {
case 0: return 'bg-blue-300';
case 1: return 'bg-green-300';
case 2: return 'bg-yellow-300';
case 3: return 'bg-orange-300';
case 4: return 'bg-red-200';
default: return 'bg-gray-300';
}
}
} }
import axiosServer from '../../../src/axiosServer'; import axiosServer from '../../../src/axiosServer';

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE `Message` MODIFY `content` TEXT NOT NULL;
-- AlterTable
ALTER TABLE `Survey` MODIFY `content` TEXT NOT NULL;

View File

@ -266,7 +266,7 @@ enum MessageType {
model Survey { model Survey {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
content String content String @db.Text
answers Json? answers Json?
messages Message[] messages Message[]
publicFrom DateTime? publicFrom DateTime?
@ -278,7 +278,7 @@ model Message {
publisher Publisher @relation(fields: [publisherId], references: [id]) publisher Publisher @relation(fields: [publisherId], references: [id])
publisherId String publisherId String
date DateTime date DateTime
content String content String @db.Text
isRead Boolean @default(false) isRead Boolean @default(false)
isPublic Boolean @default(false) isPublic Boolean @default(false)
type MessageType @default(Email) type MessageType @default(Email)