<script setup>
import { ref,computed,onMounted,onBeforeUnmount,watch,defineAsyncComponent } from 'vue';
import { departments,scenarios,validateData,selectedRows,dailyTotals,byDepartment,summary,flowData,neighbours,toCsv,money,number } from './model.js';
const ChartPanel=defineAsyncComponent(()=>import('./ChartPanel.vue'));
const scenario=ref('trend'),days=ref(28),department=ref('all'),area=ref(true),metric=ref('actual'),percent=ref(false),person=ref('Анна'),selected=ref(''),cohort=ref('current'),onlyNeighbours=ref(false),animation=ref(true),data=ref(null),status=ref('loading'),error=ref(''),loadMode=ref('normal'),notice=ref(''),sourceMode=new URLSearchParams(location.search).get('source')==='php'?'php':'static';
let controller,requestId=0;const media=matchMedia('(prefers-reduced-motion: reduce)');const reduced=ref(media.matches);const changeMotion=e=>reduced.value=e.matches;media.addEventListener('change',changeMotion);
const duration=computed(()=>animation.value&&!reduced.value?350:0),current=computed(()=>scenarios.find(s=>s.id===scenario.value)),showFilters=computed(()=>['trend','compare','share','dashboard'].includes(scenario.value));
const rows=computed(()=>data.value?selectedRows(data.value,Number(days.value),department.value):[]),totals=computed(()=>dailyTotals(rows.value)),stats=computed(()=>summary(rows.value)),groups=computed(()=>byDepartment(rows.value,percent.value)),capacity=computed(()=>data.value?.capacity.filter(d=>d.person===person.value)||[]),projects=computed(()=>data.value?.projects.filter(d=>department.value==='all'||d.department===department.value)||[]),flow=computed(()=>flowData(cohort.value));
const graph=computed(()=>data.value?neighbours(data.value.services,selected.value||'api',onlyNeighbours.value):{nodes:[],links:[],neighbours:[]});
const percentLabel=v=>number(v)+'%';
const currentData=computed(()=>({trend:totals.value,compare:groups.value,share:groups.value,capacity:capacity.value,projects:projects.value,flow:flow.value,network:graph.value,dashboard:totals.value})[scenario.value]);
const table=computed(()=>{
 switch(scenario.value){
  case 'trend':case 'dashboard':return totals.value.map(d=>({'Дата':d.date,'Выручка, ₽':d.actual,'План, ₽':d.plan,'Заявки':d.orders,'Пропусков':d.missing}));
  case 'compare':case 'share':return groups.value.map(d=>({'Отдел':d.name,'Известная выручка, ₽':d.actual,'План, ₽':d.plan,'Выполнение, %':Math.round(d.actual/d.plan*1000)/10,'Пропусков':d.missing}));
  case 'capacity':return capacity.value.map(d=>({'День':['Пн','Вт','Ср','Чт','Пт'][d.day],'Интервал':['09–11','11–13','14–16','16–18'][d.slot],'Задач, ч':d.hours}));
  case 'projects':return projects.value.map(d=>({'Проект':d.name,'Бюджет, тыс. ₽':d.budget,'Факт, тыс. ₽':d.actual,'Часы':d.hours}));
  case 'flow':return flow.value.links.map(d=>({'Откуда':flow.value.nodes.find(n=>n.id===d.source).label,'Куда':flow.value.nodes.find(n=>n.id===d.target).label,'Заявок':d.value}));
  case 'network':return graph.value.links.map(l=>({'Откуда':data.value.services.nodes.find(n=>n.id===l.source).label,'Куда':data.value.services.nodes.find(n=>n.id===l.target).label,'Связь':l.label}));
  default:return [];
 }
});
const tableFields=computed(()=>Object.keys(table.value[0]||{}));
const selectedInfo=computed(()=>{
 if(scenario.value==='trend'){const d=totals.value.find(d=>String(d.x)===selected.value)||totals.value[0];return d?`${d.date}: ${d.actual===null?'неполные данные — линия прерывается':metric.value==='actual'?money(d.actual):number(d.orders)+' заявок'}. План: ${money(d.plan)}.`:'';}
 if(['compare','share'].includes(scenario.value)){const d=groups.value.find(d=>d.id===selected.value)||groups.value[0];return d?`${d.name}: ${money(d.actual)} известных поступлений; ${percentLabel(stats.value.actual?d.actual/stats.value.actual*100:0)} от известной суммы. План ${money(d.plan)}. Пропусков: ${d.missing}.`:'';}
 if(scenario.value==='capacity'){const d=capacity.value.find(d=>`${d.day}-${d.slot}`===selected.value)||capacity.value[0];return d?`${person.value}, ${['понедельник','вторник','среда','четверг','пятница'][d.day]}, ${['09–11','11–13','14–16','16–18'][d.slot]}: ${d.hours} ч задач на 2 ч интервала. ${d.hours>2?'Перегрузка: перенесите часть задач.':d.hours===0?'Свободный интервал.':'Есть доступная ёмкость.'}`:'';}
 if(scenario.value==='projects'){const d=projects.value.find(d=>d.id===selected.value)||projects.value[0];return d?`${d.name}: бюджет ${d.budget} тыс. ₽, факт ${d.actual} тыс. ₽, разница ${d.actual-d.budget} тыс. ₽. Отработано ${d.hours} ч.`:'';}
 if(scenario.value==='flow'){const d=flow.value.links.find(l=>l.id===selected.value)||flow.value.links[0];return d?`${flow.value.nodes.find(n=>n.id===d.source).label} → ${flow.value.nodes.find(n=>n.id===d.target).label}: ${d.value} заявок. Это один переход когорты, а не дополнительные клиенты.`:'';}
 if(scenario.value==='network'){const id=selected.value||'api';return `Сервис: ${data.value?.services.nodes.find(n=>n.id===id)?.label||id}. Прямые соседи: ${graph.value.neighbours.map(id=>data.value.services.nodes.find(n=>n.id===id).label).join(', ')||'нет'}. Связь обозначает интеграцию, не доказанную причину сбоя.`;}
 return `За ${days.value} дней: ${money(stats.value.actual)} известных поступлений. Общие фильтры пересчитали графики и выгрузку.`;
});
const code=computed(()=>({trend:`<VisXYContainer :data="daily" :height="280">\n  <VisLine :x="d => d.x" :y="d => d.actual ?? undefined" />\n  <VisAxis type="x" />\n  <VisAxis type="y" />\n</VisXYContainer>`,compare:`<VisGroupedBar\n  :x="d => d.x"\n  :y="[d => d.factValue, d => d.planValue]"\n  :color="(d, i) => i === 0 ? '#197d78' : '#aeb9bc'"\n/>`,share:`<VisSingleContainer :data="departments">\n  <VisDonut :value="d => d.actual" :arcWidth="42" />\n</VisSingleContainer>`,capacity:`<VisHeatmap\n  :value="d => d.hours" :numRows="4" :numColumns="5"\n  layout="column" :colorDomain="[0, 4]"\n  :rowLabel="i => slots[i]" :columnLabel="i => days[i]"\n/>`,projects:`<VisScatter\n  :x="d => d.budget" :y="d => d.actual ?? undefined"\n  :color="d => d.actual > d.budget ? '#b66b30' : '#197d78'"\n/>`,flow:`const data = {\n  nodes: [{ id: 'lead', label: 'Заявки' }, ...],\n  links: [{ source: 'lead', target: 'qualified', value: 120 }, ...]\n}\n<VisSankey :linkValue="d => d.value" :enableZoom="false" />`,network:`<VisGraph\n  layoutType="circular" :nodeLabel="d => d.label"\n  :disableZoom="true" :disableDrag="true"\n  :nodeFill="d => d.id === selected ? '#b66b30' : '#197d78'"\n/>`,dashboard:`const rows = computed(() => selectedRows(data.value, days.value, department.value))\nconst daily = computed(() => dailyTotals(rows.value))\nconst totals = computed(() => summary(rows.value))\n// Графики, KPI и CSV читают одни и те же rows.`})[scenario.value]);
async function load(){
 const id=++requestId;controller?.abort();controller=new AbortController();status.value='loading';error.value='';
 try{
  const mode=loadMode.value;const file=mode==='error'?'missing.json':mode==='empty'?'empty.json':'service.json';
  const url=sourceMode==='php'?new URL(`/api/analytics?dataset=${encodeURIComponent(mode)}`,location.origin):new URL('data/'+file,document.baseURI);
  const response=await fetch(url,{signal:controller.signal,cache:'no-store'});if(!response.ok)throw new Error('HTTP '+response.status+' — набор не загружен.');
  const next=validateData(await response.json());if(id!==requestId)return;data.value=next;status.value=next.daily.length?'ready':'empty';notice.value='';
 }catch(e){if(id!==requestId||e.name==='AbortError')return;status.value='error';error.value=e.message;}
}
function restore(){loadMode.value='normal';load();}
function setScenario(delta){const i=scenarios.findIndex(s=>s.id===scenario.value);scenario.value=scenarios[Math.max(0,Math.min(7,i+delta))].id;}
function select(v){selected.value=String(v??'');}
function download(kind){const content=kind==='json'?JSON.stringify({scenario:scenario.value,days:Number(days.value),department:department.value,rows:table.value},null,2):toCsv(table.value);const url=URL.createObjectURL(new Blob([content],{type:kind==='json'?'application/json':'text/csv;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download=`unovis-${scenario.value}.${kind}`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);notice.value='Подготовлена выгрузка текущей таблицы: '+table.value.length+' строк.';}
watch([scenario,days,department,person,cohort],()=>{selected.value='';notice.value='';});
onMounted(load);onBeforeUnmount(()=>{controller?.abort();media.removeEventListener('change',changeMotion);});
</script>
<template>
 <main class="workshop">
  <header><p class="eyebrow">UNOVIS + VUE · 8 ИНТЕРФЕЙСОВ</p><h1>Данные, которые помогают решить</h1><p class="lead">Меняйте период, выбирайте отделы и точки, исследуйте связи. Графики пересчитываются на настоящем Unovis.</p><p class="mechanism">Учебные данные загружаются HTTP-запросом с этого сайта. Это фиксированный JSON вымышленного сервиса, без базы клиентов. PHP API для самостоятельного запуска есть в архиве. Текущий источник: <strong>{{sourceMode==='php'?'локальный PHP endpoint':'статические JSON-файлы'}}</strong>.</p></header>
  <section class="card-panel controls" aria-label="Выбор опыта"><label for="scenario">Практическая задача</label><select id="scenario" class="form-select" v-model="scenario"><option v-for="s in scenarios" :value="s.id" :key="s.id">{{s.name}}</option></select><div class="button-row"><button class="btn btn-outline-dark" @click="setScenario(-1)" :disabled="scenario==='trend'">← Назад</button><button class="btn btn-outline-dark" @click="setScenario(1)" :disabled="scenario==='dashboard'">Следующий опыт →</button><label class="motion"><input type="checkbox" v-model="animation"> Плавные переходы</label></div><p v-if="reduced" class="small-note">Системная настройка уменьшения движения включена: длительность анимации — 0.</p></section>
  <section class="card-panel" aria-labelledby="task-title"><p class="eyebrow">ГДЕ ПРИГОДИТСЯ</p><h2 id="task-title">{{current.name}}</h2><p>{{current.use}}</p><p class="try"><strong>Попробуйте:</strong> {{current.goal}}</p>
   <div class="filter-grid" v-if="showFilters"><div><label for="period">Период</label><select id="period" v-model="days" class="form-select"><option :value="7">Последние 7 дней</option><option :value="14">Последние 14 дней</option><option :value="28">Все 28 дней</option></select></div><div><label for="department">Отдел</label><select id="department" v-model="department" class="form-select"><option value="all">Все отделы</option><option v-for="d in departments" :value="d.id" :key="d.id">{{d.name}}</option></select></div></div>
   <div class="filter-grid" v-if="scenario==='trend'"><div><label for="metric">Показатель</label><select id="metric" class="form-select" v-model="metric"><option value="actual">Выручка, ₽</option><option value="orders">Заявки, шт.</option></select></div><label class="motion"><input type="checkbox" v-model="area"> Заливка под линией</label></div>
   <label class="motion" v-if="scenario==='compare'"><input type="checkbox" v-model="percent"> Сравнить выполнение плана в процентах</label>
   <div v-if="scenario==='capacity'"><label for="person">Сотрудник</label><select id="person" class="form-select" v-model="person"><option>Анна</option><option>Илья</option><option>Мария</option></select><p class="small-note">Нагрузка за одну учебную неделю. В каждом интервале 2 доступных часа; 3–4 часа задач означают перегрузку. Шкала всегда 0–4 ч.</p></div>
   <div v-if="scenario==='projects'"><label for="project-department">Отдел проектов</label><select id="project-department" class="form-select" v-model="department"><option value="all">Все отделы</option><option v-for="d in departments" :value="d.id">{{d.name}}</option></select><p class="small-note">Каждая точка — проект. X: бюджет, Y: фактическая сумма в тысячах рублей. Рыжие точки вышли за бюджет.</p></div>
   <div v-if="scenario==='flow'"><label for="cohort">Когорта заявок</label><select id="cohort" class="form-select" v-model="cohort"><option value="current">Октябрь: 150 заявок</option><option value="previous">Сентябрь: 140 заявок</option></select><p class="small-note">Схему можно прокручивать по горизонтали внутри её области; полные названия переходов есть в списке и таблице. Считаем уникальные заявки выбранной когорты. На каждом промежуточном этапе вход равен сумме выходов; числа не складываются поперёк всех этапов.</p></div>
   <div v-if="scenario==='network'"><label for="service">Сервис</label><select id="service" class="form-select" :value="selected||'api'" @change="select($event.target.value)"><option v-for="n in data?.services.nodes||[]" :value="n.id">{{n.label}}</option></select><label class="motion"><input type="checkbox" v-model="onlyNeighbours"> Только выбранный сервис и его соседи</label><p class="small-note">Круговая раскладка фиксирует небольшой граф. Масштабирование и перетаскивание выключены, чтобы не мешать прокрутке телефона.</p></div>
  </section>
  <section class="card-panel data-controls" aria-label="Загрузка данных"><div><label for="load-mode">Набор данных</label><select id="load-mode" class="form-select" v-model="loadMode"><option value="normal">Обычные данные</option><option value="empty">Пустой набор</option><option value="error">Ошибка HTTP 404</option></select></div><button class="btn btn-primary" @click="load">{{status==='loading'?'Загрузить заново':'Загрузить набор'}}</button><p class="small-note">«Ошибка» запрашивает отсутствующий учебный файл. Новый запрос отменяет предыдущий; старый ответ не заменит свежий.</p></section>
  <div v-if="status==='loading'" class="state" role="status">Загружаем данные…</div>
  <div v-else-if="status==='error'" class="state error" role="alert"><h2>Не удалось получить данные</h2><p>{{error}}</p><p>Пустая диаграмма могла бы выглядеть как нулевые продажи, поэтому ошибка показана отдельно.</p><button class="btn btn-primary" @click="restore">Загрузить обычные данные</button></div>
  <div v-else-if="status==='empty'" class="state" role="status"><h2>Пока нет записей</h2><p>Запрос выполнен успешно, но набор пуст. Нули и выдуманные точки не добавляются.</p><button class="btn btn-primary" @click="restore">Вернуть учебные данные</button></div>
  <template v-else-if="status==='ready'">
   <div class="kpi-grid" v-if="showFilters"><div><small>Известная выручка</small><strong>{{money(stats.actual)}}</strong></div><div><small>План</small><strong>{{money(stats.plan)}}</strong></div><div><small>Заявки с данными</small><strong>{{number(stats.orders)}}</strong></div></div><p v-if="showFilters&&stats.missing" class="notice warning">Есть {{stats.missing}} пропуск в исходных данных. Сводная сумма частичная; неизвестное значение не заменено нулём.</p>
   <section class="card-panel visualization" aria-labelledby="chart-title"><h2 id="chart-title">{{scenario==='dashboard'?'Выручка и структура':current.name.slice(3)}}</h2><p v-if="scenario==='compare'" class="small-note"><span class="dot teal"></span>Факт <span class="dot gray"></span>План. Единицы: {{percent?'%':'₽'}}.</p>
    <p v-if="['trend','dashboard'].includes(scenario)&&area&&totals.some(d=>d.missing)" class="small-note">Заливка временно скрыта: ряд содержит пропуск. Линия разрывается, чтобы не показывать неизвестное значение как ноль.</p><div :class="{'dashboard-charts':scenario==='dashboard'}"><ChartPanel :kind="scenario==='dashboard'?'trend':scenario" :data="currentData" :duration="duration" :area="area" :metric="scenario==='dashboard'?'actual':metric" :percent="percent" :selected="scenario==='network'?(selected||'api'):selected" @select="select"/><ChartPanel v-if="scenario==='dashboard'" kind="share" :data="groups" :duration="duration"/></div>
    <div class="selection"><label for="detail" v-if="scenario!=='dashboard'&&scenario!=='network'">Выбрать значение без наведения мыши</label><select v-if="scenario==='trend'" id="detail" class="form-select" v-model="selected"><option value="">Первый день периода</option><option v-for="d in totals" :value="String(d.x)">{{d.date}}</option></select><select v-if="['compare','share'].includes(scenario)" id="detail" class="form-select" v-model="selected"><option value="">Первый отдел</option><option v-for="d in groups" :value="d.id">{{d.name}}</option></select><select v-if="scenario==='capacity'" id="detail" class="form-select" v-model="selected"><option value="">Первый интервал</option><option v-for="d in capacity" :value="`${d.day}-${d.slot}`">{{['Пн','Вт','Ср','Чт','Пт'][d.day]}} · {{['09–11','11–13','14–16','16–18'][d.slot]}}</option></select><select v-if="scenario==='projects'" id="detail" class="form-select" v-model="selected"><option value="">Первый проект</option><option v-for="d in projects" :value="d.id">{{d.name}}</option></select><select v-if="scenario==='flow'" id="detail" class="form-select" v-model="selected"><option value="">Первый переход</option><option v-for="l in flow.links" :value="l.id">{{flow.nodes.find(n=>n.id===l.source).label}} → {{flow.nodes.find(n=>n.id===l.target).label}}</option></select><p class="detail" aria-live="polite">{{selectedInfo}}</p></div>
   </section>
   <section class="card-panel" aria-labelledby="table-title"><div class="section-head"><h2 id="table-title">Те же данные в таблице</h2><div class="button-row"><button class="btn btn-outline-dark" @click="download('csv')">Скачать CSV</button><button class="btn btn-outline-dark" @click="download('json')">Скачать JSON</button></div></div><p class="small-note">Экспортируется текущая таблица с выбранными фильтрами. Пропуски обозначены «Нет данных». Широкую таблицу можно прокручивать отдельно.</p><p v-if="notice" role="status" class="notice">{{notice}}</p><div class="table-wrap" tabindex="0" role="region" aria-label="Таблица данных, доступна горизонтальная прокрутка"><table class="table table-striped"><caption>{{current.name}} · {{table.length}} строк</caption><thead><tr><th v-for="field in tableFields" :key="field" scope="col">{{field}}</th></tr></thead><tbody><tr v-for="(row,i) in table" :key="i"><td v-for="field in tableFields" :key="field">{{row[field]===null?'Нет данных':row[field]}}</td></tr></tbody></table></div></section>
  </template>
  <section class="card-panel"><h2>Как это устроено</h2><p>Фильтры меняют реактивные данные Vue. Контейнер Unovis получает новый массив, а компоненты рисуют линии, столбцы, сегменты или связи. Доступный выбор и таблица используют те же значения.</p><details><summary>Код текущего графика</summary><pre><code>{{code}}</code></pre></details><p class="source-links"><a href="source/ChartPanel.vue.txt" target="_blank" rel="noopener">Все графики Vue</a><a href="source/App.vue.txt" target="_blank" rel="noopener">Интерфейс и загрузка</a><a href="source/model.js" target="_blank" rel="noopener">Расчёты и данные</a></p><a class="btn btn-primary" href="../unovis-project.zip" download>Скачать весь проект</a><p class="small-note mt-3">Unovis 1.8.1 · Vue 3.5.45 · Bootstrap 5.3.9. Данные вымышленные; внешний аналитический сервис не вызывается.</p></section>
 </main>
</template>
