Compare commits

..

No commits in common. "main" and "invitations-download-pdf" have entirely different histories.

10 changed files with 381 additions and 416 deletions

2
.nvmrc
View File

@ -1 +1 @@
24.8.0
24.3.0

View File

@ -8,7 +8,7 @@ import Arrangement from '@/app/ui/arrangements/arrangement';
import ArrangementsTable from '@/app/ui/arrangements/arrangements-table';
import { classNames } from '@/app/ui/components/button';
import { Toast } from 'primereact/toast';
import React, { useEffect, useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
export default function Page() {
const toast = useRef<Toast>(null);

View File

@ -10,8 +10,6 @@ export interface Api<T extends Entity> {
create(serializable: Serializable<T>, object: T, callback: (object: T) => void): void;
update(serializable: Serializable<T>, object: T, callback: () => void): void;
destroy(serializable: Serializable<T>, object: T, callback: () => void): void;
post(serializable: Serializable<T>, path: string, callback: () => void): void;
}
export interface Serializable<T> {
@ -100,14 +98,4 @@ export class AbstractApi<T extends Entity> implements Api<T> {
}).then(callback)
.catch((error) => console.error(error));
}
post(serializable: Serializable<T>, path: string, callback: () => void): void {
fetch(`/api/${getSlug()}/${serializable.apiPath()}/${path}`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': getCsrfToken(),
}
}).then(callback)
.catch((error) => console.error(error));
}
}

View File

@ -13,8 +13,6 @@ export function loadTableSimulations(onLoad?: (tableSimulations: TableArrangemen
name: record.name,
discomfort: record.discomfort,
valid: record.valid,
progress: record.progress,
status : record.status
});
}));
}, (error) => {

View File

@ -14,8 +14,6 @@ export type TableArrangement = {
guests?: Guest[];
discomfort?: number;
valid?: boolean;
progress: number;
status: 'in_progress' | 'completed' | 'not_started';
}
export type User = {

View File

@ -22,12 +22,10 @@ export type Table = {
export class TableSimulation implements Entity {
id?: string;
tables: Table[];
progress: number;
constructor(id?: string, tables?: Table[], progress?: number) {
constructor(id?: string, tables?: Table[]) {
this.id = id;
this.tables = tables || [];
this.progress = progress || 0;
}
}
@ -45,7 +43,7 @@ export class TableSimulationSerializer implements Serializable<TableSimulation>
}
},
}
}), data.progress);
}));
}
toJson(simulation: TableSimulation): string {

View File

@ -10,21 +10,11 @@ import { loadTableSimulations } from "@/app/api/tableSimulations";
import { ArchiveBoxXMarkIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
import { Tooltip } from "primereact/tooltip";
import clsx from "clsx";
import { ProgressBar } from "primereact/progressbar";
import { useEffect } from "react";
import { TableSimulation, TableSimulationSerializer } from "@/app/lib/tableSimulation";
import { AbstractApi } from "@/app/api/abstract-api";
export default function ArrangementsTable({ onArrangementSelected }: { onArrangementSelected: (arrangementId: string) => void }) {
const [arrangements, setArrangements] = useState<Array<TableArrangement>>([]);
const [arrangementsLoaded, setArrangementsLoaded] = useState(false);
useEffect(() => {
refreshSimulations();
const interval = setInterval(refreshSimulations, 10000);
return () => clearInterval(interval);
}, []);
function refreshSimulations() {
loadTableSimulations((arrangements) => {
setArrangements(arrangements);
@ -36,9 +26,11 @@ export default function ArrangementsTable({ onArrangementSelected }: { onArrange
onArrangementSelected(e.currentTarget.getAttribute('data-arrangement-id') || '');
}
!arrangementsLoaded && refreshSimulations();
return (
<TableOfContents
headers={['Name', 'Discomfort', 'Status', 'Actions']}
headers={['Name', 'Discomfort', 'Actions', 'Status']}
caption='Simulations'
elements={arrangements}
rowRender={(arrangement) => (
@ -52,19 +44,18 @@ export default function ArrangementsTable({ onArrangementSelected }: { onArrange
<td className="px-6 py-4">
{arrangement.discomfort}
</td>
<td className="px-4">
<Tooltip target=".tooltip-status" />
<>
{ arrangement.valid && arrangement.status === 'not_started' && <ProgressBar mode="indeterminate" style={{ height: '6px' }}></ProgressBar> }
{ arrangement.valid && arrangement.status !== 'not_started' && <ProgressBar value={(100 * arrangement.progress).toFixed(2) }></ProgressBar> }
{ !arrangement.valid && 'The list of potential guests has changed since this simulation.' }
</>
</td>
<td>
<button data-arrangement-id={arrangement.id} onClick={arrangementClicked} className={classNames('primary')}>Load</button>
</td>
<td>
<Tooltip target=".tooltip-status" />
{
arrangement.valid ?
<CheckBadgeIcon className='size-6 tooltip-status' data-pr-position="right" data-pr-tooltip="Simulation is valid" /> :
<ArchiveBoxXMarkIcon className='size-6 tooltip-status' data-pr-position="right" data-pr-tooltip="Simulation is expired due to attendance or affinity changes" />
}
</td>
</tr>
)}
/>

View File

@ -11,7 +11,6 @@ import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
import { useEffect, useRef } from "react";
import { useState } from "react";
import { classNames } from "../components/button";
import { Toast } from "primereact/toast";
function InvitationCard({ invitation, allGuests, onGuestAdded, onDestroy }: {
invitation: Invitation,
@ -118,7 +117,6 @@ export default function InvitationsBoard({ guests, invitations: originalInvitati
guests: Array<Guest>,
invitations: Array<Invitation>
}) {
const toast = useRef<Toast>(null);
const api = new AbstractApi<Invitation>();
const serializer = new InvitationSerializer();
@ -148,19 +146,14 @@ export default function InvitationsBoard({ guests, invitations: originalInvitati
}
function handleDownloadQrCodes() {
api.post(serializer, 'email', () => {
toast.current?.show({
severity: 'success',
summary: 'Email scheduled',
detail: 'A document with the QR codes will be sent to the organizer\'s email.'
api.getAllPdf(serializer, () => {
console.log("QR codes downloaded");
});
})
}
return (
<div className="flex h-screen">
{/* Left Column: Guests */}
<Toast ref={toast} />
<div className="w-1/4 h-full overflow-auto border-r border-gray-300 p-4">
<h2 className="text-lg font-semibold mb-4">{unassignedGuests.length} guests without invitation</h2>
<div>
@ -187,7 +180,7 @@ export default function InvitationsBoard({ guests, invitations: originalInvitati
onClick={handleDownloadQrCodes}
className={classNames('primary')}
>
Send QR codes via email
Download QR codes
</button>

View File

@ -16,7 +16,7 @@
"bcrypt": "^6.0.0",
"clsx": "^2.1.1",
"dompurify": "^3.2.6",
"next": "15.4.5",
"next": "15.3.4",
"next-auth": "5.0.0-beta.29",
"postcss": "8.5.6",
"primeicons": "^7.0.0",
@ -27,12 +27,12 @@
"typescript": "5.8.3",
"use-debounce": "^10.0.1",
"uuid": "11.1.0",
"zod": "^4.0.0"
"zod": "^3.23.8"
},
"devDependencies": {
"@playwright/test": "^1.52.0",
"@types/bcrypt": "^5.0.2",
"@types/node": "24.3.1",
"@types/node": "24.0.7",
"@types/react": "18.3.23",
"@types/react-dom": "18.3.7",
"wait-on": "^8.0.3"
@ -40,5 +40,5 @@
"engines": {
"node": ">=23.0.0"
},
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
}

717
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff