Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
e5625b76ec 1.0 2026-02-21 19:48:18 +00:00
4 changed files with 269 additions and 4 deletions

View File

@ -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 = {};

View File

@ -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<MassInstance[]>([]);
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<string | null>(null);
const [selectedReader, setSelectedReader] = useState<string>('');
const [selectedRole, setSelectedRole] = useState<string>('');
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 <LoadingSpinner />;
return (
<div className="space-y-6">
<SectionTitleLineWithButton icon={icon.mdiCalendarClock} title="Upcoming Masses & Assignments" main>
<BaseButton
label="Manage Readers"
color="info"
icon={icon.mdiAccountGroup}
href="/readers/readers-list"
/>
</SectionTitleLineWithButton>
{masses.length === 0 ? (
<CardBox>
<CardBoxComponentBody noPadding>
<div className="p-12 text-center text-gray-500">
<p>No upcoming masses scheduled.</p>
<BaseButton className="mt-4" label="Create Schedule" href="/mass_templates/mass_templates-list" color="info" />
</div>
</CardBoxComponentBody>
</CardBox>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{masses.map((mass) => (
<CardBox key={mass.id} className="flex flex-col h-full border-t-4 border-indigo-500">
<CardBoxComponentTitle title={mass.title_override || mass.mass_template?.title || 'Mass'} />
<CardBoxComponentBody>
<div className="mb-4">
<div className="flex items-center text-indigo-600 font-semibold mb-1">
<BaseIcon path={icon.mdiClockOutline} size={18} className="mr-2" />
<span>{moment(mass.start_at).format('LLLL')}</span>
</div>
</div>
<div className="space-y-3">
<h4 className="font-semibold text-xs uppercase tracking-wider text-gray-400">Assignments</h4>
{mass.reader_assignments_mass_instance?.length > 0 ? (
mass.reader_assignments_mass_instance.map((assignment) => (
<div key={assignment.id} className="flex justify-between items-center bg-gray-50 dark:bg-dark-800 p-3 rounded-lg border border-gray-100 dark:border-dark-700">
<div>
<div className="font-medium text-sm">{assignment.reader?.full_name}</div>
<div className="text-xs text-gray-500 font-medium">{assignment.assignment_role?.name}</div>
</div>
<BaseButton
icon={icon.mdiWhatsapp}
color="success"
small
roundedFull
onClick={() => handleSendReminder(assignment.id)}
title="Send WhatsApp Reminder"
/>
</div>
))
) : (
<div className="text-sm text-gray-400 italic py-4 border-2 border-dashed border-gray-200 dark:border-dark-700 rounded-lg text-center bg-gray-50/50 dark:bg-dark-900/50">
No readers assigned
</div>
)}
{assigningTo === mass.id ? (
<div className="mt-4 p-4 bg-indigo-50 dark:bg-dark-700 rounded-lg border border-indigo-100 dark:border-dark-600 space-y-3">
<FormField label="Reader" labelFor={`reader-${mass.id}`}>
<select
id={`reader-${mass.id}`}
value={selectedReader}
onChange={(e) => setSelectedReader(e.target.value)}
>
<option value="">Select Reader...</option>
{readers.map(r => <option key={r.id} value={r.id}>{r.label}</option>)}
</select>
</FormField>
<FormField label="Role" labelFor={`role-${mass.id}`}>
<select
id={`role-${mass.id}`}
value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value)}
>
<option value="">Select Role...</option>
{roles.map(r => <option key={r.id} value={r.id}>{r.label}</option>)}
</select>
</FormField>
<div className="flex space-x-2 pt-2">
<BaseButton label="Assign" color="info" small onClick={handleAssign} disabled={!selectedReader || !selectedRole} />
<BaseButton label="Cancel" color="white" small onClick={() => setAssigningTo(null)} />
</div>
</div>
) : (
<BaseButton
label="Assign Reader"
icon={icon.mdiAccountPlus}
color="info"
className="w-full mt-2"
onClick={() => {
setAssigningTo(mass.id);
setSelectedReader('');
setSelectedRole('');
}}
/>
)}
</div>
</CardBoxComponentBody>
</CardBox>
))}
</div>
)}
</div>
);
};
export default MassAssignmentDashboard;

View File

@ -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 = () => {
</title>
</Head>
<SectionMain>
<MassAssignmentDashboard />
<BaseDivider />
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Overview'

View File

@ -128,12 +128,12 @@ export default function Starter() {
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Mass Readers Scheduler app!"/>
<CardBoxComponentTitle title="Manage your Parish Readers with Ease"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<p className='text-center text-gray-500'>Schedule recurring Masses, assign readers, and send WhatsApp reminders from one modern dashboard. <a className={`${textColor}`} href="https://flatlogic.com/generator"> </a></p>
<p className='text-center text-gray-500'>Perfect for Catholic Parish administrators looking to streamline their liturgical assignments.
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation"> </a></p>
</div>
<BaseButtons>