initial commit - code moved to separate repo

This commit is contained in:
Dobromir Popov
2024-02-22 04:19:38 +02:00
commit 560d503219
240 changed files with 105125 additions and 0 deletions

View File

@ -0,0 +1,2 @@
import NewPage from "../new";
export default NewPage;

View File

@ -0,0 +1,158 @@
//next.js page to show all locatons in the database with a link to the location page
import { Availability, UserRole } from "@prisma/client";
import { format } from "date-fns";
import { useRouter } from "next/router";
import { useState } from 'react';
import Layout from "../../../components/layout";
import axiosInstance from '../../../src/axiosSecure';
import axiosServer from '../../../src/axiosServer';
import ProtectedRoute from '../../../components/protectedRoute';
import AvCalendar from '../../../components/calendar/avcalendar';
interface IProps {
initialItems: Availability[];
id: string;
}
// export default function AvPage({} : IProps) {
export default function AvPage({ initialItems, id }: IProps) {
const router = useRouter();
// items.forEach(item => {
// item.publisher = prisma.publisher.findUnique({where: {id: item.publisherId}});
// });
const [items, set] = useState(initialItems);
const events = initialItems?.map(item => ({
id: item.id,
title: item.name,
date: new Date(item.startTime),
start: new Date(item.startTime),
end: new Date(item.endTime),
isactive: item.isactive,
publisherId: item.publisher.id,
dayOfMonth: item.dayOfMonth,
dayOfWeek: item.dayOfWeek,
}));
const render = () => {
console.log("showing " + initialItems?.length + " availabilities");
if (initialItems?.length === 0) return <h1>No Items</h1>;
return ( //AvailabilityList(items));
<>
<table className="min-w-full">
<thead className="border-b">
<tr>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
#
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Publisher
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Name
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Weekday
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
From
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
To
</th>
</tr>
</thead>
<tbody>
{initialItems?.map((item: Availability) => (
<tr key={item.id} className={item.isactive ? "" : "text-gray-300"}>
<td className="px-6 py-4 whitespace-nowrap ">
{item.id} {item.isactive}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.publisher.lastName}, {item.publisher.firstName}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.name}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.dayofweek}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{format(new Date(item.startTime), "HH:mm")}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{format(new Date(item.endTime), "HH:mm")}
</td>
<td>
<button className="btn text-gray-700"
onClick={() => router.push(`/cart/availabilities/edit/${item.id}`)} >
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</>
)
};
return <Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN]}>
<AvCalendar publisherId={id} events={events} />
<div className="max-w-7xl mx-auto">
<div>
{render()}
</div>
{/* <div className="flex justify-center">
<a href="/cart/availabilities/new" className="btn">
New availability
</a>
</div> */}
</div></ProtectedRoute>
</Layout>
}
import { getSession } from "next-auth/react";
import { serverSideAuth } from '../../../components/protectedRoute'; // Adjust the path as needed
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
const auth = await serverSideAuth({
req: context.req,
allowedRoles: [/* ...allowed roles... */]
});
// const prisma = new PrismaClient()
//get current user role from session
const session = await getSession(context);
if (!session) { return { props: {} } }
const role = session?.user.role;
console.log("server role: " + role);
var queryUrl = process.env.NEXTAUTH_URL + "/api/data/availabilities?select=id,name,isactive,dayofweek,dayOfMonth,startTime,endTime,publisher.firstName,publisher.lastName,publisher.id";
if (role === UserRole.USER || context.query.my) {
queryUrl += `&where={"publisherId":"${session?.user.id}"}`;
} else if (role == UserRole.ADMIN) {
if (context.query.id) {
queryUrl += `&where={"publisherId":"${context.query.id}"}`;
} else {
queryUrl += `&where={"isactive":true}`;
}
}
var resp = await axios.get(
queryUrl
// process.env.NEXTAUTH_URL + "/api/data/availabilities?include=publisher",
, { decompress: true });
var items = resp.data;
console.log("got " + items.length + " availabilities");
return {
props: {
initialItems: items,
id: context.query.id || session?.user.id || null,
},
};
};

View File

@ -0,0 +1,42 @@
//next.js page to show all locatons in the database with a link to the location page
import { Availability } from "@prisma/client";
import AvailabilityForm from "../../../components/availability/AvailabilityForm";
import Layout from "../../../components/layout";
import axiosServer from '../../../src/axiosServer';
export default function NewPage(item: Availability) {
return (
<Layout>
<div className="h-5/6 grid place-items-center">
<AvailabilityForm id={item.id} publisherId={item.publisherId} />
</div>
</Layout>
);
}
//------------------pages\cart\availabilities\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
if (!context.query || !context.query.id) {
return {
props: {}
};
}
const { data: item } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/availabilities/" + context.params.id
);
return {
props: {
item: item,
},
};
};

View File

@ -0,0 +1,908 @@
import React, { useState, useEffect, use } from 'react';
import { useSession } from "next-auth/react"
import Link from 'next/link';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
import axiosInstance from '../../../src/axiosSecure';
import Layout from "../../../components/layout"
import Shift from '../../../components/calendar/ShiftComponent';
import { DayOfWeek, UserRole } from '@prisma/client';
import { env } from 'process'
import ShiftComponent from '../../../components/calendar/ShiftComponent';
//import { set } from 'date-fns';
const common = require('src/helpers/common');
import { toast } from 'react-toastify';
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
// import { useSession,} from 'next-auth/react';
// import { getToken } from "next-auth/jwt"
//define Shift type
interface Shift {
id: number;
startTime: Date;
endTime: Date;
cartEventId: number;
assignments: Assignment[];
}
interface Assignment {
id: number;
publisherId: number;
shiftId: number;
isConfirmed: boolean;
publisher: Publisher;
}
interface Publisher {
id: number;
firstName: string;
lastName: string;
isImported: boolean;
}
// https://www.npmjs.com/package/react-calendar
export default function CalendarPage({ initialEvents, initialShifts }) {
const { data: session } = useSession()
//if logged in, get the user's email
// var email = "";
// const [events, setEvents] = useState(initialEvents);
const events = initialEvents;
const [allShifts, setAllShifts] = useState(initialShifts);
const [value, onChange] = useState<Date>(new Date());
const [shifts, setShifts] = React.useState([]);
const [error, setError] = React.useState(null);
const [availablePubs, setAvailablePubs] = React.useState([]);
const [selectedShiftId, setSelectedShiftId] = useState(null);
const [isOperationInProgress, setIsOperationInProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [activeButton, setActiveButton] = useState(null);
const isLoading = (buttonId) => activeButton === buttonId;
// ------------------ MODAL ------------------
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalPub, setModalPub] = useState(null);
// ------------------ no assignments checkbox ------------------
const [isCheckboxChecked, setIsCheckboxChecked] = useState(false);
const handleCheckboxChange = (event) => {
setIsCheckboxChecked(!isCheckboxChecked); // Toggle the checkbox state
};
useEffect(() => {
console.log("checkbox checked: " + isCheckboxChecked);
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
}, [isCheckboxChecked]); // Dependency array
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
useEffect(() => {
const newMonth = value.getMonth();
if (newMonth !== selectedMonth) {
setSelectedMonth(newMonth);
}
}, [value, selectedMonth]);
const handleCalDateChange = async (selectedDate) => {
var date = new Date(common.getDateFromDateTime(selectedDate));//ToDo: check if seting the timezone affects the selectedDate?!
var dateStr = common.getISODateOnly(date);
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
if (isCheckboxChecked) {
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
setAvailablePubs(availablePubsForDate);
}
else {
console.log(`getting shifts for ${common.getISODateOnly(date)}`)
try {
const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`);
setShifts(shiftsForDate);
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
//remove availabilities that are isFromPreviousAssignment or from previous month for each publisher
// availablePubsForDate = availablePubsForDate.map(pub => {
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
// return pub;
// });
//commented for now: remove unavailable publishers
// availablePubsForDate = availablePubsForDate.map(pub => {
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
// return pub;
// });
setAvailablePubs(availablePubsForDate);
console.log(`found shifts for ${dateStr}: ${shiftsForDate.length}`);
} catch (err) {
console.error("Error fetching shifts:", err);
setError(err);
}
onChange(selectedDate);
}
}
const handleShiftSelection = (selectedShift) => {
setSelectedShiftId(selectedShift.id);
const updatedPubs = availablePubs.map(pub => {
const isAvailableForShift = pub.availabilities.some(avail =>
avail.startTime <= selectedShift.startTime
&& avail.endTime >= selectedShift.endTime
&& avail.isFromPreviousAssignment == false
);
const isAvailableForShiftWithPrevious = pub.availabilities.some(avail =>
avail.startTime <= selectedShift.startTime
&& avail.endTime >= selectedShift.endTime
);
console.log(`Publisher ${pub.firstName} ${pub.lastName} is available for shift ${selectedShift.id}: ${isAvailableForShift}`);
// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + pub.availabilities.map(avail => avail.startTime + " - " + avail.endTime));
// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + stringify.join(', 'pub.availabilities.map(avail => avail.id)));
const availabilitiesIds = pub.availabilities.map(avail => avail.id).join(', ');
console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities with IDs: ${availabilitiesIds}`);
return { ...pub, isAvailableForShift, isAvailableForShiftWithPrevious, isSelected: pub.id === selectedShift.selectedPublisher?.id };
});
// Sort publishers based on their availability state. use currentDayAssignments, currentWeekAssignments,
// currentMonthAssignments and previousMonthAssignments properties
// Sort publishers based on availability and then by assignment counts.
const sortedPubs = updatedPubs.sort((a, b) => {
if (a.isactive !== b.isactive) {
return a.isactive ? -1 : 1;
}
// First, sort by isselected.
if (a.isSelected !== b.isSelected) {
return a.isSelected ? -1 : 1;
}
// Them, sort by availability.
if (a.isAvailableForShift !== b.isAvailableForShift) {
return a.isAvailableForShift ? -1 : 1;
}
// If both are available (or unavailable) for the shift, continue with the additional sorting logic.
// Prioritize those without currentDayAssignments.
if (!!a.currentDayAssignments !== !!b.currentDayAssignments) {
return a.currentDayAssignments ? 1 : -1;
}
// Then prioritize those without currentWeekAssignments.
if (!!a.currentWeekAssignments !== !!b.currentWeekAssignments) {
return a.currentWeekAssignments ? 1 : -1;
}
// Prioritize those with fewer currentMonthAvailabilityHoursCount.
if (a.currentMonthAvailabilityHoursCount !== b.currentMonthAvailabilityHoursCount) {
return a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount;
}
// Finally, sort by (currentMonthAssignments - previousMonthAssignments).
return (a.currentMonthAssignments - a.previousMonthAssignments) - (b.currentMonthAssignments - b.previousMonthAssignments);
});
setAvailablePubs(sortedPubs); // Assuming availablePubs is a state managed by useState
};
const handleSelectedPublisher = (publisher) => {
// Do something with the selected publisher
console.log("handle pub clicked:", publisher);
}
const handlePublisherModalOpen = async (publisher) => {
// Do something with the selected publisher
console.log("handle pub modal opened:", publisher.firstName + " " + publisher.lastName);
let date = new Date(value);
const { data: publisherInfo } = await axiosInstance.get(`/api/?action=getPublisherInfo&id=${publisher.id}&date=${common.getISODateOnly(date)}`);
publisher.assignments = publisherInfo.assignments;
publisher.availabilities = publisherInfo.availabilities;
publisher.email = publisherInfo.email;
setModalPub(publisher);
setIsModalOpen(true);
}
// file uploads
const [fileActionUrl, setFileActionUrl] = useState('');
const [file, setFile] = useState(null);
const handleFileUpload = async (event) => {
setIsOperationInProgress(true);
console.log('handleFileUpload(): Selected file:', event.target.files[0], 'actionUrl:', fileActionUrl);
setFile(event.target.files[0]);
if (!event.target.files[0]) {
toast.error('Моля, изберете файл!');
return;
}
uploadToServer(fileActionUrl, event.target.files[0]);
};
const uploadToServer = async (actionUrl, file) => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/' + actionUrl, {
method: 'POST',
body: formData,
});
const result = await response.json();
if (result.fileId) {
pollProgress(result.fileId);
}
console.log('Result from server-side API:', result);
toast.info(result.message || "Файла е качен! Започна обработката на данните...");
} catch (error) {
toast.error(error.message || "Възникна грешка при обработката на данните.");
} finally {
}
};
const pollProgress = (fileId: any) => {
fetch(`/api/upload?fileId=${fileId}`)
.then(response => response.json())
.then(data => {
updateProgressBar(data.progress); // Update the progress bar
if (data.progress < 98 && data.progress > 0) {
// Poll every second if progress is between 0 and 100
setTimeout(() => pollProgress(fileId), 1000);
} else if (data.progress === 0) {
// Handle error case
toast.error("Възникна грешка при обработката на данните.");
setIsOperationInProgress(false);
} else {
// Handle completion case
toast.success("Файла беше обработен успешно!");
setIsOperationInProgress(false);
}
})
.catch(error => {
console.error('Error polling for progress:', error);
toast.error("Грешка при обновяването на напредъка");
setIsOperationInProgress(false)
})
.finally();
};
const updateProgressBar = (progress: string) => {
// Implement the logic to update your progress bar based on the 'progress' value
// For example, updating the width of a progress bar element
const progressBar = document.getElementById('progress-bar');
if (progressBar) {
progressBar.style.width = progress + '%';
}
};
function getEventClassname(event, allShifts, date) {
if (event && allShifts) {
const matchingShifts = allShifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return shift.cartEventId === event.id && shiftDate.getDate() === date.getDate() && shiftDate.getMonth() === date.getMonth();
});
//get matching shifts with assignments using nextcrud
//const { data: withAss } = await axiosInstance.get(`/shifts?include=assignments&where={"id":{"$in":[${matchingShifts.map(shift => shift.id)}]}}`);
const minCount = Math.min(...matchingShifts.map(shift => shift.assignedCount)) || 0;
//const minCount = 4;
//console.log("matchingShifts: " + matchingShifts) + " for date " + date;
if (matchingShifts.length < 3) { return "text-gray"; }
else {
if (minCount === 0) return "text-red-700 font-bold ";
if (minCount === 1) return "text-brown-900 font-bold ";
if (minCount === 2) return "text-orange-500";
if (minCount === 3) return "text-yellow-500";
if (minCount >= 4) return "text-blue-500";
}
}
return "text-default"; // A default color in case none of the conditions are met.
}
const onTileContent = ({ date, view }) => {
// Add your logic here
var dayName = common.DaysOfWeekArray[date.getDayEuropean()];
var classname = "";
if (events == null) {
return <div>{" "}</div>;
}
const event = events.find((event) => {
return event.dayofweek == dayName;
});
if (event != null) {
const classname = getEventClassname(event, allShifts, date);
return <div className={classname}>
{new Date(event.startTime).getHours() + "-" + new Date(event.endTime).getHours()}ч.
</div>
}
return <div>{" "}</div>;
};
const addAssignment = async (publisher, shiftId) => {
try {
console.log(`new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`);
const newAssignment = {
publisher: { connect: { id: publisher.id } },
shift: { connect: { id: shiftId } },
isactive: true,
isConfirmed: true
};
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
// Update the 'publisher' property of the returned data with the full publisher object
data.publisher = publisher;
} catch (error) {
console.error("Error adding assignment:", error);
}
};
const removeAssignment = async (publisher, shiftId) => {
try {
const assignment = publisher.assignments.find(ass => ass.shift.id === shiftId);
console.log(`remove assignment for shift ${shiftId}`);
const { data } = await axiosInstance.delete(`/api/data/assignments/${assignment.id}`);
} catch (error) {
console.error("Error removing assignment:", error);
}
}
// ----------------------------------------------------------
// button handlers
// ----------------------------------------------------------
const importShifts = async () => {
try {
setActiveButton("importShifts");
setIsOperationInProgress(true);
let date = new Date(value);
date.setDate(date.getDate() + 1);
const dateString = common.getISODateOnly(date);
const fileInput = document.getElementById('fileInput');
// setFileActionUrl(`readword/${dateString.slice(0, 4)}/${dateString.slice(5, 7)}/${dateString.slice(8, 10)}?action=import`);
setFileActionUrl(`api/upload?action=readword&date=${dateString}`);
console.log('fileaction set to ' + fileActionUrl);
fileInput.click();
//handleFileUpload({ target: { files: [file] } });
fileInput.value = null;
} catch (error) {
toast.error(error);
} finally {
setIsOperationInProgress(false);
setActiveButton(null);
}
}
const fetchShifts = async () => {
try {
setActiveButton("fetchShifts");
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/api/data/shifts?include=assignments.publisher&where={"startTime":{"$and":[{"$gte":"2022-12-04T15:09:47.768Z","$lt":"2022-12-10T15:09:47.768Z"}]}}`);
setShifts(data);
toast.success('Готово!', { autoClose: 1000 });
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const generateShifts = async (buttonId, copyFromPrevious = false, autoFill = false, forDay?: Boolean | null) => {
try {
setActiveButton(buttonId);
const endpoint = `/api/shiftgenerate?action=generate&date=${common.getISODateOnly(value)}&copyFromPreviousMonth=${copyFromPrevious}&autoFill=${autoFill}&forDay=${forDay}`;
const { shifts } = await axiosInstance.get(endpoint);
toast.success('Готово!', { autoClose: 1000 });
setIsMenuOpen(false);
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const deleteShifts = async (buttonId, forDay: Boolean) => {
try {
setActiveButton(buttonId);
await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}&forDay=${forDay}`);
toast.success('Готово!', { autoClose: 1000 });
setIsMenuOpen(false);
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const sendMails = async () => {
try {
var month = new Date(value).getMonth() + 1;
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/sendmails/${new Date(value).getFullYear()}/${month}`);
} catch (error) {
console.log(error);
}
}
const generateXLS = async () => {
try {
var month = new Date(value).getMonth() + 1;
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/generatexcel/${new Date(value).getFullYear()}/${month}/2`);
} catch (error) {
console.log(error);
}
}
const generateDOCX = async () => {
try {
setActiveButton("generateDOCX");
var month = new Date(value).getMonth() + 1;
const response = await axiosInstance.get(`/getDocxFile/${new Date(value).getFullYear()}/${month}`, { responseType: 'blob' });
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `График 2023.${month}.docx`);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.log(error);
}
}
//get all publishers and create txt file with their names, current and previous month assignments count (getPublisherInfo)
//
const generateMonthlyStatistics = async () => {
try {
var month = new Date(value).getMonth() + 1;
let { data: allPublishersInfo } = await axiosInstance.get(`/api/?action=getMonthlyStatistics&date=${common.getISODateOnly(value)}`);
//order by name and generate the list
allPublishersInfo = allPublishersInfo.sort((a, b) => {
if (a.firstName !== b.firstName) {
return a.firstName < b.firstName ? -1 : 1;
} if (a.lastName !== b.lastName) {
return a.lastName < b.lastName ? -1 : 1;
}
return 0;
});
var list = "";
allPublishersInfo.forEach(pub => {
// list += `${pub.firstName} ${pub.lastName}\t ${pub.currentMonthAssignments} / ${pub.previousMonthAssignments}\n`;
list += `${pub.firstName} ${pub.lastName}\t ${pub.currentMonthAssignments}\n`;
});
//write to google sheets file
//download the file
const url = window.URL.createObjectURL(new Blob([list]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `Статистика 2023.${month}.txt`);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.log(error);
}
}
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false);
return (
<>
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
{/* Page Overlay */}
{isOperationInProgress && (
<div className="loading-overlay">
<div className="spinner"></div>
</div>
)}
<input id="fileInput" title="file input" type="file" onChange={handleFileUpload}
accept=".json, .doc, .docx, .xls, .xlsx" style={{ display: 'none' }}
/>
<div className="mb-4">
<button className="button m-2 bg-blue-800" onClick={importShifts}>
{isLoading('importShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fa fa-file-import"></i>)} Импорт от Word
</button>
<button className="button btn m-2 bg-blue-800" onClick={generateDOCX}>
{isLoading('generateDOCX') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fa fa-file-export"></i>)}Експорт в Word
</button>
<button className="button btn m-2 bg-yellow-500 hover:bg-yellow-600 text-white" onClick={() => { setActiveButton("sendEmails"); setConfirmModalOpen(true) }}>
{isLoading('sendEmails') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-envelope mr-2"></i>)} изпрати мейли!
</button>
<ConfirmationModal
isOpen={isConfirmModalOpen}
onClose={() => setConfirmModalOpen(false)}
onConfirm={() => {
toast.info("Вие потвърдихте!", { autoClose: 2000 });
setConfirmModalOpen(false);
sendMails()
}}
message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
/>
<div className="relative inline-block text-left">
<button
className={`button m-2 ${isMenuOpen ? 'bg-gray-400 border border-blue-500' : 'bg-gray-300'} hover:bg-gray-400`}
onClick={() => { setIsMenuOpen(!isMenuOpen) }}>
<i className="fa fa-ellipsis-h"></i> Още
</button>
{isMenuOpen && (
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
{/* Group 1: Daily actions */}
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmptyDay", false, false, true)}>
{isLoading('genEmptyDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
създай празни ({value.getDate()}-ти) </button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => generateShifts("genDay", false, true, true)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени ({value.getDate()}-ти) </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" onClick={() => { deleteShifts("deleteShiftsDay", true) }}>
{isLoading('deleteShiftsDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-trash-alt mr-2"></i>)}
изтрий смените ({value.getDate()}-ти)</button>
<hr className="my-1" />
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmpty", false, false)}>
{isLoading('genEmpty') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
създай празни </button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap" onClick={() => generateShifts("genCopy", true)}>
{isLoading('genCopy') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-copy mr-2"></i>)}
копирай от миналия месец</button>
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" onClick={() => { deleteShifts("deleteShifts", false) }}>
{isLoading('deleteShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-trash-alt mr-2"></i>)}
изтрий смените</button>
<hr className="my-1" />
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateXLS}><i className="fas fa-file-excel mr-2"></i> Генерирай XLSX</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={fetchShifts}>
{isLoading('fetchShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-sync-alt mr-2"></i>)} презареди</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button>
</div>
</div>
)}
{/* <button className={`button m-2 bg-blue-800 ${isOperationInProgress ? 'disabled' : ''}`} onClick={importShifts}>
{isOperationInProgress ? <div className="spinner"></div> : 'Import shifts (and missing Publishers) from WORD'}
</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts()}>Generate empty shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true)}>Copy last month shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true, true)}>Generate Auto shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(false, true, value)}>Generate Auto shifts DAY</button>
// <button className="button m-2" onClick={fetchShifts}>Fetch shifts</button>
// <button className="button m-2" onClick={sendMails}>Send mails</button>
// <button className="button m-2" onClick={generateXLS}>Generate XLSX</button>
// <button className="button m-2" onClick={async () => {
// await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}`);
// }
// }>Delete shifts (selected date's month)</button>
// <button className="button m-2" onClick={generateMonthlyStatistics}>Generate statistics</button>
*/}
</div>
</div>
{/* progress bar holder */}
{isOperationInProgress && (
<div id="progress" className="w-full h-2 bg-gray-300">
<div id="progress-bar" className="h-full bg-green-500" style={{ width: `${progress}%` }}></div>
</div>
)}
<div className="flex">
{/* Calendar section */}
<div className="flex-3">
<Calendar
className={['customCalendar']}
onChange={handleCalDateChange}
value={value}
tileContent={onTileContent}
locale="bg-BG"
/>
{/* ------------------------------- PUBLISHERS LIST ----------------------------------
list of publishers for the selected date with availabilities
------------------AVAILABLE PUBLISHERS LIST FOR THE SELECTED DATE0 ------------------ */}
<div className="flex flex-col items-center my-8 sticky top-0">
<h2 className="text-lg font-semibold mb-4">Достъпни за този ден: <span className="text-blue-600">{availablePubs.length}</span></h2>
<label className="toggle pb-3">
<input type="checkbox" className="toggle-checkbox" onChange={handleCheckboxChange} />
<span className="toggle-slider m-1">без назначения за месеца</span>
</label>
<ul className="w-full max-w-md">
{Array.isArray(availablePubs) && availablePubs?.map((pub, index) => {
// Determine background and border classes based on conditions
let bgAndBorderColorClass;
if (pub.isAvailableForShift) {
if (pub.currentDayAssignments === 0) {
const comparisonResultClass = pub.currentMonthAvailabilityDaysCount < pub.previousMonthAssignments ? 'bg-green-100' : 'bg-green-50';
bgAndBorderColorClass = `${comparisonResultClass} border-l-4 border-green-400`;
} else if (!pub.isSelected) {
bgAndBorderColorClass = 'bg-orange-50 border-l-4 border-orange-400';
}
} else {
if (pub.isAvailableForShiftWithPrevious) // add left orange border
{
bgAndBorderColorClass = 'border-l-4 border-orange-400';
}
else {
bgAndBorderColorClass = 'bg-white';
}
}
//tOdO: CHECK WHY THIS IS NOT WORKING
if (!pub.hasEverFilledForm) {
//bgAndBorderColorClass = 'border-t-2 border-yellow-400';
}
// Determine border class if selected
const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : '';
// Determine opacity class
const activeOpacityClass = pub.isactive ? '' : 'opacity-25';
return (
<li key={index}
className={`flex justify-between items-center p-4 rounded-lg shadow-sm mb-2
${bgAndBorderColorClass} ${selectedBorderClass} ${activeOpacityClass}`}
onDoubleClick={(handlePublisherModalOpen.bind(this, pub))}
>
<span className={`text-gray-700 ${pub.isAvailableForShift ? 'font-bold' : 'font-medium'} `}>
{pub.firstName} {pub.lastName}
</span>
<div className="flex space-x-1 overflow-hidden">
<span title="Достъпност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
</span>
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
<span tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
<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>
</div>
</li>
);
})}
</ul>
</div>
</div>
{/* Shift list section */}
<div className="flex-grow mx-5">
<div className="flex-col" id="shiftlist">
{shifts.map((shift, index) => (
<ShiftComponent key={index} shift={shift}
onShiftSelect={handleShiftSelection} isSelected={shift.id == selectedShiftId}
onPublisherSelect={handleSelectedPublisher} showAllAuto={true}
allPublishersInfo={availablePubs} />
))}
</div>
</div>
</div>
<div>
{/* <CustomCalendar date={value} shifts={shifts} /> */}
</div>
{isModalOpen && <PublisherShiftsModal publisher={modalPub} shifts={allShifts} onClose={() => setIsModalOpen(false)} />}
</ProtectedRoute >
</Layout >
</>
);
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);
};
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-4 border-yellow-500' : ''} ${assignmentExists ? 'bg-blue-200' : shift.color} h-24 flex flex-col justify-center`}
>
{common.getTimeRange(shift.startTime, shift.endTime)}
{!assignmentExists && shift.isAvailable && (
<button onClick={() => { addAssignment(publisher, shift.id); onClose() }}
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 { start } from 'repl';
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
const baseUrl = common.getBaseUrl();
console.log('runtime BaseUrl: ' + baseUrl);
console.log('runtime NEXTAUTH_URL: ' + process.env.NEXTAUTH_URL);
console.log('Runtime Axios Base URL:', axios.defaults.baseURL);
const currentDate = new Date();
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() - 3, 1);
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); // 0th day of the next month gives the last day of the current month
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
const prismaClient = common.getPrismaClient();
// let events = await prismaClient.cartEvent.findMany({ where: { isactive: true } });
// events = events.map(event => ({
// ...event,
// // Convert Date objects to ISO strings
// startTime: event.startTime.toISOString(),
// endTime: event.endTime.toISOString(),
// }));
const { data: events } = await axios.get(`/api/data/cartevents?where={"isactive":true}`);
//const { data: shifts } = await axios.get(url);
// get all shifts for the month, including assigments
let shifts = await prismaClient.shift.findMany({
where: {
isactive: true,
startTime: {
gte: firstDayOfMonth,
//lt: lastDayOfMonth
}
},
include: {
assignments: {
include: {
publisher: {
select: {
id: true,
}
}
}
}
}
});
//calculate assCount for each shift
shifts = shifts.map(shift => ({
...shift,
assignedCount: shift.assignments.length,
startTime: shift.startTime.toISOString(),
endTime: shift.endTime.toISOString(),
}));
return {
props: {
initialEvents: events,
initialShifts: shifts,
},
};
}

View File

@ -0,0 +1,47 @@
import React, { useState, useEffect, use } from 'react';
import { useSession } from "next-auth/react"
import Link from 'next/link';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
import axiosInstance from '../../../src/axiosSecure';
import Layout from "../../../components/layout"
import Shift from '../../../components/calendar/ShiftComponent';
import { DayOfWeek, UserRole } from '@prisma/client';
import { env } from 'process'
import ShiftComponent from '../../../components/calendar/ShiftComponent';
//import { set } from 'date-fns';
const common = require('src/helpers/common');
import { toast } from 'react-toastify';
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
const SchedulePage = () => {
const { data: session } = useSession();
const [htmlContent, setHtmlContent] = useState(""); // State to hold fetched HTML content
useEffect(() => {
// Define an async function to fetch the HTML content
const fetchHtmlContent = async () => {
try {
// Replace '/api/schedule' with your actual API endpoint
const response = await axiosInstance.get('/api/schedule?year=2024&month=1', { responseType: 'text' });
setHtmlContent(response.data); // Set the fetched HTML content in state
} catch (error) {
console.error("Failed to fetch schedule:", error);
// Handle error (e.g., display an error message)
}
};
fetchHtmlContent(); // Call the function to fetch HTML content
}, []); // Empty dependency array means this effect runs once on component mount
return (
<Layout>
<ProtectedRoute deniedMessage="">
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
</ProtectedRoute>
</Layout>
);
};
export default SchedulePage;

View File

@ -0,0 +1,25 @@
import NewPage from "../new";
import axiosServer from '../../../../src/axiosServer';
export default NewPage;
export const getServerSideProps = async (context) => {
console.log("edit page getServerSideProps");
const axios = await axiosServer(context);
const { id } = context.query;
const { data } = await axios.get(`${process.env.NEXTAUTH_URL}/api/data/cartevents/` + id);
const locations = await axios
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`)
.then((res) => {
console.log("locations: " + JSON.stringify(res.data));
return res.data;
});
return {
props: {
item: data,
locations: locations,
inline: false,
},
};
};

View File

@ -0,0 +1,119 @@
import { CartEvent, UserRole } from '@prisma/client';
import { useRouter } from "next/router";
import { useState } from "react";
import Layout from "../../../components/layout";
import common from 'src/helpers/common';
import ProtectedRoute from '../../../components/protectedRoute';
import CartEventForm from '../../../components/cartevent/CartEventForm';
// import IProps from '../../../components/cartevent/CartEventForm'
import axiosServer from '../../../src/axiosServer';
import { getServerSession } from "next-auth/next"
import { authOptions } from "../../../pages/api/auth/[...nextauth]"
// export default CartEventForm;
export interface ICartEventPageProps {
items: [CartEvent];
locations: [Location];
inline: false;
}
export default function CartEventPage({ items, locations }: ICartEventPageProps) {
const router = useRouter();
const [addnew, setAddNew] = useState(false);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="flex flex-col">
<h1>All cart events</h1>
<table className="min-w-full">
<thead className="border-b">
<tr>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
#
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Day of Week
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Time
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Shift Duration
</th>
<th scope="col" className="text-sm font-medium text-gray-900 px-6 py-4 text-left">
Active
</th>
</tr>
</thead>
<tbody>
{items.map((item: CartEvent, i) => (
<tr key={i} className="border-b">
<td className="px-6 py-4 whitespace-nowrap">
{item.id}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<strong>{common.dayOfWeekNames[common.getDayOfWeekIndex(item.dayofweek)]} </strong>
на {locations.find(l => l.id == item.locationId).name}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{new Date(item.startTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
+ " до " + new Date(item.endTime).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.shiftDuration}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{item.isactive ? "Yes" : "No"}
</td>
<td>
<button className="button bg-blue-500 hover:bg-blue-700"
onClick={() => router.push(`/cart/cartevents/edit/${item.id}`)}
>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
<button className="button bg-blue-500 hover:bg-blue-700"
onClick={() => setAddNew(!addnew)}
> {addnew ? "обратно" : "Добави нов"}</button>
{addnew && <CartEventForm locations={locations} />}
</div>
</ProtectedRoute>
</Layout>
)
}
export const getServerSideProps = async (context) => {
const session = await getServerSession(context.req, context.res, authOptions)
context.req.session = session;
const axios = await axiosServer(context);
const { data: items } = await axios.get("/api/data/cartevents");
console.log("gettnng locations from: " + "/api/data/locations?select=id,name");
const locations = await axios
.get(`/api/data/locations?select=id,name`)
.then((res) => {
console.log("locations: " + JSON.stringify(res.data));
return res.data;
});
return {
props: {
items,
locations: locations,
inline: false,
},
};
};

View File

@ -0,0 +1,55 @@
//next.js page to show all locatons in the database with a link to the location page
import CartEventForm from "../../../components/cartevent/CartEventForm";
import Layout from "../../../components/layout";
import axiosServer from '../../../src/axiosServer';
import { ICartEventPageProps } from "./index";
export default function NewPage(props: ICartEventPageProps) {
return (
<Layout>
<div className="h-5/6 grid place-items-center">
<CartEventForm props={props} />
{/*
<AvailabilityForm id={item.id} publisherId={item.publisherId} /> */}
</div>
</Layout>
);
}
//------------------pages\cart\availabilities\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
const axios = await axiosServer(context);
const locations = await axios
.get(`${process.env.NEXTAUTH_URL}/api/data/locations?select=id,name`)
.then((res) => {
console.log("locations: " + JSON.stringify(res.data));
return res.data;
});
if (!context.query || !context.query.id) {
return {
props: {}
};
}
const { id } = context.query.id;
const { data: item } = await axiosInstance.get(
process.env.NEXTAUTH_URL + "/api/data/cartevents/" + context.params.id
);
return {
props: {
item: item,
locations: locations
},
};
};

View File

@ -0,0 +1,122 @@
import React, { useState, useEffect } from 'react';
import Layout from "../../../components/layout";
import { Carousel } from 'react-responsive-carousel';
import "react-responsive-carousel/lib/styles/carousel.min.css"; // requires a loader
import { GetServerSideProps } from 'next';
import { Location, UserRole } from "@prisma/client";
import axiosServer from '../../../src/axiosServer';
const ViewLocationPage: React.FC<ViewLocationPageProps> = ({ location }) => {
const [activeTab, setActiveTab] = useState('mainLocation');
const [activeImage, setActiveImage] = useState(0);
const [images, setImages] = useState([]);
const [mainLocationImageCount, setMainLocationImageCount] = useState(0);
useEffect(() => {
const mainLocationImages = [location.picture1, location.picture2, location.picture3].filter(Boolean);
const backupLocationImages = location.backupLocationImages?.filter(Boolean) ?? [];
setImages([...mainLocationImages, ...backupLocationImages]);
setMainLocationImageCount(mainLocationImages.length);
}, [location.picture1, location.picture2, location.picture3, location.backupLocationImages]);
const handleTabChange = (tab: string) => {
setActiveTab(tab);
//show the proper image in the carousel
if (tab === 'backupLocation') {
setActiveImage(mainLocationImageCount);
} else {
setActiveImage(0);
}
};
const handleCarouselChange = (index) => {
// Switch to backupLocation tab if the current carousel image index is from the backup location
if (index >= mainLocationImageCount) {
setActiveTab('backupLocation');
} else {
setActiveTab('mainLocation');
}
setActiveImage(index);
};
return (
<Layout>
<div className="view-location-page max-w-4xl mx-auto my-8">
{/* Tabs */}
<div className="tabs flex border-b">
{/* Main Location Tab */}
<button
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'mainLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
onClick={() => handleTabChange('mainLocation')}
>
{location.name}
</button>
{/* Backup Location Tab */}
<button
className={`tab flex-1 text-lg py-2 px-4 ${activeTab === 'backupLocation' ? 'border-b-4 border-blue-500 text-blue-600 font-semibold' : 'text-gray-600 hover:text-blue-500'}`}
onClick={() => handleTabChange('backupLocation')}
>
При лошо време: <strong>{location.backupLocationName}</strong>
</button>
</div>
{/* Carousel */}
{images.length > 0 && (
<Carousel showArrows={true}
autoPlay={false}
infiniteLoop={true}
showThumbs={false}
onChange={handleCarouselChange}
selectedItem={activeImage}
>
{images.map((src, index) => (
<div key={index}>
<img src={src} alt={`Slide ${index + 1}`} />
</div>
))}
</Carousel>
)}
{/* Tab Content */}
{(location.content || location.backupLocationContent) && (
<div className="tab-content mt-4">
{activeTab === 'mainLocation' && (
<div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.content }} />
)}
{activeTab === 'backupLocation' && location.backupLocationContent && (
<div className="p-4 bg-white shadow rounded-lg" dangerouslySetInnerHTML={{ __html: location.backupLocationContent }} />
)}
</div>
)}
</div>
</Layout>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const axios = await axiosServer(context);
const { data: location } = await axios.get(
`${process.env.NEXTAUTH_URL}/api/data/locations/${context.params.id}`
);
if (location.backupLocationId !== null) {
const { data: backupLocation } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/locations/" + location.backupLocationId
);
location.backupLocationName = backupLocation.name;
location.backupLocationContent = backupLocation ? backupLocation.content : "";
location.backupLocationImages = backupLocation ? [backupLocation.picture1, backupLocation.picture2, backupLocation.picture3].filter(Boolean) : [];
}
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
return {
props: {
location: location,
},
};
};
export default ViewLocationPage;

View File

@ -0,0 +1,41 @@
//next.js page to show all locatons in the database with a link to the location page
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../../components/layout";
import LocationForm from "../../../../components/location/LocationForm";
import axiosServer from '../../../../src/axiosServer';
import ProtectedRoute from '../../../../components/protectedRoute';
function NewPage(item: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-center">
<LocationForm key={item.id} item={item} />
</div>
</ProtectedRoute>
</Layout>
);
}
export default NewPage;
//------------------pages\cart\locations\edit\[id].tsx------------------//
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
if (context.query.id === "new" || context.query.id === 0) {
return {
props: {}
};
}
const { data: item } = await axios.get(
process.env.NEXTAUTH_URL + "/api/data/locations/" + context.params.id
);
console.log(item) //this is the location object
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
return {
props: {
item: item,
},
};
};

View File

@ -0,0 +1,51 @@
//next.js page to show all locatons in the database with a link to the location page
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import LocationCard from "../../../components/location/LocationCard";
import axiosServer from '../../../src/axiosServer';
import ProtectedRoute from '../../../components/protectedRoute';
interface IProps {
item: Location;
}
function LocationsPage({ items = [] }: IProps) {
const renderLocations = () => {
if (!Array.isArray(items) || items.length === 0) return <h1>No Locations</h1>;
return items.map((item) => (
<LocationCard key={item.id} location={item} />
));
};
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="grid gap-4 grid-cols-1 md:grid-cols-4">
{renderLocations()}
</div>
{/* add location link */}
<div className="flex justify-center">
<a href="/cart/locations/new" className="btn">
Add Location
</a>
</div>
</ProtectedRoute>
</Layout>
);
}
export default LocationsPage;
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
const { data: items } = await axios.get("/api/data/locations");
//console.log('get server props - locations:' + items.length);
//console.log(items);
return {
props: {
items,
},
};
};

View File

@ -0,0 +1,44 @@
//next.js page to show all locatons in the database with a link to the location page
// import axios from "axios";
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import LocationForm from "../../../components/location/LocationForm";
import axiosServer from '../../../src/axiosServer';
import ProtectedRoute from '../../../components/protectedRoute';
function NewPage(loc: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="h-5/6 grid place-items-center">
<LocationForm key={loc.id} location={loc} />
</div></ProtectedRoute>
</Layout>
);
}
export default NewPage;
//------------------pages\cart\locations\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
//if query is undefined, then it is a new location
if (context.query.id === undefined) {
return {
props: {}
};
}
const { data: loc } = await axios.get(
`${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
);
console.log(location) //this is the location object
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
return {
props: {
location: loc,
},
};
};

View File

@ -0,0 +1,87 @@
import { useState } from 'react';
import Layout from "../../../components/layout";
import ProtectedRoute from '../../../components/protectedRoute';
import { UserRole } from '@prisma/client';
import axiosServer from '../../../src/axiosServer';
import common from '../../../src/helpers/common';
function ContactsPage({ publishers }) {
const [searchQuery, setSearchQuery] = useState('');
const filteredPublishers = publishers.filter((publisher) =>
publisher.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
publisher.lastName.toLowerCase().includes(searchQuery.toLowerCase()) ||
publisher.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
publisher.phone?.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER, UserRole.USER]}>
<div className="container mx-auto p-4">
<h1 className="text-xl font-semibold mb-4">Контакти</h1>
<input
type="text"
placeholder="Търси по име, имейл или телефон..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="border border-gray-300 rounded-md px-2 py-2 mb-4 w-full text-base md:text-sm"
/>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr>
<th className="border-b font-medium p-4 pl-8 pt-0 pb-3">Име</th>
<th className="border-b font-medium p-4 pt-0 pb-3">Имейл</th>
<th className="border-b font-medium p-4 pt-0 pb-3">Телефон</th>
</tr>
</thead>
<tbody>
{filteredPublishers.map((publisher) => (
<tr key={publisher.id}>
<td className="border-b p-4 pl-8">{publisher.firstName} {publisher.lastName}</td>
<td className="border-b p-4">
<a href={`mailto:${publisher.email}`} className="text-blue-500">{publisher.email}</a>
</td>
<td className="border-b p-4">
<div className="flex items-center justify-between">
<span className={common.isValidPhoneNumber(publisher.phone) ? '' : 'text-red-500'}>{publisher.phone}</span>
<div className="flex items-center">
<a href={`tel:${publisher.phone}`} className="inline-block p-2 mr-2">
<i className="fas fa-phone-alt text-blue-500 text-xl" title="Обаждане"></i>
</a>
<a href={`https://wa.me/${publisher.phone}`} className="inline-block p-2 mr-2">
<i className="fab fa-whatsapp text-green-500 text-xl" title="WhatsApp"></i>
</a>
{publisher.phone ? (
<a href={`viber://chat/?number=%2B${publisher.phone.startsWith('+') ? publisher.phone.substring(1) : publisher.phone}`} className="inline-block p-2">
<i className="fab fa-viber text-purple-500 text-xl" title="Viber"></i>
</a>
) : null}
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</ProtectedRoute>
</Layout>
);
}
export default ContactsPage;
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,phone');
return {
props: {
publishers,
},
};
};

View File

@ -0,0 +1,105 @@
import axiosServer from '../../../../src/axiosServer';
import NewPubPage from "../new";
export default NewPubPage;
import { Assignment, Shift, UserRole } from "prisma/prisma-client";
// import { monthNamesBG } from "~/src/helpers/const"
import { monthNamesBG } from "src/helpers/const";
function getShiftGroups(shifts: [Shift]) {
const groupedShifts = shifts.reduce((groups, shift) => {
// Extract the year and month from the shift date
const yearMonth = shift.startTime.substring(0, 7)
// Initialize the group for the year-month if it doesn't exist
if (!groups[yearMonth]) {
groups[yearMonth] = []
}
// Add the shift to the group
groups[yearMonth].push(shift)
// Return the updated groups object
return groups
}, {})
// Sort the groups by year-month
const sortedGroups = Object.keys(groupedShifts).sort((a, b) => {
// Compare the year-month strings lexicographically
if (a < b) return -1
if (a > b) return 1
return 0
}).reduce((result, key) => {
// Rebuild the object with the sorted keys
result[key] = groupedShifts[key]
return result
}, {})
return sortedGroups;
}
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
if (!context.query || !context.query.id) {
return {
props: {}
};
}
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,assignments,assignments.shift";
console.log("GET PUBLISHER FROM:" + url)
const { data: item } = await axios.get(url);
// item.allShifts = item.assignments.map((a: Assignment[]) => a.shift);
//group shifts by month, remove duplicates
//sort availabilities by start time
// item.availabilities = item.availabilities
// .sort((a, b) => b.startTime - a.startTime);
item.assignments = item.assignments
.sort((a, b) => b.startTime - a.startTime)
.reduce((acc, assignment: Assignment) => {
const date = new Date(assignment.shift.startTime);
const year = date.getFullYear();
const month = date.getMonth();
const tabId = year + "-" + month;
const tabName = monthNamesBG[month] + " " + year;
const day = date.getDate();
// console.log("shift: year: " + year + " month: " + month + " day: " + day);
if (!acc.items[tabId]) {
acc.items[tabId] = [];
}
if (!acc.items[tabId][day]) {
acc.items[tabId][day] = [];
}
//remove duplicates
if (acc.items[tabId][day].find(s => s.id == assignment.shift.id)) {
return acc;
}
// acc.months = acc.months || [];
if (!acc.months[tabId]) {
acc.months[tabId] = [];
}
if (!acc.keys.includes(tabId)) {
acc.months[tabId] = tabName;
acc.keys.push(tabId);
}
acc.items[tabId][day].push({
start: assignment.shift.startTime,
end: assignment.shift.endTime,
id: assignment.id,
shiftId: assignment.shift.id,
isConfirmed: assignment.isConfirmed ? true : false,
});
return acc;
}, { items: {}, keys: [], months: {} });
// console.log("server item:");
// console.dir(item, { depth: null });
return {
props: {
item: item
},
};
};

View File

@ -0,0 +1,20 @@
// pages/me.jsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
export default function Me() {
const router = useRouter();
const { data: session, status } = useSession();
useEffect(() => {
if (status === 'authenticated') {
router.push(`/cart/publishers/edit/${session.user.id}?self=true`);
} else if (status === 'unauthenticated') {
router.push('/api/auth/signin');
}
}, [status, session, router]);
// You can add a fallback content or loader here if you want
return <div>Redirecting...</div>;
}

View File

@ -0,0 +1,629 @@
import { toast } from 'react-toastify';
import Layout from "../../../components/layout";
import { Publisher, Availability, AvailabilityType, DayOfWeek, UserRole } from "@prisma/client";
import ProtectedRoute from '../../../components/protectedRoute';
import axiosInstance from '../../../src/axiosSecure';
import { useState, useRef } from "react";
import * as XLSX from "xlsx";
// import { Table } from "react-bootstrap";
import { staticGenerationAsyncStorage } from "next/dist/client/components/static-generation-async-storage.external";
import moment from 'moment';
// import { DatePicker } from '@mui/x-date-pickers'; !! CAUSERS ERROR ???
// import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { set } from 'date-fns';
// import * as common from "src/helpers/common";
const common = require('../../../src/helpers/common');
export default function ImportPage() {
const [data, setData] = useState([])
const [rawData, setRawData] = useState([]);
const [status, setStatus] = useState({ status: 'idle', info: '' });
const MODE_PUBLISHERS1 = "PUBLISHERS1";
const MODE_PUBLISHERS2 = "PUBLISHERS2";
type ModeState = {
mainMode: typeof MODE_PUBLISHERS1 | typeof MODE_PUBLISHERS2;
schedule: boolean;
publishers2Import: boolean;
headerRow: number;
}
const [mode, setMode] = useState<ModeState>({
mainMode: MODE_PUBLISHERS1,
schedule: false,
publishers2Import: false,
headerRow: 0
});
const headerRef = useRef({
header: null,
dateIndex: -1,
emailIndex: -1,
nameIndex: -1,
phoneIndex: -1,
isTrainedIndex: -1,
desiredShiftsIndex: -1,
dataStartIndex: -1,
isActiveIndex: -1,
pubTypeIndex: -1
});
const handleFile = (e) => {
const [file] = e.target.files;
const reader = new FileReader();
reader.onload = (evt) => {
const bstr = evt.target.result;
const wb = XLSX.read(bstr, { type: "binary" });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const sheetData = XLSX.utils.sheet_to_json(ws, {
header: 1,
range: 0,
blankrows: false,
defval: '',
});
for (let i = 0; i < 5; i++) {
if (sheetData[i].includes('Имейл')) {
setMode({
mainMode: MODE_PUBLISHERS1,
schedule: false,
publishers2Import: false,
headerRow: i
});
headerRef.current.header = sheetData[i];
headerRef.current.dataStartIndex = i;
common.logger.debug("header at row " + i);
break;
}
// it seems we are processing availability sheet. import only publishers by default, and read only the first 3 columns as publisher data
if (sheetData[i].includes('Email Address')) {
setMode({
mainMode: MODE_PUBLISHERS2,
schedule: true,
publishers2Import: false,
headerRow: i
});
headerRef.current.header = sheetData[i];
headerRef.current.dataStartIndex = i;
break;
}
}
if (!headerRef.current.header) {
console.error("header not found in the first 5 rows!");
return;
}
const header = headerRef.current.header
headerRef.current.dateIndex = header.indexOf('Timestamp');
headerRef.current.emailIndex = header.indexOf('Имейл') !== -1 ? header.indexOf('Имейл') : header.indexOf('Email Address');
headerRef.current.nameIndex = header.indexOf('Име, Фамилия');
headerRef.current.phoneIndex = header.indexOf('Телефон');
headerRef.current.isTrainedIndex = header.indexOf('Обучен');
headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия');
headerRef.current.isActiveIndex = header.indexOf("Неактивен");
headerRef.current.pubTypeIndex = header.indexOf("Назначение");
const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => {
let date;
date = common.excelSerialDateToDate(row[headerRef.current.dateIndex]);
//substract 1 day, because excel serial date is 1 day ahead
date.setDate(date.getDate() - 1);
date = common.getDateFormated(date);
common.logger.debug(date);
return [date, row[headerRef.current.emailIndex], row[headerRef.current.nameIndex]];
});
setRawData(sheetData);
setData(filteredData);
setStatus({ status: 'зареден', info: `Заредени ${filteredData.length} от ${rawData.length} записа` });
};
reader.readAsBinaryString(file);
// Reset the file input value
e.target.value = null;
};
const handleSave = async () => {
try {
common.logger.debug("handleSave to: " + common.getBaseUrl());
const header = rawData[mode.headerRow];
for (let i = mode.headerRow + 1; i < rawData.length; i++) { //fullData.length; each publisher
//update status.info with current publisher
setStatus({ status: 'running', info: `Processing row ${i} of ${rawData.length}` });
//sleep for 300ms to allow the database to process the previous request
await new Promise(r => setTimeout(r, 100));
const row = rawData[i];
var email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = false;
//const date = new Date(row[0]).toISOS{tring().slice(0, 10);
if (mode.mainMode == MODE_PUBLISHERS1) {
email = row[headerRef.current.emailIndex];
phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace
// Remove any non-digit characters, except for the leading +
//phone = phone.replace(/(?!^\+)\D/g, '');
phone = phone.replace(/[^+\d]/g, '');
if (phone.startsWith('8') || phone.startsWith('9')) {
phone = '+359' + phone.substring(1); // Assumes all numbers starting with 8 are Bulgarian and should have +359
} else if (!phone.startsWith('+')) {
phone = '+' + phone; // Add + if it's missing, assuming the number is complete
}
names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/);
dateOfInput = importDate.value || new Date().toISOString();
// not empty == true
isTrained = row[headerRef.current.isTrainedIndex] !== '';
isActive = row[headerRef.current.isActiveIndex] == '';
desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4;
}
else {
dateOfInput = common.excelSerialDateToDate(row[0]);
email = row[1];
names = row[2].normalize('NFC').split(/[, ]+/);
}
//remove special characters from name
// let names = common.removeAccentsAndSpecialCharacters(row[2]).split(/[, ]+/);
let personId = '';
try {
try {
const select = "&select=id,firstName,lastName,phone,isTrained,desiredShiftsPerMonth,isactive,availabilities";
const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`);
let existingPublisher = responseByName.data[0];
if (!existingPublisher) {
// If no match by name, check by email
const responseByEmail = await axiosInstance.get(`/api/?action=findPublisher&email=${email}${select}`);
if (responseByEmail.data.length > 0) {
// Iterate over all matches by email to find one with a matching or similar name
const fullName = names.join(' ').toLowerCase(); // Simplify comparison
existingPublisher = responseByEmail.data.find(publisher => {
const publisherFullName = (publisher.firstName + ' ' + publisher.lastName).toLowerCase();
return fullName === publisherFullName; // Consider expanding this comparison for typos
});
}
}
if (existingPublisher?.id) { // UPDATE
// Create a flag to check if update is needed
const updatedData = {};
personId = existingPublisher?.id;
let updateNeeded = false;
// Check for name update
const fullName = names.join(' ');
const existingFullName = existingPublisher.firstName + ' ' + existingPublisher.lastName;
if (fullName !== existingFullName) {
common.logger.debug(`Existing publisher '${existingFullName}' found for ${email} (ID:${personId})`);
updatedData.firstName = names[0];
updatedData.lastName = names.slice(1).join(' ');
data[i - mode.headerRow][4] = "name updated!";
updateNeeded = true;
} else {
data[i - mode.headerRow][4] = "existing";
}
// Log existing publisher
common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`);
// Check for other updates
const fieldsToUpdate = [
{ key: 'phone', value: phone },
{ key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt },
{ key: 'isTrained', value: isTrained },
{ key: 'isactive', value: isActive }
];
fieldsToUpdate.forEach(({ key, value, parse }) => {
if (!existingPublisher[key] && value !== '' && value !== undefined) {
updatedData[key] = parse ? parse(value) : value;
updateNeeded = true;
}
});
// Update the record if needed and if MODE_PUBLISHERS1 (Import from List of Participants)
if (updateNeeded && (mode.publishers2Import || mode.mainMode == MODE_PUBLISHERS1)) {
try {
await axiosInstance.put(`/api/data/publishers/${personId}`, updatedData);
common.logger.debug(`Updated publisher ${personId}`);
data[i - mode.headerRow][4] = "updated";
} catch (error) {
console.error(`Failed to update publisher ${personId}`, error);
}
}
} else { // CREATE
// If no publisher with the email exists, create one
// const names = email.split('@')[0].split('.');\
//Save new publisher
if (mode.publishers2Import) {
const personResponse = await axiosInstance.post('/api/data/publishers', {
email,
phone,
firstName: names[0],
lastName: names[1],
isactive: isActive,
isTrained,
desiredShiftsPerMonth,
});
personId = personResponse.data.id;
data[i][4] = "new";
}
else
if (mode.mainMode == MODE_PUBLISHERS1) {
const firstname = names.length > 2 ? names.slice(0, -1).join(' ') : names[0];
const personResponse = await axiosInstance.post('/api/data/publishers', {
email,
phone,
firstName: firstname,
lastName: names[names.length - 1],
isactive: isActive,
isTrained,
desiredShiftsPerMonth
});
data[i - mode.headerRow][4] = "new";
} else {
data[i - mode.headerRow][4] = "import disabled";
}
common.logger.debug(`NEW Publisher ${personId} created for email ${email} (${names})`);
}
} catch (error) {
console.error(error);
data[i - mode.headerRow][4] = "error";
}
if (mode.schedule) {
// Parse the availability data from the Excel cell
//get days of the month and add up to the next full week
// Save availability records
const availabilities: Availability[] = [];
for (let j = 3; j < header.length; j++) {
const dayHeader = header[j];
const shifts = row[j];
if (!shifts || shifts === 'Не мога') {
continue;
}
// specific date: Седмица (17-23 април) [Четвъртък ]
// const regex = /^Седмица \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/;
// specific week: Седмица 1 <any character> [Четвъртък]
// const regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/;
//allow optional space before and after the brackets
// match both 'Седмица 3 (20-25 ноември) [пон]' and 'Седмица 4 (27 ноември - 2 декември) [четв]'
//const regex = /^\s*Седмица\s+(\d{1,2})\s+\((\d{1,2})\s+(\S+)(?:\s*-\s*(\d{1,2})\s+(\S+))?\)\s*\[\s*(\S+)\s*\]\s*$/;
//the original, but missing the two month names
let regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
regex = /^Седмица (\d{1,2}) \((\d{1,2})(?:\s*-\s*(\d{1,2}))? (\S+)(?:\s*-\s*(\d{1,2}) (\S+))?\)\s*\[(\S+)\s*\]\s*$/;
//const regex = /^Седмица (\d{1,2}) \((\d{1,2}(\s*\S*?))-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
//both Седмица 1 (6-11 ноември) [пон]
// Седмица 4 (27 ноември-2 декември) [пет]
//const regex = /^Седмица (\d{1,2}) \((\d{1,2} \S+)?-? ?(\d{1,2} \S+)\)\s*\[(\S+)\s*\]\s*$/;
//const regex = /^Седмица (\d{1,2}) \((\d{1,2} \S+)?-? ?(\d{1,2} \S+)\)\s*\[(\S+)\s*\]\s*$/;
// const regex = /^Седмица (\d{1,2}) \* \[(\S+)\]$/;
// const regex = /^Седмица (\d{1,2}) \[(\S+)\]$/;
// replace multiple spaces with single space
const normalizedHeader = dayHeader.replace(/\s+/g, ' ');
var match = normalizedHeader.match(regex);
if (!match) {
common.logger.debug("was not able to parse availability " + shifts + "trying again with different regex");
let regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/;
match = normalizedHeader.match(regex);
}
if (match) {
//ToDo: can't we use date.getDayEuropean() instead of this logic down?
const weekNr = parseInt(match[1]);
const weekStart = match[2];
// const endDate = match[2];
const month = match[4];
const dayOfWeekStr = match[7];
const dayOfWeek = common.getDayOfWeekIndex(dayOfWeekStr);
common.logger.debug("processing availability for week " + weekNr + ": " + weekStart + "." + month + "." + dayOfWeekStr)
// Create a new Date object for the start date of the range
const day = new Date();
day.setDate(1); // Set to the first day of the month to avoid overflow
//day.setMonth(day.getMonth() + 1); // Add one month to the date, because we assume we are p
day.setMonth(common.getMonthIndex(month));
day.setDate(parseInt(weekStart) + dayOfWeek);
day.setHours(0, 0, 0, 0);
common.logger.debug("processing availabilities for " + day.toLocaleDateString()); // Output: Sun Apr 17 2022 14:07:11 GMT+0300 (Eastern European Summer Time)
common.logger.debug("parsing availability input: " + shifts); // Output: 0 (Sunday)
const dayOfWeekName = common.getDayOfWeekNameEnEnum(day);
let dayOfMonth = day.getDate();
const name = `${names[0]} ${names[1]}`;
const intervals = shifts.split(",");
if (!oldAvDeleted && personId) {
if (mode.schedule && email) {
common.logger.debug(`Deleting existing availabilities for publisher ${personId} for date ${day}`);
try {
await axiosInstance.post(`/api/?action=deleteAvailabilityForPublisher&publisherId=${personId}&date=${day}&deleteFromPreviousAssignments=true`);
common.logger.debug(`Deleted all availabilities for publisher ${personId}`);
oldAvDeleted = true;
}
catch (error) {
console.error(`Failed to delete availabilities for publisher ${personId}`, error);
}
}
}
let parsedIntervals: { end: number; }[] = [];
intervals.forEach(interval => {
// Summer format: (12:00-14:00)
//if (!/\(\d{1,2}:\d{2}-\d{1,2}:\d{2}\)/.test(interval)) {
//winter regex:
//\d{1,2}-\d{1,2}:\d{2}/
// Regular expression to match both Summer format (12:00-14:00) and winter format '09-10:30'
const regex = /\d{1,2}(?::\d{2})?-\d{1,2}(?::\d{2})?/;
if (!regex.test(interval)) {
common.logger.debug(`Skipping invalid interval: ${interval}`);
return;
}
// If the interval matches the format, remove any non-essential characters
const cleanedInterval = interval.replace(/[()]/g, "");
// Extract start and end times from interval string
const [start, end] = cleanedInterval.split("-");
// Convert times like "12" to "12:00" for consistent parsing
const formattedStart = start.includes(":") ? start : start + ":00";
const formattedEnd = end.includes(":") ? end : end + ":00";
// Try to parse the times, and skip the interval if it can't be parsed
try {
const parsedStart = Number(formattedStart.split(":").join(""));
const parsedEnd = Number(formattedEnd.split(":").join(""));
// Store parsed interval
parsedIntervals.push({
start: parsedStart,
end: parsedEnd
});
} catch (error) {
common.logger.debug(`Error parsing interval: ${interval}`);
return;
}
});
// Sort intervals by start time
parsedIntervals.sort((a, b) => a.start - b.start);
// Initialize start and end times with the first interval
let minStartTime = parsedIntervals[0].start;
let maxEndTime = parsedIntervals[0].end;
let isOld = false;
// Calculate the total month difference by considering the year difference
let totalMonthDifference = (day.getFullYear() - dateOfInput.getFullYear()) * 12 + (day.getMonth() - dateOfInput.getMonth());
// If the total month difference is 2 or more, set isOld to true
if (totalMonthDifference >= 2) {
isOld = true;
}
// Iterate over intervals
for (let i = 1; i < parsedIntervals.length; i++) {
if (parsedIntervals[i].start > maxEndTime) {
availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld));
minStartTime = parsedIntervals[i].start;
maxEndTime = parsedIntervals[i].end;
} else {
maxEndTime = Math.max(maxEndTime, parsedIntervals[i].end);
}
}
// Add the last availability
availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld));
}
else {
common.logger.debug("availability not matched. header:" + dayHeader + " shifts:" + shifts);
}
}
common.logger.debug("availabilities to save for " + personId + ": " + availabilities.length);
// Send a single request to create all availabilities
axiosInstance.post('/api/?action=createAvailabilities', availabilities)
.then(response => common.logger.debug('Availabilities created:', response.data))
.catch(error => console.error('Error creating availabilities:', error));
// Experimental: add availabilities to all publishers with the same email
//check if more than one publisher has the same email, and add the availabilities to both
//check existing publishers with the same email
var sameNamePubs = axiosInstance.get(`/api/?action=findPublisher&all=true&email=${email}&select=id,firstName,lastName`);
sameNamePubs.then(function (response) {
common.logger.debug("same name pubs: " + response.data.length);
if (response.data.length > 1) {
response.data.forEach(pub => {
//check the publisher is not the same as the one we already added the availabilities to
if (pub.id != personId) {
//change the publisher id to the new one
availabilities.forEach(availability => {
availability.publisherId = pub.id;
}
);
//delete existing availabilities for the publisher
axiosInstance.post(`/api/?action=deleteAvailabilityForPublisher&publisherId=${pub.id}&date=${dateOfInput}`);
// Send a single request to create all availabilities
axiosInstance.post('/api/?action=createAvailabilities', availabilities)
.then(response => common.logger.debug('Availabilities created:', response.data))
.catch(error => console.error('Error creating availabilities:', error));
}
});
}
});
}
// await axios.post("/api/data/availabilities", availabilities);
} catch (error) {
console.error(error);
}
}
toast.info('Records saved successfully', { autoClose: 30000 });
} catch (error) {
console.error(error);
toast.error('An error occurred while saving records!');
}
};
// Function to create an availability object
function createAvailabilityObject(start: any, end: number, day: Date, dayOfWeekName: any, dayOfMonth: number, weekNr: number, personId: string, name: string, isFromPreviousMonth: boolean): Availability {
const formatTime = (time) => {
const paddedTime = String(time).padStart(4, '0');
return new Date(day.getFullYear(), day.getMonth(), day.getDate(), parseInt(paddedTime.substr(0, 2)), parseInt(paddedTime.substr(2, 4)));
};
const startTime = formatTime(start);
const endTime = formatTime(end);
return {
id: 0, // Add the missing 'id' property
publisherId: personId,
name,
dayofweek: dayOfWeekName,
dayOfMonth,
weekOfMonth: weekNr, // Add the missing 'weekOfMonth' property
startTime,
endTime,
isactive: true,
type: AvailabilityType.OneTime,
isWithTransportIn: false, // Add the missing 'isWithTransport' property
isWithTransportOut: false, // Add the missing 'isWithTransport' property
isFromPreviousAssignment: false, // Add the missing 'isFromPreviousAssignment' property
isFromPreviousMonth: isFromPreviousMonth // Add the missing 'isFromPreviousMonth' property
};
}
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="p-6">
<h1 className="text-3xl mb-4 font-semibold">Import Page</h1>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700" htmlFor="fileInput">Choose a file to import:</label>
<input type="file" id="fileInput" onChange={handleFile} className="mt-1 p-2 border rounded" placeholder="Choose a file" />
</div>
{/* <DatePicker label="Дата" value={date} onChange={setDate} /> */}
<div className="mb-4 space-y-2"> {/* Adjust this value as needed */}
<label className="flex items-center">
<input
type="radio"
value="PUBLISHERS1"
checked={mode.mainMode === "PUBLISHERS1"}
onChange={() => setMode({ mainMode: "PUBLISHERS1", schedule: false, publishers2Import: false })}
className="text-indigo-600"
/>
<span className="ml-2 space-x-4">Импортирай от Списък с участниците</span>
</label>
<label className="flex items-center">
<input
type="radio"
value="PUBLISHERS2"
checked={mode.mainMode === "PUBLISHERS2"}
onChange={() => setMode(prev => ({ ...prev, mainMode: "PUBLISHERS2" }))}
className="text-indigo-600"
/>
<span className="ml-2">Импортирай от Предпочитания за колички</span>
</label>
<label className="flex items-center">
{/* <DatePicker
label="Дата на импорт"
value={new Date()}
onChange={(date) => {
common.logger.debug("date changed to " + date);
}}
/> */}
{/* simple HTML datepicker for import date */}
<input type="date" id="importDate" name="importDate" />
</label>
{mode.mainMode === "PUBLISHERS2" && (
<div className="mt-2 space-y-1 pl-6">
<label className="flex items-center">
<input
type="checkbox"
checked={mode.schedule}
onChange={(e) => setMode(prev => ({ ...prev, schedule: e.target.checked }))}
className="text-indigo-600"
/>
<span className="ml-2">Предпочитания</span>
</label>
<label className="flex items-center ">
<input
type="checkbox"
checked={mode.publishers2Import}
onChange={(e) => setMode(prev => ({ ...prev, publishers2Import: e.target.checked }))}
className="text-indigo-600"
/>
<span className="ml-2">Вестители</span>
</label>
</div>
)}
</div>
<div className="mb-4">
<button onClick={handleSave} className="bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">Запази</button>
</div>
<div className="mb-4 flex items-center space-x-4">
<span className="text-gray-600">{data.length} вестители прочетени</span>
<span className="text-gray-600">{status.info}</span>
</div>
<table className="min-w-full border-collapse border border-gray-500">
<thead>
<tr>
{data.length > 0 &&
Object.keys(data[0]).map((key) => <th className="px-4 py-2 border-b font-medium" key={key}>{Object.values(data[0])[key]}</th>)}
</tr>
</thead>
<tbody>
{data.slice(1).map((row, index) => (
<tr key={index} className="even:bg-gray-100">
{Object.values(row).map((value, index) => (
<td key={index} className="border px-4 py-2">{value}</td>
))}
<td id={row[1]}>
<i className={`fa fa-circle ${status[row[3]] || 'text-gray-400'}`}></i>
</td>
</tr>
))}
</tbody>
</table>
</div>
</ProtectedRoute>
</Layout>
);
};

View File

@ -0,0 +1,214 @@
// Next.js page to show all locations in the database with a link to the location page
import { useSession } from "next-auth/react";
import { useEffect, useState, useRef, use } from "react";
// import { getSession } from 'next-auth/client'
// import { NextAuth } from 'next-auth/client'
import { Publisher, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import PublisherCard from "../../../components/publisher/PublisherCard";
import axiosInstance from "../../../src/axiosSecure";
import axiosServer from '../../../src/axiosServer';
import toast from "react-hot-toast";
import { levenshteinEditDistance } from "levenshtein-edit-distance";
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
interface IProps {
initialItems: Publisher[];
}
function PublishersPage({ publishers = [] }: IProps) {
const [shownPubs, setShownPubs] = useState(publishers);
const [filter, setFilter] = useState("");
const [filterIsImported, setFilterIsImported] = useState({
checked: false,
indeterminate: true,
});
const [showZeroShiftsOnly, setShowZeroShiftsOnly] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleDeleteAllVisible = async () => {
setIsDeleting(true);
for (const publisher of shownPubs) {
try {
await axiosInstance.delete(`/api/data/publishers/${publisher.id}`);
setShownPubs(shownPubs.filter(p => p.id !== publisher.id));
} catch (error) {
console.log(JSON.stringify(error));
}
}
setIsDeleting(false);
setIsModalOpen(false);
// After all publishers are deleted, you might want to refresh the list or make additional changes
};
useEffect(() => {
// const filteredPublishers = publishers.filter((publisher) => {
// return publisher.firstName.toLowerCase().includes(filter.toLowerCase())
// || publisher.lastName.toLowerCase().includes(filter.toLowerCase());
// });
//name filter
let filteredPublishersByName = publishers
.filter((publisher) => {
const fullName = publisher.firstName.toLowerCase() + " " + publisher.lastName.toLowerCase();
const distance = levenshteinEditDistance(fullName, filter.toLowerCase());
const lenDiff = Math.max(fullName.length, filter.length) - Math.min(fullName.length, filter.length);
let similarity;
if (distance === 0) {
similarity = 1; // Exact match
} else {
similarity = 1 - (distance - lenDiff) / distance;
}
console.log("distance: " + distance + "; lenDiff: " + lenDiff + " similarity: " + similarity + "; " + fullName + " =? " + filter + "")
return similarity >= 0.95;
});
// Email filter
let filteredPublishersByEmail = publishers.filter(publisher =>
publisher.email.toLowerCase().includes(filter.toLowerCase())
);
// Combine name and email filters, removing duplicates
let filteredPublishers = [...new Set([...filteredPublishersByName, ...filteredPublishersByEmail])];
// inactive publishers filter
filteredPublishers = showZeroShiftsOnly
? filteredPublishers.filter(p => p.assignments.length === 0)
: filteredPublishers;
setShownPubs(filteredPublishers);
}, [filter, showZeroShiftsOnly]);
const checkboxRef = useRef();
const renderPublishers = () => {
if (shownPubs.length === 0) {
return (
<div className="flex justify-center">
<a
className="btn"
href="javascript:void(0);"
onClick={() => {
setFilter("");
handleFilterChange({ target: { value: "" } });
}}
>
Clear filters
</a>
</div>
);
}
else {
return shownPubs.map((publisher) => (
<PublisherCard key={publisher.id} publisher={publisher} />
));
}
};
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = event.target;
// setFilter(event.target.value);
if (type === 'text') {
setFilter(value);
} else if (type === 'checkbox') {
// setFilterIsImported({ ...checkboxFilter, [name]: checked });
const { checked, indeterminate } = checkboxRef.current;
if (!checked && !indeterminate) {
// Checkbox was unchecked, set it to indeterminate state
checkboxRef.current.indeterminate = true;
setFilterIsImported({ checked: false, indeterminate: true });
} else if (!checked && indeterminate) {
// Checkbox was indeterminate, set it to checked state
checkboxRef.current.checked = true;
checkboxRef.current.indeterminate = false;
setFilterIsImported({ checked: true, indeterminate: false });
} else if (checked && !indeterminate) {
// Checkbox was checked, set it to unchecked state
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = false;
setFilterIsImported({ checked: false, indeterminate: false });
} else {
// Checkbox was checked and indeterminate (should not happen), set it to unchecked state
checkboxRef.current.checked = false;
checkboxRef.current.indeterminate = false;
setFilterIsImported({ checked: false, indeterminate: false });
}
}
};
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
<div className="">
<div className="flex items-center justify-center space-x-4 m-4">
<div className="flex justify-center m-4">
<a href="/cart/publishers/new" className="btn"> Добави вестител </a>
</div>
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpen(true)} disabled={isDeleting} >
{isDeleting ? "Изтриване..." : "Изтрий показаните вестители"}
</button>
<ConfirmationModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onConfirm={handleDeleteAllVisible}
message="Сигурни ли сте, че искате да изтриете всички показани в момента вестители?"
/>
<div className="flex justify-center m-4">
<a href="/cart/publishers/import" className="btn"> Import publishers </a>
</div>
</div>
<div name="filters" className="flex items-center justify-center space-x-4 m-4 sticky top-4 z-10 bg-gray-100 p-2">
<label htmlFor="filter">Filter:</label>
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
className="border border-gray-300 rounded-md px-2 py-1"
/>
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
className="form-checkbox text-indigo-600"
/>
<span className="ml-2">само без смени</span>
</label>
<span id="filter-info" className="ml-4">{publishers.length} от {publishers.length} вестителя</span>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-4 z-0">
{renderPublishers()}
</div>
</div>
</ProtectedRoute>
</Layout>
);
}
export default PublishersPage;
//import { set } from "date-fns";
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
//ToDo: refactor all axios calls to use axiosInstance and this URL
const { data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isactive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect');
return {
props: {
publishers,
},
};
};

View File

@ -0,0 +1,59 @@
//next.js page to show all locatons in the database with a link to the location page
import { useState } from "react";
import { useRouter } from 'next/router';
import Layout from "../../../components/layout";
import axiosServer from '../../../src/axiosServer';
import { useSession } from 'next-auth/react';
import ProtectedRoute from '../../../components/protectedRoute';
import PublisherForm from "../../../components/publisher/PublisherForm";
import { Publisher, UserRole } from "@prisma/client";
export default function NewPubPage(item: Publisher) {
item = item.item;
const [publisher, setPublisher] = useState<Publisher>(item);
const router = useRouter();
const { id, self } = router.query;
const { data: session, status } = useSession();
const userRole = session?.user?.role as UserRole;
const userId = session?.user?.id;
// Check if the user is editing their own profile and adjust allowedRoles accordingly
let allowedRoles = [UserRole.POWERUSER, UserRole.ADMIN];
if (status === 'authenticated' && userId && userId === id) {
allowedRoles.push(userRole);
}
return (
<Layout>
<ProtectedRoute allowedRoles={allowedRoles}>
<div className="h-5/6 grid place-items-center">
<PublisherForm key={item?.id} item={item} me={self} />
</div>
</ProtectedRoute>
</Layout>
);
}
//------------------pages\cart\publishers\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
if (!context.query || !context.query.id) {
return {
props: {}
};
}
var url = process.env.NEXTAUTH_URL + "/api/data/publishers/" + context.query.id + "?include=availabilities,shifts";
console.log("GET PUBLISHER FROM:" + url)
const { data } = await axios.get(url);
return {
props: {
data
},
};
};

View File

@ -0,0 +1,46 @@
//next.js page to show all locatons in the database with a link to the location page
// import axios from "axios";
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import ExperienceForm from "../../../components/reports/ExperienceForm";
import axiosInstance from '../../../src/axiosSecure';
import ProtectedRoute from '../../../components/protectedRoute';
function NewPage(loc: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<div className="h-5/6 grid place-items-center">
<ExperienceForm />
</div></ProtectedRoute>
</Layout>
);
}
export default NewPage;
//------------------pages\cart\locations\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
return {
props: {}
};
//if query is undefined, then it is a new location
// if (context.query.id === undefined) {
// return {
// props: {}
// };
// }
// const { data: loc } = await axiosInstance.get(
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
// );
// console.log(location) //this is the location object
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
// return {
// props: {
// location: loc,
// },
// };
};

130
pages/cart/reports/list.tsx Normal file
View File

@ -0,0 +1,130 @@
//page to show all repots in the database with a link to the report page
import axiosInstance from '../../../src/axiosSecure';
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useRouter } from "next/router";
import Link from "next/link";
import { useSession } from "next-auth/react"
//const common = require('src/helpers/common');
import common from '../../../src/helpers/common';
import Layout from "../../../components/layout";
import ProtectedRoute from '../../../components/protectedRoute';
import { Location, UserRole } from "@prisma/client";
export default function Reports() {
const [reports, setReports] = useState([]);
const router = useRouter();
const { data: session } = useSession();
const deleteReport = (id) => {
axiosInstance
.delete(`api/data/reports/${id}`)
.then((res) => {
toast.success("Успешно изтрит отчет");
// router.push("/cart/reports/list");
setReports(reports.filter(report => report.id !== id));
})
.catch((err) => {
console.log(err);
});
};
const [locations, setLocations] = useState([]);
useEffect(() => {
const fetchLocations = async () => {
try {
console.log("fetching locations");
const { data } = await axiosInstance.get("/api/data/locations");
setLocations(data);
console.log(data);
axiosInstance.get(`api/data/reports`)
.then((res) => {
let reports = res.data;
reports.forEach((report) => {
report.location = data.find((loc) => loc.id === report.locationId);
});
setReports(res.data);
})
.catch((err) => {
console.log(err);
});
} catch (error) {
console.error(error);
}
};
if (!locations.length) {
fetchLocations();
}
}, []);
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<div className="h-5/6 grid place-items-center">
<div className="flex flex-col w-full px-4">
<h1 className="text-2xl font-bold text-center">Отчети</h1>
<Link href="/cart/reports/report">
<button className="mt-4 bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
Добави нов отчет
</button>
</Link>
<div className="mt-4 w-full overflow-x-auto">
<table className="w-full table-auto">
<thead>
<tr>
<th className="px-4 py-2 text-left">Дата</th>
<th className="px-4 py-2 text-left" >Място</th>
<th className="px-4 py-2 text-left">Отчет</th>
<th className="px-4 py-2 text-left">Действия</th>
</tr>
</thead>
<tbody>
{reports.map((report) => (
<tr key={report.id}>
<td className="border px-4 py-2">{common.getDateFormated(new Date(report.date))}</td>
<td className="border px-4 py-2">{report.location?.name}</td>
<td className="border px-4 py-2">
{(report.experienceInfo === null || report.experienceInfo === "")
? (
<>
<div><strong>Отчет</strong></div>
Издания: {report.placementCount} <br />
Разговори: {report.conversationCount} <br />
Клипове: {report.videoCount} <br />
Адреси / Телефони: {report.returnVisitInfoCount} <br />
</>
) : (
<>
<div><strong>Случка</strong></div>
<div dangerouslySetInnerHTML={{ __html: report.experienceInfo }} />
</>
)}
</td>
<td className="border px-4 py-2">
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
onClick={() => deleteReport(report.id)}
>
Изтрий
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div >
</div >
</ProtectedRoute>
</Layout>
);
}

View File

@ -0,0 +1,45 @@
//next.js page to show all locatons in the database with a link to the location page
// import axios from "axios";
import { Location, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import ReportForm from "../../../components/reports/ReportForm";
import axiosInstance from '../../../src/axiosSecure';
import ProtectedRoute from '../../../components/protectedRoute';
function NewPage(loc: Location) {
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN, UserRole.USER, UserRole.EXTERNAL]}>
<div className="h-5/6 grid place-items-center">
<ReportForm />
</div></ProtectedRoute>
</Layout>
);
}
export default NewPage;
//------------------pages\cart\locations\edit\[id].tsx------------------
export const getServerSideProps = async (context) => {
return {
props: {}
};
//if query is undefined, then it is a new location
// if (context.query.id === undefined) {
// return {
// props: {}
// };
// }
// const { data: loc } = await axiosInstance.get(
// `${process.env.NEXTAUTH_URL}api/data/locations/` + context.params.id
// );
// console.log(location) //this is the location object
// context.res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate");
// return {
// props: {
// location: loc,
// },
// };
};