Merge branch 'feature/refactoring' into develop

This commit is contained in:
Simon Vieille 2025-03-25 23:02:43 +01:00
commit 84e201ba41
Signed by: deblan
GPG key ID: 579388D585F70417
52 changed files with 2773 additions and 2091 deletions

View file

@ -1,5 +1,5 @@
{
"bracketSpacing": false,
"bracketSpacing": true,
"bracketSameLine": false,
"semi": false,
"singleQuote": true,

View file

@ -5,6 +5,7 @@ import (
"encoding/csv"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"regexp"
"strconv"
@ -99,6 +100,10 @@ func (t *Transaction) MatchRule(rule CategoryRule) (bool, int) {
}
}
if match {
fmt.Printf("%+v\n", match)
}
return match, counter
}

View file

@ -1,6 +1,6 @@
<script setup>
import {RouterLink, RouterView} from 'vue-router'
import {BNavItem} from 'bootstrap-vue-next'
import { RouterLink, RouterView } from 'vue-router'
import { BNavItem } from 'bootstrap-vue-next'
</script>
<template>
@ -29,7 +29,7 @@ import {BNavItem} from 'bootstrap-vue-next'
'/files',
'/users',
]"
v-slot="{href, route, navigate, isActive, isExactActive}"
v-slot="{ href, route, navigate, isActive, isExactActive }"
:to="url"
custom
>
@ -41,7 +41,7 @@ import {BNavItem} from 'bootstrap-vue-next'
class="me-2"
:class="route.meta.icon"
></i>
<span class="nav-item-label">{{ route.name }}</span>
<span class="nav-item-label">{{ route.meta.label }}</span>
</BNavItem>
</RouterLink>
</ul>

View file

@ -1,4 +1,4 @@
import {isInRange} from '../lib/dateFilter'
import { isInRange } from '../lib/dateFilter'
const getDate = (value, precision) => {
const d = new Date(value)
@ -79,4 +79,4 @@ const compute = (transactions, precision, dateFrom, dateTo) => {
return config
}
export {compute}
export { compute }

View file

@ -1,4 +1,4 @@
import {isInRange} from '../lib/dateFilter'
import { isInRange } from '../lib/dateFilter'
const getDate = (value) => {
const d = new Date(value)
@ -64,7 +64,8 @@ const compute = (transactions, dateFrom, dateTo, order) => {
const monthsValues = Object.values(data[i].months)
data[i].monthAverage = monthsValues.reduce((a, b) => a + b, 0) / monthsValues.length
data[i].monthAverage =
monthsValues.reduce((a, b) => a + b, 0) / monthsValues.length
}
data = Object.values(data).sort((a, b) => {
@ -82,4 +83,4 @@ const compute = (transactions, dateFrom, dateTo, order) => {
return data
}
export {compute}
export { compute }

View file

@ -1,4 +1,4 @@
import {isInRange} from '../lib/dateFilter'
import { isInRange } from '../lib/dateFilter'
const getDate = (value) => {
const d = new Date(value)
@ -68,4 +68,4 @@ const compute = (transactions, dateFrom, dateTo) => {
return config
}
export {compute}
export { compute }

View file

@ -1,4 +1,4 @@
import {isInRange} from '../lib/dateFilter'
import { isInRange } from '../lib/dateFilter'
const getDate = (value) => {
const d = new Date(value)
@ -164,4 +164,4 @@ const computeDoughnut = (
}
}
export {computeDoughnut, computeBar}
export { computeDoughnut, computeBar }

View file

@ -1,4 +1,4 @@
import {isInRange} from '../lib/dateFilter'
import { isInRange } from '../lib/dateFilter'
const getDate = (value) => {
const d = new Date(value)
@ -41,12 +41,7 @@ const compute = (transactions, cats, dateFrom, dateTo) => {
datas[date] = {}
}
if (
!Object.hasOwn(
datas[date],
value.category.id.toString(),
)
) {
if (!Object.hasOwn(datas[date], value.category.id.toString())) {
datas[date][value.category.id.toString()] = 0
}
@ -73,4 +68,4 @@ const compute = (transactions, cats, dateFrom, dateTo) => {
return datas
}
export {compute}
export { compute }

View file

@ -27,4 +27,4 @@ const compute = (accounts) => {
}
}
export {compute}
export { compute }

View file

@ -86,8 +86,13 @@
</template>
<script setup>
import {ref, onMounted, defineEmits} from 'vue'
import {BFormGroup, BFormInput, BFormSelect, BButton} from 'bootstrap-vue-next'
import { ref, onMounted, defineEmits } from 'vue'
import {
BFormGroup,
BFormInput,
BFormSelect,
BButton,
} from 'bootstrap-vue-next'
const emit = defineEmits(['update'])
const props = defineProps(['data', 'fields'])
@ -95,15 +100,15 @@ const dataValue = ref(props.data.value)
const choices = ref([])
const choice = ref(null)
const _ = {value: null, text: null}
const eq = {value: 'eq', text: '='}
const neq = {value: 'neq', text: '!='}
const like = {value: 'like', text: 'contient'}
const nlike = {value: 'nlike', text: 'ne content pas'}
const empty = {value: 'empty', text: 'est vide'}
const nEmpty = {value: 'nempty', text: 'est pas vide'}
const gt = {value: 'gt', text: '>='}
const lt = {value: 'lt', text: '<='}
const _ = { value: null, text: null }
const eq = { value: 'eq', text: '=' }
const neq = { value: 'neq', text: '!=' }
const like = { value: 'like', text: 'contient' }
const nlike = { value: 'nlike', text: 'ne content pas' }
const empty = { value: 'empty', text: 'est vide' }
const nEmpty = { value: 'nempty', text: 'est pas vide' }
const gt = { value: 'gt', text: '>=' }
const lt = { value: 'lt', text: '<=' }
const stringOptions = [_, eq, neq, like, nlike, empty, nEmpty]
const dateOptions = [_, eq, neq, gt, lt, empty, nEmpty]
@ -174,7 +179,7 @@ onMounted(() => {
dataValue.value = props.data
for (let field of props.fields) {
choices.value.push({value: field.key, text: field.label})
choices.value.push({ value: field.key, text: field.label })
}
})
</script>

View file

@ -0,0 +1,87 @@
<template>
<BTableSimple caption-top>
<BThead>
<BTr>
<BTh
v-for="header in headers"
:key="header.key"
:width="header.width"
class="cursor"
:class="header.thClasses"
valign="top"
@click="sort(header.orderKey ?? header.key)"
>
<SortButton
:current-order="order"
:current-sort="sort"
:order="header.orderKey ?? header.key"
:label="header.label ?? header.renderLabel(rows)"
/>
</BTh>
</BTr>
</BThead>
<BTbody>
<BTr
v-for="(row, key) in rows"
:key="key"
>
<BTd
v-for="header in headers"
:key="header.key"
class="cursor"
:class="header.tdClasses"
@click="rowClick(row)"
>
<span v-if="header.key">
<span
v-if="header.render"
v-html="header.render(row)"
></span>
<span
v-else
v-text="row[header.key]"
></span>
</span>
<span
v-else
v-html="header.render(row)"
></span>
</BTd>
</BTr>
</BTbody>
</BTableSimple>
</template>
<script setup>
import { BTableSimple, BThead, BTbody, BTr, BTh, BTd } from 'bootstrap-vue-next'
import SortButton from '../SortButton.vue'
defineProps({
headers: {
type: [Array, null],
required: true,
},
rows: {
type: [Array, null],
required: true,
},
order: {
type: [String, null],
required: false,
},
sort: {
type: [String, null],
required: false,
},
})
const emit = defineEmits(['sort', 'rowClick'])
const sort = (key) => {
emit('sort', key)
}
const rowClick = (row) => {
emit('rowClick', row)
}
</script>

View file

@ -0,0 +1,28 @@
<template>
<template
v-for="(field, key) in form.fields"
:key="key"
>
<FormWidget
v-if="fields.length === 0 || fields.includes(field.key)"
:form="form"
:field="field"
/>
</template>
</template>
<script setup>
import FormWidget from './FormWidget.vue'
defineProps({
form: {
type: Object,
required: true,
},
fields: {
type: [Array],
required: false,
default: [],
},
})
</script>

View file

@ -0,0 +1,166 @@
<template>
<BFormGroup
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"
:description="field.description"
>
<BFormInput
v-if="(field.widget ?? 'text') === 'text'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:type="field.type"
:required="field.required"
/>
<BFormFile
v-if="(field.widget ?? 'text') === 'file'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:type="field.type"
:required="field.required"
/>
<BFormSelect
v-if="(field.widget ?? 'text') === 'select'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:options="field.options"
:required="field.required"
/>
<div
v-if="field.widget === 'rules'"
id="rules"
>
<div
v-if="form.data[field.key] !== null && form.data[field.key].length > 0"
>
<div
v-for="(rule, key) in form.data[field.key]"
:key="key"
class="p-3"
:class="{ 'border-bottom': key + 1 !== form.data[field.key].length }"
>
<div class="d-block d-lg-flex justify-content-between gap-1">
<BFormInput
v-if="form.data[field.key][key].id !== null"
v-model="form.data[field.key][key].id"
type="hidden"
/>
<BFormGroup
class="mb-2"
label="Libellé contient"
:label-for="'form-rule-contain-' + key"
>
<BFormInput
:id="'form-rule-contain-' + key"
v-model="form.data[field.key][key].contain"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Regex libellé"
:label-for="'form-rule-match-' + key"
>
<BFormInput
:id="'form-rule-match-' + key"
v-model="form.data[field.key][key].match"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Catégorie banque"
:label-for="'form-rule-contain-' + key"
>
<BFormInput
:id="'form-rule-bank-category-contain-' + key"
v-model="form.data[field.key][key].bank_category"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Montant"
:label-for="'form-rule-amount-' + key"
>
<BFormInput
:id="'form-rule-amount-' + key"
v-model="form.data[field.key][key].amount"
type="number"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Du"
:label-for="'form-rule-datefrom-' + key"
>
<BFormInput
:id="'form-rule-datefrom-' + key"
v-model="form.data[field.key][key].date_from"
type="date"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Au"
:label-for="'form-rule-dateto-' + key"
>
<BFormInput
:id="'form-rule-dateto-' + key"
v-model="form.data[field.key][key].date_to"
type="date"
/>
</BFormGroup>
<BButton
variant="none"
@click="
form.data[field.key] = doRemoveRule(form.data[field.key], key)
"
>
<i class="fa fa-trash"></i>
</BButton>
</div>
</div>
</div>
</div>
</BFormGroup>
</template>
<script setup>
import {
BFormGroup,
BFormSelect,
BFormInput,
BFormFile,
BButton,
} from 'bootstrap-vue-next'
defineProps({
key: {
required: true,
},
form: {
type: Object,
required: true,
},
field: {
type: Object,
required: true,
},
})
const doRemoveRule = (item, key) => {
let values = []
item.forEach((v, k) => {
if (k !== key) {
values.push(v)
}
})
return values
}
</script>

View file

@ -1,17 +1,30 @@
<template>
<div class="header d-block p-3 mb-3">
<div class="d-md-flex justify-content-between">
<div class="d-md-flex justify-content-between menu">
<h3>{{ title }}</h3>
<slot name="menu"></slot>
</div>
<div v-if="$slots.bottom" class="pt-3">
<div
v-if="$slots.bottom"
class="pt-3"
>
<slot name="bottom"></slot>
</div>
</div>
</template>
<script setup>
import {defineProps} from 'vue'
import { defineProps } from 'vue'
defineProps(['title'])
</script>
<style>
.menu .btn {
margin-bottom: 4px;
}
.menu .btn:not(:last-child) {
margin-right: 4px;
}
</style>

View file

@ -20,8 +20,8 @@
</template>
<script setup>
import {defineProps, defineEmits} from 'vue'
import {BFormSelect, BPagination} from 'bootstrap-vue-next'
import { defineProps, defineEmits } from 'vue'
import { BFormSelect, BPagination } from 'bootstrap-vue-next'
defineProps(['page', 'pages', 'limit'])
@ -43,7 +43,7 @@ const limits = () => {
for (let i of [10, 20, 50, 100, 0]) {
let label = i !== 0 ? i : 'Tout'
v.push({value: i, text: label})
v.push({ value: i, text: label })
}
return v

View file

@ -21,12 +21,12 @@
</template>
<script setup>
import {ref, watch, defineProps} from 'vue'
import {getStorage, saveStorage} from '../../lib/storage'
import {compute} from '../../chart/capital'
import {Line} from 'vue-chartjs'
import {BFormSelect} from 'bootstrap-vue-next'
import {chartStyle} from '../../lib/chartStyle'
import { ref, watch, defineProps } from 'vue'
import { getStorage, saveStorage } from '../../lib/storage'
import { compute } from '../../chart/capital'
import { Line } from 'vue-chartjs'
import { BFormSelect } from 'bootstrap-vue-next'
import { chartStyle } from '../../lib/chartStyle'
defineProps(['data', 'dateFrom', 'dateTo'])
@ -36,9 +36,9 @@ watch(precision, (v) => saveStorage('dashboard:capital:precision', v))
const precisions = () => {
return [
{value: 'month', text: 'Mois'},
{value: '2weeks', text: '2 semaines'},
{value: 'day', text: 'Jour'},
{ value: 'month', text: 'Mois' },
{ value: '2weeks', text: '2 semaines' },
{ value: 'day', text: 'Jour' },
]
}

View file

@ -19,8 +19,14 @@
<BTh
width="170px"
class="text-end"
v-html="renderLabelWithSum('Total', computed(data, dateFrom, dateTo, order), 'sum')"></BTh
>
v-html="
renderLabelWithSum(
'Total',
computed(data, dateFrom, dateTo, order),
'sum',
)
"
></BTh>
<BTh
width="250px"
class="text-end"
@ -67,8 +73,8 @@
</template>
<script setup>
import {ref, watch} from 'vue'
import {getStorage, saveStorage} from '../../lib/storage'
import { ref, watch } from 'vue'
import { getStorage, saveStorage } from '../../lib/storage'
import {
BFormSelect,
BTableSimple,
@ -78,19 +84,19 @@ import {
BTh,
BTd,
} from 'bootstrap-vue-next'
import {compute} from '../../chart/debitAverage'
import { compute } from '../../chart/debitAverage'
import {
renderLabelWithSum,
renderCategory,
renderEuro
renderEuro,
} from '../../lib/renderers'
const order = ref(getStorage('dashboard:debitAverage:order', 'sum'))
const orders = [
{value: 'sum', text: 'Total'},
{value: 'average', text: 'Débit moyen / transaction'},
{value: 'monthAverage', text: 'Débit moyen / mois'},
{value: 'count', text: 'Transactions'},
{ value: 'sum', text: 'Total' },
{ value: 'average', text: 'Débit moyen / transaction' },
{ value: 'monthAverage', text: 'Débit moyen / mois' },
{ value: 'count', text: 'Transactions' },
]
let cache = null

View file

@ -13,9 +13,9 @@
</template>
<script setup>
import {Bar} from 'vue-chartjs'
import {compute} from '../../chart/diffCreditDebit'
import {chartStyle} from '../../lib/chartStyle'
import { Bar } from 'vue-chartjs'
import { compute } from '../../chart/diffCreditDebit'
import { chartStyle } from '../../lib/chartStyle'
defineProps(['data', 'dateFrom', 'dateTo'])

View file

@ -7,7 +7,7 @@
v-for="(item, key) in categories"
:key="key"
class="col-auto checkbox"
:class="{'checkbox--unchecked': !selectedCategories[item.label]}"
:class="{ 'checkbox--unchecked': !selectedCategories[item.label] }"
>
<BFormCheckbox v-model="selectedCategories[item.label]">
<span
@ -42,7 +42,7 @@
<div class="col-12 col-lg-4">
<Doughnut
:data="computeDoughnut(data, dateFrom, dateTo, selectedCategories)"
:options="fixedOptions({plugins: {legend: {display: false}}})"
:options="fixedOptions({ plugins: { legend: { display: false } } })"
:style="chartStyle(380)"
/>
</div>
@ -55,7 +55,7 @@
:data="
computeBar(data, isStacked, dateFrom, dateTo, selectedCategories)
"
:options="stackOptions({plugins: {legend: {display: false}}})"
:options="stackOptions({ plugins: { legend: { display: false } } })"
:style="chartStyle(380)"
/>
</div>
@ -65,19 +65,19 @@
</template>
<script setup>
import {ref, toRefs, watch, defineProps, onMounted} from 'vue'
import {getStorage, saveStorage} from '../../lib/storage'
import {computeDoughnut, computeBar} from '../../chart/distribution'
import {renderCategory} from '../../lib/renderers'
import {Doughnut, Bar} from 'vue-chartjs'
import {BFormCheckbox} from 'bootstrap-vue-next'
import {chartStyle} from '../../lib/chartStyle'
import { ref, toRefs, watch, defineProps, onMounted } from 'vue'
import { getStorage, saveStorage } from '../../lib/storage'
import { computeDoughnut, computeBar } from '../../chart/distribution'
import { renderCategory } from '../../lib/renderers'
import { Doughnut, Bar } from 'vue-chartjs'
import { BFormCheckbox } from 'bootstrap-vue-next'
import { chartStyle } from '../../lib/chartStyle'
const props = defineProps(['data', 'categories', 'dateFrom', 'dateTo'])
const isStacked = ref(getStorage('dashboard:distribution:isStacked'))
const selectedCategories = ref({})
const {categories} = toRefs(props)
const { categories } = toRefs(props)
watch(isStacked, (v) => saveStorage('dashboard:distribution:isStacked', v))
@ -92,11 +92,11 @@ const fixedOptions = (opts) => {
}
const stackOptions = (opts) => {
let options = {...fixedOptions(opts)}
let options = { ...fixedOptions(opts) }
options.scales = {
y: {stacked: true, ticks: {beginAtZero: true}},
x: {stacked: true},
y: { stacked: true, ticks: { beginAtZero: true } },
x: { stacked: true },
}
return options

View file

@ -63,8 +63,8 @@
</template>
<script setup>
import {toRefs, defineProps, defineEmits} from 'vue'
import {BFormInput} from 'bootstrap-vue-next'
import { toRefs, defineProps, defineEmits } from 'vue'
import { BFormInput } from 'bootstrap-vue-next'
const emit = defineEmits([
'update:account',
@ -75,7 +75,7 @@ const emit = defineEmits([
const props = defineProps(['account', 'accounts', 'dateFrom', 'dateTo'])
const {dateFrom, dateTo} = toRefs(props)
const { dateFrom, dateTo } = toRefs(props)
const change = (event, value) => {
emit(event, value)

View file

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

View file

@ -12,10 +12,10 @@
</template>
<script setup>
import {defineProps} from 'vue'
import {compute} from '../../chart/savingAccount'
import {Bar} from 'vue-chartjs'
import {chartStyle} from '../../lib/chartStyle'
import { defineProps } from 'vue'
import { compute } from '../../chart/savingAccount'
import { Bar } from 'vue-chartjs'
import { chartStyle } from '../../lib/chartStyle'
defineProps(['data'])
@ -25,8 +25,8 @@ const options = () => {
maintainAspectRatio: true,
indexAxis: 'y',
scales: {
y: {stacked: true, ticks: {beginAtZero: true}},
x: {stacked: true},
y: { stacked: true, ticks: { beginAtZero: true } },
x: { stacked: true },
},
}
}

View file

@ -5,4 +5,4 @@ const chartStyle = (height) => {
}
}
export {chartStyle}
export { chartStyle }

View file

@ -12,4 +12,4 @@ const isInRange = (date, dateFrom, dateTo) => {
return true
}
export {isInRange}
export { isInRange }

View file

@ -20,4 +20,4 @@ const appendRequestQueryFilters = (data, route) => {
return data
}
export {queryFilters, appendRequestQueryFilters}
export { queryFilters, appendRequestQueryFilters }

View file

@ -3,10 +3,10 @@ const defineLimits = () => {
for (let i of [10, 20, 50, 100, 0]) {
let label = i !== 0 ? i : 'Tout'
v.push({value: i, text: label})
v.push({ value: i, text: label })
}
return v
}
export {defineLimits}
export { defineLimits }

View file

@ -11,6 +11,4 @@ const createRequestOptions = (options) => {
return options
}
export {
createRequestOptions
}
export { createRequestOptions }

View file

@ -43,4 +43,4 @@ const removeStorage = function (key) {
localStorage.removeItem(key)
}
export {getStorage, saveStorage, removeStorage}
export { getStorage, saveStorage, removeStorage }

View file

@ -1,8 +1,8 @@
import '../scss/main.scss'
import {createApp} from 'vue'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import {createBootstrap} from 'bootstrap-vue-next'
import { createBootstrap } from 'bootstrap-vue-next'
import {
Chart as ChartJS,
Title,

View file

@ -1,48 +1,129 @@
import {createRouter, createWebHashHistory} from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
name: 'Tableau de bord',
meta: {icon: ['fa-solid', 'fa-chart-line']},
name: 'dashboard',
meta: { label: 'Tableau de bord', icon: ['fa-solid', 'fa-chart-line'] },
component: () => import('../views/DashboardView.vue'),
},
{
path: '/transactions',
name: 'Transactions',
meta: {icon: ['fa-solid', 'fa-money-bill-transfer']},
component: () => import('../views/TransactionsView.vue'),
name: 'transactions',
meta: {
label: 'Transactions',
icon: ['fa-solid', 'fa-money-bill-transfer'],
},
component: () => import('../views/transaction/ListView.vue'),
},
{
path: '/users',
name: 'Utilisateurs',
meta: {icon: ['fa-solid', 'fa-users']},
component: () => import('../views/UsersView.vue'),
component: () => import('../views/ComponentView.vue'),
children: [
{
path: '',
name: 'users',
meta: { label: 'Utilisateurs', icon: ['fa-solid', 'fa-users'] },
component: () => import('../views/user/ListView.vue'),
},
{
path: '/user/edit/:id',
name: 'user_edit',
meta: { label: 'Utilisateur' },
component: () => import('../views/user/EditView.vue'),
},
{
path: '/user/create',
name: 'user_create',
meta: { label: 'Nouvel utilisateur' },
component: () => import('../views/user/CreateView.vue'),
},
],
},
{
path: '/categories',
name: 'Catégories',
meta: {icon: ['fa-solid', 'fa-list']},
component: () => import('../views/CategoriesView.vue'),
component: () => import('../views/ComponentView.vue'),
children: [
{
path: '',
name: 'categories',
meta: { label: 'Catégories', icon: ['fa-solid', 'fa-list'] },
component: () => import('../views/category/ListView.vue'),
},
{
path: '/category/edit/:id',
name: 'category_edit',
meta: { label: 'Catégorie' },
component: () => import('../views/category/EditView.vue'),
},
{
path: '/category/create',
name: 'category_create',
meta: { label: 'Nouvelle catégorie' },
component: () => import('../views/category/CreateView.vue'),
},
],
},
{
path: '/bank_accounts',
name: 'Comptes bancaires',
meta: {icon: ['fa-solid', 'fa-piggy-bank']},
component: () => import('../views/BankAccountsView.vue'),
component: () => import('../views/ComponentView.vue'),
children: [
{
path: '',
name: 'bank_accounts',
meta: {
label: 'Comptes bancaires',
icon: ['fa-solid', 'fa-piggy-bank'],
},
component: () => import('../views/bank_account/ListView.vue'),
},
{
path: '/bank_account/edit/:id',
name: 'bank_account_edit',
meta: { label: 'Compte bancaire' },
component: () => import('../views/bank_account/EditView.vue'),
},
{
path: '/bank_account/create',
name: 'bank_account_create',
meta: { label: 'Nouveau compte bancaire' },
component: () => import('../views/bank_account/CreateView.vue'),
},
],
},
{
path: '/saving_accounts',
name: 'Comptes épargnes',
meta: {icon: ['fa-solid', 'fa-piggy-bank']},
component: () => import('../views/SavingAccountsView.vue'),
component: () => import('../views/ComponentView.vue'),
children: [
{
path: '',
name: 'saving_accounts',
meta: {
label: 'Comptes épargnes',
icon: ['fa-solid', 'fa-piggy-bank'],
},
component: () => import('../views/saving_account/ListView.vue'),
},
{
path: '/saving_account/edit/:id',
name: 'saving_account_edit',
meta: { label: 'Compte bancaire' },
component: () => import('../views/saving_account/EditView.vue'),
},
{
path: '/saving_account/create',
name: 'saving_account_create',
meta: { label: 'Nouveau compte épargne' },
component: () => import('../views/saving_account/CreateView.vue'),
},
],
},
{
path: '/files',
name: 'Fichiers',
meta: {icon: ['fa-solid', 'fa-table']},
name: 'files',
meta: { label: 'Fichiers', icon: ['fa-solid', 'fa-table'] },
component: () => import('../views/FilesView.vue'),
},
],

View file

@ -1,319 +0,0 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header title="Comptes bancaires">
<template #menu>
<BButtonToolbar key-nav>
<BButton
variant="primary"
@click="doAdd"
>Ajouter</BButton
>
</BButtonToolbar>
</template>
<template #bottom>
<Pager
v-model:page="page"
v-model:pages="pages"
v-model:limit="limit"
@update="refresh()"
/>
</template>
</Header>
<div class="crud-list">
<BTableSimple
v-if="data !== null"
caption-top
>
<BThead>
<BTr>
<BTh
v-for="(field, key) in fields"
:key="key"
:width="field.width"
class="cursor"
:class="field.classes"
valign="top"
@click="doSort(field.key)"
>
<SortButton
:current-order="order"
:current-sort="sort"
:order="field.orderKey ?? field.key"
:label="field.label ?? field.renderLabel(data.rows)"
/>
</BTh>
</BTr>
</BThead>
<BTbody>
<BTr
v-for="row in data.rows"
:key="row.id"
>
<BTd
v-for="(field, key) in fields"
:key="key"
class="cursor"
@click="doEdit(row)"
>
<span v-if="field.key">
<span
v-if="field.render"
v-html="field.render(row)"
></span>
<span
v-else
v-text="row[field.key]"
></span>
</span>
<span
v-else
v-html="field.render(row)"
></span>
</BTd>
</BTr>
</BTbody>
</BTableSimple>
</div>
<BModal
v-if="form !== null"
v-model="formShow"
:title="form?.label"
footer-class="justify-content-between"
@ok="doSave"
>
<BAlert
:model-value="form.error !== null"
variant="danger"
v-text="form.error"
></BAlert>
<BForm @submit="doSave">
<BFormGroup
v-for="(field, key) in form.fields"
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"
:description="field.description"
>
<BFormInput
v-if="(field.widget ?? 'text') === 'text'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:type="field.type"
:required="field.required"
/>
</BFormGroup>
</BForm>
<template #footer>
<div>
<BButton
v-if="form.data.id"
variant="danger"
@click="doDelete"
>Supprimer</BButton
>
</div>
<div>
<BButton
variant="secondary"
class="me-2"
@click="formShow = false"
>Annuler</BButton
>
<BButton
variant="primary"
@click="doSave"
>OK</BButton
>
</div>
</template>
</BModal>
</BContainer>
</template>
<script setup>
import {
BTbody,
BThead,
BTr,
BTd,
BTh,
BContainer,
BTableSimple,
BModal,
BButton,
BForm,
BFormGroup,
BFormInput,
BAlert,
BButtonToolbar,
} from 'bootstrap-vue-next'
import SortButton from './../components/SortButton.vue'
import Header from './../components/crud/Header.vue'
import Pager from './../components/crud/Pager.vue'
import {ref, onMounted, watch} from 'vue'
import {getStorage, saveStorage} from '../lib/storage'
import {createRequestOptions} from '../lib/request'
const endpoint = `/api/bank_account`
const order = ref(getStorage(`${endpoint}:order`))
const sort = ref(getStorage(`${endpoint}:sort`))
const page = ref(getStorage(`${endpoint}:page`))
const limit = ref(getStorage(`${endpoint}:limit`))
const data = ref(null)
const pages = ref(null)
const form = ref(null)
const formShow = ref(false)
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const refresh = () => {
fetch(
`${endpoint}?${new URLSearchParams({
order: order.value,
sort: sort.value,
page: page.value,
limit: limit.value,
})}`,
createRequestOptions()
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value
order.value = value.order
sort.value = value.sort
page.value = value.page
pages.value = value.total_pages
limit.value = value.limit
})
}
const doEdit = (item) => {
const data = {...item}
form.value = {
action: `${endpoint}/${item.id}`,
method: 'POST',
data: data,
label: data.label,
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
],
}
formShow.value = true
}
const doAdd = () => {
const data = {label: null}
form.value = {
action: `${endpoint}`,
method: 'POST',
data: data,
label: 'Nouveau',
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
],
}
formShow.value = true
}
const doDelete = () => {
if (!confirm('Je confirme la suppression')) {
return
}
fetch(`${endpoint}/${form.value.data.id}`, createRequestOptions({
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})).then(() => {
formShow.value = false
refresh()
})
}
const doSave = (e) => {
e.preventDefault()
const url = form.value.data.id
? `${endpoint}/${form.value.data.id}`
: endpoint
fetch(url, createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}))
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
form.value = null
formShow.value = false
refresh()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const fields = [
{
key: 'label',
label: 'Libellé',
},
]
onMounted(() => {
refresh()
})
</script>

View file

@ -1,648 +0,0 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header title="Catégories">
<template #menu>
<BButtonToolbar key-nav>
<BButton
variant="secondary"
class="me-2"
@click="doApply"
>
<VueSpinner
v-if="applyInProgress"
size="20"
color="white"
/>
<span v-else>Appliquer&nbsp;les&nbsp;règles</span>
</BButton>
<BButton
variant="primary"
@click="doAdd"
>Ajouter</BButton
>
</BButtonToolbar>
</template>
<template #bottom>
<Pager
v-model:page="page"
v-model:pages="pages"
v-model:limit="limit"
@update="refresh()"
/>
</template>
</Header>
<div class="crud-list">
<BTableSimple
v-if="data !== null"
caption-top
>
<BThead>
<BTr>
<BTh
v-for="field in fields"
:key="field.key"
:width="field.width"
class="cursor"
:class="field.thClasses"
valign="top"
@click="doSort(field.key)"
>
<SortButton
:current-order="order"
:current-sort="sort"
:order="field.orderKey ?? field.key"
:label="field.label ?? field.renderLabel(data.rows)"
/>
</BTh>
</BTr>
</BThead>
<BTbody>
<BTr
v-for="(row, key) in data.rows"
:key="key"
>
<BTd
v-for="field in fields"
:key="field.key"
class="cursor"
:class="field.tdClasses"
@click="doEdit(row)"
>
<span v-if="field.key">
<span
v-if="field.render"
v-html="field.render(row)"
></span>
<span
v-else
v-text="row[field.key]"
></span>
</span>
<span
v-else
v-html="field.render(row)"
></span>
</BTd>
</BTr>
</BTbody>
</BTableSimple>
</div>
<BModal
v-if="form !== null"
ref="modal"
v-model="formShow"
scrollable
class="modal-xl"
:title="form?.label"
footer-class="justify-content-between"
@ok="doSave"
>
<BAlert
:model-value="form.error !== null"
variant="danger"
v-text="form.error"
></BAlert>
<BForm @submit="doSave">
<BFormGroup
v-for="(field, key) in form.fields"
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"
:description="field.description"
>
<BFormInput
v-if="(field.widget ?? 'text') === 'text'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:type="field.type"
:required="field.required"
/>
<BFormSelect
v-if="(field.widget ?? 'text') === 'select'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:options="field.options"
:required="field.required"
/>
<div v-if="field.widget === 'rules'">
<div
v-if="
form.data[field.key] !== null && form.data[field.key].length > 0
"
class="list-group"
>
<div
v-for="(rule, key) in form.data[field.key]"
:key="key"
class="list-group-item"
>
<div class="d-block d-lg-flex justify-content-between gap-1">
<BFormInput
v-if="form.data[field.key][key].id !== null"
v-model="form.data[field.key][key].id"
type="hidden"
/>
<BFormGroup
class="mb-2"
label="Libellé contient"
:label-for="'form-rule-contain-' + key"
>
<BFormInput
:id="'form-rule-contain-' + key"
v-model="form.data[field.key][key].contain"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Regex libellé"
:label-for="'form-rule-match-' + key"
>
<BFormInput
:id="'form-rule-match-' + key"
v-model="form.data[field.key][key].match"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Catégorie banque"
:label-for="'form-rule-contain-' + key"
>
<BFormInput
:id="'form-rule-bank-category-contain-' + key"
v-model="form.data[field.key][key].bank_category"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Montant"
:label-for="'form-rule-amount-' + key"
>
<BFormInput
:id="'form-rule-amount-' + key"
v-model="form.data[field.key][key].amount"
type="number"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Du"
:label-for="'form-rule-datefrom-' + key"
>
<BFormInput
:id="'form-rule-datefrom-' + key"
v-model="form.data[field.key][key].date_from"
type="date"
/>
</BFormGroup>
<BFormGroup
class="mb-2"
label="Au"
:label-for="'form-rule-dateto-' + key"
>
<BFormInput
:id="'form-rule-dateto-' + key"
v-model="form.data[field.key][key].date_to"
type="date"
/>
</BFormGroup>
<BButton
variant="none"
@click="
form.data[field.key] = doRemoveRule(
form.data[field.key],
key,
)
"
>
<i class="fa fa-trash"></i>
</BButton>
</div>
</div>
</div>
</div>
</BFormGroup>
</BForm>
<template #footer>
<BButton
variant="secondary"
@click="form.data['rules'] = doAddRule(form.data['rules'])"
>Ajouter règle</BButton
>
<div>
<BButton
v-if="form.data.id"
variant="danger"
@click="doDelete"
>Supprimer</BButton
>
</div>
<div>
<BButton
variant="secondary"
class="me-2"
@click="formShow = false"
>Annuler</BButton
>
<BButton
variant="primary"
@click="doSave"
>OK</BButton
>
</div>
</template>
</BModal>
</BContainer>
</template>
<script setup>
import {
BTbody,
BThead,
BTr,
BTd,
BTh,
BContainer,
BTableSimple,
BModal,
BButton,
BForm,
BFormGroup,
BFormInput,
BAlert,
BButtonToolbar,
BFormSelect,
} from 'bootstrap-vue-next'
import {VueSpinner} from 'vue3-spinners'
import SortButton from './../components/SortButton.vue'
import Header from './../components/crud/Header.vue'
import Pager from './../components/crud/Pager.vue'
import {ref, onMounted, watch, useTemplateRef} from 'vue'
import {getStorage, saveStorage} from '../lib/storage'
import {renderCategory, renderEuro, renderLabelWithSum} from '../lib/renderers'
import {createRequestOptions} from '../lib/request'
const endpoint = `/api/category`
const order = ref(getStorage(`${endpoint}:order`))
const sort = ref(getStorage(`${endpoint}:sort`))
const page = ref(getStorage(`${endpoint}:page`))
const limit = ref(getStorage(`${endpoint}:limit`))
const data = ref(null)
const pages = ref(null)
const form = ref(null)
const formShow = ref(false)
const applyInProgress = ref(false)
const modal = useTemplateRef('modal')
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const doRemoveRule = (item, key) => {
let values = []
item.forEach((v, k) => {
if (k !== key) {
values.push(v)
}
})
return values
}
const doAddRule = (item) => {
const rule = {
contain: null,
match: null,
bank_category: null,
amount: null,
date_from: null,
date_to: null,
}
if (item == null) {
item = []
}
item.push(rule)
window.setTimeout(() => {
const modalBody = document.querySelector('.modal-body')
modalBody.scrollTo({top: modalBody.scrollHeight, behavior: 'smooth'})
}, 300)
return item
}
const doApply = () => {
applyInProgress.value = true
fetch('/api/transactions/update_categories', createRequestOptions({method: 'POST'})).then(() => {
applyInProgress.value = false
})
}
const refresh = () => {
fetch(
`${endpoint}?${new URLSearchParams({
order: order.value,
sort: sort.value,
page: page.value,
limit: limit.value,
})}`,
createRequestOptions()
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value
order.value = value.order
sort.value = value.sort
page.value = value.page
pages.value = value.total_pages
limit.value = value.limit
})
}
const toISODateString = (v, h, m, s) => {
const d = new Date(v)
d.setUTCHours(h)
d.setUTCMinutes(m)
d.setUTCSeconds(s)
return d.toISOString()
}
const fromISODateString = (v) => {
return v.split('T', 1)[0]
}
const doEdit = (item) => {
const data = {...item}
if (data.rules !== null) {
data.rules.forEach((value, key) => {
if (value.date_from) {
data.rules[key].date_from = fromISODateString(value.date_from)
}
if (value.date_to) {
data.rules[key].date_to = fromISODateString(value.date_to)
}
})
}
data.ignore_transactions = data.ignore_transactions ? 1 : 0
form.value = {
action: `${endpoint}/${item.id}`,
method: 'POST',
data: data,
label: data.label,
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
{
label: 'Seuil mensuel',
type: 'number',
required: false,
key: 'month_threshold',
},
{
label: 'Ignorer les transactions liées',
widget: 'select',
required: false,
options: [
{value: 0, text: 'Non'},
{value: 1, text: 'Oui'},
],
key: 'ignore_transactions',
},
{
label: 'Couleur',
type: 'color',
required: true,
key: 'color',
},
{
label: null,
widget: 'rules',
required: false,
key: 'rules',
},
],
}
formShow.value = true
}
const doAdd = () => {
const data = {
id: null,
label: null,
color: '#9eb1e7',
rules: [],
month_threshold: null,
ignore_transactions: 0,
}
form.value = {
action: `${endpoint}`,
method: 'POST',
data: data,
label: 'Nouveau',
error: null,
fields: [
{
label: null,
type: 'hidden',
required: false,
key: 'id',
},
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
{
label: 'Seuil mensuel',
type: 'number',
required: false,
key: 'month_threshold',
},
{
label: 'Ignorer les transactions liées',
widget: 'select',
required: false,
options: [
{value: 0, text: 'Non'},
{value: 1, text: 'Oui'},
],
key: 'ignore_transactions',
},
{
label: 'Couleur',
type: 'color',
required: true,
key: 'color',
},
{
label: null,
widget: 'rules',
required: true,
key: 'rules',
},
],
}
formShow.value = true
}
const doDelete = () => {
if (!confirm('Je confirme la suppression')) {
return
}
fetch(`${endpoint}/${form.value.data.id}`, createRequestOptions({
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})).then(() => {
formShow.value = false
refresh()
})
}
const doSave = (e) => {
e.preventDefault()
const url = form.value.data.id
? `${endpoint}/${form.value.data.id}`
: endpoint
if (form.value.data.rules === null) {
form.value.data.rules = []
}
if (
form.value.data.month_threshold !== null &&
form.value.data.month_threshold !== ''
) {
form.value.data.month_threshold = parseFloat(
form.value.data.month_threshold,
)
} else {
form.value.data.month_threshold = null
}
form.value.data.ignore_transactions =
form.value.data.ignore_transactions === 1
form.value.data.rules.forEach((value, key) => {
if (value.amount !== null && value.amount !== '') {
form.value.data.rules[key].amount = parseFloat(value.amount)
}
if (value.date_from) {
form.value.data.rules[key].date_from = toISODateString(
value.date_from,
0,
0,
0,
)
}
if (value.date_to) {
form.value.data.rules[key].date_to = toISODateString(
value.date_to,
23,
59,
59,
)
}
for (let i in value) {
if (value[i] === '') {
form.value.data.rules[key][i] = null
}
}
})
fetch(url, createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}))
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
form.value = null
formShow.value = false
refresh()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const fields = [
{
key: 'label',
label: 'Libellé',
render: (item) => renderCategory(item),
},
{
key: 'month_threshold',
renderLabel: (rows) =>
renderLabelWithSum('Seuil mensuel', rows, 'month_threshold'),
width: '150px',
thClasses: ['text-end'],
tdClasses: ['text-end'],
render: (item) => renderEuro(item.month_threshold),
},
]
onMounted(() => {
refresh()
})
</script>

View file

@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View file

@ -49,14 +49,19 @@
>
<BButton
v-for="i in [
{size: 4, label: '4'},
{size: 6, label: '6'},
{size: 8, label: '8'},
{size: 10, label: '10'},
{size: 12, label: '12'},
{ size: 4, label: '4' },
{ size: 6, label: '6' },
{ size: 8, label: '8' },
{ size: 10, label: '10' },
{ size: 12, label: '12' },
]"
:variant="i.size == config[key].size ? 'primary' : 'secondary'"
@click="config[key].size = i.size; updateConfig()"
:variant="
i.size == config[key].size ? 'primary' : 'secondary'
"
@click="
config[key].size = i.size
updateConfig()
"
>{{ i.label }}</BButton
>
</BButtonGroup>
@ -101,7 +106,10 @@
</transition-group>
</Draggable>
</div>
<div v-else class="text-center p-5">
<div
v-else
class="text-center p-5"
>
<p>Chargement...</p>
<BSpinner />
</div>
@ -109,10 +117,10 @@
</template>
<script setup>
import {ref, onMounted, watch} from 'vue'
import {compute as monthThresholds} from '../chart/monthThreshold'
import {getStorage, saveStorage} from '../lib/storage'
import {BButtonGroup, BButton, BSpinner} from 'bootstrap-vue-next'
import { ref, onMounted, watch } from 'vue'
import { compute as monthThresholds } from '../chart/monthThreshold'
import { getStorage, saveStorage } from '../lib/storage'
import { BButtonGroup, BButton, BSpinner } 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'
@ -120,8 +128,8 @@ import Distribution from './../components/dashboard/Distribution.vue'
import MonthThresholds from './../components/dashboard/MonthThresholds.vue'
import DiffCreditDebit from './../components/dashboard/DiffCreditDebit.vue'
import CategoriesStats from './../components/dashboard/CategoriesStats.vue'
import {VueDraggableNext as Draggable} from 'vue-draggable-next'
import {createRequestOptions} from '../lib/request'
import { VueDraggableNext as Draggable } from 'vue-draggable-next'
import { createRequestOptions } from '../lib/request'
const data = ref(null)
const isLoading = ref(true)
@ -132,12 +140,12 @@ const savingAccounts = ref([])
const mode = ref('view')
const defaultComponents = [
{component: 'Capital', size: 8},
{component: 'SavingAccounts', size: 4},
{component: 'Distribution', size: 12},
{component: 'DiffCreditDebit', size: 12},
{component: 'CategoriesStats', size: 12},
{component: 'MonthThresholds', size: 12},
{ component: 'Capital', size: 8 },
{ component: 'SavingAccounts', size: 4 },
{ component: 'Distribution', size: 12 },
{ component: 'DiffCreditDebit', size: 12 },
{ component: 'CategoriesStats', size: 12 },
{ component: 'MonthThresholds', size: 12 },
]
const account = ref(getStorage(`dashboard:account`))
@ -211,7 +219,10 @@ const refresh = () => {
query['bank_account_id__eq'] = account.value
}
fetch(`/api/transaction?${new URLSearchParams(query)}`, createRequestOptions())
fetch(
`/api/transaction?${new URLSearchParams(query)}`,
createRequestOptions(),
)
.then((response) => response.json())
.then((value) => {
data.value = value.rows.filter(
@ -226,11 +237,18 @@ onMounted(() => {
refresh()
fetch('/api/category?order=label&sort=asc&ignore_transactions__eq=0', createRequestOptions())
fetch(
'/api/category?order=label&sort=asc&ignore_transactions__eq=0',
createRequestOptions(),
)
.then((response) => response.json())
.then((data) => {
categories.value = data.rows
categories.value.push({id: -1, label: 'Sans catégorie', color: '#cccccc'})
categories.value.push({
id: -1,
label: 'Sans catégorie',
color: '#cccccc',
})
})
fetch('/api/bank_account', createRequestOptions())
@ -246,7 +264,9 @@ onMounted(() => {
})
defaultComponents.forEach((defaultComponent) => {
const exists = config.value.filter((c) => defaultComponent.component === c.component).length !== 0
const exists =
config.value.filter((c) => defaultComponent.component === c.component)
.length !== 0
if (!exists) {
config.value.push(defaultComponent)
@ -254,7 +274,12 @@ onMounted(() => {
})
config.value = config.value.filter((configItem) => {
return defaultComponents.filter((defaultComponent) => configItem.component === defaultComponent.component).length > 0
return (
defaultComponents.filter(
(defaultComponent) =>
configItem.component === defaultComponent.component,
).length > 0
)
})
})
</script>

View file

@ -175,7 +175,7 @@
</template>
<script setup>
import {ref, onMounted} from 'vue'
import { ref, onMounted } from 'vue'
import Header from './../components/crud/Header.vue'
import {
BModal,
@ -189,7 +189,7 @@ import {
BDropdown,
BDropdownItem,
} from 'bootstrap-vue-next'
import {createRequestOptions} from '../lib/request'
import { createRequestOptions } from '../lib/request'
let formFile = false
const tree = ref(null)
@ -222,7 +222,7 @@ const isEditable = (item) => {
}
const doAddFile = () => {
const data = {file: null}
const data = { file: null }
formFile = true
form.value = {
@ -246,7 +246,7 @@ const doAddFile = () => {
}
const doAddDirectory = () => {
const data = {directory: null, path: tree.value.id}
const data = { directory: null, path: tree.value.id }
formFile = false
form.value = {
@ -273,10 +273,13 @@ const doDelete = (item) => {
return
}
fetch(`/api/filemanager/file/${item.id}`, createRequestOptions({
method: 'DELETE',
})).then(() => {
refresh({current: tree.value.id})
fetch(
`/api/filemanager/file/${item.id}`,
createRequestOptions({
method: 'DELETE',
}),
).then(() => {
refresh({ current: tree.value.id })
})
}
@ -288,13 +291,16 @@ const doSave = (e) => {
payload.append('file', form.value.data.file)
payload.append('path', tree.value.id)
fetch(`/api/filemanager/file`, createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
},
body: payload,
}))
fetch(
`/api/filemanager/file`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
},
body: payload,
}),
)
.then((response) => {
return response.json()
})
@ -304,21 +310,24 @@ const doSave = (e) => {
} else {
form.value = null
formShow.value = false
refresh({current: tree.value.id})
refresh({ current: tree.value.id })
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
} else {
fetch(`/api/filemanager/directory`, createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}))
fetch(
`/api/filemanager/directory`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
@ -328,7 +337,7 @@ const doSave = (e) => {
} else {
form.value = null
formShow.value = false
refresh({current: tree.value.id})
refresh({ current: tree.value.id })
}
})
.catch((err) => {

View file

@ -1,369 +0,0 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header title="Comptes épargnes">
<template #menu>
<BButtonToolbar key-nav>
<BButton
variant="primary"
@click="doAdd"
>Ajouter</BButton
>
</BButtonToolbar>
</template>
<template #bottom>
<Pager
v-model:page="page"
v-model:pages="pages"
v-model:limit="limit"
@update="refresh()"
/>
</template>
</Header>
<div class="crud-list">
<BTableSimple
v-if="data !== null"
caption-top
>
<BThead>
<BTr>
<BTh
v-for="field in fields"
:key="field.key"
:width="field.width"
class="cursor"
:class="field.thClasses"
valign="top"
@click="doSort(field.key)"
>
<SortButton
:current-order="order"
:current-sort="sort"
:order="field.orderKey ?? field.key"
:label="field.label ?? field.renderLabel(data.rows)"
/>
</BTh>
</BTr>
</BThead>
<BTbody>
<BTr
v-for="(row, key) in data.rows"
:key="key"
>
<BTd
v-for="field in fields"
:key="field.key"
class="cursor"
:class="field.tdClasses"
@click="doEdit(row)"
>
<span v-if="field.key">
<span
v-if="field.render"
v-html="field.render(row)"
></span>
<span
v-else
v-text="row[field.key]"
></span>
</span>
<span
v-else
v-html="field.render(row)"
></span>
</BTd>
</BTr>
</BTbody>
</BTableSimple>
</div>
<BModal
v-if="form !== null"
v-model="formShow"
:title="form?.label"
footer-class="justify-content-between"
@ok="doSave"
>
<BAlert
:model-value="form.error !== null"
variant="danger"
v-text="form.error"
></BAlert>
<BForm @submit="doSave">
<BFormGroup
v-for="(field, key) in form.fields"
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"
:description="field.description"
>
<BFormInput
v-if="(field.widget ?? 'text') === 'text'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:type="field.type"
:required="field.required"
/>
</BFormGroup>
</BForm>
<template #footer>
<div>
<BButton
v-if="form.data.id"
variant="danger"
@click="doDelete"
>Supprimer</BButton
>
</div>
<div>
<BButton
variant="secondary"
class="me-2"
@click="formShow = false"
>Annuler</BButton
>
<BButton
variant="primary"
@click="doSave"
>OK</BButton
>
</div>
</template>
</BModal>
</BContainer>
</template>
<script setup>
import {
BTbody,
BThead,
BTr,
BTd,
BTh,
BContainer,
BTableSimple,
BModal,
BButton,
BForm,
BFormGroup,
BFormInput,
BAlert,
BButtonToolbar,
} from 'bootstrap-vue-next'
import SortButton from './../components/SortButton.vue'
import Header from './../components/crud/Header.vue'
import Pager from './../components/crud/Pager.vue'
import {ref, onMounted, watch} from 'vue'
import {getStorage, saveStorage} from '../lib/storage'
import {renderDate, renderEuro, renderLabelWithSum} from '../lib/renderers'
import {createRequestOptions} from '../lib/request'
const endpoint = `/api/saving_account`
const order = ref(getStorage(`${endpoint}:order`))
const sort = ref(getStorage(`${endpoint}:sort`))
const page = ref(getStorage(`${endpoint}:page`))
const limit = ref(getStorage(`${endpoint}:limit`))
const data = ref(null)
const pages = ref(null)
const form = ref(null)
const formShow = ref(false)
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const refresh = () => {
fetch(
`${endpoint}?${new URLSearchParams({
order: order.value,
sort: sort.value,
page: page.value,
limit: limit.value,
})}`,
createRequestOptions()
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value
order.value = value.order
sort.value = value.sort
page.value = value.page
pages.value = value.total_pages
limit.value = value.limit
})
}
const doEdit = (item) => {
const data = {...item}
form.value = {
action: `${endpoint}/${item.id}`,
method: 'POST',
data: data,
label: data.label,
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
{
label: 'Montant bloqué',
type: 'number',
key: 'blocked_amount',
},
{
label: 'Montant débloqué',
type: 'number',
key: 'released_amount',
},
],
}
formShow.value = true
}
const doAdd = () => {
const data = {label: null, released_amount: 0, blocked_amount: 0}
form.value = {
action: `${endpoint}`,
method: 'POST',
data: data,
label: 'Nouveau',
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
{
label: 'Montant bloqué',
type: 'number',
key: 'blocked_amount',
},
{
label: 'Montant débloqué',
type: 'number',
key: 'released_amount',
},
],
}
formShow.value = true
}
const doDelete = () => {
if (!confirm('Je confirme la suppression')) {
return
}
fetch(`${endpoint}/${form.value.data.id}`, createRequestOptions({
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})).then(() => {
formShow.value = false
refresh()
})
}
const doSave = (e) => {
e.preventDefault()
const url = form.value.data.id
? `${endpoint}/${form.value.data.id}`
: endpoint
form.value.data.released_amount = parseFloat(form.value.data.released_amount)
form.value.data.blocked_amount = parseFloat(form.value.data.blocked_amount)
fetch(url, createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}))
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
form.value = null
formShow.value = false
refresh()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const fields = [
{
key: 'label',
label: 'Libellé',
},
{
key: 'blocked_amount',
renderLabel: (rows) => renderLabelWithSum('Bloqué', rows, 'blocked_amount'),
render: (item) => renderEuro(item.blocked_amount),
width: '170px',
thClasses: ['text-end'],
tdClasses: ['text-end'],
},
{
key: 'released_amount',
renderLabel: (rows) =>
renderLabelWithSum('Débloqué', rows, 'released_amount'),
render: (item) => renderEuro(item.released_amount),
width: '170px',
thClasses: ['text-end'],
tdClasses: ['text-end'],
},
{
key: 'updated_at',
label: 'Mise à jour',
render: (item) => renderDate(item.updated_at),
width: '170px',
thClasses: ['text-end'],
tdClasses: ['text-end'],
},
]
onMounted(() => {
refresh()
})
</script>

View file

@ -1,352 +0,0 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header title="Utilisateurs">
<template #menu>
<BButtonToolbar key-nav>
<BButton
variant="primary"
@click="doAdd"
>Ajouter</BButton
>
</BButtonToolbar>
</template>
<template #bottom>
<Pager
v-model:page="page"
v-model:pages="pages"
v-model:limit="limit"
@update="refresh()"
/>
</template>
</Header>
<div class="crud-list">
<BTableSimple
v-if="data !== null"
caption-top
>
<BThead>
<BTr>
<BTh
v-for="field in fields"
:key="field.key"
:width="field.width"
class="cursor"
:class="field.classes"
@click="doSort(field.key)"
>
<SortButton
:current-order="order"
:current-sort="sort"
:order="field.key"
:label="field.label"
/>
</BTh>
</BTr>
</BThead>
<BTbody>
<BTr
v-for="(row, key) in data.rows"
:key="key"
>
<BTd
v-for="field in fields"
:key="field.key"
class="cursor"
@click="doEdit(row)"
>
<span v-if="field.key">
<span
v-if="field.render"
v-html="field.render(row)"
></span>
<span
v-else
v-text="row[field.key]"
></span>
</span>
<span
v-else
v-html="field.render(row)"
></span>
</BTd>
</BTr>
</BTbody>
</BTableSimple>
</div>
<BModal
v-if="form !== null"
v-model="formShow"
:title="form?.label"
@ok="doSave"
>
<BAlert
:model-value="form.error !== null"
variant="danger"
v-text="form.error"
></BAlert>
<BForm @submit="doSave">
<BFormGroup
v-for="(field, key) in form.fields"
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"
:description="field.description"
>
<BFormInput
v-if="(field.widget ?? 'text') === 'text'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:type="field.type"
:required="field.required"
/>
</BFormGroup>
</BForm>
<template #footer>
<div>
<BButton
v-if="form.data.id"
variant="danger"
@click="doDelete"
>Supprimer</BButton
>
</div>
<div>
<BButton
variant="secondary"
class="me-2"
@click="formShow = false"
>Annuler</BButton
>
<BButton
variant="primary"
@click="doSave"
>OK</BButton
>
</div>
</template>
</BModal>
</BContainer>
</template>
<script setup>
import {
BTbody,
BThead,
BTr,
BTd,
BTh,
BContainer,
BTableSimple,
BModal,
BButton,
BForm,
BFormGroup,
BFormInput,
BAlert,
BButtonToolbar,
} from 'bootstrap-vue-next'
import SortButton from './../components/SortButton.vue'
import Header from './../components/crud/Header.vue'
import Pager from './../components/crud/Pager.vue'
import {ref, onMounted, watch} from 'vue'
import {getStorage, saveStorage} from '../lib/storage'
import {createRequestOptions} from '../lib/request'
import {renderDateTime} from '../lib/renderers'
const endpoint = `/api/user`
const order = ref(getStorage(`${endpoint}:order`))
const sort = ref(getStorage(`${endpoint}:sort`))
const page = ref(getStorage(`${endpoint}:page`))
const limit = ref(getStorage(`${endpoint}:limit`))
const data = ref(null)
const pages = ref(null)
const form = ref(null)
const formShow = ref(false)
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const refresh = () => {
fetch(
`${endpoint}?${new URLSearchParams({
order: order.value,
sort: sort.value,
page: page.value,
limit: limit.value,
})}`,
createRequestOptions()
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value
order.value = value.order
sort.value = value.sort
page.value = value.page
pages.value = value.total_pages
limit.value = value.limit
})
}
const doEdit = (item) => {
const data = {...item}
form.value = {
action: `${endpoint}/${item.id}`,
method: 'POST',
data: data,
label: data.display_name,
error: null,
fields: [
{
label: 'Nom',
type: 'text',
required: true,
key: 'display_name',
},
{
label: "Nom d'utilisateur",
type: 'text',
required: true,
key: 'username',
},
{
label: 'Mot de passe',
type: 'password',
required: false,
key: 'password',
},
],
}
formShow.value = true
}
const doAdd = () => {
const data = {display_name: null, username: null, password: null}
form.value = {
action: `${endpoint}`,
method: 'POST',
data: data,
label: 'Nouveau',
error: null,
fields: [
{
label: 'Nom',
type: 'text',
required: true,
key: 'display_name',
},
{
label: "Nom d'utilisateur",
type: 'text',
required: true,
key: 'username',
},
{
label: 'Mot de passe',
type: 'password',
required: false,
key: 'password',
},
],
}
formShow.value = true
}
const doDelete = () => {
if (!confirm('Je confirme la suppression')) {
return
}
fetch(`${endpoint}/${form.value.data.id}`, createRequestOptions({
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})).then(() => {
formShow.value = false
refresh()
})
}
const doSave = (e) => {
e.preventDefault()
const url = form.value.data.id
? `${endpoint}/${form.value.data.id}`
: endpoint
fetch(url, createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}))
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
form.value = null
formShow.value = false
refresh()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const fields = [
{
key: 'display_name',
label: 'Nom',
width: '30%',
},
{
key: 'username',
label: 'Utilisateur',
},
{
key: 'logged_at',
label: 'Dernière connexion',
render: (item) => renderDateTime(item.logged_at),
},
]
onMounted(() => {
refresh()
})
</script>

View file

@ -0,0 +1,128 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header :title="$route.meta.label">
<template #menu>
<div>
<BButton
variant="secondary"
@click="doBack"
>Retour</BButton
>
<BButton
variant="primary"
@click="doSave"
>Enregistrer</BButton
>
</div>
</template>
<template #bottom> </template>
</Header>
<BForm
v-if="form !== null"
class="p-3"
@submit="submit"
>
<BAlert
v-if="form?.error"
dismissible
variant="warning"
:model-value="true"
>
{{ form.error }}
</BAlert>
<BRow>
<BCol>
<FormView :form="form" />
</BCol>
</BRow>
</BForm>
</BContainer>
</template>
<script setup>
import {
BContainer,
BButton,
BForm,
BRow,
BCol,
BAlert,
} from 'bootstrap-vue-next'
import Header from './../../components/crud/Header.vue'
import FormView from './../../components/crud/FormView.vue'
import { ref, onMounted } from 'vue'
import { createRequestOptions } from '../../lib/request'
import { useRouter } from 'vue-router'
const router = useRouter()
const parentRouteName = 'bank_accounts'
const endpoint = `/api/bank_account`
const form = ref(null)
const createForm = () => {
const data = {
id: null,
label: null,
color: '#9eb1e7',
rules: [],
month_threshold: null,
ignore_transactions: 0,
}
return {
action: `${endpoint}`,
method: 'POST',
data: data,
label: 'Nouveau',
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
],
}
}
const doSave = (e) => {
fetch(
`${endpoint}`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
doBack()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doBack = () => {
router.replace({ name: parentRouteName })
}
onMounted(() => {
form.value = createForm()
})
</script>

View file

@ -0,0 +1,174 @@
<template>
<div
v-if="data === null"
class="text-center p-5"
>
<p>Chargement...</p>
<BSpinner />
</div>
<BContainer
v-else
fluid
class="p-0"
>
<Header :title="data.label">
<template #menu>
<div>
<BButton
variant="secondary"
@click="doBack"
>Retour</BButton
>
<BButton
variant="primary"
@click="doSave"
>Enregistrer</BButton
>
<BButton
variant="danger"
@click="doDelete"
>Supprimer</BButton
>
</div>
</template>
<template #bottom> </template>
</Header>
<BForm
v-if="form !== null"
class="p-3"
>
<BAlert
v-if="form?.error"
dismissible
variant="warning"
:model-value="true"
>
{{ form.error }}
</BAlert>
<BRow>
<BCol>
<FormView :form="form" />
</BCol>
</BRow>
</BForm>
</BContainer>
</template>
<script setup>
import {
BContainer,
BButton,
BForm,
BRow,
BCol,
BAlert,
} from 'bootstrap-vue-next'
import Header from './../../components/crud/Header.vue'
import FormView from './../../components/crud/FormView.vue'
import { ref, onMounted } from 'vue'
import { createRequestOptions } from '../../lib/request'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const parentRouteName = 'bank_accounts'
const endpoint = `/api/bank_account`
const data = ref(null)
const form = ref(null)
const formShow = ref(false)
const id = route.params.id
const loadData = () => {
fetch(
`${endpoint}?${new URLSearchParams({
id__eq: id,
})}`,
createRequestOptions(),
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value.rows[0]
form.value = createForm(value.rows[0])
})
}
const createForm = (item) => {
const data = { ...item }
return {
action: `${endpoint}/${item.id}`,
method: 'POST',
data: data,
label: data.label,
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
],
}
}
const doDelete = () => {
if (!confirm('Je confirme la suppression')) {
return
}
fetch(
`${endpoint}/${id}`,
createRequestOptions({
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}),
).then(() => {
doBack()
})
}
const doSave = (e) => {
e.preventDefault()
fetch(
`${endpoint}/${id}`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
form.value = null
formShow.value = false
doBack()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doBack = () => {
router.replace({ name: parentRouteName })
}
onMounted(loadData)
</script>

View file

@ -0,0 +1,128 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header :title="$route.meta.label">
<template #menu>
<BButtonToolbar key-nav>
<BButton
variant="primary"
@click="doCreate"
>Ajouter</BButton
>
</BButtonToolbar>
</template>
<template #bottom>
<Pager
v-model:page="page"
v-model:pages="pages"
v-model:limit="limit"
@update="refresh()"
/>
</template>
</Header>
<div
v-if="data"
class="crud-list"
>
<DataList
:rows="data.rows"
:headers="fields"
:sort="sort"
:order="order"
@sort="doSort"
@row-click="doEdit"
/>
</div>
</BContainer>
</template>
<script setup>
import { BContainer, BButton, BButtonToolbar } from 'bootstrap-vue-next'
import { VueSpinner } from 'vue3-spinners'
import Header from '../../components/crud/Header.vue'
import Pager from '../../components/crud/Pager.vue'
import DataList from '../../components/crud/DataList.vue'
import { ref, onMounted, watch } from 'vue'
import { getStorage, saveStorage } from '../../lib/storage'
import {
renderCategory,
renderEuro,
renderLabelWithSum,
} from '../../lib/renderers'
import { createRequestOptions } from '../../lib/request'
import { useRouter } from 'vue-router'
const endpoint = `/api/bank_account`
const order = ref(getStorage(`${endpoint}:order`))
const sort = ref(getStorage(`${endpoint}:sort`))
const page = ref(getStorage(`${endpoint}:page`))
const limit = ref(getStorage(`${endpoint}:limit`))
const data = ref(null)
const pages = ref(null)
const router = useRouter()
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const refresh = () => {
fetch(
`${endpoint}?${new URLSearchParams({
order: order.value,
sort: sort.value,
page: page.value,
limit: limit.value,
})}`,
createRequestOptions(),
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value
order.value = value.order
sort.value = value.sort
page.value = value.page
pages.value = value.total_pages
limit.value = value.limit
})
}
const doEdit = (item) => {
router.replace({ name: 'bank_account_edit', params: { id: item.id } })
}
const doCreate = () => {
router.replace({ name: 'bank_account_create' })
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const fields = [
{
key: 'label',
label: 'Libellé',
},
]
onMounted(() => {
refresh()
})
</script>

View file

@ -0,0 +1,180 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header :title="$route.meta.label">
<template #menu>
<div>
<BButton
variant="secondary"
@click="doBack"
>Retour</BButton
>
<BButton
variant="primary"
@click="doSave"
>Enregistrer</BButton
>
</div>
</template>
<template #bottom> </template>
</Header>
<BForm
v-if="form !== null"
class="p-3"
@submit="submit"
>
<BAlert
v-if="form?.error"
dismissible
variant="warning"
:model-value="true"
>
{{ form.error }}
</BAlert>
<BRow>
<BCol>
<FormView :form="form" />
</BCol>
</BRow>
</BForm>
</BContainer>
</template>
<script setup>
import {
BContainer,
BButton,
BForm,
BRow,
BCol,
BAlert,
} from 'bootstrap-vue-next'
import Header from './../../components/crud/Header.vue'
import FormView from './../../components/crud/FormView.vue'
import { ref, onMounted } from 'vue'
import { createRequestOptions } from '../../lib/request'
import { useRouter } from 'vue-router'
const router = useRouter()
const parentRouteName = 'categories'
const endpoint = `/api/category`
const form = ref(null)
const createForm = () => {
const data = {
id: null,
label: null,
color: '#9eb1e7',
rules: [],
month_threshold: null,
ignore_transactions: 0,
}
return {
action: `${endpoint}`,
method: 'POST',
data: data,
label: 'Nouveau',
error: null,
fields: [
{
label: null,
type: 'hidden',
required: false,
key: 'id',
},
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
{
label: 'Seuil mensuel',
type: 'number',
required: false,
key: 'month_threshold',
},
{
label: 'Ignorer les transactions liées',
widget: 'select',
required: false,
options: [
{ value: 0, text: 'Non' },
{ value: 1, text: 'Oui' },
],
key: 'ignore_transactions',
},
{
label: 'Couleur',
type: 'color',
required: true,
key: 'color',
},
{
label: null,
widget: 'rules',
required: true,
key: 'rules',
},
],
}
}
const doSave = (e) => {
e.preventDefault()
form.value.data.rules = []
if (
form.value.data.month_threshold !== null &&
form.value.data.month_threshold !== ''
) {
form.value.data.month_threshold = parseFloat(
form.value.data.month_threshold,
)
} else {
form.value.data.month_threshold = null
}
form.value.data.ignore_transactions =
form.value.data.ignore_transactions === 1
fetch(
`${endpoint}`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
router.replace({ name: 'category_edit', params: { id: data.id } })
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doBack = () => {
router.replace({ name: parentRouteName })
}
onMounted(() => {
form.value = createForm()
})
</script>

View file

@ -0,0 +1,338 @@
<template>
<div
v-if="data === null"
class="text-center p-5"
>
<p>Chargement...</p>
<BSpinner />
</div>
<BContainer
v-else
fluid
class="p-0"
>
<Header :title="data.label">
<template #menu>
<div>
<BButton
variant="secondary"
@click="doBack"
>Retour</BButton
>
<BButton
variant="secondary"
@click="form.data['rules'] = doAddRule(form.data['rules'])"
>Ajouter règle</BButton
>
<BButton
variant="primary"
@click="doSave"
>Enregistrer</BButton
>
<BButton
variant="danger"
@click="doDelete"
>Supprimer</BButton
>
</div>
</template>
<template #bottom> </template>
</Header>
<BForm
v-if="form !== null"
class="p-3"
>
<BAlert
v-if="form?.error"
dismissible
variant="warning"
:model-value="true"
>
{{ form.error }}
</BAlert>
<BRow>
<BCol
cols="12"
:md="form.data.rules && form.data.rules.length > 0 ? 4 : 12"
>
<FormView
:form="form"
:fields="[
'label',
'month_threshold',
'ignore_transactions',
'color',
]"
/>
</BCol>
<BCol
cols="12"
:md="form.data.rules && form.data.rules.length > 0 ? 8 : 12"
>
<div
class="rounded"
:class="{ border: form.data.rules && form.data.rules.length > 0 }"
>
<FormView
:form="form"
:fields="['rules']"
/>
</div>
</BCol>
</BRow>
</BForm>
</BContainer>
</template>
<script setup>
import {
BContainer,
BButton,
BForm,
BRow,
BCol,
BAlert,
} from 'bootstrap-vue-next'
import Header from './../../components/crud/Header.vue'
import FormView from './../../components/crud/FormView.vue'
import { ref, onMounted } from 'vue'
import { createRequestOptions } from '../../lib/request'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const parentRouteName = 'categories'
const endpoint = `/api/category`
const data = ref(null)
const form = ref(null)
const formShow = ref(false)
const id = route.params.id
const toISODateString = (v, h, m, s) => {
const d = new Date(v)
d.setUTCHours(h)
d.setUTCMinutes(m)
d.setUTCSeconds(s)
return d.toISOString()
}
const fromISODateString = (v) => {
return v.split('T', 1)[0]
}
const loadData = () => {
fetch(
`${endpoint}?${new URLSearchParams({
id__eq: id,
})}`,
createRequestOptions(),
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value.rows[0]
form.value = createForm(value.rows[0])
})
}
const doAddRule = (item) => {
const rule = {
contain: null,
match: null,
bank_category: null,
amount: null,
date_from: null,
date_to: null,
}
if (item == null) {
item = []
}
item.push(rule)
window.setTimeout(() => {
const container = document.querySelector('#rules')
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' })
}, 300)
return item
}
const createForm = (item) => {
const data = { ...item }
if (data.rules !== null) {
data.rules.forEach((value, key) => {
if (value.date_from) {
data.rules[key].date_from = fromISODateString(value.date_from)
}
if (value.date_to) {
data.rules[key].date_to = fromISODateString(value.date_to)
}
})
}
data.ignore_transactions = data.ignore_transactions ? 1 : 0
return {
action: `${endpoint}/${item.id}`,
method: 'POST',
data: data,
label: data.label,
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
{
label: 'Seuil mensuel',
type: 'number',
required: false,
key: 'month_threshold',
},
{
label: 'Ignorer les transactions liées',
widget: 'select',
required: false,
options: [
{ value: 0, text: 'Non' },
{ value: 1, text: 'Oui' },
],
key: 'ignore_transactions',
},
{
label: 'Couleur',
type: 'color',
required: true,
key: 'color',
},
{
label: null,
widget: 'rules',
required: false,
key: 'rules',
},
],
}
}
const doDelete = () => {
if (!confirm('Je confirme la suppression')) {
return
}
fetch(
`${endpoint}/${id}`,
createRequestOptions({
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}),
).then(() => {
doBack()
})
}
const doSave = (e) => {
e.preventDefault()
if (form.value.data.rules === null) {
form.value.data.rules = []
}
if (
form.value.data.month_threshold !== null &&
form.value.data.month_threshold !== ''
) {
form.value.data.month_threshold = parseFloat(
form.value.data.month_threshold,
)
} else {
form.value.data.month_threshold = null
}
form.value.data.ignore_transactions =
form.value.data.ignore_transactions === 1
form.value.data.rules.forEach((value, key) => {
if (value.amount !== null && value.amount !== '') {
form.value.data.rules[key].amount = parseFloat(value.amount)
}
if (value.date_from) {
form.value.data.rules[key].date_from = toISODateString(
value.date_from,
0,
0,
0,
)
}
if (value.date_to) {
form.value.data.rules[key].date_to = toISODateString(
value.date_to,
23,
59,
59,
)
}
for (let i in value) {
if (value[i] === '') {
form.value.data.rules[key][i] = null
}
}
})
fetch(
`${endpoint}/${id}`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
form.value = null
formShow.value = false
loadData()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doBack = () => {
router.replace({ name: parentRouteName })
}
onMounted(loadData)
</script>
<style>
#rules > div {
max-height: calc(100vh - 140px);
overflow: auto;
}
</style>

View file

@ -0,0 +1,162 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header :title="$route.meta.label">
<template #menu>
<BButtonToolbar key-nav>
<BButton
variant="secondary"
class="me-2"
@click="doApply"
>
<VueSpinner
v-if="applyInProgress"
size="20"
color="white"
/>
<span v-else>Appliquer&nbsp;les&nbsp;règles</span>
</BButton>
<BButton
variant="primary"
@click="doCreate"
>Ajouter</BButton
>
</BButtonToolbar>
</template>
<template #bottom>
<Pager
v-model:page="page"
v-model:pages="pages"
v-model:limit="limit"
@update="refresh()"
/>
</template>
</Header>
<div
v-if="data"
class="crud-list"
>
<DataList
:rows="data.rows"
:headers="fields"
:sort="sort"
:order="order"
@sort="doSort"
@row-click="doEdit"
/>
</div>
</BContainer>
</template>
<script setup>
import { BContainer, BButton, BButtonToolbar } from 'bootstrap-vue-next'
import { VueSpinner } from 'vue3-spinners'
import Header from '../../components/crud/Header.vue'
import Pager from '../../components/crud/Pager.vue'
import DataList from '../../components/crud/DataList.vue'
import { ref, onMounted, watch } from 'vue'
import { getStorage, saveStorage } from '../../lib/storage'
import {
renderCategory,
renderEuro,
renderLabelWithSum,
} from '../../lib/renderers'
import { createRequestOptions } from '../../lib/request'
import { useRouter } from 'vue-router'
const endpoint = `/api/category`
const order = ref(getStorage(`${endpoint}:order`))
const sort = ref(getStorage(`${endpoint}:sort`))
const page = ref(getStorage(`${endpoint}:page`))
const limit = ref(getStorage(`${endpoint}:limit`))
const data = ref(null)
const pages = ref(null)
const applyInProgress = ref(false)
const router = useRouter()
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const doApply = () => {
applyInProgress.value = true
fetch(
'/api/transactions/update_categories',
createRequestOptions({ method: 'POST' }),
).then(() => {
applyInProgress.value = false
})
}
const refresh = () => {
fetch(
`${endpoint}?${new URLSearchParams({
order: order.value,
sort: sort.value,
page: page.value,
limit: limit.value,
})}`,
createRequestOptions(),
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value
order.value = value.order
sort.value = value.sort
page.value = value.page
pages.value = value.total_pages
limit.value = value.limit
})
}
const doEdit = (item) => {
router.replace({ name: 'category_edit', params: { id: item.id } })
}
const doCreate = () => {
router.replace({ name: 'category_create' })
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const fields = [
{
key: 'label',
label: 'Libellé',
render: (item) => renderCategory(item),
},
{
key: 'month_threshold',
renderLabel: (rows) =>
renderLabelWithSum('Seuil mensuel', rows, 'month_threshold'),
width: '150px',
thClasses: ['text-end'],
tdClasses: ['text-end'],
render: (item) => renderEuro(item.month_threshold),
},
]
onMounted(() => {
refresh()
})
</script>

View file

@ -0,0 +1,138 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header :title="$route.meta.label">
<template #menu>
<div>
<BButton
variant="secondary"
@click="doBack"
>Retour</BButton
>
<BButton
variant="primary"
@click="doSave"
>Enregistrer</BButton
>
</div>
</template>
<template #bottom> </template>
</Header>
<BForm
v-if="form !== null"
class="p-3"
@submit="submit"
>
<BAlert
v-if="form?.error"
dismissible
variant="warning"
:model-value="true"
>
{{ form.error }}
</BAlert>
<BRow>
<BCol>
<FormView :form="form" />
</BCol>
</BRow>
</BForm>
</BContainer>
</template>
<script setup>
import {
BContainer,
BButton,
BForm,
BRow,
BCol,
BAlert,
} from 'bootstrap-vue-next'
import Header from './../../components/crud/Header.vue'
import FormView from './../../components/crud/FormView.vue'
import { ref, onMounted } from 'vue'
import { createRequestOptions } from '../../lib/request'
import { useRouter } from 'vue-router'
const router = useRouter()
const parentRouteName = 'saving_accounts'
const endpoint = `/api/saving_account`
const form = ref(null)
const createForm = () => {
const data = {
id: null,
label: null,
color: '#9eb1e7',
rules: [],
month_threshold: null,
ignore_transactions: 0,
}
return {
action: `${endpoint}`,
method: 'POST',
data: data,
label: 'Nouveau',
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
{
label: 'Montant bloqué',
type: 'number',
key: 'blocked_amount',
},
{
label: 'Montant débloqué',
type: 'number',
key: 'released_amount',
},
],
}
}
const doSave = (e) => {
fetch(
`${endpoint}`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
doBack()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doBack = () => {
router.replace({ name: parentRouteName })
}
onMounted(() => {
form.value = createForm()
})
</script>

View file

@ -0,0 +1,184 @@
<template>
<div
v-if="data === null"
class="text-center p-5"
>
<p>Chargement...</p>
<BSpinner />
</div>
<BContainer
v-else
fluid
class="p-0"
>
<Header :title="data.label">
<template #menu>
<div>
<BButton
variant="secondary"
@click="doBack"
>Retour</BButton
>
<BButton
variant="primary"
@click="doSave"
>Enregistrer</BButton
>
<BButton
variant="danger"
@click="doDelete"
>Supprimer</BButton
>
</div>
</template>
<template #bottom> </template>
</Header>
<BForm
v-if="form !== null"
class="p-3"
>
<BAlert
v-if="form?.error"
dismissible
variant="warning"
:model-value="true"
>
{{ form.error }}
</BAlert>
<BRow>
<BCol>
<FormView :form="form" />
</BCol>
</BRow>
</BForm>
</BContainer>
</template>
<script setup>
import {
BContainer,
BButton,
BForm,
BRow,
BCol,
BAlert,
} from 'bootstrap-vue-next'
import Header from './../../components/crud/Header.vue'
import FormView from './../../components/crud/FormView.vue'
import { ref, onMounted } from 'vue'
import { createRequestOptions } from '../../lib/request'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const parentRouteName = 'saving_accounts'
const endpoint = `/api/saving_account`
const data = ref(null)
const form = ref(null)
const formShow = ref(false)
const id = route.params.id
const loadData = () => {
fetch(
`${endpoint}?${new URLSearchParams({
id__eq: id,
})}`,
createRequestOptions(),
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value.rows[0]
form.value = createForm(value.rows[0])
})
}
const createForm = (item) => {
const data = { ...item }
return {
action: `${endpoint}/${item.id}`,
method: 'POST',
data: data,
label: data.label,
error: null,
fields: [
{
label: 'Libellé',
type: 'text',
required: true,
key: 'label',
},
{
label: 'Montant bloqué',
type: 'number',
key: 'blocked_amount',
},
{
label: 'Montant débloqué',
type: 'number',
key: 'released_amount',
},
],
}
}
const doDelete = () => {
if (!confirm('Je confirme la suppression')) {
return
}
fetch(
`${endpoint}/${id}`,
createRequestOptions({
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}),
).then(() => {
doBack()
})
}
const doSave = (e) => {
e.preventDefault()
fetch(
`${endpoint}/${id}`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
form.value = null
formShow.value = false
doBack()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doBack = () => {
router.replace({ name: parentRouteName })
}
onMounted(loadData)
</script>

View file

@ -0,0 +1,128 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header :title="$route.meta.label">
<template #menu>
<BButtonToolbar key-nav>
<BButton
variant="primary"
@click="doCreate"
>Ajouter</BButton
>
</BButtonToolbar>
</template>
<template #bottom>
<Pager
v-model:page="page"
v-model:pages="pages"
v-model:limit="limit"
@update="refresh()"
/>
</template>
</Header>
<div
v-if="data"
class="crud-list"
>
<DataList
:rows="data.rows"
:headers="fields"
:sort="sort"
:order="order"
@sort="doSort"
@row-click="doEdit"
/>
</div>
</BContainer>
</template>
<script setup>
import { BContainer, BButton, BButtonToolbar } from 'bootstrap-vue-next'
import { VueSpinner } from 'vue3-spinners'
import Header from '../../components/crud/Header.vue'
import Pager from '../../components/crud/Pager.vue'
import DataList from '../../components/crud/DataList.vue'
import { ref, onMounted, watch } from 'vue'
import { getStorage, saveStorage } from '../../lib/storage'
import {
renderCategory,
renderEuro,
renderLabelWithSum,
} from '../../lib/renderers'
import { createRequestOptions } from '../../lib/request'
import { useRouter } from 'vue-router'
const endpoint = `/api/saving_account`
const order = ref(getStorage(`${endpoint}:order`))
const sort = ref(getStorage(`${endpoint}:sort`))
const page = ref(getStorage(`${endpoint}:page`))
const limit = ref(getStorage(`${endpoint}:limit`))
const data = ref(null)
const pages = ref(null)
const router = useRouter()
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const refresh = () => {
fetch(
`${endpoint}?${new URLSearchParams({
order: order.value,
sort: sort.value,
page: page.value,
limit: limit.value,
})}`,
createRequestOptions(),
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value
order.value = value.order
sort.value = value.sort
page.value = value.page
pages.value = value.total_pages
limit.value = value.limit
})
}
const doEdit = (item) => {
router.replace({ name: 'saving_account_edit', params: { id: item.id } })
}
const doCreate = () => {
router.replace({ name: 'saving_account_create' })
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const fields = [
{
key: 'label',
label: 'Libellé',
},
]
onMounted(() => {
refresh()
})
</script>

View file

@ -3,7 +3,7 @@
fluid
class="p-0"
>
<Header title="Transactions">
<Header :title="$route.meta.label">
<template #menu>
<BButtonToolbar key-nav>
<BButton
@ -29,133 +29,27 @@
</template>
</Header>
<BModal
v-if="filtersShow"
v-model="filtersShow"
title="Filtres"
hide-footer
<div
v-if="data"
class="crud-list"
>
<Filters
:data="filters"
:fields="filtersFields"
@update="doUpdateFilters"
<DataList
:rows="data.rows"
:headers="fields"
:sort="sort"
:order="order"
@sort="doSort"
@row-click="doShow"
/>
</BModal>
<div class="crud-list">
<BTableSimple
v-if="data !== null"
caption-top
>
<BThead>
<BTr>
<BTh
v-for="field in fields"
:key="field.key"
:width="field.width"
class="cursor"
:class="field.thClasses"
valign="top"
@click="doSort(field.orderKey ?? field.key)"
>
<SortButton
:current-order="order"
:current-sort="sort"
:order="field.orderKey ?? field.key"
:label="field.label ?? field.renderLabel(data.rows)"
/>
</BTh>
</BTr>
</BThead>
<BTbody>
<BTr
v-for="(row, key) in data.rows"
:key="key"
>
<BTd
v-for="field in fields"
:key="field.key"
:width="field.width"
class="cursor"
:class="field.tdClasses"
@click="doInfo(row)"
>
<span v-if="field.key">
<span
v-if="field.render"
v-html="field.render(row)"
></span>
<span
v-else
v-text="row[field.key]"
></span>
</span>
<span
v-else
v-html="field.render(row)"
></span>
</BTd>
</BTr>
</BTbody>
</BTableSimple>
</div>
<BModal
v-if="info !== null"
v-model="infoShow"
class="modal-lg info"
:title="info.reference"
>
<BTableSimple
responsive
class="w-100"
>
<BTr>
<BTh>Libellé</BTh>
<BTd>{{ info.label }}</BTd>
</BTr>
<BTr>
<BTh>Libellé simplifié</BTh>
<BTd>{{ info.short_label }}</BTd>
</BTr>
<BTr>
<BTh>Référence</BTh>
<BTd>{{ info.reference }}</BTd>
</BTr>
<BTr>
<BTh>Informations</BTh>
<BTd>{{ info.information }}</BTd>
</BTr>
<BTr>
<BTh>Type d'opération</BTh>
<BTd>{{ info.operation_type }}</BTd>
</BTr>
<BTr>
<BTh>Débit</BTh>
<BTd>{{ info.debit }}</BTd>
</BTr>
<BTr>
<BTh>Crédit</BTh>
<BTd>{{ info.credit }}</BTd>
</BTr>
<BTr>
<BTh>Date</BTh>
<BTd>{{ renderDate(info.date) }}</BTd>
</BTr>
<BTr>
<BTh>Comptabilisée le</BTh>
<BTd>{{ renderDate(info.accounted_at) }}</BTd>
</BTr>
<BTr>
<BTh>Catégorie banque</BTh><BTd>{{ info.bank_category }}</BTd>
</BTr>
<BTr>
<BTh>Sous-catégorie banque</BTh>
<BTd>{{ info.bank_sub_category }}</BTd>
</BTr>
<BTr>
<BTh>Catégorie</BTh>
<BTd v-html="renderCategory(info.category)"></BTd>
</BTr>
</BTableSimple>
<ShowView :transaction="info" />
<template #footer>
<div>
@ -168,6 +62,7 @@
</div>
</template>
</BModal>
<BModal
v-if="form !== null"
v-model="formShow"
@ -180,38 +75,7 @@
v-text="form.error"
></BAlert>
<BForm @submit="doSave">
<BFormGroup
v-for="(field, key) in form.fields"
:id="'form-label-' + key"
:key="key"
class="mb-2"
:label="field.label"
:label-for="'form-label-' + key"
:description="field.description"
>
<BFormInput
v-if="(field.widget ?? 'text') === 'text'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:type="field.type"
:required="field.required"
/>
<BFormFile
v-if="field.widget === 'file'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:required="field.required"
/>
<BFormSelect
v-if="field.widget === 'select'"
:id="'form-input-' + key"
v-model="form.data[field.key]"
:options="field.options"
:required="field.required"
/>
</BFormGroup>
<FormView :form="form" />
</BForm>
<template #footer>
<div></div>
@ -230,6 +94,19 @@
</div>
</template>
</BModal>
<BModal
v-if="filtersShow"
v-model="filtersShow"
title="Filtres"
hide-footer
>
<Filters
:data="filters"
:fields="filtersFields"
@update="doUpdateFilters"
/>
</BModal>
</BContainer>
</template>
@ -253,22 +130,24 @@ import {
BFormFile,
} from 'bootstrap-vue-next'
import SortButton from './../components/SortButton.vue'
import Filters from './../components/Filters.vue'
import Header from './../components/crud/Header.vue'
import Pager from './../components/crud/Pager.vue'
import {ref, onMounted, watch} from 'vue'
import {getStorage, saveStorage} from '../lib/storage'
import Header from '../../components/crud/Header.vue'
import Filters from '../../components/Filters.vue'
import Pager from '../../components/crud/Pager.vue'
import FormView from '../../components/crud/FormView.vue'
import ShowView from './ShowView.vue'
import DataList from '../../components/crud/DataList.vue'
import { ref, onMounted, watch } from 'vue'
import { getStorage, saveStorage } from '../../lib/storage'
import { createRequestOptions } from '../../lib/request'
import { useRouter, useRoute } from 'vue-router'
import { queryFilters, appendRequestQueryFilters } from '../../lib/filter'
import {
renderDate,
renderCategory,
renderBankAccount,
renderEuro,
renderLabelWithSum,
} from '../lib/renderers'
import {queryFilters, appendRequestQueryFilters} from '../lib/filter'
import {useRoute} from 'vue-router'
import {createRequestOptions} from '../lib/request'
} from '../../lib/renderers'
const endpoint = `/api/transaction`
const order = ref(getStorage(`${endpoint}:order`))
@ -283,24 +162,13 @@ const infoShow = ref(false)
const info = ref(null)
const filters = ref(getStorage(`${endpoint}:filters`) ?? [])
const filtersShow = ref(false)
const router = useRouter()
let route = null
watch(order, (v) => saveStorage(`${endpoint}:order`, v, 'order'))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v, 'sort'))
watch(page, (v) => saveStorage(`${endpoint}:page`, v, 'page'))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v, 'limit'))
watch(filters, (v) => saveStorage(`${endpoint}:filters`, v, 'filters'))
const doInfo = (item) => {
info.value = item
infoShow.value = true
}
const doUpdateFilters = (a) => {
saveStorage(`${endpoint}:filters`, a, 'filters')
filters.value = a
refresh()
}
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const refresh = () => {
let query = {
@ -311,7 +179,7 @@ const refresh = () => {
limit: limit.value,
}
query = appendRequestQueryFilters(query, route)
query = appendRequestQueryFilters(query, endpoint)
fetch(`${endpoint}?${new URLSearchParams(query)}`, createRequestOptions())
.then((response) => {
@ -327,8 +195,33 @@ const refresh = () => {
})
}
const doShow = (item) => {
info.value = item
infoShow.value = true
}
const doUpdateFilters = (a) => {
saveStorage(`${endpoint}:filters`, a, 'filters')
filters.value = a
refresh()
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const doAdd = () => {
const data = {category_id: null, file: null, format: 'caisse_epargne'}
const data = { category_id: null, file: null, format: 'caisse_epargne' }
fetch(`/api/bank_account?order=label`, createRequestOptions())
.then((response) => {
@ -363,8 +256,8 @@ const doAdd = () => {
key: 'format',
widget: 'select',
options: [
{value: 'caisse_epargne', text: "Caisse d'épargne"},
{value: 'revolut', text: 'Revolut'},
{ value: 'caisse_epargne', text: "Caisse d'épargne" },
{ value: 'revolut', text: 'Revolut' },
],
},
{
@ -389,13 +282,16 @@ const doSave = (e) => {
payload.append('file', form.value.data.file)
payload.append('format', form.value.data.format)
fetch(`/api/transactions`, createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
},
body: payload,
}))
fetch(
`/api/transactions`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
},
body: payload,
}),
)
.then((response) => {
return response.json()
})
@ -413,28 +309,14 @@ const doSave = (e) => {
})
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const filtersFields = ref([
{key: 'label', type: 'string', label: 'Libellé'},
{key: 'debit', type: 'number', label: 'Débit'},
{key: 'credit', type: 'number', label: 'Crédit'},
{key: 'date', type: 'date', label: 'Date'},
{key: 'operation_type', type: 'string', label: 'Type'},
{key: 'category_id', type: 'select', label: 'Catégorie', options: []},
{key: 'bank_account_id', type: 'select', label: 'Compte', options: []},
{ key: 'label', type: 'string', label: 'Libellé' },
{ key: 'debit', type: 'number', label: 'Débit' },
{ key: 'credit', type: 'number', label: 'Crédit' },
{ key: 'date', type: 'date', label: 'Date' },
{ key: 'operation_type', type: 'string', label: 'Type' },
{ key: 'category_id', type: 'select', label: 'Catégorie', options: [] },
{ key: 'bank_account_id', type: 'select', label: 'Compte', options: [] },
])
const fields = [
@ -520,10 +402,3 @@ onMounted(() => {
})
})
</script>
<style scoped>
.info th,
.info td {
padding-bottom: 10px;
}
</style>

View file

@ -0,0 +1,66 @@
<template>
<BTableSimple
responsive
class="w-100"
>
<BTr>
<BTh>Libellé</BTh>
<BTd>{{ transaction.label }}</BTd>
</BTr>
<BTr>
<BTh>Libellé simplifié</BTh>
<BTd>{{ transaction.short_label }}</BTd>
</BTr>
<BTr>
<BTh>Référence</BTh>
<BTd>{{ transaction.reference }}</BTd>
</BTr>
<BTr>
<BTh>Informations</BTh>
<BTd>{{ transaction.transactionrmation }}</BTd>
</BTr>
<BTr>
<BTh>Type d'opération</BTh>
<BTd>{{ transaction.operation_type }}</BTd>
</BTr>
<BTr>
<BTh>Débit</BTh>
<BTd>{{ transaction.debit }}</BTd>
</BTr>
<BTr>
<BTh>Crédit</BTh>
<BTd>{{ transaction.credit }}</BTd>
</BTr>
<BTr>
<BTh>Date</BTh>
<BTd>{{ renderDate(transaction.date) }}</BTd>
</BTr>
<BTr>
<BTh>Comptabilisée le</BTh>
<BTd>{{ renderDate(transaction.accounted_at) }}</BTd>
</BTr>
<BTr>
<BTh>Catégorie banque</BTh><BTd>{{ transaction.bank_category }}</BTd>
</BTr>
<BTr>
<BTh>Sous-catégorie banque</BTh>
<BTd>{{ transaction.bank_sub_category }}</BTd>
</BTr>
<BTr>
<BTh>Catégorie</BTh>
<BTd v-html="renderCategory(transaction.category)"></BTd>
</BTr>
</BTableSimple>
</template>
<script setup>
import { BTableSimple, BTr, BTh, BTd } from 'bootstrap-vue-next'
import { renderDate, renderCategory } from '../../lib/renderers'
defineProps({
transaction: {
type: Object,
required: true,
},
})
</script>

View file

@ -0,0 +1,125 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header :title="$route.meta.label">
<template #menu>
<div>
<BButton
variant="secondary"
@click="doBack"
>Retour</BButton
>
<BButton
variant="primary"
@click="doSave"
>Enregistrer</BButton
>
</div>
</template>
<template #bottom> </template>
</Header>
<BForm
v-if="form !== null"
class="p-3"
@submit="submit"
>
<BRow>
<BCol>
<FormView :form="form" />
</BCol>
</BRow>
</BForm>
</BContainer>
</template>
<script setup>
import { BContainer, BButton, BForm, BRow, BCol } from 'bootstrap-vue-next'
import Header from '../../components/crud/Header.vue'
import FormView from '../../components/crud/FormView.vue'
import { ref, onMounted } from 'vue'
import { createRequestOptions } from '../../lib/request'
import { useRouter } from 'vue-router'
const router = useRouter()
const parentRouteName = 'users'
const endpoint = `/api/user`
const form = ref(null)
const createForm = () => {
const data = {
id: null,
label: null,
color: '#9eb1e7',
rules: [],
month_threshold: null,
ignore_transactions: 0,
}
return {
action: `${endpoint}`,
method: 'POST',
data: data,
label: 'Nouveau',
error: null,
fields: [
{
label: 'Nom',
type: 'text',
required: true,
key: 'display_name',
},
{
label: "Nom d'utilisateur",
type: 'text',
required: true,
key: 'username',
},
{
label: 'Mot de passe',
type: 'password',
required: true,
key: 'password',
},
],
}
}
const doSave = (e) => {
fetch(
`${endpoint}`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
doBack()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doBack = () => {
router.replace({ name: parentRouteName })
}
onMounted(() => {
form.value = createForm()
})
</script>

View file

@ -0,0 +1,186 @@
<template>
<div
v-if="data === null"
class="text-center p-5"
>
<p>Chargement...</p>
<BSpinner />
</div>
<BContainer
v-else
fluid
class="p-0"
>
<Header :title="data.label">
<template #menu>
<div>
<BButton
variant="secondary"
@click="doBack"
>Retour</BButton
>
<BButton
variant="primary"
@click="doSave"
>Enregistrer</BButton
>
<BButton
variant="danger"
@click="doDelete"
>Supprimer</BButton
>
</div>
</template>
<template #bottom> </template>
</Header>
<BForm
v-if="form !== null"
class="p-3"
>
<BAlert
v-if="form?.error"
dismissible
variant="warning"
:model-value="true"
>
{{ form.error }}
</BAlert>
<BRow>
<BCol>
<FormView :form="form" />
</BCol>
</BRow>
</BForm>
</BContainer>
</template>
<script setup>
import {
BContainer,
BButton,
BForm,
BRow,
BCol,
BAlert,
} from 'bootstrap-vue-next'
import Header from './../../components/crud/Header.vue'
import FormView from './../../components/crud/FormView.vue'
import { ref, onMounted } from 'vue'
import { createRequestOptions } from '../../lib/request'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const parentRouteName = 'users'
const endpoint = `/api/user`
const data = ref(null)
const form = ref(null)
const formShow = ref(false)
const id = route.params.id
const loadData = () => {
fetch(
`${endpoint}?${new URLSearchParams({
id__eq: id,
})}`,
createRequestOptions(),
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value.rows[0]
form.value = createForm(value.rows[0])
})
}
const createForm = (item) => {
const data = { ...item }
return {
action: `${endpoint}/${item.id}`,
method: 'POST',
data: data,
label: data.label,
error: null,
fields: [
{
label: 'Nom',
type: 'text',
required: true,
key: 'display_name',
},
{
label: "Nom d'utilisateur",
type: 'text',
required: true,
key: 'username',
},
{
label: 'Mot de passe',
type: 'password',
required: false,
key: 'password',
},
],
}
}
const doDelete = () => {
if (!confirm('Je confirme la suppression')) {
return
}
fetch(
`${endpoint}/${id}`,
createRequestOptions({
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}),
).then(() => {
doBack()
})
}
const doSave = (e) => {
e.preventDefault()
fetch(
`${endpoint}/${id}`,
createRequestOptions({
method: form.value.method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(form.value.data),
}),
)
.then((response) => {
return response.json()
})
.then((data) => {
if (data.code === 400) {
form.value.error = data.message
} else {
form.value = null
formShow.value = false
doBack()
}
})
.catch((err) => {
form.value.error = `Une erreur s'est produite : ${err}`
})
}
const doBack = () => {
router.replace({ name: parentRouteName })
}
onMounted(loadData)
</script>

View file

@ -0,0 +1,133 @@
<template>
<BContainer
fluid
class="p-0"
>
<Header :title="$route.meta.label">
<template #menu>
<BButtonToolbar key-nav>
<BButton
variant="primary"
@click="doCreate"
>Ajouter</BButton
>
</BButtonToolbar>
</template>
<template #bottom>
<Pager
v-model:page="page"
v-model:pages="pages"
v-model:limit="limit"
@update="refresh()"
/>
</template>
</Header>
<div
v-if="data"
class="crud-list"
>
<DataList
:rows="data.rows"
:headers="fields"
:sort="sort"
:order="order"
@sort="doSort"
@row-click="doEdit"
/>
</div>
</BContainer>
</template>
<script setup>
import { BContainer, BButton, BButtonToolbar } from 'bootstrap-vue-next'
import Header from '../../components/crud/Header.vue'
import Pager from '../../components/crud/Pager.vue'
import DataList from '../../components/crud/DataList.vue'
import { ref, onMounted, watch } from 'vue'
import { getStorage, saveStorage } from '../../lib/storage'
import { createRequestOptions } from '../../lib/request'
import { useRouter } from 'vue-router'
import { renderDateTime } from '../../lib/renderers'
const endpoint = `/api/user`
const order = ref(getStorage(`${endpoint}:order`))
const sort = ref(getStorage(`${endpoint}:sort`))
const page = ref(getStorage(`${endpoint}:page`))
const limit = ref(getStorage(`${endpoint}:limit`))
const data = ref(null)
const pages = ref(null)
const router = useRouter()
watch(order, (v) => saveStorage(`${endpoint}:order`, v))
watch(sort, (v) => saveStorage(`${endpoint}:sort`, v))
watch(page, (v) => saveStorage(`${endpoint}:page`, v))
watch(limit, (v) => saveStorage(`${endpoint}:limit`, v))
const refresh = () => {
fetch(
`${endpoint}?${new URLSearchParams({
order: order.value,
sort: sort.value,
page: page.value,
limit: limit.value,
})}`,
createRequestOptions(),
)
.then((response) => {
return response.json()
})
.then(function (value) {
data.value = value
order.value = value.order
sort.value = value.sort
page.value = value.page
pages.value = value.total_pages
limit.value = value.limit
})
}
const doEdit = (item) => {
router.replace({ name: 'user_edit', params: { id: item.id } })
}
const doCreate = () => {
router.replace({ name: 'user_create' })
}
const doSort = (key) => {
let nextSort = 'asc'
if (order.value === key) {
nextSort = sort.value === 'asc' ? 'desc' : 'asc'
}
order.value = key
sort.value = nextSort
page.value = 1
refresh()
}
const fields = [
{
key: 'display_name',
label: 'Nom',
width: '30%',
},
{
key: 'username',
label: 'Utilisateur',
},
{
key: 'logged_at',
label: 'Dernière connexion',
render: (item) => renderDateTime(item.logged_at),
},
]
onMounted(() => {
refresh()
})
</script>

View file

@ -2,7 +2,7 @@ $white: #ffffff;
$theme-colors: (
'navBg': #f5f6f7,
'navText': #47516B,
'navText': #47516b,
'navActiveBg': #3072c7,
'light': #f1c3a2,
'dark': #07070b,
@ -21,10 +21,10 @@ $nav-pills-link-active-bg: map-get($theme-colors, 'navActiveBg');
@import '~bootstrap/scss/bootstrap';
@import '~bootstrap-vue-next/dist/bootstrap-vue-next.css';
@import '~@fortawesome/fontawesome-free/css/all.css';
@import "@fontsource/ubuntu/300.css";
@import '@fontsource/ubuntu/300.css';
body {
font-family: "Ubuntu";
font-family: 'Ubuntu';
}
$light-grey: #f5f6f7;
@ -34,21 +34,13 @@ $light-grey: #f5f6f7;
}
#login-form {
background: linear-gradient(
-45deg,
#f1f1f1 50%,
#e1e1e1 100%
);
background: linear-gradient(-45deg, #f1f1f1 50%, #e1e1e1 100%);
.card {
background: linear-gradient(
-45deg,
#f1f1f1 50%,
#e1e1e1 100%
);
background: linear-gradient(-45deg, #f1f1f1 50%, #e1e1e1 100%);
}
color: #47516B;
color: #47516b;
}
.cursor {
@ -114,11 +106,13 @@ $nav-size-sm: 50px;
}
tr {
td:first-child, th:first-child {
td:first-child,
th:first-child {
padding-left: 1rem;
}
td:last-child, th:last-child {
td:last-child,
th:last-child {
padding-right: 1rem;
}
@ -204,7 +198,16 @@ $nav-size-sm: 50px;
}
.header {
background-image: linear-gradient(45deg, #fafafa 25%, #f5f6f7 25%, #f5f6f7 50%, #fafafa 50%, #fafafa 75%, #f5f6f7 75%, #f5f6f7 100%);
background-image: linear-gradient(
45deg,
#fafafa 25%,
#f5f6f7 25%,
#f5f6f7 50%,
#fafafa 50%,
#fafafa 75%,
#f5f6f7 75%,
#f5f6f7 100%
);
background-size: 56.57px 56.57px;
color: #47516B;
color: #47516b;
}