Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52addba909 | ||
|
|
81250953eb | ||
|
|
c4803ef650 |
@@ -14,7 +14,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
autoClose: true
|
||||
})
|
||||
|
||||
const isOpen = ref(false);
|
||||
const isOpen = defineModel<boolean>('open', {default: false});
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
const floating = ref<HTMLElement | null>(null);
|
||||
|
||||
|
||||
@@ -44,15 +44,15 @@ const submit = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click="isOpen = true" v-on-click-outside="() => isOpen = false">
|
||||
<div class="relative" @click="isOpen = true" v-on-click-outside="() => isOpen = false">
|
||||
<div class="fake-input" @keyup.enter="submit" @keyup.down="moveDown" @keyup.up="moveUp">
|
||||
<slot name="input" :value="modelValue" />
|
||||
</div>
|
||||
<div v-if="isOpen && items.length" class="z-20 absolute">
|
||||
<div v-if="isOpen && items.length" class="z-20 absolute w-full">
|
||||
<div v-bind="containerProps" class="rounded-b" style="height: 300px">
|
||||
<div v-bind="wrapperProps">
|
||||
<div v-for="s in list" :key="s.index"
|
||||
class="hover:bg-slate-700 cursor-pointer"
|
||||
class="hover:bg-slate-700 cursor-pointer overflow-hidden"
|
||||
:class="s.index === currentIndex ? 'bg-emerald-500' : 'bg-slate-500'"
|
||||
@click.stop="select(s.data)">
|
||||
<slot name="item" :item="s.data" />
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
ReprocessingApi,
|
||||
RuleBookApi,
|
||||
RuleScriptApi,
|
||||
TransactionApi
|
||||
TransactionApi,
|
||||
UniverseApi
|
||||
} from "@/generated/mammon";
|
||||
|
||||
export const mammonUrl = import.meta.env.VITE_MAMMON_URL;
|
||||
@@ -105,4 +106,5 @@ export const ruleScriptApi = new RuleScriptApi(undefined, mammonUrl, mammonAxios
|
||||
export const activityApi = new ActivityApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const reprocessingApi = new ReprocessingApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const reprocessingApi = new ReprocessingApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const universeApi = new UniverseApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
@@ -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,128 @@
|
||||
<script setup lang="ts">
|
||||
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||
import {MarketTypeLabel} from '@/market/type';
|
||||
import {computed} from 'vue';
|
||||
import {MarketLocation} from '../location';
|
||||
import IskLabel from '../IskLabel.vue';
|
||||
import {ComparedItem} from './compare';
|
||||
|
||||
interface Props {
|
||||
result?: ComparedItem[];
|
||||
locations?: MarketLocation[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
result: () => [],
|
||||
locations: () => []
|
||||
});
|
||||
|
||||
const buyKey = (id: number): `buy:${number}` => `buy:${id}`;
|
||||
const sellKey = (id: number): `sell:${number}` => `sell:${id}`;
|
||||
|
||||
type SortableItem = ComparedItem & Record<`buy:${number}` | `sell:${number}`, number>;
|
||||
|
||||
const rows = computed<SortableItem[]>(() => props.result.map(item => {
|
||||
const sortable = {...item} as SortableItem;
|
||||
|
||||
props.locations.forEach(l => {
|
||||
const price = item.prices[l.id];
|
||||
sortable[buyKey(l.id)] = price?.buy ?? 0;
|
||||
sortable[sellKey(l.id)] = price?.sell ?? 0;
|
||||
});
|
||||
|
||||
return sortable;
|
||||
}));
|
||||
|
||||
const {sortedArray, headerProps} = useSort(rows, {
|
||||
defaultSortKey: 'name',
|
||||
defaultSortDirection: 'asc'
|
||||
});
|
||||
|
||||
const bestBuy = (row: ComparedItem): number | undefined => {
|
||||
let best: number | undefined;
|
||||
let bestValue = -Infinity;
|
||||
props.locations.forEach(l => {
|
||||
const cell = row.prices[l.id];
|
||||
if (cell && cell.buy > bestValue) {
|
||||
bestValue = cell.buy;
|
||||
best = l.id;
|
||||
}
|
||||
});
|
||||
return bestValue > 0 ? best : undefined;
|
||||
};
|
||||
|
||||
const bestSell = (row: ComparedItem): number | undefined => {
|
||||
let best: number | undefined;
|
||||
let bestValue = -Infinity;
|
||||
props.locations.forEach(l => {
|
||||
const cell = row.prices[l.id];
|
||||
if (cell && cell.sell > bestValue) {
|
||||
bestValue = cell.sell;
|
||||
best = l.id;
|
||||
}
|
||||
});
|
||||
return bestValue > 0 ? best : undefined;
|
||||
};
|
||||
|
||||
const totals = computed(() => new Map(props.locations.map(l => [l.id, props.result.reduce((acc, row) => {
|
||||
const cell = row.prices[l.id];
|
||||
return {
|
||||
buy: acc.buy + (cell?.buy ?? 0),
|
||||
sell: acc.sell + (cell?.sell ?? 0),
|
||||
};
|
||||
}, {buy: 0, sell: 0})])));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VirtualScrollTable :list="sortedArray" :itemHeight="33" :headerHeight="66" :footerHeight="33" bottom="1rem">
|
||||
<template #default="{ list }">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortableHeader v-bind="headerProps" sortKey="name" rowspan="2">Item</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="quantity" rowspan="2">Qty</SortableHeader>
|
||||
<th v-for="location in locations" :key="location.id" colspan="2" class="text-center border-l border-slate-500" :title="location.name">
|
||||
{{ location.systemName }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<template v-for="location in locations" :key="location.id">
|
||||
<SortableHeader v-bind="headerProps" :sortKey="buyKey(location.id)" class="border-l border-slate-500">Buy</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" :sortKey="sellKey(location.id)">Sell</SortableHeader>
|
||||
</template>
|
||||
</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>
|
||||
<template v-for="location in locations" :key="location.id">
|
||||
<td class="text-right border-l border-slate-500" :class="r.data.prices[location.id] ? (bestBuy(r.data) === location.id ? 'bg-emerald-500' : 'bg-amber-900') : ''">
|
||||
<IskLabel v-if="r.data.prices[location.id]" :amount="r.data.prices[location.id].buy" :colored="false" />
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td class="text-right" :class="r.data.prices[location.id] ? (bestSell(r.data) === location.id ? 'bg-emerald-500' : 'bg-amber-900') : ''">
|
||||
<IskLabel v-if="r.data.prices[location.id]" :amount="r.data.prices[location.id].sell" :colored="false" />
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2" class="font-semibold">Total</td>
|
||||
<template v-for="location in locations" :key="location.id">
|
||||
<td class="text-right border-l border-slate-500"><IskLabel :amount="totals.get(location.id)?.buy ?? 0" :colored="false" /></td>
|
||||
<td class="text-right"><IskLabel :amount="totals.get(location.id)?.sell ?? 0" :colored="false" /></td>
|
||||
</template>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="text-center mt-4">
|
||||
<span>No items found</span>
|
||||
</div>
|
||||
</template>
|
||||
</VirtualScrollTable>
|
||||
</template>
|
||||
@@ -8,4 +8,4 @@ export type MarketTypePrice = {
|
||||
orderCount: number;
|
||||
};
|
||||
|
||||
export type PriceGetter = (types: MarketType[]) => Promise<MarketTypePrice[]>;
|
||||
export type PriceGetter = (types: MarketType[], locationId?: number) => Promise<MarketTypePrice[]>;
|
||||
|
||||
@@ -13,14 +13,14 @@ export const useAppraisalStore = defineStore('appraisal', () => {
|
||||
|
||||
const getPricesUncached = getMammonPrices;
|
||||
|
||||
const getPrice = async (type: MarketType, regionId?: number): Promise<MarketTypePrice> => (await getPrices([type], regionId))[0];
|
||||
const getPrices = async (types: MarketType[], regionId?: number): Promise<MarketTypePrice[]> => {
|
||||
const getPrice = async (type: MarketType, locationId?: number): Promise<MarketTypePrice> => (await getPrices([type], locationId))[0];
|
||||
const getPrices = async (types: MarketType[], locationId?: number): Promise<MarketTypePrice[]> => {
|
||||
const cached: MarketTypePrice[] = [];
|
||||
const uncached: MarketType[] = [];
|
||||
const rId = regionId ?? jitaId;
|
||||
const lId = locationId ?? jitaId;
|
||||
|
||||
types.forEach(t => {
|
||||
const cachedPrice = cache.get(rId, t.id);
|
||||
const cachedPrice = cache.get(lId, t.id);
|
||||
|
||||
if (cachedPrice) {
|
||||
cached.push(cachedPrice);
|
||||
@@ -33,12 +33,12 @@ export const useAppraisalStore = defineStore('appraisal', () => {
|
||||
const batches: Promise<MarketTypePrice[]>[] = [];
|
||||
|
||||
for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
|
||||
batches.push(getPricesUncached(uncached.slice(i, i + BATCH_SIZE)));
|
||||
batches.push(getPricesUncached(uncached.slice(i, i + BATCH_SIZE), lId));
|
||||
}
|
||||
|
||||
const prices = (await Promise.all(batches)).flat();
|
||||
|
||||
prices.forEach(p => cache.set(rId, p.type.id, p));
|
||||
prices.forEach(p => cache.set(lId, p.type.id, p));
|
||||
return [ ...cached, ...prices ];
|
||||
}
|
||||
return cached;
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import {marketApi} from '@/mammon';
|
||||
import {getMarketTypes, MarketType} from '../type';
|
||||
import {MarketTypePrice} from './MarketTypePrice';
|
||||
|
||||
export type LocationPrice = {
|
||||
buyUnit: number;
|
||||
sellUnit: number;
|
||||
buy: number;
|
||||
sell: number;
|
||||
};
|
||||
|
||||
export type ComparedItem = {
|
||||
typeId: number;
|
||||
name: string;
|
||||
quantity: number;
|
||||
volume: number;
|
||||
prices: Record<number, LocationPrice>;
|
||||
};
|
||||
|
||||
export type PricesForLocation = (types: MarketType[], locationId: number) => Promise<MarketTypePrice[]>;
|
||||
|
||||
export const compareAppraisal = async (
|
||||
raw: string,
|
||||
locationIds: number[],
|
||||
getPrices: PricesForLocation
|
||||
): Promise<ComparedItem[]> => {
|
||||
if (!raw?.trim() || locationIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stacks = await marketApi.parseStacks(raw).then(r => r.data);
|
||||
|
||||
if (stacks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const quantities = new Map(stacks.map(s => [s.marketTypeId, s.quantity]));
|
||||
const types = await getMarketTypes(stacks.map(s => s.marketTypeId));
|
||||
|
||||
const rows = new Map<number, ComparedItem>(types.map(t => [t.id, {
|
||||
typeId: t.id,
|
||||
name: t.name,
|
||||
quantity: quantities.get(t.id) ?? 0,
|
||||
volume: t.volume,
|
||||
prices: {},
|
||||
}]));
|
||||
|
||||
const pricesByLocation = await Promise.all(
|
||||
locationIds.map(async locationId => [locationId, await getPrices(types, locationId)] as const)
|
||||
);
|
||||
|
||||
pricesByLocation.forEach(([locationId, prices]) => prices.forEach(p => {
|
||||
const row = rows.get(p.type.id);
|
||||
|
||||
if (row) {
|
||||
row.prices[locationId] = {
|
||||
buyUnit: p.buy,
|
||||
sellUnit: p.sell,
|
||||
buy: p.buy * row.quantity,
|
||||
sell: p.sell * row.quantity,
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
return [...rows.values()];
|
||||
};
|
||||
@@ -1,2 +1,7 @@
|
||||
export * from './MarketTypePrice';
|
||||
export * from './appraisal';
|
||||
export * from './appraise';
|
||||
export * from './compare';
|
||||
|
||||
export { default as AppraisalResultTable } from './AppraisalResultTable.vue';
|
||||
export { default as CompareResultTable } from './CompareResultTable.vue';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {marketApi} from '@/mammon/mammonService';
|
||||
import {MarketTypePrice, PriceGetter} from './MarketTypePrice';
|
||||
|
||||
export const getMammonPrices: PriceGetter = async types => {
|
||||
export const getMammonPrices: PriceGetter = async (types, locationId) => {
|
||||
if (types.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const typesById = new Map(types.map(t => [t.id, t]));
|
||||
const response = await marketApi.currentPrices(types.map(t => t.id));
|
||||
const response = await marketApi.currentPrices(types.map(t => t.id), locationId);
|
||||
|
||||
return response.data.reduce<MarketTypePrice[]>((prices, p) => {
|
||||
const type = typesById.get(p.marketTypeId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './RegionalMarketCache';
|
||||
export * from './history';
|
||||
export * from './location';
|
||||
export * from './order';
|
||||
export * from './tax';
|
||||
export * from './type';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {universeApi} from '@/mammon/mammonService';
|
||||
import type {MarketLocationResponse} from '@/generated/mammon';
|
||||
|
||||
export type MarketLocation = MarketLocationResponse;
|
||||
|
||||
const cache = new Map<number, MarketLocation>();
|
||||
|
||||
const defaultHubIds = [
|
||||
60003760, // Jita IV - Moon 4 - Caldari Navy Assembly Plant
|
||||
60008494, // Amarr VIII (Oris) - Emperor Family Academy
|
||||
60011866, // Dodixie IX - Moon 20 - Federation Navy Assembly Plant
|
||||
60004588, // Rens VI - Moon 8 - Brutor Tribe Treasury
|
||||
60005686, // Hek VIII - Moon 12 - Boundless Creation Factory
|
||||
];
|
||||
|
||||
const hubRank = (id: number) => {
|
||||
const rank = defaultHubIds.indexOf(id);
|
||||
return rank < 0 ? defaultHubIds.length : rank;
|
||||
};
|
||||
|
||||
export const isDefaultHub = (id: number): boolean => defaultHubIds.includes(id);
|
||||
|
||||
export const searchMarketLocations = async (search: string): Promise<MarketLocation[]> => {
|
||||
if (search.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const locations = await universeApi.searchLocations(search).then(r => r.data);
|
||||
locations.forEach(l => cache.set(l.id, l));
|
||||
return locations.toSorted((a, b) => hubRank(a.id) - hubRank(b.id));
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import {watchDebounced} from '@vueuse/core';
|
||||
import {ref, watch} from 'vue';
|
||||
import {SelectInput} from '@/components';
|
||||
import {isDefaultHub, MarketLocation, searchMarketLocations} from './MarketLocation';
|
||||
|
||||
const modelValue = defineModel<MarketLocation>();
|
||||
|
||||
const search = ref(modelValue.value?.name ?? '');
|
||||
const suggestions = ref<MarketLocation[]>([]);
|
||||
|
||||
watch(() => modelValue.value, v => {
|
||||
search.value = v?.name ?? '';
|
||||
});
|
||||
|
||||
watchDebounced(search, async value => {
|
||||
const term = value.trim();
|
||||
|
||||
if (term.length < 3 || term === modelValue.value?.name) {
|
||||
suggestions.value = [];
|
||||
} else {
|
||||
suggestions.value = await searchMarketLocations(term);
|
||||
}
|
||||
}, {debounce: 300});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectInput v-model="modelValue" :items="suggestions" class="w-96">
|
||||
<template #input>
|
||||
<input type="text" v-model="search" placeholder="Search a market location…" />
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<div class="px-1 whitespace-nowrap overflow-hidden text-ellipsis" :class="{'text-emerald-400': isDefaultHub(item.id)}">
|
||||
{{ item.name }} <span class="text-slate-300">({{ item.regionName }})</span>
|
||||
</div>
|
||||
</template>
|
||||
</SelectInput>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/style.css";
|
||||
input {
|
||||
@apply w-full border-none bg-transparent block focus-visible:outline-none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './MarketLocation';
|
||||
export { default as MarketLocationInput } from './MarketLocationInput.vue';
|
||||
@@ -1 +1 @@
|
||||
export const jitaId = 10000002;
|
||||
export const jitaId = 60003760;
|
||||
@@ -15,6 +15,12 @@ import {routeNames} from '@/routes';
|
||||
<RouterLink to="/market/acquisitions" class="tab">
|
||||
<span>Acquisitions</span>
|
||||
</RouterLink>
|
||||
<RouterLink :to="{name: routeNames.appraise}" class="tab">
|
||||
<span>Appraisal</span>
|
||||
</RouterLink>
|
||||
<RouterLink :to="{name: routeNames.compareAppraisal}" class="tab">
|
||||
<span>Compare</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<RouterView />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import {Dropdown} from '@/components';
|
||||
import {AppraisalItemValue, AppraisalResultTable, appraise} from '@/market/appraisal';
|
||||
import {ref} from 'vue';
|
||||
|
||||
const items = ref('');
|
||||
const showInputs = ref(true);
|
||||
|
||||
const result = ref<AppraisalItemValue[]>([]);
|
||||
|
||||
const send = async () => result.value = await appraise(items.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dropdown inline :auto-close="false" v-model:open="showInputs" class="mt-4">
|
||||
<template #button>Items</template>
|
||||
<div class="flex items-stretch mt-2">
|
||||
<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>
|
||||
</Dropdown>
|
||||
<template v-if="result.length > 0">
|
||||
<hr />
|
||||
<div class="grid mt-2">
|
||||
<AppraisalResultTable :result="result" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue';
|
||||
import {Dropdown} from '@/components';
|
||||
import {compareAppraisal, ComparedItem, CompareResultTable, useAppraisalStore} from '@/market/appraisal';
|
||||
import {MarketLocation, MarketLocationInput} from '@/market/location';
|
||||
|
||||
const store = useAppraisalStore();
|
||||
|
||||
const items = ref('');
|
||||
const from = ref<MarketLocation>();
|
||||
const to = ref<MarketLocation>();
|
||||
const showInputs = ref(true);
|
||||
|
||||
const result = ref<ComparedItem[]>([]);
|
||||
const resultLocations = ref<MarketLocation[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const send = async () => {
|
||||
const selected = [from.value, to.value].filter((l): l is MarketLocation => l !== undefined);
|
||||
loading.value = true;
|
||||
try {
|
||||
result.value = await compareAppraisal(items.value, selected.map(l => l.id), store.getPrices);
|
||||
resultLocations.value = selected;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dropdown inline :auto-close="false" v-model:open="showInputs" class="mt-4">
|
||||
<template #button>Items & locations</template>
|
||||
<div class="flex items-stretch mt-2">
|
||||
<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="flex flex-wrap gap-6 mx-1 mt-2">
|
||||
<div>
|
||||
<span>From</span>
|
||||
<div class="mt-1">
|
||||
<MarketLocationInput v-model="from" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>To</span>
|
||||
<div class="mt-1">
|
||||
<MarketLocationInput v-model="to" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid my-2">
|
||||
<button class="justify-self-end" @click="send" :disabled="!items.trim() || !from || !to || loading">Send</button>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<template v-if="result.length > 0">
|
||||
<hr />
|
||||
<div class="grid mt-2">
|
||||
<CompareResultTable :result="result" :locations="resultLocations" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -15,6 +15,8 @@ export const routeNames = {
|
||||
listLedgerAcquisitions: 'list-ledger-acquisitions',
|
||||
editRuleBook: 'edit-rule-book',
|
||||
marketTypes: 'market-types',
|
||||
appraise: 'appraise',
|
||||
compareAppraisal: 'compare-appraisal',
|
||||
about: 'about',
|
||||
} as const;
|
||||
|
||||
@@ -39,6 +41,8 @@ export const routes: RouteRecordRaw[] = [
|
||||
{path: 'types/:type?', name: routeNames.marketTypes, component: () => import('@/pages/market/TypeInfo.vue')},
|
||||
{path: 'scan', component: () => import('@/pages/market/Scan.vue')},
|
||||
{path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')},
|
||||
{path: 'appraisal', name: routeNames.appraise, component: () => import('@/pages/market/Appraise.vue')},
|
||||
{path: 'appraisal/compare', name: routeNames.compareAppraisal, component: () => import('@/pages/market/CompareAppraisal.vue')},
|
||||
]},
|
||||
|
||||
{path: '/reprocess', component: () => import('@/pages/Reprocess.vue')},
|
||||
|
||||
Reference in New Issue
Block a user