Add global multi-account filter to all reports with chart hover interaction
Move account selector to date range bar and apply filtering to all four report types. Add bidirectional hover highlighting between spending donut chart and category table. Fix donut data label readability with white text. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,23 +2,27 @@ import api from './api'
|
|||||||
import type { SpendingByCategoryReport, IncomeVsExpenseReport, NetWorthReport, CashFlowReport } from '@/types'
|
import type { SpendingByCategoryReport, IncomeVsExpenseReport, NetWorthReport, CashFlowReport } from '@/types'
|
||||||
|
|
||||||
export const reportsApi = {
|
export const reportsApi = {
|
||||||
spendingByCategory: (startDate: string, endDate: string, accountId?: string) =>
|
spendingByCategory: (startDate: string, endDate: string, accountIds?: string[]) =>
|
||||||
api.get<SpendingByCategoryReport>('/reports/spending-by-category', {
|
api.get<SpendingByCategoryReport>('/reports/spending-by-category', {
|
||||||
params: { startDate, endDate, accountId },
|
params: { startDate, endDate, accountIds },
|
||||||
|
paramsSerializer: { indexes: null },
|
||||||
}),
|
}),
|
||||||
|
|
||||||
incomeVsExpense: (startDate: string, endDate: string) =>
|
incomeVsExpense: (startDate: string, endDate: string, accountIds?: string[]) =>
|
||||||
api.get<IncomeVsExpenseReport>('/reports/income-vs-expense', {
|
api.get<IncomeVsExpenseReport>('/reports/income-vs-expense', {
|
||||||
params: { startDate, endDate },
|
params: { startDate, endDate, accountIds },
|
||||||
|
paramsSerializer: { indexes: null },
|
||||||
}),
|
}),
|
||||||
|
|
||||||
netWorth: (startDate: string, endDate: string) =>
|
netWorth: (startDate: string, endDate: string, accountIds?: string[]) =>
|
||||||
api.get<NetWorthReport>('/reports/net-worth', {
|
api.get<NetWorthReport>('/reports/net-worth', {
|
||||||
params: { startDate, endDate },
|
params: { startDate, endDate, accountIds },
|
||||||
|
paramsSerializer: { indexes: null },
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cashFlow: (startDate: string, endDate: string) =>
|
cashFlow: (startDate: string, endDate: string, accountIds?: string[]) =>
|
||||||
api.get<CashFlowReport>('/reports/cash-flow', {
|
api.get<CashFlowReport>('/reports/cash-flow', {
|
||||||
params: { startDate, endDate },
|
params: { startDate, endDate, accountIds },
|
||||||
|
paramsSerializer: { indexes: null },
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,22 @@
|
|||||||
style="max-width: 180px"
|
style="max-width: 180px"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
<v-spacer />
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedAccountIds"
|
||||||
|
:items="accountsStore.accounts"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
label="Accounts"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
clearable
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
style="min-width: 250px; max-width: 350px"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -56,21 +72,7 @@
|
|||||||
<v-window v-model="activeTab">
|
<v-window v-model="activeTab">
|
||||||
<v-window-item value="spending">
|
<v-window-item value="spending">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="d-flex align-center">
|
<v-card-title>Spending by Category</v-card-title>
|
||||||
<span>Spending by Category</span>
|
|
||||||
<v-spacer />
|
|
||||||
<v-select
|
|
||||||
v-model="selectedAccountId"
|
|
||||||
:items="accountOptions"
|
|
||||||
item-title="name"
|
|
||||||
item-value="id"
|
|
||||||
label="Account"
|
|
||||||
density="compact"
|
|
||||||
hide-details
|
|
||||||
clearable
|
|
||||||
style="max-width: 250px"
|
|
||||||
/>
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text v-if="spending">
|
<v-card-text v-if="spending">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="5">
|
<v-col cols="12" md="5">
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
<p class="text-medium-emphasis mb-4">Total spending</p>
|
<p class="text-medium-emphasis mb-4">Total spending</p>
|
||||||
<apexchart
|
<apexchart
|
||||||
v-if="spending.categories.length > 0"
|
v-if="spending.categories.length > 0"
|
||||||
|
ref="spendingChartRef"
|
||||||
type="donut"
|
type="donut"
|
||||||
height="300"
|
height="300"
|
||||||
:options="spendingChartOptions"
|
:options="spendingChartOptions"
|
||||||
@@ -93,6 +96,7 @@
|
|||||||
:items-per-page="-1"
|
:items-per-page="-1"
|
||||||
hide-default-footer
|
hide-default-footer
|
||||||
:sort-by="[{ key: 'amount', order: 'desc' }]"
|
:sort-by="[{ key: 'amount', order: 'desc' }]"
|
||||||
|
:row-props="spendingRowProps"
|
||||||
>
|
>
|
||||||
<template #item.amount="{ item }">{{ formatCurrency(item.amount) }}</template>
|
<template #item.amount="{ item }">{{ formatCurrency(item.amount) }}</template>
|
||||||
<template #item.percentage="{ item }">{{ item.percentage.toFixed(1) }}%</template>
|
<template #item.percentage="{ item }">{{ item.percentage.toFixed(1) }}%</template>
|
||||||
@@ -266,19 +270,20 @@ const activeTab = ref('spending')
|
|||||||
const preset = ref('thisMonth')
|
const preset = ref('thisMonth')
|
||||||
const customStart = ref('')
|
const customStart = ref('')
|
||||||
const customEnd = ref('')
|
const customEnd = ref('')
|
||||||
const selectedAccountId = ref<string | undefined>(undefined)
|
const selectedAccountIds = ref<string[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const snackbar = ref(false)
|
const snackbar = ref(false)
|
||||||
const snackbarText = ref('')
|
const snackbarText = ref('')
|
||||||
|
|
||||||
|
const spendingChartRef = ref()
|
||||||
|
const highlightedCategory = ref<string | null>(null)
|
||||||
|
let selectedChartIndex = -1
|
||||||
|
|
||||||
const spending = ref<SpendingByCategoryReport | null>(null)
|
const spending = ref<SpendingByCategoryReport | null>(null)
|
||||||
const incomeExpense = ref<IncomeVsExpenseReport | null>(null)
|
const incomeExpense = ref<IncomeVsExpenseReport | null>(null)
|
||||||
const netWorth = ref<NetWorthReport | null>(null)
|
const netWorth = ref<NetWorthReport | null>(null)
|
||||||
const cashFlow = ref<CashFlowReport | null>(null)
|
const cashFlow = ref<CashFlowReport | null>(null)
|
||||||
|
|
||||||
const accountOptions = computed(() =>
|
|
||||||
[{ id: undefined as string | undefined, name: 'All Accounts' }, ...accountsStore.accounts.map(a => ({ id: a.id, name: a.name }))]
|
|
||||||
)
|
|
||||||
|
|
||||||
const dateRange = computed(() => {
|
const dateRange = computed(() => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -332,10 +337,21 @@ const spendingChartSeries = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const spendingChartOptions = computed(() => ({
|
const spendingChartOptions = computed(() => ({
|
||||||
chart: { type: 'donut' as const },
|
chart: {
|
||||||
|
type: 'donut' as const,
|
||||||
|
events: {
|
||||||
|
dataPointMouseEnter: (_e: any, _ctx: any, config: any) => {
|
||||||
|
const labels = spending.value?.categories.map(c => c.categoryName) ?? []
|
||||||
|
highlightedCategory.value = labels[config.dataPointIndex] ?? null
|
||||||
|
},
|
||||||
|
dataPointMouseLeave: () => {
|
||||||
|
highlightedCategory.value = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
labels: spending.value?.categories.map(c => c.categoryName) ?? [],
|
labels: spending.value?.categories.map(c => c.categoryName) ?? [],
|
||||||
legend: { position: 'bottom' as const, labels: { colors: labelColor.value } },
|
legend: { position: 'bottom' as const, labels: { colors: labelColor.value } },
|
||||||
dataLabels: { style: { colors: undefined }, dropShadow: { enabled: false } },
|
dataLabels: { style: { colors: ['#FFFFFF'] }, dropShadow: { enabled: true, top: 1, left: 1, blur: 2, opacity: 0.5 } },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
y: {
|
y: {
|
||||||
formatter: (val: number) => formatCurrency(val),
|
formatter: (val: number) => formatCurrency(val),
|
||||||
@@ -343,6 +359,37 @@ const spendingChartOptions = computed(() => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
function highlightCategory(categoryName: string) {
|
||||||
|
highlightedCategory.value = categoryName
|
||||||
|
const chart = spendingChartRef.value?.chart
|
||||||
|
if (!chart) return
|
||||||
|
const labels = spending.value?.categories.map(c => c.categoryName) ?? []
|
||||||
|
const idx = labels.indexOf(categoryName)
|
||||||
|
if (idx === -1) return
|
||||||
|
if (selectedChartIndex >= 0 && selectedChartIndex !== idx) {
|
||||||
|
chart.toggleDataPointSelection(0, selectedChartIndex)
|
||||||
|
}
|
||||||
|
if (selectedChartIndex !== idx) {
|
||||||
|
chart.toggleDataPointSelection(0, idx)
|
||||||
|
selectedChartIndex = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhighlightCategory() {
|
||||||
|
highlightedCategory.value = null
|
||||||
|
const chart = spendingChartRef.value?.chart
|
||||||
|
if (chart && selectedChartIndex >= 0) {
|
||||||
|
chart.toggleDataPointSelection(0, selectedChartIndex)
|
||||||
|
selectedChartIndex = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const spendingRowProps = ({ item }: any) => ({
|
||||||
|
class: highlightedCategory.value === item.categoryName ? 'spending-row-highlight' : '',
|
||||||
|
onMouseenter: () => highlightCategory(item.categoryName),
|
||||||
|
onMouseleave: () => unhighlightCategory(),
|
||||||
|
})
|
||||||
|
|
||||||
// --- Income vs Expense ---
|
// --- Income vs Expense ---
|
||||||
const incomeHeaders = [
|
const incomeHeaders = [
|
||||||
{ title: 'Month', key: 'label' },
|
{ title: 'Month', key: 'label' },
|
||||||
@@ -488,11 +535,12 @@ async function loadReports() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
const { startDate, endDate } = dateRange.value
|
const { startDate, endDate } = dateRange.value
|
||||||
|
|
||||||
|
const accts = selectedAccountIds.value.length ? selectedAccountIds.value : undefined
|
||||||
const [s, ie, nw, cf] = await Promise.all([
|
const [s, ie, nw, cf] = await Promise.all([
|
||||||
fetchReport(reportsApi.spendingByCategory(startDate, endDate, selectedAccountId.value)),
|
fetchReport(reportsApi.spendingByCategory(startDate, endDate, accts)),
|
||||||
fetchReport(reportsApi.incomeVsExpense(startDate, endDate)),
|
fetchReport(reportsApi.incomeVsExpense(startDate, endDate, accts)),
|
||||||
fetchReport(reportsApi.netWorth(startDate, endDate)),
|
fetchReport(reportsApi.netWorth(startDate, endDate, accts)),
|
||||||
fetchReport(reportsApi.cashFlow(startDate, endDate)),
|
fetchReport(reportsApi.cashFlow(startDate, endDate, accts)),
|
||||||
])
|
])
|
||||||
|
|
||||||
spending.value = s
|
spending.value = s
|
||||||
@@ -502,18 +550,13 @@ async function loadReports() {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSpending() {
|
|
||||||
const { startDate, endDate } = dateRange.value
|
|
||||||
spending.value = await fetchReport(reportsApi.spendingByCategory(startDate, endDate, selectedAccountId.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(text: string) {
|
function showError(text: string) {
|
||||||
snackbarText.value = text
|
snackbarText.value = text
|
||||||
snackbar.value = true
|
snackbar.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(dateRange, () => loadReports())
|
watch(dateRange, () => loadReports())
|
||||||
watch(selectedAccountId, () => loadSpending())
|
watch(selectedAccountIds, () => loadReports(), { deep: true })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (accountsStore.accounts.length === 0) {
|
if (accountsStore.accounts.length === 0) {
|
||||||
@@ -522,3 +565,10 @@ onMounted(async () => {
|
|||||||
await loadReports()
|
await loadReports()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.spending-row-highlight) {
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.15);
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -17,20 +17,20 @@ public class ReportsController : ControllerBase
|
|||||||
public ReportsController(IReportService reportService) => _reportService = reportService;
|
public ReportsController(IReportService reportService) => _reportService = reportService;
|
||||||
|
|
||||||
[HttpGet("spending-by-category")]
|
[HttpGet("spending-by-category")]
|
||||||
public async Task<ActionResult<SpendingByCategoryReport>> SpendingByCategory([FromQuery] DateTime startDate, [FromQuery] DateTime endDate, [FromQuery] Guid? accountId)
|
public async Task<ActionResult<SpendingByCategoryReport>> SpendingByCategory([FromQuery] DateTime startDate, [FromQuery] DateTime endDate, [FromQuery] Guid[]? accountIds)
|
||||||
=> Ok(await _reportService.GetSpendingByCategoryAsync(UserId, Utc(startDate), Utc(endDate), accountId));
|
=> Ok(await _reportService.GetSpendingByCategoryAsync(UserId, Utc(startDate), Utc(endDate), accountIds));
|
||||||
|
|
||||||
[HttpGet("income-vs-expense")]
|
[HttpGet("income-vs-expense")]
|
||||||
public async Task<ActionResult<IncomeVsExpenseReport>> IncomeVsExpense([FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
|
public async Task<ActionResult<IncomeVsExpenseReport>> IncomeVsExpense([FromQuery] DateTime startDate, [FromQuery] DateTime endDate, [FromQuery] Guid[]? accountIds)
|
||||||
=> Ok(await _reportService.GetIncomeVsExpenseAsync(UserId, Utc(startDate), Utc(endDate)));
|
=> Ok(await _reportService.GetIncomeVsExpenseAsync(UserId, Utc(startDate), Utc(endDate), accountIds));
|
||||||
|
|
||||||
[HttpGet("net-worth")]
|
[HttpGet("net-worth")]
|
||||||
public async Task<ActionResult<NetWorthReport>> NetWorth([FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
|
public async Task<ActionResult<NetWorthReport>> NetWorth([FromQuery] DateTime startDate, [FromQuery] DateTime endDate, [FromQuery] Guid[]? accountIds)
|
||||||
=> Ok(await _reportService.GetNetWorthAsync(UserId, Utc(startDate), Utc(endDate)));
|
=> Ok(await _reportService.GetNetWorthAsync(UserId, Utc(startDate), Utc(endDate), accountIds));
|
||||||
|
|
||||||
[HttpGet("cash-flow")]
|
[HttpGet("cash-flow")]
|
||||||
public async Task<ActionResult<CashFlowReport>> CashFlow([FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
|
public async Task<ActionResult<CashFlowReport>> CashFlow([FromQuery] DateTime startDate, [FromQuery] DateTime endDate, [FromQuery] Guid[]? accountIds)
|
||||||
=> Ok(await _reportService.GetCashFlowAsync(UserId, Utc(startDate), Utc(endDate)));
|
=> Ok(await _reportService.GetCashFlowAsync(UserId, Utc(startDate), Utc(endDate), accountIds));
|
||||||
|
|
||||||
private static DateTime Utc(DateTime dt) =>
|
private static DateTime Utc(DateTime dt) =>
|
||||||
dt.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) : dt;
|
dt.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(dt, DateTimeKind.Utc) : dt;
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ public class ReportService : IReportService
|
|||||||
|
|
||||||
public ReportService(PurrseDbContext db) => _db = db;
|
public ReportService(PurrseDbContext db) => _db = db;
|
||||||
|
|
||||||
public async Task<SpendingByCategoryReport> GetSpendingByCategoryAsync(Guid userId, DateTime startDate, DateTime endDate, Guid? accountId = null)
|
public async Task<SpendingByCategoryReport> GetSpendingByCategoryAsync(Guid userId, DateTime startDate, DateTime endDate, Guid[]? accountIds = null)
|
||||||
{
|
{
|
||||||
var query = _db.Transactions
|
var query = _db.Transactions
|
||||||
.Include(t => t.Category).ThenInclude(c => c!.Parent)
|
.Include(t => t.Category).ThenInclude(c => c!.Parent)
|
||||||
.Where(t => t.Account.UserId == userId && t.Date >= startDate && t.Date <= endDate
|
.Where(t => t.Account.UserId == userId && t.Date >= startDate && t.Date <= endDate
|
||||||
&& t.Amount < 0 && !t.IsVoid && t.CategoryId.HasValue);
|
&& t.Amount < 0 && !t.IsVoid && t.CategoryId.HasValue);
|
||||||
|
|
||||||
if (accountId.HasValue)
|
if (accountIds is { Length: > 0 })
|
||||||
query = query.Where(t => t.AccountId == accountId.Value);
|
query = query.Where(t => accountIds.Contains(t.AccountId));
|
||||||
|
|
||||||
var groups = await query
|
var groups = await query
|
||||||
.GroupBy(t => t.CategoryId)
|
.GroupBy(t => t.CategoryId)
|
||||||
@@ -45,11 +45,13 @@ public class ReportService : IReportService
|
|||||||
return new SpendingByCategoryReport(startDate, endDate, total, categories);
|
return new SpendingByCategoryReport(startDate, endDate, total, categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IncomeVsExpenseReport> GetIncomeVsExpenseAsync(Guid userId, DateTime startDate, DateTime endDate)
|
public async Task<IncomeVsExpenseReport> GetIncomeVsExpenseAsync(Guid userId, DateTime startDate, DateTime endDate, Guid[]? accountIds = null)
|
||||||
{
|
{
|
||||||
var transactions = await _db.Transactions
|
var query = _db.Transactions
|
||||||
.Where(t => t.Account.UserId == userId && t.Date >= startDate && t.Date <= endDate && !t.IsVoid && t.Type != TransactionType.Transfer)
|
.Where(t => t.Account.UserId == userId && t.Date >= startDate && t.Date <= endDate && !t.IsVoid && t.Type != TransactionType.Transfer);
|
||||||
.ToListAsync();
|
if (accountIds is { Length: > 0 })
|
||||||
|
query = query.Where(t => accountIds.Contains(t.AccountId));
|
||||||
|
var transactions = await query.ToListAsync();
|
||||||
|
|
||||||
var months = transactions
|
var months = transactions
|
||||||
.GroupBy(t => new { t.Date.Year, t.Date.Month })
|
.GroupBy(t => new { t.Date.Year, t.Date.Month })
|
||||||
@@ -65,15 +67,18 @@ public class ReportService : IReportService
|
|||||||
return new IncomeVsExpenseReport(startDate, endDate, months);
|
return new IncomeVsExpenseReport(startDate, endDate, months);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NetWorthReport> GetNetWorthAsync(Guid userId, DateTime startDate, DateTime endDate)
|
public async Task<NetWorthReport> GetNetWorthAsync(Guid userId, DateTime startDate, DateTime endDate, Guid[]? accountIds = null)
|
||||||
{
|
{
|
||||||
var accounts = await _db.Accounts.Where(a => a.UserId == userId && a.IsActive).ToListAsync();
|
var accounts = await _db.Accounts.Where(a => a.UserId == userId && a.IsActive).ToListAsync();
|
||||||
|
if (accountIds is { Length: > 0 })
|
||||||
|
accounts = accounts.Where(a => accountIds.Contains(a.Id)).ToList();
|
||||||
var assetTypes = new[] { AccountType.Checking, AccountType.Savings, AccountType.Cash, AccountType.Brokerage, AccountType.Retirement401k, AccountType.IRA };
|
var assetTypes = new[] { AccountType.Checking, AccountType.Savings, AccountType.Cash, AccountType.Brokerage, AccountType.Retirement401k, AccountType.IRA };
|
||||||
|
|
||||||
var transactions = await _db.Transactions
|
var txQuery = _db.Transactions
|
||||||
.Where(t => t.Account.UserId == userId && t.Date >= startDate && t.Date <= endDate && !t.IsVoid)
|
.Where(t => t.Account.UserId == userId && t.Date >= startDate && t.Date <= endDate && !t.IsVoid);
|
||||||
.OrderBy(t => t.Date)
|
if (accountIds is { Length: > 0 })
|
||||||
.ToListAsync();
|
txQuery = txQuery.Where(t => accountIds.Contains(t.AccountId));
|
||||||
|
var transactions = await txQuery.OrderBy(t => t.Date).ToListAsync();
|
||||||
|
|
||||||
var entries = new List<NetWorthEntry>();
|
var entries = new List<NetWorthEntry>();
|
||||||
var currentDate = startDate;
|
var currentDate = startDate;
|
||||||
@@ -94,12 +99,13 @@ public class ReportService : IReportService
|
|||||||
return new NetWorthReport(entries);
|
return new NetWorthReport(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CashFlowReport> GetCashFlowAsync(Guid userId, DateTime startDate, DateTime endDate)
|
public async Task<CashFlowReport> GetCashFlowAsync(Guid userId, DateTime startDate, DateTime endDate, Guid[]? accountIds = null)
|
||||||
{
|
{
|
||||||
var transactions = await _db.Transactions
|
var query = _db.Transactions
|
||||||
.Where(t => t.Account.UserId == userId && t.Date >= startDate && t.Date <= endDate && !t.IsVoid && t.Type != TransactionType.Transfer)
|
.Where(t => t.Account.UserId == userId && t.Date >= startDate && t.Date <= endDate && !t.IsVoid && t.Type != TransactionType.Transfer);
|
||||||
.OrderBy(t => t.Date)
|
if (accountIds is { Length: > 0 })
|
||||||
.ToListAsync();
|
query = query.Where(t => accountIds.Contains(t.AccountId));
|
||||||
|
var transactions = await query.OrderBy(t => t.Date).ToListAsync();
|
||||||
|
|
||||||
var groups = transactions.GroupBy(t => t.Date.Date)
|
var groups = transactions.GroupBy(t => t.Date.Date)
|
||||||
.Select(g =>
|
.Select(g =>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ namespace Purrse.Core.Interfaces.Services;
|
|||||||
|
|
||||||
public interface IReportService
|
public interface IReportService
|
||||||
{
|
{
|
||||||
Task<SpendingByCategoryReport> GetSpendingByCategoryAsync(Guid userId, DateTime startDate, DateTime endDate, Guid? accountId = null);
|
Task<SpendingByCategoryReport> GetSpendingByCategoryAsync(Guid userId, DateTime startDate, DateTime endDate, Guid[]? accountIds = null);
|
||||||
Task<IncomeVsExpenseReport> GetIncomeVsExpenseAsync(Guid userId, DateTime startDate, DateTime endDate);
|
Task<IncomeVsExpenseReport> GetIncomeVsExpenseAsync(Guid userId, DateTime startDate, DateTime endDate, Guid[]? accountIds = null);
|
||||||
Task<NetWorthReport> GetNetWorthAsync(Guid userId, DateTime startDate, DateTime endDate);
|
Task<NetWorthReport> GetNetWorthAsync(Guid userId, DateTime startDate, DateTime endDate, Guid[]? accountIds = null);
|
||||||
Task<CashFlowReport> GetCashFlowAsync(Guid userId, DateTime startDate, DateTime endDate);
|
Task<CashFlowReport> GetCashFlowAsync(Guid userId, DateTime startDate, DateTime endDate, Guid[]? accountIds = null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user