refactoring: add sub routes and slit in several components

This commit is contained in:
Simon Vieille 2025-03-25 22:54:34 +01:00
commit 58b78dd335
Signed by: deblan
GPG key ID: 579388D585F70417
26 changed files with 2425 additions and 1909 deletions

View file

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

View file

@ -41,7 +41,7 @@ import {BNavItem} from 'bootstrap-vue-next'
class="me-2"
:class="route.meta.icon"
></i>
<span class="nav-item-label">{{ route.name }}</span>
<span class="nav-item-label">{{ route.meta.label }}</span>
</BNavItem>
</RouterLink>
</ul>

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<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>
@ -15,3 +15,13 @@ import {defineProps} from 'vue'
defineProps(['title'])
</script>
<style>
.menu .btn {
margin-bottom: 4px;
}
.menu .btn:not(:last-child) {
margin-right: 4px;
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@
fluid
class="p-0"
>
<Header title="Transactions">
<Header :title="$route.meta.label">
<template #menu>
<BButtonToolbar key-nav>
<BButton
@ -29,133 +29,24 @@
</template>
</Header>
<BModal
v-if="filtersShow"
v-model="filtersShow"
title="Filtres"
hide-footer
>
<Filters
:data="filters"
:fields="filtersFields"
@update="doUpdateFilters"
<div v-if="data" class="crud-list">
<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 +59,7 @@
</div>
</template>
</BModal>
<BModal
v-if="form !== null"
v-model="formShow"
@ -180,38 +72,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 +91,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 +127,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 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 {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 +159,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,9 +176,12 @@ const refresh = () => {
limit: limit.value,
}
query = appendRequestQueryFilters(query, route)
query = appendRequestQueryFilters(query, endpoint)
fetch(`${endpoint}?${new URLSearchParams(query)}`, createRequestOptions())
fetch(
`${endpoint}?${new URLSearchParams(query)}`,
createRequestOptions()
)
.then((response) => {
return response.json()
})
@ -327,6 +195,31 @@ 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'}
@ -413,20 +306,6 @@ 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'},
@ -519,11 +398,5 @@ onMounted(() => {
})
})
})
</script>
<style scoped>
.info th,
.info td {
padding-bottom: 10px;
}
</style>
</script>

View file

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

View file

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

View file

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

View file

@ -0,0 +1,134 @@
<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>