Compare commits
No commits in common. "main" and "groups-specs" have entirely different histories.
main
...
groups-spe
@ -8,7 +8,7 @@ import Arrangement from '@/app/ui/arrangements/arrangement';
|
|||||||
import ArrangementsTable from '@/app/ui/arrangements/arrangements-table';
|
import ArrangementsTable from '@/app/ui/arrangements/arrangements-table';
|
||||||
import { classNames } from '@/app/ui/components/button';
|
import { classNames } from '@/app/ui/components/button';
|
||||||
import { Toast } from 'primereact/toast';
|
import { Toast } from 'primereact/toast';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const toast = useRef<Toast>(null);
|
const toast = useRef<Toast>(null);
|
||||||
|
@ -3,15 +3,12 @@
|
|||||||
import { Entity } from '@/app/lib/definitions';
|
import { Entity } from '@/app/lib/definitions';
|
||||||
import { getCsrfToken, getSlug } from '@/app/lib/utils';
|
import { getCsrfToken, getSlug } from '@/app/lib/utils';
|
||||||
|
|
||||||
|
|
||||||
export interface Api<T extends Entity> {
|
export interface Api<T extends Entity> {
|
||||||
getAll(serializable: Serializable<T>, callback: (objets: T[]) => void): void;
|
getAll(serializable: Serializable<T>, callback: (objets: T[]) => void): void;
|
||||||
get(serializable: Serializable<T>, id: string, callback: (object: T) => void): void;
|
get(serializable: Serializable<T>, id: string, callback: (object: T) => void): void;
|
||||||
create(serializable: Serializable<T>, object: T, callback: (object: T) => void): void;
|
create(serializable: Serializable<T>, object: T, callback: (object: T) => void): void;
|
||||||
update(serializable: Serializable<T>, object: T, callback: () => void): void;
|
update(serializable: Serializable<T>, object: T, callback: () => void): void;
|
||||||
destroy(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> {
|
export interface Serializable<T> {
|
||||||
@ -33,18 +30,6 @@ export class AbstractApi<T extends Entity> implements Api<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllPdf(serializable: Serializable<T>, callback: () => void): void {
|
|
||||||
fetch(`/api/${getSlug()}/${serializable.apiPath()}`, {
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/pdf',
|
|
||||||
}
|
|
||||||
}).then(res => res.blob())
|
|
||||||
.then(blob => {
|
|
||||||
var file = window.URL.createObjectURL(blob);
|
|
||||||
window.location.assign(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get(serializable: Serializable<T>, id: (string | undefined), callback: (object: T) => void): void {
|
get(serializable: Serializable<T>, id: (string | undefined), callback: (object: T) => void): void {
|
||||||
const endpoint = id ? `/api/${getSlug()}/${serializable.apiPath()}/${id}` : `/api/${getSlug()}/${serializable.apiPath()}`;
|
const endpoint = id ? `/api/${getSlug()}/${serializable.apiPath()}/${id}` : `/api/${getSlug()}/${serializable.apiPath()}`;
|
||||||
fetch(endpoint)
|
fetch(endpoint)
|
||||||
@ -100,14 +85,4 @@ export class AbstractApi<T extends Entity> implements Api<T> {
|
|||||||
}).then(callback)
|
}).then(callback)
|
||||||
.catch((error) => console.error(error));
|
.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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@ export function loadTableSimulations(onLoad?: (tableSimulations: TableArrangemen
|
|||||||
name: record.name,
|
name: record.name,
|
||||||
discomfort: record.discomfort,
|
discomfort: record.discomfort,
|
||||||
valid: record.valid,
|
valid: record.valid,
|
||||||
progress: record.progress,
|
|
||||||
status : record.status
|
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
|
@ -14,8 +14,6 @@ export type TableArrangement = {
|
|||||||
guests?: Guest[];
|
guests?: Guest[];
|
||||||
discomfort?: number;
|
discomfort?: number;
|
||||||
valid?: boolean;
|
valid?: boolean;
|
||||||
progress: number;
|
|
||||||
status: 'in_progress' | 'completed' | 'not_started';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
|
@ -22,12 +22,10 @@ export type Table = {
|
|||||||
export class TableSimulation implements Entity {
|
export class TableSimulation implements Entity {
|
||||||
id?: string;
|
id?: string;
|
||||||
tables: Table[];
|
tables: Table[];
|
||||||
progress: number;
|
|
||||||
|
|
||||||
constructor(id?: string, tables?: Table[], progress?: number) {
|
constructor(id?: string, tables?: Table[]) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.tables = tables || [];
|
this.tables = tables || [];
|
||||||
this.progress = progress || 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +43,7 @@ export class TableSimulationSerializer implements Serializable<TableSimulation>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}), data.progress);
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
toJson(simulation: TableSimulation): string {
|
toJson(simulation: TableSimulation): string {
|
||||||
|
@ -10,21 +10,11 @@ import { loadTableSimulations } from "@/app/api/tableSimulations";
|
|||||||
import { ArchiveBoxXMarkIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
|
import { ArchiveBoxXMarkIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
|
||||||
import { Tooltip } from "primereact/tooltip";
|
import { Tooltip } from "primereact/tooltip";
|
||||||
import clsx from "clsx";
|
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 }) {
|
export default function ArrangementsTable({ onArrangementSelected }: { onArrangementSelected: (arrangementId: string) => void }) {
|
||||||
const [arrangements, setArrangements] = useState<Array<TableArrangement>>([]);
|
const [arrangements, setArrangements] = useState<Array<TableArrangement>>([]);
|
||||||
const [arrangementsLoaded, setArrangementsLoaded] = useState(false);
|
const [arrangementsLoaded, setArrangementsLoaded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refreshSimulations();
|
|
||||||
const interval = setInterval(refreshSimulations, 10000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function refreshSimulations() {
|
function refreshSimulations() {
|
||||||
loadTableSimulations((arrangements) => {
|
loadTableSimulations((arrangements) => {
|
||||||
setArrangements(arrangements);
|
setArrangements(arrangements);
|
||||||
@ -36,9 +26,11 @@ export default function ArrangementsTable({ onArrangementSelected }: { onArrange
|
|||||||
onArrangementSelected(e.currentTarget.getAttribute('data-arrangement-id') || '');
|
onArrangementSelected(e.currentTarget.getAttribute('data-arrangement-id') || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
!arrangementsLoaded && refreshSimulations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableOfContents
|
<TableOfContents
|
||||||
headers={['Name', 'Discomfort', 'Status', 'Actions']}
|
headers={['Name', 'Discomfort', 'Actions', 'Status']}
|
||||||
caption='Simulations'
|
caption='Simulations'
|
||||||
elements={arrangements}
|
elements={arrangements}
|
||||||
rowRender={(arrangement) => (
|
rowRender={(arrangement) => (
|
||||||
@ -52,19 +44,18 @@ export default function ArrangementsTable({ onArrangementSelected }: { onArrange
|
|||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
{arrangement.discomfort}
|
{arrangement.discomfort}
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<button data-arrangement-id={arrangement.id} onClick={arrangementClicked} className={classNames('primary')}>Load</button>
|
<button data-arrangement-id={arrangement.id} onClick={arrangementClicked} className={classNames('primary')}>Load</button>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -5,7 +5,7 @@ import clsx from "clsx";
|
|||||||
type ButtonColor = 'primary' | 'blue' | 'green' | 'red' | 'yellow' | 'gray';
|
type ButtonColor = 'primary' | 'blue' | 'green' | 'red' | 'yellow' | 'gray';
|
||||||
|
|
||||||
export function classNames(type: ButtonColor) {
|
export function classNames(type: ButtonColor) {
|
||||||
return (clsx("text-white py-1 px-2 m-2 rounded disabled:opacity-50 disabled:cursor-not-allowed", {
|
return (clsx("text-white py-1 px-2 mx-1 rounded disabled:opacity-50 disabled:cursor-not-allowed", {
|
||||||
'bg-blue-400 hover:bg-blue-600': type === 'primary' || type === 'blue',
|
'bg-blue-400 hover:bg-blue-600': type === 'primary' || type === 'blue',
|
||||||
'bg-green-500 hover:bg-green-600': type === 'green',
|
'bg-green-500 hover:bg-green-600': type === 'green',
|
||||||
'bg-red-500 hover:bg-red-600': type === 'red',
|
'bg-red-500 hover:bg-red-600': type === 'red',
|
||||||
|
@ -10,8 +10,6 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
|
|||||||
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
|
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { classNames } from "../components/button";
|
|
||||||
import { Toast } from "primereact/toast";
|
|
||||||
|
|
||||||
function InvitationCard({ invitation, allGuests, onGuestAdded, onDestroy }: {
|
function InvitationCard({ invitation, allGuests, onGuestAdded, onDestroy }: {
|
||||||
invitation: Invitation,
|
invitation: Invitation,
|
||||||
@ -118,7 +116,6 @@ export default function InvitationsBoard({ guests, invitations: originalInvitati
|
|||||||
guests: Array<Guest>,
|
guests: Array<Guest>,
|
||||||
invitations: Array<Invitation>
|
invitations: Array<Invitation>
|
||||||
}) {
|
}) {
|
||||||
const toast = useRef<Toast>(null);
|
|
||||||
const api = new AbstractApi<Invitation>();
|
const api = new AbstractApi<Invitation>();
|
||||||
const serializer = new InvitationSerializer();
|
const serializer = new InvitationSerializer();
|
||||||
|
|
||||||
@ -147,20 +144,9 @@ 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.'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
{/* Left Column: Guests */}
|
{/* Left Column: Guests */}
|
||||||
<Toast ref={toast} />
|
|
||||||
<div className="w-1/4 h-full overflow-auto border-r border-gray-300 p-4">
|
<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>
|
<h2 className="text-lg font-semibold mb-4">{unassignedGuests.length} guests without invitation</h2>
|
||||||
<div>
|
<div>
|
||||||
@ -178,18 +164,11 @@ export default function InvitationsBoard({ guests, invitations: originalInvitati
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateInvitation}
|
onClick={handleCreateInvitation}
|
||||||
className={classNames('primary')}
|
className="mb-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
Create New Invitation
|
Create New Invitation
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleDownloadQrCodes}
|
|
||||||
className={classNames('primary')}
|
|
||||||
>
|
|
||||||
Send QR codes via email
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-6">
|
<div className="grid grid-cols-4 gap-6">
|
||||||
|
|
||||||
|
10
package.json
10
package.json
@ -16,8 +16,8 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"next": "15.4.5",
|
"next": "15.3.3",
|
||||||
"next-auth": "5.0.0-beta.29",
|
"next-auth": "5.0.0-beta.28",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.8.2",
|
"primereact": "^10.8.2",
|
||||||
@ -27,12 +27,12 @@
|
|||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"use-debounce": "^10.0.1",
|
"use-debounce": "^10.0.1",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"zod": "^4.0.0"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/node": "24.3.1",
|
"@types/node": "24.0.3",
|
||||||
"@types/react": "18.3.23",
|
"@types/react": "18.3.23",
|
||||||
"@types/react-dom": "18.3.7",
|
"@types/react-dom": "18.3.7",
|
||||||
"wait-on": "^8.0.3"
|
"wait-on": "^8.0.3"
|
||||||
@ -40,5 +40,5 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=23.0.0"
|
"node": ">=23.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
|
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||||
}
|
}
|
||||||
|
748
pnpm-lock.yaml
generated
748
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user