0.2.0 #4

Merged
mfnefd merged 15 commits from dev into main 2024-12-11 04:42:35 +04:00
10 changed files with 219 additions and 37 deletions
Showing only changes of commit 3c301ee41d - Show all commits

View File

@ -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']

View File

@ -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;

View File

@ -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'">

View File

@ -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;

View 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>

View File

@ -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>

View 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>

View File

@ -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",

View File

@ -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!);
}
})
}

View File

@ -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',