budget-go/frontend/js/views/TransactionsView.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>