Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -3,37 +3,65 @@
|
||||
<div ref="stickyStackRef" class="sticky-stack pcd-sticky-stack">
|
||||
<div ref="saveToolbarRef" class="save-toolbar pcd-save-toolbar q-px-md">
|
||||
<div class="pcd-toolbar-row">
|
||||
<div class="pcd-toolbar-left">
|
||||
<div class="pcd-toolbar-title">Maliyet Detay Sayfasi</div>
|
||||
<div v-if="detailHeader && !detailLoading" class="pcd-toolbar-summary">
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||
<span class="pcd-toolbar-pill-label">USD</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.usdTotal) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||
<span class="pcd-toolbar-pill-label">EUR</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.eurTotal) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||
<span class="pcd-toolbar-pill-label">GBP</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.gbpTotal) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||||
<span class="pcd-toolbar-pill-label">USD Kur</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.usdRate) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||||
<span class="pcd-toolbar-pill-label">EUR Kur</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.eurRate) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||||
<span class="pcd-toolbar-pill-label">GBP Kur</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.gbpRate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pcd-toolbar-left">
|
||||
<div class="pcd-toolbar-title">Maliyet Detay Sayfasi</div>
|
||||
<div v-if="detailHeader && !detailLoading" class="pcd-toolbar-summary">
|
||||
<!-- tbStok kontrolu (exists-bulk) manuel giris kapandigi icin devre disi -->
|
||||
<div class="pcd-toolbar-summary-row">
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||
<span class="pcd-toolbar-pill-label">USD</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.usdTotal) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||
<span class="pcd-toolbar-pill-label">TRY</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.tryTotal) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||
<span class="pcd-toolbar-pill-label">EUR</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.eurTotal) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-emphasis">
|
||||
<span class="pcd-toolbar-pill-label">GBP</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(toolbarSummary.gbpTotal) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pcd-toolbar-summary-row">
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||||
<span class="pcd-toolbar-pill-label">USD Kur</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.usdRate) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||||
<span class="pcd-toolbar-pill-label">EUR Kur</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.eurRate) }}</span>
|
||||
</div>
|
||||
<div class="pcd-toolbar-pill pcd-toolbar-pill-neutral">
|
||||
<span class="pcd-toolbar-pill-label">GBP Kur</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ formatMoney(exchangeRates.gbpRate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pcd-toolbar-actions">
|
||||
<div class="pcd-toolbar-actions">
|
||||
<div
|
||||
v-if="last10WarningCount > 0"
|
||||
class="pcd-toolbar-pill pcd-toolbar-pill-warn"
|
||||
style="cursor:pointer;"
|
||||
title="Son 10 ort. fiyata gore %10+ sapma var"
|
||||
@click="last10WarningDialogOpen = true"
|
||||
>
|
||||
<span class="pcd-toolbar-pill-label">Fiyat Uyari</span>
|
||||
<span class="pcd-toolbar-pill-value">{{ last10WarningCount }}</span>
|
||||
</div>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
color="grey-7"
|
||||
class="pcd-toolbar-btn"
|
||||
:label="summaryOpen ? 'OZET GIZLE' : 'OZET GOSTER'"
|
||||
:icon="summaryOpen ? 'visibility_off' : 'visibility'"
|
||||
@click="summaryOpen = !summaryOpen"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
@@ -114,6 +142,178 @@
|
||||
Hata: {{ detailError }}
|
||||
</q-banner>
|
||||
|
||||
<q-expansion-item
|
||||
v-if="detailHeader && !detailLoading"
|
||||
v-model="summaryOpen"
|
||||
dense
|
||||
expand-separator
|
||||
class="pcd-summary-expansion q-mx-md q-mb-md"
|
||||
icon="summarize"
|
||||
label="Ozet"
|
||||
>
|
||||
<div class="row q-col-gutter-md q-pa-sm">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcd-summary-title">Maliyetlere Islenen Toplam Tutar</div>
|
||||
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pcd-summary-k">USD</td>
|
||||
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.usd) }}</td>
|
||||
<td class="pcd-summary-k">TRY</td>
|
||||
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.try) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pcd-summary-k">EUR</td>
|
||||
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.eur) }}</td>
|
||||
<td class="pcd-summary-k">GBP</td>
|
||||
<td class="pcd-summary-v">{{ formatMoney(mailSummary.headerTotals.gbp) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</q-markup-table>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcd-summary-title">Iscilik Fiyatlari (CM2)</div>
|
||||
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Parca</th>
|
||||
<th class="text-right">Giris</th>
|
||||
<th class="text-left">Pr.Br.</th>
|
||||
<th class="text-right">USD Tutar</th>
|
||||
<th class="text-right">TRY Tutar</th>
|
||||
<th class="text-center">CMT/Malzemeli</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in mailSummary.laborRows" :key="'labor-'+r.part">
|
||||
<td class="text-left">{{ r.part }}</td>
|
||||
<td class="text-right">{{ r.inputAmountLabel }}</td>
|
||||
<td class="text-left">{{ r.inputCurrencyLabel }}</td>
|
||||
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||||
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||||
<td class="text-center">{{ r.hasCmtOrMalzemeli ? '✓' : '' }}</td>
|
||||
</tr>
|
||||
<tr class="pcd-summary-total-row">
|
||||
<td class="text-left text-weight-bold">TOPLAM</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.laborRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.laborRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</q-markup-table>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcd-summary-title">Malzeme Fiyatlari (DT/TP, maliyete dahil)</div>
|
||||
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Parca</th>
|
||||
<th class="text-right">USD Tutar</th>
|
||||
<th class="text-right">TRY Tutar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in mailSummary.materialRows" :key="'mat-'+r.part">
|
||||
<td class="text-left">{{ r.part }}</td>
|
||||
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||||
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||||
</tr>
|
||||
<tr class="pcd-summary-total-row">
|
||||
<td class="text-left text-weight-bold">TOPLAM</td>
|
||||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.materialRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.materialRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</q-markup-table>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="pcd-summary-title">Kumas Fiyatlari (FABRIC, maliyete dahil)</div>
|
||||
<q-markup-table dense flat bordered class="pcd-summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Parca</th>
|
||||
<th class="text-right">Metraj</th>
|
||||
<th class="text-right">MT Giris Fiyat</th>
|
||||
<th class="text-left">Pr.Br.</th>
|
||||
<th class="text-right">USD Tutar</th>
|
||||
<th class="text-right">TRY Tutar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in mailSummary.fabricRows" :key="'fab-'+r.part">
|
||||
<td class="text-left">{{ r.part }}</td>
|
||||
<td class="text-right">{{ r.meterLabel }}</td>
|
||||
<td class="text-right">{{ r.inputUnitLabel }}</td>
|
||||
<td class="text-left">{{ r.inputCurrencyLabel }}</td>
|
||||
<td class="text-right">{{ formatMoney(r.usd) }}</td>
|
||||
<td class="text-right">{{ formatMoney(r.try) }}</td>
|
||||
</tr>
|
||||
<tr class="pcd-summary-total-row">
|
||||
<td class="text-left text-weight-bold">TOPLAM</td>
|
||||
<td class="text-right text-weight-bold">
|
||||
{{
|
||||
(() => {
|
||||
const total = mailSummary.fabricRows.reduce((a, x) => a + (Number(x.meterQty || 0) || 0), 0)
|
||||
return total > 0 ? `${formatBarQuantity(total)} MT` : '-'
|
||||
})()
|
||||
}}
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.fabricRows.reduce((a, x) => a + (x.usd || 0), 0)) }}</td>
|
||||
<td class="text-right text-weight-bold">{{ formatMoney(mailSummary.fabricRows.reduce((a, x) => a + (x.try || 0), 0)) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</q-markup-table>
|
||||
</div>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-dialog v-model="last10WarningDialogOpen" persistent>
|
||||
<q-card style="min-width: 860px; max-width: 92vw;">
|
||||
<q-card-section class="row items-center">
|
||||
<div class="text-h6">Son 10 Ort. Fiyat Sapmalari</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section style="max-height: 70vh; overflow: auto;">
|
||||
<q-markup-table dense flat bordered>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Kod</th>
|
||||
<th class="text-left">Doviz</th>
|
||||
<th class="text-right">Giris</th>
|
||||
<th class="text-right">Ort10</th>
|
||||
<th class="text-right">Sapma %</th>
|
||||
<th class="text-right">Sample</th>
|
||||
<th class="text-left">Tarih Araligi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="w in last10Warnings" :key="w.item_code + '|' + w.currency_code">
|
||||
<td class="text-left">{{ w.item_code }}</td>
|
||||
<td class="text-left">{{ w.currency_code }}</td>
|
||||
<td class="text-right">{{ formatMoney(w.input_price) }}</td>
|
||||
<td class="text-right">{{ formatMoney(w.avg_doc_price) }}</td>
|
||||
<td class="text-right">{{ formatPercent(w.diff_ratio) }}</td>
|
||||
<td class="text-right">{{ w.sample_count }}</td>
|
||||
<td class="text-left">{{ (w.min_invoice_date || '-') + ' / ' + (w.max_invoice_date || '-') }}</td>
|
||||
</tr>
|
||||
<tr v-if="last10Warnings.length === 0">
|
||||
<td colspan="7" class="text-center text-grey-7">Kayit yok</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</q-markup-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<div v-if="detailHeader && !detailLoading && !headerInfoCollapsed" class="filter-bar pcd-detail-header-bar q-mx-md q-mb-md">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-3">
|
||||
@@ -189,7 +389,7 @@
|
||||
<q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" />
|
||||
</div>
|
||||
|
||||
<div v-if="partSummary && partSummary.length > 0" class="col-12">
|
||||
<div v-if="false && partSummary && partSummary.length > 0" class="col-12">
|
||||
<div class="pcd-part-summary-card">
|
||||
<div class="pcd-part-summary-title">Parça Bazlı Maliyet Özellikleri</div>
|
||||
<q-markup-table dense flat bordered separator="cell" class="pcd-part-summary-table">
|
||||
@@ -386,7 +586,7 @@
|
||||
<template #body-cell-sKodu="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="resolveAutoOrICodeHighlightClass(props.row)"
|
||||
:class="[resolveAutoOrICodeHighlightClass(props.row), resolveMissingStockCodeClass(props.row)]"
|
||||
>
|
||||
<span>{{ props.value }}</span>
|
||||
</q-td>
|
||||
@@ -395,7 +595,7 @@
|
||||
<template #body-cell-sAciklama="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="resolveAutoOrICodeHighlightClass(props.row)"
|
||||
:class="[resolveAutoOrICodeHighlightClass(props.row), resolveMissingStockCodeClass(props.row)]"
|
||||
>
|
||||
<span>{{ props.value }}</span>
|
||||
</q-td>
|
||||
@@ -880,6 +1080,7 @@ const rowEditorHammaddeLoading = ref(false)
|
||||
const rowEditorItemOptions = ref([])
|
||||
const rowEditorItemAllOptions = ref([])
|
||||
const rowEditorItemLoading = ref(false)
|
||||
const rowEditorLastValidItemValue = ref('')
|
||||
const rowEditorColorOptions = ref([])
|
||||
const rowEditorColorAllOptions = ref([])
|
||||
const rowEditorColorLoading = ref(false)
|
||||
@@ -894,7 +1095,89 @@ const lineHistoryTargetSummary = ref('')
|
||||
const lineHistorySearchMode = ref('exact')
|
||||
const lineHistoryLastPurchaseMatchStage = ref('')
|
||||
const lineHistoryLastRecipeMatchStage = ref('')
|
||||
|
||||
// tbStok validation (missing stock cards)
|
||||
// Manuel giris kapandigi icin tbStok exists-bulk kontrolunu devre disi biraktik.
|
||||
// Eski UI/CSS hook'lari kalsin diye state'i tutuyoruz ama request atmayacagiz.
|
||||
const missingTbStokCodesMap = ref({})
|
||||
const tbStokValidationLastError = ref('')
|
||||
const tbStokMissingDialogShown = ref(false)
|
||||
let tbStokValidationTimer = null
|
||||
|
||||
function normalizeStockCodeKey (code) {
|
||||
return String(code || '').trim().toUpperCase()
|
||||
}
|
||||
|
||||
function openMissingTbStokDialog () {
|
||||
const missing = Object.keys(missingTbStokCodesMap.value || {})
|
||||
if (missing.length === 0) return
|
||||
$q.dialog({
|
||||
title: 'tbStok Eksik Kodlar',
|
||||
message: `Asagidaki kodlar tbStok'ta yok. Stok kartlarini duzeltmeden maliyet kaydetmeyin:\n\n${missing.slice(0, 80).join(', ')}${missing.length > 80 ? `\n(+${missing.length - 80} daha)` : ''}`,
|
||||
ok: { label: 'Tamam' }
|
||||
})
|
||||
}
|
||||
|
||||
function isTbStokMissingCode (code) {
|
||||
code = normalizeStockCodeKey(code)
|
||||
if (!code) return false
|
||||
return Boolean(missingTbStokCodesMap.value?.[code])
|
||||
}
|
||||
|
||||
function resolveMissingStockCodeClass (row) {
|
||||
const code = normalizeStockCodeKey(row?.sKodu)
|
||||
if (!code) return ''
|
||||
if (isTbStokMissingCode(code)) return 'pcd-missing-stock-code'
|
||||
return ''
|
||||
}
|
||||
|
||||
function collectUniqueCodesFromRows () {
|
||||
const out = []
|
||||
const seen = new Set()
|
||||
for (const r of flatDetailRows.value || []) {
|
||||
const code = normalizeStockCodeKey(r?.sKodu)
|
||||
if (!code) continue
|
||||
if (seen.has(code)) continue
|
||||
seen.add(code)
|
||||
out.push(code)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function refreshTbStokMissingCodes () {
|
||||
tbStokValidationLastError.value = ''
|
||||
missingTbStokCodesMap.value = {}
|
||||
}
|
||||
|
||||
function formatPercent (ratio) {
|
||||
const n = Number(ratio)
|
||||
if (!Number.isFinite(n)) return '-'
|
||||
return `${(n * 100).toFixed(0)}%`
|
||||
}
|
||||
|
||||
async function refreshLast10Warnings () {
|
||||
if (!onMLNo.value) {
|
||||
last10Warnings.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const resp = await get('/pricing/production-product-costing/last10-warnings', { n_onml_no: onMLNo.value })
|
||||
const items = Array.isArray(resp?.items) ? resp.items : []
|
||||
last10Warnings.value = items
|
||||
} catch (err) {
|
||||
// non-blocking
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleTbStokValidation () {
|
||||
// no-op (tbStok kontrolu devre disi)
|
||||
if (tbStokValidationTimer) clearTimeout(tbStokValidationTimer)
|
||||
tbStokValidationTimer = null
|
||||
}
|
||||
const purchaseAvgUSDCachedByCode = ref({})
|
||||
const last10Warnings = ref([])
|
||||
const last10WarningDialogOpen = ref(false)
|
||||
const last10WarningCount = computed(() => (Array.isArray(last10Warnings.value) ? last10Warnings.value.length : 0))
|
||||
const headerInfoCollapsed = ref(false)
|
||||
const subHeaderTop = ref(140)
|
||||
const stickyStackRef = ref(null)
|
||||
@@ -1114,6 +1397,167 @@ const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) =>
|
||||
gbpTotal: 0
|
||||
}))
|
||||
|
||||
const summaryOpen = ref(false)
|
||||
|
||||
function makeEmptySummaryRow (part) {
|
||||
return {
|
||||
part,
|
||||
inputAmountLabel: '-',
|
||||
inputCurrencyLabel: '-',
|
||||
inputUnitLabel: '-',
|
||||
meterLabel: '-',
|
||||
meterQty: 0,
|
||||
meterUom: '',
|
||||
usd: 0,
|
||||
try: 0,
|
||||
hasCmtOrMalzemeli: false
|
||||
}
|
||||
}
|
||||
|
||||
function accumulateInputAmount (bucket, row) {
|
||||
const qty = resolveNumericRowQuantity(row)
|
||||
const inputPrice = resolveNumericRowInputPrice(row)
|
||||
const cur = resolveInputCurrency(row) || 'USD'
|
||||
const amount = (Number.isFinite(inputPrice) ? inputPrice : 0) * (Number.isFinite(qty) ? qty : 0)
|
||||
if (!Number.isFinite(amount) || amount === 0) return
|
||||
if (!bucket.inputByCur) bucket.inputByCur = {}
|
||||
bucket.inputByCur[cur] = (bucket.inputByCur[cur] || 0) + amount
|
||||
}
|
||||
|
||||
function accumulateUnitPrice (bucket, row) {
|
||||
// For fabric summary: show a representative unit input price (first non-zero).
|
||||
const inputPrice = resolveNumericRowInputPrice(row)
|
||||
const cur = resolveInputCurrency(row) || 'USD'
|
||||
if (!Number.isFinite(inputPrice) || inputPrice <= 0) return
|
||||
if (bucket.unitPrice === undefined || bucket.unitPrice === null) {
|
||||
bucket.unitPrice = inputPrice
|
||||
bucket.unitCur = cur
|
||||
}
|
||||
}
|
||||
|
||||
function finalizeInputLabel (bucket) {
|
||||
if (Number(bucket.meterQty || 0) > 0) {
|
||||
const uom = String(bucket.meterUom || '').trim()
|
||||
bucket.meterLabel = `${formatBarQuantity(bucket.meterQty)}${uom ? ' ' + uom : ''}`
|
||||
} else {
|
||||
bucket.meterLabel = bucket.meterLabel || '-'
|
||||
}
|
||||
const byCur = bucket.inputByCur || {}
|
||||
const currencies = Object.keys(byCur).filter(Boolean)
|
||||
if (currencies.length === 0) {
|
||||
bucket.inputAmountLabel = '-'
|
||||
bucket.inputCurrencyLabel = '-'
|
||||
bucket.inputUnitLabel = bucket.inputUnitLabel || '-'
|
||||
return bucket
|
||||
}
|
||||
if (currencies.length === 1) {
|
||||
const cur = currencies[0]
|
||||
bucket.inputCurrencyLabel = cur
|
||||
bucket.inputAmountLabel = formatMoney(byCur[cur] || 0)
|
||||
if (bucket.unitPrice !== undefined && bucket.unitCur) {
|
||||
bucket.inputUnitLabel = formatMoney(bucket.unitPrice)
|
||||
bucket.inputCurrencyLabel = String(bucket.unitCur || cur).toUpperCase()
|
||||
}
|
||||
return bucket
|
||||
}
|
||||
// Multiple currencies mixed: keep labels short.
|
||||
bucket.inputCurrencyLabel = 'MIX'
|
||||
bucket.inputAmountLabel = formatMoney(currencies.reduce((acc, c) => acc + (byCur[c] || 0), 0))
|
||||
return bucket
|
||||
}
|
||||
|
||||
const mailSummary = computed(() => {
|
||||
// Parts must be dynamic. Use sParcaAdi coming from backend (spUrtMTBolum.sAdi),
|
||||
// and keep a stable order: common parts first, then the rest by first appearance.
|
||||
const preferredParts = ['CEKET', 'PANTOLON', 'YELEK', 'AKSESUAR', 'YAKA']
|
||||
const seenParts = new Set()
|
||||
const dynamicParts = []
|
||||
flatDetailRows.value.forEach((row) => {
|
||||
const partRaw = normalizeGroupName(row?.sParcaAdi)
|
||||
const part = String(partRaw || '').trim().toUpperCase()
|
||||
if (!part) return
|
||||
if (seenParts.has(part)) return
|
||||
seenParts.add(part)
|
||||
dynamicParts.push(part)
|
||||
})
|
||||
const parts = [
|
||||
...preferredParts.filter(p => seenParts.has(p)),
|
||||
...dynamicParts.filter(p => !preferredParts.includes(p))
|
||||
]
|
||||
const base = {
|
||||
headerTotals: {
|
||||
usd: toolbarSummary.value.usdTotal || 0,
|
||||
try: toolbarSummary.value.tryTotal || 0,
|
||||
eur: toolbarSummary.value.eurTotal || 0,
|
||||
gbp: toolbarSummary.value.gbpTotal || 0
|
||||
},
|
||||
laborByPart: {},
|
||||
materialByPart: {},
|
||||
fabricByPart: {}
|
||||
}
|
||||
|
||||
parts.forEach((p) => {
|
||||
base.laborByPart[p] = makeEmptySummaryRow(p)
|
||||
base.materialByPart[p] = makeEmptySummaryRow(p)
|
||||
base.fabricByPart[p] = makeEmptySummaryRow(p)
|
||||
})
|
||||
|
||||
flatDetailRows.value.forEach((row) => {
|
||||
const partRaw = normalizeGroupName(row?.sParcaAdi)
|
||||
const part = String(partRaw || '').trim().toUpperCase()
|
||||
if (!part || !parts.includes(part)) return
|
||||
|
||||
const group = String(normalizeGroupName(row?.sAciklama3)).trim().toUpperCase()
|
||||
const included = Boolean(row?.maliyeteDahil) || isCMGroupName(group)
|
||||
if (!included) return
|
||||
|
||||
const usdAmount = resolveRowUSDTutar(row)
|
||||
const tryAmount = resolveRowTRYTutar(row)
|
||||
|
||||
if (isCMGroupName(group)) {
|
||||
const b = base.laborByPart[part]
|
||||
b.usd += usdAmount
|
||||
b.try += tryAmount
|
||||
accumulateInputAmount(b, row)
|
||||
// Tick should reflect actual checkbox state (only when user selected "malzemeli"/type=2).
|
||||
if (resolveCMPriceTypeChecked(row)) b.hasCmtOrMalzemeli = true
|
||||
return
|
||||
}
|
||||
|
||||
if (group === 'DT' || group.includes(' DT') || group === 'TP' || group.includes(' TP')) {
|
||||
const b = base.materialByPart[part]
|
||||
b.usd += usdAmount
|
||||
b.try += tryAmount
|
||||
accumulateInputAmount(b, row)
|
||||
return
|
||||
}
|
||||
|
||||
if (group === 'FABRIC' || group.includes('FABRIC')) {
|
||||
const b = base.fabricByPart[part]
|
||||
b.usd += usdAmount
|
||||
b.try += tryAmount
|
||||
accumulateInputAmount(b, row)
|
||||
accumulateUnitPrice(b, row)
|
||||
const qty = resolveNumericRowQuantity(row)
|
||||
if (Number.isFinite(qty) && qty > 0) {
|
||||
b.meterQty += qty
|
||||
if (!b.meterUom) b.meterUom = String(row?.sBirim || '').trim()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const laborRows = parts.map((p) => finalizeInputLabel(base.laborByPart[p]))
|
||||
const materialRows = parts.map((p) => finalizeInputLabel(base.materialByPart[p]))
|
||||
const fabricRows = parts.map((p) => finalizeInputLabel(base.fabricByPart[p]))
|
||||
|
||||
return {
|
||||
headerTotals: base.headerTotals,
|
||||
laborRows,
|
||||
materialRows,
|
||||
fabricRows
|
||||
}
|
||||
})
|
||||
|
||||
const partSummary = computed(() => {
|
||||
const summary = {}
|
||||
flatDetailRows.value.forEach(row => {
|
||||
@@ -1129,25 +1573,27 @@ const partSummary = computed(() => {
|
||||
})
|
||||
return Object.entries(summary).map(([name, totals]) => ({ name, ...totals }))
|
||||
})
|
||||
const lineHistoryColumns = [
|
||||
{ name: 'sourceLabel', label: 'Kaynak', field: 'sourceLabel', align: 'left', sortable: false, style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'dateLabel', label: 'Tarih', field: 'dateLabel', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'invoiceCode', label: 'Fatura/OnML', field: 'invoiceCode', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'companyCode', label: 'Firma Kodu', field: 'companyCode', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'companyDescription', label: 'Firma Aciklama', field: 'companyDescription', align: 'left', sortable: true, style: 'width:12%', headerStyle: 'width:12%' },
|
||||
{ name: 'itemCode', label: 'Masraf/sKodu', field: 'itemCode', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'itemDescription', label: 'Masraf Detay', field: 'itemDescription', align: 'left', sortable: true, style: 'width:11%', headerStyle: 'width:11%' },
|
||||
{ name: 'colorCode', label: 'Renk', field: 'colorCode', align: 'left', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
|
||||
{ name: 'colorDescription', label: 'Renk Aciklama', field: 'colorDescription', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'itemDim1Code', label: 'Dim1', field: 'itemDim1Code', align: 'left', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
|
||||
{ name: 'itemDim1Description', label: 'Dim1 Aciklama', field: 'itemDim1Description', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'quantity', label: 'Miktar', field: 'quantity', align: 'right', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'unit', label: 'Birim', field: 'unit', align: 'left', sortable: true, style: 'width:4%', headerStyle: 'width:4%' },
|
||||
{ name: 'price', label: 'Fiyat', field: 'price', align: 'right', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'amount', label: 'Tutar', field: 'amount', align: 'right', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'currency', label: 'Pr. Br.', field: 'currency', align: 'left', sortable: true, style: 'width:5%', headerStyle: 'width:5%' },
|
||||
{ name: 'select', label: '', field: 'select', align: 'right', sortable: false, style: 'width:6%', headerStyle: 'width:6%' }
|
||||
]
|
||||
const lineHistoryColumns = [
|
||||
// Not: fixed percentage widths were overflowing at 100% zoom.
|
||||
// Let the table layout decide widths so the "Sec" column stays visible.
|
||||
{ name: 'sourceLabel', label: 'Kaynak', field: 'sourceLabel', align: 'left', sortable: false },
|
||||
{ name: 'dateLabel', label: 'Tarih', field: 'dateLabel', align: 'left', sortable: true },
|
||||
{ name: 'invoiceCode', label: 'Fatura/OnML', field: 'invoiceCode', align: 'left', sortable: true },
|
||||
{ name: 'companyCode', label: 'Firma Kodu', field: 'companyCode', align: 'left', sortable: true },
|
||||
{ name: 'companyDescription', label: 'Firma Aciklama', field: 'companyDescription', align: 'left', sortable: true },
|
||||
{ name: 'itemCode', label: 'Masraf/sKodu', field: 'itemCode', align: 'left', sortable: true },
|
||||
{ name: 'itemDescription', label: 'Masraf Detay', field: 'itemDescription', align: 'left', sortable: true },
|
||||
{ name: 'colorCode', label: 'Renk', field: 'colorCode', align: 'left', sortable: true },
|
||||
{ name: 'colorDescription', label: 'Renk Aciklama', field: 'colorDescription', align: 'left', sortable: true },
|
||||
{ name: 'itemDim1Code', label: 'Dim1', field: 'itemDim1Code', align: 'left', sortable: true },
|
||||
{ name: 'itemDim1Description', label: 'Dim1 Aciklama', field: 'itemDim1Description', align: 'left', sortable: true },
|
||||
{ name: 'quantity', label: 'Miktar', field: 'quantity', align: 'right', sortable: true },
|
||||
{ name: 'unit', label: 'Birim', field: 'unit', align: 'left', sortable: true },
|
||||
{ name: 'price', label: 'Fiyat', field: 'price', align: 'right', sortable: true },
|
||||
{ name: 'amount', label: 'Tutar', field: 'amount', align: 'right', sortable: true },
|
||||
{ name: 'currency', label: 'Pr. Br.', field: 'currency', align: 'left', sortable: true },
|
||||
{ name: 'select', label: '', field: 'select', align: 'right', sortable: false }
|
||||
]
|
||||
|
||||
function resolveLineHistoryRowClass (row) {
|
||||
if (row?.priceType === 'BNZ') return 'pcd-history-row-similar'
|
||||
@@ -1166,7 +1612,7 @@ const detailColumns = [
|
||||
{ name: 'sRenk', label: 'Renk', field: 'sRenk', align: 'left', sortable: true },
|
||||
{ name: 'lMiktar', label: 'Miktar', field: 'lMiktar', align: 'right', sortable: true, format: val => formatQuantity(val), style: 'width: 80px', headerStyle: 'width: 80px' },
|
||||
{ name: 'inputPrice', label: 'Fiyat Giriş', field: 'inputPrice', align: 'right', sortable: false, style: 'width: 80px', headerStyle: 'width: 80px' },
|
||||
{ name: 'inputPricePrBr', label: 'Fiyat Giriş Pr.Br.', field: 'inputPricePrBr', align: 'left', sortable: false, style: 'width: 80px', headerStyle: 'width: 80px' },
|
||||
{ name: 'inputPricePrBr', label: 'Fiyat Giriş Pr.Br.', field: 'inputPricePrBr', align: 'left', sortable: false, style: 'width: 92px', headerStyle: 'width: 92px' },
|
||||
{ name: 'maliyeteDahil', label: 'Maliyete Dahil', field: 'maliyeteDahil', align: 'center', sortable: false },
|
||||
{ name: 'cmPriceType', label: 'CMT', field: 'cm_price_type_id', align: 'center', sortable: false, style: 'width: 72px', headerStyle: 'width: 72px' },
|
||||
{ name: 'lFiyat', label: 'lFiyat', field: 'lFiyat', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||||
@@ -1290,14 +1736,22 @@ function formatQuantity (value) {
|
||||
})
|
||||
}
|
||||
|
||||
function formatQuantity2 (value) {
|
||||
return parseMoneyInput(value).toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
function formatBarMoney (value) {
|
||||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100
|
||||
return formatMoney(roundedValue)
|
||||
}
|
||||
|
||||
function formatBarQuantity (value) {
|
||||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 10000) / 10000
|
||||
return formatQuantity(roundedValue)
|
||||
// Bar/summary quantities: show 2 decimals (requested for MT totals).
|
||||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100
|
||||
return formatQuantity2(roundedValue)
|
||||
}
|
||||
|
||||
function convertPriceToUSD (price, currency) {
|
||||
@@ -1630,8 +2084,15 @@ async function loadRowEditorColorOptions () {
|
||||
function primeRowEditorOptionsFromForm () {
|
||||
upsertEditorOption(rowEditorHammaddeAllOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorHammaddeOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorItemAllOptions, buildRowEditorItemOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorItemOptions, buildRowEditorItemOption(rowEditorForm.value))
|
||||
// Do not inject invalid free-typed values (e.g. "GAMBOÇ") into the item select options.
|
||||
// Item options must represent real tbStok model codes (usually like "M.X", "K.X", "I.X").
|
||||
const itemCandidate = buildRowEditorItemOption(rowEditorForm.value)
|
||||
const itemKey = String(itemCandidate?.value || '').trim()
|
||||
const looksLikeModel = /^[A-Za-z0-9]\./.test(itemKey)
|
||||
if (looksLikeModel) {
|
||||
upsertEditorOption(rowEditorItemAllOptions, itemCandidate)
|
||||
upsertEditorOption(rowEditorItemOptions, itemCandidate)
|
||||
}
|
||||
upsertEditorOption(rowEditorColorAllOptions, buildRowEditorColorOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorColorOptions, buildRowEditorColorOption(rowEditorForm.value))
|
||||
}
|
||||
@@ -2402,7 +2863,8 @@ async function fetchDetail (options = {}) {
|
||||
])
|
||||
detailHeader.value = headerData && typeof headerData === 'object' ? headerData : null
|
||||
productionTypes.value = Array.isArray(typesData) ? typesData : []
|
||||
costDate.value = normalizeDateInput(detailHeader.value?.dteKayitTarihi)
|
||||
// Prefer true costing date (spUrtOnMLMas.Tarihi) over record create/update timestamps.
|
||||
costDate.value = normalizeDateInput(detailHeader.value?.maliyetTarihi || detailHeader.value?.dteKayitTarihi)
|
||||
detailGroups.value = normalizeDetailGroups(groupsData)
|
||||
initialHeaderSnapshot.value = currentHeaderSnapshot.value
|
||||
// Optional: hydrate local draft after base data load.
|
||||
@@ -2437,7 +2899,11 @@ async function fetchDetail (options = {}) {
|
||||
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
||||
n_urt_recete_id: detailHeader.value?.nUrtReceteID || ''
|
||||
})
|
||||
} catch (err) {
|
||||
// tbStok exists-bulk kontrolu devre disi (manual giris kapali).
|
||||
// Load last10 avg deviation warnings panel (non-blocking).
|
||||
await refreshLast10Warnings()
|
||||
// (eski tbStok missing dialog/notify kaldirildi)
|
||||
} catch (err) {
|
||||
detailError.value = await extractApiErrorDetail(err)
|
||||
slog.error('production-product-costing.detail', 'fetch-detail:error', {
|
||||
trace_id: traceId.value,
|
||||
@@ -2908,6 +3374,8 @@ function resolveDetailRowClass (row) {
|
||||
const key = String(row?.__rowKey || '').trim()
|
||||
if (key && requiredAttentionRowKeys.value?.[key]) return 'pcd-detail-row-required'
|
||||
if (row?.requiredPlaceholder) return 'pcd-detail-row-required'
|
||||
const code = String(row?.sKodu || '').trim()
|
||||
if (code && isTbStokMissingCode(code)) return 'pcd-detail-row-missing-stock'
|
||||
return row?.draftChanged ? 'pcd-detail-row-secondary' : ''
|
||||
}
|
||||
|
||||
@@ -3011,8 +3479,27 @@ async function applyFabricCopySelection () {
|
||||
|
||||
async function onRowEditorItemChange (value) {
|
||||
const selected = rowEditorItemOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||||
// Prevent free-typed values from being accepted as "code". User must pick from list.
|
||||
if (!selected) {
|
||||
const typed = String(value || '').trim()
|
||||
// If user cleared the field, allow clearing (other validations will catch blank-on-save if needed).
|
||||
if (!typed) {
|
||||
rowEditorForm.value.sKodu = ''
|
||||
rowEditorLastValidItemValue.value = ''
|
||||
return
|
||||
}
|
||||
// Revert to last valid selection.
|
||||
rowEditorForm.value.sKodu = String(rowEditorLastValidItemValue.value || '').trim()
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Kod secilmeden serbest metin girilemez. Listeden secim yapin.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rowEditorForm.value.sKodu = String(value || '').trim()
|
||||
if (!selected) return
|
||||
rowEditorLastValidItemValue.value = String(value || '').trim()
|
||||
const previousColorCode = String(rowEditorForm.value.ColorCode || '').trim()
|
||||
rowEditorForm.value.nStokID = String(selected.nStokID || '').trim()
|
||||
rowEditorForm.value.sModel = String(selected.sModel || '').trim()
|
||||
@@ -3129,7 +3616,8 @@ function applyEditorRowToGroups (nextRow) {
|
||||
detailGroups.value = sortDetailGroups(nextGroups)
|
||||
syncAllGroupsOpen()
|
||||
schedulePersistLocalDraft()
|
||||
}
|
||||
// tbStok validation devre disi
|
||||
}
|
||||
|
||||
function syncAllGroupsOpen () {
|
||||
const openState = {}
|
||||
@@ -3704,6 +4192,7 @@ function openNewRowDialog () {
|
||||
maliyeteDahil: true,
|
||||
sBirim: 'AD'
|
||||
})
|
||||
rowEditorLastValidItemValue.value = ''
|
||||
primeRowEditorOptionsFromForm()
|
||||
rowEditorDialogOpen.value = true
|
||||
void bootstrapRowEditorOptions()
|
||||
@@ -3716,6 +4205,7 @@ function openRowEditorForEdit (row) {
|
||||
...row,
|
||||
sAciklama3: row?.sAciklama3 || ''
|
||||
})
|
||||
rowEditorLastValidItemValue.value = String(rowEditorForm.value?.sKodu || '').trim()
|
||||
primeRowEditorOptionsFromForm()
|
||||
rowEditorDialogOpen.value = true
|
||||
void bootstrapRowEditorOptions()
|
||||
@@ -4113,6 +4603,7 @@ async function saveChanges () {
|
||||
saveLoading.value = true
|
||||
try {
|
||||
requiredAttentionRowKeys.value = {}
|
||||
// tbStok exists-bulk kontrolu devre disi (manual giris kapali): save bloke edilmez.
|
||||
if (isNoCostDetail.value) {
|
||||
const missing = computeMissingRequiredSlots()
|
||||
slog.info('production-product-costing.detail', 'required:missing:computed', {
|
||||
@@ -4319,6 +4810,10 @@ async function saveChanges () {
|
||||
|
||||
// For existing costing, just refresh the detail.
|
||||
await fetchDetail({ clearDraft: true, hydrateDraft: false })
|
||||
// last10 warnings are computed async on backend; re-check shortly after save.
|
||||
window.setTimeout(() => {
|
||||
try { refreshLast10Warnings() } catch {}
|
||||
}, 1200)
|
||||
} catch (e) {
|
||||
// Surface backend message (http.Error text) when available.
|
||||
const msg = String(
|
||||
@@ -4485,14 +4980,22 @@ watch(
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pcd-toolbar-summary {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.pcd-toolbar-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.pcd-toolbar-summary-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pcd-toolbar-pill {
|
||||
display: flex;
|
||||
@@ -4511,9 +5014,67 @@ watch(
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pcd-toolbar-pill-neutral {
|
||||
background: #f5f7fa;
|
||||
.pcd-toolbar-pill-neutral {
|
||||
background: #f5f7fa;
|
||||
color: #2b3c54;
|
||||
}
|
||||
|
||||
.pcd-toolbar-pill-warn {
|
||||
background: #ef6c00;
|
||||
border-color: #ef6c00;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pcd-summary-expansion :deep(.q-expansion-item__container) {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pcd-missing-stock-code {
|
||||
background: #c62828 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.pcd-missing-stock-code :deep(*) {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.pcd-detail-row-missing-stock td {
|
||||
background: #c62828 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.pcd-detail-row-missing-stock td :deep(*) {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.pcd-summary-title {
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
color: #2b3c54;
|
||||
margin: 2px 0 6px;
|
||||
}
|
||||
|
||||
.pcd-summary-table td,
|
||||
.pcd-summary-table th {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.pcd-summary-total-row td {
|
||||
border-top: 2px solid rgba(0, 0, 0, 0.15);
|
||||
background: #fbfbfd;
|
||||
}
|
||||
|
||||
.pcd-summary-k {
|
||||
width: 60px;
|
||||
color: #6b7680;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pcd-summary-v {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pcd-toolbar-pill-label {
|
||||
|
||||
Reference in New Issue
Block a user