Skip to content

Commit

Permalink
feat: add total in spending year
Browse files Browse the repository at this point in the history
  • Loading branch information
jesusantguerrero committed Aug 2, 2024
1 parent 57efc4b commit 81260ab
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 55 deletions.
17 changes: 17 additions & 0 deletions app/Domains/Transaction/Services/ReportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ public static function generateExpensesByPeriod($teamId, $startDate, $timeUnitDi
}, $resultGroup)->sortBy('date');
}

public static function generateExpensesByPeriodInDate($teamId, $startDate, $endDate, $categories = null)
{
$rangeStartAt = Carbon::createFromFormat('Y-m-d', $startDate)->startOfMonth()->format('Y-m-d');
$rangeEndAt = Carbon::createFromFormat('Y-m-d', $endDate)->endOfMonth()->format('Y-m-d');

$results = self::getExpensesByCategoriesInPeriod($teamId, $rangeStartAt, $rangeEndAt, $categories);
$resultGroup = $results->groupBy('date');

return $resultGroup->map(function ($monthItems) {
return [
'date' => $monthItems->first()->date,
'data' => $monthItems->sortByDesc('total_amount')->values(),
'total' => $monthItems->sum('total_amount'),
];
}, $resultGroup)->sortBy('date');
}

public static function getAssignedByPeriod($teamId, $startDate, $timeUnitDiff = 2, $timeUnit = 'month', $categories = null)
{
$rangeEndAt = Carbon::createFromFormat('Y-m-d', $startDate)->endOfMonth()->format('Y-m-d');
Expand Down
10 changes: 8 additions & 2 deletions app/Http/Controllers/Finance/FinanceTrendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,19 @@ public function spendingYear()
$excludedAccounts = collect(explode(',', $filters['category']))->map(fn ($id) => "-$id")->all();
}

$endDate = Carbon::createFromFormat('Y-m-d', $startDate)->endOfYear()->format('Y-m-d');
$startDate = Carbon::createFromFormat('Y-m-d', $startDate)->startOfYear()->format('Y-m-d');

$monthlyExpensesInYear = ReportService::generateExpensesByPeriodInDate($teamId, $startDate, $endDate, $excludedAccounts);

return [
'data' => ReportService::generateExpensesByPeriod($teamId, $startDate, 12, 'month', $excludedAccounts),
'data' => $monthlyExpensesInYear,
'metaData' => [
'name' => 'spendingYear',
'title' => 'Expenses',
'title' => 'Expenses this year',
'props' => [
'headerTemplate' => 'grid',
'total' => $monthlyExpensesInYear->sum('total'),
],
],
];
Expand Down
19 changes: 19 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,31 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
IIonEllipsisVertical: typeof import('~icons/ion/ellipsis-vertical')['default']
IMaterialSymbolsBrightnessAlertOutlineRounded: typeof import('~icons/material-symbols/brightness-alert-outline-rounded')['default']
IMdiBankTransfer: typeof import('~icons/mdi/bank-transfer')['default']
IMdiBankTransferIn: typeof import('~icons/mdi/bank-transfer-in')['default']
IMdiBankTransferOut: typeof import('~icons/mdi/bank-transfer-out')['default']
IMdiBell: typeof import('~icons/mdi/bell')['default']
IMdiCallSplit: typeof import('~icons/mdi/call-split')['default']
IMdiCheck: typeof import('~icons/mdi/check')['default']
IMdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
IMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
IMdiClose: typeof import('~icons/mdi/close')['default']
IMdiEdit: typeof import('~icons/mdi/edit')['default']
IMdiEllipsisVertical: typeof import('~icons/mdi/ellipsis-vertical')['default']
IMdiEmailCheck: typeof import('~icons/mdi/email-check')['default']
IMdiExport: typeof import('~icons/mdi/export')['default']
IMdiFile: typeof import('~icons/mdi/file')['default']
IMdiFilter: typeof import('~icons/mdi/filter')['default']
IMdiHistory: typeof import('~icons/mdi/history')['default']
IMdiLink: typeof import('~icons/mdi/link')['default']
IMdiLock: typeof import('~icons/mdi/lock')['default']
IMdiMinus: typeof import('~icons/mdi/minus')['default']
IMdiMoney: typeof import('~icons/mdi/money')['default']
IMdiPlus: typeof import('~icons/mdi/plus')['default']
IMdiSearch: typeof import('~icons/mdi/search')['default']
IMdiSort: typeof import('~icons/mdi/sort')['default']
IMdiStar: typeof import('~icons/mdi/star')['default']
IMdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
IMdiSync: typeof import('~icons/mdi/sync')['default']
Expand Down
2 changes: 1 addition & 1 deletion resources/js/Components/molecules/WidgetTitleCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { AtButton } from "atmosphere-ui";
withDefaults(defineProps<{
title: string;
title?: string;
action?: {
label: string,
iconClass?: string,
Expand Down
69 changes: 25 additions & 44 deletions resources/js/Components/widgets/ChartComparison.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,29 @@ import { formatMonth } from "@/utils";
import formatMoney from "@/utils/formatMoney";
import WidgetTitleCard from "../molecules/WidgetTitleCard.vue";
const props = defineProps({
title: {
type: String
},
hideHeader: {
type: Boolean
},
headerTemplate: {
type: String,
default: 'row'
},
type: {
type: String,
default: "bar"
},
data: {
type: Object,
required: true
},
groupTotal: {
type: String,
default: "total"
},
dataItemTotal: {
type: String,
default: "total"
},
dataItemLabel: {
type: Function
},
hideDivider: {
type: Boolean
},
headerLabel: {
type: String,
},
headerTitleDate: {
default: true,
}
const props = withDefaults(defineProps<{
title: string,
hideHeader: boolean
headerTemplate: "row" | "grid",
type: string,
data: Record<string, any>,
groupTotal: string,
dataItemTotal: string,
dataItemLabel: Function
hideDivider: boolean
headerLabel: string
headerTitleDate: boolean
withPadding: boolean
}>(), {
headerTemplate: 'row',
type: "bar",
groupTotal: "total",
dataItemTotal: "total",
headerTitleDate: true,
withPadding: true
});
const emit = defineEmits(['click']);
const emit = defineEmits(['click', 'subitem-clicked', 'action']);
const selectedDate = ref(null)
const currentSeries = computed(() => {
Expand Down Expand Up @@ -110,7 +91,7 @@ const handleSelection = (index: number) => {
:hide-divider="hideDivider"
@action="$emit('action', $event)"
:border="false"
:with-padding="false"
:with-padding="withPadding"
>
<template #icon v-if="selectedDate">
<LogerButtonTab @click="selectedDate=null">
Expand All @@ -123,13 +104,13 @@ const handleSelection = (index: number) => {

<section class="w-full card-text" >
<div
:class="[headerTemplate == 'grid' ? 'md:grid md:grid-cols-4' : 'md:flex']"
class="w-full mb-2 divide-y comparison-header md:px-10 text-body-1/50 md:space-x-2 md:divide-x md:divide-y-0 divide-dashed divide-opacity-20 divide-body-1 bg-base-lvl-2">
:class="[headerTemplate == 'grid' ? 'md:grid md:grid-cols-4' : 'md:flex md:divide-x']"
class="w-full mb-2 divide-y comparison-header md:px-10 text-body-1/50 md:space-x-2 md:divide-y-0 divide-dashed divide-opacity-20 divide-body-1 bg-base-lvl-2">
<div
v-for="header in state.headers"
:key="header.id"
@click="selectedDate = header.id"
class="flex items-center justify-between w-full px-4 py-2 cursor-pointer comparison-header__item md:py-6 md:justify-center md:flex-col previous-period hover:text-body/80"
class="flex items-center justify-between w-full px-4 py-2 cursor-pointer comparison-header__item md:py-6 md:justify-center md:flex-col previous-period hover:text-body/80"
>
<h6 class="period-title">{{ header.label }}</h6>
<span class="mt-2 text-xs period-value">
Expand Down
170 changes: 170 additions & 0 deletions resources/js/Components/widgets/WidgetYearSpending.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<script setup lang="ts">
import { computed, ref, inject, watch } from "vue";
import LogerChart from "@/Components/organisms/LogerChart.vue";
import NumberHider from "@/Components/molecules/NumberHider.vue";
import LogerButtonTab from "@/Components/atoms/LogerButtonTab.vue";
import { formatMonth } from "@/utils";
import formatMoney from "@/utils/formatMoney";
import WidgetTitleCard from "../molecules/WidgetTitleCard.vue";
const props = withDefaults(defineProps<{
title: string;
hideHeader: boolean;
headerTemplate: "row" | "grid";
type: string;
data: Record<string, any>;
total: number;
groupTotal: string;
dataItemTotal: string;
dataItemLabel: Function;
hideDivider: boolean;
headerLabel: string;
headerTitleDate: boolean;
withPadding: boolean;
}>(), {
headerTemplate: 'row',
type: "bar",
groupTotal: "total",
dataItemTotal: "total",
headerTitleDate: true,
withPadding: true
});
const emit = defineEmits(['click', 'subitem-clicked', 'action']);
const selectedDate = ref(null)
const currentSeries = computed(() => {
const generalSeries = [{
name: 'Expenses',
data: Object.values(props.data).map(item => item[props.groupTotal]),
labels: Object.keys(props.data).map(month => formatMonth(month))
}]
const dateSeries = selectedDate.value ? [{
name: formatMonth(selectedDate.value),
data: props.data[selectedDate.value].data.map(item => {
return item[props.dataItemTotal]
}),
labels: props.data[selectedDate.value].data.map(item => {
return props.dataItemLabel ? props.dataItemLabel(item) : item.name
})
}] : []
return selectedDate.value ? dateSeries : generalSeries;
})
const hasHiddenValues = inject('hasHiddenValues', ref(false))
const state = computed(() => {
return {
headers: Object.entries(props.data).map(([dateString, item]) => ({
label: props.headerTitleDate ? formatMonth(dateString) : item[props.headerLabel ?? 'name'],
value: item[props.groupTotal],
id: dateString
})),
options: {
colors: ["#7B77D1", "#80CDFE"],
hasHiddenValues: hasHiddenValues.value,
},
series: currentSeries.value
}
});
watch(() => props.data, () => {
selectedDate.value = null
})
const handleSelection = (index: number) => {
if (selectedDate.value) {
const item = props.data[selectedDate.value].data[index];
emit('subitem-clicked', item, selectedDate.value)
}
}
</script>

<template>
<WidgetTitleCard
:title="title"
:action="action"
:hide-divider="hideDivider"
@action="$emit('action', $event)"
:border="false"
:with-padding="withPadding"
>
<template #icon v-if="selectedDate">
<LogerButtonTab @click="selectedDate=null">
<IMdiChevronLeft />
</LogerButtonTab>
</template>
<template #title>
<span>{{ title }}</span>
<span class="text-green-500 font-bold ml-4"> {{ formatMoney(total)}}</span>
</template>
<template #action v-if="selectedDate">
<span v-if="selectedDate" class="capitalize text-primary">{{ formatMonth(selectedDate) }}</span>
</template>

<section class="w-full card-text" >
<div
:class="[headerTemplate == 'grid' ? 'md:grid md:grid-cols-4' : 'md:flex md:divide-x']"
class="w-full mb-2 divide-y comparison-header md:px-10 text-body-1/50 md:space-x-2 md:divide-y-0 divide-dashed divide-opacity-20 divide-body-1 bg-base-lvl-2">
<div
v-for="header in state.headers"
:key="header.id"
@click="selectedDate = header.id"
class="flex items-center justify-between w-full px-4 py-2 cursor-pointer comparison-header__item md:py-6 md:justify-center md:flex-col previous-period hover:text-body/80"
>
<h6 class="period-title">{{ header.label }}</h6>
<span class="mt-2 text-xs period-value">
<NumberHider />
{{ formatMoney(header.value) }}
</span>
</div>
</div>
<LogerChart
class="bg-white"
style="height:300px; width: 100%"
label="name"
type="bar"
:labels="currentSeries[0].labels"
:options="state.options"
:series="state.series"
:has-hidden-values="hasHiddenValues"
@clicked="handleSelection"
/>
</section>
</WidgetTitleCard>
</template>


<style lang="scss" scoped>
.comparison-card {
border-radius: 0;
}
.comparison-header {
width: 100%;
&__item {
.period-value {
position: relative;
font-size: 18px;
font-weight: bolder;
&::before {
position: absolute;
left: -20px;
top: 25%;
content: "";
height: 10px;
width: 10px;
border-radius: 50%;
background: #8a00d4;
}
}
}
}
</style>
3 changes: 2 additions & 1 deletion resources/js/Pages/Trends/Overview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import ExpenseChartWidget from "@/domains/transactions/components/ExpenseChartWi
import { useServerSearch } from "@/composables/useServerSearch";
import AccountFilters from "@/domains/transactions/components/AccountFilters.vue";
import WidgetYearSpending from "@/Components/widgets/WidgetYearSpending.vue";
const props = defineProps({
user: {
Expand Down Expand Up @@ -62,7 +63,7 @@ const components = {
netWorth: ChartNetWorth,
incomeExpenses: IncomeExpenses,
incomeExpensesGraph: ChartNetWorth,
spendingYear: ChartComparison,
spendingYear: WidgetYearSpending,
assignedYear: ChartComparison,
}
Expand Down
Loading

0 comments on commit 81260ab

Please sign in to comment.