Corp/frontend/src/components/ViewEventDetailsModal.tsx

253 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { Edit2, Trash2, Clock, Calendar, FileText, Users } from "lucide-react";
import { Modal } from "@/components/ui";
import { resolveEventColor } from "@/lib/calendar-event-colors";
import { format } from "date-fns";
import { ru } from "date-fns/locale";
import { useState, useEffect } from "react";
import { apiClient } from "@/lib/api";
interface ViewEventDetailsModalProps {
isOpen: boolean;
onClose: () => void;
event: any | null;
onEdit: () => void;
onDelete: () => void;
showParticipants?: boolean;
}
// Маппинг частот на русский
const FREQUENCY_LABELS: Record<string, string> = {
YEARLY: "Каждый год",
MONTHLY: "Каждый месяц",
WEEKLY: "Каждую неделю",
DAILY: "Каждый день",
HOURLY: "Каждый час",
MINUTELY: "Каждую минуту",
SECONDLY: "Каждую секунду",
};
// Маппинг дней недели (0 = Monday в dateutil.rrule)
const WEEKDAY_LABELS: Record<number, string> = {
0: "ПН",
1: "ВТ",
2: "СР",
3: "ЧТ",
4: "ПТ",
5: "СБ",
6: "ВС",
};
// Нормализация byweekday в массив чисел
function normalizeByweekday(byweekday: any): number[] {
if (!byweekday) return [];
if (Array.isArray(byweekday)) return byweekday;
if (typeof byweekday === 'string') {
return byweekday.split(',').map(d => parseInt(d.trim(), 10)).filter(n => !isNaN(n));
}
if (typeof byweekday === 'number') return [byweekday];
return [];
}
export function ViewEventDetailsModal({
isOpen,
onClose,
event,
onEdit,
onDelete,
showParticipants = false,
}: ViewEventDetailsModalProps) {
const [participants, setParticipants] = useState<any[]>([]);
const [loadingParticipants, setLoadingParticipants] = useState(false);
useEffect(() => {
if (isOpen && event?.id && showParticipants) {
loadParticipants();
} else {
setParticipants([]);
}
}, [isOpen, event?.id, showParticipants]);
const loadParticipants = async () => {
if (!event?.id) return;
try {
setLoadingParticipants(true);
const result = await apiClient.getEventParticipants(event.id);
const participantsList = Array.isArray(result) ? result : (result?.results || []);
setParticipants(participantsList);
} catch (error) {
console.error("Failed to load participants:", error);
} finally {
setLoadingParticipants(false);
}
};
if (!isOpen || !event) return null;
const formatDateTime = (dateStr: string) => {
const date = new Date(dateStr);
return format(date, "d MMMM yyyy, HH:mm", { locale: ru });
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return format(date, "d MMMM yyyy", { locale: ru });
};
const capitalizedStart = formatDateTime(event.start).charAt(0).toUpperCase() + formatDateTime(event.start).slice(1);
const capitalizedEnd = formatDateTime(event.end).charAt(0).toUpperCase() + formatDateTime(event.end).slice(1);
const canEdit = event.can_edit !== false;
const canDelete = event.can_delete !== false;
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={event.title}
size="sm"
footer={canEdit || canDelete ? (
<div className="flex gap-2">
{canEdit ? (
<button
onClick={onEdit}
className="app-action-primary flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium"
>
<Edit2 size={16} />
Редактировать
</button>
) : null}
{canDelete ? (
<button
onClick={onDelete}
className="app-action-danger rounded-lg px-4 py-2.5 text-sm font-medium"
title="Удалить событие"
>
<Trash2 size={16} />
</button>
) : null}
</div>
) : undefined}
>
<div className="space-y-4">
{/* Event meta */}
<div className="flex items-center gap-2">
{event.rule && (
<span className="app-badge app-badge-accent shrink-0 rounded px-1.5 py-0.5 text-xs">
⟲
</span>
)}
<div className="app-text-muted flex items-center gap-1.5 text-xs">
<div
className="h-2.5 w-2.5 rounded-full flex-shrink-0"
style={{ backgroundColor: resolveEventColor(event.color_event) }}
/>
<span className="truncate">Календарь #{event.calendar}</span>
</div>
</div>
{/* Time */}
<div className="flex items-start gap-2.5">
<Clock size={18} className="app-text-muted mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="mb-1 text-sm font-medium text-[var(--foreground)]">Время</p>
<p className="app-text-muted text-xs">
<span className="font-medium">Начало:</span> {capitalizedStart}
</p>
<p className="app-text-muted mt-0.5 text-xs">
<span className="font-medium">Конец:</span> {capitalizedEnd}
</p>
</div>
</div>
{/* Recurring Info */}
{event.rule && event.rule_data && (
<div className="app-selected space-y-2 rounded-lg p-4">
<div className="app-accent-text flex items-center gap-2 text-sm font-medium">
<Calendar size={16} />
<span>Повторяющееся событие</span>
</div>
<div className="app-accent-text space-y-1 text-sm">
<div>
Частота: <span className="font-medium">{FREQUENCY_LABELS[event.rule_data.frequency] || event.rule_data.frequency}</span>
</div>
{event.rule_data.params?.byweekday && (
<div>
Дни недели: <span className="font-medium">
{normalizeByweekday(event.rule_data.params.byweekday).map((day: number) => WEEKDAY_LABELS[day]).join(", ")}
</span>
</div>
)}
{event.rule_data.params?.count && (
<div>
Повторений: <span className="font-medium">{event.rule_data.params.count}</span>
</div>
)}
{event.end_recurring_period && (
<div>
До: <span className="font-medium">{formatDate(event.end_recurring_period)}</span>
</div>
)}
</div>
</div>
)}
{/* Description */}
{event.description && (
<div className="flex items-start gap-2.5">
<FileText size={18} className="app-text-muted mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="mb-1 text-sm font-medium text-[var(--foreground)]">Описание</p>
<p className="app-text-wrap app-text-muted whitespace-pre-wrap text-xs">
{event.description}
</p>
</div>
</div>
)}
{/* Participants */}
{showParticipants && (
<div className="flex items-start gap-2.5">
<Users size={18} className="app-text-muted mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="mb-1.5 text-sm font-medium text-[var(--foreground)]">
Участники {participants.length > 0 && `(${participants.length})`}
</p>
{loadingParticipants ? (
<p className="app-text-muted text-xs">Загрузка...</p>
) : participants.length > 0 ? (
<div className="space-y-1.5">
{participants.map((participant: any) => (
<div
key={participant.id}
className="app-surface-muted flex items-center gap-2 rounded-lg px-2.5 py-2"
>
<div className="app-avatar-fallback flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full">
<span className="text-xs font-medium">
{participant.user_name?.[0] || "?"}
</span>
</div>
<div className="text-xs flex-1 min-w-0">
<div className="truncate font-medium text-[var(--foreground)]">
{participant.user_name}
</div>
<div className="app-text-muted">
{participant.distinction || "attendee"}
</div>
</div>
</div>
))}
</div>
) : (
<p className="app-text-muted text-xs">Нет участников</p>
)}
</div>
</div>
)}
</div>
</Modal>
);
}