892 lines
No EOL
41 KiB
Handlebars
892 lines
No EOL
41 KiB
Handlebars
<style>
|
||
.chart-container {
|
||
position: relative;
|
||
margin: auto;
|
||
height: 25rem;
|
||
width: 100%;
|
||
}
|
||
</style>
|
||
|
||
<div id="root" x-data="{ loading: true }" x-init="fetchData()" x-ref="root">
|
||
<div x-show="loading" class="position-absolute spinner-border start-50 top-50" role="status">
|
||
<span class="visually-hidden">Loading...</span>
|
||
</div>
|
||
|
||
<div x-show="!loading" x-data="{ tab: $persist('vgroups') }" x-ref="tabs-holder" x-cloak x-transition>
|
||
<header class="navbar navbar-expand-md navbar-dark bd-navbar">
|
||
<ul class="nav nav-pills container flex-wrap flex-md-nowrap gap-2">
|
||
<li class="nav-item" @click="tab = 'vgroups'">
|
||
<a :class="{ 'active': tab === 'vgroups' }" @click.prevent="tab = 'vgroups'" href="#vgroups" class="nav-link" aria-current="page" href="#vgroups">Группы</a>
|
||
</li>
|
||
<li class="nav-item" @click="tab = 'voters'">
|
||
<a :class="{ 'active': tab === 'voters' }" @click.prevent="tab = 'voters'" href="#voters" class="nav-link" href="#voters">Пользователи</a>
|
||
</li>
|
||
<li class="nav-item" @click="tab = 'votes'">
|
||
<a :class="{ 'active': tab === 'votes' }" @click.prevent="tab = 'votes'" href="#votes" class="nav-link" href="#votes">Голоса</a>
|
||
</li>
|
||
<li class="nav-item" @click="tab = 'analysis'">
|
||
<a :class="{ 'active': tab === 'analysis' }" @click.prevent="tab = 'analysis'" href="#analysis" class="nav-link" href="#analysis">Анализ</a>
|
||
</li>
|
||
<div class="flex-grow-1"></div>
|
||
<li class="nav-item">
|
||
<a class="nav-link active" href="/gateway/logout">Выйти</a>
|
||
</li>
|
||
</ul>
|
||
</header>
|
||
|
||
<div class="container p-0">
|
||
<div x-show="tab === 'vgroups'">
|
||
<form x-data="{ newModelName: '', newModelDescription: '' }" class="mb-4 d-flex flex-row gap-2" action="/admin/vgroups/create" method="POST">
|
||
<input type="text" x-model="newModelName" name="name" class="form-control form-control-sm flex-grow-1" placeholder="Название">
|
||
<input type="text" x-model="newModelDescription" name="description" class="form-control form-control-sm flex-grow-1" placeholder="Описание">
|
||
<button x-bind:disabled="newModelName.length < 3" class="btn btn-info btn-sm w-50">Создать новую группу</button>
|
||
</form>
|
||
<table x-data="{ titleSearch: '' }" id="vgroupsTable" class="w-100 table table-sm table-bordered">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col" class="align-middle text-center">ID</th>
|
||
<th scope="col" class="align-middle text-start">
|
||
Название
|
||
<input type="text" class="form-control form-control-sm" placeholder="Поиск"
|
||
@input="titleSearch = $event.target.value;">
|
||
</th>
|
||
<th scope="col" class="align-middle text-center">Описание</th>
|
||
<th scope="col" class="align-middle text-center">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="table-group-divider">
|
||
<template x-if="!$store.dataset.vgroups">
|
||
<tr><td colspan="4" scope="row"><i>Loading...</i></td></tr>
|
||
</template>
|
||
<template x-for="vgroup in $store.dataset.vgroups">
|
||
<tr x-show="vgroup.name.includes(titleSearch)">
|
||
<td x-text="vgroup.id" scope="row"></td>
|
||
<td x-text="vgroup.name"></td>
|
||
<td x-text="vgroup.description || '[Нет описания]'"></td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-danger"
|
||
@click="deleteVgroup(vgroup.id)">
|
||
Удалить
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div x-show="tab === 'voters'">
|
||
<form x-data="{ newVoterFullName: '', newVoterLogin: '', newVoterPassword: '' }" class="mb-4 d-flex flex-row gap-2" action="/admin/voters/create" method="POST">
|
||
<input type="text" x-model="newVoterFullName" name="full_name" class="form-control form-control-sm flex-grow-1" placeholder="Полное имя">
|
||
<input type="text" x-model="newVoterLogin" name="login" class="form-control form-control-sm flex-grow-1" placeholder="Логин">
|
||
<input type="text" x-model="newVoterPassword" name="password" class="form-control form-control-sm flex-grow-1" placeholder="Пароль">
|
||
<button x-bind:disabled="newVoterFullName.length < 4 || newVoterLogin.length < 5 || newVoterPassword.length < 4" class="btn btn-info btn-sm w-50">Создать пользователя</button>
|
||
</form>
|
||
<table x-data="{ groupFilter: -1 }" id="votersTable" class="w-100 table table-sm table-bordered">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col" class="align-middle text-center">ID</th>
|
||
<th scope="col" class="align-middle text-center">Имя</th>
|
||
<th scope="col" class="align-middle text-center">Логин</th>
|
||
<th scope="col" class="align-middle text-start">
|
||
Группа
|
||
<select class="form-select form-select-sm" @change="groupFilter = $event.target.value">
|
||
<option value="-1" selected>Без фильтра</option>
|
||
<template x-for="vgroup in $store.dataset.vgroups">
|
||
<option x-bind:value="vgroup.id" x-text="vgroup.name"></option>
|
||
</template>
|
||
</select>
|
||
</th>
|
||
<th scope="col" class="align-middle text-center">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="table-group-divider">
|
||
<template x-if="!$store.dataset.voters">
|
||
<tr><td colspan="5" scope="row"><i>Loading...</i></td></tr>
|
||
</template>
|
||
<template x-for="voter in $store.dataset.voters">
|
||
<tr x-show="groupFilter == -1 || groupFilter == voter.vgroup_id">
|
||
<td x-text="voter.id" scope="row"></td>
|
||
<td x-text="voter.full_name"></td>
|
||
<td x-text="voter.login"></td>
|
||
<td>
|
||
<template x-if="!$store.dataset.vgroups">
|
||
<tr><td colspan="4" scope="row"><i>Loading...</i></td></tr>
|
||
</template>
|
||
<select class="form-select form-select-sm"
|
||
@change="updateUserGroup(voter.id, $event.target.value)">
|
||
<option value="null" x-bind:selected="voter.vgroup_id === null">Не привязан</option>
|
||
<template x-for="vgroup in $store.dataset.vgroups">
|
||
<option x-bind:value="vgroup.id" x-text="vgroup.name" x-bind:selected="voter.vgroup_id === vgroup.id"></option>
|
||
</template>
|
||
</select>
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-danger"
|
||
@click="deleteVoter(voter.id)">
|
||
Удалить
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div x-show="tab === 'votes'" x-data="{ colorCoded: false }">
|
||
<div class="form-check form-switch form-check-reverse">
|
||
<input class="form-check-input" type="checkbox" id="flexSwitchCheckReverse" @change="colorCoded = $event.target.checked">
|
||
<label class="form-check-label" for="flexSwitchCheckReverse">Колор-кодинг</label>
|
||
</div>
|
||
<table x-data="{ groupFilter: -1, dateFilter: '' }" id="votesTable" class="w-100 table table-sm table-bordered">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col" class="align-middle text-center">
|
||
Когда
|
||
<select class="form-select form-select-sm"
|
||
@change="dateFilter = $event.target.value">
|
||
<option value="" selected>Без фильтра</option>
|
||
<template x-for="voteDate in getDatesFromDatasets()">
|
||
<option x-bind:value="voteDate" x-text="voteDate"></option>
|
||
</template>
|
||
</select>
|
||
</th>
|
||
<th scope="col" class="align-middle text-center">Кто</th>
|
||
<th scope="col" class="align-middle text-start">
|
||
Группа
|
||
<select class="form-select form-select-sm" @change="groupFilter = $event.target.value">
|
||
<option value="-1" selected>Без фильтра</option>
|
||
<template x-for="vgroup in $store.dataset.vgroups">
|
||
<option x-bind:value="vgroup.id" x-text="vgroup.name"></option>
|
||
</template>
|
||
</select>
|
||
</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Здоровье</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Любовь</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Секс</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Работа</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Отдых</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Деньги</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Отношения</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Личн. рост</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Смысл жизни</th>
|
||
<th scope="col" class="align-middle text-center wm-vlr">Тревожность</th>
|
||
<th scope="col" class="align-middle text-center">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="table-group-divider">
|
||
<template x-if="!$store.dataset.votes">
|
||
<tr><td colspan="14" scope="row"><i>Loading...</i></td></tr>
|
||
</template>
|
||
<template x-for="vote in $store.dataset.votes">
|
||
<tr
|
||
x-data="{ voter: $store.dataset.voters.find((el) => el.id == vote.voter_id), vfd: formatDateMMYYYY(new Date(vote.vote_date)) }"
|
||
x-show="(groupFilter == -1 || groupFilter == voter.vgroup_id) && (vfd.includes(dateFilter))">
|
||
<td x-text="vfd" scope="row"></td>
|
||
<td x-text="voter?.full_name || '[Нет имени]'"></td>
|
||
<td x-text="getVGroupNameById(voter.vgroup_id)"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.health"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.health >= 7,
|
||
'status-warning': colorCoded && vote.health < 7 && vote.health > 4,
|
||
'status-dangerous': colorCoded && vote.health <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.love"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.love >= 7,
|
||
'status-warning': colorCoded && vote.love < 7 && vote.love > 4,
|
||
'status-dangerous': colorCoded && vote.love <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.sex"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.sex >= 7,
|
||
'status-warning': colorCoded && vote.sex < 7 && vote.sex > 4,
|
||
'status-dangerous': colorCoded && vote.sex <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.work"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.work >= 7,
|
||
'status-warning': colorCoded && vote.work < 7 && vote.work > 4,
|
||
'status-dangerous': colorCoded && vote.work <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.rest"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.rest >= 7,
|
||
'status-warning': colorCoded && vote.rest < 7 && vote.rest > 4,
|
||
'status-dangerous': colorCoded && vote.rest <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.finances"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.finances >= 7,
|
||
'status-warning': colorCoded && vote.finances < 7 && vote.finances > 4,
|
||
'status-dangerous': colorCoded && vote.finances <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.relations"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.relations >= 7,
|
||
'status-warning': colorCoded && vote.relations < 7 && vote.relations > 4,
|
||
'status-dangerous': colorCoded && vote.relations <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.pers_growth"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.pers_growth >= 7,
|
||
'status-warning': colorCoded && vote.pers_growth < 7 && vote.pers_growth > 4,
|
||
'status-dangerous': colorCoded && vote.pers_growth <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.meaning_of_life"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.meaning_of_life >= 7,
|
||
'status-warning': colorCoded && vote.meaning_of_life < 7 && vote.meaning_of_life > 4,
|
||
'status-dangerous': colorCoded && vote.meaning_of_life <= 4
|
||
}" scope="row"></td>
|
||
<td class="fading-bg stats-cell" x-text="vote.serenity"
|
||
:class="{
|
||
'status-ok': colorCoded && vote.serenity <= 4,
|
||
'status-warning': colorCoded && vote.serenity > 4 && vote.serenity < 7,
|
||
'status-dangerous': colorCoded && vote.serenity >= 7
|
||
}" scope="row"></td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-danger"
|
||
@click="deleteVote(vote.id)">
|
||
Удалить
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div x-show="tab === 'analysis'" x-data="{
|
||
show_acloub: $persist(true),
|
||
show_agc: $persist(true),
|
||
shown_agac: $persist(true),
|
||
shown_gc: $persist(true),
|
||
shown_cat: $persist(true),
|
||
}">
|
||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||
<h4>Среднее за клуб</h4>
|
||
<button class="badge text-bg-secondary"
|
||
@click="show_acloub = !show_acloub"
|
||
x-text="show_acloub? 'Скрыть' : 'Показать'"></button>
|
||
</div>
|
||
<div x-show="show_acloub" x-transition>
|
||
<div class="chart-container">
|
||
<canvas chart-id="clubAverageCategories" width="400" height="200"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||
<h4>Среднее по категориям в группах</h4>
|
||
<button class="badge text-bg-secondary"
|
||
@click="show_agc = !show_agc"
|
||
x-text="show_agc? 'Скрыть' : 'Показать'"></button>
|
||
</div>
|
||
<div x-show="show_agc" x-transition>
|
||
<div class="m-0 mb-1 mb-2 row">
|
||
<select class="form-select form-select-sm col me-1"
|
||
@change="$store.dataset.selectedGacavVgroup = parseInt($event.target.value); updateGACChart()">
|
||
<template x-for="vgroup_id in Object.keys($store.dataset.grouppedVotes)">
|
||
<option x-bind:value="vgroup_id" x-text="getVGroupNameById(vgroup_id)" x-bind:selected="$store.dataset.selectedGacavVgroup === vgroup_id"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
<div class="chart-container">
|
||
<canvas chart-id="groupAverageCategories" width="400" height="200"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||
<h4>Среднее за по категориям среди всех групп</h4>
|
||
<button class="badge text-bg-secondary"
|
||
@click="shown_agac = !shown_agac"
|
||
x-text="shown_agac? 'Скрыть' : 'Показать'"></button>
|
||
</div>
|
||
<div x-show="shown_agac" x-transition>
|
||
<div class="m-0 mb-1 mb-2 row">
|
||
<select class="form-select form-select-sm col ms-1"
|
||
@change="$store.dataset.agacVoteDate = $event.target.value; updateAGACChart()">
|
||
<template x-for="voteDate in getDatesFromDatasets()">
|
||
<option x-bind:value="voteDate" x-text="voteDate" x-bind:selected="$store.dataset.agacVoteDate === voteDate"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
<div class="chart-container">
|
||
<canvas chart-id="allGroupAverageCategories" width="400" height="200"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||
<h4>Значения в группе по категориям за дату</h4>
|
||
<button class="badge text-bg-secondary"
|
||
@click="shown_gc = !shown_gc"
|
||
x-text="shown_gc? 'Скрыть' : 'Показать'"></button>
|
||
</div>
|
||
<div x-show="shown_gc" x-transition>
|
||
<div class="m-0 mb-1 mb-2 row">
|
||
<select class="form-select form-select-sm col me-1"
|
||
@change="$store.dataset.selectedRadarVgroup = parseInt($event.target.value); updateRadarChart()">
|
||
<template x-for="vgroup_id in Object.keys($store.dataset.grouppedVotes)">
|
||
<option x-bind:value="vgroup_id" x-text="getVGroupNameById(vgroup_id)" x-bind:selected="$store.dataset.selectedRadarVgroup === vgroup_id"></option>
|
||
</template>
|
||
</select>
|
||
<select class="form-select form-select-sm col ms-1"
|
||
@change="$store.dataset.voteDate = $event.target.value; updateRadarChart()">
|
||
<template x-for="voteDate in getDatesFromDatasets()">
|
||
<option x-bind:value="voteDate" x-text="voteDate" x-bind:selected="$store.dataset.voteDate === voteDate"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
<div id="lowDatasetCountNotification" class="alert alert-info" role="alert"
|
||
x-show="$store.dataset.grouppedVotes[$store.dataset.selectedRadarVgroup] && !$store.dataset.grouppedVotes[$store.dataset.selectedRadarVgroup].find(e => e.vote_date == $store.dataset.voteDate)"
|
||
x-intersect:enter="$store.dynamicsChart = 'bar'">
|
||
У группы нет данных за эту дату
|
||
</div>
|
||
<div class="chart-container">
|
||
<canvas chart-id="groupCategories" width="400" height="200"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<div class="align-items-center d-flex flex-row justify-content-between">
|
||
<h4>Динамика категорий группы во времени (среднее)</h4>
|
||
<button class="badge text-bg-secondary"
|
||
@click="shown_cat = !shown_cat"
|
||
x-text="shown_cat? 'Скрыть' : 'Показать'"></button>
|
||
</div>
|
||
<div x-show="shown_cat" x-transition>
|
||
<div class="m-0 mb-1 mb-2 row">
|
||
<select class="form-select form-select-sm col"
|
||
@change="$store.dataset.selectedVgroup = parseInt($event.target.value); updateLineChart()">
|
||
<template x-for="vgroup_id in Object.keys($store.dataset.grouppedVotes)">
|
||
<option x-bind:value="vgroup_id" x-text="getVGroupNameById(vgroup_id)" x-bind:selected="$store.dataset.selectedVgroup === vgroup_id"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
<div id="lowDatasetCountNotification" class="alert alert-info" role="alert"
|
||
x-show="$store.dataset.grouppedVotes[$store.dataset.selectedVgroup] && $store.dataset.grouppedVotes[$store.dataset.selectedVgroup].length == 1"
|
||
x-intersect:enter="$store.dynamicsChart = 'bar'">
|
||
Мало данных по группе, лучше использовать столбчатую диаграмму
|
||
</div>
|
||
<div class="chart-container">
|
||
<canvas chart-id="categoryBars" width="400" height="300"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pb-5 pt-5"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const randomNum = () => Math.floor(Math.random() * (235 - 52 + 1) + 52);
|
||
const randomRGB = () => `rgba(${randomNum()}, ${randomNum()}, ${randomNum()}, 0.5)`;
|
||
const randomRGBO = (opacity) => `rgba(${randomNum()}, ${randomNum()}, ${randomNum()}, ${opacity})`;
|
||
const randomRGBV = () => [randomNum(), randomNum(), randomNum()];
|
||
const average = (arr) => (arr.reduce((acc, c) => acc + c, 0) / arr.length);
|
||
const fetchDatasetCategory = (dataset, group, category) => dataset[group].map(e => average(e[category]));
|
||
const getVGroupNameById = (vgroup_id) => Alpine.store("dataset").vgroups.find((el) => el.id == vgroup_id)?.name || '[Нет группы]';
|
||
const getDatesFromDatasets = () => [...new Set(Object.values(Alpine.store("dataset").grouppedVotes).map(e => e.map(i => i.vote_date)).reduce((acc, v) => [...acc, ...v], []))].sort();
|
||
const formatDateMMYYYY = (date) => date.toLocaleString('ru', { month: "numeric", year: "numeric" });
|
||
const getColorBasedOnValue = (value) => {
|
||
if (value <= 4) return 'rgba(255, 0, 0, 0.5)';
|
||
if (value > 4 && value < 7) return 'rgba(255, 255, 0, 0.5)';
|
||
return 'rgba(0, 255, 0, 0.5)';
|
||
};
|
||
</script>
|
||
|
||
<script src="/javascripts/chart.min.js"></script>
|
||
<script src="/javascripts/chartjs-plugin-datalabels-v2.1.0.js"></script>
|
||
|
||
<script>
|
||
const dbToCategory = {
|
||
health: "Здоровье",
|
||
love: "Любовь",
|
||
sex: "Секс",
|
||
rest: "Отдых",
|
||
finances: "Деньги",
|
||
meaning_of_life: "Смысл жизни",
|
||
serenity: "Тревожность",
|
||
relations: "Отношения",
|
||
pers_growth: "Личн. рост",
|
||
work: "Работа",
|
||
};
|
||
|
||
class ChartsController {
|
||
constructor() {
|
||
this.charts = {};
|
||
}
|
||
|
||
createChart(id, type, data, options) {
|
||
const ctx = document.querySelector(`[chart-id=${id}]`).getContext('2d');
|
||
this.charts[id] = {
|
||
ctx,
|
||
chart: new Chart(ctx, {
|
||
type: type,
|
||
data: data,
|
||
options: options,
|
||
})
|
||
};
|
||
}
|
||
|
||
updateChartData(id, labels, data) {
|
||
this.charts[id].chart.data.labels.length = 0;
|
||
this.charts[id].chart.data.datasets.length = 0;
|
||
|
||
this.charts[id].chart.data.labels.push(...labels);
|
||
this.charts[id].chart.data.datasets.push(...data);
|
||
|
||
this.charts[id].chart.update();
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
Chart.register(ChartDataLabels);
|
||
Chart.defaults.set("plugins.datalabels", {display: false});
|
||
const ct = new ChartsController();
|
||
|
||
let rootElement;
|
||
document.addEventListener("DOMContentLoaded", function() {
|
||
rootElement = document.getElementById("root");
|
||
});
|
||
|
||
document.addEventListener('alpine:init', () => {
|
||
Alpine.store("dataset", {
|
||
vgroups: [],
|
||
voters: [],
|
||
votes: [],
|
||
grouppedVotes: {},
|
||
});
|
||
|
||
ct.createChart("clubAverageCategories", "radar", {}, {
|
||
maintainAspectRatio: false,
|
||
plugins: {},
|
||
scales: {
|
||
v: {
|
||
min: 0,
|
||
max: 10,
|
||
axis: "r",
|
||
},
|
||
}
|
||
});
|
||
ct.createChart("groupAverageCategories", "radar", {}, {
|
||
maintainAspectRatio: false,
|
||
plugins: {},
|
||
scales: {
|
||
v: {
|
||
min: 0,
|
||
max: 10,
|
||
axis: "r",
|
||
pointLabels: {
|
||
display: true,
|
||
centerPointLabels: true,
|
||
},
|
||
},
|
||
}
|
||
});
|
||
ct.createChart("allGroupAverageCategories", "polarArea", {}, {
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false,
|
||
},
|
||
},
|
||
scales: {
|
||
v: {
|
||
min: 0,
|
||
max: 10,
|
||
axis: "r",
|
||
pointLabels: {
|
||
display: true,
|
||
centerPointLabels: true,
|
||
},
|
||
},
|
||
}
|
||
});
|
||
ct.createChart("groupCategories", "polarArea", {}, {
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false,
|
||
},
|
||
},
|
||
scales: {
|
||
v: {
|
||
min: 0,
|
||
max: 10,
|
||
axis: "r",
|
||
pointLabels: {
|
||
display: true,
|
||
centerPointLabels: true,
|
||
},
|
||
},
|
||
}
|
||
});
|
||
ct.createChart("categoryBars", "bar", {}, {
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false,
|
||
},
|
||
datalabels: {
|
||
display: true,
|
||
color: 'black',
|
||
font: {
|
||
size: 14
|
||
},
|
||
rotation: -90,
|
||
align: 'end',
|
||
anchor: 'end',
|
||
clip: true,
|
||
display: 'auto',
|
||
formatter: function(value, context) {
|
||
return context.dataset.label;
|
||
},
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
min: 0,
|
||
max: 12,
|
||
},
|
||
x: {
|
||
min: 0,
|
||
max: 10,
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
const fetchData = async function() {
|
||
Alpine.store("dataset").vgroups = [];
|
||
Alpine.store("dataset").voters = [];
|
||
Alpine.store("dataset").votes = [];
|
||
Alpine.store("dataset").grouppedVotes = {};
|
||
|
||
try {
|
||
let resp;
|
||
|
||
resp = await fetch("/admin/vgroups");
|
||
Alpine.store("dataset").vgroups = await resp.json();
|
||
|
||
resp = await fetch("/admin/voters");
|
||
Alpine.store("dataset").voters = await resp.json();
|
||
|
||
resp = await fetch("/admin/votes");
|
||
Alpine.store("dataset").votes = await resp.json();
|
||
|
||
resp = await fetch("/admin/votes/groupped");
|
||
const data = await resp.json();
|
||
data.forEach((e) => {
|
||
Alpine.store("dataset").grouppedVotes[e.vgroup_id] = [...(Alpine.store("dataset").grouppedVotes[e.vgroup_id] || []), e];
|
||
});
|
||
|
||
try {
|
||
const vg = Object.keys(Alpine.store("dataset").grouppedVotes)[0];
|
||
Alpine.store("dataset").selectedVgroup = vg;
|
||
Alpine.store("dataset").selectedRadarVgroup = vg;
|
||
Alpine.store("dataset").selectedGacavVgroup = vg;
|
||
|
||
const ad = getDatesFromDatasets()[0];
|
||
Alpine.store("dataset").voteDate = ad;
|
||
Alpine.store("dataset").agacVoteDate = ad;
|
||
|
||
updateCACChart();
|
||
updateGACChart();
|
||
updateAGACChart();
|
||
updateAGACChart();
|
||
updateLineChart();
|
||
updateRadarChart();
|
||
} catch (e) {
|
||
alert("Произошла ошибка выделения данных для графиков, обратитесь к разработчику");
|
||
console.error(e);
|
||
}
|
||
} catch (e) {
|
||
alert("Ошибка загрузки данных");
|
||
console.error(e);
|
||
}
|
||
|
||
Alpine.$data(rootElement).loading = false;
|
||
}
|
||
|
||
const deleteVgroup = async function (vgroupId) {
|
||
try {
|
||
const resp = await fetch(`/admin/vgroups/${vgroupId}/delete`);
|
||
const json = await resp.json();
|
||
if (json.error) {
|
||
alert(json.error);
|
||
}
|
||
} catch (e) {
|
||
alert(e.message);
|
||
}
|
||
|
||
fetchData();
|
||
}
|
||
|
||
const updateUserGroup = async function (userId, newGroup) {
|
||
try {
|
||
const resp = await fetch(`/admin/voters/${userId}/promote/${newGroup}`);
|
||
const json = await resp.json();
|
||
if (json.error) {
|
||
alert(json.error);
|
||
}
|
||
} catch (e) {
|
||
alert(e.message);
|
||
}
|
||
|
||
fetchData();
|
||
}
|
||
|
||
const deleteVoter = async function (voterId) {
|
||
try {
|
||
const resp = await fetch(`/admin/voters/${voterId}/delete`);
|
||
const json = await resp.json();
|
||
if (json.error) {
|
||
alert(json.error);
|
||
}
|
||
} catch (e) {
|
||
alert(e.message);
|
||
}
|
||
|
||
fetchData();
|
||
}
|
||
|
||
const deleteVote = async function (voteId) {
|
||
try {
|
||
const resp = await fetch(`/admin/votes/${voteId}/delete`);
|
||
const json = await resp.json();
|
||
if (json.error) {
|
||
alert(json.error);
|
||
}
|
||
} catch (e) {
|
||
alert(e.message);
|
||
}
|
||
|
||
fetchData();
|
||
}
|
||
|
||
const updateCACChart = function () {
|
||
const baseDataset = Alpine.store("dataset").grouppedVotes;
|
||
|
||
const combinedDataset = {};
|
||
Object.values(Alpine.store("dataset").grouppedVotes).forEach(e => {
|
||
const rds = e.reduce((acc, v) => {
|
||
acc[v.vote_date] ??= {};
|
||
Object.keys(dbToCategory).forEach(key => {
|
||
acc[v.vote_date][key] ??= [];
|
||
acc[v.vote_date][key].push(...v[key])
|
||
});
|
||
return acc;
|
||
}, {});
|
||
Object.keys(rds).forEach(key => {
|
||
combinedDataset[key] ??= {};
|
||
Object.keys(rds[key]).forEach(kv => {
|
||
combinedDataset[key][kv] ??= [];
|
||
combinedDataset[key][kv].push(...rds[key][kv]);
|
||
});
|
||
});
|
||
}, {});
|
||
console.log(combinedDataset);
|
||
|
||
const labels = Object.keys(dbToCategory).map(key => dbToCategory[key]);
|
||
const datasets = Object.keys(combinedDataset).map(voteDate => {
|
||
const avgs = Object.keys(dbToCategory).map(key => average(combinedDataset[voteDate][key]));
|
||
const d = {
|
||
label: voteDate,
|
||
backgroundColor: randomRGBO(0.2), // FIIXME: randomize
|
||
pointBackgroundColor: avgs.map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: avgs,
|
||
};
|
||
|
||
return d;
|
||
});
|
||
|
||
ct.updateChartData("clubAverageCategories", labels, datasets);
|
||
}
|
||
|
||
const updateGACChart = function () {
|
||
const [baseDataset, group] = [
|
||
Alpine.store("dataset").grouppedVotes,
|
||
Alpine.store("dataset").selectedGacavVgroup,
|
||
];
|
||
|
||
const labels = Object.keys(dbToCategory).map(key => dbToCategory[key]);
|
||
const datasets = baseDataset[group].map(e => {
|
||
const avgs = Object.keys(dbToCategory).map(key => e[key]).map(average);
|
||
const d = {
|
||
label: e.vote_date,
|
||
backgroundColor: randomRGBO(0.2), // FIIXME: randomize
|
||
pointBackgroundColor: avgs.map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: avgs,
|
||
};
|
||
|
||
return d;
|
||
});
|
||
|
||
ct.updateChartData("groupAverageCategories", labels, datasets);
|
||
}
|
||
|
||
const update__Chart = function () {
|
||
const [baseDataset, voteDate] = [
|
||
Alpine.store("dataset").grouppedVotes,
|
||
Alpine.store("dataset").agacVoteDate,
|
||
];
|
||
|
||
const labels = [];
|
||
const datasets = [];
|
||
Object.keys(gvotes).forEach(cat => {
|
||
labels.push(dbToCategory[cat]);
|
||
// colors.push(getColorBasedOnValue(cat == "serenity" ? 10-av : av));
|
||
});
|
||
|
||
ct.updateChartData("allGroupAverageCategories", labels, datasets);
|
||
}
|
||
|
||
const updateAGACChart = function() {
|
||
const [baseDataset, voteDate] = [
|
||
Alpine.store("dataset").grouppedVotes,
|
||
Alpine.store("dataset").agacVoteDate,
|
||
];
|
||
|
||
const datasets = [];
|
||
|
||
const gvotes = {};
|
||
Object.values(baseDataset).forEach(vg => {
|
||
vg.forEach(ds => {
|
||
if (ds.vote_date === voteDate) {
|
||
Object.keys(dbToCategory).forEach(cat => {
|
||
const av = average(ds[cat]);
|
||
gvotes[cat] ??= [];
|
||
gvotes[cat].push(av);
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
const labels = [];
|
||
const colors = [];
|
||
const vals = [];
|
||
Object.keys(gvotes).forEach(cat => {
|
||
const av = average(gvotes[cat]);
|
||
labels.push(dbToCategory[cat]);
|
||
colors.push(getColorBasedOnValue(cat == "serenity" ? 10-av : av));
|
||
vals.push(av);
|
||
});
|
||
datasets.push({
|
||
label: labels,
|
||
backgroundColor: colors,
|
||
borderWidth: 2,
|
||
data: vals,
|
||
});
|
||
|
||
ct.updateChartData("allGroupAverageCategories", labels, datasets);
|
||
}
|
||
|
||
const updateRadarChart = function() {
|
||
const [baseDataset, group, voteDate] = [
|
||
Alpine.store("dataset").grouppedVotes,
|
||
Alpine.store("dataset").selectedRadarVgroup,
|
||
Alpine.store("dataset").voteDate,
|
||
];
|
||
|
||
const groupDataset = baseDataset[group].find(e => e.vote_date === voteDate);
|
||
const labels = [];
|
||
const datasets = [];
|
||
if (groupDataset) {
|
||
const vals = [];
|
||
Object.keys(dbToCategory).forEach(k => {
|
||
labels.push(dbToCategory[k]);
|
||
vals.push(average(groupDataset[k]));
|
||
});
|
||
const dataset = {
|
||
label: getVGroupNameById(group),
|
||
backgroundColor: vals.map((v, i) => getColorBasedOnValue(labels[i] == "Тревожность" ? 10-v : v)),
|
||
borderWidth: 2,
|
||
data: vals
|
||
};
|
||
datasets.push(dataset);
|
||
}
|
||
|
||
ct.updateChartData("groupCategories", labels, datasets);
|
||
}
|
||
|
||
const updateLineChart = function() {
|
||
const [baseDataset, group] = [Alpine.store("dataset").grouppedVotes, Alpine.store("dataset").selectedVgroup];
|
||
const labels = baseDataset[group].map(e => e.vote_date);
|
||
|
||
const datasets = [
|
||
{
|
||
label: "Здоровье",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "health").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "health")
|
||
},
|
||
{
|
||
label: "Любовь",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "love").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "love")
|
||
},
|
||
{
|
||
label: "Секс",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "sex").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "sex")
|
||
},
|
||
{
|
||
label: "Отдых",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "rest").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "rest")
|
||
},
|
||
{
|
||
label: "Деньги",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "finances").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "finances")
|
||
},
|
||
{
|
||
label: "Смысл жизни",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "meaning_of_life").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "meaning_of_life")
|
||
},
|
||
{
|
||
label: "Тревожность",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "serenity").map(e => getColorBasedOnValue(10-e)),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "serenity")
|
||
},
|
||
{
|
||
label: "Отношения",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "relations").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "relations")
|
||
},
|
||
{
|
||
label: "Личн. рост",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "pers_growth").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "pers_growth")
|
||
},
|
||
{
|
||
label: "Работа",
|
||
backgroundColor: fetchDatasetCategory(baseDataset, group, "work").map(getColorBasedOnValue),
|
||
borderWidth: 2,
|
||
data: fetchDatasetCategory(baseDataset, group, "work")
|
||
}
|
||
];
|
||
|
||
ct.updateChartData("categoryBars", labels, datasets);
|
||
}
|
||
</script> |