Rework the table simulations UI #331

Open
bustikiller wants to merge 6 commits from simulations-ui-rework into main
6 changed files with 108 additions and 47 deletions

View File

@ -6,39 +6,35 @@ import { AbstractApi } from '@/app/api/abstract-api';
import { TableSimulation, TableSimulationSerializer } from '@/app/lib/tableSimulation'; import { TableSimulation, TableSimulationSerializer } from '@/app/lib/tableSimulation';
import Arrangement from '@/app/ui/arrangements/arrangement'; import Arrangement from '@/app/ui/arrangements/arrangement';
import ArrangementsTable from '@/app/ui/arrangements/arrangements-table'; import ArrangementsTable from '@/app/ui/arrangements/arrangements-table';
import CalculatingSummary from '@/app/ui/arrangements/calculating-summary';
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, { useEffect, useRef, useState } from 'react';
export default function Page() { 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); 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 ( return (
<> <>
<div className="flex flex-row w-full gap-4">
<div className="flex flex-col w-full items-center justify-between"> <div className="flex-1 border rounded-lg">
<Toast ref={toast} /> <ArrangementsTable onArrangementSelected={setCurrentArrangement} />
<button onClick={createSimulation} className={classNames('primary')}>Add new</button> </div>
<div className="flex-1 border rounded-lg p-5 shadow-md">
<CalculatingSummary />
</div>
<div className="flex-1 border rounded-lg p-5 shadow-md">
<p className="text-lg font-semibold mb-4">Inventory</p>
</div>
</div> </div>
<ArrangementsTable onArrangementSelected={setCurrentArrangement} /> <>
{currentArrangement && <Arrangement key={currentArrangement} id={currentArrangement} />} {currentArrangement && <Arrangement key={currentArrangement} id={currentArrangement} />}
</>
</> </>
) )
} }

View File

@ -4,7 +4,7 @@ import { TableArrangement } from '@/app/lib/definitions';
import { getSlug } from '../lib/utils'; import { getSlug } from '../lib/utils';
export function loadTableSimulations(onLoad?: (tableSimulations: TableArrangement[]) => void) { export function loadTableSimulations(onLoad?: (tableSimulations: TableArrangement[]) => void) {
fetch(`/api/${getSlug()}/tables_arrangements`) fetch(`/api/${getSlug()}/tables_arrangements?limit=3&status=completed`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
onLoad && onLoad(data.map((record: any) => { onLoad && onLoad(data.map((record: any) => {

View File

@ -7,6 +7,8 @@ export interface Entity {
id?: string; id?: string;
} }
export type TableArrangementStatus = 'in_progress' | 'completed' | 'not_started';
export type TableArrangement = { export type TableArrangement = {
id: string; id: string;
number: number; number: number;
@ -15,7 +17,7 @@ export type TableArrangement = {
discomfort?: number; discomfort?: number;
valid?: boolean; valid?: boolean;
progress: number; progress: number;
status: 'in_progress' | 'completed' | 'not_started'; status: TableArrangementStatus;
} }
export type User = { export type User = {

View File

@ -33,7 +33,7 @@ export class TableSimulation implements Entity {
export class TableSimulationSerializer implements Serializable<TableSimulation> { export class TableSimulationSerializer implements Serializable<TableSimulation> {
fromJson(data: any): TableSimulation { fromJson(data: any): TableSimulation {
return new TableSimulation(data.id, data.tables.map((table: any) => { return new TableSimulation(data.id, data.tables?.map((table: any) => {
return { return {
number: table.number, number: table.number,
guests: table.guests.map((guest: any) => new Guest(guest.id, guest.name, guest.color, guest.status, [], guest.group)), guests: table.guests.map((guest: any) => new Guest(guest.id, guest.name, guest.color, guest.status, [], guest.group)),

View File

@ -2,18 +2,12 @@
'use client' 'use client'
import React, { useState } from "react"
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 { loadTableSimulations } from "@/app/api/tableSimulations";
import { ArchiveBoxXMarkIcon, CheckBadgeIcon } from "@heroicons/react/24/outline"; import { TableArrangement } from '@/app/lib/definitions';
import { Tooltip } from "primereact/tooltip"; import { ArrowsPointingOutIcon } from "@heroicons/react/24/outline";
import clsx from "clsx"; import clsx from "clsx";
import { ProgressBar } from "primereact/progressbar"; import { useEffect, useState } from "react";
import { useEffect } from "react"; import TableOfContents from "../components/table-of-contents";
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>>([]);
@ -32,14 +26,14 @@ export default function ArrangementsTable({ onArrangementSelected }: { onArrange
}); });
} }
function arrangementClicked(e: React.MouseEvent<HTMLElement>) { function arrangementClicked(arrangement: TableArrangement) {
onArrangementSelected(e.currentTarget.getAttribute('data-arrangement-id') || ''); onArrangementSelected(arrangement.id);
} }
return ( return (
<TableOfContents <TableOfContents
headers={['Name', 'Discomfort', 'Status', 'Actions']} headers={['Name', 'Discomfort', 'Actions']}
caption='Simulations' caption='Best simulations'
elements={arrangements} elements={arrangements}
rowRender={(arrangement) => ( rowRender={(arrangement) => (
<tr key={arrangement.id} className={clsx("border-b", { <tr key={arrangement.id} className={clsx("border-b", {
@ -52,18 +46,8 @@ 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> <ArrowsPointingOutIcon data-arrangement-id={arrangement.id} onClick={() => arrangementClicked(arrangement)} className='size-6 cursor-pointer' title="Load" />
</td> </td>
</tr> </tr>
)} )}

View File

@ -0,0 +1,79 @@
/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
'use client'
import { AbstractApi } from "@/app/api/abstract-api";
import { TableArrangementStatus } from "@/app/lib/definitions";
import { TableSimulation, TableSimulationSerializer } from "@/app/lib/tableSimulation";
import { getSlug } from "@/app/lib/utils";
import { Toast } from "primereact/toast";
import { useEffect, useRef, useState } from "react";
import { ProgressSpinner } from 'primereact/progressspinner';
import { classNames } from "../components/button";
export default function CalculatingSummary() {
const [stats, setStats] = useState<{ [key in TableArrangementStatus]: number }>({
in_progress: 0,
completed: 0,
not_started: 0,
});
const [inProgress, setInProgress] = useState<number[]>([]);
useEffect(() => {
const fetchStats = () => {
fetch(`/api/${getSlug()}/tables_arrangements/stats`)
.then((response) => response.json())
.then((data) => {
setStats(data.count);
setInProgress(data.in_progress);
});
};
fetchStats();
const interval = setInterval(fetchStats, 10000);
return () => clearInterval(interval);
}, []);
const toast = useRef<Toast>(null);
function createSimulation() {
const api = new AbstractApi<TableSimulation>();
const serializer = new TableSimulationSerializer();
api.create(serializer, new TableSimulation(), () => {
toast.current?.show({
severity: 'success',
summary: 'Simulation created',
detail: 'Table distributions will be calculated shortly, please come back in some minutes'
});
});
}
return (
<>
<Toast ref={toast} />
<p className="text-lg font-semibold mb-4">Processing engine</p>
<p>{stats.in_progress || 0 } processing</p>
<div className="my-4">
<div className="flex flex-row items-center gap-2">
{inProgress.map((progress, index) => (
<div key={index} className="relative size-16 flex items-center justify-center">
<ProgressSpinner className="size-16" strokeWidth="4" animationDuration={`${Math.random() * 2 + 2}s`} />
<span className="absolute text-s">
{Math.round(progress * 100)}%
</span>
</div>
))}
</div>
</div>
<p>+{stats.not_started || 0 } in queue</p>
<div className="flex justify-center">
<button onClick={createSimulation} className={classNames('primary')}>Add new</button>
</div>
</>
);
}