initial commit - code moved to separate repo
This commit is contained in:
101
components/publisher/PublisherCard.js
Normal file
101
components/publisher/PublisherCard.js
Normal file
@ -0,0 +1,101 @@
|
||||
import Link from "next/link";
|
||||
import { Publisher } from "@prisma/client"
|
||||
// import {IsDateXMonthsAgo} from "../../helpers/const"
|
||||
import { useEffect, useState } from 'react'
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import axiosInstance from '../../src/axiosSecure';
|
||||
|
||||
//add months to date. works with negative numbers and numbers > 12
|
||||
export function addMonths(numOfMonths, date) {
|
||||
var date = new Date(date);
|
||||
var m, d = (date = new Date(+date)).getDate();
|
||||
date.setMonth(date.getMonth() + numOfMonths, 1);
|
||||
m = date.getMonth();
|
||||
date.setDate(d);
|
||||
if (date.getMonth() !== m) date.setDate(0);
|
||||
return date;
|
||||
|
||||
}
|
||||
|
||||
//is date in range of months from and to
|
||||
//usage:
|
||||
//is date in last month: IsDateInXMonths(date, -1, 0)
|
||||
//is date in current month: IsDateInXMonths(date, 0, 0)
|
||||
//is date in next month: IsDateInXMonths(date, 0, 1)
|
||||
|
||||
export function IsDateInXMonths(date, monthsFrom, monthsTo) {
|
||||
var date = new Date(date);
|
||||
var dateYearMonth = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
if (monthsFrom === undefined) monthsFrom = -100;
|
||||
if (monthsTo === undefined) monthsTo = 100;
|
||||
// var from = new Date(date.setMonth(dateYearMonth.getMonth()+monthsFrom));
|
||||
// var to = new Date(date.setMonth(dateYearMonth.getMonth()+monthsTo));
|
||||
var from = addMonths(monthsFrom, dateYearMonth);
|
||||
var to = addMonths(monthsTo, dateYearMonth);
|
||||
//is date between from and to
|
||||
return date >= from && date <= to;
|
||||
};
|
||||
|
||||
export default function PublisherCard({ publisher }) {
|
||||
|
||||
const [isCardVisible, setIsCardVisible] = useState(true);
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
console.log("card: deleting publisher = ", id, "url: ", `/api/data/publishers/${id}`);
|
||||
const response = await axiosInstance.delete(`/api/data/publishers/${id}`);
|
||||
if (response.status === 200) {
|
||||
document.getElementById(`publisher-card-${id}`).classList.add('cardFadeOut');
|
||||
setTimeout(() => setIsCardVisible(false), 300);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify(error));
|
||||
}
|
||||
};
|
||||
|
||||
return isCardVisible ? (
|
||||
// className="block p-6 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3"
|
||||
<div id={`publisher-card-${publisher.id}`} className={`relative block p-6 max-w-sm rounded-lg border border-gray-200 shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 mb-3
|
||||
${publisher.isImported ? "bg-orange-50" : (publisher.isTrained ? "bg-white" : "bg-red-50")}`}>
|
||||
<a
|
||||
href={`/cart/publishers/edit/${publisher.id}`}
|
||||
className=""
|
||||
key={publisher.id}
|
||||
>
|
||||
|
||||
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{publisher.firstName} {publisher.lastName} ({publisher.isactive ? "active" : "inactive"})
|
||||
</h5>
|
||||
<div className="font-normal text-gray-700 dark:text-gray-200">
|
||||
<p> {publisher.assignments.length} смени общо</p>
|
||||
<p> достъпност: {publisher.availabilities?.length}</p>
|
||||
{/* <p> {publisher.shifts.filter(s => IsDateInXMonths(s.startTime, -1, 0)).length} last month</p>
|
||||
<p> {publisher.shifts.filter(s => IsDateInXMonths(s.startTime, 0, 0)).length} this month</p>
|
||||
<p> {publisher.shifts.filter(s => IsDateInXMonths(s.startTime, 0, 1)).length} next month</p> */}
|
||||
</div>
|
||||
</a>
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<button onClick={() => handleDelete(publisher.id)} aria-label="Delete Publisher">
|
||||
<svg className="w-5 h-6 text-red-500 hover:text-red-700" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="M10 11V17" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M14 11V17" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M4 7H20" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M6 7H12H18V18C18 19.6569 16.6569 21 15 21H9C7.34315 21 6 19.6569 6 18V7Z" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
||||
{/* <path d="M8 9a1 1 0 000 2h4a1 1 0 100-2H8z" />
|
||||
<path fillRule="evenodd" d="M4.293 4.293A1 1 0 015.707 3.707L10 8l4.293-4.293a1 1 0 111.414 1.414L11.414 9l4.293 4.293a1 1 0 01-1.414 1.414L10 10.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 9 4.293 4.707a1 1 0 010-1.414z" clipRule="evenodd" /> */}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.cardFadeOut {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
317
components/publisher/PublisherForm.js
Normal file
317
components/publisher/PublisherForm.js
Normal file
@ -0,0 +1,317 @@
|
||||
// import axios from "axios";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import axiosInstance from '../../src/axiosSecure';
|
||||
//import { getDate } from "date-fns";
|
||||
|
||||
import { monthNamesBG, GetTimeFormat, GetDateFormat } from "../../src/helpers/const"
|
||||
import PublisherSearchBox from './PublisherSearchBox';
|
||||
import AvailabilityList from "../availability/AvailabilityList";
|
||||
import ShiftsList from "../publisher/ShiftsList.tsx";
|
||||
import common from "../../src/helpers/common";
|
||||
|
||||
import ProtectedRoute from '../../components/protectedRoute';
|
||||
import { UserRole } from "@prisma/client";
|
||||
|
||||
// import { Tabs, List } from 'tw-elements'
|
||||
|
||||
// model Publisher {
|
||||
// id String @id @default(cuid())
|
||||
// firstName String
|
||||
// lastName String
|
||||
// email String @unique
|
||||
// phone String?
|
||||
// isactive Boolean @default(true)
|
||||
// isImported Boolean @default(false)
|
||||
// age Int?
|
||||
// availabilities Availability[]
|
||||
// assignments Assignment[]
|
||||
|
||||
// emailVerified DateTime?
|
||||
// accounts Account[]
|
||||
// sessions Session[]
|
||||
// role UserRole @default(USER)
|
||||
// desiredShiftsPerMonth Int @default(4)
|
||||
// isMale Boolean @default(true)
|
||||
// isNameForeign Boolean @default(false)
|
||||
|
||||
// familyHeadId String? // Optional familyHeadId for each family member
|
||||
// familyHead Publisher? @relation("FamilyMember", fields: [familyHeadId], references: [id])
|
||||
// familyMembers Publisher[] @relation("FamilyMember")
|
||||
// type PublisherType @default(Publisher)
|
||||
// Town String?
|
||||
// Comments String?
|
||||
// }
|
||||
|
||||
Array.prototype.groupBy = function (prop) {
|
||||
return this.reduce(function (groups, item) {
|
||||
const val = item[prop]
|
||||
groups[val] = groups[val] || []
|
||||
groups[val].push(item)
|
||||
return groups
|
||||
}, {})
|
||||
}
|
||||
|
||||
export default function PublisherForm({ item, me }) {
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
console.log("init PublisherForm: ");
|
||||
const urls = {
|
||||
apiUrl: "/api/data/publishers/",
|
||||
indexUrl: "/cart/publishers"
|
||||
}
|
||||
|
||||
const [helpers, setHelper] = useState(null);
|
||||
const fetchModules = async () => {
|
||||
const h = (await import("../../src/helpers/const.js")).default;
|
||||
//console.log("fetchModules: " + JSON.stringify(h));
|
||||
setHelper(h);
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchModules();
|
||||
}, []);
|
||||
|
||||
const [publisher, set] = useState(item || {
|
||||
isactive: true,
|
||||
});
|
||||
|
||||
const handleChange = ({ target }) => {
|
||||
if (target.type === "checkbox") {
|
||||
set({ ...publisher, [target.name]: target.checked });
|
||||
} else if (target.type === "number") {
|
||||
set({ ...publisher, [target.name]: parseInt(target.value) });
|
||||
} else {
|
||||
set({ ...publisher, [target.name]: target.value });
|
||||
}
|
||||
if (item?.firstName) {
|
||||
publisher.isMale = item.firstName && item.firstName.endsWith('а') ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleParentSelection = (head) => {
|
||||
//setSelectedParent(parent);
|
||||
// Update the publisher state with the selected publisher's ID
|
||||
console.log("handleParentSelection: " + JSON.stringify(head));
|
||||
set({ ...publisher, familyHeadId: head.id });
|
||||
// Create a new object excluding the familyHeadId property
|
||||
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
router.query.id = router.query.id || "";
|
||||
console.log("handleSubmit: " + JSON.stringify(publisher));
|
||||
console.log("urls.apiUrl + router.query.id: " + urls.apiUrl + router.query.id)
|
||||
e.preventDefault();
|
||||
//remove availabilities, assignments from publisher
|
||||
publisher.availabilities = undefined;
|
||||
publisher.assignments = undefined;
|
||||
|
||||
let { familyHeadId, userId, ...rest } = publisher;
|
||||
// Set the familyHead relation based on the selected head
|
||||
const familyHeadRelation = familyHeadId ? { connect: { id: familyHeadId } } : { disconnect: true };
|
||||
const userRel = userId ? { connect: { id: userId } } : { disconnect: true };
|
||||
// Return the new state without familyHeadId and with the correct familyHead relation
|
||||
rest = {
|
||||
...rest,
|
||||
familyHead: familyHeadRelation,
|
||||
user: userRel
|
||||
};
|
||||
|
||||
try {
|
||||
if (router.query?.id) {
|
||||
await axiosInstance.put(urls.apiUrl + router.query.id, {
|
||||
...rest,
|
||||
});
|
||||
toast.success("Task Updated", {
|
||||
position: "bottom-center",
|
||||
});
|
||||
} else {
|
||||
await axiosInstance.post(urls.apiUrl, publisher);
|
||||
toast.success("Task Saved", {
|
||||
position: "bottom-center",
|
||||
});
|
||||
}
|
||||
router.push(urls.indexUrl);
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify(error));
|
||||
//toast.error(error.response.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
//console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id);
|
||||
await axiosInstance.delete(urls.apiUrl + router.query.id);
|
||||
toast.success("Записът изтрит", {
|
||||
position: "bottom-center",
|
||||
});
|
||||
router.push(urls.indexUrl);
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify(error));
|
||||
toast.error(error.response.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
let formTitle;
|
||||
me = common.parseBool(me);
|
||||
if (me) {
|
||||
formTitle = "Моят профил / Настройки";
|
||||
} else if (router.query?.id) {
|
||||
formTitle = "Редактирай вестител"; // "Edit Publisher"
|
||||
} else {
|
||||
formTitle = "Създай вестител"; // "Create Publisher"
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-2xl font-semibold mt-6 mb-4">{formTitle}</h3>
|
||||
<div className="h-4"></div>
|
||||
<div className="flex flex-row">
|
||||
|
||||
<form className="form"
|
||||
onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="firstName">Име</label>
|
||||
<input type="text" name="firstName" value={publisher.firstName} onChange={handleChange} className="textbox" placeholder="First Name" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="lastName">Фамилия</label>
|
||||
<input type="text" name="lastName" value={publisher.lastName} onChange={handleChange} className="textbox" placeholder="Last Name" autoFocus />
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" ">
|
||||
<div className="form-check">
|
||||
<input className="checkbox" type="checkbox" value={publisher.isNameForeign} id="isNameForeign" name="isNameForeign" onChange={handleChange} checked={publisher.isNameForeign} autoComplete="off" />
|
||||
<label className="label" htmlFor="isNameForeign">
|
||||
Чуждестранна фамилия</label>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
</div>
|
||||
{/* //desiredShiftsPerMonth */}
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="desiredShiftsPerMonth">Желани смeни на месец</label>
|
||||
<input type="number" name="desiredShiftsPerMonth" value={publisher.desiredShiftsPerMonth} onChange={handleChange} className="textbox" placeholder="desiredShiftsPerMonth" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="email">Имейл</label>
|
||||
<input type="text" name="email" value={publisher.email} onChange={handleChange} className="textbox" placeholder="Email" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="phone">Телефон</label>
|
||||
<input type="text" name="phone" value={publisher.phone} onChange={handleChange} className="textbox" placeholder="Phone" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="parentPublisher">
|
||||
Семейство (избери главата на семейството)
|
||||
</label>
|
||||
<PublisherSearchBox selectedId={publisher.familyHeadId} onChange={handleParentSelection} />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="form-check">
|
||||
<input className="radio" type="radio" id="male" name="isMale"
|
||||
onChange={() => handleChange({ target: { name: "isMale", value: true } })}
|
||||
checked={publisher.isMale}
|
||||
/>
|
||||
<label className="label" htmlFor="male">
|
||||
Мъж
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="radio" type="radio" id="female" name="isMale"
|
||||
onChange={() => handleChange({ target: { name: "isMale", value: false } })}
|
||||
checked={!publisher.isMale}
|
||||
/>
|
||||
<label className="label" htmlFor="female">
|
||||
Жена
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="type">Тип</label>
|
||||
<select name="type" value={publisher.type} onChange={handleChange} className="textbox" placeholder="Type" autoFocus >
|
||||
<option value="Publisher">Вестител</option>
|
||||
<option value="Bethelite">Бетелит</option>
|
||||
<option value="RegularPioneer">Редовен Пионер</option>
|
||||
<option value="SpecialPioneer">Специален Пионер/Мисионер</option>
|
||||
{/* <option value="Missionary">Мисионер</option>
|
||||
<option value="CircuitOverseer">Пътуваща служба</option> */}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="town">Град</label>
|
||||
<input type="text" name="town" value={publisher.town} onChange={handleChange} className="textbox" placeholder="Град" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="comments">Коментари</label>
|
||||
<input type="text" name="comments" value={publisher.comments} onChange={handleChange} className="textbox" placeholder="Коментари" autoFocus />
|
||||
</div>
|
||||
</div>
|
||||
<ProtectedRoute allowedRoles={[UserRole.ADMIN]} deniedMessage=" ">
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="age">Възраст</label>
|
||||
<input type="number" name="age" value={publisher.age} onChange={handleChange} className="textbox" placeholder="Age" autoFocus />
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<div className="form-check">
|
||||
<input className="checkbox" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={publisher.isactive} autoComplete="off" />
|
||||
<label className="label" htmlFor="isactive">Активен</label>
|
||||
<input className="checkbox" type="checkbox" id="isTrained" name="isTrained" onChange={handleChange} checked={publisher.isTrained} autoComplete="off" />
|
||||
<label className="label" htmlFor="isTrained">Получил обучение</label>
|
||||
<input className="checkbox disabled" type="checkbox" id="isImported" name="isImported" onChange={handleChange} checked={publisher.isImported} autoComplete="off" />
|
||||
<label className="label " htmlFor="isImported">Импортиран от график</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="label" htmlFor="role">Роля Потребител</label>
|
||||
<select name="role" id="role" className="select" value={publisher.role} onChange={handleChange} >
|
||||
{/* <option value='${UserRole.USER}'>Потребител</option> */}
|
||||
<option value={`${UserRole.USER}`}>Потребител</option>
|
||||
<option value={`${UserRole.EXTERNAL}`}>Външен</option>
|
||||
<option value={`${UserRole.POWERUSER}`}>Организатор</option>
|
||||
<option value={`${UserRole.ADMIN}`}>Администратор</option>
|
||||
{/* Add other roles as needed */}
|
||||
|
||||
|
||||
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
{/* ---------------------------- Actions --------------------------------- */}
|
||||
<div className="panel-actions">
|
||||
<Link href={urls.indexUrl} className="action-button"> обратно </Link>
|
||||
{/* delete */}
|
||||
<button className="button bg-red-500 hover:bg-red-700 focus:outline-none focus:shadow-outline" type="button" onClick={handleDelete}>
|
||||
Delete
|
||||
</button>
|
||||
<button className="button bg-blue-500 hover:bg-blue-700 focus:outline-none focus:shadow-outline" type="submit">
|
||||
{router.query?.id ? "Update" : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row">
|
||||
<div className="flex-col" id="shiftlist" >
|
||||
<div className="">
|
||||
<ShiftsList assignments={publisher.assignments} selectedtab={common.getCurrentYearMonth()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 p-5">
|
||||
<AvailabilityList publisher={publisher} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
|
73
components/publisher/PublisherInlineForm.js
Normal file
73
components/publisher/PublisherInlineForm.js
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axiosInstance from '../../src/axiosSecure';
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const PublisherInlineForm = ({ publisherId, initialShiftsPerMonth }) => {
|
||||
const [desiredShiftsPerMonth, setDesiredShiftsPerMonth] = useState(initialShiftsPerMonth);
|
||||
const router = useRouter();
|
||||
const storedValue = useRef(initialShiftsPerMonth);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPublisherData = async () => {
|
||||
if (publisherId != null) {
|
||||
try {
|
||||
const response = await axiosInstance.get(`/api/data/publishers/${publisherId}`);
|
||||
const publisher = response.data;
|
||||
setDesiredShiftsPerMonth(publisher.desiredShiftsPerMonth);
|
||||
storedValue.current = publisher.desiredShiftsPerMonth;
|
||||
} catch (error) {
|
||||
console.error("Error fetching publisher data:", error);
|
||||
toast.error("Не може да се зареди информация.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//if (storedValue.current == null) {
|
||||
fetchPublisherData();
|
||||
//}
|
||||
}, [publisherId]);
|
||||
|
||||
useEffect(() => {
|
||||
const saveShiftsPerMonth = async () => {
|
||||
|
||||
if (publisherId && desiredShiftsPerMonth != null
|
||||
&& initialShiftsPerMonth != desiredShiftsPerMonth
|
||||
&& storedValue.current != desiredShiftsPerMonth) {
|
||||
try {
|
||||
await axiosInstance.put(`/api/data/publishers/${publisherId}`, {
|
||||
desiredShiftsPerMonth,
|
||||
});
|
||||
toast.success("Смени на месец запазени", {
|
||||
position: "bottom-center",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving desired shifts per month:", error);
|
||||
toast.error("Грешка при запазване на смени на месец");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
saveShiftsPerMonth();
|
||||
}, [desiredShiftsPerMonth]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row items-center space-y-2 sm:space-y-0 sm:space-x-2">
|
||||
<label htmlFor="desiredShiftsPerMonth" className="block text-sm font-medium text-gray-700">
|
||||
Желани смени на месец:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="desiredShiftsPerMonth"
|
||||
name="desiredShiftsPerMonth"
|
||||
value={desiredShiftsPerMonth}
|
||||
onChange={(e) => setDesiredShiftsPerMonth(parseInt(e.target.value))}
|
||||
className="textbox mt-1 sm:mt-0 w-full sm:w-auto flex-grow"
|
||||
placeholder="Желани смени на месец"
|
||||
min="0" max="10"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublisherInlineForm;
|
140
components/publisher/PublisherSearchBox.js
Normal file
140
components/publisher/PublisherSearchBox.js
Normal file
@ -0,0 +1,140 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axiosInstance from '../../src/axiosSecure';
|
||||
import common from '../../src/helpers/common';
|
||||
//import { is } from 'date-fns/locale';
|
||||
|
||||
function PublisherSearchBox({ selectedId, onChange, isFocused, filterDate, showSearch = true, showList = false, showAllAuto = false, infoText = " Семеен глава" }) {
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [publishers, setPublishers] = useState([]);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [selectedDate, setSelectedDate] = useState(filterDate);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPublishers();
|
||||
}, []); // Empty dependency array ensures this useEffect runs only once
|
||||
|
||||
const fetchPublishers = async () => {
|
||||
console.log("fetchPublishers called");
|
||||
try {
|
||||
let url = `/api/?action=filterPublishers&select=id,firstName,lastName,email,isactive&searchText=${searchText}&availabilities=false`;
|
||||
|
||||
if (filterDate) {
|
||||
url += `&filterDate=${common.getISODateOnly(filterDate)}`;
|
||||
}
|
||||
if (showList) {
|
||||
url += `&assignments=true`;
|
||||
}
|
||||
|
||||
const { data: publishersData } = await axiosInstance.get(url);
|
||||
//setPublishers(publishersData);
|
||||
const activePublishers = publishersData.filter(publisher => publisher.isactive === true);
|
||||
setPublishers(activePublishers);
|
||||
|
||||
} catch (error) {
|
||||
// Handle errors
|
||||
console.error("Error fetching publishers:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHeadSelection = (pub) => {
|
||||
setSearchText('');
|
||||
setSearchResults([]);
|
||||
setSelectedItem(pub);
|
||||
onChange(pub); // Pass the selected parent to the parent component
|
||||
};
|
||||
|
||||
//allows us to trigger a focus on the input field when we trigger to show the search box from outside
|
||||
const inputRef = React.useRef(null);
|
||||
useEffect(() => {
|
||||
console.log("isFocused changed = ", isFocused);
|
||||
if (isFocused && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isFocused]);
|
||||
|
||||
// Update selectedDate filter from outside
|
||||
// useEffect(() => {
|
||||
// setSelectedDate(filterDate);
|
||||
// console.log("filterDate changed = ", filterDate);
|
||||
// }, [filterDate]);
|
||||
|
||||
// Update publishers when filterDate or showList changes
|
||||
useEffect(() => {
|
||||
fetchPublishers();
|
||||
}, [filterDate, showList]);
|
||||
|
||||
// Update selectedItem when selectedId changes and also at the initial load
|
||||
useEffect(() => {
|
||||
if (publishers) {
|
||||
const head = publishers.find((publisher) => publisher.id === selectedId);
|
||||
if (head) {
|
||||
//setSearchText(`${head.firstName} ${head.lastName}`);
|
||||
setSelectedItem(head);
|
||||
}
|
||||
}
|
||||
}, [selectedId, publishers]);
|
||||
|
||||
// Update searchResults when searchText or publishers change
|
||||
useEffect(() => {
|
||||
if (searchText || showAllAuto) {
|
||||
const filteredResults = publishers.filter((publisher) => {
|
||||
const fullName = `${publisher.firstName} ${publisher.lastName} `.toLowerCase();
|
||||
return fullName.includes(searchText.trim().toLowerCase())
|
||||
|| publisher.email.toLowerCase().includes(searchText.trim().toLowerCase());
|
||||
});
|
||||
setSearchResults(filteredResults);
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
}, [searchText, publishers]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{showSearch ? (
|
||||
<>
|
||||
<input ref={inputRef}
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onFocus={() => { isFocused = true; }}
|
||||
className="textbox"
|
||||
placeholder={`${selectedItem?.firstName || ""} ${selectedItem?.lastName || ""}`}
|
||||
/>
|
||||
{(showSearch) && (searchResults.length > 0 || showAllAuto) && (
|
||||
<ul className="absolute bg-white border border-gray-300 w-full z-10">
|
||||
{/* showAllAuto ? publishers : searchResults */}
|
||||
{(searchResults).map((publisher) => (
|
||||
<li key={publisher.id}
|
||||
className={`p-2 cursor-pointer hover:bg-gray-200 ${publisher.assignmentsCurrentWeek > 0 ? 'text-orange-500' : ''}`}
|
||||
onClick={() => { handleHeadSelection(publisher); }} >
|
||||
{publisher.firstName} {publisher.lastName}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{selectedItem && infoText && (
|
||||
<p className="font-semibold pl-1">
|
||||
{infoText}: {selectedItem.firstName} {selectedItem.lastName}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{showList ? (
|
||||
// Display only clickable list of all publishers
|
||||
<ul className="absolute bg-white border border-gray-300 w-full z-10">
|
||||
{publishers.map((publisher) => (
|
||||
<li key={publisher.id}
|
||||
className="p-2 cursor-pointer hover:bg-gray-200"
|
||||
onClick={() => { handleHeadSelection(publisher); }} >
|
||||
{publisher.firstName} {publisher.lastName}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublisherSearchBox;
|
149
components/publisher/ShiftsList.tsx
Normal file
149
components/publisher/ShiftsList.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import React, { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import axiosInstance from '../../src/axiosSecure';
|
||||
|
||||
import { monthNamesBG, GetTimeFormat, GetDateFormat } from "../../src/helpers/const"
|
||||
import common from "../../src/helpers/common";
|
||||
|
||||
type Assignment = {
|
||||
items: {
|
||||
[key: string]: any[];
|
||||
};
|
||||
keys: string[];
|
||||
months: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ShiftsListProps = {
|
||||
assignments: Assignment;
|
||||
selectedtab: string;
|
||||
};
|
||||
|
||||
|
||||
const ShiftsList = ({ assignments, selectedtab }: ShiftsListProps) => {
|
||||
|
||||
const { keys: assignmentKeys = [], months = [], items = [] } = assignments || {};
|
||||
const [currentTab, setCurrentTab] = useState<string>(selectedtab || assignmentKeys[-1]);
|
||||
|
||||
console.log("assignments = ", assignments);
|
||||
console.log("currentTab = ", currentTab);
|
||||
|
||||
const searchReplacement = async (id) => {
|
||||
try {
|
||||
var assignment = (await axiosInstance.get("/api/data/ssignments/" + id)).data;
|
||||
assignment.isConfirmed = true;
|
||||
// assignment.isDeleted = true;
|
||||
await axiosInstance.put("/api/data/assignments/" + id, assignment);
|
||||
toast.success("Shift Tentative", {
|
||||
position: "bottom-center",
|
||||
});
|
||||
// router.push(urls.indexUrl);
|
||||
} catch (error) {
|
||||
console.log(JSON.stringify(error));
|
||||
toast.error(error.response.data.message);
|
||||
}
|
||||
}
|
||||
const AddToGoogleCalendar = async (id) => {
|
||||
try {
|
||||
const { url, event } = await axiosInstance.get(`/api/shiftgenerate?action=createcalendarevent&id=${id}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
window.open(url, '_blank')
|
||||
.addEventListener('load', function () {
|
||||
console.log('loaded');
|
||||
});
|
||||
|
||||
// fetchShifts();
|
||||
// console.log(shifts);
|
||||
res.writeHead(301, { "Location": url });
|
||||
res.end();
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
console.log(JSON.stringify(error));
|
||||
if (error.response?.data?.message) {
|
||||
toast.error(error.response.data.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col m-5 w-full">
|
||||
<ul className="nav nav-tabs flex flex-col md:flex-row flex-wrap list-none border-b-0 pl-0 mb-1" role="tablist">
|
||||
{assignmentKeys?.slice(-4).map(m => (
|
||||
<li key={m.toString()} className="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => setCurrentTab(m)}
|
||||
className={`text-blue-500 border-l border-t border-r inline-block py-2 px-4 ${currentTab === m ? 'active border-gray-300 font-bold' : ' border-transparent'}`}
|
||||
// className={`nav-link block font-medium text-xs leading-tight uppercase border-x-0 border-t-0 border-b-2 ${currentTab === m ? "active border-blue-500 font-bold" : "border-transparent"} px-6 py-3 my-2 hover:border-transparent hover:bg-gray-100 focus:border-transparent`}
|
||||
role="tab"
|
||||
aria-controls={"tabs-" + m}
|
||||
aria-selected={currentTab === m}
|
||||
>
|
||||
{months[m]}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="tab-content flex flow w-full p-2 border-2 border-gray-300 rounded-md">
|
||||
{assignmentKeys?.map(month => (
|
||||
// <div className={`tab-pane fade ${month === currentTab ? "active show" : ""}`} key={month.toString()} role="tabpanel" aria-labelledby={"tabs-tab" + month}>
|
||||
//if tab is selected
|
||||
//common.getCurrentYearMonth(month)
|
||||
currentTab === month ?
|
||||
<div key={month} className={`tab-pane fade ${month === currentTab ? "active show" : ""}`} role="tabpanel" aria-labelledby={"tabs-tab" + month}>
|
||||
<div className="flex items-center py-3 px-4">
|
||||
<span className="text-xl font-medium">
|
||||
{items[month]?.filter(Boolean).reduce((total, item) => (
|
||||
Array.isArray(item) || typeof item === 'object' ? total + Object.keys(item).length : total
|
||||
), 0)} смени за {months[month]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{items[month]?.map((shiftDay, i) => (
|
||||
shiftDay && shiftDay.length > 0 ? (
|
||||
<div className="flex items-center space-x-2 py-1" key={i}>
|
||||
<div className="font-bold flex-shrink-0 w-6 text-right">{i + ":"}</div> {/*This is the column for the date. I've given it a fixed width (w-8) which you can adjust*/}
|
||||
<div className="flex-grow flex">
|
||||
{shiftDay.map(assignment => (
|
||||
<div className="flow space-x-2 bg-gray-200 rounded-lg shadow-md py-2 px-3" key={assignment.id}>
|
||||
<span>{GetTimeFormat(assignment.start)} - {GetTimeFormat(assignment.end)}</span>
|
||||
<button
|
||||
className={`text-sm text-white font-semibold px-2 rounded-lg shadow ${assignment.isConfirmed ? "bg-yellow-400 hover:bg-yellow-500" : "bg-red-400 hover:bg-red-500"}`}
|
||||
onClick={() => searchReplacement(assignment.id)}
|
||||
>
|
||||
Търси заместник
|
||||
</button>
|
||||
<button
|
||||
className="text-sm bg-green-400 hover:bg-green-500 text-white font-semibold px-2 rounded-lg shadow"
|
||||
onClick={() => AddToGoogleCalendar(assignment.id)}
|
||||
>
|
||||
Добави в календар
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>) : null
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
: null
|
||||
))}
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default ShiftsList;
|
Reference in New Issue
Block a user