import React, { useState, useRef } from 'react'; import { X, Plus, Trash2, Upload, Download } from 'lucide-react'; import { backendApi } from '../../services/apiClient'; export interface BulkBallotRow { apartment: string; ownerName: string; area: number; voteResult: '' | 'for' | 'against' | 'abstain'; notes: string; } interface Props { isOpen: boolean; onClose: () => void; onSuccess: () => void; ossId: string; ossAddress: string; agendaItems?: string[]; } const emptyRow = (): BulkBallotRow => ({ apartment: '', ownerName: '', area: 0, voteResult: '', notes: '', }); export const BulkBallotModal: React.FC = ({ isOpen, onClose, onSuccess, ossId, ossAddress, agendaItems: _agendaItems }) => { const [rows, setRows] = useState([emptyRow()]); const [loading, setLoading] = useState(false); const fileInputRef = useRef(null); if (!isOpen) return null; const addRow = () => setRows((r) => [...r, emptyRow()]); const removeRow = (index: number) => { if (rows.length <= 1) return; setRows((r) => r.filter((_, i) => i !== index)); }; const updateRow = (index: number, field: keyof BulkBallotRow, value: string | number) => { setRows((r) => { const next = [...r]; next[index] = { ...next[index], [field]: value }; return next; }); }; const validRows = rows.filter((r) => r.apartment.trim() && r.area > 0); const handleImportCSV = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const text = String(reader.result); const lines = text.split(/\r?\n/).filter((line) => line.trim()); if (lines.length < 2) return; const sep = lines[0].includes(';') ? ';' : ','; const cols = (line: string) => line.split(sep).map((c) => c.replace(/^"|"$/g, '').trim()); const headerCols = cols(lines[0]).map((c) => c.toLowerCase()); const colIndex = (names: string[]) => headerCols.findIndex((h) => names.some((n) => h.includes(n))); const getCol = (names: string[], arr: string[]) => { const idx = colIndex(names); return idx >= 0 && arr[idx] !== undefined ? arr[idx] : ''; }; const newRows: BulkBallotRow[] = []; for (let i = 1; i < lines.length; i++) { const arr = cols(lines[i]); const apartment = getCol(['apartment', 'помещение', 'кв'], arr) || arr[0] || ''; const ownerName = getCol(['owner_name', 'owner', 'собственник', 'фио'], arr) || arr[1] || ''; const areaStr = getCol(['area', 'площадь', 'площадь (м²)', 'м²'], arr) || arr[2] || '0'; const area = parseFloat(areaStr.replace(/,/, '.')) || 0; const voteStr = (getCol(['vote_result', 'vote', 'голос', 'результат'], arr) || arr[3] || '').toLowerCase(); let voteResult: '' | 'for' | 'against' | 'abstain' = ''; if (voteStr === 'for' || voteStr === 'за') voteResult = 'for'; else if (voteStr === 'against' || voteStr === 'против') voteResult = 'against'; else if (voteStr === 'abstain' || voteStr === 'воздержался') voteResult = 'abstain'; const notes = getCol(['notes', 'примечания'], arr) || arr[4] || ''; if (apartment.trim() || area > 0) { newRows.push({ apartment, ownerName, area, voteResult, notes }); } } if (newRows.length > 0) setRows(newRows); }; reader.readAsText(file, 'UTF-8'); e.target.value = ''; }; const handleDownloadTemplate = () => { const header = 'Помещение;Собственник;Площадь (м²);Голос;Примечания'; const sample = '15;Иванов И.И.;45.2;За;\n25;Петров П.П.;52;Против;\n101;;38.5;Воздержался;'; const csv = [header, sample].join('\n'); const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'шаблон_бюллетеней.csv'; link.click(); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (validRows.length === 0) { alert('Добавьте хотя бы одну строку с помещением и площадью > 0'); return; } try { setLoading(true); const ballots = validRows.map((r) => ({ apartment: r.apartment.trim(), owner_name: r.ownerName.trim() || undefined, area: r.area, vote_result: r.voteResult || undefined, notes: r.notes.trim() || undefined, })); const result = await backendApi.submitOSSBallotsBulk(ossId, ballots); alert(`Внесено: ${result.inserted} новых, обновлено: ${result.updated}. Всего: ${result.total}`); window.dispatchEvent(new CustomEvent('mkd-oss-changed')); onSuccess(); onClose(); setRows([emptyRow()]); } catch (error: any) { console.error('Error submitting bulk ballots:', error); const msg = error?.response?.data?.error || error?.message || error?.error || 'Ошибка при загрузке бюллетеней'; alert(msg); } finally { setLoading(false); } }; return (
e.stopPropagation()} >

Массовый ввод бюллетеней

{ossAddress}

{rows.map((row, index) => ( ))}
Помещение * Собственник Площадь (м²) * Голос Примечания
updateRow(index, 'apartment', e.target.value)} placeholder="15, 25А" className="w-full p-1.5 rounded-lg border border-slate-200 text-sm" /> updateRow(index, 'ownerName', e.target.value)} placeholder="ФИО" className="w-full p-1.5 rounded-lg border border-slate-200 text-sm" /> updateRow(index, 'area', parseFloat(e.target.value) || 0)} placeholder="0" className="w-20 p-1.5 rounded-lg border border-slate-200 text-sm" /> updateRow(index, 'notes', e.target.value)} placeholder="—" className="w-full p-1.5 rounded-lg border border-slate-200 text-sm" />

Строки без помещения или с площадью 0 не отправляются. Формат CSV: разделитель «;», колонки: Помещение;Собственник;Площадь (м²);Голос (За/Против/Воздержался);Примечания

К отправке: {validRows.length} строк
); };