Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

291 recurring events #414

Open
wants to merge 13 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion server/mongodb/models/Event/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const eventInputClientValidator = z.object({
eventParentId: z.instanceof(Types.ObjectId),
isEnded: z.boolean().optional(),
});

export const eventPopulatedInputClientValidator = (minMaxVolunteers?: number) =>
z.object({
date: z.coerce.date(),
Expand Down
3 changes: 3 additions & 0 deletions server/mongodb/models/EventParent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const eventParentSchema = new Schema(
orgState: String,
orgZip: String,
description: String,
isRecurring: [{ type: Boolean, default: false }],
isRecurringString: { type: String, default: "Never" },
recurrenceEndDate: { type: Date, default: false },
},
{ timestamps: true }
);
Expand Down
6 changes: 6 additions & 0 deletions server/mongodb/models/EventParent/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export const eventParentInputClientValidator = (minMaxVolunteers?: number) =>
//.regex(/^[0-9]{5}$/, "orgZip must be a five-digit number")
.optional(),
description: z.string().optional(),
isRecurring: z.array(z.boolean()).optional(),
isRecurringString: z.string().optional(),
recurrenceEndDate: z.coerce.date().optional(),
});

export const eventParentInputServerValidator = z.object({
Expand Down Expand Up @@ -105,6 +108,9 @@ export const eventParentInputServerValidator = z.object({
//.regex(/^[0-9]{5}$/, "orgZip must be a five-digit number")
.optional(),
description: z.string().optional(),
isRecurring: z.array(z.boolean()).optional(),
isRecurringString: z.string(),
recurrenceEndDate: z.coerce.date().optional(),
});

export type EventParentInputClient = z.infer<
Expand Down
1 change: 0 additions & 1 deletion src/components/EventCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ const EventCard = (props) => {
hours = ((hours + 11) % 12) + 1;
return hours.toString() + ":" + min + suffix;
};

return (
<div
className={`mx-18 mb-2 flex flex-col ${
Expand Down
71 changes: 71 additions & 0 deletions src/components/Forms/SelectField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Checkbox, Label, Select, Tooltip } from "flowbite-react";
import { ErrorMessage, Field } from "formik";
import PropTypes from "prop-types";
import { InformationCircleIcon } from "@heroicons/react/24/solid";

const SelectField = (props) => (
<div className={props.className + " mb-3"}>
{props.label && (
<div className="flex flex-row">
{props.tooltip && (
<Tooltip className="flex flex-row" content={props.tooltip}>
<Label
className="mb-1 flex h-6 items-center font-medium text-slate-600"
htmlFor={props.name}
>
{props.label}
<InformationCircleIcon className="ml-1 flex w-4 text-black"></InformationCircleIcon>
</Label>
{props.isRequired && <p className="mb-0 text-red-600">*</p>}
</Tooltip>
)}
{!props.tooltip && (
<>
<Label
className="mb-1 h-6 font-medium text-slate-600"
htmlFor={props.name}
>
{props.label}
</Label>
{props.isRequired && <p className="mb-0 text-red-600">*</p>}
</>
)}
</div>
)}
<Field name={props.name}>
{({ field }) => (
<Select
class="border-1 mt-0 h-10 w-full rounded-md border-gray-300 bg-white disabled:border-gray-500 disabled:bg-gray-300"
id={props.name}
name={props.name}
{...field}
disabled={props.disabled}
onChange={props.onChange}
>
{!props.isCheckBox &&
props.options.map((option, key) => {
return <option key={key}>{option}</option>;
})}
</Select>
)}
</Field>
<ErrorMessage
component="div"
className="mt-1 inline-block pt-0 text-sm text-red-600"
name={props.name}
/>{" "}
</div>
);

SelectField.propTypes = {
name: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
label: PropTypes.string,
isRequired: PropTypes.bool,
disabled: PropTypes.bool,
className: PropTypes.string,
tooltip: PropTypes.string,
onChange: PropTypes.func,
};

export default SelectField;
24 changes: 18 additions & 6 deletions src/pages/api/events/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,27 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
.safeParse(req.body?.eventPopulatedInput);
if (!result.success)
return res.status(400).json({ error: result.error });

await eventParent.updateOne(result.data.eventParent);
delete result.data.eventParent;
await event.updateOne(result.data);
if (!req.body?.editAllRecurrences) {
const eventParent = await EventParent.create(result.data.eventParent);
console.log(eventParent._id);
await Event.updateOne(
{ _id: eventId },
{ eventParent: eventParent._id }
).then((result) => {
console.log(result);
});
} else {
await eventParent
.updateOne(result.data.eventParent)
.then((result) => {
console.log(result);
});
}
} else {
const result = eventInputServerValidator.safeParse(req.body);
if (!result.success) return res.status(400).json(result);

await event.updateOne(result.data);
await Event.updateOne(result.data);
}

if (req.body?.sendConfirmationEmail) {
Expand All @@ -79,7 +91,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
case "DELETE": {
await Attendance.deleteMany({ eventId: event._id });
await Registration.deleteMany({ eventId: event._id });
await event.deleteOne();
await Event.deleteOne();

const eventParentId = event.eventParent;
if ((await Event.count({ eventParent: eventParentId })) === 0) {
Expand Down
51 changes: 51 additions & 0 deletions src/pages/api/events/[id]/deleteAllRecurring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getServerSession } from "next-auth/next";
import { NextApiRequest, NextApiResponse } from "next/types";
import { createHistoryEventDeleteEvent } from "../../../../../server/actions/historyEvent";
import dbConnect from "../../../../../server/mongodb";
import Attendance from "../../../../../server/mongodb/models/Attendance";
import Event, {
EventDocument,
} from "../../../../../server/mongodb/models/Event";
import EventParent from "../../../../../server/mongodb/models/EventParent";
import Registration from "../../../../../server/mongodb/models/Registration";
import { authOptions } from "../../auth/[...nextauth]";

export default async (req: NextApiRequest, res: NextApiResponse) => {
await dbConnect();
const eventId = req.query.id as string;

const event = await Event.findById(eventId);
if (!event)
return res
.status(404)
.json({ error: `Event with id ${eventId} not found` });

const eventParent = await EventParent.findById(event.eventParent);
if (!eventParent)
return res.status(404).json({
error: `Event with id ${eventId} has no EventParent`,
});

const session = await getServerSession(req, res, authOptions);
if (!session?.user)
return res.status(400).json({ error: "User session not found" });
const user = session.user;

const eventParentId = event.eventParent;
const currDate = event.date;
//deleting all attendance and registration info
await Event.find({ eventParent: eventParentId }).then(
async (docsToDelete) => {
const docIds = docsToDelete.map((doc) => doc._id);
await Attendance.deleteMany({ eventId: { $in: docIds } });
await Registration.deleteMany({ eventId: { $in: docIds } });
}
);

await Event.deleteMany({ eventParent: eventParentId });
await EventParent.findByIdAndDelete(eventParentId);

await createHistoryEventDeleteEvent(user, event, eventParent);

return res.status(204).end();
};
23 changes: 23 additions & 0 deletions src/pages/api/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,33 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
.json({ error: "User session not found to create event" });
const eventParent = await EventParent.create(result.data.eventParent);

// creates first event
const event = await Event.create({
date: result.data.date,
eventParent: eventParent._id,
});
if (
eventParent.isRecurring.includes(true) &&
eventParent.recurrenceEndDate
) {
//since we already created this event for the day we can increment this.
const currDate = event.date;
currDate.setDate(currDate.getDate() + 1);
const recurrence = eventParent.isRecurring;
const recurringEvents = [];
while (currDate < eventParent.recurrenceEndDate) {
for (let i = currDate.getDay(); i < recurrence.length; i++) {
if (recurrence[i]) {
const recurringEvent = await Event.create({
date: result.data.date,
eventParent: eventParent._id,
});
recurringEvents.push(recurringEvent);
}
currDate.setDate(currDate.getDate() + 1);
}
}
}

const user = session.user;
await createHistoryEventCreateEvent(user, event, eventParent);
Expand Down
9 changes: 8 additions & 1 deletion src/queries/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ export const createChildEvent = (eventInput: EventInputClient) =>
export const updateEvent = (
eventId: Types.ObjectId,
eventPopulatedInput: Partial<EventPopulatedInputClient>,
editAllRecurrences: boolean,
sendConfirmationEmail = true
) =>
axios.put<{
event?: EventPopulatedDocument;
event?: Omit<EventPopulatedDocument, "isRecurringString">;
error?: ZodError | string;
}>(`/api/events/${eventId.toString()}`, {
eventPopulatedInput,
editAllRecurrences,
sendConfirmationEmail,
});

Expand Down Expand Up @@ -90,3 +92,8 @@ export const deleteEvent = (eventId: Types.ObjectId) =>
axios.delete<{ error?: ZodError | string }>(
`/api/events/${eventId.toString()}`
);

export const deleteAllRecurringEvents = (eventId: Types.ObjectId) =>
axios.delete<{ error?: ZodError | string }>(
`/api/events/${eventId.toString()}/deleteAllRecurring`
);
40 changes: 30 additions & 10 deletions src/screens/Events/Admin/EventDeleteModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,50 @@ import PropTypes from "prop-types";
import { useState } from "react";
import { Modal, ModalBody, ModalFooter, ModalHeader } from "reactstrap";
import BoGButton from "../../../components/BoGButton";
import { deleteEvent } from "../../../queries/events";
import { deleteAllRecurringEvents, deleteEvent } from "../../../queries/events";

const EventDeleteModal = ({ open, toggle, event, onEventDelete }) => {
const [isDeleting, setDeleting] = useState(false);
const {
data: { user },
} = useSession();

const handleSubmit = () => {
const [hasRecurrence, setHasRecurrence] = useState(
event.eventParent?.isRecurring?.includes(true) ?? false
);
const handleSubmit = (deleteAllRecurrences) => {
setDeleting(true);
onEventDelete(event._id);
deleteEvent(event._id).then(() => {
toggle();
setDeleting(false);
});
if (deleteAllRecurrences) {
deleteAllRecurringEvents(event._id).then(() => {
toggle();
setDeleting(false);
onEventDelete(event._id);
});
} else {
deleteEvent(event._id).then(() => {
toggle();
setDeleting(false);
onEventDelete(event._id);
});
}
};

return (
<Modal isOpen={open} toggle={toggle} backdrop="static">
<ModalHeader toggle={toggle}>Delete Event</ModalHeader>
<ModalBody>Are you sure you want to delete this event?</ModalBody>
<ModalFooter>
<BoGButton text="Cancel" onClick={toggle} outline={true} />
<BoGButton text="Delete" onClick={handleSubmit} disabled={isDeleting} />
<BoGButton
text="Delete"
onClick={() => handleSubmit(false)}
disabled={isDeleting}
/>
{hasRecurrence && (
<BoGButton
text="Delete All Recurrences"
onClick={() => handleSubmit(true)}
disabled={isDeleting}
/>
)}
</ModalFooter>
</Modal>
);
Expand Down
Loading