From 56f70eb59962e5c76c5eb285f238eaae6dccd928 Mon Sep 17 00:00:00 2001 From: Sirttas Date: Mon, 31 Aug 2026 19:28:21 +0200 Subject: [PATCH] feat(#52): Add Daily Balance mode to ledger Statistics view --- docs/mammon-api.yml | 60 ++++++++ src/generated/mammon/api.ts | 89 +++++++++++ src/ledger/index.ts | 1 + src/ledger/statistics.spec.ts | 46 ++++++ src/ledger/statistics.ts | 15 ++ src/pages/ledger/DailyBalanceChart.vue | 156 +++++++++++++++++++ src/pages/ledger/ProfitChart.vue | 154 +++++++++++++++++++ src/pages/ledger/ViewLedger.vue | 4 +- src/pages/ledger/ViewLedgerStatistics.vue | 178 +++------------------- 9 files changed, 544 insertions(+), 159 deletions(-) create mode 100644 src/ledger/statistics.spec.ts create mode 100644 src/ledger/statistics.ts create mode 100644 src/pages/ledger/DailyBalanceChart.vue create mode 100644 src/pages/ledger/ProfitChart.vue diff --git a/docs/mammon-api.yml b/docs/mammon-api.yml index 46f123b..665e21a 100644 --- a/docs/mammon-api.yml +++ b/docs/mammon-api.yml @@ -1037,6 +1037,36 @@ paths: $ref: "#/components/schemas/BalanceResponse" "404": description: No ledger with this id + /ledgers/{ledgerId}/balance/daily-valuation: + get: + tags: + - ledger + summary: Get the daily mark-to-market valuation series of a ledger's held balance + (items and ISK) + operationId: dailyValuationForLedger + parameters: + - name: ledgerId + in: path + description: Id of the ledger + required: true + schema: + type: string + format: uuid + responses: + "200": + description: "Daily average/highest/lowest valuation for the ledger, ascending\ + \ by date. For a combined ledger, this is the per-date sum of its covered\ + \ main ledgers' valuations." + content: + '*/*': + schema: + type: array + items: + $ref: "#/components/schemas/DailyBalanceResponse" + "400": + description: The ledger is the system ledger + "404": + description: No ledger with this id /corporations: get: tags: @@ -2260,6 +2290,36 @@ components: required: - quantity - typeId + DailyBalanceResponse: + type: object + description: "Mark-to-market value of a ledger's held balance (items and ISK)\ + \ for a single day: average, highest and lowest." + properties: + date: + type: string + format: date + description: The UTC (EVE) day this valuation is for. + example: 2026-06-22 + averageValue: + type: number + description: ISK balance plus held item balance valued at that day's average + price. + example: 1.5E8 + highestValue: + type: number + description: ISK balance plus held item balance valued at that day's highest + price. + example: 1.6E8 + lowestValue: + type: number + description: ISK balance plus held item balance valued at that day's lowest + price. + example: 1.4E8 + required: + - averageValue + - date + - highestValue + - lowestValue CorporationResponse: type: object description: An EVE Online corporation. diff --git a/src/generated/mammon/api.ts b/src/generated/mammon/api.ts index a79de42..cc7fbab 100644 --- a/src/generated/mammon/api.ts +++ b/src/generated/mammon/api.ts @@ -207,6 +207,27 @@ export interface CreateMainLedgerRequest { */ 'name': string; } +/** + * Mark-to-market value of a ledger\'s held balance (items and ISK) for a single day: average, highest and lowest. + */ +export interface DailyBalanceResponse { + /** + * The UTC (EVE) day this valuation is for. + */ + 'date': string; + /** + * ISK balance plus held item balance valued at that day\'s average price. + */ + 'averageValue': number; + /** + * ISK balance plus held item balance valued at that day\'s highest price. + */ + 'highestValue': number; + /** + * ISK balance plus held item balance valued at that day\'s lowest price. + */ + 'lowestValue': number; +} /** * Profit for a single day: ISK in, ISK out, and their difference. */ @@ -2041,6 +2062,40 @@ export const LedgerApiAxiosParamCreator = function (configuration?: Configuratio options: localVarRequestOptions, }; }, + /** + * + * @summary Get the daily mark-to-market valuation series of a ledger\'s held balance (items and ISK) + * @param {string} ledgerId Id of the ledger + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + dailyValuationForLedger: async (ledgerId: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'ledgerId' is not null or undefined + assertParamExists('dailyValuationForLedger', 'ledgerId', ledgerId) + const localVarPath = `/ledgers/{ledgerId}/balance/daily-valuation` + .replace('{ledgerId}', encodeURIComponent(String(ledgerId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + localVarHeaderParameter['Accept'] = '*/*'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Find all ledgers owned by the authenticated user @@ -2252,6 +2307,19 @@ export const LedgerApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['LedgerApi.createMainLedger']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * + * @summary Get the daily mark-to-market valuation series of a ledger\'s held balance (items and ISK) + * @param {string} ledgerId Id of the ledger + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async dailyValuationForLedger(ledgerId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.dailyValuationForLedger(ledgerId, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['LedgerApi.dailyValuationForLedger']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * * @summary Find all ledgers owned by the authenticated user @@ -2347,6 +2415,16 @@ export const LedgerApiFactory = function (configuration?: Configuration, basePat createMainLedger(createMainLedgerRequest: CreateMainLedgerRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.createMainLedger(createMainLedgerRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Get the daily mark-to-market valuation series of a ledger\'s held balance (items and ISK) + * @param {string} ledgerId Id of the ledger + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + dailyValuationForLedger(ledgerId: string, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.dailyValuationForLedger(ledgerId, options).then((request) => request(axios, basePath)); + }, /** * * @summary Find all ledgers owned by the authenticated user @@ -2427,6 +2505,17 @@ export class LedgerApi extends BaseAPI { return LedgerApiFp(this.configuration).createMainLedger(createMainLedgerRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Get the daily mark-to-market valuation series of a ledger\'s held balance (items and ISK) + * @param {string} ledgerId Id of the ledger + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public dailyValuationForLedger(ledgerId: string, options?: RawAxiosRequestConfig) { + return LedgerApiFp(this.configuration).dailyValuationForLedger(ledgerId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Find all ledgers owned by the authenticated user diff --git a/src/ledger/index.ts b/src/ledger/index.ts index ea43500..4fdf61f 100644 --- a/src/ledger/index.ts +++ b/src/ledger/index.ts @@ -1,5 +1,6 @@ export * from './ledger'; export * from './balanceAppraisal'; +export * from './statistics'; export {default as LedgerLabel} from './LedgerLabel.vue'; export {default as LedgerSelect} from './LedgerSelect.vue'; diff --git a/src/ledger/statistics.spec.ts b/src/ledger/statistics.spec.ts new file mode 100644 index 0000000..09d39bf --- /dev/null +++ b/src/ledger/statistics.spec.ts @@ -0,0 +1,46 @@ +import {describe, expect, test} from 'vitest'; +import {latestDailyBalanceValue, resolveStatisticsMode, StatisticsModes} from './statistics'; +import type {CombinedLedger, MainLedger} from './ledger'; +import {LedgerTypes} from './ledger'; +import type {DailyBalanceResponse} from '@/generated/mammon'; + +const mainLedger = {type: LedgerTypes.Main, ledgerId: 'main-1', name: 'Main', balance: 0} as MainLedger; +const combinedLedger = {type: LedgerTypes.Combined, ledgerId: 'combined-1', name: 'Combined', balance: 0} as unknown as CombinedLedger; + +const balance = (date: string, averageValue: number): DailyBalanceResponse => ({ + date, averageValue, highestValue: averageValue, lowestValue: averageValue, +}); + +describe('resolveStatisticsMode', () => { + test('keeps cash flow mode for a main ledger', () => { + expect(resolveStatisticsMode(mainLedger, StatisticsModes.CashFlow)).toBe(StatisticsModes.CashFlow); + }); + + test('keeps margin mode for a main ledger', () => { + expect(resolveStatisticsMode(mainLedger, StatisticsModes.Margin)).toBe(StatisticsModes.Margin); + }); + + test('keeps daily balance mode for a combined ledger', () => { + expect(resolveStatisticsMode(combinedLedger, StatisticsModes.DailyBalance)).toBe(StatisticsModes.DailyBalance); + }); + + test('falls back to daily balance when cash flow is selected for a combined ledger', () => { + expect(resolveStatisticsMode(combinedLedger, StatisticsModes.CashFlow)).toBe(StatisticsModes.DailyBalance); + }); + + test('falls back to daily balance when margin is selected for a combined ledger', () => { + expect(resolveStatisticsMode(combinedLedger, StatisticsModes.Margin)).toBe(StatisticsModes.DailyBalance); + }); +}); + +describe('latestDailyBalanceValue', () => { + test('returns the average value of the most recent day in range', () => { + const result = latestDailyBalanceValue([balance('2026-08-01', 100), balance('2026-08-02', 150)]); + + expect(result).toBe(150); + }); + + test('returns zero for an empty range', () => { + expect(latestDailyBalanceValue([])).toBe(0); + }); +}); diff --git a/src/ledger/statistics.ts b/src/ledger/statistics.ts new file mode 100644 index 0000000..e0c0ea9 --- /dev/null +++ b/src/ledger/statistics.ts @@ -0,0 +1,15 @@ +import type {DailyBalanceResponse} from "@/generated/mammon"; +import {isMain, type Ledger} from "./ledger"; + +export const StatisticsModes = { + CashFlow: 'CASH_FLOW', + Margin: 'MARGIN', + DailyBalance: 'DAILY_BALANCE', +} as const; +export type StatisticsMode = typeof StatisticsModes[keyof typeof StatisticsModes]; + +export const resolveStatisticsMode = (ledger: Ledger, mode: StatisticsMode): StatisticsMode => + !isMain(ledger) && mode !== StatisticsModes.DailyBalance ? StatisticsModes.DailyBalance : mode; + +export const latestDailyBalanceValue = (range: DailyBalanceResponse[]): number => + range.at(-1)?.averageValue ?? 0; diff --git a/src/pages/ledger/DailyBalanceChart.vue b/src/pages/ledger/DailyBalanceChart.vue new file mode 100644 index 0000000..8b36b46 --- /dev/null +++ b/src/pages/ledger/DailyBalanceChart.vue @@ -0,0 +1,156 @@ + + + diff --git a/src/pages/ledger/ProfitChart.vue b/src/pages/ledger/ProfitChart.vue new file mode 100644 index 0000000..1a9f5c9 --- /dev/null +++ b/src/pages/ledger/ProfitChart.vue @@ -0,0 +1,154 @@ + + + diff --git a/src/pages/ledger/ViewLedger.vue b/src/pages/ledger/ViewLedger.vue index 87276c8..4550011 100644 --- a/src/pages/ledger/ViewLedger.vue +++ b/src/pages/ledger/ViewLedger.vue @@ -1,6 +1,6 @@ @@ -182,4 +47,3 @@ button.switch { } } -