improve the bill of production

This commit is contained in:
2026-03-05 02:48:33 -05:00
parent 4214eeb659
commit d8386dccf4
8 changed files with 279 additions and 105 deletions

View File

@@ -41,36 +41,26 @@
</td>
<!-- Effective material cost (after RRR) -->
<td class="px-4 py-3 text-sm font-mono text-gray-300"
<td class="px-4 py-3 text-right text-sm font-mono text-gray-300"
:title="result.effectiveMaterialCost > 0 ? result.effectiveMaterialCost.toLocaleString() : undefined">
{{ formatSilver(result.effectiveMaterialCost) }}
</td>
<!-- +15% markup -->
<td class="px-4 py-3 text-sm font-mono text-blue-300"
<!-- +15% markup (no focus) -->
<td class="px-4 py-3 text-right text-sm font-mono text-blue-300"
:title="result.effectiveMaterialCost > 0 ? Math.round(result.effectiveMaterialCost * 1.15).toLocaleString() : undefined">
{{ result.effectiveMaterialCost > 0 ? formatSilver(Math.round(result.effectiveMaterialCost * 1.15)) : '—' }}
</td>
<!-- +30% markup -->
<td class="px-4 py-3 text-sm font-mono text-emerald-300"
<!-- +30% markup (no focus) -->
<td class="px-4 py-3 text-right text-sm font-mono text-emerald-300"
:title="result.effectiveMaterialCost > 0 ? Math.round(result.effectiveMaterialCost * 1.30).toLocaleString() : undefined">
{{ result.effectiveMaterialCost > 0 ? formatSilver(Math.round(result.effectiveMaterialCost * 1.30)) : '—' }}
</td>
<!-- Price age -->
<td class="px-4 py-3">
<span
class="inline-block w-2.5 h-2.5 rounded-full"
:class="ageDotClass(result)"
:title="ageDotTitle(result)"
/>
</td>
<!-- Add to bill -->
<!-- Add to bill (no focus) -->
<td class="px-3 py-3 text-right whitespace-nowrap" @click.stop>
<!-- Inline qty form -->
<div v-if="addingToBill" class="inline-flex items-center gap-1">
<div v-if="addingMode === 'nofocus'" class="inline-flex items-center gap-1">
<input
v-focus
type="number"
@@ -83,31 +73,90 @@
<button class="text-amber-500 hover:text-amber-300 text-xs leading-none" @click="confirmBill"></button>
<button class="text-gray-500 hover:text-gray-300 text-xs leading-none" @click="cancelBill"></button>
</div>
<!-- In-order badge + remove -->
<div v-else-if="isInOrder" class="inline-flex items-center gap-1">
<div v-else-if="isInOrderNoFocus" class="inline-flex items-center gap-1">
<button
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-600/20 border border-amber-600/40 text-amber-400 text-xs font-mono hover:border-amber-500 transition-colors"
@click="startBill"
>×{{ currentQty }}</button>
@click="startBill('nofocus')"
>×{{ currentQtyNoFocus }}</button>
<button
class="text-gray-600 hover:text-red-400 transition-colors text-xs leading-none"
title="Remove from order"
@click="remove(result.recipe.outputItemId)"
@click="remove(result.recipe.outputItemId, false)"
></button>
</div>
<!-- Add button -->
<button
v-else
class="w-6 h-6 rounded-full bg-gray-700 hover:bg-amber-600/30 hover:text-amber-400 text-gray-400 text-sm leading-none transition-colors inline-flex items-center justify-center"
title="Add to Bill of Production"
@click="startBill"
title="Add to Bill of Production (no focus)"
@click="startBill('nofocus')"
>+</button>
</td>
<!-- Cost (with focus) -->
<td class="px-4 py-3 text-right text-sm font-mono text-violet-300 border-l border-gray-700/40"
:title="result.effectiveMaterialCostFocus > 0 ? result.effectiveMaterialCostFocus.toLocaleString() : undefined">
{{ formatSilver(result.effectiveMaterialCostFocus) }}
</td>
<!-- +15% markup (with focus) -->
<td class="px-4 py-3 text-right text-sm font-mono text-blue-300/70"
:title="result.effectiveMaterialCostFocus > 0 ? Math.round(result.effectiveMaterialCostFocus * 1.15).toLocaleString() : undefined">
{{ result.effectiveMaterialCostFocus > 0 ? formatSilver(Math.round(result.effectiveMaterialCostFocus * 1.15)) : '—' }}
</td>
<!-- +30% markup (with focus) -->
<td class="px-4 py-3 text-right text-sm font-mono text-emerald-300/70"
:title="result.effectiveMaterialCostFocus > 0 ? Math.round(result.effectiveMaterialCostFocus * 1.30).toLocaleString() : undefined">
{{ result.effectiveMaterialCostFocus > 0 ? formatSilver(Math.round(result.effectiveMaterialCostFocus * 1.30)) : '—' }}
</td>
<!-- Add to bill (with focus) -->
<td class="px-3 py-3 text-right whitespace-nowrap" @click.stop>
<div v-if="addingMode === 'focus'" class="inline-flex items-center gap-1">
<input
v-focus
type="number"
min="1"
v-model.number="billQty"
class="w-12 bg-gray-900 border border-violet-500 rounded px-1.5 py-0.5 text-center text-xs font-mono text-gray-200 focus:outline-none"
@keydown.enter="confirmBill"
@keydown.escape="cancelBill"
/>
<button class="text-violet-400 hover:text-violet-200 text-xs leading-none" @click="confirmBill"></button>
<button class="text-gray-500 hover:text-gray-300 text-xs leading-none" @click="cancelBill"></button>
</div>
<div v-else-if="isInOrderFocus" class="inline-flex items-center gap-1">
<button
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-violet-600/20 border border-violet-600/40 text-violet-400 text-xs font-mono hover:border-violet-500 transition-colors"
@click="startBill('focus')"
>×{{ currentQtyFocus }}</button>
<button
class="text-gray-600 hover:text-red-400 transition-colors text-xs leading-none"
title="Remove from order"
@click="remove(result.recipe.outputItemId, true)"
></button>
</div>
<button
v-else
class="w-6 h-6 rounded-full bg-gray-700 hover:bg-violet-600/30 hover:text-violet-400 text-gray-400 text-sm leading-none transition-colors inline-flex items-center justify-center"
title="Add to Bill of Production (with focus)"
@click="startBill('focus')"
>+</button>
</td>
<!-- Status -->
<td class="px-4 py-3 text-center">
<span
class="inline-block w-2.5 h-2.5 rounded-full"
:class="ageDotClass(result)"
:title="ageDotTitle(result)"
/>
</td>
</tr>
<!-- Expanded detail row -->
<tr v-if="expanded" class="border-b border-gray-700/50 bg-gray-900/60">
<td colspan="9" class="px-6 py-4">
<td colspan="13" class="px-6 py-4">
<div class="flex gap-8">
<!-- Ingredients breakdown -->
@@ -184,13 +233,13 @@
<span class="text-gray-400">Raw Materials</span>
<span class="text-gray-200">{{ formatSilver(result.materialCost) }}</span>
</div>
<div class="flex justify-between gap-4">
<span class="text-gray-400">RRR ({{ result.rrr.toFixed(1) }}%)</span>
<span class="text-green-400">{{ formatSilver(result.materialCost - result.effectiveMaterialCost) }}</span>
</div>
<div class="flex justify-between gap-4 border-t border-gray-700/50 pt-1.5">
<span class="text-gray-300 font-semibold">Craft Cost</span>
<span class="font-semibold text-gray-100">{{ formatSilver(result.effectiveMaterialCost) }}</span>
<span class="text-gray-400">RRR ({{ result.rrr.toFixed(1) }}%)</span>
<span class="text-gray-200">{{ formatSilver(result.effectiveMaterialCost) }}</span>
</div>
<div class="flex justify-between gap-4">
<span class="text-violet-400/80">RRR + Focus ({{ result.rrrFocus.toFixed(1) }}%)</span>
<span class="text-violet-300">{{ formatSilver(result.effectiveMaterialCostFocus) }}</span>
</div>
</div>
</div>
@@ -241,24 +290,30 @@ const inputValue = ref('')
// ─── Bill of Production ───────────────────────────────────────────────────────
const addingToBill = ref(false)
const billQty = ref(1)
const addingMode = ref<null | 'nofocus' | 'focus'>(null)
const billQty = ref(1)
const isInOrder = computed(() => inOrder(props.result.recipe.outputItemId))
const currentQty = computed(() => getQty(props.result.recipe.outputItemId))
const isInOrderNoFocus = computed(() => inOrder(props.result.recipe.outputItemId, false))
const currentQtyNoFocus = computed(() => getQty(props.result.recipe.outputItemId, false))
const isInOrderFocus = computed(() => inOrder(props.result.recipe.outputItemId, true))
const currentQtyFocus = computed(() => getQty(props.result.recipe.outputItemId, true))
function startBill() {
billQty.value = isInOrder.value ? currentQty.value : 1
addingToBill.value = true
function startBill(mode: 'nofocus' | 'focus') {
const already = mode === 'nofocus' ? isInOrderNoFocus.value : isInOrderFocus.value
const qty = mode === 'nofocus' ? currentQtyNoFocus.value : currentQtyFocus.value
billQty.value = already ? qty : 1
addingMode.value = mode
}
function confirmBill() {
if (billQty.value > 0) upsert(props.result.recipe, billQty.value)
addingToBill.value = false
if (billQty.value > 0 && addingMode.value) {
upsert(props.result.recipe, billQty.value, addingMode.value === 'focus')
}
addingMode.value = null
}
function cancelBill() {
addingToBill.value = false
addingMode.value = null
}
// ─── Price age display ────────────────────────────────────────────────────────

View File

@@ -1,21 +1,63 @@
<template>
<thead class="sticky top-0 z-10">
<!-- Group row -->
<tr class="bg-gray-800 border-b border-gray-700/50">
<th colspan="4" class="bg-gray-800" />
<th colspan="4" class="px-4 py-1.5 text-center text-[10px] font-semibold text-gray-500 uppercase tracking-wider border-l border-gray-700/60">
No Focus
</th>
<th colspan="4" class="px-4 py-1.5 text-center text-[10px] font-semibold text-violet-400/70 uppercase tracking-wider border-l border-gray-700/60">
With Focus
</th>
<th colspan="1" class="bg-gray-800" />
</tr>
<!-- Column row -->
<tr class="bg-gray-800 border-b border-gray-700">
<!-- Fixed left cols -->
<th class="px-3 py-2 w-4" />
<th
v-for="col in columns"
:key="col.field"
class="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider whitespace-nowrap select-none"
v-for="col in leftCols" :key="col.field"
class="px-4 py-2 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider whitespace-nowrap select-none"
:class="col.sortable ? 'cursor-pointer hover:text-gray-200 transition-colors' : ''"
@click="col.sortable && col.field !== 'status' ? $emit('sort', col.field as SortField) : undefined"
@click="col.sortable ? $emit('sort', col.field as SortField) : undefined"
>
<span class="flex items-center gap-1">
{{ col.label }}
<span v-if="col.sortable && sortState.field === col.field" class="text-amber-400">
{{ sortState.direction === 'asc' ? '' : '' }}
</span>
<span v-if="sortState.field === col.field" class="text-amber-400">{{ sortState.direction === 'asc' ? '' : '' }}</span>
<span v-else class="text-gray-600"></span>
</span>
</th>
<!-- No-focus group -->
<th
v-for="col in priceCols" :key="'nf-' + col.field"
class="px-4 py-2 text-right text-xs font-semibold text-gray-400 uppercase tracking-wider whitespace-nowrap select-none border-l border-gray-700/60"
:class="[col.sortable ? 'cursor-pointer hover:text-gray-200 transition-colors' : '', col.first ? 'border-l border-gray-700/60' : '']"
@click="col.sortable ? $emit('sort', col.field as SortField) : undefined"
>
<span class="flex items-center gap-1">
{{ col.label }}
<span v-if="col.sortable && sortState.field === col.field" class="text-amber-400">{{ sortState.direction === 'asc' ? '' : '' }}</span>
<span v-else-if="col.sortable" class="text-gray-600"></span>
</span>
</th>
<th class="px-3 py-2 w-8" />
<!-- With-focus group -->
<th
v-for="col in priceCols" :key="'f-' + col.field"
class="px-4 py-2 text-right text-xs font-semibold text-violet-400/70 uppercase tracking-wider whitespace-nowrap select-none border-l border-gray-700/60"
:class="col.first ? 'border-l border-gray-700/60' : ''"
>
{{ col.label }}
</th>
<th class="px-3 py-2 w-8" />
<!-- Status -->
<th class="px-4 py-2 text-center text-xs font-semibold text-gray-400 uppercase tracking-wider whitespace-nowrap select-none">
Status
</th>
</tr>
</thead>
</template>
@@ -23,23 +65,19 @@
<script setup lang="ts">
import type { SortField, SortState } from '../../types/crafting'
const columns: { field: SortField | 'status' | 'bill' | 'markup15' | 'markup30' | 'expand'; label: string; sortable: boolean }[] = [
{ field: 'expand', label: '', sortable: false },
{ field: 'variantType', label: 'Variant', sortable: true },
{ field: 'displayName', label: 'Item', sortable: true },
{ field: 'tier', label: 'Tier', sortable: true },
{ field: 'materialCost', label: 'Cost', sortable: true },
{ field: 'markup15', label: '+15%', sortable: false },
{ field: 'markup30', label: '+30%', sortable: false },
{ field: 'status', label: 'Price Age', sortable: false },
{ field: 'bill', label: '', sortable: false },
const leftCols = [
{ field: 'variantType' as SortField, label: 'Variant', sortable: true },
{ field: 'displayName' as SortField, label: 'Item', sortable: true },
{ field: 'tier' as SortField, label: 'Tier', sortable: true },
]
defineProps<{
sortState: SortState
}>()
const priceCols = [
{ field: 'materialCost' as SortField, label: 'Cost', sortable: true, first: true },
{ field: 'markup15' as const, label: '+15%', sortable: false, first: false },
{ field: 'markup30' as const, label: '+30%', sortable: false, first: false },
]
defineEmits<{
sort: [field: SortField]
}>()
defineProps<{ sortState: SortState }>()
defineEmits<{ sort: [field: SortField] }>()
</script>