0.2.0 #4
4
front/components.d.ts
vendored
4
front/components.d.ts
vendored
@ -9,6 +9,7 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
@ -18,6 +19,7 @@ declare module 'vue' {
|
||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
@ -26,11 +28,13 @@ declare module 'vue' {
|
||||
ATypographyText: typeof import('ant-design-vue/es')['TypographyText']
|
||||
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
|
||||
ChangeRecordManager: typeof import('./src/components/support/ChangeRecordManager.vue')['default']
|
||||
GetPeriodReportForm: typeof import('./src/components/support/GetPeriodReportForm.vue')['default']
|
||||
Groups: typeof import('./src/components/pages/Groups.vue')['default']
|
||||
Header: typeof import('./src/components/main/Header.vue')['default']
|
||||
Home: typeof import('./src/components/pages/Home.vue')['default']
|
||||
Login: typeof import('./src/components/pages/Login.vue')['default']
|
||||
OffsetReport: typeof import('./src/components/pages/OffsetReport.vue')['default']
|
||||
PeriodReport: typeof import('./src/components/pages/PeriodReport.vue')['default']
|
||||
PlanManager: typeof import('./src/components/support/PlanManager.vue')['default']
|
||||
Plans: typeof import('./src/components/pages/Plans.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
@ -4,6 +4,7 @@ import { inject } from 'vue';
|
||||
import { GroupService } from '../../core/services/group-service';
|
||||
import SpendingGroupManager from '../support/SpendingGroupManager.vue';
|
||||
import { DeleteOutlined, CalendarOutlined } from '@ant-design/icons-vue';
|
||||
import GetPeriodReportForm from '../support/GetPeriodReportForm.vue';
|
||||
|
||||
const groupService = inject(GroupService.name) as GroupService;
|
||||
|
||||
|
@ -4,6 +4,7 @@ import ChangeRecordMenu from '../support/ChangeRecordManager.vue';
|
||||
import { ChangeRecordService } from '../../core/services/change-record-service';
|
||||
import { inject } from 'vue';
|
||||
import { DeleteOutlined } from '@ant-design/icons-vue';
|
||||
import GetPeriodReportForm from '../support/GetPeriodReportForm.vue';
|
||||
|
||||
const changeRecordService = inject(ChangeRecordService.name) as ChangeRecordService;
|
||||
|
||||
@ -49,7 +50,10 @@ const onDelete = (key: string) => {
|
||||
<template>
|
||||
<div class="base-page">
|
||||
<a-typography-title>История изменений баланса</a-typography-title>
|
||||
<a-divider />
|
||||
<ChangeRecordMenu :refreshData="refreshData" />
|
||||
<GetPeriodReportForm />
|
||||
<a-divider />
|
||||
<a-table :dataSource="state" :columns="columns" v-if="isReady" >
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'operation'">
|
||||
|
@ -10,33 +10,44 @@
|
||||
<Responsive class="w-full">
|
||||
<template #main="{ width }">
|
||||
<Chart
|
||||
direction="circular"
|
||||
:direction="'horizontal'"
|
||||
:data="reportData.data"
|
||||
:size="{ width, height: 400 }"
|
||||
:margin="{
|
||||
left: Math.round((width - 360)/2),
|
||||
left: 0,
|
||||
top: 20,
|
||||
right: 0,
|
||||
bottom: 20
|
||||
right: 20,
|
||||
bottom: 0
|
||||
}"
|
||||
:axis="{
|
||||
primary: {
|
||||
domain: ['dataMin', 'dataMax'],
|
||||
type: 'band',
|
||||
hide: true
|
||||
type: 'band'
|
||||
},
|
||||
secondary: {
|
||||
domain: ['dataMin', 'dataMax'],
|
||||
type: 'linear',
|
||||
hide: true
|
||||
domain: ['dataMin', 'dataMax + 20'],
|
||||
type: 'linear'
|
||||
}
|
||||
}"
|
||||
:config="{ controlHover: false }"
|
||||
>
|
||||
<template #layers>
|
||||
<Pie
|
||||
<Grid strokeDasharray="2,2" />
|
||||
<Area :dataKeys="['label', 'data']"
|
||||
type="monotone" :areaStyle="{ fill: 'url(#grad)' }"
|
||||
/>
|
||||
<Line
|
||||
:dataKeys="['label', 'data']"
|
||||
:pie-style="{ innerRadius: 100, padAngle: 0.05 }" />
|
||||
type="monotone"
|
||||
:lineStyle="{
|
||||
stroke: '#9f7aea'}"
|
||||
/>
|
||||
<Marker :value="reportData.planSum" label="План" color="#e76f51" :strokeWidth="2" strokeDasharray="6 6" />
|
||||
<defs>
|
||||
<linearGradient id="grad" gradientTransform="rotate(90)">
|
||||
<stop offset="0%" stop-color="#be90ff" stop-opacity="1" />
|
||||
<stop offset="100%" stop-color="white" stop-opacity="0.4" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</template>
|
||||
<template #widgets>
|
||||
<Tooltip
|
||||
@ -44,7 +55,6 @@
|
||||
label: { label: 'Дата' },
|
||||
data: { label: 'Затраты' },
|
||||
}"
|
||||
hideLine
|
||||
/>
|
||||
</template>
|
||||
</Chart>
|
||||
@ -79,7 +89,7 @@ import { inject } from 'vue';
|
||||
import { OffsetFromPlanReportData, ReportService } from '../../core/services/report-service';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
import { Chart, Tooltip, Pie, Responsive } from 'vue3-charts';
|
||||
import { Chart, Grid, Tooltip, Line, Pie, Marker, Responsive, Area } from 'vue3-charts';
|
||||
import { CheckOutlined, WarningOutlined } from '@ant-design/icons-vue';
|
||||
let reportService = inject(ReportService.name) as ReportService;
|
||||
let planId = useRoute().params.planId as string;
|
||||
|
82
front/src/components/pages/PeriodReport.vue
Normal file
82
front/src/components/pages/PeriodReport.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<style scoped>
|
||||
</style>
|
||||
<template>
|
||||
<div class="base-page">
|
||||
<a-typography-title>Отчет за период</a-typography-title>
|
||||
<div v-if="isReady">
|
||||
<a-typography-title :level="4">
|
||||
с {{ dayjs(startAt).format("DD.MM.YYYY") }} по {{ dayjs(endAt).format("DD.MM.YYYY") }}
|
||||
</a-typography-title>
|
||||
<Responsive class="w-full">
|
||||
<template #main="{ width }">
|
||||
<Chart
|
||||
direction="circular"
|
||||
:data="reportData.data"
|
||||
:size="{ width, height: 400 }"
|
||||
:margin="{
|
||||
left: Math.round((width - 360)/2),
|
||||
top: 20,
|
||||
right: 0,
|
||||
bottom: 20
|
||||
}"
|
||||
:axis="{
|
||||
primary: {
|
||||
domain: ['dataMin', 'dataMax'],
|
||||
type: 'band',
|
||||
hide: true
|
||||
},
|
||||
secondary: {
|
||||
domain: ['dataMin', 'dataMax'],
|
||||
type: 'linear',
|
||||
hide: true
|
||||
}
|
||||
}"
|
||||
:config="{ controlHover: false }"
|
||||
>
|
||||
<template #layers>
|
||||
<Pie
|
||||
:dataKeys="['label', 'data']"
|
||||
:pie-style="{ innerRadius: 100, padAngle: 0.05 }" />
|
||||
</template>
|
||||
<template #widgets>
|
||||
<Tooltip
|
||||
:config="{
|
||||
label: { label: 'Группа' },
|
||||
data: { label: 'Затраты' },
|
||||
}"
|
||||
hideLine
|
||||
/>
|
||||
</template>
|
||||
</Chart>
|
||||
</template>
|
||||
</Responsive>
|
||||
<a-typography-title :level="4">
|
||||
<MonitorOutlined /> Сумма всех затрат за этот период: {{ reportData.total }}
|
||||
</a-typography-title>
|
||||
<br/>
|
||||
<a-table
|
||||
:dataSource="reportData.data"
|
||||
:columns="[
|
||||
{ title: 'Группа затрат', dataIndex: 'label', key: 'label' },
|
||||
{ title: 'Сумма', dataIndex: 'data', key: 'data' },
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue';
|
||||
import { PeriodReportData, ReportService } from '../../core/services/report-service';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
import { Grid, Pie, Chart, Tooltip, Bar, Responsive, Marker, Line } from 'vue3-charts';
|
||||
import { MonitorOutlined, DollarOutlined } from '@ant-design/icons-vue';
|
||||
import dayjs from 'dayjs';
|
||||
let reportService = inject(ReportService.name) as ReportService;
|
||||
let { startAt, endAt } = useRoute().query as { startAt: string, endAt: string };
|
||||
const { state : reportData, isReady } = useAsyncState(() => reportService.getPeriodData(dayjs(startAt), dayjs(endAt)), {} as PeriodReportData);
|
||||
|
||||
</script>
|
@ -4,7 +4,7 @@ import { inject } from 'vue';
|
||||
import { PlanService } from '../../core/services/plans-service';
|
||||
import { useRoute } from 'vue-router';
|
||||
import PlanManager from '../support/PlanManager.vue';
|
||||
import { DeleteOutlined, PieChartOutlined } from '@ant-design/icons-vue';
|
||||
import { DeleteOutlined, BarChartOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const planService = inject(PlanService.name) as PlanService;
|
||||
const groupId = useRoute().params.groupId as string;
|
||||
@ -72,7 +72,7 @@ const onDelete = (key: string) => {
|
||||
params: { planId: record.id }
|
||||
}"
|
||||
>
|
||||
<PieChartOutlined /> Отчет
|
||||
<BarChartOutlined /> Отчет
|
||||
</router-link>
|
||||
</template>
|
||||
</template>
|
||||
|
36
front/src/components/support/GetPeriodReportForm.vue
Normal file
36
front/src/components/support/GetPeriodReportForm.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
|
||||
<a-space>
|
||||
|
||||
<a-range-picker v-model:value="dateRange" />
|
||||
<a-button type="primary"
|
||||
:disabled="!dateRange"
|
||||
@click="onFinish"
|
||||
:loading="isClicked"
|
||||
>
|
||||
<PieChartOutlined />Получить отчет
|
||||
</a-button>
|
||||
|
||||
</a-space>
|
||||
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { ref } from 'vue';
|
||||
import { PieChartOutlined } from '@ant-design/icons-vue';
|
||||
import router from '../../router';
|
||||
|
||||
const dateRange = ref<[Dayjs, Dayjs]>();
|
||||
const isClicked = ref(false);
|
||||
const onFinish = () => {
|
||||
isClicked.value = true;
|
||||
console.log(dateRange.value);
|
||||
router.push({
|
||||
name: 'periodReport',
|
||||
query: {
|
||||
startAt: dateRange.value![0].toISOString(),
|
||||
endAt: dateRange.value![1].toISOString()
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
@ -170,10 +170,11 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
*
|
||||
* @tags Report
|
||||
* @name ReportPeriodList
|
||||
* @request GET:/api/Report/period
|
||||
* @request GET:/api/Report/period/{id}
|
||||
* @response `200` `(ChangeRecordViewModel)[]` Success
|
||||
*/
|
||||
reportPeriodList = (
|
||||
id: string,
|
||||
query?: {
|
||||
/** @format date-time */
|
||||
from?: string;
|
||||
@ -183,7 +184,7 @@ export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
this.request<ChangeRecordViewModel[], any>({
|
||||
path: `/api/Report/period`,
|
||||
path: `/api/Report/period/${id}`,
|
||||
method: "GET",
|
||||
query: query,
|
||||
format: "json",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Dayjs } from "dayjs";
|
||||
import { Api } from "../api/Api";
|
||||
import { SpendingGroupViewModel } from "../api/data-contracts";
|
||||
import { ChangeRecordViewModel, SpendingGroupViewModel } from "../api/data-contracts";
|
||||
import { useUserStore } from "../../store";
|
||||
|
||||
export type ReportData = {
|
||||
label: string;
|
||||
@ -15,6 +16,11 @@ export interface OffsetFromPlanReportData {
|
||||
data: ReportData[];
|
||||
}
|
||||
|
||||
export interface PeriodReportData {
|
||||
data: ReportData[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export class ReportService {
|
||||
private readonly _api: Api
|
||||
constructor(api: Api) {
|
||||
@ -30,36 +36,69 @@ export class ReportService {
|
||||
return reportData;
|
||||
}
|
||||
|
||||
public async getPeriodData(from: Dayjs, to: Dayjs): Promise<SpendingGroupViewModel[]> {
|
||||
let res = await this._api.reportPeriodList({
|
||||
public async getPeriodData(from: Dayjs, to: Dayjs): Promise<PeriodReportData> {
|
||||
let userId = useUserStore().user.id;
|
||||
if (!userId) throw new Error("Id пользователя не найден");
|
||||
let res = await this._api.reportPeriodList(userId, {
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString()
|
||||
});
|
||||
console.log(res);
|
||||
return res.data;
|
||||
let reportData = getPeriodData(res.data);
|
||||
if (!reportData) throw new Error("Cannot get report data");
|
||||
console.log(reportData);
|
||||
return reportData;
|
||||
}
|
||||
}
|
||||
|
||||
function getOffsetFromPlanData(group: SpendingGroupViewModel): OffsetFromPlanReportData | null {
|
||||
if (!group.changeRecords || !group.spendingPlans) return null;
|
||||
let reportData : ReportData[] = [];
|
||||
group.changeRecords.forEach((cr) => {
|
||||
if (!reportData.find(x => x.label == cr.changedAt!)) {
|
||||
reportData.push({
|
||||
label: cr.changedAt!,
|
||||
data: Math.abs(cr.sum!)
|
||||
})
|
||||
}
|
||||
else {
|
||||
(reportData.find(x => x.label == cr.changedAt)!.data as number) += Math.abs(cr.sum!);
|
||||
}
|
||||
})
|
||||
getReportDataFromGroup(group, reportData,
|
||||
(cr) => reportData.push({
|
||||
label: cr.changedAt!,
|
||||
data: Math.abs(cr.sum!)
|
||||
})
|
||||
);
|
||||
if (reportData.length == 0) return null;
|
||||
return {
|
||||
groupName: group.name!,
|
||||
startAt: group.spendingPlans[0].startAt!,
|
||||
endAt: group.spendingPlans[0].endAt!,
|
||||
planSum: group.spendingPlans[0].sum!,
|
||||
startAt: group.spendingPlans![0].startAt!,
|
||||
endAt: group.spendingPlans![0].endAt!,
|
||||
planSum: group.spendingPlans![0].sum!,
|
||||
total: reportData.map(x => x.data as number).reduce((a, b) => a + b, 0),
|
||||
data: reportData
|
||||
};
|
||||
}
|
||||
|
||||
function getPeriodData(records: ChangeRecordViewModel[]): PeriodReportData | null {
|
||||
let reportData : ReportData[] = [];
|
||||
getReportDataFromRecords(records, reportData,
|
||||
(cr) => reportData.push({
|
||||
label: cr.spendingGroupName!,
|
||||
data: Math.abs(cr.sum!)
|
||||
}),
|
||||
(x, y) => x.label == y.spendingGroupName
|
||||
);
|
||||
return {
|
||||
data: reportData,
|
||||
total: reportData.map(x => x.data as number).reduce((a, b) => a + b, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function getReportDataFromGroup(group: SpendingGroupViewModel, reportData: ReportData[], callbackPush: (changeRecor: ChangeRecordViewModel) => void) {
|
||||
if (!group.changeRecords) return;
|
||||
getReportDataFromRecords(group.changeRecords, reportData, callbackPush, (x, y) => x.label == y.changedAt);
|
||||
}
|
||||
|
||||
function getReportDataFromRecords(records: ChangeRecordViewModel[], reportData: ReportData[],
|
||||
callbackPush: (changeRecor: ChangeRecordViewModel) => void,
|
||||
callbackFind: (x: ReportData, y: ChangeRecordViewModel) => boolean) {
|
||||
records.forEach((cr) => {
|
||||
if (!reportData.find(x => callbackFind(x, cr))) {
|
||||
callbackPush(cr);
|
||||
}
|
||||
else {
|
||||
(reportData.find(x => callbackFind(x, cr))!.data as number) += Math.abs(cr.sum!);
|
||||
}
|
||||
})
|
||||
}
|
@ -26,6 +26,11 @@ const router = createRouter({
|
||||
name: 'groups',
|
||||
component: () => import('./components/pages/Groups.vue'),
|
||||
},
|
||||
{
|
||||
path: '/groups/report/period',
|
||||
name: 'periodReport',
|
||||
component: () => import('./components/pages/PeriodReport.vue'),
|
||||
},
|
||||
{
|
||||
path: '/plans/:groupId',
|
||||
name: 'plans',
|
||||
|
Loading…
Reference in New Issue
Block a user