feat(#52): Add Daily Balance mode to ledger Statistics view

This commit is contained in:
Sirttas committed 2026-08-31 19:28:21 +02:00
1 parent 999abd1d1e
commit 56f70eb599
9 files changed
+544 -159

No files matched your search

+60
View File
@@ -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.
+89
View File
@@ -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<RequestArgs> => {
// 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<Array<DailyBalanceResponse>>> {
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<MainLedgerResponse> {
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<Array<DailyBalanceResponse>> {
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
+1
View File
@@ -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';
+46
View File
@@ -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);
});
});
+15
View File
@@ -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;
+156
View File
@@ -0,0 +1,156 @@
<script setup lang="ts">
import {computed, ref, watch} from "vue";
import {
CategoryScale,
Chart as ChartJS,
type ChartData,
type ChartDataset,
type ChartOptions,
Filler,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Tooltip,
} from "chart.js";
import {Chart} from "vue-chartjs";
import {latestDailyBalanceValue} from "@/ledger";
import {ledgerApi} from "@/mammon";
import {useProcessedResource} from "@/activity";
import {DailyBalanceResponse} from "@/generated/mammon";
import {IskLabel} from "@/market";
import {formatIsk} from "@/formaters";
import {DualRangeSlider} from "@/components";
ChartJS.register(
CategoryScale, LinearScale,
LineController, LineElement, PointElement,
Tooltip, Legend, Filler,
);
const props = defineProps<{ ledgerId: string }>();
const dailyBalances = useProcessedResource<DailyBalanceResponse[]>(async () => {
if (!props.ledgerId) {
return [];
}
const {data} = await ledgerApi.dailyValuationForLedger(props.ledgerId);
return data;
}, []);
const dates = computed(() => dailyBalances.value.map(b => b.date));
const from = ref(0);
const to = ref(0);
watch(dates, d => {
from.value = 0;
to.value = Math.max(d.length - 1, 0);
}, {immediate: true});
const maxIndex = computed(() => Math.max(dates.value.length - 1, 0));
const dailyBalanceRange = computed(() => dailyBalances.value.slice(from.value, to.value + 1));
const hasData = computed(() => dailyBalances.value.length > 0);
const baseline = computed(() => dailyBalanceRange.value[0]?.averageValue ?? 0);
const total = computed(() => latestDailyBalanceValue(dailyBalanceRange.value) - baseline.value);
const chartData = computed<ChartData<'line'>>(() => {
const highest: ChartDataset<'line'> = {
label: 'Highest',
data: dailyBalanceRange.value.map(b => b.highestValue - baseline.value),
borderColor: 'transparent',
backgroundColor: 'rgba(56, 189, 248, 0.15)',
pointRadius: 0,
tension: 0.2,
fill: '+1',
order: 2,
};
const lowest: ChartDataset<'line'> = {
label: 'Lowest',
data: dailyBalanceRange.value.map(b => b.lowestValue - baseline.value),
borderColor: 'transparent',
backgroundColor: 'rgba(56, 189, 248, 0.15)',
pointRadius: 0,
tension: 0.2,
fill: false,
order: 2,
};
const average: ChartDataset<'line'> = {
type: 'line',
label: 'Average',
data: dailyBalanceRange.value.map(b => b.averageValue - baseline.value),
borderColor: '#38bdf8',
backgroundColor: 'rgba(56, 189, 248, 0.15)',
pointBackgroundColor: ctx => (Number(ctx.dataset.data[ctx.dataIndex]) < 0 ? '#f87171' : '#34d399'),
pointBorderColor: ctx => (Number(ctx.dataset.data[ctx.dataIndex]) < 0 ? '#f87171' : '#34d399'),
segment: {
borderColor: ctx => ((ctx.p0.parsed.y ?? 0) < 0 || (ctx.p1.parsed.y ?? 0) < 0 ? '#f87171' : '#34d399'),
},
fill: false,
tension: 0.2,
order: 1,
};
return {
labels: dailyBalanceRange.value.map(b => b.date),
datasets: [highest, lowest, average],
};
});
const chartOptions = computed<ChartOptions<'line'>>(() => ({
responsive: true,
maintainAspectRatio: false,
interaction: {mode: 'index', intersect: false},
plugins: {
legend: {display: true},
tooltip: {
callbacks: {
label: ctx => `${ctx.dataset.label}: ${formatIsk(ctx.parsed.y ?? 0)}`,
},
},
},
scales: {
x: {stacked: false},
y: {
position: 'left',
stacked: false,
ticks: {
callback: value => formatIsk(Number(value)),
},
grid: {
lineWidth: ctx => (ctx.tick.value === 0 ? 2 : 1),
color: ctx => (ctx.tick.value === 0 ? 'rgba(226, 232, 240, 0.6)' : 'rgba(148, 163, 184, 0.2)'),
},
}
},
}));
</script>
<template>
<div class="flex justify-end mb-4">
<span class="mr-2">Change:</span>
<IskLabel :amount="total" />
</div>
<p v-if="!hasData" class="text-center text-gray-400 mt-8">No daily balance recorded for this ledger</p>
<template v-else>
<div class="h-96">
<Chart type="line" :data="chartData" :options="chartOptions" />
</div>
<div class="flex items-center gap-3 mt-4">
<span class="text-sm w-24 text-gray-400">{{ dates[from] }}</span>
<DualRangeSlider class="grow" :min="0" :max="maxIndex" v-model:from="from" v-model:to="to" />
<span class="text-sm w-24 text-right text-gray-400">{{ dates[to] }}</span>
</div>
</template>
</template>
+154
View File
@@ -0,0 +1,154 @@
<script setup lang="ts">
import {computed, ref, watch} from "vue";
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
type ChartData,
type ChartDataset,
type ChartOptions,
Filler,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Tooltip,
} from "chart.js";
import {Chart} from "vue-chartjs";
import {StatisticsModes, type StatisticsMode} from "@/ledger";
import {profitApi} from "@/mammon";
import {useProcessedResource} from "@/activity";
import {DailyProfitResponse} from "@/generated/mammon";
import {IskLabel} from "@/market";
import {formatIsk} from "@/formaters";
import {DualRangeSlider} from "@/components";
ChartJS.register(
CategoryScale, LinearScale,
BarController, BarElement,
LineController, LineElement, PointElement,
Tooltip, Legend, Filler,
);
const props = defineProps<{ ledgerId: string; mode: StatisticsMode }>();
const profits = useProcessedResource<DailyProfitResponse[]>(async () => {
if (!props.ledgerId) {
return [];
}
const {data} = props.mode === StatisticsModes.Margin
? await profitApi.marginPerDayInLedger(props.ledgerId)
: await profitApi.profitPerDayInLedger(props.ledgerId);
return data;
}, []);
const dates = computed(() => profits.value.map(p => p.date));
const from = ref(0);
const to = ref(0);
watch(dates, d => {
from.value = 0;
to.value = Math.max(d.length - 1, 0);
}, {immediate: true});
const maxIndex = computed(() => Math.max(dates.value.length - 1, 0));
const profitRange = computed(() => profits.value.slice(from.value, to.value + 1));
const total = computed(() => profitRange.value.reduce((sum, p) => sum + p.profit, 0));
const hasData = computed(() => profits.value.length > 0);
const cumulativeProfit = computed(() => {
let running = 0;
return profitRange.value.map(p => (running += p.profit));
});
const chartData = computed<ChartData<'bar' | 'line'>>(() => {
const cumulative: ChartDataset<'line'> = {
type: 'line',
label: 'Cumulative profit',
data: cumulativeProfit.value,
borderColor: '#38bdf8',
backgroundColor: 'rgba(56, 189, 248, 0.15)',
pointBackgroundColor: '#38bdf8',
fill: true,
tension: 0.2,
yAxisID: 'y',
order: 0,
};
const iskIn: ChartDataset<'bar'> = {
type: 'bar',
label: 'ISK in',
data: profitRange.value.map(p => p.iskIn),
backgroundColor: 'rgba(16, 185, 129, 0.7)',
yAxisID: 'y',
order: 1,
};
const iskOut: ChartDataset<'bar'> = {
type: 'bar',
label: 'ISK out',
data: profitRange.value.map(p => -p.iskOut),
backgroundColor: 'rgba(180, 83, 9, 0.7)',
yAxisID: 'y',
order: 1,
};
return {
labels: profitRange.value.map(p => p.date),
datasets: [cumulative, iskIn, iskOut],
};
});
const chartOptions = computed<ChartOptions<'bar' | 'line'>>(() => ({
responsive: true,
maintainAspectRatio: false,
interaction: {mode: 'index', intersect: false},
plugins: {
legend: {display: true},
tooltip: {
callbacks: {
label: ctx => `${ctx.dataset.label}: ${formatIsk(ctx.parsed.y ?? 0)}`,
},
},
},
scales: {
x: {stacked: true},
y: {
position: 'left',
stacked: true,
ticks: {
callback: value => formatIsk(Number(value)),
},
}
},
}));
</script>
<template>
<div class="flex justify-end mb-4">
<span class="mr-2">Total profit:</span>
<IskLabel :amount="total" />
</div>
<p v-if="!hasData" class="text-center text-gray-400 mt-8">No trade profit recorded for this ledger</p>
<template v-else>
<div class="h-96">
<Chart type="bar" :data="chartData" :options="chartOptions" />
</div>
<div class="flex items-center gap-3 mt-4">
<span class="text-sm w-24 text-gray-400">{{ dates[from] }}</span>
<DualRangeSlider class="grow" :min="0" :max="maxIndex" v-model:from="from" v-model:to="to" />
<span class="text-sm w-24 text-right text-gray-400">{{ dates[to] }}</span>
</div>
</template>
</template>
+2 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import {RouterLink, RouterView} from 'vue-router';
import {appraiseItemBalances, getLedgerBalance, isMain, LedgerLabel, useLedgerParam} from "@/ledger";
import {appraiseItemBalances, getLedgerBalance, LedgerLabel, useLedgerParam} from "@/ledger";
import {routeNames} from "@/routes.ts";
import {getMarketTypes, getPrices, IskLabel} from "@/market";
import {useMarketLocationStore} from '@/market/location';
@@ -53,7 +53,7 @@ const itemAppraisal = computedAsync(async () => {
<RouterLink :to="{name: routeNames.listLedgerAcquisitions}" class="tab">
<span>Acquisitions</span>
</RouterLink>
<RouterLink v-if="isMain(ledger)" :to="{name: routeNames.viewLedgerStatistics}" class="tab">
<RouterLink :to="{name: routeNames.viewLedgerStatistics}" class="tab">
<span>Statistics</span>
</RouterLink>
</div>
+21 -157
View File
@@ -1,174 +1,39 @@
<script setup lang="ts">
import {computed, ref, watch} from "vue";
import {
BarController,
BarElement,
CategoryScale,
Chart as ChartJS,
type ChartData,
type ChartDataset,
type ChartOptions,
Filler,
Legend,
LinearScale,
LineController,
LineElement,
PointElement,
Tooltip,
} from "chart.js";
import {Chart} from "vue-chartjs";
import {useLedgerParam} from "@/ledger";
import {profitApi} from "@/mammon";
import {useProcessedResource} from "@/activity";
import {DailyProfitResponse} from "@/generated/mammon";
import {IskLabel} from "@/market";
import {formatIsk} from "@/formaters";
import {DualRangeSlider} from "@/components";
import {ref, watch} from "vue";
import {isMain, resolveStatisticsMode, StatisticsModes, type StatisticsMode, useLedgerParam} from "@/ledger";
import DailyBalanceChart from "./DailyBalanceChart.vue";
import ProfitChart from "./ProfitChart.vue";
ChartJS.register(
CategoryScale, LinearScale,
BarController, BarElement,
LineController, LineElement, PointElement,
Tooltip, Legend, Filler,
);
const {ledgerId, ledger} = useLedgerParam();
const {ledgerId} = useLedgerParam();
const mode = ref<StatisticsMode>(StatisticsModes.DailyBalance);
const ProfitModes = {
CashFlow: 'CASH_FLOW',
Margin: 'MARGIN',
} as const;
type ProfitMode = typeof ProfitModes[keyof typeof ProfitModes];
const mode = ref<ProfitMode>(ProfitModes.CashFlow);
const profits = useProcessedResource<DailyProfitResponse[]>(async () => {
if (!ledgerId.value) {
return [];
watch(ledger, l => {
if (l) {
mode.value = resolveStatisticsMode(l, mode.value);
}
const {data} = mode.value === ProfitModes.Margin
? await profitApi.marginPerDayInLedger(ledgerId.value)
: await profitApi.profitPerDayInLedger(ledgerId.value);
return data;
}, []);
const from = ref(0);
const to = ref(0);
watch(() => profits.value.length, length => {
from.value = 0;
to.value = Math.max(length - 1, 0);
}, {immediate: true});
const maxIndex = computed(() => Math.max(profits.value.length - 1, 0));
const range = computed(() => profits.value.slice(from.value, to.value + 1));
const total = computed(() => range.value.reduce((sum, p) => sum + p.profit, 0));
const cumulativeProfit = computed(() => {
let running = 0;
return range.value.map(p => (running += p.profit));
});
const chartData = computed<ChartData<'bar' | 'line'>>(() => {
const cumulative: ChartDataset<'line'> = {
type: 'line',
label: 'Cumulative profit',
data: cumulativeProfit.value,
borderColor: '#38bdf8',
backgroundColor: 'rgba(56, 189, 248, 0.15)',
pointBackgroundColor: '#38bdf8',
fill: true,
tension: 0.2,
yAxisID: 'y',
order: 0,
};
const iskIn: ChartDataset<'bar'> = {
type: 'bar',
label: 'ISK in',
data: range.value.map(p => p.iskIn),
backgroundColor: 'rgba(16, 185, 129, 0.7)',
yAxisID: 'y',
order: 1,
};
const iskOut: ChartDataset<'bar'> = {
type: 'bar',
label: 'ISK out',
data: range.value.map(p => -p.iskOut),
backgroundColor: 'rgba(180, 83, 9, 0.7)',
yAxisID: 'y',
order: 1,
};
return {
labels: range.value.map(p => p.date),
datasets: [cumulative, iskIn, iskOut],
};
});
const chartOptions = computed<ChartOptions<'bar' | 'line'>>(() => ({
responsive: true,
maintainAspectRatio: false,
interaction: {mode: 'index', intersect: false},
plugins: {
legend: {display: true},
tooltip: {
callbacks: {
label: ctx => `${ctx.dataset.label}: ${formatIsk(Math.abs(ctx.parsed.y ?? 0))}`,
},
},
},
scales: {
x: {
stacked: true,
},
y: {
position: 'left',
stacked: true,
ticks: {
callback: value => formatIsk(Math.abs(Number(value))),
},
}
},
}));
const isDailyBalance = (mode: StatisticsMode): mode is typeof StatisticsModes.DailyBalance =>
mode === StatisticsModes.DailyBalance;
</script>
<template>
<div class="mt-4">
<div class="flex justify-center mb-4">
<div v-if="ledger" class="mt-4">
<div v-if="isMain(ledger)" class="flex justify-center mb-4">
<div class="flex bg-slate-600 rounded-s-md p-1">
<button class="switch" :class="{active: mode === ProfitModes.CashFlow}" @click="mode = ProfitModes.CashFlow">Cash Flow</button>
<button class="switch" :class="{active: mode === StatisticsModes.DailyBalance}" @click="mode = StatisticsModes.DailyBalance">Daily Balance</button>
</div>
<div class="switch flex bg-slate-600 rounded-e-md p-1">
<button class="switch" :class="{active: mode === ProfitModes.Margin}" @click="mode = ProfitModes.Margin">Margin</button>
<div class="flex bg-slate-600 p-1">
<button class="switch" :class="{active: mode === StatisticsModes.Margin}" @click="mode = StatisticsModes.Margin">Margin</button>
</div>
<div class="flex bg-slate-600 rounded-e-md p-1">
<button class="switch" :class="{active: mode === StatisticsModes.CashFlow}" @click="mode = StatisticsModes.CashFlow">Cash Flow</button>
</div>
</div>
<div class="flex justify-end mb-4">
<span class="mr-2">Total profit:</span>
<IskLabel :amount="total" />
</div>
<p v-if="profits.length === 0" class="text-center text-gray-400 mt-8">
No trade profit recorded for this ledger
</p>
<template v-else>
<div class="h-96">
<Chart type="bar" :data="chartData" :options="chartOptions" />
</div>
<div class="flex items-center gap-3 mt-4">
<span class="text-sm w-24 text-gray-400">{{ profits[from]?.date }}</span>
<DualRangeSlider class="grow" :min="0" :max="maxIndex" v-model:from="from" v-model:to="to" />
<span class="text-sm w-24 text-right text-gray-400">{{ profits[to]?.date }}</span>
</div>
</template>
<DailyBalanceChart v-if="isDailyBalance(mode)" :ledger-id="ledgerId" />
<ProfitChart v-else :ledger-id="ledgerId" :mode="mode" />
</div>
</template>
@@ -182,4 +47,3 @@ button.switch {
}
}
</style>