feat(dashboard): add customization

This commit is contained in:
Simon Vieille 2025-02-08 15:58:58 +01:00
commit d8ec62c239
16 changed files with 193 additions and 86 deletions

View file

@ -27,7 +27,7 @@ tpl:
TEMPL_EXPERIMENT=rawgo templ generate
lint:
npm run lint
npm run lint || true
npm run format
.PHONY:

View file

@ -41,7 +41,12 @@ const compute = (transactions, cats, dateFrom, dateTo) => {
datas[date] = {}
}
if (!Object.prototype.hasOwnProperty.call(datas[date], value.category.id.toString())) {
if (
!Object.prototype.hasOwnProperty.call(
datas[date],
value.category.id.toString(),
)
) {
datas[date][value.category.id.toString()] = 0
}

View file

@ -2,7 +2,7 @@
<div v-if="dataValue && dataValue.length">
<div
v-for="(item, key) in dataValue"
v-bind:key="key"
:key="key"
>
<BFormGroup
:label="item.label"
@ -18,7 +18,9 @@
</div>
<div class="col-12 col-md-7 mb-1">
<BFormInput
v-if="item.type == 'string' && needValue(dataValue[key].comparator)"
v-if="
item.type == 'string' && needValue(dataValue[key].comparator)
"
v-model="dataValue[key].value"
@change="changed"
/>
@ -31,14 +33,18 @@
/>
<BFormInput
v-if="item.type == 'number' && needValue(dataValue[key].comparator)"
v-if="
item.type == 'number' && needValue(dataValue[key].comparator)
"
v-model="dataValue[key].value"
type="number"
@change="changed"
/>
<BFormSelect
v-if="item.type == 'select' && needValue(dataValue[key].comparator)"
v-if="
item.type == 'select' && needValue(dataValue[key].comparator)
"
v-model="dataValue[key].value"
:options="item.options"
@change="changed"

View file

@ -10,10 +10,12 @@
</div>
<div>
<Line
v-if="data.length > 0"
:data="compute(data, precision, dateFrom, dateTo)"
:options="options()"
:style="chartStyle(300)"
/>
<div v-else>Aucune donnée</div>
</div>
</div>
</template>

View file

@ -5,7 +5,7 @@
<div class="row mt-3 mb-5">
<div
v-for="(item, key) in categories"
v-bind:key="key"
:key="key"
class="col-auto checkbox"
:class="{'checkbox--unchecked': !selectedCategories[item.label]}"
>

View file

@ -9,7 +9,7 @@
<option value="">Tous</option>
<option
v-for="item in accounts"
v-bind:key="item.id"
:key="item.id"
:value="item.id"
v-text="item.label"
></option>

View file

@ -60,14 +60,7 @@
</template>
<script setup>
import {
BTableSimple,
BThead,
BTbody,
BTr,
BTh,
BTd,
} from 'bootstrap-vue-next'
import {BTableSimple, BThead, BTbody, BTr, BTh, BTd} from 'bootstrap-vue-next'
import {renderCategory, renderEuro} from '../../lib/renderers'
defineProps(['data'])

View file

@ -29,11 +29,13 @@ const saveStorage = (key, data, name) => {
localStorage.setItem(key, JSON.stringify(data))
}
const getStorage = (key) => {
const getStorage = (key, defaultValue) => {
try {
return JSON.parse(localStorage.getItem(key))
const value = JSON.parse(localStorage.getItem(key))
return value === null ? defaultValue : value
} catch {
return null
return defaultValue
}
}

View file

@ -44,7 +44,7 @@
<BTr>
<BTh
v-for="field in fields"
v-bind:key="field.key"
:key="field.key"
:width="field.width"
class="cursor"
:class="field.thClasses"
@ -61,10 +61,13 @@
</BTr>
</BThead>
<BTbody>
<BTr v-for="(row, key) in data.rows" v-bind:key="key">
<BTr
v-for="(row, key) in data.rows"
:key="key"
>
<BTd
v-for="field in fields"
v-bind:key="field.key"
:key="field.key"
class="cursor"
:class="field.tdClasses"
@click="doEdit(row)"
@ -90,8 +93,8 @@
</div>
<BModal
v-if="form !== null"
v-model="formShow"
ref="modal"
v-model="formShow"
scrollable
class="modal-xl"
:title="form?.label"
@ -106,8 +109,8 @@
<BForm @submit="doSave">
<BFormGroup
v-for="(field, key) in form.fields"
v-bind:key="key"
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"
@ -138,7 +141,7 @@
>
<div
v-for="(rule, key) in form.data[field.key]"
v-bind:key="key"
:key="key"
class="list-group-item"
>
<div class="d-block d-lg-flex justify-content-between gap-1">

View file

@ -8,76 +8,100 @@
categories.length > 0
"
>
<Filters
v-model:account="account"
v-model:date-from="dateFrom"
v-model:date-to="dateTo"
:accounts="accounts"
@update="refresh"
/>
<div class="d-flex">
<Filters
v-model:account="account"
v-model:date-from="dateFrom"
v-model:date-to="dateTo"
:accounts="accounts"
@update="refresh"
/>
<div class="row text-center">
<div
class="col-12 col-md-8 mb-4"
:class="{'col-md-8': savingAccounts.length > 0}"
>
<Capital
:data="data"
:date-from="dateFrom"
:date-to="dateTo"
/>
<div class="ms-auto">
<BButton @click="toggleMode()" variant="none">
<i class="fa-solid fa-gear"></i>
</BButton>
</div>
</div>
<div
v-if="savingAccounts.length > 0"
class="col-12 col-md-4 mb-4"
>
<SavingAccounts :data="savingAccounts" />
</div>
</div>
<div class="row">
<div class="mb-4">
<Distribution
:data="data"
:categories="categories"
:date-from="dateFrom"
:date-to="dateTo"
/>
</div>
</div>
<div class="row">
<div class="col-12 mb-4 col-lg-12">
<DiffCreditDebit
:data="data"
:date-from="dateFrom"
:date-to="dateTo"
/>
</div>
</div>
<div class="row">
<div class="col-12 mb-4 col-lg-12">
<MonthThresholds
:data="monthThresholdsData()"
:date-from="dateFrom"
:date-to="dateTo"
/>
</div>
</div>
<Draggable v-model="config" class="row" handle=".handle" @change="refresh()">
<transition-group>
<div
v-for="(item, key) in config"
:key="key"
class="component mb-4 col-12"
:class="componentClasses(item)"
>
<div
v-if="mode === 'edit'"
class="mb-3 d-flex justify-content-beetween"
>
<BButton variant="none" size="sm" class="handle">
<i class="fa-solid fa-up-down-left-right"></i>
</BButton>
<BButtonGroup size="sm" class="d-sm-none d-md-block">
<BButton
v-for="i in [
{size: 3, label: '1'},
{size: 6, label: '2'},
{size: 9, label: '3'},
{size: 12, label: '4'},
]"
:variant="i.size == config[key].size ? 'primary' : 'secondary'"
@click="config[key].size = i.size"
>{{ i.label }}</BButton>
</BButtonGroup>
</div>
<Capital
v-if="item.component === 'Capital'"
:data="data"
:date-from="dateFrom"
:date-to="dateTo"
/>
<SavingAccounts
v-if="item.component === 'SavingAccounts'"
:data="savingAccounts"
/>
<Distribution
v-if="item.component === 'Distribution'"
:data="data"
:categories="categories"
:date-from="dateFrom"
:date-to="dateTo"
/>
<DiffCreditDebit
v-if="item.component === 'DiffCreditDebit'"
:data="data"
:date-from="dateFrom"
:date-to="dateTo"
/>
<MonthThresholds
v-if="item.component === 'MonthThresholds'"
:data="monthThresholdsData()"
:date-from="dateFrom"
:date-to="dateTo"
/>
</div>
</transition-group>
</Draggable>
</div>
<div v-else>Chargement...</div>
</div>
</template>
<script setup>
import {ref, onMounted, watch} from 'vue'
import {ref, reactive, onMounted, watch} from 'vue'
import {compute as monthThresholds} from '../chart/monthThreshold'
import {getStorage, saveStorage} from '../lib/storage'
import {BButtonGroup, BButton} from 'bootstrap-vue-next'
import Filters from './../components/dashboard/Filters.vue'
import Capital from './../components/dashboard/Capital.vue'
import SavingAccounts from './../components/dashboard/SavingAccounts.vue'
import Distribution from './../components/dashboard/Distribution.vue'
import MonthThresholds from './../components/dashboard/MonthThresholds.vue'
import DiffCreditDebit from './../components/dashboard/DiffCreditDebit.vue'
import { VueDraggableNext as Draggable } from 'vue-draggable-next'
const data = ref(null)
const isLoading = ref(true)
@ -86,9 +110,20 @@ const accounts = ref([])
const categories = ref([])
const savingAccounts = ref([])
const mode = ref('view')
const account = ref(getStorage(`dashboard:account`))
const dateFrom = ref(getStorage(`dashboard:dateFrom`))
const dateTo = ref(getStorage(`dashboard:dateTo`))
const config = reactive(
getStorage(`dashboard:config`, [
{component: 'Capital', size: 8},
{component: 'SavingAccounts', size: 4},
{component: 'Distribution', size: 12},
{component: 'DiffCreditDebit', size: 12},
{component: 'MonthThresholds', size: 12},
]),
)
let winWidth = 0
const _monthThresholdsData = ref(null)
@ -107,9 +142,27 @@ const monthThresholdsData = () => {
return _monthThresholdsData.value
}
const toggleMode = () => {
mode.value = mode.value === 'edit' ? 'view' : 'edit'
}
const componentClasses = (item) => {
const data = [`col-md-${item.size}`]
if (mode.value === 'edit') {
data.push('editable')
}
return data
}
watch(dateFrom, (v) => saveStorage(`dashboard:dateFrom`, v))
watch(dateTo, (v) => saveStorage(`dashboard:dateTo`, v))
watch(account, (v) => saveStorage(`dashboard:account`, v))
watch(config, (v) => {
saveStorage(`dashboard:config`, v)
refresh()
})
window.addEventListener('resize', () => {
if (Math.abs(window.innerWidth - winWidth) < 20) {
@ -130,6 +183,7 @@ const refresh = () => {
limit: 0,
}
isLoading.value = true
_monthThresholdsData.value = null
if (account.value) {
@ -171,3 +225,14 @@ onMounted(() => {
})
})
</script>
<style scoped>
.config {
width: 200px;
max-width: 100%;
}
.handle {
cursor: grab;
}
</style>

View file

@ -33,7 +33,7 @@
</div>
<div
v-for="(item, key) in tree.directories"
v-bind:key="key"
:key="key"
class="p-3 border-bottom flex justify-content-center"
>
<div class="float-end">
@ -55,7 +55,10 @@
{{ item.name }}
</div>
</div>
<div v-for="item in tree.files" v-bind:key="item">
<div
v-for="item in tree.files"
:key="item"
>
<div class="float-end pt-3 me-3">
<BDropdown
text=""
@ -120,8 +123,8 @@
<BForm @submit="doSave">
<BFormGroup
v-for="(field, key) in form.fields"
v-bind:key="key"
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"

View file

@ -49,7 +49,10 @@
</BTr>
</BThead>
<BTbody>
<BTr v-for="(row, key) in data.rows" :key="key">
<BTr
v-for="(row, key) in data.rows"
:key="key"
>
<BTd
v-for="field in fields"
:key="field.key"

View file

@ -66,7 +66,10 @@
</BTr>
</BThead>
<BTbody>
<BTr v-for="(row, key) in data.rows" :key="key">
<BTr
v-for="(row, key) in data.rows"
:key="key"
>
<BTd
v-for="field in fields"
:key="field.key"

View file

@ -48,7 +48,10 @@
</BTr>
</BThead>
<BTbody>
<BTr v-for="(row, key) in data.rows" :key="key">
<BTr
v-for="(row, key) in data.rows"
:key="key"
>
<BTd
v-for="field in fields"
:key="field.key"
@ -88,8 +91,8 @@
<BForm @submit="doSave">
<BFormGroup
v-for="(field, key) in form.fields"
v-bind:key="key"
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"

18
package-lock.json generated
View file

@ -13,6 +13,7 @@
"raw-loader": "^4.0.2",
"vue": "^3.4.29",
"vue-chartjs": "^5.3.1",
"vue-draggable-next": "^2.2.1",
"vue-template-compiler": "^2.7.16",
"vue3-spinners": "^1.2.2"
},
@ -7548,6 +7549,13 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/sortablejs": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz",
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==",
"license": "MIT",
"peer": true
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -8122,6 +8130,16 @@
"vue": "^3.0.0-0 || ^2.7.0"
}
},
"node_modules/vue-draggable-next": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vue-draggable-next/-/vue-draggable-next-2.2.1.tgz",
"integrity": "sha512-EAMS1IRHF0kZO0o5PMOinsQsXIqsrKT1hKmbICxG3UEtn7zLFkLxlAtajcCcUTisNvQ6TtCB5COjD9a1raNADw==",
"license": "MIT",
"peerDependencies": {
"sortablejs": "^1.14.0",
"vue": "^3.2.2"
}
},
"node_modules/vue-eslint-parser": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",

View file

@ -8,6 +8,7 @@
"raw-loader": "^4.0.2",
"vue": "^3.4.29",
"vue-chartjs": "^5.3.1",
"vue-draggable-next": "^2.2.1",
"vue-template-compiler": "^2.7.16",
"vue3-spinners": "^1.2.2"
},