improve the bill of production
This commit is contained in:
@@ -23,16 +23,33 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-700 bg-gray-700/30">
|
||||
<th class="text-left px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider">Item</th>
|
||||
<th class="text-center px-3 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider w-28">Qty</th>
|
||||
<th class="text-right px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider">Cost / unit</th>
|
||||
<th
|
||||
class="text-left px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-200 transition-colors select-none"
|
||||
@click="toggleSort('name')"
|
||||
><span class="flex items-center gap-1">Item <span class="text-amber-400">{{ sortIndicator('name') }}</span></span></th>
|
||||
<th
|
||||
class="text-left px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-200 transition-colors select-none"
|
||||
@click="toggleSort('station')"
|
||||
><span class="flex items-center gap-1">Station <span class="text-amber-400">{{ sortIndicator('station') }}</span></span></th>
|
||||
<th
|
||||
class="text-left px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-200 transition-colors select-none"
|
||||
@click="toggleSort('focus')"
|
||||
><span class="flex items-center gap-1">Focus <span class="text-amber-400">{{ sortIndicator('focus') }}</span></span></th>
|
||||
<th
|
||||
class="text-center px-3 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider w-28 cursor-pointer hover:text-gray-200 transition-colors select-none"
|
||||
@click="toggleSort('qty')"
|
||||
><span class="flex items-center justify-center gap-1">Qty <span class="text-amber-400">{{ sortIndicator('qty') }}</span></span></th>
|
||||
<th
|
||||
class="text-right px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider cursor-pointer hover:text-gray-200 transition-colors select-none"
|
||||
@click="toggleSort('cost')"
|
||||
><span class="flex items-center justify-end gap-1">Cost / unit <span class="text-amber-400">{{ sortIndicator('cost') }}</span></span></th>
|
||||
<th class="w-8"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in computedItems"
|
||||
:key="item.recipe.outputItemId"
|
||||
v-for="item in sortedItems"
|
||||
:key="`${item.recipe.outputItemId}|${item.focus ? 'f' : 'b'}`"
|
||||
class="border-t border-gray-700/40 hover:bg-gray-700/20"
|
||||
>
|
||||
<td class="px-4 py-2">
|
||||
@@ -43,16 +60,22 @@
|
||||
class="w-12 h-12 -my-2 rounded flex-shrink-0"
|
||||
/>
|
||||
<span class="text-gray-200 font-medium">{{ item.recipe.displayName }}</span>
|
||||
<span class="text-xs text-gray-500">{{ item.recipe.category }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold" :class="stationClass(item.recipe.journalType)">{{ stationLabel(item.recipe.journalType) }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<span v-if="item.focus" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-violet-600/20 border border-violet-600/40 text-violet-400">Focus</span>
|
||||
<span v-else class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-gray-700/50 border border-gray-600/40 text-gray-400">No Focus</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
:value="item.qty"
|
||||
class="w-20 bg-gray-900 border border-gray-700 rounded px-2 py-0.5 text-center text-sm font-mono text-gray-200 focus:outline-none focus:border-amber-500"
|
||||
@change="upsert(item.recipe, Number(($event.target as HTMLInputElement).value))"
|
||||
@change="upsert(item.recipe, Number(($event.target as HTMLInputElement).value), item.focus)"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-right font-mono text-gray-300">
|
||||
@@ -63,7 +86,7 @@
|
||||
<button
|
||||
class="text-gray-600 hover:text-red-400 transition-colors text-sm leading-none"
|
||||
title="Remove"
|
||||
@click="remove(item.recipe.outputItemId)"
|
||||
@click="remove(item.recipe.outputItemId, item.focus)"
|
||||
>✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -209,7 +232,7 @@ import { useAlbionPrices } from '../composables/useAlbionPrices'
|
||||
import { useFilters } from '../composables/useFilters'
|
||||
import { formatSilver, formatItemId, formatLastUpdated, tierStyle, itemImageUrl } from '../utils/formatting'
|
||||
import { fameTypeOf, craftsPerJournal } from '../data/constants'
|
||||
import { cityRrr, isRrrExempt } from '../data/cityBonuses'
|
||||
import { localProductionBonus, rrrFromBonus, FOCUS_LPB, isRrrExempt } from '../data/cityBonuses'
|
||||
import type { Tier, JournalType } from '../types/crafting'
|
||||
|
||||
const vFocus = { mounted: (el: HTMLElement) => el.focus() }
|
||||
@@ -261,6 +284,43 @@ function priceTitle(itemId: string, currentPrice: number): string {
|
||||
return exact ? `${exact} — click to set price` : 'Click to set price'
|
||||
}
|
||||
|
||||
// ─── Order sort ───────────────────────────────────────────────────────────────
|
||||
|
||||
type OrderSortField = 'name' | 'station' | 'focus' | 'qty' | 'cost'
|
||||
const orderSortField = ref<OrderSortField>('name')
|
||||
const orderSortDir = ref<'asc' | 'desc'>('asc')
|
||||
|
||||
function toggleSort(field: OrderSortField) {
|
||||
if (orderSortField.value === field) {
|
||||
orderSortDir.value = orderSortDir.value === 'asc' ? 'desc' : 'asc'
|
||||
} else {
|
||||
orderSortField.value = field
|
||||
orderSortDir.value = 'asc'
|
||||
}
|
||||
}
|
||||
|
||||
function sortIndicator(field: OrderSortField): string {
|
||||
if (orderSortField.value !== field) return ''
|
||||
return orderSortDir.value === 'asc' ? '↑' : '↓'
|
||||
}
|
||||
|
||||
const STATION_LABEL: Record<JournalType, string> = {
|
||||
warrior: "Warrior's Forge",
|
||||
hunter: "Hunter's Lodge",
|
||||
mage: "Mage's Tower",
|
||||
toolmaker: "Toolmaker's Workshop",
|
||||
}
|
||||
|
||||
const STATION_CLASS: Record<JournalType, string> = {
|
||||
warrior: 'bg-red-900/30 border border-red-700/40 text-red-400',
|
||||
hunter: 'bg-green-900/30 border border-green-700/40 text-green-400',
|
||||
mage: 'bg-blue-900/30 border border-blue-700/40 text-blue-400',
|
||||
toolmaker: 'bg-orange-900/30 border border-orange-700/40 text-orange-400',
|
||||
}
|
||||
|
||||
function stationLabel(type: JournalType): string { return STATION_LABEL[type] }
|
||||
function stationClass(type: JournalType): string { return STATION_CLASS[type] }
|
||||
|
||||
const JOURNAL_DISPLAY_NAME: Record<JournalType, string> = {
|
||||
warrior: "Blacksmith's",
|
||||
hunter: "Fletcher's",
|
||||
@@ -280,7 +340,7 @@ const JOURNAL_ITEM_ID: Record<JournalType, string> = {
|
||||
const computedItems = computed(() => {
|
||||
const city = filters.value.city
|
||||
|
||||
return orderItems.value.map(({ recipe, qty }) => {
|
||||
return orderItems.value.map(({ recipe, qty, focus }) => {
|
||||
let basicCost = 0
|
||||
let artefactCost = 0
|
||||
let missingPrices = false
|
||||
@@ -296,9 +356,24 @@ const computedItems = computed(() => {
|
||||
}
|
||||
|
||||
const rawCost = basicCost + artefactCost
|
||||
const rrr = cityRrr(city, recipe.outputItemId)
|
||||
const lpb = localProductionBonus(city, recipe.outputItemId)
|
||||
const rrr = rrrFromBonus(focus ? lpb + FOCUS_LPB : lpb)
|
||||
const craftCostPerUnit = basicCost * (1 - rrr / 100) + artefactCost
|
||||
return { recipe, qty, rawCostPerUnit: rawCost, craftCostPerUnit, totalCraftCost: craftCostPerUnit * qty, rrr, missingPrices }
|
||||
return { recipe, qty, focus, rawCostPerUnit: rawCost, craftCostPerUnit, totalCraftCost: craftCostPerUnit * qty, rrr, missingPrices }
|
||||
})
|
||||
})
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
const dir = orderSortDir.value === 'asc' ? 1 : -1
|
||||
return [...computedItems.value].sort((a, b) => {
|
||||
switch (orderSortField.value) {
|
||||
case 'name': return dir * a.recipe.displayName.localeCompare(b.recipe.displayName)
|
||||
case 'station': return dir * a.recipe.journalType.localeCompare(b.recipe.journalType)
|
||||
case 'focus': return dir * (Number(a.focus) - Number(b.focus))
|
||||
case 'qty': return dir * (a.qty - b.qty)
|
||||
case 'cost': return dir * (a.craftCostPerUnit - b.craftCostPerUnit)
|
||||
default: return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user