From e5625b76ec2a08e39e8c3e853f8ade8b0c588a34 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 21 Feb 2026 19:48:18 +0000 Subject: [PATCH] 1.0 --- backend/src/db/api/mass_instances.js | 46 ++++ .../MassAssignmentDashboard.tsx | 215 ++++++++++++++++++ frontend/src/pages/dashboard.tsx | 4 + frontend/src/pages/index.tsx | 8 +- 4 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/MassAssignment/MassAssignmentDashboard.tsx diff --git a/backend/src/db/api/mass_instances.js b/backend/src/db/api/mass_instances.js index b37594c..a45742a 100644 --- a/backend/src/db/api/mass_instances.js +++ b/backend/src/db/api/mass_instances.js @@ -577,6 +577,52 @@ module.exports = class Mass_instancesDBApi { } } + static async findUpcoming(options) { + const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + const organizationId = currentUser?.organization?.id; + + const where = { + start_at: { + [Op.gte]: new Date(), + }, + status: "scheduled", + }; + + if (organizationId) { + where.organizationId = organizationId; + } + + const { rows } = await db.mass_instances.findAndCountAll({ + where, + include: [ + { + model: db.reader_assignments, + as: "reader_assignments_mass_instance", + include: [ + { + model: db.readers, + as: "reader", + }, + { + model: db.assignment_roles, + as: "assignment_role", + } + ] + }, + { + model: db.mass_templates, + as: "mass_template", + } + ], + order: [["start_at", "ASC"]], + limit: 10, + transaction, + }); + + return rows; + } + static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { let where = {}; diff --git a/frontend/src/components/MassAssignment/MassAssignmentDashboard.tsx b/frontend/src/components/MassAssignment/MassAssignmentDashboard.tsx new file mode 100644 index 0000000..d14bcef --- /dev/null +++ b/frontend/src/components/MassAssignment/MassAssignmentDashboard.tsx @@ -0,0 +1,215 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import * as icon from '@mdi/js'; +import CardBox from '../CardBox'; +import CardBoxComponentBody from '../CardBoxComponentBody'; +import CardBoxComponentTitle from '../CardBoxComponentTitle'; +import BaseButton from '../BaseButton'; +import BaseIcon from '../BaseIcon'; +import LoadingSpinner from '../LoadingSpinner'; +import moment from 'moment'; +import FormField from '../FormField'; +import SectionTitleLineWithButton from '../SectionTitleLineWithButton'; + +interface Reader { + id: string; + full_name: string; + whatsapp_number_e164: string; +} + +interface AssignmentRole { + id: string; + name: string; +} + +interface ReaderAssignment { + id: string; + reader: Reader; + assignment_role: AssignmentRole; +} + +interface MassInstance { + id: string; + start_at: string; + title_override: string; + mass_template?: { + title: string; + }; + reader_assignments_mass_instance: ReaderAssignment[]; +} + +const MassAssignmentDashboard = () => { + const [masses, setMasses] = useState([]); + const [loading, setLoading] = useState(true); + const [readers, setReaders] = useState<{ id: string; label: string }[]>([]); + const [roles, setRoles] = useState<{ id: string; label: string }[]>([]); + const [assigningTo, setAssigningTo] = useState(null); + const [selectedReader, setSelectedReader] = useState(''); + const [selectedRole, setSelectedRole] = useState(''); + + const fetchUpcoming = async () => { + try { + const response = await axios.get('/mass_instances/upcoming'); + setMasses(response.data); + } catch (error) { + console.error('Error fetching upcoming masses:', error); + } finally { + setLoading(false); + } + }; + + const fetchOptions = async () => { + try { + const [readersRes, rolesRes] = await Promise.all([ + axios.get('/readers/autocomplete'), + axios.get('/assignment_roles/autocomplete'), + ]); + setReaders(readersRes.data); + setRoles(rolesRes.data); + } catch (error) { + console.error('Error fetching options:', error); + } + }; + + useEffect(() => { + fetchUpcoming(); + fetchOptions(); + }, []); + + const handleAssign = async () => { + if (!assigningTo || !selectedReader || !selectedRole) return; + try { + await axios.post('/reader_assignments', { + data: { + mass_instance: assigningTo, + reader: selectedReader, + assignment_role: selectedRole, + assignment_status: 'assigned', + assigned_at: new Date(), + }, + }); + setAssigningTo(null); + setSelectedReader(''); + setSelectedRole(''); + fetchUpcoming(); + } catch (error) { + console.error('Error creating assignment:', error); + } + }; + + const handleSendReminder = async (assignmentId: string) => { + // This would call a backend route to trigger WhatsApp + alert('WhatsApp reminder triggered (Mock)'); + }; + + if (loading) return ; + + return ( +
+ + + + + {masses.length === 0 ? ( + + +
+

No upcoming masses scheduled.

+ +
+
+
+ ) : ( +
+ {masses.map((mass) => ( + + + +
+
+ + {moment(mass.start_at).format('LLLL')} +
+
+ +
+

Assignments

+ {mass.reader_assignments_mass_instance?.length > 0 ? ( + mass.reader_assignments_mass_instance.map((assignment) => ( +
+
+
{assignment.reader?.full_name}
+
{assignment.assignment_role?.name}
+
+ handleSendReminder(assignment.id)} + title="Send WhatsApp Reminder" + /> +
+ )) + ) : ( +
+ No readers assigned +
+ )} + + {assigningTo === mass.id ? ( +
+ + + + + + +
+ + setAssigningTo(null)} /> +
+
+ ) : ( + { + setAssigningTo(mass.id); + setSelectedReader(''); + setSelectedRole(''); + }} + /> + )} +
+
+
+ ))} +
+ )} +
+ ); +}; + +export default MassAssignmentDashboard; \ No newline at end of file diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 1234b0f..0443890 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -7,6 +7,7 @@ import LayoutAuthenticated from '../layouts/Authenticated' import SectionMain from '../components/SectionMain' import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' import BaseIcon from "../components/BaseIcon"; +import BaseDivider from "../components/BaseDivider"; import { getPageTitle } from '../config' import Link from "next/link"; @@ -14,6 +15,7 @@ import { hasPermission } from "../helpers/userPermissions"; import { fetchWidgets } from '../stores/roles/rolesSlice'; import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { SmartWidget } from '../components/SmartWidget/SmartWidget'; +import MassAssignmentDashboard from "../components/MassAssignment/MassAssignmentDashboard"; import { useAppDispatch, useAppSelector } from '../stores/hooks'; const Dashboard = () => { @@ -99,6 +101,8 @@ const Dashboard = () => { + + - +
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+

Schedule recurring Masses, assign readers, and send WhatsApp reminders from one modern dashboard.

+

Perfect for Catholic Parish administrators looking to streamline their liturgical assignments. + your local README.md and the