700 lines
29 KiB
TypeScript
Executable File
700 lines
29 KiB
TypeScript
Executable File
|
||
import React, { useState, useEffect } from 'react';
|
||
import { ResidentReport, Building, WorkPhoto } from '../../types';
|
||
import { apiClient } from '../../services/apiClient';
|
||
import { FileText, Sparkles, Send, Download, Eye, CheckCircle2, History, Bot, X, Loader2, Calendar, Link2, Key } from 'lucide-react';
|
||
import { BuildingReportPage } from './BuildingReportPage';
|
||
|
||
export const ResidentReports: React.FC = () => {
|
||
const [reports, setReports] = useState<ResidentReport[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [selectedReportId, setSelectedReportId] = useState<string | number | null>(null);
|
||
const [showAccessKeyModal, setShowAccessKeyModal] = useState(false);
|
||
|
||
useEffect(() => {
|
||
loadReports();
|
||
}, []);
|
||
|
||
const loadReports = async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
const data = await apiClient.get<ResidentReport[]>('/pr/reports');
|
||
setReports(data);
|
||
} catch (err) {
|
||
console.error('Error loading reports:', err);
|
||
setReports([]);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handlePublish = async (reportId: string | number) => {
|
||
try {
|
||
// При публикации отчет обновляется актуальными данными
|
||
await apiClient.post(`/pr/reports/${reportId}/publish`);
|
||
await loadReports();
|
||
alert('Отчет обновлен актуальными данными и опубликован');
|
||
} catch (err: any) {
|
||
console.error('Error publishing report:', err);
|
||
alert(`Ошибка публикации отчета: ${err.message || 'Неизвестная ошибка'}`);
|
||
}
|
||
};
|
||
|
||
// Если выбран отчет для просмотра, показываем страницу отчета
|
||
if (selectedReportId) {
|
||
// Для демо используем BuildingReportPage
|
||
if (selectedReportId === 'demo') {
|
||
return (
|
||
<BuildingReportPage
|
||
buildingAddress="Кавказская, 12"
|
||
month="Январь 2025"
|
||
onBack={() => setSelectedReportId(null)}
|
||
mode="portal"
|
||
reportId="demo"
|
||
/>
|
||
);
|
||
}
|
||
|
||
// Для реальных отчетов используем BuildingReportPage с данными из БД
|
||
const report = reports.find(r => r.id === selectedReportId);
|
||
if (report) {
|
||
// Определяем текущий месяц из отчета или используем текущий месяц
|
||
const now = new Date();
|
||
const currentMonthName = now.toLocaleDateString('ru-RU', { month: 'long' });
|
||
const currentYear = now.getFullYear();
|
||
const initialMonth = report.month || `${currentMonthName} ${currentYear}`;
|
||
|
||
return (
|
||
<BuildingReportPage
|
||
buildingId={report.buildingId}
|
||
buildingAddress={report.address || `Отчет #${report.id}`}
|
||
month={initialMonth}
|
||
onBack={() => setSelectedReportId(null)}
|
||
mode="portal"
|
||
reportId={report.buildingId} // Используем buildingId для загрузки данных по месяцам
|
||
/>
|
||
);
|
||
}
|
||
|
||
// Fallback, если отчет не найден
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center">
|
||
<div className="text-center">
|
||
<p className="text-slate-600 mb-4">Отчет не найден</p>
|
||
<button
|
||
onClick={() => setSelectedReportId(null)}
|
||
className="px-4 py-2 bg-primary-600 text-white rounded-xl"
|
||
>
|
||
Вернуться к списку
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6 animate-fade-in">
|
||
{/* Header */}
|
||
<div className="flex justify-between items-center">
|
||
<div>
|
||
<h3 className="font-black text-slate-500 text-[10px] uppercase tracking-[0.2em]">Реестр ежемесячных отчетов</h3>
|
||
<p className="text-xs text-slate-400 mt-1">Отчеты создаются автоматически 1 числа каждого месяца</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* List of Reports */}
|
||
{isLoading ? (
|
||
<div className="flex justify-center items-center py-12">
|
||
<Loader2 className="w-8 h-8 animate-spin text-slate-400" />
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
|
||
{reports.map(report => (
|
||
<div key={report.id} className="bg-white p-6 rounded-[2rem] border border-slate-200 shadow-sm flex flex-col md:flex-row justify-between gap-6 hover:border-primary-200 transition-all group">
|
||
<div className="flex items-center gap-4 flex-1">
|
||
<div className={`p-4 rounded-3xl ${report.status === 'published' ? 'bg-emerald-50 text-emerald-600' : 'bg-amber-50 text-amber-600'}`}>
|
||
<FileText className="w-8 h-8"/>
|
||
</div>
|
||
<div>
|
||
<h4 className="font-black text-slate-800 text-base leading-tight group-hover:text-primary-600 transition-colors">
|
||
{report.address || `Отчет #${report.id}`}
|
||
</h4>
|
||
<p className="text-[10px] text-slate-400 font-bold uppercase mt-1">
|
||
{report.month} • {report.status === 'published' ? 'Опубликован' : 'Черновик'}
|
||
</p>
|
||
|
||
{(report.liveStats || (report.content && typeof report.content === 'object')) && (
|
||
<div className="flex gap-4 mt-4">
|
||
<ReportStat
|
||
label="Заявок"
|
||
value={report.liveStats?.applicationsTotal ?? report.content?.applications?.total ?? 0}
|
||
/>
|
||
<ReportStat
|
||
label="NPS"
|
||
value={report.liveStats?.npsScore ?? (report.content as any)?.nps?.score ?? report.content?.statistics?.nps ?? 0}
|
||
/>
|
||
<ReportStat
|
||
label="Собрано"
|
||
value={(() => {
|
||
const collected = report.liveStats?.fundsCollected ?? (report.content as any)?.finances?.collected ?? report.content?.finances?.income ?? 0;
|
||
return `${(collected / 1000000).toFixed(1)}M ₽`;
|
||
})()}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between md:justify-end gap-2 border-t md:border-t-0 border-slate-50 pt-4 md:pt-0">
|
||
<button
|
||
onClick={() => setSelectedReportId(report.id)}
|
||
className="p-3 bg-slate-50 text-slate-500 rounded-2xl hover:bg-slate-100"
|
||
title="Открыть отчет"
|
||
>
|
||
<Eye className="w-5 h-5"/>
|
||
</button>
|
||
<button className="p-3 bg-slate-50 text-slate-500 rounded-2xl hover:bg-slate-100" title="Скачать PDF">
|
||
<Download className="w-5 h-5"/>
|
||
</button>
|
||
{report.status === 'draft' ? (
|
||
<button
|
||
onClick={() => handlePublish(report.id)}
|
||
className="bg-primary-600 text-white px-5 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest shadow-lg flex items-center gap-2 ml-2 active:scale-95 transition-all"
|
||
title="Обновить данные и опубликовать отчет"
|
||
>
|
||
<Send className="w-4 h-4"/> Опубликовать
|
||
</button>
|
||
) : (
|
||
<div className="bg-emerald-50 text-emerald-600 px-5 py-3 rounded-2xl text-[10px] font-black uppercase flex items-center gap-2 border border-emerald-100 ml-2">
|
||
<CheckCircle2 className="w-4 h-4"/> Опубликован
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Модальное окно с ключом доступа */}
|
||
{showAccessKeyModal && (
|
||
<AccessKeyModal
|
||
onClose={() => setShowAccessKeyModal(false)}
|
||
reportId="demo"
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const ReportStat = ({ label, value }: { label: string; value: string | number }) => (
|
||
<div className="text-left">
|
||
<p className="text-[8px] font-black text-slate-400 uppercase tracking-tighter">{label}</p>
|
||
<p className="text-xs font-black text-slate-700">{value}</p>
|
||
</div>
|
||
);
|
||
|
||
interface ReportCreateFormProps {
|
||
onClose: () => void;
|
||
onSuccess: () => void;
|
||
selectedBuilding: Building | null;
|
||
onBuildingSelect: (building: Building | null) => void;
|
||
}
|
||
|
||
const ReportCreateForm: React.FC<ReportCreateFormProps> = ({ onClose, onSuccess, selectedBuilding, onBuildingSelect }) => {
|
||
const [buildings, setBuildings] = useState<Building[]>([]);
|
||
const [formData, setFormData] = useState({
|
||
building_id: '',
|
||
month: '',
|
||
period_start: '',
|
||
period_end: '',
|
||
createForAll: false, // Создать для всех домов
|
||
selectedBuildings: [] as string[] // Выбранные дома для массового создания
|
||
});
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [reportData, setReportData] = useState<any>(null);
|
||
const [isGenerating, setIsGenerating] = useState(false);
|
||
const [creationProgress, setCreationProgress] = useState<{
|
||
total: number;
|
||
completed: number;
|
||
current: string;
|
||
} | null>(null);
|
||
|
||
useEffect(() => {
|
||
loadBuildings();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (formData.building_id && formData.period_start && formData.period_end) {
|
||
loadReportData();
|
||
}
|
||
}, [formData.building_id, formData.period_start, formData.period_end]);
|
||
|
||
const loadBuildings = async () => {
|
||
try {
|
||
const data = await apiClient.get<Building[]>('/buildings');
|
||
setBuildings(data);
|
||
} catch (err) {
|
||
console.error('Error loading buildings:', err);
|
||
}
|
||
};
|
||
|
||
const loadReportData = async () => {
|
||
try {
|
||
setIsGenerating(true);
|
||
// Загружаем данные для отчета
|
||
const building = buildings.find(b => b.id === formData.building_id);
|
||
if (!building) return;
|
||
|
||
// Заявки
|
||
const applications = await apiClient.get(`/applications`).catch(() => []);
|
||
|
||
// Финансовые данные
|
||
const financialData = await apiClient.get(`/finance/buildings/${formData.building_id}/summary`).catch(() => null);
|
||
|
||
// Фото отчеты
|
||
const workPhotos = await apiClient.get<WorkPhoto[]>(`/pr/work-photos?building_id=${formData.building_id}`).catch(() => []);
|
||
|
||
setReportData({
|
||
building,
|
||
applications: Array.isArray(applications) ? applications : [],
|
||
financialData,
|
||
workPhotos: Array.isArray(workPhotos) ? workPhotos : []
|
||
});
|
||
} catch (err) {
|
||
console.error('Error loading report data:', err);
|
||
} finally {
|
||
setIsGenerating(false);
|
||
}
|
||
};
|
||
|
||
const handleGenerate = async () => {
|
||
if (!formData.period_start || !formData.period_end) {
|
||
alert('Заполните период отчета');
|
||
return;
|
||
}
|
||
|
||
if (!formData.createForAll && !formData.building_id) {
|
||
alert('Выберите дом или включите создание для всех домов');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setIsGenerating(true);
|
||
setIsSubmitting(true);
|
||
|
||
if (formData.createForAll) {
|
||
// Массовое создание для всех домов
|
||
const targetBuildings = formData.selectedBuildings.length > 0
|
||
? formData.selectedBuildings
|
||
: buildings.map(b => b.id);
|
||
|
||
setCreationProgress({ total: targetBuildings.length, completed: 0, current: 'Начало создания...' });
|
||
|
||
const result = await apiClient.post<{
|
||
success: boolean;
|
||
reportsCreated: number;
|
||
reports: Array<{ buildingId: string; reportId: number; updated: boolean }>;
|
||
}>('/pr/reports/bulk-create', {
|
||
month: formData.month,
|
||
period_start: formData.period_start,
|
||
period_end: formData.period_end,
|
||
building_ids: targetBuildings
|
||
});
|
||
|
||
setCreationProgress({
|
||
total: targetBuildings.length,
|
||
completed: result.reportsCreated,
|
||
current: 'Завершено'
|
||
});
|
||
|
||
setTimeout(() => {
|
||
setCreationProgress(null);
|
||
alert(`Успешно создано отчетов: ${result.reportsCreated} из ${targetBuildings.length}`);
|
||
onSuccess();
|
||
}, 1000);
|
||
} else {
|
||
// Создание для одного дома
|
||
const report = await apiClient.post<ResidentReport>('/pr/reports', {
|
||
building_id: formData.building_id,
|
||
month: formData.month,
|
||
period_start: formData.period_start,
|
||
period_end: formData.period_end
|
||
});
|
||
|
||
// Генерируем контент
|
||
await apiClient.post(`/pr/reports/${report.id}/generate`);
|
||
|
||
onSuccess();
|
||
}
|
||
} catch (err: any) {
|
||
console.error('Error creating report:', err);
|
||
alert(`Ошибка создания отчета: ${err.message || 'Неизвестная ошибка'}`);
|
||
} finally {
|
||
setIsGenerating(false);
|
||
setIsSubmitting(false);
|
||
setCreationProgress(null);
|
||
}
|
||
};
|
||
|
||
const selectedBuildingData = buildings.find(b => b.id === formData.building_id);
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto">
|
||
<div className="bg-white rounded-3xl p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto my-8">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<h3 className="text-xl font-black text-slate-800">Создать отчет жителям</h3>
|
||
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-xl">
|
||
<X className="w-5 h-5 text-slate-400" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
{/* Опция создания для всех домов */}
|
||
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-4">
|
||
<label className="flex items-center gap-3 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.createForAll}
|
||
onChange={e => setFormData({
|
||
...formData,
|
||
createForAll: e.target.checked,
|
||
building_id: e.target.checked ? '' : formData.building_id
|
||
})}
|
||
className="w-5 h-5 text-indigo-600 rounded focus:ring-indigo-500"
|
||
/>
|
||
<div>
|
||
<span className="font-black text-slate-800">Создать отчеты для всех домов</span>
|
||
<p className="text-xs text-slate-600 mt-1">
|
||
Автоматически создаст отчеты для всех домов за указанный период
|
||
</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
|
||
{/* Форма выбора дома и периода */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{!formData.createForAll ? (
|
||
<div>
|
||
<label className="block text-xs font-black text-slate-600 uppercase tracking-wider mb-2">
|
||
Дом
|
||
</label>
|
||
<select
|
||
value={formData.building_id}
|
||
onChange={e => setFormData({ ...formData, building_id: e.target.value })}
|
||
className="w-full p-3 border border-slate-200 rounded-xl text-sm"
|
||
required={!formData.createForAll}
|
||
>
|
||
<option value="">Выберите дом</option>
|
||
{buildings.map(b => (
|
||
<option key={b.id} value={b.id}>
|
||
{b.passport?.address || b.id}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
<label className="block text-xs font-black text-slate-600 uppercase tracking-wider mb-2">
|
||
Выбрать дома (оставьте пустым для всех)
|
||
</label>
|
||
<div className="max-h-48 overflow-y-auto border border-slate-200 rounded-xl p-3 space-y-2">
|
||
{buildings.map(b => (
|
||
<label key={b.id} className="flex items-center gap-2 cursor-pointer hover:bg-slate-50 p-2 rounded">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.selectedBuildings.includes(b.id)}
|
||
onChange={e => {
|
||
if (e.target.checked) {
|
||
setFormData({
|
||
...formData,
|
||
selectedBuildings: [...formData.selectedBuildings, b.id]
|
||
});
|
||
} else {
|
||
setFormData({
|
||
...formData,
|
||
selectedBuildings: formData.selectedBuildings.filter(id => id !== b.id)
|
||
});
|
||
}
|
||
}}
|
||
className="w-4 h-4 text-indigo-600 rounded"
|
||
/>
|
||
<span className="text-sm text-slate-700">{b.passport?.address || b.id}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
<p className="text-xs text-slate-500 mt-2">
|
||
Выбрано: {formData.selectedBuildings.length || 'Все'} домов
|
||
</p>
|
||
</div>
|
||
)}
|
||
<div>
|
||
<label className="block text-xs font-black text-slate-600 uppercase tracking-wider mb-2">
|
||
Месяц (например: Май 2024)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.month}
|
||
onChange={e => setFormData({ ...formData, month: e.target.value })}
|
||
className="w-full p-3 border border-slate-200 rounded-xl text-sm"
|
||
placeholder="Май 2024"
|
||
required
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-black text-slate-600 uppercase tracking-wider mb-2">
|
||
Период начала
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={formData.period_start}
|
||
onChange={e => setFormData({ ...formData, period_start: e.target.value })}
|
||
className="w-full p-3 border border-slate-200 rounded-xl text-sm"
|
||
required
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs font-black text-slate-600 uppercase tracking-wider mb-2">
|
||
Период окончания
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={formData.period_end}
|
||
onChange={e => setFormData({ ...formData, period_end: e.target.value })}
|
||
className="w-full p-3 border border-slate-200 rounded-xl text-sm"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Прогресс создания */}
|
||
{creationProgress && (
|
||
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h4 className="font-black text-slate-800">Создание отчетов</h4>
|
||
<span className="text-sm font-bold text-indigo-600">
|
||
{creationProgress.completed} / {creationProgress.total}
|
||
</span>
|
||
</div>
|
||
<div className="w-full bg-slate-200 rounded-full h-3 mb-2">
|
||
<div
|
||
className="bg-indigo-600 h-3 rounded-full transition-all duration-300"
|
||
style={{ width: `${(creationProgress.completed / creationProgress.total) * 100}%` }}
|
||
/>
|
||
</div>
|
||
{creationProgress.current && (
|
||
<p className="text-xs text-slate-600 mt-2">
|
||
Обрабатывается: {creationProgress.current}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Данные отчета */}
|
||
{isGenerating && !creationProgress && (
|
||
<div className="flex justify-center py-8">
|
||
<Loader2 className="w-8 h-8 animate-spin text-primary-600" />
|
||
</div>
|
||
)}
|
||
|
||
{reportData && !isGenerating && (
|
||
<div className="space-y-4 border-t pt-6">
|
||
<h4 className="font-black text-slate-800 text-lg">Данные для отчета</h4>
|
||
|
||
{/* Информация о доме */}
|
||
{reportData.building && (
|
||
<div className="bg-slate-50 p-4 rounded-xl">
|
||
<h5 className="font-bold text-slate-800 mb-2">Информация о доме</h5>
|
||
<p className="text-sm text-slate-600">
|
||
{reportData.building.passport?.address || 'Адрес не указан'}
|
||
</p>
|
||
{reportData.building.passport?.apartmentsCount && (
|
||
<p className="text-xs text-slate-500 mt-1">
|
||
Квартир: {reportData.building.passport.apartmentsCount}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Заявки */}
|
||
{reportData.applications && (
|
||
<div className="bg-slate-50 p-4 rounded-xl">
|
||
<h5 className="font-bold text-slate-800 mb-2">Заявки за период</h5>
|
||
<p className="text-sm text-slate-600">
|
||
Всего: {reportData.applications.length}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Финансы */}
|
||
{reportData.financialData && (
|
||
<div className="bg-slate-50 p-4 rounded-xl">
|
||
<h5 className="font-bold text-slate-800 mb-2">Финансы</h5>
|
||
<p className="text-sm text-slate-600">
|
||
Доходы: {reportData.financialData.totalIncome || 0} ₽
|
||
</p>
|
||
<p className="text-sm text-slate-600">
|
||
Расходы: {reportData.financialData.totalExpenses || 0} ₽
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Фото отчеты */}
|
||
{reportData.workPhotos && reportData.workPhotos.length > 0 && (
|
||
<div className="bg-slate-50 p-4 rounded-xl">
|
||
<h5 className="font-bold text-slate-800 mb-2">Фото отчеты</h5>
|
||
<p className="text-sm text-slate-600">
|
||
Работ: {reportData.workPhotos.length}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Кнопки */}
|
||
<div className="flex gap-3 pt-4 border-t">
|
||
<button
|
||
onClick={handleGenerate}
|
||
disabled={isSubmitting || isGenerating || (!formData.createForAll && !formData.building_id) || !formData.period_start || !formData.period_end}
|
||
className="flex-1 px-6 py-3 bg-primary-600 text-white rounded-xl text-xs font-black uppercase tracking-widest disabled:opacity-50"
|
||
>
|
||
{isSubmitting || isGenerating
|
||
? (formData.createForAll ? `Создание отчетов...` : 'Создание...')
|
||
: (formData.createForAll ? `Создать отчеты для всех домов` : 'Создать отчет')}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="px-6 py-3 bg-slate-100 text-slate-600 rounded-xl text-xs font-black uppercase tracking-widest"
|
||
>
|
||
Отмена
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Модальное окно с ключом доступа
|
||
interface AccessKeyModalProps {
|
||
onClose: () => void;
|
||
reportId: string | number;
|
||
}
|
||
|
||
const AccessKeyModal: React.FC<AccessKeyModalProps> = ({ onClose, reportId }) => {
|
||
const accessKey = `mkd-${String(reportId)}-key`; // Уникальный ключ на каждый отчёт (демо)
|
||
const link = `${window.location.origin}/reports/${reportId}?mode=published&key=${accessKey}`;
|
||
const [copied, setCopied] = useState(false);
|
||
const [showKey, setShowKey] = useState(false);
|
||
|
||
const handleCopyLink = () => {
|
||
navigator.clipboard.writeText(link);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 2000);
|
||
};
|
||
|
||
const handleCopyKey = () => {
|
||
navigator.clipboard.writeText(accessKey);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 2000);
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-3xl p-6 max-w-lg w-full shadow-2xl">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="p-2 bg-indigo-100 rounded-xl">
|
||
<Key className="w-6 h-6 text-indigo-600" />
|
||
</div>
|
||
<h3 className="text-xl font-black text-slate-800">Ключ доступа к отчету</h3>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="p-2 hover:bg-slate-100 rounded-xl transition-colors"
|
||
>
|
||
<X className="w-5 h-5 text-slate-400" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-xs font-black text-slate-600 uppercase tracking-wider mb-2">
|
||
Ключ доступа
|
||
</label>
|
||
<div className="flex gap-2">
|
||
<div className="flex-1 relative">
|
||
<input
|
||
type={showKey ? "text" : "password"}
|
||
value={accessKey}
|
||
readOnly
|
||
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 pr-12"
|
||
/>
|
||
<button
|
||
onClick={() => setShowKey(!showKey)}
|
||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||
type="button"
|
||
>
|
||
{showKey ? <X className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
<button
|
||
onClick={handleCopyKey}
|
||
className="px-4 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-sm font-bold transition-colors"
|
||
>
|
||
{copied ? '✓' : 'Копировать'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-black text-slate-600 uppercase tracking-wider mb-2">
|
||
Ссылка на опубликованную версию
|
||
</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={link}
|
||
readOnly
|
||
className="flex-1 px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||
/>
|
||
<button
|
||
onClick={handleCopyLink}
|
||
className="px-4 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-sm font-bold transition-colors"
|
||
>
|
||
{copied ? '✓' : 'Копировать'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-4 bg-amber-50 border border-amber-200 rounded-xl">
|
||
<p className="text-xs text-amber-800 leading-relaxed">
|
||
<strong className="font-black">Важно:</strong> Сохраните ключ доступа в безопасном месте.
|
||
Он потребуется для просмотра опубликованной версии отчета.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-2">
|
||
<button
|
||
onClick={() => {
|
||
window.open(link, '_blank');
|
||
onClose();
|
||
}}
|
||
className="flex-1 px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-sm font-bold transition-colors"
|
||
>
|
||
Открыть опубликованную версию
|
||
</button>
|
||
<button
|
||
onClick={onClose}
|
||
className="px-6 py-3 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl text-sm font-bold transition-colors"
|
||
>
|
||
Закрыть
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|