Various components

This commit is contained in:
James Cole
2022-02-27 10:13:25 +01:00
parent d1e1314dcf
commit e542a65bf3
7 changed files with 951 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
<!--
- Alert.vue
- Copyright (c) 2022 james@firefly-iii.org
-
- This file is part of Firefly III (https://github.com/firefly-iii).
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="q-ma-md" v-if="showAlert">
<div class="row">
<div class="col-12">
<q-banner :class="alertClass" inline-actions>
{{ message }}
<template v-slot:action>
<q-btn flat @click="dismissBanner" color="white" label="Dismiss"/>
<q-btn flat color="white" v-if="showAction" :to="actionLink" :label="actionText"/>
</template>
</q-banner>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Alert",
data() {
return {
showAlert: false,
alertClass: 'bg-green text-white',
message: '',
showAction: false,
actionText: '',
actionLink: {}
}
},
watch: {
'$route': function () {
this.checkAlert();
}
},
mounted() {
this.checkAlert();
window.addEventListener('flash', (event) => {
this.renderAlert(event.detail.flash);
});
},
methods: {
checkAlert: function () {
let alert = this.$q.localStorage.getItem('flash');
if (alert) {
this.renderAlert(alert);
}
if (false === alert) {
this.showAlert = false;
}
},
renderAlert: function (alert) {
// show?
this.showAlert = alert.show ?? false;
// get class
let level = alert.level ?? 'unknown';
this.alertClass = 'bg-green text-white';
if ('warning' === level) {
// untested yet.
this.alertClass = 'bg-orange text-white';
}
// render message:
this.message = alert.text ?? '';
let action = alert.action ?? {};
if (true === action.show) {
this.showAction = true;
this.actionText = action.text;
this.actionLink = action.link;
}
this.$q.localStorage.set('flash', false);
},
dismissBanner: function() {
this.showAlert = false;
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div>
<p>{{ title }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id" @click="increment">
{{ todo.id }} - {{ todo.content }}
</li>
</ul>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script>
import {
defineComponent,
PropType,
computed,
ref,
toRef,
Ref,
} from 'vue';
import { Todo, Meta } from './models';
function useClickCount() {
const clickCount = ref(0);
function increment() {
clickCount.value += 1
return clickCount.value;
}
return { clickCount, increment };
}
function useDisplayTodo(todos: Ref<Todo[]>) {
const todoCount = computed(() => todos.value.length);
return { todoCount };
}
export default defineComponent({
name: 'CompositionComponent',
props: {
title: {
type: String,
required: true
},
todos: {
type: Array as PropType<Todo[]>,
default: () => []
},
meta: {
type: Object as PropType<Meta>,
required: true
},
active: {
type: Boolean
}
},
setup(props) {
return { ...useClickCount(), ...useDisplayTodo(toRef(props, 'todos')) };
},
});
</script>

View File

@@ -0,0 +1,129 @@
<!--
- DateRange.vue
- Copyright (c) 2022 james@firefly-iii.org
-
- This file is part of Firefly III (https://github.com/firefly-iii).
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="q-pa-xs">
<div>
<!-- <DatePicker v-model="range" is-range :is-dark="darkMode" :model-config="modelConfig"/> -->
<q-date v-model="localRange" range minimal mask="YYYY-MM-DD"/>
</div>
<div class="q-mt-xs">
<span class="q-mr-xs"><q-btn @click="resetRange" size="sm" color="primary" label="Reset"/></span>
<q-btn color="primary" size="sm" label="Change range" icon-right="fas fa-caret-down" title="More options in preferences">
<q-menu>
<q-list style="min-width: 100px">
<q-item clickable v-close-popup v-for="choice in rangeChoices" @click="setViewRange(choice)">
<q-item-section>{{ $t('firefly.pref_' + choice.value) }}</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations} from "vuex";
import {useQuasar} from 'quasar'
import Preferences from "../api/preferences";
import format from 'date-fns/format';
export default {
name: "DateRange",
computed: {
...mapGetters('fireflyiii', ['getRange']),
...mapMutations('fireflyiii', ['setRange'])
},
created() {
// set dark mode:
const $q = useQuasar();
this.darkMode = $q.dark.isActive;
this.localRange = {
from: format(this.getRange.start, 'yyyy-MM-dd'),
to: format(this.getRange.end, 'yyyy-MM-dd')
};
},
watch: {
localRange: function (value) {
if (null !== value) {
const updatedRange = {
start: Date.parse(value.from),
end: Date.parse(value.to)
};
this.$store.commit('fireflyiii/setRange', updatedRange);
}
},
},
mounted() {
},
methods: {
resetRange: function () {
this.$store.dispatch('fireflyiii/resetRange').then(() => {
this.localRange = {
from: format(this.getRange.start, 'yyyy-MM-dd'),
to: format(this.getRange.end, 'yyyy-MM-dd')
};
});
},
setViewRange: function (value) {
let submission = value.value;
let preferences = new Preferences();
preferences.postByName('viewRange', submission);
this.$store.commit('fireflyiii/updateViewRange', submission);
this.$store.dispatch('fireflyiii/setDatesFromViewRange');
},
updateViewRange: function () {
}
},
data() {
return {
rangeChoices: [
{value: 'last30'},
{value: 'last7'},
{value: 'MTD'},
{value: '1M'},
{value: '3M'},
{value: '6M'},
],
darkMode: false,
range: {
start: new Date,
end: new Date
},
localRange: {
start: new Date,
end: new Date
},
modelConfig: {
start: {
timeAdjust: '00:00:00',
},
end: {
timeAdjust: '23:59:59',
},
},
}
},
components: {},
}
</script>

View File

@@ -0,0 +1,51 @@
<template>
<q-item
clickable
tag="a"
target="_blank"
:href="link"
>
<q-item-section
v-if="icon"
avatar
>
<q-icon :name="icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<q-item-label caption>
{{ caption }}
</q-item-label>
</q-item-section>
</q-item>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'EssentialLink',
props: {
title: {
type: String,
required: true
},
caption: {
type: String,
default: ''
},
link: {
type: String,
default: '#'
},
icon: {
type: String,
default: ''
}
}
})
</script>

View File

@@ -0,0 +1,373 @@
<template>
<div class="row">
<div class="col q-mb-xs">
<q-banner rounded class="bg-purple-8 text-white">
Hi! You must be new to Firefly III. Welcome! Please fill in this form to create some basic accounts and get you
started.
</q-banner>
</div>
</div>
<div class="row">
<div class="col-8 offset-2 q-mb-md">
<q-card bordered>
<q-card-section>
<div class="text-h6">Bank accounts</div>
</q-card-section>
<q-card-section>
<div class="row q-mb-xs">
<div class="col-8 offset-2">
<q-input
:error-message="bank_name_error"
:error="bank_name_has_error"
bottom-slots
:disable="disabledInput"
clearable
outlined v-model="bank_name" label="The name of your bank">
<template v-slot:prepend>
<q-icon name="fas fa-university"/>
</template>
</q-input>
</div>
</div>
<div class="row q-mb-xs">
<div class="col-3 offset-2">
<q-select
:error-message="currency_error"
:error="currency_has_error"
bottom-slots
:disable="disabledInput"
outlined
v-model="currency" emit-value class="q-pr-xs"
map-options :options="currencies" label="Currency"/>
</div>
<div class="col-5">
<q-input
:error-message="bank_balance_error"
:error="bank_balance_has_error"
bottom-slots
:disable="disabledInput"
outlined
v-model="bank_balance" :mask="balance_input_mask" reverse-fill-mask fill-mask="0"
label="Today's balance" hint="Enter your current balance">
<template v-slot:prepend>
<q-icon name="fas fa-money-bill-wave"/>
</template>
</q-input>
</div>
</div>
<div class="row q-mb-xs">
<div class="col-8 offset-2">
<q-input
:error-message="savings_balance_error"
:error="savings_balance_has_error"
bottom-slots
:disable="disabledInput"
outlined
v-model="savings_balance" :mask="balance_input_mask" reverse-fill-mask fill-mask="0"
label="Today's savings account balance" hint="Leave empty or set to zero if not relevant.">
<template v-slot:prepend>
<q-icon name="fas fa-coins"/>
</template>
</q-input>
</div>
</div>
</q-card-section>
</q-card>
</div>
</div>
<div class="row">
<div class="col-8 offset-2 q-mb-md">
<q-card>
<q-card-section>
<div class="text-h6">Preferences</div>
</q-card-section>
<q-card-section>
<div class="row q-mb-xs">
<div class="col-8 offset-2">
<q-select
:error-message="language_error"
:error="language_has_error"
bottom-slots
outlined
:disable="disabledInput"
v-model="language" emit-value
map-options :options="languages" label="I prefer the following language"/>
</div>
</div>
<div class="row">
<div class="col-10 offset-2">
<q-checkbox
:disable="disabledInput"
v-model="manage_cash" label="I want to manage cash using Firefly III"/>
<q-banner v-if="manage_cash_has_error" class="text-white bg-red">{{ manage_cash_error }}</q-banner>
</div>
</div>
<div class="row">
<div class="col-8 offset-2">
<q-checkbox
:disable="disabledInput"
v-model="have_cc" label="I have a credit card."/>
<q-banner v-if="have_cc_has_error" class="text-white bg-red">{{ have_cc_error }}</q-banner>
</div>
</div>
<div class="row">
<div class="col-8 offset-2">
<q-checkbox
:disable="disabledInput"
v-model="have_questions" label="I know where to go when I have questions"/>
<div class="q-px-sm">
Hint: visit <a href="https://github.com/firefly-iii/firefly-iii/discussions/">GitHub</a>
or <a href="#">Gitter.im</a>. You can also
contact me on <a href="#">Twitter</a> or via <a href="#">email</a>.
</div>
<q-banner v-if="have_questions_has_error" class="text-white bg-red">{{ have_questions_error }}</q-banner>
</div>
</div>
</q-card-section>
<q-card-section>
<div class="row">
<div class="col-8 offset-2">
<q-btn color="primary" @click="submitNewUser">Submit</q-btn>
</div>
</div>
</q-card-section>
</q-card>
</div>
</div>
</template>
<script>
import Configuration from "../../api/system/configuration";
import List from "../../api/currencies/list";
import Post from "../../api/accounts/post";
import PostCurrency from "../../api/currencies/post";
import Put from "../../api/preferences/put";
import {format, startOfMonth} from "date-fns";
export default {
name: "NewUser",
data() {
return {
bank_name: '',
bank_name_error: '',
bank_name_has_error: false,
currency: 'EUR',
currency_error: '',
currency_has_error: false,
bank_balance: '',
bank_balance_error: '',
bank_balance_has_error: false,
savings_balance: '',
savings_balance_error: '',
savings_balance_has_error: false,
language: 'en_US',
language_error: '',
language_has_error: false,
manage_cash: false,
manage_cash_error: '',
manage_cash_has_error: false,
have_cc: false,
have_cc_error: '',
have_cc_has_error: false,
have_questions: false,
have_questions_error: '',
have_questions_has_error: false,
balance_input_mask: '#.##',
balance_prefix: '€',
languages: [],
currencies: [],
disabledInput: false,
}
},
watch: {
currency: function (value) {
for (let key in this.currencies) {
if (this.currencies.hasOwnProperty(key)) {
let currency = this.currencies[key];
if (currency.value === value) {
let hash = '#';
this.balance_input_mask = '#.' + hash.repeat(currency.decimal_places);
}
}
}
}
},
mounted() {
// get languages
let config = new Configuration();
config.get('firefly.languages').then((response) => {
let obj = response.data.data.value;
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
let lang = obj[key];
this.languages.push({value: key, label: lang.name_locale + ' (' + lang.name_english + ')'});
}
}
});
// get currencies
let list = new List;
list.list(1).then((response) => {
let all = response.data.data;
for (let key in all) {
if (all.hasOwnProperty(key)) {
let currency = all[key];
this.currencies.push({
value: currency.attributes.code,
label: currency.attributes.name,
decimal_places: currency.attributes.decimal_places,
symbol: currency.attributes.symbol
});
}
}
});
},
methods: {
submitNewUser: function () {
this.resetErrors();
this.disabledInput = true;
if ('' === this.bank_name) {
this.bank_name_error = 'A name is required';
this.bank_name_has_error = true;
this.disabledInput = false;
return;
}
if (false === this.have_questions) {
this.have_questions_error = 'Please check this little box';
this.have_questions_has_error = true;
this.disabledInput = false;
return;
}
// submit banks
let main = this.submitMainAccount();
let savings = this.submitSavingsAccount();
let creditCard = this.submitCC();
let cash = this.submitCash();
// save language and currency:
let currency = this.submitCurrency();
let language = this.submitLanguage();
Promise.all([main, savings, creditCard, cash, currency, language]).then(() => {
this.$emit('created-accounts');
});
},
submitCurrency: function () {
// /api/v1/currencies/{code}/default
let poster = new PostCurrency;
return poster.makeDefault(this.currency);
},
submitLanguage: function () {
// /api/v1/currencies/{code}/default
return (new Put).put('language', this.language);
},
submitMainAccount: function () {
let poster = new Post;
let submission = {
name: this.bank_name + ' checking account TODO',
type: 'asset',
account_role: 'defaultAsset',
currency_code: this.currency_code,
};
if ('' !== this.bank_balance && 0.0 !== parseFloat(this.bank_balance)) {
let today = format(new Date, 'y-MM-dd');
submission.opening_balance = this.bank_balance;
submission.opening_balance_date = today;
}
return poster.post(submission);
},
submitSavingsAccount: function () {
let poster = new Post;
let submission = {
name: this.bank_name + ' savings account TODO',
type: 'asset',
account_role: 'savingAsset',
currency_code: this.currency_code,
};
if ('' !== this.savings_balance && 0.0 !== parseFloat(this.savings_balance)) {
let today = format(new Date, 'y-MM-dd');
submission.opening_balance = this.savings_balance;
submission.opening_balance_date = today;
return poster.post(submission);
}
return Promise.resolve();
},
submitCC: function () {
if (this.have_cc) {
let poster = new Post;
let today = format(startOfMonth(new Date), 'y-MM-dd');
let submission = {
name: this.bank_name + ' Credit card',
type: 'asset',
account_role: 'ccAsset',
currency_code: this.currency_code,
credit_card_type: 'monthlyFull',
monthly_payment_date: today
};
return poster.post(submission);
}
return Promise.resolve();
},
submitCash: function () {
if (this.manage_cash) {
let poster = new Post;
let submission = {
name: this.bank_name + ' Cash account',
type: 'asset',
account_role: 'cashWalletAsset',
currency_code: this.currency_code,
};
return poster.post(submission);
}
return Promise.resolve();
},
resetErrors: function () {
this.disabledInput = false;
this.bank_name_error = '';
this.bank_name_has_error = false;
this.currency_error = '';
this.currency_has_error = false;
this.bank_balance_error = '';
this.bank_balance_has_error = false;
this.savings_balance_error = '';
this.savings_balance_has_error = false;
this.language_error = '';
this.language_has_error = false;
this.manage_cash_error = '';
this.manage_cash_has_error = false;
this.have_cc_error = '';
this.have_cc_has_error = false;
this.have_questions_error = '';
this.have_questions_has_error = false;
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,8 @@
export interface Todo {
id: number;
content: string;
}
export interface Meta {
totalCount: number;
}

View File

@@ -0,0 +1,224 @@
<!--
- LargeTable.vue
- Copyright (c) 2022 james@firefly-iii.org
-
- This file is part of Firefly III (https://github.com/firefly-iii).
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<q-table
:title="title"
:rows="rows"
:columns="columns"
row-key="group_id"
v-model:pagination="pagination"
:loading="loading"
class="q-ma-md"
@request="onRequest"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn size="sm" v-if="props.row.splits.length > 1" round dense @click="props.expand = !props.expand"
:icon="props.expand ? 'fas fa-minus-circle' : 'fas fa-plus-circle'"/>
</q-td>
<q-td key="type" :props="props">
<q-icon class="fas fa-long-arrow-alt-right" v-if="'deposit' === props.row.type.toLowerCase()"></q-icon>
<q-icon class="fas fa-long-arrow-alt-left" v-if="'withdrawal' === props.row.type.toLowerCase()"></q-icon>
<q-icon class="fas fa-arrows-alt-h" v-if="'transfer' === props.row.type.toLowerCase()"></q-icon>
</q-td>
<q-td key="description" :props="props">
<router-link :to="{ name: 'transactions.show', params: {id: props.row.group_id} }" class="text-primary">
<span v-if="1 === props.row.splits.length">{{ props.row.description }}</span>
<span v-if="props.row.splits.length > 1">{{ props.row.group_title }}</span>
</router-link>
</q-td>
<q-td key="amount" :props="props">
{{ formatAmount(props.row.currencyCode, props.row.amount) }}
</q-td>
<q-td key="date" :props="props">
{{ formatDate(props.row.date) }}
</q-td>
<q-td key="source" :props="props">
{{ props.row.source }}
</q-td>
<q-td key="destination" :props="props">
{{ props.row.destination }}
</q-td>
<q-td key="category" :props="props">
{{ props.row.category }}
</q-td>
<q-td key="budget" :props="props">
{{ props.row.budget }}
</q-td>
<q-td key="menu" :props="props">
<q-btn-dropdown color="primary" label="Actions" size="sm">
<q-list>
<q-item clickable v-close-popup :to="{name: 'transactions.edit', params: {id: props.row.group_id}}">
<q-item-section>
<q-item-label>Edit</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="deleteTransaction(props.row.group_id, props.row.description, props.row.group_title)">
<q-item-section>
<q-item-label>Delete</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-td>
</q-tr>
<q-tr v-show="props.expand" :props="props" v-for="currentRow in props.row.splits">
<q-td auto-width/>
<q-td auto-width/>
<q-td>
<div class="text-left">{{ currentRow.description }}</div>
</q-td>
<q-td key="amount" :props="props">
{{ formatAmount(currentRow.currencyCode, currentRow.amount) }}
</q-td>
<q-td key="date">
</q-td>
<q-td key="source">
{{ currentRow.source }}
</q-td>
<q-td key="destination">
{{ currentRow.destination }}
</q-td>
<q-td key="category">
{{ currentRow.category }}
</q-td>
<q-td key="budget">
{{ currentRow.budget }}
</q-td>
<q-td key="menu" :props="props">
j
</q-td>
</q-tr>
</template>
</q-table>
</template>
<script>
import format from "date-fns/format";
import Destroy from "../../api/transactions/destroy";
export default {
name: "LargeTable",
props: {
title: String,
rows: Array,
loading: Boolean,
page: Number,
rowsPerPage: Number,
rowsNumber: Number
},
data() {
return {
pagination: {
sortBy: 'desc',
descending: false,
page: 1,
rowsPerPage: 5,
rowsNumber: 100
},
columns: [
{name: 'type', label: ' ', field: 'type', style: 'width: 30px'},
{name: 'description', label: 'Description', field: 'description', align: 'left'},
{
name: 'amount', label: 'Amount', field: 'amount'
},
{
name: 'date', label: 'Date', field: 'date',
align: 'left',
},
{name: 'source', label: 'Source', field: 'source', align: 'left'},
{name: 'destination', label: 'Destination', field: 'destination', align: 'left'},
{name: 'category', label: 'Category', field: 'category', align: 'left'},
{name: 'budget', label: 'Budget', field: 'budget', align: 'left'},
{name: 'menu', label: ' ', field: 'menu', align: 'left'},
],
}
},
mounted() {
this.pagination.page = this.page;
this.pagination.rowsPerPage = this.rowsPerPage;
this.pagination.rowsNumber = this.rowsNumber;
},
watch: {
page: function (value) {
this.pagination.page = value
},
rowsPerPage: function (value) {
this.pagination.rowsPerPage = value;
},
rowsNumber: function (value) {
this.pagination.rowsNumber = value;
}
},
methods: {
formatDate: function (date) {
return format(new Date(date), this.$t('config.month_and_day_fns'));
},
formatAmount: function (currencyCode, amount) {
return Intl.NumberFormat('en-US', {style: 'currency', currency: currencyCode}).format(amount);
},
onRequest: function (props) {
this.$emit('on-request', {page: props.pagination.page});
//this.page = props.pagination.page;
// this.triggerUpdate();
},
deleteTransaction: function(identifier, description, groupTitle) {
let title = description;
if('' !== groupTitle) {
title = groupTitle;
}
this.$q.dialog({
title: 'Confirm',
message: 'Do you want to delete transaction "' + title + '"?',
cancel: true,
persistent: true
}).onOk(() => {
this.destroyTransaction(identifier);
});
},
destroyTransaction: function (id) {
let destr = new Destroy;
destr.destroy(id).then(() => {
this.$store.dispatch('fireflyiii/refreshCacheKey');
//this.triggerUpdate();
});
},
},
}
</script>
<style scoped>
</style>