527 lines
12 KiB
Vue
527 lines
12 KiB
Vue
<template>
|
|
<BContainer
|
|
fluid
|
|
class="p-0"
|
|
>
|
|
<Header title="Transactions">
|
|
<template #menu>
|
|
<BButtonToolbar key-nav>
|
|
<BButton
|
|
variant="secondary"
|
|
class="me-2"
|
|
@click="filtersShow = true"
|
|
>Filtres</BButton
|
|
>
|
|
<BButton
|
|
variant="primary"
|
|
@click="doAdd"
|
|
>Importer</BButton
|
|
>
|
|
</BButtonToolbar>
|
|
</template>
|
|
</Header>
|
|
|
|
<Pager
|
|
v-model:page="page"
|
|
v-model:pages="pages"
|
|
v-model:limit="limit"
|
|
@update="refresh()"
|
|
/>
|
|
|
|
<BModal
|
|
v-if="filtersShow"
|
|
v-model="filtersShow"
|
|
title="Filtres"
|
|
hide-footer
|
|
>
|
|
<Filters
|
|
:data="filters"
|
|
:fields="filtersFields"
|
|
@update="doUpdateFilters"
|
|
/>
|
|
</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>
|
|
|
|
<template #footer>
|
|
<div>
|
|
<BButton
|
|
variant="secondary"
|
|
class="me-2"
|
|
@click="infoShow = false"
|
|
>Fermer</BButton
|
|
>
|
|
</div>
|
|
</template>
|
|
</BModal>
|
|
<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"
|
|
/>
|
|
|
|
<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>
|
|
</BForm>
|
|
<template #footer>
|
|
<div></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,
|
|
BFormFile,
|
|
} from 'bootstrap-vue-next'
|
|
|
|
import SortButton from './../components/SortButton.vue'
|
|
import Filters from './../components/Filters.vue'
|
|
import Header from './../components/crud/Header.vue'
|
|
import Pager from './../components/crud/Pager.vue'
|
|
import {ref, onMounted, watch} from 'vue'
|
|
import {getStorage, saveStorage} from '../lib/storage'
|
|
import {
|
|
renderDate,
|
|
renderCategory,
|
|
renderBankAccount,
|
|
renderEuro,
|
|
renderLabelWithSum,
|
|
} from '../lib/renderers'
|
|
import {queryFilters, appendRequestQueryFilters} from '../lib/filter'
|
|
import {useRoute} from 'vue-router'
|
|
|
|
const endpoint = `/api/transaction`
|
|
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 infoShow = ref(false)
|
|
const info = ref(null)
|
|
const filters = ref(getStorage(`${endpoint}:filters`) ?? [])
|
|
const filtersShow = ref(false)
|
|
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()
|
|
}
|
|
|
|
const refresh = () => {
|
|
let query = {
|
|
...queryFilters(filters.value),
|
|
order: order.value,
|
|
sort: sort.value,
|
|
page: page.value,
|
|
limit: limit.value,
|
|
}
|
|
|
|
query = appendRequestQueryFilters(query, route)
|
|
|
|
fetch(`${endpoint}?${new URLSearchParams(query)}`)
|
|
.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 doAdd = () => {
|
|
const data = {category_id: null, file: null, format: 'caisse_epargne'}
|
|
|
|
fetch(`/api/bank_account?order=label`)
|
|
.then((response) => {
|
|
return response.json()
|
|
})
|
|
.then((accounts) => {
|
|
let options = []
|
|
|
|
accounts.rows.forEach((item) => {
|
|
options.push({
|
|
value: item.id,
|
|
text: item.label,
|
|
})
|
|
})
|
|
|
|
form.value = {
|
|
action: `${endpoint}`,
|
|
method: 'POST',
|
|
data: data,
|
|
label: 'Importer',
|
|
error: null,
|
|
fields: [
|
|
{
|
|
label: 'Compte bancaire',
|
|
widget: 'select',
|
|
options: options,
|
|
required: true,
|
|
key: 'bank_account_id',
|
|
},
|
|
{
|
|
label: 'Format de fichier',
|
|
key: 'format',
|
|
widget: 'select',
|
|
options: [
|
|
{value: 'caisse_epargne', text: "Caisse d'épargne"},
|
|
{value: 'revolut', text: 'Revolut'},
|
|
],
|
|
},
|
|
{
|
|
label: 'Fichier',
|
|
description: `Fichier CSV des opérations`,
|
|
widget: 'file',
|
|
required: true,
|
|
key: 'file',
|
|
},
|
|
],
|
|
}
|
|
|
|
formShow.value = true
|
|
})
|
|
}
|
|
|
|
const doSave = (e) => {
|
|
e.preventDefault()
|
|
|
|
const payload = new FormData()
|
|
payload.append('bank_account_id', form.value.data.bank_account_id)
|
|
payload.append('file', form.value.data.file)
|
|
payload.append('format', form.value.data.format)
|
|
|
|
fetch(`/api/transactions`, {
|
|
method: form.value.method,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
body: payload,
|
|
})
|
|
.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 filtersFields = ref([
|
|
{key: 'label', type: 'string', label: 'Libellé'},
|
|
{key: 'debit', type: 'number', label: 'Débit'},
|
|
{key: 'credit', type: 'number', label: 'Crédit'},
|
|
{key: 'date', type: 'date', label: 'Date'},
|
|
{key: 'operation_type', type: 'string', label: 'Type'},
|
|
{key: 'category_id', type: 'select', label: 'Catégorie', options: []},
|
|
{key: 'bank_account_id', type: 'select', label: 'Compte', options: []},
|
|
])
|
|
|
|
const fields = [
|
|
{
|
|
key: 'date',
|
|
label: 'Date',
|
|
width: '90px',
|
|
render: (item) => renderDate(item.date),
|
|
},
|
|
{
|
|
key: 'label',
|
|
label: 'Libellé',
|
|
},
|
|
{
|
|
key: 'operation_type',
|
|
width: '200px',
|
|
label: 'Type',
|
|
},
|
|
{
|
|
key: 'category',
|
|
label: 'Catégorie',
|
|
orderKey: 'category_id',
|
|
width: '400px',
|
|
render: (item) => renderCategory(item.category),
|
|
},
|
|
{
|
|
key: 'bank_account',
|
|
label: 'Compte',
|
|
width: '200px',
|
|
orderKey: 'bank_account_id',
|
|
render: (item) => renderBankAccount(item.bank_account),
|
|
},
|
|
{
|
|
key: 'debit',
|
|
renderLabel: (rows) => renderLabelWithSum('Débit', rows, 'debit'),
|
|
width: '120px',
|
|
thClasses: ['text-end'],
|
|
tdClasses: ['text-end'],
|
|
render: (item) => renderEuro(item.debit),
|
|
},
|
|
{
|
|
key: 'credit',
|
|
renderLabel: (rows) => renderLabelWithSum('Crédit', rows, 'credit'),
|
|
width: '120px',
|
|
thClasses: ['text-end'],
|
|
tdClasses: ['text-end'],
|
|
render: (item) => renderEuro(item.credit),
|
|
},
|
|
]
|
|
|
|
onMounted(() => {
|
|
route = useRoute()
|
|
refresh()
|
|
|
|
fetch('/api/category')
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
filtersFields.value.forEach((value, key) => {
|
|
if (value.key === 'category_id') {
|
|
data.rows.forEach((item) => {
|
|
filtersFields.value[key].options.push({
|
|
value: item.id,
|
|
text: item.label,
|
|
})
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
fetch('/api/bank_account')
|
|
.then((response) => response.json())
|
|
.then((data) => {
|
|
filtersFields.value.forEach((value, key) => {
|
|
if (value.key === 'bank_account_id') {
|
|
data.rows.forEach((item) => {
|
|
filtersFields.value[key].options.push({
|
|
value: item.id,
|
|
text: item.label,
|
|
})
|
|
})
|
|
}
|
|
})
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.info th,
|
|
.info td {
|
|
padding-bottom: 10px;
|
|
}
|
|
</style>
|