feat(#39): Add a Statistics tab to the ledger view
This commit is contained in:
+55
-1
@@ -904,6 +904,34 @@ paths:
|
|||||||
$ref: "#/components/schemas/TransactionResponse"
|
$ref: "#/components/schemas/TransactionResponse"
|
||||||
"404":
|
"404":
|
||||||
description: No ledger with this id
|
description: No ledger with this id
|
||||||
|
/ledgers/{ledgerId}/profit:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- transaction
|
||||||
|
summary: Get the daily profit series for a single ledger
|
||||||
|
operationId: profitPerDayInLedger
|
||||||
|
parameters:
|
||||||
|
- name: ledgerId
|
||||||
|
in: path
|
||||||
|
description: Id of the ledger
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: "Daily profit for the ledger, ascending by date, only for days\
|
||||||
|
\ with ISK activity"
|
||||||
|
content:
|
||||||
|
'*/*':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/DailyProfitResponse"
|
||||||
|
"400":
|
||||||
|
description: The ledger is the system ledger or a combined ledger
|
||||||
|
"404":
|
||||||
|
description: No ledger with this id
|
||||||
/ledgers/{ledgerId}/balance:
|
/ledgers/{ledgerId}/balance:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@@ -1682,7 +1710,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: When the order was issued.
|
description: When the order was issued.
|
||||||
example: 2026-06-22T05:12:25Z
|
example: 2026-06-22T05:12:25
|
||||||
escrow:
|
escrow:
|
||||||
type: number
|
type: number
|
||||||
description: ISK held in escrow for a buy order; null for sell orders.
|
description: ISK held in escrow for a buy order; null for sell orders.
|
||||||
@@ -1910,6 +1938,32 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
|
DailyProfitResponse:
|
||||||
|
type: object
|
||||||
|
description: "Profit for a single day: ISK in, ISK out, and their difference."
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
description: The UTC (EVE) day this profit is for.
|
||||||
|
example: 2026-06-22
|
||||||
|
iskIn:
|
||||||
|
type: number
|
||||||
|
description: Total ISK that entered the ledger on this day.
|
||||||
|
example: 1500000.0
|
||||||
|
iskOut:
|
||||||
|
type: number
|
||||||
|
description: Total ISK that left the ledger on this day.
|
||||||
|
example: 500000.0
|
||||||
|
profit:
|
||||||
|
type: number
|
||||||
|
description: "Profit for the day: iskIn minus iskOut."
|
||||||
|
example: 1000000.0
|
||||||
|
required:
|
||||||
|
- date
|
||||||
|
- iskIn
|
||||||
|
- iskOut
|
||||||
|
- profit
|
||||||
BalanceResponse:
|
BalanceResponse:
|
||||||
type: object
|
type: object
|
||||||
description: "The balance of a ledger: its ISK total plus the quantity of each\
|
description: "The balance of a ledger: its ISK total plus the quantity of each\
|
||||||
|
|||||||
Generated
+30
@@ -14,6 +14,7 @@
|
|||||||
"@vueuse/integrations": "^14.3.0",
|
"@vueuse/integrations": "^14.3.0",
|
||||||
"@vueuse/router": "^14.3.0",
|
"@vueuse/router": "^14.3.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"gemory": "file:",
|
"gemory": "file:",
|
||||||
"loglevel": "^1.8.1",
|
"loglevel": "^1.8.1",
|
||||||
"loglevel-plugin-prefix": "^0.8.4",
|
"loglevel-plugin-prefix": "^0.8.4",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-chartjs": "^5.3.4",
|
||||||
"vue-router": "^5.0.7"
|
"vue-router": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -229,6 +231,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||||
@@ -1483,6 +1491,18 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||||
@@ -2975,6 +2995,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-chartjs": {
|
||||||
|
"version": "5.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.4.tgz",
|
||||||
|
"integrity": "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@vueuse/integrations": "^14.3.0",
|
"@vueuse/integrations": "^14.3.0",
|
||||||
"@vueuse/router": "^14.3.0",
|
"@vueuse/router": "^14.3.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"gemory": "file:",
|
"gemory": "file:",
|
||||||
"loglevel": "^1.8.1",
|
"loglevel": "^1.8.1",
|
||||||
"loglevel-plugin-prefix": "^0.8.4",
|
"loglevel-plugin-prefix": "^0.8.4",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"sortablejs": "^1.15.7",
|
"sortablejs": "^1.15.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
"vue-chartjs": "^5.3.4",
|
||||||
"vue-router": "^5.0.7"
|
"vue-router": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ min?: number; max?: number }>(), {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const from = defineModel<number>('from', {default: 0});
|
||||||
|
const to = defineModel<number>('to', {default: 0});
|
||||||
|
|
||||||
|
const container = ref<HTMLElement>();
|
||||||
|
|
||||||
|
const dragFill = (event: PointerEvent) => {
|
||||||
|
const rect = container.value?.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!rect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = (props.max - props.min) || 1;
|
||||||
|
const width = to.value - from.value;
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startFrom = from.value;
|
||||||
|
|
||||||
|
const onMove = (moveEvent: PointerEvent) => {
|
||||||
|
const delta = Math.round(((moveEvent.clientX - startX) / rect.width) * span);
|
||||||
|
const newFrom = Math.min(Math.max(startFrom + delta, props.min), props.max - width);
|
||||||
|
|
||||||
|
from.value = newFrom;
|
||||||
|
to.value = newFrom + width;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUp = () => {
|
||||||
|
window.removeEventListener('pointermove', onMove);
|
||||||
|
window.removeEventListener('pointerup', onUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', onMove);
|
||||||
|
window.addEventListener('pointerup', onUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(from, value => {
|
||||||
|
if (value > to.value) {
|
||||||
|
to.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(to, value => {
|
||||||
|
if (value < from.value) {
|
||||||
|
from.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fillStyle = computed(() => {
|
||||||
|
const span = (props.max - props.min) || 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: `${((from.value - props.min) / span) * 100}%`,
|
||||||
|
width: `${((to.value - from.value) / span) * 100}%`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="container" class="dual-range relative h-6 select-none">
|
||||||
|
<div class="absolute inset-x-0 top-1/2 h-1 -translate-y-1/2 rounded-full bg-gray-700 pointer-events-none"></div>
|
||||||
|
<div class="absolute top-1/2 h-2 -translate-y-1/2 rounded-full bg-emerald-500 cursor-grab active:cursor-grabbing"
|
||||||
|
:style="fillStyle" @pointerdown="dragFill"></div>
|
||||||
|
<input type="range" :min="min" :max="max" v-model.number="from"
|
||||||
|
class="absolute inset-0 w-full h-full m-0 bg-transparent appearance-none pointer-events-none" />
|
||||||
|
<input type="range" :min="min" :max="max" v-model.number="to"
|
||||||
|
class="absolute inset-0 w-full h-full m-0 bg-transparent appearance-none pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/style.css";
|
||||||
|
|
||||||
|
.dual-range input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
@apply w-4 h-4 rounded-full bg-emerald-500 border-2 border-gray-200 cursor-pointer pointer-events-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-range input[type="range"]::-moz-range-thumb {
|
||||||
|
@apply w-4 h-4 rounded-full bg-emerald-500 border-2 border-gray-200 cursor-pointer pointer-events-auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as ClipboardButton } from './ClipboardButton.vue';
|
export { default as ClipboardButton } from './ClipboardButton.vue';
|
||||||
export { default as Dropdown } from './Dropdown.vue';
|
export { default as Dropdown } from './Dropdown.vue';
|
||||||
|
export { default as DualRangeSlider } from './DualRangeSlider.vue';
|
||||||
export { default as LoadingSpinner } from './LoadingSpinner.vue';
|
export { default as LoadingSpinner } from './LoadingSpinner.vue';
|
||||||
export { default as Modal } from './Modal.vue';
|
export { default as Modal } from './Modal.vue';
|
||||||
export { default as ProgressBar } from './ProgressBar.vue';
|
export { default as ProgressBar } from './ProgressBar.vue';
|
||||||
|
|||||||
@@ -190,6 +190,27 @@ export interface CreateMainLedgerRequest {
|
|||||||
*/
|
*/
|
||||||
'name': string;
|
'name': string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Profit for a single day: ISK in, ISK out, and their difference.
|
||||||
|
*/
|
||||||
|
export interface DailyProfitResponse {
|
||||||
|
/**
|
||||||
|
* The UTC (EVE) day this profit is for.
|
||||||
|
*/
|
||||||
|
'date': string;
|
||||||
|
/**
|
||||||
|
* Total ISK that entered the ledger on this day.
|
||||||
|
*/
|
||||||
|
'iskIn': number;
|
||||||
|
/**
|
||||||
|
* Total ISK that left the ledger on this day.
|
||||||
|
*/
|
||||||
|
'iskOut': number;
|
||||||
|
/**
|
||||||
|
* Profit for the day: iskIn minus iskOut.
|
||||||
|
*/
|
||||||
|
'profit': number;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* A market location (NPC station) with its containing solar system and region.
|
* A market location (NPC station) with its containing solar system and region.
|
||||||
*/
|
*/
|
||||||
@@ -3707,6 +3728,40 @@ export const TransactionApiAxiosParamCreator = function (configuration?: Configu
|
|||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @summary Get the daily profit series for a single ledger
|
||||||
|
* @param {string} ledgerId Id of the ledger
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
profitPerDayInLedger: async (ledgerId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'ledgerId' is not null or undefined
|
||||||
|
assertParamExists('profitPerDayInLedger', 'ledgerId', ledgerId)
|
||||||
|
const localVarPath = `/ledgers/{ledgerId}/profit`
|
||||||
|
.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 {
|
return {
|
||||||
url: toPathString(localVarUrlObj),
|
url: toPathString(localVarUrlObj),
|
||||||
options: localVarRequestOptions,
|
options: localVarRequestOptions,
|
||||||
@@ -3734,6 +3789,19 @@ export const TransactionApiFp = function(configuration?: Configuration) {
|
|||||||
const localVarOperationServerBasePath = operationServerMap['TransactionApi.finAllTransactionsInLedger']?.[localVarOperationServerIndex]?.url;
|
const localVarOperationServerBasePath = operationServerMap['TransactionApi.finAllTransactionsInLedger']?.[localVarOperationServerIndex]?.url;
|
||||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @summary Get the daily profit series for a single ledger
|
||||||
|
* @param {string} ledgerId Id of the ledger
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async profitPerDayInLedger(ledgerId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<DailyProfitResponse>>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.profitPerDayInLedger(ledgerId, options);
|
||||||
|
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||||
|
const localVarOperationServerBasePath = operationServerMap['TransactionApi.profitPerDayInLedger']?.[localVarOperationServerIndex]?.url;
|
||||||
|
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3753,6 +3821,16 @@ export const TransactionApiFactory = function (configuration?: Configuration, ba
|
|||||||
finAllTransactionsInLedger(ledgerId: string, options?: RawAxiosRequestConfig): AxiosPromise<Array<TransactionResponse>> {
|
finAllTransactionsInLedger(ledgerId: string, options?: RawAxiosRequestConfig): AxiosPromise<Array<TransactionResponse>> {
|
||||||
return localVarFp.finAllTransactionsInLedger(ledgerId, options).then((request) => request(axios, basePath));
|
return localVarFp.finAllTransactionsInLedger(ledgerId, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @summary Get the daily profit series for a single ledger
|
||||||
|
* @param {string} ledgerId Id of the ledger
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
profitPerDayInLedger(ledgerId: string, options?: RawAxiosRequestConfig): AxiosPromise<Array<DailyProfitResponse>> {
|
||||||
|
return localVarFp.profitPerDayInLedger(ledgerId, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3770,6 +3848,17 @@ export class TransactionApi extends BaseAPI {
|
|||||||
public finAllTransactionsInLedger(ledgerId: string, options?: RawAxiosRequestConfig) {
|
public finAllTransactionsInLedger(ledgerId: string, options?: RawAxiosRequestConfig) {
|
||||||
return TransactionApiFp(this.configuration).finAllTransactionsInLedger(ledgerId, options).then((request) => request(this.axios, this.basePath));
|
return TransactionApiFp(this.configuration).finAllTransactionsInLedger(ledgerId, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @summary Get the daily profit series for a single ledger
|
||||||
|
* @param {string} ledgerId Id of the ledger
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
public profitPerDayInLedger(ledgerId: string, options?: RawAxiosRequestConfig) {
|
||||||
|
return TransactionApiFp(this.configuration).profitPerDayInLedger(ledgerId, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {RouterLink, RouterView} from 'vue-router';
|
import {RouterLink, RouterView} from 'vue-router';
|
||||||
import {LedgerLabel, useLedgerParam} from "@/ledger";
|
import {isMain, LedgerLabel, useLedgerParam} from "@/ledger";
|
||||||
import {routeNames} from "@/routes.ts";
|
import {routeNames} from "@/routes.ts";
|
||||||
import {IskLabel} from "@/market";
|
import {IskLabel} from "@/market";
|
||||||
|
|
||||||
@@ -28,6 +28,9 @@ const {ledger} = useLedgerParam();
|
|||||||
<RouterLink :to="{name: routeNames.listLedgerAcquisitions}" class="tab">
|
<RouterLink :to="{name: routeNames.listLedgerAcquisitions}" class="tab">
|
||||||
<span>Acquisitions</span>
|
<span>Acquisitions</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink v-if="isMain(ledger)" :to="{name: routeNames.viewLedgerStatistics}" class="tab">
|
||||||
|
<span>Statistics</span>
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<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 {transactionApi} 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 {ledgerId} = useLedgerParam();
|
||||||
|
|
||||||
|
const profits = useProcessedResource<DailyProfitResponse[]>(async () => {
|
||||||
|
if (!ledgerId.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = await transactionApi.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))),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-4">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export const routeNames = {
|
|||||||
viewLedgerBalance: 'view-ledger-balance',
|
viewLedgerBalance: 'view-ledger-balance',
|
||||||
listLedgerTransactions: 'list-ledger-transactions',
|
listLedgerTransactions: 'list-ledger-transactions',
|
||||||
listLedgerAcquisitions: 'list-ledger-acquisitions',
|
listLedgerAcquisitions: 'list-ledger-acquisitions',
|
||||||
|
viewLedgerStatistics: 'view-ledger-statistics',
|
||||||
editRuleBook: 'edit-rule-book',
|
editRuleBook: 'edit-rule-book',
|
||||||
marketTypes: 'market-types',
|
marketTypes: 'market-types',
|
||||||
appraise: 'appraise',
|
appraise: 'appraise',
|
||||||
@@ -31,6 +32,7 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
{path: 'balance', name: routeNames.viewLedgerBalance, component: () => import('@/pages/ledger/ViewLedgerBalance.vue')},
|
{path: 'balance', name: routeNames.viewLedgerBalance, component: () => import('@/pages/ledger/ViewLedgerBalance.vue')},
|
||||||
{path: 'transactions', name: routeNames.listLedgerTransactions, component: () => import('@/pages/ledger/ListLedgerTransactions.vue')},
|
{path: 'transactions', name: routeNames.listLedgerTransactions, component: () => import('@/pages/ledger/ListLedgerTransactions.vue')},
|
||||||
{path: 'acquisitions', name: routeNames.listLedgerAcquisitions, component: () => import('@/pages/ledger/ViewLedgerAcquisitions.vue')},
|
{path: 'acquisitions', name: routeNames.listLedgerAcquisitions, component: () => import('@/pages/ledger/ViewLedgerAcquisitions.vue')},
|
||||||
|
{path: 'statistics', name: routeNames.viewLedgerStatistics, component: () => import('@/pages/ledger/ViewLedgerStatistics.vue')},
|
||||||
]},
|
]},
|
||||||
]},
|
]},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user