Staff, supervisor and admin dashboards start with what's on today (checked in vs places, children in now, volunteers here; money in today and this week for supervisors) and, for supervisors and admins, what's waiting for someone (unpaid registrations, consent, waitlists, cover requests, roster gaps; for admins cashups, reconciling, privacy requests, failed webhooks), each linking to where it's dealt with. Ctrl+K opens a palette of pages, settings, reports and actions for the role, with search: events, payments by amount, people and registrations (admins). All campus-scoped. Bulk actions and saved filters follow as 1.49.1 and 1.49.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
4.4 KiB
JavaScript
87 lines
4.4 KiB
JavaScript
const count = jest.fn().mockResolvedValue(2);
|
|
const mockDb = {
|
|
registration: { count, findMany: jest.fn().mockResolvedValue([]) },
|
|
consentRequest: { findMany: jest.fn().mockResolvedValue([]) },
|
|
waitlistEntry: { count },
|
|
volunteerSwap: { count },
|
|
servingRole: { findMany: jest.fn().mockResolvedValue([{ needed: 3, _count: { slots: 1 } }, { needed: 2, _count: { slots: 2 } }]) },
|
|
event: { count, findMany: jest.fn().mockResolvedValue([]) },
|
|
providerTransaction: { count },
|
|
dataRequest: { count },
|
|
webhookDelivery: { count },
|
|
payment: { findMany: jest.fn().mockResolvedValue([]) },
|
|
user: { findMany: jest.fn().mockResolvedValue([]) },
|
|
};
|
|
jest.mock('../src/config/db', () => mockDb);
|
|
const mockScope = jest.fn();
|
|
jest.mock('../src/utils/campusScope', () => ({
|
|
scopeFor: (...a) => mockScope(...a),
|
|
eventWhere: scope => (scope ? { OR: [{ campusId: { in: scope.campusIds } }, { campusId: null }] } : {}),
|
|
}));
|
|
|
|
const dashboard = require('../src/utils/dashboard');
|
|
|
|
beforeEach(() => jest.clearAllMocks());
|
|
|
|
test('an amount typed in the palette', () => {
|
|
expect(dashboard.amountOf('R450')).toBe(450);
|
|
expect(dashboard.amountOf('r 450,50')).toBe(450.5);
|
|
expect(dashboard.amountOf('450.5')).toBe(450.5);
|
|
expect(dashboard.amountOf('Camp 2026')).toBeNull();
|
|
expect(dashboard.amountOf('')).toBeNull();
|
|
});
|
|
|
|
describe('what is waiting', () => {
|
|
test('admins see everything they can act on', async () => {
|
|
const items = await dashboard.pendingItems({ user: { role: 'admin' } }, null);
|
|
expect(items.map(i => i.key)).toEqual(['privacy', 'unpaid', 'cover', 'roster', 'waitlist', 'reconcile', 'cashup', 'webhooks']);
|
|
// Only roles still short of people count.
|
|
expect(items.find(i => i.key === 'roster').count).toBe(1);
|
|
expect(items.find(i => i.key === 'unpaid').href).toBe('/dashboard/admin/registrations?status=pending');
|
|
});
|
|
|
|
test('a campus-limited supervisor: no cashups, provider payments, privacy or webhooks; only their events', async () => {
|
|
const scope = { campusIds: ['north'] };
|
|
const items = await dashboard.pendingItems({ user: { role: 'supervisor' } }, scope);
|
|
expect(items.map(i => i.key)).toEqual(['unpaid', 'cover', 'roster', 'waitlist']);
|
|
expect(items.find(i => i.key === 'unpaid').href).toBe('/dashboard/supervisor/reports?report=regStatus');
|
|
const where = mockDb.registration.count.mock.calls[0][0].where;
|
|
expect(where.event.AND[0]).toEqual({ OR: [{ campusId: { in: ['north'] } }, { campusId: null }] });
|
|
});
|
|
|
|
test('items with nothing waiting are left out', async () => {
|
|
count.mockResolvedValue(0);
|
|
mockDb.servingRole.findMany.mockResolvedValueOnce([]);
|
|
expect(await dashboard.pendingItems({ user: { role: 'admin' } }, null)).toEqual([]);
|
|
count.mockResolvedValue(2);
|
|
});
|
|
});
|
|
|
|
describe('palette search', () => {
|
|
test('too short a query finds nothing', async () => {
|
|
expect(await dashboard.search({ user: { role: 'admin' } }, 'a')).toEqual([]);
|
|
});
|
|
|
|
test('staff get events only, linked to the public page', async () => {
|
|
mockScope.mockResolvedValue(null);
|
|
mockDb.event.findMany.mockResolvedValueOnce([{ id: 'e1', title: 'Camp 2026', startDate: new Date('2026-03-01'), isActive: true, campus: null }]);
|
|
const out = await dashboard.search({ user: { role: 'staff' } }, 'camp');
|
|
expect(out).toEqual([expect.objectContaining({ type: 'event', href: '/events/e1' })]);
|
|
expect(mockDb.user.findMany).not.toHaveBeenCalled();
|
|
expect(mockDb.payment.findMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('supervisors find payments by amount; admins also people', async () => {
|
|
mockScope.mockResolvedValue(null);
|
|
mockDb.payment.findMany.mockResolvedValueOnce([{ id: 'p1', amount: 450, method: 'card', createdAt: new Date(), registrationId: 'r1', user: { name: 'Thandi' }, event: { title: 'Camp' } }]);
|
|
const sup = await dashboard.search({ user: { role: 'supervisor' } }, 'R450');
|
|
expect(sup.find(r => r.type === 'payment')).toMatchObject({ title: 'R450.00 — Thandi', href: '/dashboard/supervisor/payments' });
|
|
expect(mockDb.payment.findMany.mock.calls[0][0].where.amount).toEqual({ in: [450, -450] });
|
|
expect(mockDb.user.findMany).not.toHaveBeenCalled();
|
|
|
|
mockDb.user.findMany.mockResolvedValueOnce([{ id: 'u1', name: 'Thandi Test', email: 't@test.local', role: 'user' }]);
|
|
const adm = await dashboard.search({ user: { role: 'admin' } }, 'thandi');
|
|
expect(adm.find(r => r.type === 'person')).toMatchObject({ href: '/dashboard/admin/users?q=t%40test.local' });
|
|
});
|
|
});
|