refactoring: add sub routes and slit in several components
This commit is contained in:
parent
262b9cab17
commit
58b78dd335
26 changed files with 2425 additions and 1909 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
94
frontend/js/components/crud/DataList.vue
Normal file
94
frontend/js/components/crud/DataList.vue
Normal 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>
|
||||
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>
|
||||
164
frontend/js/components/crud/FormWidget.vue
Normal file
164
frontend/js/components/crud/FormWidget.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
122
frontend/js/views/bank_account/CreateView.vue
Normal file
122
frontend/js/views/bank_account/CreateView.vue
Normal 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>
|
||||
156
frontend/js/views/bank_account/EditView.vue
Normal file
156
frontend/js/views/bank_account/EditView.vue
Normal 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>
|
||||
125
frontend/js/views/bank_account/ListView.vue
Normal file
125
frontend/js/views/bank_account/ListView.vue
Normal 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>
|
||||
173
frontend/js/views/category/CreateView.vue
Normal file
173
frontend/js/views/category/CreateView.vue
Normal 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>
|
||||
300
frontend/js/views/category/EditView.vue
Normal file
300
frontend/js/views/category/EditView.vue
Normal 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>
|
||||
156
frontend/js/views/category/ListView.vue
Normal file
156
frontend/js/views/category/ListView.vue
Normal 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 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>
|
||||
132
frontend/js/views/saving_account/CreateView.vue
Normal file
132
frontend/js/views/saving_account/CreateView.vue
Normal 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>
|
||||
166
frontend/js/views/saving_account/EditView.vue
Normal file
166
frontend/js/views/saving_account/EditView.vue
Normal 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>
|
||||
125
frontend/js/views/saving_account/ListView.vue
Normal file
125
frontend/js/views/saving_account/ListView.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
73
frontend/js/views/transaction/ShowView.vue
Normal file
73
frontend/js/views/transaction/ShowView.vue
Normal 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>
|
||||
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>
|
||||
168
frontend/js/views/user/EditView.vue
Normal file
168
frontend/js/views/user/EditView.vue
Normal 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>
|
||||
134
frontend/js/views/user/ListView.vue
Normal file
134
frontend/js/views/user/ListView.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue