Files
mwitnessing/components/survey/SurveyForm.tsx

357 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import axiosInstance from '../../src/axiosSecure';
import { use, 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"
import { MessageType, Message, Survey } from "@prisma/client";
// import { content } from 'googleapis/build/src/apis/content';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
// import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
// import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import dayjs from 'dayjs';
import { set } from 'lodash';
const common = require('src/helpers/common');
// ------------------ ------------------
// This component is used to create and edit
/* location model:
model Survey {
id Int @id @default(autoincrement())
content String
answers Json?
messages Message[]
publicFrom DateTime?
publicUntil DateTime?
}
model Message {
id Int @id @default(autoincrement())
publisher Publisher @relation(fields: [publisherId], references: [id])
publisherId String
date DateTime
content String
isRead Boolean @default(false)
isPublic Boolean @default(false)
type MessageType @default(Email)
publicUntil DateTime?
shownDate DateTime?
answer String?
answerDate DateTime?
Survey Survey? @relation(fields: [surveyId], references: [id])
surveyId Int?
}
*/
interface SurveyFormProps {
existingItem: Survey | null;
}
const SurveyForm: React.FC<SurveyFormProps> = ({ existingItem }) => {
const router = useRouter();
const [editMode, setEditMode] = useState(existingItem ? true : false);
const [pubs, setPubs] = useState([]);
const [item, setItem] = useState(existingItem || {
...existingItem,
content: existingItem?.content || "Нова анкета",
answers: existingItem?.answers.split(",") || [],
publicFrom: existingItem?.publicFrom ? dayjs(existingItem.publicFrom).toISOString() : '',
publicUntil: existingItem?.publicUntil ? dayjs(existingItem.publicUntil).toISOString() : null,
});
useEffect(() => {
let transformedItem = { ...existingItem };
transformedItem.answersCount = existingItem?.answers.split(",") || [];
setEditMode(existingItem ? true : false);
setItem(transformedItem);
}, [existingItem]);
useEffect(async () => {
const pubs = await axiosInstance.get("/api/data/publishers?select=id,firstName,lastName,email");
setPubs(pubs.data);
}, []);
const handleChange = ({ target }) => {
setItem({ ...item, [target.name]: target.value });
};
const handleDateChange = (fieldName, newDate) => {
setItem((prevItem) => ({
...prevItem,
[fieldName]: newDate
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
delete item.answersCount;
try {
if (editMode) {
delete item.messages;
const { data } = await axiosInstance.put(`/api/data/surveys/${existingItem.id}`, item);
toast.success("Анкетата е обновена успешно");
}
else {
//get all publisherIds and create a message for each
const messages = pubs.map(pub => {
return {
publisherId: pub.id,
content: JSON.stringify({ message: item.content, options: item.answers }),
date: new Date(),
isPublic: false,
type: MessageType.InApp,
publicUntil: item.publicUntil,
}
});
item.messages = { create: messages };
const { data } = await axiosInstance.post("/api/data/surveys", item);
toast.success("Анкетата е създадена успешно");
}
router.push("/cart/surveys");
} catch (error) {
toast.error("Възникна грешка при създаването на анкетата");
console.error("Error creating survey:", error);
}
}
const handleDelete = async (e) => {
e.preventDefault();
if (!editMode) return;
try {
await axiosInstance.delete(`/api/data/surveys/${existingItem.id}`);
toast.success("Записът изтрит", {
position: "bottom-center",
});
router.push("/cart/surveys");
} catch (error) {
//alert("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас");
console.log(JSON.stringify(error));
toast.error(error.response?.data?.message || "Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи.");
}
};
function handleDeleteAnswers(e: MouseEvent<HTMLButtonElement, MouseEvent>): void {
e.preventDefault();
if (!editMode) return;
Promise.all(existingItem.messages.map(message =>
axiosInstance.put(`/api/data/messages/${message.id}`, { answer: null })
))
.then(() => {
toast.success("Отговорите изтрити", {
position: "bottom-center",
});
})
.catch((error) => {
console.log(JSON.stringify(error));
toast.error(error.response?.data?.message || "Нещо се обърка при изтриването. Моля, опитай отново и се свържете с нас ако проблема продължи.");
});
}
const getNamesByIds = (ids) => {
return ids
.map((id) => {
const pub = pubs.find((p) => p.id === id);
return pub ? `${pub.firstName} ${pub.lastName}` : null;
})
.filter((name) => name !== null)
.join(", ");
};
const getIdsForAnswer = (answer) => {
return item.messages
.filter((message) => message.answer === answer)
.map((message) => message.publisherId);
};
const getIdsForAnswered = () => {
return item.messages
.filter((message) => message.answer)
.map((message) => message.publisherId);
};
const getIdsForUnanswered = () => {
return item.messages
.filter((message) => !message.answer)
.map((message) => message.publisherId);
};
// const copyToClipboard = (text) => {
// navigator.clipboard.writeText(text).then(
// () => toast.success('Copied to clipboard!'),
// (err) => toast.error('Failed to copy text: ', err)
// );
// };
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(
() => alert('Имената са копирани: ' + text),
(err) => alert('Не успяхме да копираме имената: ', err)
);
};
const sendIndividualNotification = async (id, message) => {
const response = await fetch('/api/notify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id,
title: 'Нямаме отговор',
message: `${message}`,
})
});
if (response.ok) {
console.log(`Notification sent successfully to ${name}`);
} else {
console.error(`Failed to send notification to ${name}`);
}
};
const handleSendNotificationsToAllUnanswered = async (message) => {
getIdsForUnanswered().forEach((id, index) => sendIndividualNotification(id, message));
};
return (
<div className="w-full max-w-md mx-auto" >
< form className="bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4" onSubmit={handleSubmit} >
<h1 className="text-2xl font-bold mb-8">Анкета {existingItem?.id}</h1>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="date">
Видима от
</label>
<DatePicker className="textbox form-input px-4 py-2 rounded" name="publicFrom" onChange={(newDate) => handleDateChange('publicFrom', newDate)}
value={item && item.publicFrom ? dayjs(item.publicFrom) : null} />
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="date">
Видима до
</label>
<DatePicker className="textbox form-input px-4 py-2 rounded" name="publicUntil" onChange={(newDate) => handleDateChange('publicUntil', newDate)}
value={item && item.publicUntil ? dayjs(item.publicUntil) : null}
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="content">
Съдържание
</label>
<textarea className={`textbox form-input px-4 py-2 rounded ${editMode ? 'opacity-50 cursor-not-allowed' : ''}`} id="content" name="content" onChange={handleChange} value={item?.content} autoComplete="off" disabled={editMode} />
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="answers">
Отговори
</label>
<input className={`textbox form-input px-4 py-2 rounded ${editMode ? 'opacity-50 cursor-not-allowed' : ''}`} id="answers" name="answers" type="text" onChange={handleChange} value={item?.answers} autoComplete="off" disabled={editMode}
/>
</div>
{/* show count of each answer and the total answered/unanswered messages */}
{item?.answersCount?.length > 0 && (
<div className="mb-4">
<h3 className="text-lg font-semibold mb-2">Отговори:</h3>
{item.answersCount.map((answer, index) => {
const currentCount = item.messages ? item.messages.filter((message) => message.answer === answer).length : 0;
const totalCount = item.messages ? item.messages.length : 0;
const percentage = totalCount > 0 ? (currentCount / totalCount) * 100 : 0;
const ids = getIdsForAnswer(answer);
const names = getNamesByIds(ids);
return (
<div key={index} className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={`answer-${index}`}>
{answer}
</label>
<div className="relative h-6 w-full bg-gray-200 rounded" title={names}
onClick={() => copyToClipboard(names)} style={{ cursor: 'copy' }}>
<div className="absolute h-full bg-blue-600 rounded" style={{ width: `${percentage}%` }}></div>
<div className="absolute inset-0 flex items-center justify-center text-white font-bold">
{currentCount} ({percentage.toFixed(1)}%)
</div>
</div>
</div>
);
})}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">Общо отговорили</label>
<div className="relative h-6 w-full bg-gray-200 rounded" title={getNamesByIds(getIdsForAnswered())}
onClick={() => copyToClipboard(getNamesByIds(getIdsForAnswered()))} style={{ cursor: 'copy' }}
>
<div className="absolute h-full bg-green-600 rounded" style={{ width: `${item.messages ? (item.messages.filter((message) => message.answer).length / item.messages.length) * 100 : 0}%` }}></div>
<div className="absolute inset-0 flex items-center justify-center text-white font-bold">
{item.messages ? item.messages.filter((message) => message.answer).length : 0}
</div>
</div>
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">Общо неотговорили</label>
<div className="relative h-6 w-full bg-gray-200 rounded" title={getNamesByIds(getIdsForUnanswered())}
style={{ cursor: 'copy' }}
onClick={() => copyToClipboard(getNamesByIds(getIdsForUnanswered()))}>
<div className="absolute h-full bg-red-600 rounded" style={{ width: `${item.messages ? (item.messages.filter((message) => !message.answer).length / item.messages.length) * 100 : 0}%` }}></div>
<div className="absolute inset-0 flex items-center justify-center text-white font-bold">
{item.messages ? item.messages.filter((message) => !message.answer).length : 0}
</div>
</div>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => { handleSendNotificationsToAllUnanswered(item?.content) }}>
Подсети ВСИЧКИ неотховорили с нотификация
</button>
<div className="mt-2">
{getIdsForUnanswered().map((id) => {
const pub = pubs.find((p) => p.id === id);
const name = pub ? `${pub.firstName} ${pub.lastName}` : '???';
return (
<button
key={id}
// className="block mt-1 px-1 py-0.5 bg-orange-500 text-white rounded"
className="mt-1 px-2 py-1 bg-green-500 text-white rounded text-xs"
onClick={() => sendIndividualNotification(id, item?.content)}>
{name}
</button>
);
})}
</div>
</div>
</div>
)}
<div className="flex items-center justify-between">
{editMode && (<>
<button className="button btn-outline bg-red-500 hover:bg-red-700 focus:outline-none focus:shadow-outline" type="button" onClick={handleDelete}>
Изтрий
</button>
<button className="button btn-outline bg-red-500 hover:bg-red-700 focus:outline-none focus:shadow-outline" type="button" onClick={handleDeleteAnswers}>
Изтрий отговорите
</button>
</>)}
<button className="btn bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded transition duration-300" type="submit">
Запази
</button>
</div>
</form >
</div >
);
}
export default SurveyForm;