Compare commits

..

1 Commits

Author SHA1 Message Date
faf0ee4dbd WIP define UI for invitations
All checks were successful
Build Nginx-based docker image / build-static-assets (push) Successful in 3m27s
2025-01-28 09:00:09 +01:00
10 changed files with 115 additions and 126 deletions

2
.nvmrc
View File

@ -1 +1 @@
23.7.0
23.6.1

View File

@ -2,41 +2,15 @@
'use client';
import { AbstractApi } from '@/app/api/abstract-api';
import { TableSimulation, TableSimulationSerializer } from '@/app/lib/tableSimulation';
import Arrangement from '@/app/ui/arrangements/arrangement';
import React, { useState } from 'react';
import ArrangementsTable from '@/app/ui/arrangements/arrangements-table';
import { classNames } from '@/app/ui/components/button';
import { Toast } from 'primereact/toast';
import React, { useRef, useState } from 'react';
export default function Page() {
const toast = useRef<Toast>(null);
const show = () => {
toast.current?.show({
severity: 'success',
summary: 'Simulation created',
detail: 'Table distributions will be calculated shortly, please come back in some minutes'
});
};
const [currentArrangement, setCurrentArrangement] = useState<string | null>(null);
function createSimulation() {
const api = new AbstractApi<TableSimulation>();
const serializer = new TableSimulationSerializer();
api.create(serializer, new TableSimulation(), show);
}
return (
<>
<div className="flex flex-col w-full items-center justify-between">
<Toast ref={toast} />
<button onClick={createSimulation} className={classNames('primary')}>Add new</button>
</div>
<ArrangementsTable onArrangementSelected={setCurrentArrangement} />
{currentArrangement && <Arrangement key={currentArrangement} id={currentArrangement} />}
</>

View File

@ -12,7 +12,6 @@ export function loadTableSimulations(onLoad?: (tableSimulations: TableArrangemen
id: record.id,
name: record.name,
discomfort: record.discomfort,
valid: record.valid,
});
}));
}, (error) => {

View File

@ -12,8 +12,7 @@ export type TableArrangement = {
number: number;
name: string;
guests?: Guest[];
discomfort?: number;
valid?: boolean;
discomfort?: number
}
export type User = {

View File

@ -14,8 +14,9 @@ export class Guest implements Entity {
color?: string;
status?: GuestStatus;
children?: Guest[];
invitationId?: string;
constructor(id?: string, name?: string, group_name?: string, groupId?: string, color?: string, status?: GuestStatus, children?: Guest[]) {
constructor(id?: string, name?: string, group_name?: string, groupId?: string, color?: string, status?: GuestStatus, children?: Guest[], invitationId?: string) {
this.id = id;
this.name = name;
this.group_name = group_name;
@ -23,12 +24,13 @@ export class Guest implements Entity {
this.color = color;
this.status = status;
this.children = children;
this.invitationId = invitationId;
}
}
export class GuestSerializer implements Serializable<Guest> {
fromJson(data: any): Guest {
return new Guest(data.id, data.name, data.group?.name, data.group?.id, data.color, data.status, data.children);
return new Guest(data.id, data.name, data.group?.name, data.group?.id, data.color, data.status, data.children, data.invitation_id);
}
toJson(guest: Guest): string {

View File

@ -19,12 +19,12 @@ export type Table = {
}
export class TableSimulation implements Entity {
id?: string;
id: string;
tables: Table[];
constructor(id?: string, tables?: Table[]) {
constructor(id: string, tables: Table[]) {
this.id = id;
this.tables = tables || [];
this.tables = tables;
}
}

View File

@ -7,57 +7,42 @@ import { TableArrangement } from '@/app/lib/definitions';
import { classNames } from "../components/button";
import TableOfContents from "../components/table-of-contents";
import { loadTableSimulations } from "@/app/api/tableSimulations";
import { ArchiveBoxXMarkIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
import { Tooltip } from "primereact/tooltip";
import clsx from "clsx";
export default function ArrangementsTable({ onArrangementSelected }: { onArrangementSelected: (arrangementId: string) => void }) {
const [arrangements, setArrangements] = useState<Array<TableArrangement>>([]);
const [arrangementsLoaded, setArrangementsLoaded] = useState(false);
export default function ArrangementsTable ({onArrangementSelected}: {onArrangementSelected: (arrangementId: string) => void}) {
const [arrangements, setArrangements] = useState<Array<TableArrangement>>([]);
const [arrangementsLoaded, setArrangementsLoaded] = useState(false);
function refreshSimulations() {
loadTableSimulations((arrangements) => {
setArrangements(arrangements);
setArrangementsLoaded(true);
});
}
function refreshSimulations() {
loadTableSimulations((arrangements) => {
setArrangements(arrangements);
setArrangementsLoaded(true);
});
}
function arrangementClicked(e: React.MouseEvent<HTMLElement>) {
onArrangementSelected(e.currentTarget.getAttribute('data-arrangement-id') || '');
}
function arrangementClicked(e: React.MouseEvent<HTMLElement>) {
onArrangementSelected(e.currentTarget.getAttribute('data-arrangement-id') || '');
}
!arrangementsLoaded && refreshSimulations();
!arrangementsLoaded && refreshSimulations();
return (
<TableOfContents
headers={['Name', 'Discomfort', 'Actions', 'Status']}
caption='Simulations'
elements={arrangements}
rowRender={(arrangement) => (
<tr key={arrangement.id} className={clsx("border-b", {
"bg-white odd:bg-white even:bg-gray-50": arrangement.valid,
"bg-red-50 odd:bg-red-50 even:bg-red-100": !arrangement.valid
})}>
<th scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
{arrangement.name}
</th>
<td className="px-6 py-4">
{arrangement.discomfort}
</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>
)}
/>
);
return(
<TableOfContents
headers={['Name', 'Discomfort', 'Actions']}
caption='Simulations'
elements={arrangements}
rowRender={(arrangement) => (
<tr key={arrangement.id} className="bg-white border-b odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800">
<th scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{arrangement.name}
</th>
<td className="px-6 py-4">
{arrangement.discomfort}
</td>
<td>
<button data-arrangement-id={arrangement.id} onClick={arrangementClicked} className={classNames('primary')}>Load</button>
</td>
</tr>
)}
/>
);
}

View File

@ -3,9 +3,10 @@
'use client';
import { AbstractApi } from '@/app/api/abstract-api';
import { Guest , GuestSerializer} from '@/app/lib/guest';
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
import { Guest, GuestSerializer } from '@/app/lib/guest';
import { EnvelopeIcon, FunnelIcon, PencilIcon, TrashIcon, UserPlusIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { useState } from 'react';
import TableOfContents from '../components/table-of-contents';
export default function guestsTable({ guests, onUpdate, onEdit }: {
@ -16,22 +17,40 @@ export default function guestsTable({ guests, onUpdate, onEdit }: {
const api = new AbstractApi<Guest>();
const serializer = new GuestSerializer();
const [invitationId, setInvitationId] = useState<string | undefined>(undefined);
const [selecting, setSelecting] = useState<boolean>(false);
function toggleInvitationId(id: string | undefined) {
console.log('toggleInvitationId', id, invitationId);
if (id === invitationId) {
setInvitationId(undefined);
} else {
setInvitationId(id);
}
console.log('invitationId', invitationId);
}
return (
<TableOfContents
headers={['Name', 'Group', 'Status', 'Actions']}
headers={['Name', 'Group', 'Status', 'Invitation', 'Actions']}
caption='Guests'
elements={guests}
rowRender={(guest) => (
<tr key={guest.id} className="bg-white border-b odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800">
<td scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
<tr key={guest.id} className={clsx("bg-white odd:bg-white even:bg-gray-50 border-b", {
"opacity-25": invitationId && !selecting && guest.invitationId !== invitationId,
"hover:bg-emerald-100" : selecting && guest.invitationId !== invitationId && guest.status !== 'considered',
"opacity-25 hover:cursor-not-allowed" : selecting && (guest.invitationId === invitationId || guest.status === 'considered')
})} >
<td scope="row" className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
{guest.name}
</td>
<td className="px-6 py-4">
{guest.group_name}
</td>
<td className="px-6 py-4">
<span className="flex items-center text-sm dark:text-white me-3">
<span className="flex items-center text-sm me-3">
<span className={clsx(
'flex w-2.5 h-2.5 rounded-full me-1.5 flex-shrink-0',
{
@ -46,10 +65,21 @@ export default function guestsTable({ guests, onUpdate, onEdit }: {
{guest.status}
</span>
</td>
<td>
{
guest.invitationId &&
<div className="flex">
{!selecting && <FunnelIcon className='size-6 cursor-pointer' onClick={() => toggleInvitationId(guest?.invitationId)} />}
{ invitationId === guest.invitationId && !selecting && <UserPlusIcon className='size-6 cursor-pointer' onClick={() => setSelecting(!selecting)}/>}
{ selecting && invitationId !== guest.invitationId && <UserPlusIcon className='size-6 cursor-pointer' />}
</div>
}
</td>
<td>
<div className="flex flex-row items-center">
<TrashIcon className='size-6 cursor-pointer' onClick={() => { api.destroy(serializer, guest, onUpdate)}} />
<TrashIcon className='size-6 cursor-pointer' onClick={() => { api.destroy(serializer, guest, onUpdate) }} />
<PencilIcon className='size-6 cursor-pointer' onClick={() => onEdit(guest)} />
</div>
</td>
</tr>

View File

@ -27,7 +27,7 @@
"devDependencies": {
"@playwright/test": "^1.46.0",
"@types/bcrypt": "^5.0.2",
"@types/node": "22.13.1",
"@types/node": "22.10.10",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5"
},

66
pnpm-lock.yaml generated
View File

@ -25,10 +25,10 @@ importers:
version: 2.1.1
next:
specifier: 15.1.6
version: 15.1.6(@playwright/test@1.50.1)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
version: 15.1.6(@playwright/test@1.50.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
next-auth:
specifier: 5.0.0-beta.25
version: 5.0.0-beta.25(next@15.1.6(@playwright/test@1.50.1)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
version: 5.0.0-beta.25(next@15.1.6(@playwright/test@1.50.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
postcss:
specifier: 8.5.1
version: 8.5.1
@ -37,7 +37,7 @@ importers:
version: 7.0.0
primereact:
specifier: ^10.8.2
version: 10.9.2(@types/react@18.3.18)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
version: 10.9.1(@types/react@18.3.18)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
react:
specifier: 19.0.0-rc-f38c22b244-20240704
version: 19.0.0-rc-f38c22b244-20240704
@ -62,13 +62,13 @@ importers:
devDependencies:
'@playwright/test':
specifier: ^1.46.0
version: 1.50.1
version: 1.50.0
'@types/bcrypt':
specifier: ^5.0.2
version: 5.0.2
'@types/node':
specifier: 22.13.1
version: 22.13.1
specifier: 22.10.10
version: 22.10.10
'@types/react':
specifier: 18.3.18
version: 18.3.18
@ -96,8 +96,8 @@ packages:
nodemailer:
optional: true
'@babel/runtime@7.26.7':
resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==}
'@babel/runtime@7.26.0':
resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
engines: {node: '>=6.9.0'}
'@emnapi/runtime@1.2.0':
@ -309,8 +309,8 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@playwright/test@1.50.1':
resolution: {integrity: sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==}
'@playwright/test@1.50.0':
resolution: {integrity: sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==}
engines: {node: '>=18'}
hasBin: true
@ -331,8 +331,8 @@ packages:
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/node@22.13.1':
resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==}
'@types/node@22.10.10':
resolution: {integrity: sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==}
'@types/prop-types@15.7.12':
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
@ -842,13 +842,13 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
playwright-core@1.50.1:
resolution: {integrity: sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==}
playwright-core@1.50.0:
resolution: {integrity: sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==}
engines: {node: '>=18'}
hasBin: true
playwright@1.50.1:
resolution: {integrity: sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==}
playwright@1.50.0:
resolution: {integrity: sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==}
engines: {node: '>=18'}
hasBin: true
@ -911,8 +911,8 @@ packages:
primeicons@7.0.0:
resolution: {integrity: sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==}
primereact@10.9.2:
resolution: {integrity: sha512-uJTghCPlnPWJc0mvkqYJDj6bl4udROPGrMEfV4CPh7UurMS+E8b+82ABZ+OPWibQOWxnEQl5NXcfG4F/7YRXwA==}
primereact@10.9.1:
resolution: {integrity: sha512-mq5Pr6/zySO913RlL5oUWD+n1ygTjJjhhxBoRWhLNKx7Na8pHpswh/QCesShFXlfYyCjDOVXFVT6J2sSDceF1g==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
@ -1176,7 +1176,7 @@ snapshots:
preact: 10.11.3
preact-render-to-string: 5.2.3(preact@10.11.3)
'@babel/runtime@7.26.7':
'@babel/runtime@7.26.0':
dependencies:
regenerator-runtime: 0.14.1
@ -1348,9 +1348,9 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@playwright/test@1.50.1':
'@playwright/test@1.50.0':
dependencies:
playwright: 1.50.1
playwright: 1.50.0
'@swc/counter@0.1.3': {}
@ -1365,11 +1365,11 @@ snapshots:
'@types/bcrypt@5.0.2':
dependencies:
'@types/node': 22.13.1
'@types/node': 22.10.10
'@types/cookie@0.6.0': {}
'@types/node@22.13.1':
'@types/node@22.10.10':
dependencies:
undici-types: 6.20.0
@ -1542,7 +1542,7 @@ snapshots:
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.26.7
'@babel/runtime': 7.26.0
csstype: 3.1.3
eastasianwidth@0.2.0: {}
@ -1740,13 +1740,13 @@ snapshots:
nanoid@3.3.8: {}
next-auth@5.0.0-beta.25(next@15.1.6(@playwright/test@1.50.1)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
next-auth@5.0.0-beta.25(next@15.1.6(@playwright/test@1.50.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
dependencies:
'@auth/core': 0.37.2
next: 15.1.6(@playwright/test@1.50.1)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
next: 15.1.6(@playwright/test@1.50.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
react: 19.0.0-rc-f38c22b244-20240704
next@15.1.6(@playwright/test@1.50.1)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
next@15.1.6(@playwright/test@1.50.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
dependencies:
'@next/env': 15.1.6
'@swc/counter': 0.1.3
@ -1766,7 +1766,7 @@ snapshots:
'@next/swc-linux-x64-musl': 15.1.6
'@next/swc-win32-arm64-msvc': 15.1.6
'@next/swc-win32-x64-msvc': 15.1.6
'@playwright/test': 1.50.1
'@playwright/test': 1.50.0
sharp: 0.33.5
transitivePeerDependencies:
- '@babel/core'
@ -1826,11 +1826,11 @@ snapshots:
pirates@4.0.6: {}
playwright-core@1.50.1: {}
playwright-core@1.50.0: {}
playwright@1.50.1:
playwright@1.50.0:
dependencies:
playwright-core: 1.50.1
playwright-core: 1.50.0
optionalDependencies:
fsevents: 2.3.2
@ -1888,7 +1888,7 @@ snapshots:
primeicons@7.0.0: {}
primereact@10.9.2(@types/react@18.3.18)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
primereact@10.9.1(@types/react@18.3.18)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
dependencies:
'@types/react-transition-group': 4.4.12(@types/react@18.3.18)
react: 19.0.0-rc-f38c22b244-20240704
@ -1914,7 +1914,7 @@ snapshots:
react-transition-group@4.4.5(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
dependencies:
'@babel/runtime': 7.26.7
'@babel/runtime': 7.26.0
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1