Rework the table simulations UI #331
@ -6,39 +6,35 @@ import { AbstractApi } from '@/app/api/abstract-api';
|
||||
import { TableSimulation, TableSimulationSerializer } from '@/app/lib/tableSimulation';
|
||||
import Arrangement from '@/app/ui/arrangements/arrangement';
|
||||
import ArrangementsTable from '@/app/ui/arrangements/arrangements-table';
|
||||
import CalculatingSummary from '@/app/ui/arrangements/calculating-summary';
|
||||
import { classNames } from '@/app/ui/components/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import React, { useEffect, 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 className="flex flex-row w-full gap-4">
|
||||
<div className="flex-1 border rounded-lg">
|
||||
<ArrangementsTable onArrangementSelected={setCurrentArrangement} />
|
||||
</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>
|
||||
|
||||
<ArrangementsTable onArrangementSelected={setCurrentArrangement} />
|
||||
{currentArrangement && <Arrangement key={currentArrangement} id={currentArrangement} />}
|
||||
<>
|
||||
{currentArrangement && <Arrangement key={currentArrangement} id={currentArrangement} />}
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { TableArrangement } from '@/app/lib/definitions';
|
||||
import { getSlug } from '../lib/utils';
|
||||
|
||||
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((data) => {
|
||||
onLoad && onLoad(data.map((record: any) => {
|
||||
|
||||
@ -7,6 +7,8 @@ export interface Entity {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export type TableArrangementStatus = 'in_progress' | 'completed' | 'not_started';
|
||||
|
||||
export type TableArrangement = {
|
||||
id: string;
|
||||
number: number;
|
||||
@ -15,7 +17,7 @@ export type TableArrangement = {
|
||||
discomfort?: number;
|
||||
valid?: boolean;
|
||||
progress: number;
|
||||
status: 'in_progress' | 'completed' | 'not_started';
|
||||
status: TableArrangementStatus;
|
||||
}
|
||||
|
||||
export type User = {
|
||||
|
||||
@ -33,7 +33,7 @@ export class TableSimulation implements Entity {
|
||||
|
||||
export class TableSimulationSerializer implements Serializable<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 {
|
||||
number: table.number,
|
||||
guests: table.guests.map((guest: any) => new Guest(guest.id, guest.name, guest.color, guest.status, [], guest.group)),
|
||||
|
||||
@ -2,18 +2,12 @@
|
||||
|
||||
'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 { ArchiveBoxXMarkIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
|
||||
import { Tooltip } from "primereact/tooltip";
|
||||
import { TableArrangement } from '@/app/lib/definitions';
|
||||
import { ArrowsPointingOutIcon } from "@heroicons/react/24/outline";
|
||||
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";
|
||||
import { useEffect, useState } from "react";
|
||||
import TableOfContents from "../components/table-of-contents";
|
||||
|
||||
export default function ArrangementsTable({ onArrangementSelected }: { onArrangementSelected: (arrangementId: string) => void }) {
|
||||
const [arrangements, setArrangements] = useState<Array<TableArrangement>>([]);
|
||||
@ -32,14 +26,14 @@ export default function ArrangementsTable({ onArrangementSelected }: { onArrange
|
||||
});
|
||||
}
|
||||
|
||||
function arrangementClicked(e: React.MouseEvent<HTMLElement>) {
|
||||
onArrangementSelected(e.currentTarget.getAttribute('data-arrangement-id') || '');
|
||||
function arrangementClicked(arrangement: TableArrangement) {
|
||||
onArrangementSelected(arrangement.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableOfContents
|
||||
headers={['Name', 'Discomfort', 'Status', 'Actions']}
|
||||
caption='Simulations'
|
||||
headers={['Name', 'Discomfort', 'Actions']}
|
||||
caption='Best simulations'
|
||||
elements={arrangements}
|
||||
rowRender={(arrangement) => (
|
||||
<tr key={arrangement.id} className={clsx("border-b", {
|
||||
@ -52,18 +46,8 @@ 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>
|
||||
<ArrowsPointingOutIcon data-arrangement-id={arrangement.id} onClick={() => arrangementClicked(arrangement)} className='size-6 cursor-pointer' title="Load" />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
79
app/ui/arrangements/calculating-summary.tsx
Normal file
79
app/ui/arrangements/calculating-summary.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user