nitro-v36-with-renderer-v36
/
src /
components /
housekeeping /
views /
common /
HousekeepingListView.tsx
HousekeepingListView.tsx 9.6 Ko
Télécharger le fichier
import { FC, useEffect, useState } from 'react';
import { FaExternalLinkAlt, FaEye, FaEyeSlash, FaSync } from 'react-icons/fa';
import { formatHousekeepingListCell, HousekeepingApi, housekeepingFailureKey, HousekeepingTabId, IHousekeepingList, LocalizeText } from '../../../../api';
import { useHasPermission, useHousekeepingStore } from '../../../../hooks';
import { HousekeepingButton, HousekeepingEmptyState } from './HousekeepingParts';
import { HousekeepingSubTabs } from './HousekeepingSubTabs';
import { BaseButton } from '../../../../common/BaseButton';
import { Hint } from '../../../../common/Tooltip';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
export interface HousekeepingListChoice {
key: string;
labelKey: string;
}
/** Columns that point at another user or room the operator can jump to. */
const USER_ID_COLUMN = 'id';
const ROOM_ID_COLUMN = 'room_id';
/**
* A server list (chat, visits, same-IP accounts, ...) as a table, with a choice
* of lists on top. Rows that name a user or a room open it in the panel.
* Hotel-wide lists (hotelWide) take no target and are asked for with 0.
*/
export const HousekeepingListView: FC<{ lists: HousekeepingListChoice[]; targetId: number; hotelWide?: boolean }> = ({
lists,
targetId,
hotelWide = false
}) => {
const { lookupUserById, lookupRoomById, setActiveTab } = useHousekeepingStore();
const [listKey, setListKey] = useState(lists[0]?.key ?? '');
const [list, setList] = useState<IHousekeepingList | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [reload, setReload] = useState(0);
// IPs come masked; an operator with acc_hk_view_private can ask for them in clear (audited).
const canReveal = useHasPermission('acc_hk_view_private');
const [reveal, setReveal] = useState(false);
useEffect(() => {
if (!listKey || (targetId <= 0 && !hotelWide)) return;
const controller = new AbortController();
setIsLoading(true);
setError(null);
HousekeepingApi.requestList(listKey, targetId, controller.signal, reveal)
.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'));
})
.finally(() => {
if (!controller.signal.aborted) setIsLoading(false);
});
return () => controller.abort();
}, [listKey, targetId, hotelWide, reload, reveal]);
const shown = list && list.listKey === listKey && list.targetId === targetId ? list : null;
const userColumn = shown ? shown.columns.indexOf(USER_ID_COLUMN) : -1;
const roomColumn = shown ? shown.columns.indexOf(ROOM_ID_COLUMN) : -1;
const openRow = (row: string[]) => {
const userId = userColumn >= 0 ? parseInt(row[userColumn]) : 0;
const roomId = roomColumn >= 0 ? parseInt(row[roomColumn]) : 0;
if (userId > 0) {
setActiveTab(HousekeepingTabId.USERS);
lookupUserById(userId);
} else if (roomId > 0) {
setActiveTab(HousekeepingTabId.ROOMS);
lookupRoomById(roomId);
}
};
return (
<NativeTextTree>
<div className="flex flex-col gap-1.5">
<div className="flex flex-wrap items-center gap-1">
<HousekeepingSubTabs
compact
active={listKey}
tabs={lists.map((choice) => ({ id: choice.key, label: LocalizeText(choice.labelKey) }))}
onChange={(key) => {
setListKey(key);
setReveal(false);
}}
/>
{canReveal && shown?.columns.includes('ip') && (
<HousekeepingButton
classNames={['ml-auto']}
disabled={isLoading}
gap={1}
size="sm"
title={LocalizeText('housekeeping.user.private.reveal_hint')}
variant={reveal ? 'warning' : 'secondary'}
onClick={() => setReveal((value) => !value)}
>
{reveal ? <FaEyeSlash size={9} /> : <FaEye size={9} />}
<span>{LocalizeText(reveal ? 'housekeeping.user.private.hide' : 'housekeeping.user.private.show')}</span>
</HousekeepingButton>
)}
<HousekeepingButton
classNames={canReveal && shown?.columns.includes('ip') ? [] : ['ml-auto']}
disabled={isLoading}
gap={1}
size="sm"
variant="secondary"
onClick={() => setReload((value) => value + 1)}
>
<FaSync className={isLoading ? 'animate-spin' : ''} size={9} />
<span>{LocalizeText('housekeeping.history.refresh')}</span>
</HousekeepingButton>
</div>
{error && <HousekeepingEmptyState>{LocalizeText(error)}</HousekeepingEmptyState>}
{!error && isLoading && !shown && <HousekeepingEmptyState>{LocalizeText('generic.loading')}</HousekeepingEmptyState>}
{!error && shown && shown.rows.length === 0 && <HousekeepingEmptyState>{LocalizeText('housekeeping.list.empty')}</HousekeepingEmptyState>}
{!error && shown && shown.rows.length > 0 && (
<div className="max-h-[300px] overflow-auto rounded border border-zinc-200 bg-white">
<table className="w-full border-collapse text-[11px]">
<thead className="sticky top-0 bg-zinc-100">
<tr>
{shown.columns.map((column) => (
<th
key={column}
className="whitespace-nowrap px-1.5 py-1 text-left text-[10px] font-semibold uppercase tracking-wide text-zinc-600"
>
{LocalizeText(`housekeeping.list.column.${column}`)}
</th>
))}
{(userColumn >= 0 || roomColumn >= 0) && <th className="w-6" />}
</tr>
</thead>
<tbody>
{shown.rows.map((row, index) => (
<tr key={index} className="border-t border-zinc-100 align-top hover:bg-sky-50/50">
{shown.columns.map((column, columnIndex) => (
<td
key={column}
className={`px-1.5 py-0.5 ${column === 'message' || column === 'reason' ? 'break-words' : 'whitespace-nowrap'} text-zinc-800`}
>
{formatHousekeepingListCell(column, row[columnIndex])}
</td>
))}
{(userColumn >= 0 || roomColumn >= 0) && (
<td className="px-1 py-0.5">
<Hint content={LocalizeText(
userColumn >= 0 ? 'housekeeping.audit.detail.open_user' : 'housekeeping.audit.detail.open_room'
)}>
<BaseButton aria-label={LocalizeText(
userColumn >= 0 ? 'housekeeping.audit.detail.open_user' : 'housekeeping.audit.detail.open_room'
)}
className="text-zinc-400 hover:text-sky-600"
type="button"
onClick={() => openRow(row)}
>
<FaExternalLinkAlt size={9} />
</BaseButton>
</Hint>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</NativeTextTree>
);
};
