- Added TelegramStopAliasService for handling stop aliases in the database. - Created API endpoints for managing stop aliases (list, create, update, delete). - Introduced TelegramStopAliases component in the frontend for user interaction with stop aliases. - Updated TelegramImportService to utilize stop aliases for message processing. - Enhanced telegramCancellationParser to resolve stop names using aliases. - Added migration script to create telegram_stop_aliases table and seed initial data. - Updated documentation to include details about stop alias functionality.
95 lines
7.3 KiB
JavaScript
95 lines
7.3 KiB
JavaScript
import { useEffect, useState } from 'react';
|
||
import { BookOpen, Pencil, Trash2, X } from 'lucide-react';
|
||
import { stopsApi, telegramImportApi } from '../services/api';
|
||
import { useCity } from '../contexts/CityContext';
|
||
|
||
const emptyForm = { alias: '', stop_id: '' };
|
||
|
||
export default function TelegramStopAliases({ onChanged }) {
|
||
const { activeCity } = useCity();
|
||
const [open, setOpen] = useState(false);
|
||
const [aliases, setAliases] = useState([]);
|
||
const [stops, setStops] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [busy, setBusy] = useState(false);
|
||
const [editingId, setEditingId] = useState(null);
|
||
const [form, setForm] = useState(emptyForm);
|
||
const [error, setError] = useState('');
|
||
const [notice, setNotice] = useState('');
|
||
|
||
useEffect(() => {
|
||
setEditingId(null); setForm(emptyForm); setNotice(''); setError('');
|
||
setAliases([]); setStops([]);
|
||
if (!open) return;
|
||
let current = true;
|
||
setLoading(true);
|
||
Promise.all([telegramImportApi.getStopAliases(), stopsApi.getAll()])
|
||
.then(([dictionary, catalog]) => { if (current) { setAliases(dictionary.data); setStops(catalog.data); } })
|
||
.catch(err => { if (current) setError(err.response?.data?.error || 'Не удалось загрузить словарь остановок.'); })
|
||
.finally(() => { if (current) setLoading(false); });
|
||
return () => { current = false; };
|
||
}, [open, activeCity?.id]);
|
||
|
||
const refresh = async () => { setAliases((await telegramImportApi.getStopAliases()).data); onChanged(); };
|
||
const save = async event => {
|
||
event.preventDefault(); setBusy(true); setError(''); setNotice('');
|
||
try {
|
||
if (editingId === null) await telegramImportApi.createStopAlias(form);
|
||
else await telegramImportApi.updateStopAlias(editingId, form);
|
||
setEditingId(null); setForm(emptyForm); await refresh();
|
||
setNotice('Соответствие сохранено. При следующем чтении канала сообщения на проверке за последние три дня будут разобраны заново.');
|
||
} catch (err) { setError(err.response?.data?.error || 'Не удалось сохранить соответствие.'); }
|
||
finally { setBusy(false); }
|
||
};
|
||
const remove = async id => {
|
||
setBusy(true); setError(''); setNotice('');
|
||
try {
|
||
await telegramImportApi.deleteStopAlias(id);
|
||
if (editingId === id) { setEditingId(null); setForm(emptyForm); }
|
||
await refresh(); setNotice('Соответствие удалено.');
|
||
} catch (err) { setError(err.response?.data?.error || 'Не удалось удалить соответствие.'); }
|
||
finally { setBusy(false); }
|
||
};
|
||
|
||
return <div className="mt-4 border-t pt-3">
|
||
<button type="button" className="text-sm text-primary-600 inline-flex items-center gap-2 hover:underline" aria-expanded={open} onClick={() => setOpen(!open)} disabled={busy}>
|
||
<BookOpen className="h-4 w-4" />{open ? 'Скрыть словарь остановок' : 'Словарь названий остановок'}
|
||
</button>
|
||
{open && <div className="mt-3 space-y-3">
|
||
<p className="text-sm text-gray-600">Свяжите название из канала с остановкой {activeCity?.name ? `города ${activeCity.name}` : 'выбранного города'}. Например: ЗОДЧЕГО → Зодчего Иоанна. Регистр, пробелы и знаки препинания не учитываются.</p>
|
||
{error && <p role="alert" className="text-sm text-red-700">{error}</p>}
|
||
{notice && <p role="status" className="text-sm text-green-700">{notice}</p>}
|
||
{loading ? <p className="text-sm text-gray-500">Загрузка словаря…</p> : <>
|
||
<form onSubmit={save} className="rounded-lg bg-gray-50 p-3 space-y-3">
|
||
<div className="flex justify-between items-center"><h3 className="text-sm font-medium">{editingId === null ? 'Новое соответствие' : 'Изменить соответствие'}</h3>
|
||
{editingId !== null && <button type="button" aria-label="Отменить изменение соответствия" onClick={() => { setEditingId(null); setForm(emptyForm); }} disabled={busy}><X className="h-4 w-4" /></button>}
|
||
</div>
|
||
<div className="grid sm:grid-cols-2 gap-3">
|
||
<label className="text-sm">Название в канале<input className="input mt-1 w-full" placeholder="ЗОДЧЕГО" value={form.alias} maxLength={200} onChange={e => setForm({ ...form, alias: e.target.value })} required disabled={busy} /></label>
|
||
<label className="text-sm">Остановка в админке<select className="input mt-1 w-full" value={form.stop_id} onChange={e => setForm({ ...form, stop_id: e.target.value })} required disabled={busy}>
|
||
<option value="">Выберите остановку</option>
|
||
{form.stop_id && !stops.some(stop => Number(stop.id) === Number(form.stop_id)) && <option value={form.stop_id} disabled>Остановка отключена — выберите другую</option>}
|
||
{stops.map(stop => <option key={stop.id} value={stop.id}>{stop.name}{stop.address ? ` · ${stop.address}` : ''} · #{stop.id}</option>)}
|
||
</select></label>
|
||
</div>
|
||
<button type="submit" className="btn-primary text-sm" disabled={busy || !stops.length}>{editingId === null ? 'Добавить соответствие' : 'Сохранить соответствие'}</button>
|
||
</form>
|
||
<div className="space-y-2">
|
||
{aliases.map(alias => <div key={alias.id} className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-gray-200 p-3">
|
||
<div className="min-w-0 break-words text-sm"><strong>{alias.alias}</strong> <span className="text-gray-400">→</span> {alias.stop_name}
|
||
{!alias.stop_active && <span className="text-xs text-amber-700 ml-2">Остановка отключена</span>}
|
||
<p className="text-xs text-gray-500">{alias.stop_address ? `${alias.stop_address} · ` : ''}Остановка #{alias.stop_id}</p>
|
||
</div>
|
||
<div className="flex gap-3 shrink-0">
|
||
<button type="button" className="text-primary-600 text-sm inline-flex items-center gap-1" disabled={busy} onClick={() => { setEditingId(alias.id); setForm({ alias: alias.alias, stop_id: String(alias.stop_id) }); setNotice(''); setError(''); }}><Pencil className="h-3 w-3" />Изменить</button>
|
||
<button type="button" className="text-red-600 text-sm inline-flex items-center gap-1" disabled={busy} onClick={() => remove(alias.id)}><Trash2 className="h-3 w-3" />Удалить</button>
|
||
</div>
|
||
</div>)}
|
||
{!aliases.length && <p className="text-sm text-gray-500">Добавьте первое соответствие. Для нескольких вариантов одного названия можно создать отдельные записи.</p>}
|
||
</div>
|
||
<p className="text-xs text-gray-500">Изменения применяются к новым сообщениям и сообщениям на проверке. Опубликованные уведомления сохраняют свой текст.</p>
|
||
</>}
|
||
</div>}
|
||
</div>;
|
||
}
|