From 01cb4333b567b44b69d02f3a33ad108a0835eb74 Mon Sep 17 00:00:00 2001 From: Francisco Macedo Date: Sun, 21 Jan 2024 20:16:37 +0000 Subject: [PATCH] feature: read params from url and reverse as well --- src/components/Dashboard.vue | 60 +++++----- src/router/index.ts | 10 ++ src/store/index.ts | 217 ++++++++++++++++++++++++++++++----- src/views/SimulatorView.vue | 4 + 4 files changed, 235 insertions(+), 56 deletions(-) diff --git a/src/components/Dashboard.vue b/src/components/Dashboard.vue index 4acbc48..e1fe6dc 100644 --- a/src/components/Dashboard.vue +++ b/src/components/Dashboard.vue @@ -30,6 +30,7 @@ displayFrequency === FrequencyChoices[frequencyChoice], }" @click="setFrequency(frequencyChoice)" + data-cy="frequency-button" > {{ frequencyChoice }} @@ -37,7 +38,7 @@

Nr. of months to simulate your earnings

- +

In a portuguese company, you can get payed 2 extra months per year @@ -54,6 +55,7 @@ v-model:value="ssDiscountPosition" :min="0" :max="ssDiscountChoices.length - 1" + data-cy="ss-discount" > {{ ssDiscountDisplay }} @@ -74,6 +76,7 @@ :choices="SUPPORTED_TAX_RANK_YEARS" @change="changeCurrentTaxRankYear" :value="getCurrentTaxRankYear.toString()" + data-cy="tax-rank-years-dropdown" />

@@ -96,7 +99,9 @@ > { const ssDiscountChoices = [ -0.25, -0.2, -0.15, -0.1, -0.05, 0, +0.05, +0.1, +0.15, +0.2, +0.25, ]; -const ssDiscountPosition = ref(5); +const ssDiscountPosition = ref(store.ssDiscountChoices.indexOf(store.ssDiscount)); watch( () => ssDiscountPosition.value, (newPosition) => { @@ -343,23 +357,17 @@ const ssDiscountDisplay = computed(() => { return `${store.ssDiscount > 0 ? "+" : ""}${store.ssDiscount * 100}%`; }); -// Automatically switch first and second year -watch( - () => firstYear.value, - (value) => { - if(value === true) { - secondYear.value = false; - secondYearKey.value++; - } - }, -); -watch( - () => secondYear.value, - (value) => { - if(value === true) { - firstYear.value = false; - firstYearKey.value++; - } - }, -); +const setFirstYear = (value: boolean) => { + store.setFirstYear(value); + if (value === true) { + secondYear.value = false; + } +}; + +const setSecondYear = (value: boolean) => { + store.setSecondYear(value); + if (value === true) { + firstYear.value = false; + } +}; diff --git a/src/router/index.ts b/src/router/index.ts index b8af0fe..bebb506 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -22,4 +22,14 @@ const router = createRouter({ linkExactActiveClass: "underline underline-offset-8", }); +export const updateUrlQuery = (param: string, value: any) => { + const queryParams = { ...router.currentRoute.value.query }; + queryParams[param] = value; + router.push({ query: queryParams }); +}; + +export const clearUrlQuery = () => { + router.push({ query: undefined }); +}; + export default router; diff --git a/src/store/index.ts b/src/store/index.ts index 9044056..ef2e9e8 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,6 +1,7 @@ import { defineStore } from "pinia"; import { FrequencyChoices, GrossIncome, TaxRank, Colors } from "@/typings"; import { asCurrency } from "@/utils.js"; +import { updateUrlQuery, clearUrlQuery } from "@/router"; export const YEAR_BUSINESS_DAYS = 248; export const MONTH_BUSINESS_DAYS = 22; @@ -19,6 +20,7 @@ interface TaxesState { maxExpensesTax: number; expenses: number; ssDiscount: number; + ssDiscountChoices: number[]; currentTaxRankYear: (typeof SUPPORTED_TAX_RANK_YEARS)[number]; taxRanks: { [K in (typeof SUPPORTED_TAX_RANK_YEARS)[number]]: TaxRank[] }; colors: Colors; @@ -38,6 +40,9 @@ const useTaxesStore = defineStore({ displayFrequency: FrequencyChoices.Month, nrMonthsDisplay: 12, ssDiscount: 0, + ssDiscountChoices: [ + -0.25, -0.2, -0.15, -0.1, -0.05, 0, +0.05, +0.1, +0.15, +0.2, +0.25, + ], expensesTax: 15, maxExpensesTax: 15, expenses: 0, @@ -121,18 +126,14 @@ const useTaxesStore = defineStore({ }; } const monthSS = - this.ssTax - * Math.min( - SS_MAX_MONTH_INCOME, - this.grossIncome.month * 0.7) - * (1 + this.ssDiscount); + this.ssTax * + Math.min(SS_MAX_MONTH_INCOME, this.grossIncome.month * 0.7) * + (1 + this.ssDiscount); const yearSS = - this.ssTax - * Math.min( - SS_MAX_MONTH_INCOME, - this.grossIncome.month * 0.7) - * (1 + this.ssDiscount) - * 12; + this.ssTax * + Math.min(SS_MAX_MONTH_INCOME, this.grossIncome.month * 0.7) * + (1 + this.ssDiscount) * + 12; return { year: Math.max(yearSS, 20 * 12), month: Math.max(monthSS, 20), @@ -142,7 +143,7 @@ const useTaxesStore = defineStore({ specificDeductions() { return Math.max( 4104, - Math.min(this.ssPay.year, 0.1 * this.grossIncome.year) + Math.min(this.ssPay.year, 0.1 * this.grossIncome.year), ); }, @@ -168,19 +169,26 @@ const useTaxesStore = defineStore({ ? this.expensesNeeded - this.expenses : 0; - return grossIncome * (this.firstYear ? 0.375 : this.secondYear ? 0.5625 : 0.75) + expensesMissing; + return ( + grossIncome * + (this.firstYear ? 0.375 : this.secondYear ? 0.5625 : 0.75) + + expensesMissing + ); }, taxRank(): TaxRank { - return this.taxRanks[this.currentTaxRankYear].filter((taxRank: TaxRank, index: number) => { - const isLastRank = index === this.taxRanks[this.currentTaxRankYear].length - 1; - const isBiggerThanMin = taxRank.min < this.taxableIncome; - const isSmallerThanMax = taxRank.max >= this.taxableIncome; + return this.taxRanks[this.currentTaxRankYear].filter( + (taxRank: TaxRank, index: number) => { + const isLastRank = + index === this.taxRanks[this.currentTaxRankYear].length - 1; + const isBiggerThanMin = taxRank.min < this.taxableIncome; + const isSmallerThanMax = taxRank.max >= this.taxableIncome; - if (isLastRank && isBiggerThanMin) { - return taxRank; - } - return isBiggerThanMin && isSmallerThanMax; - })[0]; + if (isLastRank && isBiggerThanMin) { + return taxRank; + } + return isBiggerThanMin && isSmallerThanMax; + }, + )[0]; }, getTaxRanks(): TaxRank[] { return this.taxRanks[this.currentTaxRankYear]; @@ -193,11 +201,13 @@ const useTaxesStore = defineStore({ return this.taxRank; } const avgID = this.taxRank.id - 1; - return this.taxRanks[this.currentTaxRankYear].filter((taxRank: TaxRank) => taxRank.id == avgID)[0]; + return this.taxRanks[this.currentTaxRankYear].filter( + (taxRank: TaxRank) => taxRank.id == avgID, + )[0]; }, taxIncomeAvg() { - if (this.rnh){ - return null + if (this.rnh) { + return null; } if (this.taxRank.id <= 1) { return this.taxableIncome; @@ -205,8 +215,8 @@ const useTaxesStore = defineStore({ return this.taxRankAvg.max; }, taxIncomeNormal() { - if (this.rnh){ - return null + if (this.rnh) { + return null; } if (this.taxRank.id <= 1) { return 0; @@ -236,7 +246,7 @@ const useTaxesStore = defineStore({ }, taxesDisplay() { return asCurrency( - this.irsPay[this.displayFrequency] + this.ssPay[this.displayFrequency] + this.irsPay[this.displayFrequency] + this.ssPay[this.displayFrequency], ); }, netIncome() { @@ -273,22 +283,169 @@ const useTaxesStore = defineStore({ this.income = null; } else { this.income = value ? value : 0; - this.expenses = this.expensesNeeded; + this.setExpenses(this.expensesNeeded); } + updateUrlQuery("income", this.income); }, setSsDiscount(value: number) { this.ssDiscount = value; + updateUrlQuery("ssDiscount", this.ssDiscount); }, setIncomeFrequency(frequency: FrequencyChoices) { this.incomeFrequency = frequency; + updateUrlQuery("incomeFrequency", this.incomeFrequency); }, setDisplayFrequency(frequency: FrequencyChoices) { this.displayFrequency = frequency; + updateUrlQuery("displayFrequency", this.displayFrequency); + }, + setNrMonthsDisplay(nrMonthsDisplay: number) { + this.nrMonthsDisplay = nrMonthsDisplay; + updateUrlQuery("nrMonthsDisplay", this.nrMonthsDisplay); }, setCurrentTaxRankYear( - taxRankYear: (typeof SUPPORTED_TAX_RANK_YEARS)[number] + taxRankYear: (typeof SUPPORTED_TAX_RANK_YEARS)[number], ) { this.currentTaxRankYear = taxRankYear; + updateUrlQuery("currentTaxRankYear", this.currentTaxRankYear); + }, + setExpenses(value: number) { + if (value < 0) { + this.expenses = 0; + } else { + this.expenses = value; + } + updateUrlQuery("expenses", this.expenses); + }, + setSsFirstYear(value: boolean) { + this.ssFirstYear = value; + updateUrlQuery("ssFirstYear", this.ssFirstYear); + }, + setFirstYear(value: boolean) { + this.firstYear = value; + updateUrlQuery("firstYear", this.firstYear); + }, + setSecondYear(value: boolean) { + this.secondYear = value; + updateUrlQuery("secondYear", this.secondYear); + }, + setRnh(value: boolean) { + this.rnh = value; + updateUrlQuery("rnh", this.rnh); + }, + + setParameterFromUrl( + value: any, + setter: CallableFunction, + parser: Function | null = null, + validator: CallableFunction | null = null, + ): boolean { + if (value === undefined) { + // undefined means the parameter was not in the URL + return false; + } + if (parser !== null) { + value = parser(value); + } + + if (validator !== null && !validator(value)) { + return false; + } + + setter(value); + return true; + }, + setParametersFromURL(params: object) { + const numericValidator = (value: number) => !isNaN(value); + const positiveNumericValidator = (value: number) => + numericValidator(value) && value > 0; + const booleanParser = (value: string) => value.toLowerCase() === "true"; + const frequencyChoicesValidator = (value: string) => + Object.values(FrequencyChoices).includes(value as FrequencyChoices); + const taxRankYearValidator = (value: number) => + SUPPORTED_TAX_RANK_YEARS.includes( + value as (typeof SUPPORTED_TAX_RANK_YEARS)[number], + ); + + const incomeResult = this.setParameterFromUrl( + params["income"], + this.setIncome, + parseInt, + positiveNumericValidator, + ); + if (incomeResult) { + this.validationCount++; + } + + this.setParameterFromUrl( + params["incomeFrequency"], + this.setIncomeFrequency, + null, + frequencyChoicesValidator, + ); + this.setParameterFromUrl( + params["displayFrequency"], + this.setDisplayFrequency, + null, + frequencyChoicesValidator, + ); + this.setParameterFromUrl( + params["nrMonthsDisplay"], + this.setNrMonthsDisplay, + parseInt, + (value: number) => value > 0, + ); + this.setParameterFromUrl( + params["ssDiscount"], + this.setSsDiscount, + parseFloat, + (value: number) => this.ssDiscountChoices.includes(value), + ); + this.setParameterFromUrl( + params["expenses"], + this.setExpenses, + parseInt, + numericValidator, + ); + this.setParameterFromUrl( + params["currentTaxRankYear"], + this.setCurrentTaxRankYear, + parseInt, + taxRankYearValidator, + ); + this.setParameterFromUrl( + params["ssFirstYear"], + this.setSsFirstYear, + booleanParser, + null, + ); + this.setParameterFromUrl( + params["firstYear"], + this.setFirstYear, + booleanParser, + null, + ); + this.setParameterFromUrl( + params["secondYear"], + this.setSecondYear, + booleanParser, + null, + ); + this.setParameterFromUrl(params["rnh"], this.setRnh, booleanParser, null); + }, + reset() { + this.setIncome(null); + this.setIncomeFrequency(FrequencyChoices.Year); + this.setDisplayFrequency(FrequencyChoices.Month); + this.setNrMonthsDisplay(12); + this.setSsDiscount(0); + this.setExpenses(0); + this.setCurrentTaxRankYear(2023); + this.setSsFirstYear(false); + this.setFirstYear(false); + this.setSecondYear(false); + this.setRnh(false); + clearUrlQuery(); }, }, }); diff --git a/src/views/SimulatorView.vue b/src/views/SimulatorView.vue index 8771eae..1cffdf6 100644 --- a/src/views/SimulatorView.vue +++ b/src/views/SimulatorView.vue @@ -8,6 +8,7 @@ \ No newline at end of file