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,) {
|
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) {
|
||||||
let where = {};
|
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 SectionMain from '../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||||
import BaseIcon from "../components/BaseIcon";
|
import BaseIcon from "../components/BaseIcon";
|
||||||
|
import BaseDivider from "../components/BaseDivider";
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ import { hasPermission } from "../helpers/userPermissions";
|
|||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
||||||
|
import MassAssignmentDashboard from "../components/MassAssignment/MassAssignmentDashboard";
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
@ -99,6 +101,8 @@ const Dashboard = () => {
|
|||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
|
<MassAssignmentDashboard />
|
||||||
|
<BaseDivider />
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton
|
||||||
icon={icon.mdiChartTimelineVariant}
|
icon={icon.mdiChartTimelineVariant}
|
||||||
title='Overview'
|
title='Overview'
|
||||||
|
|||||||
@ -128,12 +128,12 @@ export default function Starter() {
|
|||||||
: null}
|
: null}
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
<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'>
|
<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">
|
<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'>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'>For guides and documentation please check
|
<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">Flatlogic documentation</a></p>
|
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation"> </a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user