Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5625b76ec |
@ -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 = {};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user