Merge branch 'feature/refactoring' into develop
This commit is contained in:
commit
84e201ba41
52 changed files with 2773 additions and 2091 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"bracketSpacing": false,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ const compute = (accounts) => {
|
|||
}
|
||||
}
|
||||
|
||||
export {compute}
|
||||
export { compute }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
87
frontend/js/components/crud/DataList.vue
Normal file
87
frontend/js/components/crud/DataList.vue
Normal 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>
|
||||
28
frontend/js/components/crud/FormView.vue
Normal file
28
frontend/js/components/crud/FormView.vue
Normal 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>
|
||||
166
frontend/js/components/crud/FormWidget.vue
Normal file
166
frontend/js/components/crud/FormWidget.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ const chartStyle = (height) => {
|
|||
}
|
||||
}
|
||||
|
||||
export {chartStyle}
|
||||
export { chartStyle }
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@ const isInRange = (date, dateFrom, dateTo) => {
|
|||
return true
|
||||
}
|
||||
|
||||
export {isInRange}
|
||||
export { isInRange }
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ const appendRequestQueryFilters = (data, route) => {
|
|||
return data
|
||||
}
|
||||
|
||||
export {queryFilters, appendRequestQueryFilters}
|
||||
export { queryFilters, appendRequestQueryFilters }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -11,6 +11,4 @@ const createRequestOptions = (options) => {
|
|||
return options
|
||||
}
|
||||
|
||||
export {
|
||||
createRequestOptions
|
||||
}
|
||||
export { createRequestOptions }
|
||||
|
|
|
|||
|
|
@ -43,4 +43,4 @@ const removeStorage = function (key) {
|
|||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
export {getStorage, saveStorage, removeStorage}
|
||||
export { getStorage, saveStorage, removeStorage }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 les 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>
|
||||
3
frontend/js/views/ComponentView.vue
Normal file
3
frontend/js/views/ComponentView.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
128
frontend/js/views/bank_account/CreateView.vue
Normal file
128
frontend/js/views/bank_account/CreateView.vue
Normal 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>
|
||||
174
frontend/js/views/bank_account/EditView.vue
Normal file
174
frontend/js/views/bank_account/EditView.vue
Normal 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>
|
||||
128
frontend/js/views/bank_account/ListView.vue
Normal file
128
frontend/js/views/bank_account/ListView.vue
Normal 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>
|
||||
180
frontend/js/views/category/CreateView.vue
Normal file
180
frontend/js/views/category/CreateView.vue
Normal 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>
|
||||
338
frontend/js/views/category/EditView.vue
Normal file
338
frontend/js/views/category/EditView.vue
Normal 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>
|
||||
162
frontend/js/views/category/ListView.vue
Normal file
162
frontend/js/views/category/ListView.vue
Normal 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 les 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>
|
||||
138
frontend/js/views/saving_account/CreateView.vue
Normal file
138
frontend/js/views/saving_account/CreateView.vue
Normal 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>
|
||||
184
frontend/js/views/saving_account/EditView.vue
Normal file
184
frontend/js/views/saving_account/EditView.vue
Normal 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>
|
||||
128
frontend/js/views/saving_account/ListView.vue
Normal file
128
frontend/js/views/saving_account/ListView.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
66
frontend/js/views/transaction/ShowView.vue
Normal file
66
frontend/js/views/transaction/ShowView.vue
Normal 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>
|
||||
125
frontend/js/views/user/CreateView.vue
Normal file
125
frontend/js/views/user/CreateView.vue
Normal 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>
|
||||
186
frontend/js/views/user/EditView.vue
Normal file
186
frontend/js/views/user/EditView.vue
Normal 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>
|
||||
133
frontend/js/views/user/ListView.vue
Normal file
133
frontend/js/views/user/ListView.vue
Normal 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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue