Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/app.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions src/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ export class Bot {
}
if (action === 'g') {
const active=Object.entries(e.guests).filter(([uid])=>Number(uid)!==e.owner && invitationAvailable(e,Number(uid))).map(([,g])=>g);
const unopened=Object.entries(e.invitees || {}).filter(([token])=>!active.some(g=>g.invitationToken===token)).map(([,g])=>g);
const unopened=Object.entries(e.invitees || {}).filter(([token])=>!active.some(g=>g.invitationToken===token)).map(([token,g])=>({...g,invitationToken:token}));
const accepted=active.filter(g=>confirmed(e,g));
const pending=active.filter(g=>g.status==='yes' && requiresApproval(e) && g.approval!=='approved');
const payment=active.filter(g=>g.status==='yes' && !confirmed(e,g) && !pending.includes(g));
Expand All @@ -699,9 +699,11 @@ export class Bot {
['📭 Not responded',[...active.filter(g=>!hasRecordedResponse(g)),...unopened]],
['⏳ Awaiting approval',pending],...(payment.length ? [['⭐ Awaiting payment',payment]] : [])
];
let text = `👥 ${e.title}\n${this.updatedAt(e, id)}\n\nTotal invitations: ${active.length+unopened.length}\nPeople accepted: ${accepted.reduce((sum,g)=>sum+participantCount(e,g),0)}\n`;
const people=group=>group.reduce((sum,g)=>sum+participantCount(e,g),0);
let text = `👥 ${e.title}\n${this.updatedAt(e, id)}\n\nTotal people: ${people([...active,...unopened])}\nPeople accepted: ${people(accepted)}\n`;
for (const [label,group] of groups) {
text += `\n${label} (${group.length})\n`;
const count=people(group);
text += `\n${label} (${count} ${count===1?'person':'people'})\n`;
text += group.length ? group.map(g => `• ${g.name}${g.status === 'yes' ? ' (' + participantCount(e, g) + ' people)' : ''}${g.comment ? ' — ' + g.comment : ''}`).join('\n') + '\n' : 'Nobody yet\n';
}
return this.long(id, text, keyboard([button('↻ Refresh', `g:${eid}`),button('Back to event', `v:${eid}`)]));
Expand Down
2 changes: 1 addition & 1 deletion src/mini-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function publicEvent(e, id, username) {
return {
invitationMode:invitationMode(e),guestName:e.owner!==id ? e.guests[id]?.name || null : null,
askPhone:asksPhone(e),askComments:asksComments(e),
...(manager ? {guestRoster:[...Object.entries(e.guests).filter(([uid])=>Number(uid)!==e.owner).map(([uid,g])=>({id:Number(uid),name:g.name,status:g.status,responded:hasRecordedResponse(g,invitationHistory(e,g.invitationToken),Number(uid)),approval:g.status==='yes' ? requiresApproval(e) ? g.approval || 'pending' : 'approved' : null,confirmed:confirmed(e,g),participants:g.status==='yes'?participantCount(e,g):0,paymentStatus:g.payment?.status || null})),...Object.entries(e.invitees || {}).filter(([token])=>!Object.values(e.guests).some(g=>g.invitationToken===token)).map(([,g])=>({id:null,name:g.name,status:'unopened',responded:false,participants:0,confirmed:false}))]} : {}),
...(manager ? {guestRoster:[...Object.entries(e.guests).filter(([uid])=>Number(uid)!==e.owner).map(([uid,g])=>({id:Number(uid),name:g.name,status:g.status,responded:hasRecordedResponse(g,invitationHistory(e,g.invitationToken),Number(uid)),approval:g.status==='yes' ? requiresApproval(e) ? g.approval || 'pending' : 'approved' : null,confirmed:confirmed(e,g),participants:participantCount(e,g),paymentStatus:g.payment?.status || null})),...Object.entries(e.invitees || {}).filter(([token])=>!Object.values(e.guests).some(g=>g.invitationToken===token)).map(([token,g])=>({id:null,name:g.name,status:'unopened',responded:false,participants:participantCount(e,{...g,invitationToken:token}),confirmed:false}))]} : {}),
inviteMessage:e.inviteMessage || '',qrEnabled:e.qrEnabled!==false,oneTimeInvite:oneTimeInvites(e),
...(manager ? {invitees:managedInvitations(e,username),revokedInvitees:revokedInvitations(e,username),invitationsVersion:invitationsVersion(e),cohosts,cohost:cohosts[0] || null} : {}),
...(owner ? {cohostLinks:entries.map(entry=>({id:entry.id,label:entry.label,status:entry.status,createdAt:entry.createdAt,cohost:entry.cohost,url:cohostLink(e,username,entry.id),...(entry.revokedAt ? {revokedAt:entry.revokedAt}:{})})),cohostInviteUrl:cohostLink(e,username),cohostVersion:cohostVersion(e)} : {}),
Expand Down
24 changes: 17 additions & 7 deletions test/bot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,30 @@ test('guest list counts invitations, attendees and unanswered named links withou
8:{name:'Revoked',status:'yes',invitationToken:'removed'}
};
await f.cb(1,`g:${e.id}`);let text=f.calls.at(-1).text;
assert.match(text,/Total invitations: 7\nPeople accepted: 5/);
assert.match(text,/Accepted \(2\)/);assert.match(text,/Awaiting approval \(0\)/);
for(const label of ['Rejected','Tentative','Respond later'])assert.ok(text.includes(label+' (1)'));
assert.match(text,/Not responded \(2\)\n• Opened\n• Unopened/);assert.doesNotMatch(text,/Revoked|• Host/);
assert.match(text,/Total people: 10\nPeople accepted: 5/);
assert.match(text,/Accepted \(5 people\)/);assert.match(text,/Awaiting approval \(0 people\)/);
for(const label of ['Rejected','Tentative','Respond later'])assert.ok(text.includes(label+' (1 person)'));
assert.match(text,/Not responded \(2 people\)\n• Opened\n• Unopened/);assert.doesNotMatch(text,/Revoked|• Host/);
e.guests[6]={...e.guests[6],status:'no',responseRecorded:true};await f.cb(1,`g:${e.id}`);
text=f.calls.at(-1).text;assert.match(text,/Rejected \(2\)/);assert.match(text,/Not responded \(1\)/);
text=f.calls.at(-1).text;assert.match(text,/Rejected \(2 people\)/);assert.match(text,/Not responded \(1 person\)/);
});

test('guest list keeps pending approvals and payments out of confirmed acceptance counts',async()=>{
const f=fixture(),e=await f.create();e.requireApproval=true;e.starPrice=10;e.askParticipantCount=true;
e.guests={2:{name:'Confirmed',status:'yes',approval:'approved',participants:2,payment:{status:'paid'}},3:{name:'Pending',status:'yes'},4:{name:'Unpaid',status:'yes',approval:'approved'}};
await f.cb(1,`g:${e.id}`);const text=f.calls.at(-1).text;
assert.match(text,/Total invitations: 3\nPeople accepted: 2/);
for(const label of ['Accepted','Awaiting approval','Awaiting payment'])assert.ok(text.includes(label+' (1)'));
assert.match(text,/Total people: 4\nPeople accepted: 2/);
assert.ok(text.includes('Accepted (2 people)'));
for(const label of ['Awaiting approval','Awaiting payment'])assert.ok(text.includes(label+' (1 person)'));
});

test('guest list counts reserved people in declined, tentative and unopened invitations',async()=>{
const f=fixture(),e=await f.create();e.invitationMode='named';
e.invitees={a:{name:'Declined family',participants:3},b:{name:'Maybe pair',participants:2},c:{name:'Unopened group',participants:4}};
e.guests={2:{name:'Declined family',status:'no',invitationToken:'a'},3:{name:'Maybe pair',status:'maybe',invitationToken:'b'}};
const before=JSON.stringify(e);await f.cb(1,`g:${e.id}`);const text=f.calls.at(-1).text;
assert.match(text,/Total people: 9/);assert.match(text,/Rejected \(3 people\)/);assert.match(text,/Tentative \(2 people\)/);assert.match(text,/Not responded \(4 people\)/);
assert.equal(JSON.stringify(e),before);
});

test('event card offers cancellation before optional deletion and keeps records by default',async()=>{
Expand Down
Loading