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 { 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} />}
|
||||||
|
</>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -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) => {
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
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