feat(#16): Appraisal page
This commit is contained in:
@@ -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>
|
||||||
@@ -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,
|
||||||
|
}));
|
||||||
|
};
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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')},
|
||||||
|
|||||||
Reference in New Issue
Block a user