5180 fichiers 38.5 Mo 0 téléchargements 476 vues mis à jour il y a 7 heures ui
nitro-v36-with-renderer-v36 / src / components / housekeeping / views / dashboard / HousekeepingStatsView.tsx
HousekeepingStatsView.tsx 6.5 Ko Télécharger le fichier
import { FC, ReactNode, useEffect, useState } from 'react';
import { FaBan, FaDoorOpen } from 'react-icons/fa';
import {
    HOUSEKEEPING_DAY,
    HOUSEKEEPING_HOUR,
    HOUSEKEEPING_STATS_LIST,
    HousekeepingApi,
    housekeepingFailureKey,
    HousekeepingStatPoint,
    IHousekeepingList,
    LocalizeText,
    readHousekeepingStatSeries
} from '../../../../api';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';

/** The series move by the hour; a minute keeps the current bar fresh without crowding the rate-limited list request. */
const REFRESH_MS = 60_000;

const ACTIVITY_HOURS = 24;
const BAN_DAYS = 14;

const hourLabel = (bucket: number) => new Date(bucket * 1000).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
const dayLabel = (bucket: number) => new Date(bucket * 1000).toLocaleDateString(undefined, { day: 'numeric', month: 'short' });

/**
 * One series as thin bars on a baseline: no grid, no legend (the title names the series), the
 * first, middle and last buckets labelled on the axis, and a readout line for the bar under
 * the pointer. A visually hidden table carries the same numbers for screen readers.
 */
const HousekeepingBarChart: FC<{
    title: string;
    icon: ReactNode;
    points: HousekeepingStatPoint[];
    label: (bucket: number) => string;
    unit: string;
}> = ({ title, icon, points, label, unit }) => {
    const [hovered, setHovered] = useState<number | null>(null);
    const max = Math.max(1, ...points.map((point) => point.value));
    const total = points.reduce((sum, point) => sum + point.value, 0);
    const shown = hovered !== null ? points[hovered] : null;
    const axis = [0, Math.floor((points.length - 1) / 2), points.length - 1];

    return (
        <NativeTextTree>
            <figure className="m-0 flex min-w-0 flex-col gap-1 rounded-lg border border-zinc-200 bg-white p-2">
                <figcaption className="flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wider text-zinc-600">
                    {icon}
                    <span className="truncate">{title}</span>
                </figcaption>
                <div className="h-4 text-[11px] tabular-nums text-zinc-800">
                    {shown ? (
                        <>
                            <span className="font-semibold">{shown.value}</span> {unit} · <span className="text-zinc-500">{label(shown.bucket)}</span>
                        </>
                    ) : (
                        <span className="text-zinc-500">{LocalizeText('housekeeping.dashboard.chart.total', ['count'], [String(total)])}</span>
                    )}
                </div>
                <div aria-hidden="true" className="flex h-20 items-end gap-[2px] border-b border-zinc-300" onMouseLeave={() => setHovered(null)}>
                    {points.map((point, index) => (
                        <div key={point.bucket} className="flex h-full min-w-0 flex-1 cursor-default items-end" onMouseEnter={() => setHovered(index)}>
                            <div
                                className={`w-full rounded-t-[4px] ${hovered === index ? 'bg-sky-800' : 'bg-sky-600'}`}
                                style={{ height: point.value > 0 ? `${Math.max(4, (point.value / max) * 100)}%` : '0' }}
                            />
                        </div>
                    ))}
                </div>
                <div aria-hidden="true" className="flex justify-between text-[9px] tabular-nums text-zinc-500">
                    {axis.map((index) => (
                        <span key={index}>{points[index] ? label(points[index].bucket) : ''}</span>
                    ))}
                </div>
                {/* The table sits in a clipped div: a <caption> is drawn outside its table box and would escape sr-only on the table itself. */}
                <div className="sr-only">
                    <table>
                        <caption>{title}</caption>
                        <tbody>
                            {points.map((point) => (
                                <tr key={point.bucket}>
                                    <th scope="row">{label(point.bucket)}</th>
                                    <td>{point.value}</td>
                                </tr>
                            ))}
                        </tbody>
                    </table>
                </div>
            </figure>
        </NativeTextTree>
    );
};

/** Room activity per hour of the last day and bans per day of the last two weeks. */
export const HousekeepingStatsView: FC = () => {
    const [list, setList] = useState<IHousekeepingList | null>(null);
    const [error, setError] = useState<string | null>(null);
    const [tick, setTick] = useState(0);

    useEffect(() => {
        const id = window.setInterval(() => setTick((value) => value + 1), REFRESH_MS);

        return () => window.clearInterval(id);
    }, []);

    useEffect(() => {
        const controller = new AbortController();

        HousekeepingApi.requestList(HOUSEKEEPING_STATS_LIST, 0, controller.signal)
            .then((result) => {
                if (controller.signal.aborted) return;

                setList(result);
                setError(result.ok ? null : result.message || 'housekeeping.list.failed');
            })
            .catch((reason) => {
                if (!controller.signal.aborted) setError(housekeepingFailureKey(reason, 'housekeeping.list.failed'));
            });

        return () => controller.abort();
    }, [tick]);

    if (error) return <div className="text-[11px] italic text-zinc-500">{LocalizeText(error)}</div>;

    const now = Math.floor(Date.now() / 1000);

    return (
        <NativeTextTree>
            <div className="grid grid-cols-2 gap-1.5">
                <HousekeepingBarChart
                    icon={<FaDoorOpen size={9} />}
                    label={hourLabel}
                    points={readHousekeepingStatSeries(list, 'activity', HOUSEKEEPING_HOUR, ACTIVITY_HOURS, now)}
                    title={LocalizeText('housekeeping.dashboard.chart.activity')}
                    unit={LocalizeText('housekeeping.dashboard.chart.activity.unit')}
                />
                <HousekeepingBarChart
                    icon={<FaBan size={9} />}
                    label={dayLabel}
                    points={readHousekeepingStatSeries(list, 'bans', HOUSEKEEPING_DAY, BAN_DAYS, now)}
                    title={LocalizeText('housekeeping.dashboard.chart.bans')}
                    unit={LocalizeText('housekeeping.dashboard.chart.bans.unit')}
                />
            </div>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
3En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser