/* ===== Личный кабинет участника Лиги: пять модулей ===== */ const LEAGUE_ROUTES = { 'league-rating':'rating', 'league-infrastructure':'infrastructure', 'league-discounts':'discounts', 'league-pilot-card':'pilot', 'league-support':'support', }; const LEAGUE_STATUS = { pending:'Ожидает подтверждения',confirmed:'Подтверждено',cancelled:'Отменено',completed:'Завершено',no_show:'Неявка', new:'Новое',diagnostics:'Диагностика',approval:'Ожидает согласования',repairing:'В ремонте',ready:'Готово',issued:'Выдано', accepted:'Принято',working:'В работе',waiting_user:'Ожидает пользователя',workshop:'Передано в мастерскую',resolved:'Решено',closed:'Закрыто', }; const LEAGUE_PRIORITY = {normal:'Обычный',high:'Высокий',urgent:'Срочный'}; async function leagueFetch(options={}){ const method=String(options.method||'GET').toUpperCase(); const url=method==='GET'?`api/league.php?_=${Date.now()}`:'api/league.php'; const res=await fetch(url,{credentials:'same-origin',cache:'no-store',...options,headers:{'Content-Type':'application/json',...(options.headers||{})}}); const data=await res.json().catch(()=>({ok:false,error:'Пустой ответ сервера'})); if(!res.ok||data.ok===false) throw new Error(data.error||`HTTP ${res.status}`); return data; } function leagueDate(value, time=true){ if(!value)return '—'; const date=new Date(String(value).replace(' ','T')); return Number.isNaN(date.getTime())?String(value):date.toLocaleString('ru-RU',{day:'2-digit',month:'2-digit',year:'numeric',...(time?{hour:'2-digit',minute:'2-digit'}:{})}); } function leagueLap(ms){ if(ms===null||ms===undefined||ms==='')return '—'; const n=Number(ms);return `${Math.floor(n/60000)}:${String(Math.floor((n%60000)/1000)).padStart(2,'0')}.${String(n%1000).padStart(3,'0')}`; } function LeagueNotice({message,type=''}){ return message?
{message}
:null; } function LeagueEmpty({children='Данных пока нет.',action=null}){ return
{children}{action&&
{action}
}
; } function LeagueStatus({value}){ return {LEAGUE_STATUS[value]||value}; } function LeagueMemberApp({module,go,user}){ const [data,setData]=useState(null),[error,setError]=useState(''),[loading,setLoading]=useState(true); async function load(){ setLoading(true);setError(''); try{setData((await leagueFetch()).data);}catch(e){setError(e.message);}finally{setLoading(false);} } useEffect(()=>{load();},[]); if(loading)return
Загружаю сервисы Лиги…
; if(error)return

Сервисы Лиги

; const props={data,go,user,reload:load}; if(module==='infrastructure')return ; if(module==='discounts')return ; if(module==='pilot')return ; if(module==='support')return ; return ; } function LeagueHeading({eyebrow='Лига Дронов',title,subtitle,actions,icon='drone',stats=[]}){ return
{eyebrow}

{title}

{subtitle}

{stats.length>0&&
{stats.map((item,index)=>
{item.value}{item.label}
)}
}{actions&&
{actions}
}
; } function LeagueRating({data}){ const r=data.rating, current=r.level.current, next=r.level.next; const [city,setCity]=useState(''),[discipline,setDiscipline]=useState(''); const cities=[...new Set(r.ranking.map(x=>x.city).filter(Boolean))]; const disciplines=[...new Set(r.ranking.map(x=>x.discipline).filter(Boolean))]; const rows=r.ranking.filter(x=>(!city||x.city===city)&&(!discipline||x.discipline===discipline)); const progress=next?Math.max(0,Math.min(100,Math.round((r.balance-current.minPoints)/(next.minPoints-current.minPoints)*100))):100; return
{r.balance}баллов
#{r.place||'—'}место
{current.name}уровень
{next?`До уровня ${next.name}`:'Максимальный уровень'}{next?`${Math.max(0,next.minPoints-r.balance)} баллов`:'Master'}

Общий рейтинг

Фильтры по городу и дисциплине.

{rows.map(row=>
#{row.place}
{row.nickname||row.name}
{row.name}
{row.discipline}{row.city||'—'}{row.points}
)}{!rows.length&&}

История операций

Записи не удаляются и содержат автора изменения.

{r.transactions.map(x=>
{leagueDate(x.createdAt)}=0?'var(--green-700)':'#b42318'}}>{Number(x.points)>0?'+':''}{x.points}
{x.reason}
{x.activityType}{x.actorName?` · ${x.actorName}`:''}
{x.balanceAfter}
)}{!r.transactions.length&&Операций пока нет.}
; } function LeagueInfrastructure({data,reload}){ const d=data.infrastructure,[trackId,setTrackId]=useState(''),[message,setMessage]=useState(''),[busy,setBusy]=useState(false),[reslot,setReslot]=useState({}); const [repair,setRepair]=useState({serviceId:'',droneModel:'',problem:'',attachments:[]}); async function act(payload){setBusy(true);setMessage('');try{await leagueFetch({method:'POST',body:JSON.stringify(payload)});setMessage('Готово. Данные обновлены.');await reload();}catch(e){setMessage(e.message);}finally{setBusy(false);}} const visibleSlots=d.slots.filter(s=>!trackId||String(s.trackId)===String(trackId)); const selectedTrack=d.tracks.find(t=>String(t.id)===String(trackId)); return
!['issued','cancelled'].includes(x.status)).length,label:'заявок в работе'}]} actions={}/>

Трассы

Адреса, расписание и правила площадок.

{d.tracks.map(t=>
{t.photos?.[0]?:
}

{t.name}

{t.address}

{t.description}

{t.hours||'По расписанию'}
{t.rules&&
Правила

{t.rules}

}
)}{!d.tracks.length&&Трассы появятся после публикации администратором.}

Свободные слоты

Бронирование создаётся со статусом ожидания.

{visibleSlots.map(s=>{const t=d.tracks.find(x=>Number(x.id)===Number(s.trackId));return
{t?.name||'Трасса'}
{t?.address}
{leagueDate(s.startsAt)}{s.durationMinutes} мин.
})}{!visibleSlots.length&& Обновить расписание}>{selectedTrack?`Для трассы «${selectedTrack.name}» свободное время пока не опубликовано.`:'Свободных слотов пока нет. Выберите трассу или обновите расписание.'}}

Мои бронирования

{d.bookings.map(b=>
{b.trackName}

{leagueDate(b.startsAt)} · {b.durationMinutes} мин.

{b.address}

{['pending','confirmed'].includes(b.status)&&
{b.status==='pending'&&
}
}
)}{!d.bookings.length&&У вас ещё нет бронирований.}

Ремонтная мастерская

Создайте заявку и отслеживайте диагностику и ремонт.

setRepair({...repair,droneModel:e.target.value})}/>