Compare commits

...

19 Commits
master ... dev

Author SHA1 Message Date
Bertrand Benjamin 494f302cc5 Feat: new config 2021-10-28 06:59:52 +02:00
Bertrand Benjamin 0493816dbe Supprimer 'analyse_comptes.ipynb' 2020-08-06 19:40:22 +00:00
Bertrand Benjamin 1825c5ffba Supprimer 'requirements.txt' 2020-08-06 19:40:17 +00:00
Bertrand Benjamin a6a37539bd Feat: force date order in graph 2019-01-30 17:21:09 +01:00
Bertrand Benjamin 68653aaabe Feat(data): Compute tags instead of read again csv 2019-01-30 16:03:20 +01:00
Bertrand Benjamin af2330d2fa Feat(tag): modal for adding keywords to tag 2019-01-30 15:41:27 +01:00
Bertrand Benjamin d486268921 Feat(tag): forgot to add new_tag component 2019-01-28 20:55:58 +01:00
Bertrand Benjamin 1482c7f862 Feat(tag): New tag and delete tags 2019-01-28 17:08:14 +01:00
Bertrand Benjamin 62ec65ce53 Feat(tag): Start creating new tag 2019-01-28 15:14:22 +01:00
Bertrand Benjamin f5bbf195f3 Feat(tag): Can modify tags and save it into config.yml 2019-01-28 12:34:23 +01:00
Bertrand Benjamin 342b2efd1c Feat(tag): Save modification in vuex 2019-01-28 12:14:56 +01:00
Bertrand Benjamin 48df3ecf7b Feat(tag): edit form for tag (no saving yet) 2019-01-28 12:05:22 +01:00
Bertrand Benjamin 62b193b04a Refact: rename #_item to item_# 2019-01-28 10:57:41 +01:00
Bertrand Benjamin bc7f771d25 Feat(Tags): transform graph_tags_comparison legend 2019-01-28 09:47:41 +01:00
Bertrand Benjamin ecf7db6a8f Feat(Categories): use tags as legend for graphs 2019-01-27 20:05:41 +01:00
Bertrand Benjamin 6f069832c0 Fix(Categorie): filter table on categories 2019-01-27 19:48:31 +01:00
Bertrand Benjamin 4d6a61fa58 Fix(Categories): forgot to add extra files 2019-01-27 19:01:53 +01:00
Bertrand Benjamin 66082aaa45 Feat(Categories): Show categories no filter yet 2019-01-27 18:59:09 +01:00
Bertrand Benjamin 22fc1a9b48 Refact: Rename tag comparison graphic component 2019-01-27 18:01:37 +01:00
17 changed files with 866 additions and 1232 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,33 @@
---
categories:
tout:
name: Tout
variant: info
icon: file-invoice-dollar
color: '#78e08f'
words: []
cash:
name: Cash
variant: info
icon: money-bill-wave
color: '#78e08f'
words:
- RETRAIT
cb:
name: CB
variant: info
icon: credit-card
color: '#4a69bd'
words:
- PAIEMENT
virements:
name: Virements
variant: info
icon: directions
invert: true
color: '#f6b93b'
words:
- PAIEMENT
- RETRAIT
tags:
cash:
name: Cash
@ -7,39 +36,22 @@ tags:
color: '#78e08f'
words:
- RETRAIT
CB:
name: CB
variant: info
icon: credit-card
color: "#4a69bd"
words:
- PAIEMENT
- PAIEMENT CB 2404 ALIXAN CASH BIO CARTE 74142381
- PAIEMENT CB 2404 SAINT MARCEL PRIMEUR DU CHANT CARTE 74142381
virements:
name: Virements
variant: info
icon: directions
invert: true
color: "#f6b93b"
color: '#b0afaf'
words:
- PAIEMENT
- RETRAIT
autoroute:
name: Autoroute
variant: danger
icon: road
color: "#eb2f06"
words:
- AUTOROUTE
- APRR
essence:
essence:
name: Essence
variant: danger
icon: charging-station
color: "#0c2461"
color: '#0c2461'
words:
- CARBU
- TOTAL
@ -49,20 +61,26 @@ tags:
- ESSO
- BELLEGARDE SU CARREFOUR
- ST CLAUDE CASINOCARB
- RUNGIS MAGASIN U
- LES ROUSSES DAC ANGEDI
- LES ROUSSES MARKET DAC
- COMMUNAY RELAIS COMMUNAY
- DORLISHEIM CORA
- ALIXAN 838782 ELF
- TENCE CLEAT DIS DAC
- VALSERHONE CARREFOUR
train:
name: train
name: Train
variant: danger
icon: train
color: "#eb2f06"
color: '#eb2f06'
words:
- SNCF
courses:
courses:
name: Courses
variant: warning
icon: shopping-cart
color: "#665191"
color: '#665191'
words:
- BIOCOOP
- LA VIE CLAIRE
@ -75,3 +93,104 @@ tags:
- INTERMARCHE
- LA FERME DU COIN
- ARBENT GEANT
- CHATILLON EN BIO VALSERINE
- ST LAURENT EN BIO COOP GRANDVA
- ST CLAUDE MAISON VIAL
- OYONNAX OYONNAXIENNE
- BOURG DE PEAG GEANT CG839
- CHATILLON EN LE TRAM PAYSAN
- BELLEGARDE SU PTIT CHEZ VILLE
- VALSERHONE LE PETIT CHEZERY
- VALSERHONE L'AIR DU LARGE
- ST GERMAIN DE EPICERIE FAURAX
vacances:
color: '#ff8a00'
icon: map-marked
name: Vacances
variant: ''
words:
- VACANCES
- PRALOGNAN LA PETIT CASINO
- PRALOGNAN LA AUGUSTE
- PRALOGNAN LA CASINO SHOP
resto-bar:
color: '#6ea5fc'
icon: utensils
name: Resto-bar
variant: ''
words:
- ALVEOLES
- LYON FRITE ALORS !
- OYONNAX CAFES PAT
- BELLECOMBE LA GUIENETTE
- MOUTIERS TARE LA PETITE FABRIQ
- BELLEGARDE SU L'AIR DU LARGE
- LYON ENGIMONO
- OBERNAI LE CHEVAL BLANC
- LA PESSE REFUGE BERBOIS
- LA PESSE AUB LES ERABLES
- LA PESSE LES TAVAILLONS
- LEUTENHEIM BOEUF ROUGE
voiture:
color: '#0077c8'
icon: car
name: Voiture
variant: ''
words:
- ARBENT A84 OYONNAX
- ST CLAUDE ALAIN PNEU
- VALEN2931212/ VALENHOTEVILAUTO
- AUTOROUTE
- APRR
- MORBIER PE
- DORLISHEIM EU VERT 067
- 'ANNECY SUD A R E A '
- '69 BRON CEDEX A R E A '
culture:
color: '#199c00'
icon: book
name: Culture
variant: ''
words:
- LYON MOMIE MANGAS
- ROMANS SUR IS LIBRAIRIE LA MAU
- PREMANON ESP MONDES POL
- VALSERHONE LIBR PAP BIGUET
- BELLEGARDE SU LES ARTS FRONTIE
enfant:
color: '#fc00d5'
icon: child
name: Enfant
variant: ''
words:
- Baby
- ST MARCEL LES GEKA TISSUS
- ST SAUVEUR DE ARDELAINE
- LEZIGNAN CORB LES BABILLEUSES
- NORTHAMPTON BAMBINO MIO LTD
- PARIS PAYPLUG COM
- PARIS HAMAC PARIS
- ERGUE GABERIC JEUJOUETHIQUE
- CHATILLON EN CHAUSS'EXPO
- LONDON GLOBALE /FRUGI
- NANTUA OPTIQUE PHOTO
- LA PESSE SYLVAIN BOULARD
- BOULOGNE-BILL AIGLE INTERNATIO
- TROYES PETIT BATEAU
- ST PIERREVILL ARDELAINE
maison:
color: '#f5ad61'
icon: home
name: Maison
variant: ''
words:
- BRICOLAGE
- CHATILLON EN BRICOMARCHE
- VALSERHONE GAMM VERT
- ROMANS SUR IS HOME COOK ROMANS
- CHATILLON EN KILOUTOU
- FR 34MONTPELL PG RIPATON
- OSTHOFEN PROFOLIO GMBH
- CHATILLON EN MR SAUSSAC J P
- CHABEUIL WWW MATERIAUX-NA
- 75017 PARIS MANOMANO

View File

@ -1,46 +0,0 @@
backcall==0.1.0
bleach==3.0.2
cycler==0.10.0
decorator==4.3.0
defusedxml==0.5.0
entrypoints==0.2.3
ipykernel==5.1.0
ipython==7.1.1
ipython-genutils==0.2.0
jedi==0.13.1
Jinja2==2.10
jsonschema==2.6.0
jupyter-client==5.2.3
jupyter-core==4.4.0
jupyterlab==0.35.4
jupyterlab-server==0.2.0
kiwisolver==1.0.1
MarkupSafe==1.1.0
matplotlib==3.0.2
mistune==0.8.4
nbconvert==5.4.0
nbformat==4.4.0
notebook==5.7.2
numpy==1.15.4
pandas==0.23.4
pandocfilters==1.4.2
parso==0.3.1
pexpect==4.6.0
pickleshare==0.7.5
prometheus-client==0.4.2
prompt-toolkit==2.0.7
ptyprocess==0.6.0
Pygments==2.3.0
pyparsing==2.3.0
python-dateutil==2.7.5
pytz==2018.7
PyYAML==3.13
pyzmq==17.1.2
Send2Trash==1.5.0
six==1.11.0
terminado==0.8.1
testpath==0.4.2
tornado==5.1.1
traitlets==4.3.2
wcwidth==0.1.7
webencodings==0.5.1

View File

@ -0,0 +1,77 @@
<template>
<b-form @submit="saveExit">
<b-form-group label="Mot clé"
label-for="keyword"
>
<b-form-input v-model="keyword"
id="keyword"
type="text"
required
>
</b-form-input>
</b-form-group>
<b-form-group label="Tag associé"
label-for="tag"
>
<b-form-select v-model="tagName"
id="tag"
:options="tagsName"
required
>
</b-form-select>
</b-form-group>
<b-btn type="submit" class="mt-3" variant="outline-success" block >Valider</b-btn>
<b-btn class="mt-3" variant="outline-danger" block @click="discardExit">Annuler</b-btn>
</b-form>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'associateTagKeyword',
props: [
'libelle'
],
data () {
return {
tagName: '',
keyword: this.cleanLibelle(this.libelle)
}
},
computed: {
...mapGetters({
'tags': 'config/tags'
}),
tagsName () {
return Object.keys(this.tags)
}
},
methods: {
...mapActions({
'append_keywords': 'config/append_keywords'
}),
cleanLibelle (libelle) {
var head = /PAIEMENT CB \d* /g
var tail = / CARTE \d*/g
return libelle.replace(head, '').replace(tail, '')
},
hideModal () {
this.$root.$emit('bv::hide::modal', this.libelle)
},
saveExit () {
this.append_keywords({
tagName: this.tagName,
keyword: this.keyword
})
this.hideModal()
},
discardExit () {
this.hideModal()
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,72 @@
<template>
<b-card v-if="categorie"
:bg-variant="categorie.variant"
text-variant="white"
class="text-center">
<div class="card-text">
<div class="icon">
<font-awesome-icon :icon="categorie.icon" class="fa-3x"/>
</div>
<div class="amount">
<h3>{{ total() }}</h3>
{{ categorie.name }}
</div>
</div>
</b-card>
</template>
<script>
import { mapGetters } from 'vuex'
import { total } from '../libs/data_processing'
export default {
name: 'cardCategorie',
props: [
'categoriename',
'rows'
],
data () {
return {
}
},
computed: {
...mapGetters('config', [
'categories'
]),
...mapGetters('datas', [
'categorie_filter_rows'
]),
categorie () {
return this.categories[this.categoriename.toLowerCase()]
}
},
methods: {
filter_rows () {
return this.categorie_filter_rows([this.categorie.name])
},
total () {
return total(this.filter_rows())
},
count () {
}
}
}
</script>
<style scoped>
.card-body {
padding: 10px;
}
.card-text {
display: flex;
}
.icon {
flex: 40%;
align-self: center;
}
.amount {
flex: 50%;
text-align: right;
margin-left: 10px;
}
</style>

View File

@ -19,7 +19,7 @@
import { mapGetters } from 'vuex'
import { total } from '../libs/data_processing'
export default {
name: 'box',
name: 'cardTag',
props: [
'tagname',
'rows'

View File

@ -0,0 +1,87 @@
<template>
<div class="tag">
<div class="icon">
<font-awesome-icon :style="{ color: value.color}" :icon="value.icon" class="fa-2x"/>
</div>
<div class="description">
<div v-if="existing_tag">
<h4>{{ value.name }}</h4>
</div>
<div v-else>
<b-form-input type="text" v-model="value.name"></b-form-input>
</div>
<b-form-group horizontal
label="Icône:"
label-class="text-sm"
label-for="icon">
<b-form-input type="text" v-model="value.icon" id="icon"></b-form-input>
</b-form-group>
<b-form-group horizontal
label="Couleur:"
label-class="text-sm"
label-for="color">
<b-form-input type="color" v-model="value.color" id="color"></b-form-input>
</b-form-group>
<b-form-group horizontal
label="Mots clés:"
label-class="text-sm"
label-for="keywords"
description="Une expression clé par ligne">
<b-form-textarea v-model="keywords" id="keywords"></b-form-textarea>
</b-form-group>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'editTag',
props: [
'value'
],
data () {
return {
}
},
computed: {
...mapGetters({
'tag': 'config/tag',
}),
keywords: {
get: function () {
return this.value.words.join('\n')
},
set: function (keywords) {
var kwds = keywords.split('\n')
this.value.words = kwds
}
},
existing_tag () {
return this.tag(this.value.name) ? true : false
}
},
methods: {
}
}
</script>
<style scoped>
.tag {
text-align: left;
align-self: center;
display: flex;
}
.icon {
flex: 10%;
align-self: center;
}
.description {
flex: 90%;
text-align: left;
align-self: center;
}
</style>

View File

@ -1,6 +1,16 @@
<template>
<div class="container">
<pie :chart-data="chartdata" :options="options" v-if="spendings[0] !== 0"></pie>
<div>
<div class="container">
<pie :chart-data="chartdata" :options="options" v-if="spendings[0] !== 0"></pie>
</div>
<div class="text-center">
<b-button-group size="sm">
<b-button v-for="tag in selected_tags"
:style="{'background-color': backgroundColor(tag)}">
{{ tag }}
</b-button>
</b-button-group>
</div>
</div>
</template>
@ -16,19 +26,11 @@ export default {
},
data () {
return {
selected_tags: [
'virements',
'cash',
'autoroute',
'train',
'essence',
'courses',
'sans tags'
],
options: {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
position: 'left'
}
}
@ -39,8 +41,14 @@ export default {
'tag_filter_rows'
]),
...mapGetters('config', [
'tag'
'tag',
'tags'
]),
selected_tags () {
var sel = Object.keys(this.tags)
sel.push('sans tags')
return sel
},
chartdata () {
return {
labels: this.selected_tags,
@ -49,11 +57,7 @@ export default {
label: 'Dépenses',
data: this.spendings,
backgroundColor: this.selected_tags.map(t => {
if (this.tag(t)) {
return this.tag(t).color
} else {
return '#A9A9A9'
}
return this.backgroundColor(t)
})
}
]
@ -72,6 +76,13 @@ export default {
}
},
methods: {
backgroundColor (t) {
if (this.tag(t)) {
return this.tag(t).color
} else {
return '#A9A9A9'
}
}
}
}

View File

@ -17,18 +17,22 @@ export default {
},
data () {
return {
selected_tags: [
'virements',
'cash',
'autoroute',
'train',
'essence',
'courses',
'sans tags'
],
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
xAxes: [{
type: 'time',
time: {
parser: 'MMMM YYYY',
},
scaleLabel: {
display: true,
format: 'MMMM YYYY',
labelString: 'Date'
}
}]
},
legend: {
position: 'left'
}
@ -41,8 +45,14 @@ export default {
'tag_filter_rows': 'tag_filter_rows'
}),
...mapGetters('config', [
'tag'
'tag',
'tags'
]),
selected_tags () {
var sel = Object.keys(this.tags)
sel.push('sans tags')
return sel
},
datasets () {
var datas = []
this.selected_tags.forEach(t => {

View File

@ -1,17 +1,16 @@
<template>
<div>
<div v-if="edit_mode" class="tag">
<div v-if="edit_mode" class="categorie">
<div class="icon">
<font-awesome-icon :icon="edited_tag.icon" class="fa-2x"/>
<font-awesome-icon :icon="edited_categorie.icon" class="fa-2x"/>
<!--
Icône inconnue
-->
</div>
<div class="description">
<b-form-input type="text" v-model="edited_tag.name"></b-form-input>
<b-form-input type="text" v-model="edited_tag.icon"></b-form-input>
<b-form-input type="color" v-model="edited_tag.color"></b-form-input>
<b-form-input type="text" v-model="edited_categorie.name"></b-form-input>
<b-form-input type="text" v-model="edited_categorie.icon"></b-form-input>
<b-form-input type="color" v-model="edited_categorie.color"></b-form-input>
</div>
<div class="actions">
<b-button-group vertical v-if="editable">
@ -20,14 +19,15 @@
</b-button-group>
</div>
</div>
<div v-else class="tag">
<div class="icon">
<font-awesome-icon :style="{ color: tag.color}" :icon="tag.icon" class="fa-2x"/>
<div v-else class="categorie">
<div class="icon text-info">
<font-awesome-icon :icon="categorie.icon" class="fa-2x"/>
</div>
<div class="description">
<h4>{{ tag.name }}</h4>
<h4>{{ categorie.name }}</h4>
Mots clés<span v-if="tag.invert"> (tout sauf)</span>: {{ tag.words.join(" - ") }}
Mots clés<span v-if="categorie.invert"> (tout sauf)</span>: {{ categorie.words.length != 0 ? categorie.words.join(" - ") : 'Aucun' }}
</div>
<div class="actions">
<!--
@ -44,34 +44,34 @@
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'tagConfig',
name: 'categorieItem',
props: [
'tagname'
'categoriename'
],
data () {
return {
default_tag: {
default_categorie: {
name: 'Tout',
variant: 'info',
icon: 'file-invoice-dollar'
},
edit_mode: false,
edited_tag: {}
edited_categorie: {}
}
},
computed: {
...mapGetters('config', {
getTag: 'tag'
getcategorie: 'categorie'
}),
tag () {
if (this.tagname) {
return this.getTag(this.tagname)
categorie () {
if (this.categoriename) {
return this.getcategorie(this.categoriename)
} else {
return this.default_tag
return this.default_categorie
}
},
editable () {
if (this.tagname) {
if (this.categoriename) {
return true
} else {
return false
@ -80,14 +80,14 @@ export default {
},
methods: {
...mapActions('config', [
'edit_tag'
'edit_categorie'
]),
toggleEdit () {
this.edited_tag = { ...this.tag }
this.edited_categorie = { ...this.categorie }
this.edit_mode = !this.edit_mode
},
save () {
this.edit_tag(this.edited_tag)
this.edit_categorie(this.edited_categorie)
this.toggleEdit()
}
}
@ -96,7 +96,7 @@ export default {
</script>
<style scoped>
.tag {
.categorie {
display: flex;
}
.icon {

106
src/components/item_tag.vue Normal file
View File

@ -0,0 +1,106 @@
<template>
<div class="item">
<div v-if="edit_mode" class="tag">
<edit-tag v-model="edited_tag"></edit-tag>
</div>
<div v-else class="tag">
<div class="icon">
<font-awesome-icon :style="{ color: tag.color}" :icon="tag.icon" class="fa-2x"/>
</div>
<div class="description">
<h4>{{ tag.name }}</h4>
Mots clés<span v-if="tag.invert"> (tout sauf)</span>: {{ tag.words.join(" - ") }}
</div>
</div>
<div class="actions">
<b-button-group vertical>
<div v-if="edit_mode">
<b-button @click="save()">Sauver</b-button>
<b-button @click="toggleEdit()">Annuler</b-button>
<b-button @click="deleteIt()">Supprimer</b-button>
</div>
<div v-else>
<b-button @click="toggleEdit()">Editer</b-button>
</div>
</b-button-group>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import editTag from './edit_tag'
export default {
name: 'tagItem',
components: {
editTag: editTag
},
props: [
'tagname'
],
data () {
return {
edit_mode: false,
edited_tag: {}
}
},
computed: {
...mapGetters('config', {
getTag: 'tag'
}),
tag () {
return this.getTag(this.tagname)
},
},
methods: {
...mapActions('config', [
'edit_tag',
'delete_tag'
]),
toggleEdit () {
// toggle edit mod for the tag
this.edited_tag = { ...this.tag }
this.edit_mode = !this.edit_mode
},
save () {
// Save the edited tag
this.edit_tag(this.edited_tag)
this.toggleEdit()
},
deleteIt () {
// Delete the edited tag
this.delete_tag(this.edited_tag.name)
this.toggleEdit()
}
}
}
</script>
<style scoped>
.item {
display: flex;
}
.tag {
flex: 90%;
text-align: left;
align-self: center;
display: flex;
}
.actions {
flex: 10%;
text-align: left;
margin-left: 10px;
align-self: center;
}
.icon {
flex: 10%;
align-self: center;
}
.description {
flex: 90%;
text-align: left;
align-self: center;
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<div class="item">
<div class="tag">
<div v-if="!isNewTag">
<b-button @click="toggle_new_tag" size="sm">
<font-awesome-icon icon="plus" class="fa" /> Ajouter un tag
</b-button>
</div>
<div class="description">
<div v-if="isNewTag" class="tag">
<edit-tag v-model="editedTag"></edit-tag>
</div>
</div>
</div>
<div class="actions">
<b-button-group vertical>
<div v-if="isNewTag">
<b-button @click="save()">Sauver</b-button>
<b-button @click="toggle_new_tag()">Annuler</b-button>
</div>
</b-button-group>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import editTag from './edit_tag'
export default {
name: 'newTag',
components: {
editTag: editTag
},
data () {
return {
isNewTag: false,
emptyTag: {
color: '',
icon: 'question',
name: '',
variant: '',
words: []
},
editedTag: {}
}
},
computed: {
},
methods: {
...mapActions('config', [
'edit_tag'
]),
toggle_new_tag () {
this.isNewTag = !this.isNewTag
this.editedTag = { ...this.emptyTag }
},
save () {
this.edit_tag(this.editedTag)
this.toggle_new_tag()
}
}
}
</script>
<style scoped>
.item {
display: flex;
}
.tag {
flex: 90%;
text-align: left;
align-self: center;
display: flex;
}
.actions {
flex: 10%;
text-align: left;
margin-left: 10px;
align-self: center;
}
.icon {
flex: 10%;
align-self: center;
}
.description {
flex: 90%;
text-align: left;
align-self: center;
}
</style>

View File

@ -1,17 +1,17 @@
import moment from 'moment'
export function appendTag (row, keywords, field = 'Libellé') {
// Append row.tag
export function appendKeywordField (row, toField, keywords, fromField = 'Libellé') {
// Append row[toField] base on keywords
// if row.libellé contains one of words and not invert it gets tagged
// if row.libellé contains no words and invert it gets tagged
// according to keywords [{name: string, words: [], invert: bool}]
row.tags = keywords.filter(k => {
return strContains(row[field], k.words, k.invert)
row[toField] = keywords.filter(k => {
return strContains(row[fromField], k.words, k.invert)
})
}
function strContains (string, words, invert) {
// Does a string contain one of words or the opposite
if (!words) {
if (words.length === 0) {
return true
}
if (invert) {
@ -35,19 +35,19 @@ export function total (rows, field = 'Montant') {
return Math.round(sum)
}
export function tagFilter (rows, tags, invert = false) {
// filter rows by tags
export function keywordFilter (rows, field, keywords, invert = false) {
// filter rows by keywords in rows[field]
// invert inverts the selection
return rows.filter(row => {
if (invert) {
return tags.some(t => {
return row.tags.map(t => t.name.toLowerCase())
.indexOf(t.toLowerCase()) < 0
return keywords.some(kwd => {
return row[field].map(k => k.name.toLowerCase())
.indexOf(kwd.toLowerCase()) < 0
})
} else {
return tags.every(t => {
return row.tags.map(t => t.name.toLowerCase())
.indexOf(t.toLowerCase()) > -1
return keywords.every(kwd => {
return row[field].map(k => k.name.toLowerCase())
.indexOf(kwd.toLowerCase()) > -1
})
}
})

View File

@ -1,4 +1,5 @@
import { readFile } from 'fs'
import { readFile, writeFile } from 'fs'
import Vue from 'vue'
import path from 'path'
import yaml from 'js-yaml'
@ -8,7 +9,8 @@ export default {
data_dir: '/home/lafrite/scripts/comptes/data/',
config_dir: '/home/lafrite/scripts/comptes/config/',
config_filename: 'config.yml',
tags: {}
tags: {},
categories: {}
},
getters: {
data_dir: (state) => {
@ -24,17 +26,37 @@ export default {
return state.tags
},
tag: (state) => (tagname) => {
return state.tags[tagname.toLowerCase()]
return state.tags[tagname.toLowerCase()]
},
categories: (state) => {
return state.categories
},
categorie: (state) => (categorieName) => {
return state.categories[categorieName.toLowerCase()]
}
},
mutations: {
SET_TAGS: (state, { tags }) => {
state.tags = Object.keys(tags)
.reduce((c, k) => (c[k.toLowerCase()] = tags[k], c), {})
}
// Set tags list
state.tags = Object.keys(tags)
.reduce((c, k) => (c[k.toLowerCase()] = tags[k], c), {})
},
APPEND_TAG: (state, { tag }) => {
// Append or replace et tag
Vue.set(state.tags, tag.name.toLowerCase(), tag)
},
DELETE_TAG: (state, { tagname }) => {
// delete tag by its name
Vue.delete(state.tags, tagname.toLowerCase())
},
SET_CATEGORIES: (state, { categories }) => {
state.categories = Object.keys(categories)
.reduce((c, k) => (c[k.toLowerCase()] = categories[k], c), {})
},
},
actions: {
load (context) {
// load config file
readFile(path.join(context.getters.config_dir, context.getters.config_filename), 'utf8', (err, content) => {
if (err) {
console.log(err)
@ -44,11 +66,37 @@ export default {
}
var parsed = yaml.safeLoad(content, parseConfig)
context.commit('SET_TAGS', { tags: parsed.tags })
context.commit('SET_CATEGORIES', { categories: parsed.categories })
}
})
},
save (context) {
// save config file
var config = {
categories: context.state.categories,
tags: context.state.tags
}
var yamlConfig = yaml.safeDump(config)
writeFile(path.join(context.getters.config_dir, context.getters.config_filename), yamlConfig, (err) => {
if (err) throw err
console.log('The file has been saved!')
})
context.dispatch('datas/compute_tags', null, { root: true })
},
edit_tag (context, tag) {
console.log(tag)
// Edit or append a tag to config
context.commit('APPEND_TAG', { tag: tag })
context.dispatch('save')
},
delete_tag (context, tagname) {
// Revome a tag from the config
context.commit('DELETE_TAG', { tagname: tagname })
context.dispatch('save')
},
append_keywords (context, { tagName, keyword }) {
var tag = context.getters.tag(tagName)
tag.words.push(keyword)
context.dispatch('edit_tag', tag)
}
}
}

View File

@ -3,13 +3,13 @@ import Vue from 'vue'
import path from 'path'
import Papa from 'papaparse'
import moment from 'moment'
import { appendTag, formatDate, tagFilter } from '../../libs/data_processing'
import { appendKeywordField, formatDate, keywordFilter } from '../../libs/data_processing'
export default {
namespaced: true,
state: {
csv: {},
month: moment()
month: ''
},
getters: {
csvs: (state) => {
@ -48,9 +48,13 @@ export default {
// return data with negatives 'Montant'
return getters.rows.filter(x => x.Montant < 0)
},
month: (state) => {
month: (state, getters) => {
// month date
return state.month
if (state.month){
return state.month
} else {
return moment(getters.months.slice(-1)[0], "MMMM YYYY")
}
},
date_filter_rows: (state, getters) => {
// return rows filtered by date
@ -69,11 +73,33 @@ export default {
rows = getters.spending_rows
}
if (tags.length > 0) {
return tagFilter(rows, tags, invert)
return keywordFilter(rows, 'tags', tags, invert)
} else {
if (invert) {
return rows.filter(r => {
return r.tags.map(t => t.name.toLowerCase()).toString() === ['cb'].toString()
return r.tags.length === 0
})
} else {
return rows
}
}
},
categorie_filter_rows: (state, getters) => (categories, invert, dateFilter = true) => {
// return rows filtered by categories
// by default it filters rows by date
// to disable date filtering set date_filter to false
var rows
if (dateFilter) {
rows = getters.date_filter_rows
} else {
rows = getters.spending_rows
}
if (categories.length > 0) {
return keywordFilter(rows, 'categorie', categories, invert)
} else {
if (invert) {
return rows.filter(r => {
return r.categories.length === 0
})
} else {
return rows
@ -101,9 +127,10 @@ export default {
},
months: (state, getters) => {
// Set of month
return [...new Set(getters.rows.map(x => moment(x.Date).format('MMMM YYYY')))]
return [...new Set(getters.rows.map(x => moment(x.Date).format('MMMM YYYY')))].sort((left, right) => {
return moment(left, 'MMMM YYYY').diff(moment(right, 'MMMM YYYY'))
})
}
},
mutations: {
CLEAR_DATA: (state) => {
@ -158,9 +185,11 @@ export default {
},
clean_store_data (context, { filename, parsed }) {
var tags = Object.values(context.rootGetters['config/tags'])
var categories = Object.values(context.rootGetters['config/categories'])
parsed.data = parsed.data.filter(x => x.Libellé !== undefined)
parsed.data.forEach(row => {
appendTag(row, tags, 'Libellé')
appendKeywordField(row, 'tags', tags, 'Libellé')
appendKeywordField(row, 'categorie', categories, 'Libellé')
formatDate(row, 'Date')
})
@ -169,6 +198,19 @@ export default {
{ filename: filename, data: parsed }
)
},
compute_tags (context) {
var tags = Object.values(context.rootGetters['config/tags'])
var categories = Object.values(context.rootGetters['config/categories'])
Object.values(context.state.csv).forEach(csv => {
csv.data.forEach(row => {
appendKeywordField(row, 'tags', tags, 'Libellé')
appendKeywordField(row, 'categorie', categories, 'Libellé')
})
context.commit('SET_DATA',
{ filename: csv.filename, data: csv }
)
})
},
next_month (context) {
var next = moment(context.getters.month).add(1, 'months')
context.commit('SET_MONTH', { month: next })
@ -176,6 +218,9 @@ export default {
prev_month (context) {
var prev = moment(context.getters.month).subtract(1, 'months')
context.commit('SET_MONTH', { month: prev })
},
set_month (context, month) {
context.commit('SET_MONTH', { month: month })
}
}
}

View File

@ -2,12 +2,14 @@
<div class="tags">
<h1>
Fichiers CSV
<b-button @click="reload_csvs">
<font-awesome-icon icon="sync-alt" class="fa" />
</b-button>
</h1>
<p>
Les fichiers csv sont cherché dans <span class="datadir">{{ data_dir }}</span>
<p>
<b-button @click="reload_csvs" size="sm">
<font-awesome-icon icon="sync-alt" class="fa" /> Recharger
</b-button>
</p>
<p>
Les fichiers csv sont cherchés dans <span class="dir">{{ data_dir }}</span>
<!--
<b-button variant="link" @click="open_filebrowser(data_dir)"> Ouvrir <font-awesome-icon icon="folder-open" class="fa"/></b-button>
-->
@ -24,10 +26,30 @@
</b-list-group-item>
</b-list-group>
</p>
<h1>Tags</h1>
<h1>
Config
</h1>
<p>
<b-button @click="reload_config" size="sm">
<font-awesome-icon icon="sync-alt" class="fa" /> Recharger
</b-button>
</p>
<p>
Le fichier configuration se trouve à <span class="dir">{{ config_dir + config_filename}}</span>
</p>
<h2>Tags</h2>
<b-list-group>
<b-list-group-item>
<new-tag></new-tag>
</b-list-group-item>
<b-list-group-item v-for="tag in tags">
<tag-config :tagname="tag.name"></tag-config>
<tag-item :tagname="tag.name"></tag-item>
</b-list-group-item>
</b-list-group>
<h2>Categories</h2>
<b-list-group>
<b-list-group-item v-for="categorie in categories">
<categorie-item :categoriename="categorie.name"></categorie-item>
</b-list-group-item>
</b-list-group>
</div>
@ -35,31 +57,36 @@
<script>
import { mapGetters, mapActions } from 'vuex'
import { shell } from 'electron'
import tagConfig from '../components/tag_config'
import tagItem from '../components/item_tag'
import tagEdit from '../components/edit_tag'
import newTag from '../components/new_tag'
import categorieItem from '../components/item_categorie'
export default {
name: 'home',
components: {
'tag-config': tagConfig
},
data () {
return {
file: ''
}
},
mounted: function () {
'tag-item': tagItem,
'tag-edit': tagEdit,
'new-tag': newTag,
'categorie-item': categorieItem
},
computed: {
...mapGetters({
'data_dir': 'config/data_dir',
'config_dir': 'config/config_dir',
'config_filename': 'config/config_filename',
'csvs': 'datas/csvs',
'tags': 'config/tags'
'tags': 'config/tags',
'categories': 'config/categories'
})
},
methods: {
...mapActions('datas', {
'reload_csvs': 'load_csvs'
'reload_csvs': 'load_csvs'
}),
...mapActions('config', {
'reload_config': 'load',
'save_config': 'save'
}),
open_filebrowser (dir) {
console.log("plop")
@ -69,13 +96,13 @@ export default {
</script>
<style scoped>
.datadir {
.dir {
font-weight: bold;
}
.left {
float: left;
float: left;
}
.right {
float: right;
float: right;
}
</style>

View File

@ -21,21 +21,35 @@
</b-container>
<b-card-group deck class="mb-3">
<box @click.native="set_tags_filter([])"></box>
<box @click.native="set_tags_filter(['cash'])" tagname="cash"></box>
<box @click.native="set_tags_filter(['cb'])" tagname="cb"></box>
<box @click.native="set_tags_filter(['virements'])" tagname="virements"></box>
<div v-for="categorie in categories">
<card-categorie @click.native="set_categories_filter([categorie.name])" :categoriename="categorie.name"></card-categorie>
</div>
</b-card-group>
<tags-comparison></tags-comparison>
<b-table striped hover :items="filtered_rows" :fields='fields'>
<template slot="tags" slot-scope="data">
<div v-for="tag in data.item.tags" :key="tag.name">
<div v-if="tag.name !== 'Tout'">
<div v-if="data.item.tags.length > 0">
<div v-for="tag in data.item.tags" :key="tag.name">
<font-awesome-icon :icon="tag.icon" class="fa"/>
</div>
</div>
<div v-else>
<b-btn v-b-modal="data.item.Libellé">
<font-awesome-icon icon="plus" class="fa"/>
</b-btn>
<!-- Modal Component -->
<b-modal :id="data.item.Libellé"
title="Tag pour le mot clé"
hide-footer
>
<associate-tag-keyword
:libelle="data.item.Libellé">
</associate-tag-keyword>
</b-modal>
</div>
</template>
</b-table>
</div>
@ -56,9 +70,10 @@
<script>
import { mapGetters, mapActions } from 'vuex'
import box from '../components/box'
import tagsComparison from '../components/tags_comparison'
import cardCategorie from '../components/card_categorie'
import tagsComparison from '../components/graph_tags_comparison'
import graphTime from '../components/graph_time'
import associateTagKeyword from '../components/associate_tag_keyword'
import moment from 'moment'
moment.locale('fr')
@ -66,9 +81,10 @@ moment.locale('fr')
export default {
name: 'home',
components: {
box: box,
cardCategorie: cardCategorie,
tagsComparison: tagsComparison,
graphTime: graphTime
graphTime: graphTime,
associateTagKeyword: associateTagKeyword
},
data () {
return {
@ -95,30 +111,35 @@ export default {
variant: 'info',
icon: 'file-invoice-dollar'
},
tags_filter: []
categories_filter: [],
keyword: ''
}
},
computed: {
...mapGetters({
'csvs': 'datas/csvs',
'tag_filter_rows': 'datas/tag_filter_rows',
'categorie_filter_rows': 'datas/categorie_filter_rows',
'datas_present': 'datas/present',
'month': 'datas/month',
'months': 'datas/months',
'tags': 'config/tags'
'tags': 'config/tags',
'categories': 'config/categories'
}),
filtered_rows () {
return this.tag_filter_rows(this.tags_filter)
},
return this.categorie_filter_rows(this.categories_filter)
}
},
methods: {
...mapActions('datas', [
'next_month',
'prev_month',
'prev_month'
]),
set_tags_filter (tagnames) {
this.tags_filter = tagnames
set_categories_filter (categorienames) {
this.categories_filter = categorienames
},
showModal (name) {
console.log(name)
}
}
}
</script>