feat(#16): Appraisal page

This commit is contained in:
Sirttas
2026-07-09 17:43:28 +02:00
parent 0f97a49e26
commit c4803ef650
6 changed files with 132 additions and 0 deletions
@@ -0,0 +1,64 @@
<script setup lang="ts">
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
import {MarketTypeLabel} from '@/market/type';
import {computed} from 'vue';
import IskLabel from '../IskLabel.vue';
import {AppraisalItemValue} from './appraise';
interface Props {
result?: AppraisalItemValue[];
}
const props = withDefaults(defineProps<Props>(), {
result: () => []
});
const { sortedArray, headerProps } = useSort(computed(() => props.result), {
defaultSortKey: 'sell',
defaultSortDirection: 'desc'
});
const totalBuy = computed(() => props.result.reduce((sum, r) => sum + r.buy, 0));
const totalSell = computed(() => props.result.reduce((sum, r) => sum + r.sell, 0));
</script>
<template>
<VirtualScrollTable :list="sortedArray" :itemHeight="33" :footerHeight="33" bottom="1rem">
<template #default="{ list }">
<thead>
<tr>
<SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="quantity">Qty</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="buyUnit">Buy/unit</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="sellUnit">Sell/unit</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="buy">Buy total</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="sell">Sell total</SortableHeader>
</tr>
</thead>
<tbody>
<tr v-for="r in list" :key="r.data.typeID">
<td>
<MarketTypeLabel :id="r.data.typeID" :name="r.data.name" />
</td>
<td class="text-right">{{ r.data.quantity.toLocaleString() }}</td>
<td class="text-right"><IskLabel :amount="r.data.buyUnit" :colored="false" /></td>
<td class="text-right"><IskLabel :amount="r.data.sellUnit" :colored="false" /></td>
<td class="text-right"><IskLabel :amount="r.data.buy" :colored="false" /></td>
<td class="text-right"><IskLabel :amount="r.data.sell" :colored="false" /></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4" class="font-semibold">Total</td>
<td class="text-right"><IskLabel :amount="totalBuy" :colored="false" /></td>
<td class="text-right"><IskLabel :amount="totalSell" :colored="false" /></td>
</tr>
</tfoot>
</template>
<template #empty>
<div class="text-center mt-4">
<span>No items found</span>
</div>
</template>
</VirtualScrollTable>
</template>
+32
View File
@@ -0,0 +1,32 @@
import {marketApi} from '@/mammon';
import {getMarketTypes} from '../type';
export type AppraisalItemValue = {
typeID: number;
name: string;
quantity: number;
buy: number;
sell: number;
buyUnit: number;
sellUnit: number;
};
export const appraise = async (raw?: string, locationId?: number): Promise<AppraisalItemValue[]> => {
if (!raw?.trim()) {
return [];
}
const results = await marketApi.pastePrices(raw, locationId).then(r => r.data);
const names = await getMarketTypes(results.map(r => r.marketTypeId))
.then(types => new Map(types.map(t => [t.id, t.name])));
return results.map(r => ({
typeID: r.marketTypeId,
name: names.get(r.marketTypeId) ?? '',
quantity: r.quantity,
buy: r.buy,
sell: r.sell,
buyUnit: r.quantity ? r.buy / r.quantity : 0,
sellUnit: r.quantity ? r.sell / r.quantity : 0,
}));
};
+3
View File
@@ -1,2 +1,5 @@
export * from './MarketTypePrice'; export * from './MarketTypePrice';
export * from './appraisal'; export * from './appraisal';
export * from './appraise';
export { default as AppraisalResultTable } from './AppraisalResultTable.vue';
+3
View File
@@ -15,6 +15,9 @@ import {routeNames} from '@/routes';
<RouterLink to="/market/acquisitions" class="tab"> <RouterLink to="/market/acquisitions" class="tab">
<span>Acquisitions</span> <span>Acquisitions</span>
</RouterLink> </RouterLink>
<RouterLink :to="{name: routeNames.appraise}" class="tab">
<span>Appraisal</span>
</RouterLink>
</div> </div>
<RouterView /> <RouterView />
</div> </div>
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import {AppraisalItemValue, AppraisalResultTable, appraise} from '@/market/appraisal';
import {ref} from 'vue';
const items = ref('');
const result = ref<AppraisalItemValue[]>([]);
const send = async () => result.value = await appraise(items.value);
</script>
<template>
<div class="flex items-stretch mt-4">
<div class="flex-1 mx-1">
<span>Items</span>
<textarea class="mt-1" v-model="items" placeholder="Paste an EVE inventory listing" />
</div>
</div>
<div class="grid my-2">
<button class="justify-self-end" @click="send" :disabled="!items.trim()">Send</button>
</div>
<template v-if="result.length > 0">
<hr />
<div class="grid mt-2">
<AppraisalResultTable :result="result" />
</div>
</template>
</template>
+2
View File
@@ -15,6 +15,7 @@ export const routeNames = {
listLedgerAcquisitions: 'list-ledger-acquisitions', listLedgerAcquisitions: 'list-ledger-acquisitions',
editRuleBook: 'edit-rule-book', editRuleBook: 'edit-rule-book',
marketTypes: 'market-types', marketTypes: 'market-types',
appraise: 'appraise',
about: 'about', about: 'about',
} as const; } as const;
@@ -39,6 +40,7 @@ export const routes: RouteRecordRaw[] = [
{path: 'types/:type?', name: routeNames.marketTypes, component: () => import('@/pages/market/TypeInfo.vue')}, {path: 'types/:type?', name: routeNames.marketTypes, component: () => import('@/pages/market/TypeInfo.vue')},
{path: 'scan', component: () => import('@/pages/market/Scan.vue')}, {path: 'scan', component: () => import('@/pages/market/Scan.vue')},
{path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')}, {path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')},
{path: 'appraisal', name: routeNames.appraise, component: () => import('@/pages/market/Appraise.vue')},
]}, ]},
{path: '/reprocess', component: () => import('@/pages/Reprocess.vue')}, {path: '/reprocess', component: () => import('@/pages/Reprocess.vue')},