diff --git a/_deploy/deoloy.azure.production.yml b/_deploy/deoloy.azure.production.yml index 55a11d4..c91c26e 100644 --- a/_deploy/deoloy.azure.production.yml +++ b/_deploy/deoloy.azure.production.yml @@ -9,6 +9,7 @@ services: # - "3000:3000" volumes: - /mnt/docker_volumes/pw/app/public/content/uploads/:/app/public/content/uploads + - /mnt/docker_volumes/pw/app/logs:/app/logs environment: - NODE_ENV=production - TZ=Europe/Sofia @@ -19,7 +20,7 @@ services: - GIT_USERNAME=deploy - GIT_PASSWORD=L3Kr2R438u4F7 - 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" tty: true stdin_open: true @@ -56,15 +57,15 @@ services: networks: - infrastructure_default command: | - apk update && \ + "apk update && \ 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 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 && \ # chmod +x /usr/bin/gdrive && \ # 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: infrastructure_default: external: true diff --git a/_deploy/entrypoint.sh b/_deploy/entrypoint.sh index db16b09..fa78e35 100644 --- a/_deploy/entrypoint.sh +++ b/_deploy/entrypoint.sh @@ -12,8 +12,9 @@ if [ "$UPDATE_CODE_FROM_GIT" = "true" ]; then # 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 - # Synchronize all files except package.json and package-lock.json to /app - rsync -av --delete --exclude 'package.json' --exclude 'package-lock.json' /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files" + # 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' --exclude '/app/public/content/uploads' + /tmp/clone/ /app/ || echo "Rsync failed: Issue synchronizing files" # Determine if package.json or package-lock.json has changed PACKAGE_CHANGE=0 diff --git a/_doc/ToDo.md b/_doc/ToDo.md index 1bc87ef..8a37727 100644 --- a/_doc/ToDo.md +++ b/_doc/ToDo.md @@ -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 [] user - add createdAt field -[] FIX insecure logins \ No newline at end of file +[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 + + + + diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx index 2dbbd89..87a148f 100644 --- a/components/ErrorBoundary.tsx +++ b/components/ErrorBoundary.tsx @@ -27,7 +27,7 @@ class ErrorBoundary extends React.Component { render() { if (this.state.hasError) { // Render any custom fallback UI - return

Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи.

; + return

Нещо се обърка. Моля, опитай отново и се свържете с нас ако проблема продължи.

; } return this.props.children; diff --git a/components/PwaManager.tsx b/components/PwaManager.tsx index 464afda..852fd0d 100644 --- a/components/PwaManager.tsx +++ b/components/PwaManager.tsx @@ -6,7 +6,7 @@ import e from 'express'; import ProtectedRoute from './protectedRoute'; 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 const isSupported = () => 'Notification' in window && @@ -271,6 +271,37 @@ function PwaManager({ subs }) { { 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): Promise { @@ -382,7 +413,7 @@ function PwaManager({ subs }) { > Тестово уведомление - + {isAdmin &&
{/*
} - {notificationPermission !== "granted" && ( - - )} + { + notificationPermission !== "granted" && ( + + ) + } - {isAdmin &&
-
- - Телеграм - Телеграм - + { + isAdmin &&
+
-
} ); diff --git a/components/publisher/PublisherForm.js b/components/publisher/PublisherForm.js index 7d76de3..85cdb6f 100644 --- a/components/publisher/PublisherForm.js +++ b/components/publisher/PublisherForm.js @@ -303,7 +303,8 @@ export default function PublisherForm({ item, me }) { {/* In-App notifications group */}

Известия в приложението

- + +
diff --git a/components/publisher/PublisherShiftsModal.js b/components/publisher/PublisherShiftsModal.js index c873d8b..b0b6247 100644 --- a/components/publisher/PublisherShiftsModal.js +++ b/components/publisher/PublisherShiftsModal.js @@ -185,5 +185,4 @@ const removeAssignment = async (publisher, shiftId) => { } } - export default PublisherShiftsModal; \ No newline at end of file diff --git a/components/survey/SurveyForm.tsx b/components/survey/SurveyForm.tsx index 25f4373..017c690 100644 --- a/components/survey/SurveyForm.tsx +++ b/components/survey/SurveyForm.tsx @@ -62,8 +62,8 @@ const SurveyForm: React.FC = ({ existingItem }) => { ...existingItem, content: existingItem?.content || "Нова анкета", answers: existingItem?.answers.split(",") || [], - publicFrom: existingItem?.publicFrom ? dayjs(existingItem.publicFrom).toISOString() : new Date().toISOString(), - publicUntil: existingItem?.publicUntil ? dayjs(existingItem.publicUntil).toISOString() : new Date().toISOString(), + publicFrom: existingItem?.publicFrom ? dayjs(existingItem.publicFrom).toISOString() : '', + publicUntil: existingItem?.publicUntil ? dayjs(existingItem.publicUntil).toISOString() : null, }); @@ -104,7 +104,7 @@ const SurveyForm: React.FC = ({ existingItem }) => { } else { //get all publisherIds and create a message for each - const messages = pubs.data.map(pub => { + const messages = pubs.map(pub => { return { publisherId: pub.id, content: JSON.stringify({ message: item.content, options: item.answers }), @@ -236,13 +236,16 @@ const SurveyForm: React.FC = ({ existingItem }) => { - handleDateChange('publicFrom', newDate)} value={dayjs(item?.publicFrom)} /> + handleDateChange('publicFrom', newDate)} + value={item && item.publicFrom ? dayjs(item.publicFrom) : null} />
- handleDateChange('publicUntil', newDate)} value={dayjs(item?.publicUntil)} /> + handleDateChange('publicUntil', newDate)} + value={item && item.publicUntil ? dayjs(item.publicUntil) : null} + />
-
    +
      {Array.isArray(availablePubs) && availablePubs?.map((pub, index) => { // Determine background and border classes based on conditions let bgAndBorderColorClass; @@ -814,8 +820,8 @@ export default function CalendarPage({ initialEvents, initialShifts }) {
    • = pub.desiredShiftsPerMonth ? 'text-gray-400' : 'text-gray-800'}`} - onDoubleClick={(handlePublisherModalOpen.bind(this, pub))} + ${pub.currentMonthAssignments === pub.desiredShiftsPerMonth ? 'text-gray-400' : pub.currentMonthAssignments > pub.desiredShiftsPerMonth ? 'text-orange-300' : 'text-gray-800'}`} + onDoubleClick={() => handlePublisherModalOpen(pub)} > {pub.firstName} {pub.lastName} @@ -831,36 +837,10 @@ export default function CalendarPage({ initialEvents, initialShifts }) { -
); @@ -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 ( -
-
-

График на - {publisher.firstName} {publisher.lastName} - {publisher.email} - тази седмица:

- - {/* ... Display shifts in a calendar-like UI ... */} -
- {Object.entries(dayShifts).map(([dayIndex, shiftsForDay]) => ( -
- {/* Day header */} -
{new Date(shiftsForDay[0].startTime).getDate()}-ти
- - {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 ( -
- {common.getTimeRange(shift.startTime, shift.endTime)} {shift.id} - - {!assignmentExists && shift.isAvailable && ( - - )} - {assignmentExists && ( - - )} -
- ); - } - )} -
- ))} -
- - {/* Close button in the top right corner */} - - - {/* - - */} - {/* Edit button in the top right corner, next to the close button */} - - - - -
-
- ); - } - - 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'; diff --git a/prisma/migrations/20240621124135_/migration.sql b/prisma/migrations/20240621124135_/migration.sql new file mode 100644 index 0000000..5dc706d --- /dev/null +++ b/prisma/migrations/20240621124135_/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE `Message` MODIFY `content` TEXT NOT NULL; + +-- AlterTable +ALTER TABLE `Survey` MODIFY `content` TEXT NOT NULL; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 35996b6..91a0197 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -266,7 +266,7 @@ enum MessageType { model Survey { id Int @id @default(autoincrement()) - content String + content String @db.Text answers Json? messages Message[] publicFrom DateTime? @@ -278,7 +278,7 @@ model Message { publisher Publisher @relation(fields: [publisherId], references: [id]) publisherId String date DateTime - content String + content String @db.Text isRead Boolean @default(false) isPublic Boolean @default(false) type MessageType @default(Email)