Skip to content

Commit

Permalink
feat(stats): add total debt over period
Browse files Browse the repository at this point in the history
  • Loading branch information
mtthp committed Oct 9, 2024
1 parent 7d244b2 commit 05a72b2
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 70 deletions.
4 changes: 4 additions & 0 deletions src/i18n/locales/fr-FR/stats/incomes.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"label": "Revenu moyen journalier",
"threshold": "sur {amount} de charges fixes journalières"
},
"debt": {
"label": "Dette recouvrable",
"tickets": "{''} | soit {count} ticket redevable | soit {count} tickets redevables"
},
"label": "En quelques chiffres",
"total": {
"label": "Au total",
Expand Down
69 changes: 10 additions & 59 deletions src/services/api/incomes.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,10 @@
import HTTP from '../http';
import dayjs from 'dayjs';

export const CHARGES_PER_YEAR_IN_EUR = {
2014: 3_240.73,
2015: 9_877.77,
2017: 9_795.03,
2018: 10_772.51,
2019: 12_052.23,
2020: 19_022.58,
2021: 18_509.5,
2022: 18_769.51,

2016: 9_877.77, // estimation
2023: 1_761.73 * 12,
// estimation
2024:
1_645.3 * 12 + // rent
208.35 + // insurance
60 * 12 + // internet
700 + // transactions fees
100 + // bank fees
2_000, // other
};

export const INCOME_PER_TICKET = 6 as const;
export const INCOME_PER_SUBSCRIPTION_DAY = 2 as const;

export const getCharges = (date: string, period: 'year' | 'month' | 'week' | 'day'): number => {
const day = dayjs(date);
const year = day.year();
// @ts-ignore
const yearCharges = CHARGES_PER_YEAR_IN_EUR[year] ?? 0;
switch (period) {
case 'year':
return yearCharges;
case 'month':
return yearCharges / 12;
case 'week':
return yearCharges / 52;
case 'day':
return yearCharges / 12 / day.daysInMonth();
default:
throw new Error(`Unknown period ${period}`);
}
};

export type IncomePeriod<PeriodType extends 'year' | 'month' | 'week' | 'day'> = {
date: string;
type: PeriodType;
data: {
charges: number; // regular expenses in euro
tickets: {
count: number; // tickets count consumed
amount: number; // amount in euro
Expand All @@ -65,19 +21,18 @@ export type IncomePeriod<PeriodType extends 'year' | 'month' | 'week' | 'day'> =
};
};

export type IncomePeriodWithCharges<PeriodType extends 'year' | 'month' | 'week' | 'day'> =
export type IncomePeriodWithTotal<PeriodType extends 'year' | 'month' | 'week' | 'day'> =
IncomePeriod<PeriodType> & {
data: IncomePeriod<PeriodType>['data'] & {
charges: number; // regular expenses in euro
incomes: number; // total incomes in euro
};
};

export const getIncomesPerYear = (
from?: string,
to?: string,
): Promise<IncomePeriodWithCharges<'year'>[]> => {
return HTTP.get('/stats/incomes/year', {
): Promise<IncomePeriodWithTotal<'year'>[]> => {
return HTTP.get('/stats/incomes/year/from-orders', {
params: {
...(from && { from }),
...(to && { to }),
Expand All @@ -88,7 +43,6 @@ export const getIncomesPerYear = (
...income,
data: {
...income.data,
charges: getCharges(income.date, 'year'),
incomes: income.data.tickets.amount + income.data.subscriptions.amount,
},
})),
Expand All @@ -98,8 +52,8 @@ export const getIncomesPerYear = (
export const getIncomesPerMonth = (
from?: string,
to?: string,
): Promise<IncomePeriodWithCharges<'month'>[]> => {
return HTTP.get('/stats/incomes/month', {
): Promise<IncomePeriodWithTotal<'month'>[]> => {
return HTTP.get('/stats/incomes/month/from-orders', {
params: {
...(from && { from }),
...(to && { to }),
Expand All @@ -110,7 +64,6 @@ export const getIncomesPerMonth = (
...income,
data: {
...income.data,
charges: getCharges(income.date, 'month'),
incomes: income.data.tickets.amount + income.data.subscriptions.amount,
},
})),
Expand All @@ -120,8 +73,8 @@ export const getIncomesPerMonth = (
export const getIncomesPerWeek = (
from?: string,
to?: string,
): Promise<IncomePeriodWithCharges<'week'>[]> => {
return HTTP.get('/stats/incomes/week', {
): Promise<IncomePeriodWithTotal<'week'>[]> => {
return HTTP.get('/stats/incomes/week/from-orders', {
params: {
...(from && { from }),
...(to && { to }),
Expand All @@ -132,7 +85,6 @@ export const getIncomesPerWeek = (
...income,
data: {
...income.data,
charges: getCharges(income.date, 'week'),
incomes: income.data.tickets.amount + income.data.subscriptions.amount,
},
})),
Expand All @@ -142,8 +94,8 @@ export const getIncomesPerWeek = (
export const getIncomesPerDay = (
from?: string,
to?: string,
): Promise<IncomePeriodWithCharges<'day'>[]> => {
return HTTP.get('/stats/incomes/day', {
): Promise<IncomePeriodWithTotal<'day'>[]> => {
return HTTP.get('/stats/incomes/day/from-orders', {
params: {
...(from && { from }),
...(to && { to }),
Expand All @@ -154,7 +106,6 @@ export const getIncomesPerDay = (
...income,
data: {
...income.data,
charges: getCharges(income.date, 'day'),
incomes: income.data.tickets.amount + income.data.subscriptions.amount,
},
})),
Expand Down
52 changes: 49 additions & 3 deletions src/views/Private/Stats/Incomes/StatsIncomesDaily.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
{{ $t('stats.incomes.daily.summary.label') }}
</h3>
<dl
class="mt-5 grid grid-cols-1 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow md:grid-cols-2 md:divide-x md:divide-y-0">
class="mt-5 grid grid-cols-1 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow lg:grid-cols-3 lg:divide-x lg:divide-y-0">
<div class="px-4 py-5 sm:p-6">
<dt class="truncate font-medium text-gray-500 sm:text-sm">
{{ $t('stats.incomes.daily.summary.average.label') }}
Expand Down Expand Up @@ -115,6 +115,42 @@
</div>
</dd>
</div>

<div class="px-4 py-5 sm:p-6">
<dt class="truncate font-medium text-gray-500 sm:text-sm">
{{ $t('stats.incomes.daily.summary.debt.label') }}
</dt>
<dd class="mt-1 flex flex-col">
<div
v-if="state.isFetchingIncomes"
class="mb-1 h-8 w-32 animate-pulse rounded-3xl bg-slate-200" />
<AnimatedCounter
v-else
class="block text-3xl font-semibold tracking-tight text-gray-900"
:duration="1"
:format="fractionAmount"
:to="totalDebtAmount" />

<div
v-if="state.isFetchingIncomes"
class="mt-1 h-5 w-48 animate-pulse rounded-3xl bg-slate-200" />
<div
v-else-if="state.incomes.length"
class="flex flex-row items-baseline justify-between text-sm">
<span class="shrink grow basis-0 truncate font-normal text-gray-500">
{{
$t('stats.incomes.daily.summary.debt.tickets', {
count: totalDebtCount,
})
}}
</span>

<div class="rounded-full bg-gray-100 px-2.5 py-0.5 font-medium text-gray-800">
{{ fractionPercentage(totalDebtAmount / totalIncome, $i18n.locale) }}
</div>
</div>
</dd>
</div>
</dl>
</section>
</div>
Expand All @@ -124,7 +160,7 @@
import StatsIncomesPeriodGraph from './StatsIncomesPeriodGraph.vue';
import { fractionAmount, fractionPercentage } from '@/helpers/currency';
import { handleSilentError } from '@/helpers/errors';
import { IncomePeriodWithCharges, getIncomesPerDay } from '@/services/api/incomes';
import { IncomePeriodWithTotal, getIncomesPerDay } from '@/services/api/incomes';
import { useNotificationsStore } from '@/store/notifications';
import { theme } from '@/styles/colors';
import { Head } from '@unhead/vue/components';
Expand Down Expand Up @@ -156,7 +192,7 @@ const i18n = useI18n();
const notificationsStore = useNotificationsStore();
const state = reactive({
isFetchingIncomes: false,
incomes: [] as IncomePeriodWithCharges<'day'>[],
incomes: [] as IncomePeriodWithTotal<'day'>[],
});
const totalIncome = computed(() =>
Expand All @@ -177,6 +213,16 @@ const averageCharges = computed(() => {
return totalCharges.value / state.incomes.length || 0;
});
const totalDebtCount = computed(() =>
state.incomes.map(({ data }) => data.tickets.debt.count).reduce((acc, count) => acc + count, 0),
);
const totalDebtAmount = computed(() =>
state.incomes
.map(({ data }) => data.tickets.debt.amount)
.reduce((acc, amount) => acc + amount, 0),
);
const options = computed<ComposeOption<GridComponentOption | TooltipComponentOption>>(() => ({
tooltip: {
formatter: (params) => {
Expand Down
4 changes: 2 additions & 2 deletions src/views/Private/Stats/Incomes/StatsIncomesMonthly.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ import { fractionAmount, fractionPercentage } from '@/helpers/currency';
import { DATE_FORMAT } from '@/helpers/dates';
import { handleSilentError } from '@/helpers/errors';
import { ROUTE_NAMES } from '@/router/names';
import { IncomePeriodWithCharges, getIncomesPerMonth } from '@/services/api/incomes';
import { IncomePeriodWithTotal, getIncomesPerMonth } from '@/services/api/incomes';
import { useNotificationsStore } from '@/store/notifications';
import { theme } from '@/styles/colors';
import { Head } from '@unhead/vue/components';
Expand Down Expand Up @@ -161,7 +161,7 @@ const i18n = useI18n();
const notificationsStore = useNotificationsStore();
const state = reactive({
isFetchingIncomes: false,
incomes: [] as IncomePeriodWithCharges<'month'>[],
incomes: [] as IncomePeriodWithTotal<'month'>[],
});
const totalIncome = computed(() =>
Expand Down
4 changes: 2 additions & 2 deletions src/views/Private/Stats/Incomes/StatsIncomesPeriodGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import AnalyticsGraph from '@/assets/animations/analytics-graph.lottie';
import EmptyState from '@/components/EmptyState.vue';
import LoadingSpinner from '@/components/LoadingSpinner.vue';
import { fractionAmount } from '@/helpers/currency';
import { IncomePeriodWithCharges } from '@/services/api/incomes';
import { IncomePeriodWithTotal } from '@/services/api/incomes';
import { theme } from '@/styles/colors';
import { useWindowSize } from '@vueuse/core';
import { BarChart, LineChart } from 'echarts/charts.js';
Expand Down Expand Up @@ -48,7 +48,7 @@ const props = defineProps({
default: false,
},
incomes: {
type: Array as PropType<IncomePeriodWithCharges<'year' | 'month' | 'week' | 'day'>[]>,
type: Array as PropType<IncomePeriodWithTotal<'year' | 'month' | 'week' | 'day'>[]>,
default: () => [],
},
/**
Expand Down
4 changes: 2 additions & 2 deletions src/views/Private/Stats/Incomes/StatsIncomesWeekly.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ import { fractionAmount, fractionPercentage } from '@/helpers/currency';
import { DATE_FORMAT } from '@/helpers/dates';
import { handleSilentError } from '@/helpers/errors';
import { ROUTE_NAMES } from '@/router/names';
import { IncomePeriodWithCharges, getIncomesPerWeek } from '@/services/api/incomes';
import { IncomePeriodWithTotal, getIncomesPerWeek } from '@/services/api/incomes';
import { useNotificationsStore } from '@/store/notifications';
import { theme } from '@/styles/colors';
import { Head } from '@unhead/vue/components';
Expand Down Expand Up @@ -161,7 +161,7 @@ const i18n = useI18n();
const notificationsStore = useNotificationsStore();
const state = reactive({
isFetchingIncomes: false,
incomes: [] as IncomePeriodWithCharges<'week'>[],
incomes: [] as IncomePeriodWithTotal<'week'>[],
});
const totalIncome = computed(() =>
Expand Down
4 changes: 2 additions & 2 deletions src/views/Private/Stats/Incomes/StatsIncomesYearly.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ import { fractionAmount, fractionPercentage } from '@/helpers/currency';
import { DATE_FORMAT } from '@/helpers/dates';
import { handleSilentError } from '@/helpers/errors';
import { ROUTE_NAMES } from '@/router/names';
import { IncomePeriodWithCharges, getIncomesPerYear } from '@/services/api/incomes';
import { IncomePeriodWithTotal, getIncomesPerYear } from '@/services/api/incomes';
import { useNotificationsStore } from '@/store/notifications';
import { theme } from '@/styles/colors';
import { Head } from '@unhead/vue/components';
Expand Down Expand Up @@ -161,7 +161,7 @@ const i18n = useI18n();
const notificationsStore = useNotificationsStore();
const state = reactive({
isFetchingIncomes: false,
incomes: [] as IncomePeriodWithCharges<'year'>[],
incomes: [] as IncomePeriodWithTotal<'year'>[],
});
const totalIncome = computed(() =>
Expand Down

0 comments on commit 05a72b2

Please sign in to comment.