修复客户订单列设置持久化并统一最新计费量展示
This commit is contained in:
@@ -443,18 +443,16 @@ function clearanceResultStatusLabel(value: MerchantClearanceResult) {
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<template #footer><el-button @click="close">关闭</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(.customs-documents-dialog.el-dialog) { display: flex; max-height: 70vh; flex-direction: column; overflow: hidden; padding: 0; border: 1px solid var(--client-border); border-radius: var(--client-radius); }
|
||||
:global(.customs-documents-dialog .el-dialog__header), :global(.customs-documents-dialog .el-dialog__footer) { flex: 0 0 auto; }
|
||||
:global(.customs-documents-dialog .el-dialog__header) { flex: 0 0 auto; }
|
||||
:global(.customs-documents-dialog .el-dialog__header) { margin: 0; padding: 18px 48px 18px 20px; border-bottom: 1px solid var(--client-border); }
|
||||
:global(.customs-documents-dialog .el-dialog__title) { color: var(--client-text); font-size: 16px; font-weight: 600; line-height: 24px; }
|
||||
:global(.customs-documents-dialog .el-dialog__headerbtn) { top: 8px; right: 8px; }
|
||||
:global(.customs-documents-dialog .el-dialog__body) { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 20px; color: var(--client-text); line-height: 1.6; overscroll-behavior: contain; }
|
||||
:global(.customs-documents-dialog .el-dialog__footer) { padding: 14px 20px; border-top: 1px solid var(--client-border); background: var(--client-header-bg); }
|
||||
.customs-error { display: grid; gap: 10px; justify-items: start; padding: 22px; border: 1px solid #f1b8b8; background: #fff7f7; color: #9f2d2d; }
|
||||
.customs-error span { overflow-wrap: anywhere; font-size: 12px; }
|
||||
.customs-summary { display: grid; grid-template-columns: minmax(0, 1.6fr) repeat(2, minmax(0, 1fr)) auto; align-items: center; gap: 18px; padding: 16px; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: var(--client-header-bg); }
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { compile, computed, createSSRApp, h, inject, provide } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import ts from 'typescript'
|
||||
|
||||
const source = readFileSync(new URL('./InboundBillingPanel.vue', import.meta.url), 'utf8')
|
||||
const confirmationBody = source.slice(source.indexOf('<dl class="fee-dialog-summary"'), source.indexOf('<div class="fee-confirm-notice"'))
|
||||
const formatterSource = readFileSync(new URL('../utils/inboundBilling.ts', import.meta.url), 'utf8')
|
||||
const formatters = await import(`data:text/javascript;base64,${Buffer.from(ts.transpileModule(formatterSource, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText).toString('base64')}`)
|
||||
|
||||
async function renderConfirmation(data) {
|
||||
const app = createSSRApp({
|
||||
render: compile(`<div>${confirmationBody}</div>`),
|
||||
setup: () => ({ props: { data }, ...formatters }),
|
||||
})
|
||||
app.component('ElTable', {
|
||||
props: ['data'],
|
||||
setup(tableProps, { slots }) {
|
||||
provide('billingRows', computed(() => tableProps.data))
|
||||
return () => h('table', slots.default?.())
|
||||
},
|
||||
})
|
||||
app.component('ElTableColumn', {
|
||||
props: ['label', 'prop'],
|
||||
setup(columnProps, { slots }) {
|
||||
const rows = inject('billingRows')
|
||||
return () => h('tr', [h('th', columnProps.label), ...rows.value.map(row =>
|
||||
h('td', slots.default?.({ row }) ?? row[columnProps.prop] ?? ''))])
|
||||
},
|
||||
})
|
||||
return renderToString(app)
|
||||
}
|
||||
|
||||
test('renders loading, pending, isolated error, and published states', () => {
|
||||
assert.match(source, /v-if="props\.loading"/)
|
||||
@@ -45,17 +76,55 @@ test('shows the published merchant inbound, measurement details and customer con
|
||||
assert.match(source, /getCalculationRuleLabel\(row\.calculationRuleVersion\)/)
|
||||
})
|
||||
|
||||
test('matches the confirmed billing prototype summary, table, notice and footer', () => {
|
||||
test('keeps the billing summary, confirmation table, notice and footer', () => {
|
||||
assert.match(source, /width="min\(1180px, calc\(100vw - 40px\)\)"/)
|
||||
assert.match(source, /class="fee-dialog-summary" aria-label="计费汇总"/)
|
||||
for (const label of ['计算重', '单价', '总金额', '箱规长(cm)', '箱规宽(cm)', '箱规高(cm)', '每箱体积', '总体积', '总材积重']) assert.ok(source.includes(label), `missing prototype label: ${label}`)
|
||||
for (const label of ['计算重', '单价', '总金额', '计费毛重', '计费体积', '计费方式', '最终计费量']) assert.ok(confirmationBody.includes(label), `missing billing label: ${label}`)
|
||||
assert.match(source, /class="fee-confirm-notice"/)
|
||||
assert.match(source, /请核对计算重、单价和总金额。确认后当前费用版本将锁定。/)
|
||||
assert.match(source, /请核对计费毛重、计费体积、计费量、单价和总金额。确认后当前费用版本将锁定。/)
|
||||
assert.match(source, /确认计费量和费用/)
|
||||
assert.match(source, /\.fee-dialog-summary \{ display:grid; grid-template-columns:repeat\(3,minmax\(0,1fr\)\)/)
|
||||
assert.match(source, /\.fee-dialog-table :deep\(\.el-table__body td\.el-table__cell\)/)
|
||||
})
|
||||
|
||||
test('confirms the selected billing weight and volume even when outer carton totals differ', async () => {
|
||||
const html = await renderConfirmation({
|
||||
latestFee: {
|
||||
actualWeightKg: 570.5, actualVolumeCbm: 3.6894,
|
||||
chargeableQuantity: 616.1298, chargeMode: 'KG', chargeUnit: 'KG',
|
||||
unitPrice: 13.6, totalAmount: 8379.37, currency: 'CNY',
|
||||
},
|
||||
billingBasis: { chargeableWeightKg: 650.298 },
|
||||
measurementItems: [{ lengthCm: 63, widthCm: 53, heightCm: 53, packageCount: 22,
|
||||
unitVolumeCbm: 0.177, totalVolumeCbm: 3.894, volumeWeightKg: 650.298 }],
|
||||
})
|
||||
|
||||
assert.match(html, /570\.5 kg/)
|
||||
assert.match(html, /3\.6894 m³/)
|
||||
assert.match(html, /616\.13 kg/)
|
||||
assert.match(html, /8,379\.37/)
|
||||
assert.doesNotMatch(html, /3\.894|650\.298|0\.177/)
|
||||
assert.doesNotMatch(confirmationBody, /measurementItems|billingBasis|箱规长|每箱体积/)
|
||||
})
|
||||
|
||||
test('uses the published volume charge quantity and retains six decimal places', async () => {
|
||||
const html = await renderConfirmation({
|
||||
latestFee: {
|
||||
actualWeightKg: 600, actualVolumeCbm: 1.234567,
|
||||
chargeableQuantity: 1.652893, chargeMode: 'CBM', chargeUnit: 'CBM',
|
||||
unitPrice: 100, totalAmount: 165.29, currency: 'CNY',
|
||||
},
|
||||
billingBasis: { chargeableWeightKg: 600 }, measurementItems: [],
|
||||
})
|
||||
|
||||
assert.match(html, /计费量/)
|
||||
assert.match(html, /按体积计费/)
|
||||
assert.match(html, /600 kg/)
|
||||
assert.match(html, /1\.234567 m³/)
|
||||
assert.match(html, /1\.652893 m³/)
|
||||
assert.doesNotMatch(html, /1\.652893 kg/)
|
||||
})
|
||||
|
||||
test('stacks core information for 720 and 360 pixel layouts', () => {
|
||||
assert.match(source, /@media \(max-width: 720px\)/)
|
||||
assert.match(source, /\.inbound-overview, \.inbound-measurements, \.fee-calculation-grid \{ grid-template-columns: 1fr;/)
|
||||
|
||||
@@ -172,8 +172,8 @@ async function confirmFee() {
|
||||
props.data.latestFee.roundingMode,
|
||||
) }}</dd>
|
||||
</div>
|
||||
<div><dt>实际重量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>实际体积</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualVolumeCbm, 'm³') }}</dd></div>
|
||||
<div><dt>计费毛重</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>计费体积</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualVolumeCbm, 'm³', 6) }}</dd></div>
|
||||
<div><dt>体积重量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.convertedQuantity, props.data.latestFee.chargeUnit) }}</dd></div>
|
||||
<div><dt>最终计费量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.chargeableQuantity, props.data.latestFee.chargeUnit) }}</dd></div>
|
||||
<div><dt>单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit }}</dd></div>
|
||||
@@ -214,26 +214,20 @@ async function confirmFee() {
|
||||
</div>
|
||||
<template v-else>
|
||||
<dl class="fee-dialog-summary" aria-label="计费汇总">
|
||||
<div><dt>计算重</dt><dd>{{ formatInboundMeasure(props.data.billingBasis.chargeableWeightKg, 'kg') }}</dd></div>
|
||||
<div><dt>{{ props.data.latestFee.chargeUnit === 'KG' ? '计算重' : '计费量' }}</dt><dd>{{ formatInboundMeasure(props.data.latestFee.chargeableQuantity, props.data.latestFee.chargeUnit === 'KG' ? 'kg' : 'm³', props.data.latestFee.chargeUnit === 'KG' ? 3 : 6) }}</dd></div>
|
||||
<div><dt>单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit.toUpperCase() }}</dd></div>
|
||||
<div><dt>总金额</dt><dd>{{ formatInboundMoney(props.data.latestFee.totalAmount, props.data.latestFee.currency) }}</dd></div>
|
||||
</dl>
|
||||
<el-table class="measurement-detail-table fee-dialog-table" :data="props.data.measurementItems" border empty-text="暂无箱规测量明细">
|
||||
<el-table-column type="index" label="#" width="54" />
|
||||
<el-table-column prop="lengthCm" label="箱规长(cm)" min-width="105" />
|
||||
<el-table-column prop="widthCm" label="箱规宽(cm)" min-width="105" />
|
||||
<el-table-column prop="heightCm" label="箱规高(cm)" min-width="105" />
|
||||
<el-table-column prop="packageCount" label="箱数" min-width="76" />
|
||||
<el-table-column label="每箱体积" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.unitVolumeCbm, 'm³') }}</template></el-table-column>
|
||||
<el-table-column label="总体积" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.totalVolumeCbm, 'm³') }}</template></el-table-column>
|
||||
<el-table-column label="总材积重" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.volumeWeightKg, 'kg') }}</template></el-table-column>
|
||||
<el-table-column label="箱规规则" min-width="170"><template #default="{ row }">{{ getCalculationRuleLabel(row.calculationRuleVersion) }}</template></el-table-column>
|
||||
<el-table-column prop="boxSpecification" label="备注" min-width="130" />
|
||||
<el-table class="measurement-detail-table fee-dialog-table" :data="[props.data.latestFee]" border>
|
||||
<el-table-column label="计费毛重" min-width="180"><template #default="{ row }">{{ formatInboundMeasure(row.actualWeightKg, 'kg') }}</template></el-table-column>
|
||||
<el-table-column label="计费体积" min-width="180"><template #default="{ row }">{{ formatInboundMeasure(row.actualVolumeCbm, 'm³', 6) }}</template></el-table-column>
|
||||
<el-table-column label="计费方式" min-width="150"><template #default="{ row }">{{ getChargeModeLabel(row.chargeMode) }}</template></el-table-column>
|
||||
<el-table-column label="最终计费量" min-width="180"><template #default="{ row }">{{ formatInboundMeasure(row.chargeableQuantity, row.chargeUnit === 'KG' ? 'kg' : 'm³', row.chargeUnit === 'KG' ? 3 : 6) }}</template></el-table-column>
|
||||
</el-table>
|
||||
<div class="fee-confirm-notice" :class="{ 'is-confirmed': props.data.latestFee.customerConfirmationStatus === 'CONFIRMED' }" role="status">
|
||||
<CircleAlert :size="21" aria-hidden="true" />
|
||||
<span v-if="props.data.latestFee.customerConfirmationStatus === 'CONFIRMED'">客户已于 {{ formatInboundDateTime(props.data.latestFee.customerConfirmedAt) }} 确认,费用已锁定,不允许修改。</span>
|
||||
<span v-else>请核对计算重、单价和总金额。确认后当前费用版本将锁定。</span>
|
||||
<span v-else>请核对计费毛重、计费体积、计费量、单价和总金额。确认后当前费用版本将锁定。</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
@@ -283,7 +277,7 @@ async function confirmFee() {
|
||||
.fee-dialog-table { margin-top:16px; overflow:hidden; border-radius:10px; box-shadow:0 5px 18px rgba(51,65,85,.04); }
|
||||
.fee-dialog-table :deep(.el-table__header th.el-table__cell) { height:42px; padding:9px 0; background:linear-gradient(180deg,#f8f9fc,#f3f5f9); color:#697386; font-size:13px; font-weight:650; }
|
||||
.fee-dialog-table :deep(.el-table__body td.el-table__cell) { height:82px; padding:9px 0; color:#515a6c; font-size:14px; font-variant-numeric:tabular-nums; }
|
||||
.fee-dialog-table :deep(.el-table__body td:nth-child(7)), .fee-dialog-table :deep(.el-table__body td:nth-child(8)) { color:#34466b; font-weight:650; }
|
||||
.fee-dialog-table :deep(.el-table__body td:nth-child(2)), .fee-dialog-table :deep(.el-table__body td:nth-child(4)) { color:#34466b; font-weight:650; }
|
||||
.fee-dialog-table :deep(.cell) { line-height:1.6; }
|
||||
.fee-confirm-notice { display:flex; align-items:center; gap:10px; margin-top:14px; padding:10px 14px; border:1px solid #f5dfba; border-radius:9px; background:linear-gradient(90deg,#fff9ef,#fffaf2); color:#c6790c; font-size:14px; font-weight:500; }
|
||||
.fee-confirm-notice > svg { flex:0 0 auto; fill:#eda332; color:#fff; filter:drop-shadow(0 2px 3px rgba(233,154,37,.25)); }
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
import { computed, effectScope, ref, watch } from 'vue'
|
||||
import { readOrderColumnPreferences, saveOrderColumnPreferences } from './orderColumnPreferences.ts'
|
||||
|
||||
const page = readFileSync(new URL('../views/FirstLegOrdersView.vue', import.meta.url), 'utf8')
|
||||
const columnSource = page.slice(page.indexOf('const fixedOrderColumns:'), page.indexOf('const tablePage ='))
|
||||
const columnScript = ts.transpileModule(columnSource, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
|
||||
const createColumns = new Function('auth', 'ref', 'computed', 'watch', 'readOrderColumnPreferences', 'saveOrderColumnPreferences', `${columnScript}
|
||||
return { visibleColumnKeys, orderedColumnKeys, visibleOrderColumns, draggedColumnKey, moveOrderColumn, dropOrderColumn, resetOrderColumns }
|
||||
`)
|
||||
|
||||
function mockStorage(t) {
|
||||
const original = Object.getOwnPropertyDescriptor(globalThis, 'localStorage')
|
||||
const entries = new Map()
|
||||
const storage = {
|
||||
getItem: key => entries.get(key) ?? null,
|
||||
setItem: (key, value) => entries.set(key, value),
|
||||
}
|
||||
Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: storage })
|
||||
t.after(() => {
|
||||
if (original) Object.defineProperty(globalThis, 'localStorage', original)
|
||||
else delete globalThis.localStorage
|
||||
})
|
||||
return storage
|
||||
}
|
||||
|
||||
function openColumns(t, ownerId = 'customer-one') {
|
||||
const scope = effectScope()
|
||||
const columns = scope.run(() => createColumns({ user: { id: ownerId } }, ref, computed, watch, readOrderColumnPreferences, saveOrderColumnPreferences))
|
||||
t.after(() => scope.stop())
|
||||
return { ...columns, close: () => scope.stop() }
|
||||
}
|
||||
|
||||
test('restores checked fields and both arrow and drag ordering after a fresh page setup', t => {
|
||||
mockStorage(t)
|
||||
const columns = openColumns(t)
|
||||
columns.visibleColumnKeys.value.push('destinationWarehouse', 'grossWeight', 'volume')
|
||||
columns.visibleColumnKeys.value.splice(columns.visibleColumnKeys.value.indexOf('createdAt'), 1)
|
||||
columns.moveOrderColumn('destinationWarehouse', -1)
|
||||
columns.draggedColumnKey.value = 'grossWeight'
|
||||
columns.dropOrderColumn('status')
|
||||
const visibleKeys = [...columns.visibleColumnKeys.value]
|
||||
const orderedKeys = [...columns.orderedColumnKeys.value]
|
||||
const renderedKeys = columns.visibleOrderColumns.value.map(column => column.key)
|
||||
columns.close()
|
||||
|
||||
const refreshed = openColumns(t)
|
||||
assert.deepEqual(refreshed.visibleColumnKeys.value, visibleKeys)
|
||||
assert.deepEqual(refreshed.orderedColumnKeys.value, orderedKeys)
|
||||
assert.deepEqual(refreshed.visibleOrderColumns.value.map(column => column.key), renderedKeys)
|
||||
assert.deepEqual(renderedKeys.slice(0, 3), ['selection', 'actions', 'inboundCode'])
|
||||
assert.equal(renderedKeys[3], 'grossWeight')
|
||||
})
|
||||
|
||||
test('persists restore-default and intentionally hiding every selectable field', t => {
|
||||
mockStorage(t)
|
||||
const columns = openColumns(t)
|
||||
const defaults = [...columns.visibleColumnKeys.value]
|
||||
const defaultOrder = [...columns.orderedColumnKeys.value]
|
||||
columns.visibleColumnKeys.value = []
|
||||
columns.close()
|
||||
const refreshed = openColumns(t)
|
||||
assert.deepEqual(refreshed.visibleColumnKeys.value, [])
|
||||
assert.deepEqual(refreshed.visibleOrderColumns.value.map(column => column.key), ['selection', 'actions', 'inboundCode'])
|
||||
|
||||
refreshed.moveOrderColumn('grossWeight', -1)
|
||||
refreshed.resetOrderColumns()
|
||||
refreshed.close()
|
||||
const reset = openColumns(t)
|
||||
assert.deepEqual(reset.visibleColumnKeys.value, defaults)
|
||||
assert.deepEqual(reset.orderedColumnKeys.value, defaultOrder)
|
||||
})
|
||||
|
||||
test('keeps the field selection separate for each signed-in account', t => {
|
||||
mockStorage(t)
|
||||
const first = openColumns(t)
|
||||
first.visibleColumnKeys.value = ['volume']
|
||||
first.close()
|
||||
const second = openColumns(t, 'customer-two')
|
||||
assert.notDeepEqual(second.visibleColumnKeys.value, ['volume'])
|
||||
second.visibleColumnKeys.value = ['grossWeight']
|
||||
second.close()
|
||||
assert.deepEqual(openColumns(t).visibleColumnKeys.value, ['volume'])
|
||||
assert.deepEqual(openColumns(t, 'customer-two').visibleColumnKeys.value, ['grossWeight'])
|
||||
})
|
||||
|
||||
test('discards unknown, duplicate and invalid keys and includes newly available fields in ordering', t => {
|
||||
mockStorage(t)
|
||||
saveOrderColumnPreferences('customer-one', {
|
||||
visibleColumnKeys: ['volume', 'removed', 'volume', null, 3],
|
||||
orderedColumnKeys: ['volume', 'removed', 'volume', false],
|
||||
})
|
||||
assert.deepEqual(readOrderColumnPreferences('customer-one', ['status', 'volume', 'newField'], ['status']), {
|
||||
visibleColumnKeys: ['volume'],
|
||||
orderedColumnKeys: ['volume', 'status', 'newField'],
|
||||
})
|
||||
})
|
||||
|
||||
test('uses the original defaults for missing or damaged saved preferences', t => {
|
||||
const storage = mockStorage(t)
|
||||
const defaults = { visibleColumnKeys: ['status'], orderedColumnKeys: ['status', 'volume'] }
|
||||
assert.deepEqual(readOrderColumnPreferences('customer-one', ['status', 'volume'], ['status']), defaults)
|
||||
saveOrderColumnPreferences('customer-one', defaults)
|
||||
const realGetItem = storage.getItem
|
||||
for (const saved of ['{invalid', 'null', '[]', '42', '{"visibleColumnKeys":"status","orderedColumnKeys":[]}']) {
|
||||
storage.getItem = () => saved
|
||||
assert.deepEqual(readOrderColumnPreferences('customer-one', ['status', 'volume'], ['status']), defaults)
|
||||
}
|
||||
storage.getItem = realGetItem
|
||||
})
|
||||
|
||||
test('keeps column controls usable when browser storage is blocked', t => {
|
||||
const storage = mockStorage(t)
|
||||
storage.getItem = () => { throw new Error('Storage denied') }
|
||||
storage.setItem = () => { throw new Error('Storage denied') }
|
||||
const columns = openColumns(t)
|
||||
assert.ok(columns.visibleColumnKeys.value.includes('status'))
|
||||
assert.doesNotThrow(() => { columns.visibleColumnKeys.value.push('volume') })
|
||||
assert.doesNotThrow(() => columns.resetOrderColumns())
|
||||
})
|
||||
|
||||
test('does not read or save preferences without an account', t => {
|
||||
const storage = mockStorage(t)
|
||||
let storageCalls = 0
|
||||
storage.getItem = () => { storageCalls++; return null }
|
||||
storage.setItem = () => { storageCalls++ }
|
||||
const columns = openColumns(t, '')
|
||||
columns.visibleColumnKeys.value = ['volume']
|
||||
assert.equal(openColumns(t, '').visibleColumnKeys.value.includes('status'), true)
|
||||
assert.equal(storageCalls, 0)
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
interface OrderColumnPreferences {
|
||||
visibleColumnKeys: string[]
|
||||
orderedColumnKeys: string[]
|
||||
}
|
||||
|
||||
function storageKey(ownerId: string) {
|
||||
return `cc:client:first-leg:order-columns:${ownerId}`
|
||||
}
|
||||
|
||||
export function readOrderColumnPreferences(ownerId: string, selectableKeys: string[], defaultVisibleKeys: string[]): OrderColumnPreferences {
|
||||
const defaults = { visibleColumnKeys: [...defaultVisibleKeys], orderedColumnKeys: [...selectableKeys] }
|
||||
if (!ownerId) return defaults
|
||||
try {
|
||||
const saved: unknown = JSON.parse(localStorage.getItem(storageKey(ownerId)) ?? 'null')
|
||||
if (!saved || typeof saved !== 'object') return defaults
|
||||
const preferences = saved as Partial<OrderColumnPreferences>
|
||||
if (!Array.isArray(preferences.visibleColumnKeys) || !Array.isArray(preferences.orderedColumnKeys)) return defaults
|
||||
const validKeys = (keys: unknown[]) => [...new Set(keys.filter((key): key is string => typeof key === 'string' && selectableKeys.includes(key)))]
|
||||
return {
|
||||
visibleColumnKeys: validKeys(preferences.visibleColumnKeys),
|
||||
orderedColumnKeys: [...new Set([...validKeys(preferences.orderedColumnKeys), ...selectableKeys])],
|
||||
}
|
||||
} catch {
|
||||
return defaults
|
||||
}
|
||||
}
|
||||
|
||||
export function saveOrderColumnPreferences(ownerId: string, preferences: OrderColumnPreferences) {
|
||||
if (!ownerId) return
|
||||
try {
|
||||
localStorage.setItem(storageKey(ownerId), JSON.stringify(preferences))
|
||||
} catch {
|
||||
// Column selection remains usable when browser storage is unavailable.
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ import { orderBillingMetric } from '../utils/orderDetailSummary'
|
||||
import type { OrderQueue } from '../api/unifiedOrders'
|
||||
import { validateProductImageFile } from '@/modules/first-leg/utils/productImage'
|
||||
import { createPriceEstimateBreakdown } from '@/modules/first-leg/utils/priceEstimate'
|
||||
import { readOrderColumnPreferences, saveOrderColumnPreferences } from '@/modules/first-leg/utils/orderColumnPreferences'
|
||||
|
||||
const store = useFirstLegOrdersStore()
|
||||
const tenantStore = useTenantAdministrationStore()
|
||||
@@ -182,8 +183,13 @@ const selectableOrderColumns: DataTableColumn[] = [
|
||||
]
|
||||
const allSelectableColumnKeys = selectableOrderColumns.map((column) => String(column.key))
|
||||
const defaultVisibleColumnKeys = ['status', 'progress', 'channel', 'transportMode', 'pickupMethod', 'receivingWarehouse', 'createdAt']
|
||||
const visibleColumnKeys = ref<string[]>([...defaultVisibleColumnKeys])
|
||||
const orderedColumnKeys = ref<string[]>([...allSelectableColumnKeys])
|
||||
const columnPreferencesOwnerId = auth.user?.id ?? ''
|
||||
const savedColumnPreferences = readOrderColumnPreferences(columnPreferencesOwnerId, allSelectableColumnKeys, defaultVisibleColumnKeys)
|
||||
const visibleColumnKeys = ref<string[]>(savedColumnPreferences.visibleColumnKeys)
|
||||
const orderedColumnKeys = ref<string[]>(savedColumnPreferences.orderedColumnKeys)
|
||||
watch([visibleColumnKeys, orderedColumnKeys], ([visibleKeys, orderedKeys]) => {
|
||||
saveOrderColumnPreferences(columnPreferencesOwnerId, { visibleColumnKeys: visibleKeys, orderedColumnKeys: orderedKeys })
|
||||
}, { deep: true, flush: 'sync' })
|
||||
const draggedColumnKey = ref('')
|
||||
const orderedSelectableColumns = computed(() => orderedColumnKeys.value
|
||||
.map((key) => selectableOrderColumns.find((column) => String(column.key) === key))
|
||||
|
||||
Reference in New Issue
Block a user