5180 fichiers 38.5 Mo 0 téléchargements 378 vues mis à jour il y a 7 heures ui
nitro-v36-with-renderer-v36 / src / components / housekeeping / views / users / HousekeepingUsersTab.tsx
HousekeepingUsersTab.tsx 16.4 Ko Télécharger le fichier
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import {
    FaBan,
    FaCircle,
    FaCoins,
    FaComments,
    FaGavel,
    FaHistory,
    FaSearch,
    FaShieldAlt,
    FaStickyNote,
    FaTimes,
    FaUserCog,
    FaUserSlash,
    FaVolumeMute
} from 'react-icons/fa';
import { HousekeepingUserSection, isAuditEntryAboutUser, LocalizeText } from '../../../../api';
import { useHasPermission, useHousekeeping, useHousekeepingDangerConfirm, useRoomUserListSnapshot } from '../../../../hooks';
import { HousekeepingHistoryView } from '../common/HousekeepingHistoryView';
import { HousekeepingListChoice, HousekeepingListView } from '../common/HousekeepingListView';
import { HousekeepingButton, HousekeepingEmptyState } from '../common/HousekeepingParts';
import { HousekeepingSubTab, HousekeepingSubTabs } from '../common/HousekeepingSubTabs';
import { HousekeepingUserAccountView } from './HousekeepingUserAccountView';
import { HousekeepingUserNotesView } from './HousekeepingUserNotesView';
import { HousekeepingUserCard } from './HousekeepingUserCard';
import { HousekeepingUserEconomyView } from './HousekeepingUserEconomyView';
import { DEFAULT_SANCTION_DRAFT, HousekeepingSanctionDraft, HousekeepingUserSanctionsView, sanctionReason } from './HousekeepingUserSanctionsView';
import { Checkbox } from '../../../../common/Checkbox';
import { BaseButton } from '../../../../common/BaseButton';
import { Hint } from '../../../../common/Tooltip';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';

const BULK_CONFIRM_THRESHOLD = 5;

const USER_ACTIVITY_LISTS: HousekeepingListChoice[] = [
    { key: 'user.chatlog', labelKey: 'housekeeping.list.user.chatlog' },
    { key: 'user.visits', labelKey: 'housekeeping.list.user.visits' }
];

const USER_SECURITY_LISTS: HousekeepingListChoice[] = [
    { key: 'user.sanctions', labelKey: 'housekeeping.list.user.sanctions' },
    { key: 'user.clones', labelKey: 'housekeeping.list.user.clones' },
    { key: 'user.names', labelKey: 'housekeeping.list.user.names' }
];

/**
 * The user page: search (with multi-select for bulk actions), the user card,
 * and the sub-pages Sanctions / Economy / Account / History.
 */
export const HousekeepingUsersTab: FC = () => {
    const {
        selectedUser,
        setSelectedUser,
        lookupUserByName,
        lookupUserById,
        isUserLoading,
        isActionPending,
        userSuggestions,
        requestUserSuggestions,
        recentLookups,
        selectedUserIds,
        toggleUserSelection,
        clearUserSelection,
        banUsersBulk,
        kickUsersBulk,
        muteUsersBulk,
        userSection,
        setUserSection,
        actionLog
    } = useHousekeeping();
    const confirmDanger = useHousekeepingDangerConfirm();
    const roomUsers = useRoomUserListSnapshot();
    const [query, setQuery] = useState('');
    const [isFocused, setIsFocused] = useState(false);
    const [draft, setDraft] = useState<HousekeepingSanctionDraft>(DEFAULT_SANCTION_DRAFT);
    const blurTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

    useEffect(
        () => () => {
            if (blurTimerRef.current) clearTimeout(blurTimerRef.current);
        },
        []
    );

    useEffect(() => {
        requestUserSuggestions(query);
    }, [query, requestUserSuggestions]);

    const submitLookup = () => {
        const trimmed = query.trim();

        if (!trimmed.length) return;

        lookupUserByName(trimmed);
        setIsFocused(false);
    };

    const recentUsers = recentLookups.filter((entry) => entry.kind === 'user').slice(0, 5);
    const showSuggestionPanel = isFocused && (userSuggestions.length > 0 || (recentUsers.length > 0 && query.trim().length < 2));
    // The live shortcuts act through the room session, so they only make sense
    // while the target stands in the room the operator is in.
    const isInCurrentRoom = !!selectedUser && roomUsers.some((entry) => entry.webID === selectedUser.id);
    const history = useMemo(() => (selectedUser ? actionLog.filter((entry) => isAuditEntryAboutUser(entry, selectedUser.id)) : []), [actionLog, selectedUser]);
    const reason = sanctionReason(draft);

    const runBulkWithGate = (actionLabel: string, runner: () => void) => {
        if (selectedUserIds.length === 0) return;

        if (selectedUserIds.length >= BULK_CONFIRM_THRESHOLD) {
            confirmDanger(
                LocalizeText('housekeeping.bulk.confirm', ['action', 'count'], [actionLabel, String(selectedUserIds.length)]),
                String(selectedUserIds.length),
                runner,
                actionLabel
            );

            return;
        }

        runner();
    };

    const canEconomy = useHasPermission('acc_hk_economy');
    const allSections: HousekeepingSubTab<HousekeepingUserSection>[] = [
        { id: HousekeepingUserSection.SANCTIONS, label: LocalizeText('housekeeping.user.section.sanctions'), icon: <FaGavel size={9} /> },
        { id: HousekeepingUserSection.ACTIVITY, label: LocalizeText('housekeeping.user.section.activity'), icon: <FaComments size={9} /> },
        { id: HousekeepingUserSection.SECURITY, label: LocalizeText('housekeeping.user.section.security'), icon: <FaShieldAlt size={9} /> },
        { id: HousekeepingUserSection.ACCOUNT, label: LocalizeText('housekeeping.user.section.account'), icon: <FaUserCog size={9} /> },
        { id: HousekeepingUserSection.NOTES, label: LocalizeText('housekeeping.user.section.notes'), icon: <FaStickyNote size={9} /> },
        { id: HousekeepingUserSection.HISTORY, label: LocalizeText('housekeeping.user.section.history'), icon: <FaHistory size={9} />, count: history.length }
    ];
    // The economy page needs its area permission; the server refuses the actions without it anyway.
    const sections = canEconomy
        ? [
              ...allSections.slice(0, 3),
              { id: HousekeepingUserSection.ECONOMY, label: LocalizeText('housekeeping.user.section.economy'), icon: <FaCoins size={9} /> },
              ...allSections.slice(3)
          ]
        : allSections;

    return (
        <NativeTextTree>
            <div className="flex flex-col gap-2">
                <div className="relative">
                    <div className="flex items-center gap-1.5">
                        <div className="flex grow items-center gap-1 rounded-md border border-zinc-300 bg-white px-2 py-1 shadow-sm focus-within:border-sky-400 focus-within:ring-1 focus-within:ring-sky-300">
                            <FaSearch className="shrink-0 text-zinc-400" size={11} />
                            <input
                                className="grow bg-transparent text-xs outline-none placeholder:italic placeholder:text-zinc-500"
                                placeholder={LocalizeText('housekeeping.user.search.placeholder')}
                                value={query}
                                onBlur={() => {
                                    if (blurTimerRef.current) clearTimeout(blurTimerRef.current);
                                    blurTimerRef.current = setTimeout(() => setIsFocused(false), 120);
                                }}
                                onChange={(event) => setQuery(event.target.value)}
                                onFocus={() => setIsFocused(true)}
                                onKeyDown={(event) => {
                                    if (event.key === 'Enter') submitLookup();
                                    if (event.key === 'Escape') setIsFocused(false);
                                }}
                            />
                        </div>
                        <HousekeepingButton disabled={isUserLoading} gap={1} onClick={submitLookup}>
                            <FaSearch className={isUserLoading ? 'animate-pulse' : ''} size={10} />
                            <span>{LocalizeText('housekeeping.user.search.button')}</span>
                        </HousekeepingButton>
                    </div>
                    {showSuggestionPanel && (
                        <div className="octane-hk-popover absolute left-0 right-0 top-full z-30 mt-1 max-h-[200px] overflow-y-auto rounded border border-zinc-200 bg-white shadow-lg">
                            {userSuggestions.length > 0
                                ? userSuggestions.map((entry) => {
                                      const isChecked = selectedUserIds.includes(entry.id);

                                      return (
                                          <div
                                              key={entry.id}
                                              className="flex w-full items-center gap-2 border-b border-zinc-100 px-2 py-1 text-xs last:border-b-0 hover:bg-sky-50"
                                              onMouseDown={(event) => event.preventDefault()}
                                          >
                                              <Checkbox checked={isChecked} className="shrink-0" title={isChecked ? LocalizeText('housekeeping.bulk.clear') : LocalizeText('housekeeping.bulk.apply')} onCheckedChange={() => toggleUserSelection(entry.id)} />
                                              <BaseButton
                                                  className="flex grow items-center gap-2 text-left"
                                                  onClick={() => {
                                                      setQuery(entry.username);
                                                      setIsFocused(false);
                                                      lookupUserById(entry.id);
                                                  }}
                                              >
                                                  <FaCircle className={entry.online ? 'text-emerald-500' : 'text-zinc-400'} size={6} />
                                                  <span className="grow truncate font-medium">{entry.username}</span>
                                                  <span className="shrink-0 text-[10px] text-zinc-500">
                                                      #{entry.id} · {entry.rank}
                                                  </span>
                                              </BaseButton>
                                          </div>
                                      );
                                  })
                                : recentUsers.map((entry) => (
                                      <BaseButton
                                          key={entry.id}
                                          className="flex w-full items-center gap-2 border-b border-zinc-100 px-2 py-1 text-left text-xs last:border-b-0 hover:bg-sky-50"
                                          onClick={() => {
                                              setQuery(entry.label);
                                              setIsFocused(false);
                                              lookupUserById(entry.id);
                                          }}
                                          onMouseDown={(event) => event.preventDefault()}
                                      >
                                          <span className="shrink-0 text-[10px] uppercase text-zinc-400">{LocalizeText('housekeeping.user.recent')}</span>
                                          <span className="grow truncate font-medium">{entry.label}</span>
                                          <span className="shrink-0 text-[10px] text-zinc-500">#{entry.id}</span>
                                      </BaseButton>
                                  ))}
                        </div>
                    )}
                </div>

                {selectedUserIds.length > 0 && (
                    <div className="flex flex-wrap items-center gap-1 rounded border border-sky-300 bg-sky-50 p-1.5">
                        <span className="mr-1 text-[10px] font-semibold uppercase tracking-wide text-sky-800">
                            {LocalizeText('housekeeping.bulk.label', ['count'], [String(selectedUserIds.length)])}
                        </span>
                        <HousekeepingButton
                            disabled={isActionPending}
                            gap={1}
                            size="sm"
                            variant="danger"
                            onClick={() =>
                                runBulkWithGate(LocalizeText('housekeeping.action.ban_h', ['h'], [String(draft.banHours)]), () =>
                                    banUsersBulk(selectedUserIds, reason, draft.banHours)
                                )
                            }
                        >
                            <FaBan size={10} />
                            <span>{LocalizeText('housekeeping.action.ban_h', ['h'], [String(draft.banHours)])}</span>
                        </HousekeepingButton>
                        <HousekeepingButton
                            disabled={isActionPending}
                            gap={1}
                            size="sm"
                            variant="warning"
                            onClick={() =>
                                runBulkWithGate(LocalizeText('housekeeping.action.mute_min', ['m'], [String(draft.muteMinutes)]), () =>
                                    muteUsersBulk(selectedUserIds, reason, draft.muteMinutes)
                                )
                            }
                        >
                            <FaVolumeMute size={10} />
                            <span>{LocalizeText('housekeeping.action.mute_min', ['m'], [String(draft.muteMinutes)])}</span>
                        </HousekeepingButton>
                        <HousekeepingButton
                            disabled={isActionPending}
                            gap={1}
                            size="sm"
                            variant="warning"
                            onClick={() => runBulkWithGate(LocalizeText('housekeeping.action.kick'), () => kickUsersBulk(selectedUserIds, reason))}
                        >
                            <FaUserSlash size={10} />
                            <span>{LocalizeText('housekeeping.action.kick')}</span>
                        </HousekeepingButton>
                        <Hint content={LocalizeText('housekeeping.bulk.clear')}>
                            <BaseButton aria-label={LocalizeText('housekeeping.bulk.clear')}
                                className="ml-auto px-1 text-zinc-500 hover:text-rose-600"
                                onClick={clearUserSelection}
                            >
                                <FaTimes size={10} />
                            </BaseButton>
                        </Hint>
                    </div>
                )}

                {!selectedUser && <HousekeepingEmptyState icon={<FaUserSlash size={14} />}>{LocalizeText('housekeeping.user.none')}</HousekeepingEmptyState>}

                {selectedUser && (
                    <>
                        <HousekeepingUserCard user={selectedUser} onClear={() => setSelectedUser(null)} />
                        <HousekeepingSubTabs<HousekeepingUserSection> active={userSection} tabs={sections} onChange={setUserSection} />
                        {userSection === HousekeepingUserSection.SANCTIONS && (
                            <HousekeepingUserSanctionsView draft={draft} isInCurrentRoom={isInCurrentRoom} setDraft={setDraft} user={selectedUser} />
                        )}
                        {userSection === HousekeepingUserSection.ACTIVITY && (
                            <HousekeepingListView key={`activity-${selectedUser.id}`} lists={USER_ACTIVITY_LISTS} targetId={selectedUser.id} />
                        )}
                        {userSection === HousekeepingUserSection.SECURITY && (
                            <HousekeepingListView key={`security-${selectedUser.id}`} lists={USER_SECURITY_LISTS} targetId={selectedUser.id} />
                        )}
                        {canEconomy && userSection === HousekeepingUserSection.ECONOMY && <HousekeepingUserEconomyView user={selectedUser} />}
                        {userSection === HousekeepingUserSection.ACCOUNT && <HousekeepingUserAccountView key={selectedUser.id} user={selectedUser} />}
                        {userSection === HousekeepingUserSection.NOTES && <HousekeepingUserNotesView key={selectedUser.id} user={selectedUser} />}
                        {userSection === HousekeepingUserSection.HISTORY && <HousekeepingHistoryView entries={history} />}
                    </>
                )}
            </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