Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -325,7 +325,7 @@ const menuItems = [
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Fiyatlandırma',
|
||||
label: 'Fiyatlandırma/Maliyetlendirme',
|
||||
icon: 'request_quote',
|
||||
|
||||
children: [
|
||||
@@ -333,6 +333,16 @@ const menuItems = [
|
||||
label: 'Ürün Fiyatlandırma',
|
||||
to: '/app/pricing/product-pricing',
|
||||
permission: 'order:view'
|
||||
},
|
||||
{
|
||||
label: "Üretim'den Ürün Maliyetlendirme",
|
||||
to: '/app/pricing/production-product-costing',
|
||||
permission: 'order:view'
|
||||
},
|
||||
{
|
||||
label: 'Maliyet Parça Eşleştirme',
|
||||
to: '/app/pricing/production-product-costing/maliyet-parca-eslestirme',
|
||||
permission: 'order:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
110
ui/src/pages/ProductionProductCosting.vue
Normal file
110
ui/src/pages/ProductionProductCosting.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<q-page v-if="canReadOrder" class="costing-gateway-page flex flex-center">
|
||||
<div class="gateway-container">
|
||||
<div class="gateway-header">
|
||||
<div class="text-h5">Üretim'den Ürün Maliyetlendirme</div>
|
||||
<div class="text-subtitle2 text-grey-7">
|
||||
İşlem seçerek maliyetlendirme ekranına geçin
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gateway-actions row q-col-gutter-lg q-mt-lg">
|
||||
<q-card
|
||||
class="gateway-card cursor-pointer"
|
||||
flat
|
||||
bordered
|
||||
@click="goHasCostProducts"
|
||||
>
|
||||
<q-card-section class="text-center">
|
||||
<q-icon name="fact_check" size="48px" color="primary" />
|
||||
<div class="text-h6 q-mt-sm">Mevcut Maliyeti Olan Ürünleri Göster</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
class="gateway-card cursor-pointer"
|
||||
flat
|
||||
bordered
|
||||
@click="goNoCostProducts"
|
||||
>
|
||||
<q-card-section class="text-center">
|
||||
<q-icon name="price_change" size="48px" color="primary" />
|
||||
<div class="text-h6 q-mt-sm">Maliyeti Olmayan Ürünler İçin Maliyet Oluştur</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
class="gateway-card cursor-pointer"
|
||||
flat
|
||||
bordered
|
||||
@click="goMTBolumMapping"
|
||||
>
|
||||
<q-card-section class="text-center">
|
||||
<q-icon name="hub" size="48px" color="primary" />
|
||||
<div class="text-h6 q-mt-sm">Maliyet Parça Eşleştirme</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
<div class="text-negative text-subtitle1">
|
||||
Bu module erisim yetkiniz yok.
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const { canRead } = usePermission()
|
||||
const canReadOrder = canRead('order')
|
||||
const router = useRouter()
|
||||
|
||||
function goHasCostProducts () {
|
||||
if (!canReadOrder.value) return
|
||||
router.push({ name: 'production-product-costing-has-cost' })
|
||||
}
|
||||
|
||||
function goNoCostProducts () {
|
||||
if (!canReadOrder.value) return
|
||||
router.push({ name: 'production-product-costing-no-cost' })
|
||||
}
|
||||
|
||||
function goMTBolumMapping () {
|
||||
if (!canReadOrder.value) return
|
||||
router.push({ name: 'production-product-costing-maliyet-parca-eslestirme' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.costing-gateway-page {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.gateway-container {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.gateway-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gateway-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gateway-card {
|
||||
width: 360px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.gateway-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
478
ui/src/pages/ProductionProductCostingHasCost.vue
Normal file
478
ui/src/pages/ProductionProductCostingHasCost.vue
Normal file
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<q-page v-if="canReadOrder" class="npc-page">
|
||||
<div class="ol-filter-bar npc-filter-bar">
|
||||
<div class="npc-filter-row">
|
||||
<q-input
|
||||
v-model="filters.search"
|
||||
class="npc-filter-input npc-search"
|
||||
dense
|
||||
filled
|
||||
clearable
|
||||
debounce="300"
|
||||
label="Arama (Maliyet No / Urun Kodu / Urun Adi / Aciklama)"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="ol-filter-actions npc-filter-actions">
|
||||
<q-btn
|
||||
label="Temizle"
|
||||
icon="clear"
|
||||
color="grey-7"
|
||||
flat
|
||||
:disable="loading"
|
||||
@click="clearFilters"
|
||||
/>
|
||||
<q-btn
|
||||
label="Kolon Filtreleri"
|
||||
icon="filter_alt_off"
|
||||
color="grey-7"
|
||||
flat
|
||||
:disable="loading"
|
||||
@click="clearAllColumnFilters"
|
||||
/>
|
||||
<q-btn
|
||||
label="Yenile"
|
||||
icon="refresh"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="fetchRows"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
title="Mevcut Maliyeti Olan Urunler"
|
||||
class="ol-table npc-table"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
virtual-scroll
|
||||
:virtual-scroll-item-size="34"
|
||||
table-style="max-height: calc(100vh - 190px)"
|
||||
separator="cell"
|
||||
row-key="__rowKey"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
no-data-label="Kayit bulunamadi"
|
||||
:rows-per-page-options="[0]"
|
||||
v-model:pagination="tablePagination"
|
||||
hide-bottom
|
||||
>
|
||||
<template #header-cell="props">
|
||||
<q-th :props="props">
|
||||
<div class="npc-header-cell">
|
||||
<div class="npc-head-wrap-3">{{ props.col.label }}</div>
|
||||
<q-btn
|
||||
v-if="props.col.name !== 'open'"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
icon="filter_alt"
|
||||
:color="isColumnFilterActive(props.col.name) ? 'primary' : 'grey-6'"
|
||||
>
|
||||
<q-menu class="npc-filter-menu" fit>
|
||||
<div class="npc-filter-menu-content">
|
||||
<div class="text-caption text-weight-bold q-mb-sm">{{ props.col.label }}</div>
|
||||
<q-input
|
||||
v-model="getColumnFilter(props.col.name).text"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Icerir"
|
||||
/>
|
||||
<q-select
|
||||
v-model="getColumnFilter(props.col.name).selected"
|
||||
class="q-mt-sm"
|
||||
dense
|
||||
outlined
|
||||
multiple
|
||||
use-chips
|
||||
use-input
|
||||
emit-value
|
||||
map-options
|
||||
:options="columnDistinctOptions[props.col.name] || []"
|
||||
label="Deger Sec"
|
||||
/>
|
||||
<div class="row justify-end q-gutter-sm q-mt-sm">
|
||||
<q-btn dense flat color="grey-7" label="Temizle" @click="clearColumnFilter(props.col.name)" />
|
||||
</div>
|
||||
</div>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #body-cell="props">
|
||||
<q-td v-if="props.col.name === 'open'" :props="props" class="text-center">
|
||||
<q-btn
|
||||
icon="open_in_new"
|
||||
color="primary"
|
||||
flat
|
||||
round
|
||||
dense
|
||||
@click="openRow(props.row)"
|
||||
>
|
||||
<q-tooltip>Ac</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-else :props="props" class="npc-wrap-col">
|
||||
<div class="npc-wrap-3" :title="String(props.value || '')">{{ props.value }}</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-banner v-if="error" class="bg-red text-white q-mt-sm">
|
||||
Hata: {{ error }}
|
||||
</q-banner>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
<div class="text-negative text-subtitle1">
|
||||
Bu module erisim yetkiniz yok.
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { get, extractApiErrorDetail } from 'src/services/api'
|
||||
|
||||
const { canRead } = usePermission()
|
||||
const canReadOrder = canRead('order')
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const allRows = ref([])
|
||||
const tablePagination = ref({
|
||||
sortBy: 'Tarihi',
|
||||
descending: true,
|
||||
page: 1,
|
||||
rowsPerPage: 0
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
search: ''
|
||||
})
|
||||
const dateColumns = new Set(['Tarihi', 'dteKayitTarihi', 'dteGuncellemeTarihi', 'SonSiparisTarihi'])
|
||||
|
||||
const columns = [
|
||||
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' },
|
||||
{ name: 'UretimSekli', label: 'Uretim Sekli', field: 'UretimSekli', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' },
|
||||
{ name: 'nOnMLNo', label: 'nOnMLNo', field: 'nOnMLNo', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'UrunKodu', label: 'UrunKodu', field: 'UrunKodu', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'UrunAdi', label: 'UrunAdi', field: 'UrunAdi', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'Tarihi', label: 'Tarihi', field: 'Tarihi', align: 'center', sortable: true, format: val => formatDateTR(val), style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'dteKayitTarihi', label: 'dteKayitTarihi', field: 'dteKayitTarihi', align: 'center', sortable: true, format: val => formatDateTR(val), style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'sKullaniciAdi', label: 'sKullaniciAdi', field: 'sKullaniciAdi', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'lTutarTL', label: 'lTutarTL', field: 'lTutarTL', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'lTutarUSD', label: 'lTutarUSD', field: 'lTutarUSD', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'lTutarEURO', label: 'lTutarEURO', field: 'lTutarEURO', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'dteGuncellemeTarihi', label: 'dteGuncellemeTarihi', field: 'dteGuncellemeTarihi', align: 'center', sortable: true, format: val => formatDateTR(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'sGuncellemeKullaniciAdi', label: 'sGuncellemeKullaniciAdi', field: 'sGuncellemeKullaniciAdi', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'nUrtReceteID', label: 'nUrtReceteID', field: 'nUrtReceteID', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' },
|
||||
{ name: 'sAciklama', label: 'sAciklama', field: 'sAciklama', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'SonSiparisTarihi', label: 'SonSiparisTarihi', field: 'SonSiparisTarihi', align: 'center', sortable: true, format: val => formatDateTR(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'MaliyetDurumu', label: 'MaliyetDurumu', field: 'MaliyetDurumu', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' }
|
||||
]
|
||||
|
||||
const columnFilters = reactive({})
|
||||
|
||||
function getColumnFilter (name) {
|
||||
if (!columnFilters[name]) {
|
||||
columnFilters[name] = {
|
||||
text: '',
|
||||
selected: []
|
||||
}
|
||||
}
|
||||
return columnFilters[name]
|
||||
}
|
||||
|
||||
function formatDateTR (value) {
|
||||
const s = String(value || '').trim()
|
||||
if (!s) return ''
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s)
|
||||
if (!m) return s
|
||||
return `${m[3]}.${m[2]}.${m[1]}`
|
||||
}
|
||||
|
||||
function formatMoney (value) {
|
||||
return Number(value || 0).toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
const columnDistinctOptions = computed(() => {
|
||||
const optionsByColumn = {}
|
||||
|
||||
for (const col of columns) {
|
||||
if (col.name === 'open') continue
|
||||
const set = new Set()
|
||||
|
||||
for (const row of allRows.value) {
|
||||
const val = getColumnComparableValue(row, col.name)
|
||||
if (val) set.add(val)
|
||||
}
|
||||
|
||||
optionsByColumn[col.name] = Array.from(set)
|
||||
.sort((a, b) => a.localeCompare(b, 'tr'))
|
||||
.map(v => ({ label: v, value: v }))
|
||||
}
|
||||
|
||||
return optionsByColumn
|
||||
})
|
||||
|
||||
const rows = computed(() => {
|
||||
let result = allRows.value
|
||||
|
||||
for (const col of columns) {
|
||||
if (col.name === 'open') continue
|
||||
|
||||
const cf = getColumnFilter(col.name)
|
||||
const text = String(cf.text || '').trim().toLowerCase()
|
||||
const selected = Array.isArray(cf.selected) ? cf.selected : []
|
||||
|
||||
if (!text && selected.length === 0) continue
|
||||
|
||||
result = result.filter((row) => {
|
||||
const value = getColumnComparableValue(row, col.name)
|
||||
const valueLC = value.toLowerCase()
|
||||
|
||||
if (text && !valueLC.includes(text)) return false
|
||||
if (selected.length > 0 && !selected.includes(value)) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
function getColumnComparableValue (row, colName) {
|
||||
if (row?.__cmp?.[colName] !== undefined) return row.__cmp[colName]
|
||||
if (dateColumns.has(colName)) return formatDateTR(row?.[colName])
|
||||
return String(row?.[colName] ?? '').trim()
|
||||
}
|
||||
|
||||
function buildComparableMap (row) {
|
||||
const cmp = {}
|
||||
for (const col of columns) {
|
||||
if (col.name === 'open') continue
|
||||
cmp[col.name] = dateColumns.has(col.name)
|
||||
? formatDateTR(row?.[col.name])
|
||||
: String(row?.[col.name] ?? '').trim()
|
||||
}
|
||||
return cmp
|
||||
}
|
||||
|
||||
function isColumnFilterActive (name) {
|
||||
const cf = getColumnFilter(name)
|
||||
return !!String(cf.text || '').trim() || (Array.isArray(cf.selected) && cf.selected.length > 0)
|
||||
}
|
||||
|
||||
function clearColumnFilter (name) {
|
||||
const cf = getColumnFilter(name)
|
||||
cf.text = ''
|
||||
cf.selected = []
|
||||
}
|
||||
|
||||
function clearAllColumnFilters () {
|
||||
for (const col of columns) {
|
||||
if (col.name === 'open') continue
|
||||
clearColumnFilter(col.name)
|
||||
}
|
||||
}
|
||||
|
||||
let searchTimer = null
|
||||
watch(
|
||||
() => filters.search,
|
||||
() => {
|
||||
clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(() => {
|
||||
fetchRows()
|
||||
}, 400)
|
||||
}
|
||||
)
|
||||
|
||||
async function fetchRows () {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await get('/pricing/production-product-costing/has-cost-products', {
|
||||
search: filters.search || '',
|
||||
offset: 0,
|
||||
limit: 300
|
||||
})
|
||||
|
||||
const list = Array.isArray(data) ? data : []
|
||||
const sortedList = list.slice().sort((a, b) => {
|
||||
const aDate = Date.parse(String(a?.Tarihi || ''))
|
||||
const bDate = Date.parse(String(b?.Tarihi || ''))
|
||||
const aTs = Number.isNaN(aDate) ? 0 : aDate
|
||||
const bTs = Number.isNaN(bDate) ? 0 : bDate
|
||||
return bTs - aTs
|
||||
})
|
||||
|
||||
allRows.value = sortedList.map((x, i) => ({
|
||||
__rowKey: `${x?.nOnMLNo || ''}-${x?.UrunKodu || ''}-${i}`,
|
||||
__cmp: buildComparableMap(x),
|
||||
...x
|
||||
}))
|
||||
|
||||
tablePagination.value = {
|
||||
...tablePagination.value,
|
||||
sortBy: 'Tarihi',
|
||||
descending: true,
|
||||
page: 1
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = await extractApiErrorDetail(err)
|
||||
allRows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilters () {
|
||||
filters.search = ''
|
||||
clearAllColumnFilters()
|
||||
fetchRows()
|
||||
}
|
||||
|
||||
function openRow (row) {
|
||||
const urunKodu = String(row?.UrunKodu || '').trim()
|
||||
if (!urunKodu) return
|
||||
|
||||
router.push({
|
||||
name: 'production-product-costing-has-cost-history',
|
||||
query: { urun_kodu: urunKodu }
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!canReadOrder.value) return
|
||||
fetchRows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.npc-page {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.npc-filter-bar {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.npc-filter-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.npc-filter-input {
|
||||
min-width: 118px;
|
||||
width: 136px;
|
||||
flex: 0 0 136px;
|
||||
}
|
||||
|
||||
.npc-search {
|
||||
min-width: 240px;
|
||||
max-width: 420px;
|
||||
flex: 1 1 360px;
|
||||
}
|
||||
|
||||
.npc-filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.npc-filter-menu {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.npc-filter-menu-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.npc-table :deep(.q-table thead th) {
|
||||
font-size: 11px;
|
||||
padding: 3px 4px;
|
||||
white-space: normal !important;
|
||||
vertical-align: top !important;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.npc-table :deep(.q-table tbody td) {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
white-space: normal !important;
|
||||
vertical-align: top !important;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.npc-table :deep(.q-table) {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.npc-wrap-col {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.npc-header-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.npc-head-wrap-3 {
|
||||
min-width: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.npc-wrap-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.npc-filter-row {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.npc-filter-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.npc-filter-input {
|
||||
flex: 1 1 140px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3474
ui/src/pages/ProductionProductCostingHasCostDetail.vue
Normal file
3474
ui/src/pages/ProductionProductCostingHasCostDetail.vue
Normal file
@@ -0,0 +1,3474 @@
|
||||
<template>
|
||||
<q-page v-if="canReadOrder" class="pcd-page" :style="pageVars">
|
||||
<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-actions">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
color="grey-7"
|
||||
class="pcd-toolbar-btn"
|
||||
:label="headerInfoCollapsed ? 'HEADER GOSTER' : 'HEADER DARALT'"
|
||||
:icon="headerInfoCollapsed ? 'expand_more' : 'expand_less'"
|
||||
@click="toggleHeaderInfo"
|
||||
/>
|
||||
<q-btn icon="arrow_back" label="Geri" dense flat color="grey-8" class="pcd-toolbar-btn" @click="goBack" />
|
||||
<q-btn label="Yenile" icon="refresh" dense color="primary" class="pcd-toolbar-btn" :loading="detailLoading" @click="fetchDetail" />
|
||||
<q-btn
|
||||
label="Toplu Fiyat Cagir"
|
||||
icon="playlist_add_check"
|
||||
dense
|
||||
color="secondary"
|
||||
outline
|
||||
class="pcd-toolbar-btn"
|
||||
:loading="bulkPriceLoading"
|
||||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||||
@click="fetchBulkItemPrices"
|
||||
/>
|
||||
<q-btn
|
||||
label="SATIR EKLE"
|
||||
dense
|
||||
color="secondary"
|
||||
icon="add"
|
||||
class="pcd-toolbar-btn"
|
||||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||||
@click="openNewRowDialog"
|
||||
/>
|
||||
<q-btn
|
||||
label="Kaydet"
|
||||
icon="save"
|
||||
dense
|
||||
color="primary"
|
||||
outline
|
||||
class="pcd-toolbar-btn"
|
||||
:loading="saveLoading"
|
||||
:disable="!detailHeader || detailLoading || saveLoading || bulkPriceLoading"
|
||||
@click="saveChanges"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="detailError" class="bg-red text-white q-mb-md">
|
||||
Hata: {{ detailError }}
|
||||
</q-banner>
|
||||
|
||||
<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">
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
readonly
|
||||
label="Maliyet Tarihi"
|
||||
:model-value="formatDateTR(costDate)"
|
||||
class="pcd-emphasis-field-alt"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="costDate" mask="YYYY-MM-DD" locale="tr-TR" />
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-3">
|
||||
<q-select
|
||||
v-if="!isNoCostDetail"
|
||||
v-model="detailHeader.UretimSekliID"
|
||||
:options="productionTypes"
|
||||
option-value="id"
|
||||
option-label="aciklama"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
filled
|
||||
label="Uretim Sekli"
|
||||
class="pcd-emphasis-field-alt"
|
||||
@update:model-value="onUretimSekliChange"
|
||||
/>
|
||||
<q-input
|
||||
v-else
|
||||
dense
|
||||
filled
|
||||
readonly
|
||||
label="Uretim Sekli"
|
||||
:model-value="detailHeader.UretimSekli || '-'"
|
||||
class="pcd-emphasis-field-alt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="row q-col-gutter-xs">
|
||||
<div class="col-4">
|
||||
<q-input dense filled readonly label="USD Kuru" :model-value="formatMoney(exchangeRates.usdRate)" class="pcd-emphasis-field-alt" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<q-input dense filled readonly label="EUR Kuru" :model-value="formatMoney(exchangeRates.eurRate)" class="pcd-emphasis-field-alt" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<q-input dense filled readonly label="GBP Kuru" :model-value="formatMoney(exchangeRates.gbpRate)" class="pcd-emphasis-field-alt" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input dense filled readonly label="Uretimi Yapan Firma" :model-value="detailHeader.UretimiYapanFirma || '-'" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input dense filled readonly label="2.Firma" :model-value="detailHeader.SonIsEmriVeren || '-'" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<q-input dense filled readonly label="nOnMLNo" :model-value="detailHeader.nOnMLNo || '-'" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<q-input dense filled readonly label="UrunKodu" :model-value="detailHeader.UrunKodu || '-'" />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="UrunAdi" :model-value="detailHeader.UrunAdi || '-'" />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="Urun Ana Grubu" :model-value="detailHeader.UrunAnaGrubu || '-'" />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="Urun Alt Grubu" :model-value="detailHeader.UrunAltGrubu || '-'" />
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<q-input dense filled readonly label="sKullaniciAdi" :model-value="detailHeader.sKullaniciAdi || '-'" />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="Son Guncelleme Tarihi" :model-value="formatDateTR(detailHeader.dteGuncellemeTarihi)" />
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<q-input dense filled readonly label="sGuncellemeKullaniciAdi" :model-value="detailHeader.sGuncellemeKullaniciAdi || '-'" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<q-input dense filled readonly label="nUrtReceteID" :model-value="detailHeader.nUrtReceteID || '-'" />
|
||||
</div>
|
||||
|
||||
<div v-if="!isNoCostDetail && partSummary && partSummary.length > 0" class="col-12">
|
||||
<div class="pcd-part-summary-card">
|
||||
<div class="pcd-part-summary-title">Parça Bazlı Maliyet Özetleri</div>
|
||||
<q-markup-table dense flat bordered separator="cell" class="pcd-part-summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Parça</th>
|
||||
<th class="text-right">TRY</th>
|
||||
<th class="text-right">USD</th>
|
||||
<th class="text-right">EUR</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="ps in partSummary" :key="ps.name">
|
||||
<td class="text-left text-weight-bold">{{ ps.name }}</td>
|
||||
<td class="text-right">{{ formatMoney(ps.try) }}</td>
|
||||
<td class="text-right">{{ formatMoney(ps.usd) }}</td>
|
||||
<td class="text-right">{{ formatMoney(ps.eur) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</q-markup-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-6 col-md-2">
|
||||
<q-input dense filled readonly label="nOnMLNo" :model-value="detailHeader.nOnMLNo || '-'" />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="Uretimi Yapan Firma" :model-value="detailHeader.UretimiYapanFirma || '-'" />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="2.Firma" :model-value="detailHeader.SonIsEmriVeren || '-'" />
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<q-input dense filled readonly label="UrunKodu" :model-value="detailHeader.UrunKodu || '-'" />
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<q-input dense filled readonly label="Uretim Sekli" :model-value="formatUretimSekli(detailHeader)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detailLoading" class="row justify-center q-pa-lg">
|
||||
<q-spinner color="primary" size="36px" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="detailGroups.length === 0" class="text-grey-7 q-pa-md">
|
||||
Kayit bulunamadi.
|
||||
</div>
|
||||
|
||||
<div v-else class="column q-gutter-md pcd-content-body">
|
||||
<div
|
||||
v-for="(grp, gi) in detailGroups"
|
||||
:key="groupKey(grp, gi)"
|
||||
class="pcd-group-card"
|
||||
>
|
||||
<div class="order-sub-header pcd-sub-header">
|
||||
<div class="sub-left">
|
||||
{{ grp.sAciklama3 || 'TANIMSIZ' }}
|
||||
</div>
|
||||
<div class="sub-right pcd-sub-right-clickable" @click="toggleGroup(grp, gi)">
|
||||
Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }}
|
||||
<q-icon
|
||||
:name="isGroupOpen(grp, gi) ? 'expand_less' : 'expand_more'"
|
||||
size="18px"
|
||||
class="q-ml-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
v-if="isGroupOpen(grp, gi)"
|
||||
class="pcd-detail-table"
|
||||
dense
|
||||
flat
|
||||
bordered
|
||||
separator="cell"
|
||||
row-key="__rowKey"
|
||||
:rows="grp.items"
|
||||
:columns="detailColumns"
|
||||
:table-row-class-fn="resolveDetailRowClass"
|
||||
@row-click="onDetailRowClick"
|
||||
hide-bottom
|
||||
:rows-per-page-options="[0]"
|
||||
>
|
||||
<template #header-cell-lMiktar="props">
|
||||
<q-th :props="props" class="pcd-entry-header">
|
||||
{{ props.col.label }}
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #header-cell-inputPrice="props">
|
||||
<q-th :props="props" class="pcd-entry-header">
|
||||
{{ props.col.label }}
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #header-cell-inputPricePrBr="props">
|
||||
<q-th :props="props" class="pcd-entry-header">
|
||||
{{ props.col.label }}
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #header-cell-maliyeteDahil="props">
|
||||
<q-th :props="props" class="pcd-secondary-header">
|
||||
{{ props.col.label }}
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #header-cell-cmPriceType="props">
|
||||
<q-th :props="props" class="pcd-secondary-header">
|
||||
{{ props.col.label }}
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #body-cell-actions="props">
|
||||
<q-td :props="props" class="q-gutter-xs no-wrap">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
size="sm"
|
||||
color="primary"
|
||||
icon="history"
|
||||
@click.stop="openLineHistory(props.row)"
|
||||
>
|
||||
<q-tooltip>Eski Fiyatlar / Geçmiş</q-tooltip>
|
||||
</q-btn>
|
||||
<q-icon
|
||||
name="edit"
|
||||
size="xs"
|
||||
color="grey-7"
|
||||
class="cursor-pointer"
|
||||
@click.stop="openRowEditorForEdit(props.row)"
|
||||
>
|
||||
<q-tooltip>Satırı Düzenle</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-maliyeteDahil="props">
|
||||
<q-td :props="props" class="text-center pcd-secondary-cell">
|
||||
<q-checkbox
|
||||
v-model="props.row.maliyeteDahil"
|
||||
color="primary"
|
||||
keep-color
|
||||
dense
|
||||
@update:model-value="value => onRowMaliyeteDahilChange(props.row, value)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-cmPriceType="props">
|
||||
<q-td :props="props" class="text-center pcd-secondary-cell">
|
||||
<q-checkbox
|
||||
v-if="isCMGroupName(props.row.sAciklama3)"
|
||||
:model-value="resolveCMPriceTypeChecked(props.row)"
|
||||
color="secondary"
|
||||
keep-color
|
||||
dense
|
||||
@update:model-value="value => onRowCMPriceTypeChange(props.row, value)"
|
||||
/>
|
||||
<span v-else class="text-grey-6">-</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-nOnMLDetNo="props">
|
||||
<q-td :props="props">
|
||||
<div v-if="props.row.isNew" class="text-primary text-weight-bold">YENI</div>
|
||||
<div>{{ props.value }}</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-sParcaAdi="props">
|
||||
<q-td :props="props">
|
||||
{{ props.value || props.row.sAciklama3 || '-' }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-nHammaddeTuruNo="props">
|
||||
<q-td :props="props">
|
||||
<div class="text-weight-medium" style="font-size: 1.1em;">
|
||||
{{ props.value }}<span v-if="props.row.sHammaddeTuruAdi"> - {{ props.row.sHammaddeTuruAdi }}</span>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-sKodu="props">
|
||||
<q-td :props="props">
|
||||
<span>{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-sAciklama="props">
|
||||
<q-td :props="props">
|
||||
<span>{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-sRenk="props">
|
||||
<q-td :props="props">
|
||||
<span>{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-lMiktar="props">
|
||||
<q-td :props="props">
|
||||
<q-input
|
||||
v-model="props.row.miktarInput"
|
||||
class="pcd-inline-input pcd-entry-input"
|
||||
color="primary"
|
||||
dense
|
||||
filled
|
||||
@update:model-value="value => onRowQuantityInput(props.row, value)"
|
||||
@blur="normalizeRowQuantityDisplay(props.row)"
|
||||
input-class="text-right"
|
||||
inputmode="decimal"
|
||||
placeholder="0,0000"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-inputPrice="props">
|
||||
<q-td :props="props">
|
||||
<q-input
|
||||
v-model="props.row.inputPrice"
|
||||
class="pcd-inline-input pcd-entry-input"
|
||||
color="primary"
|
||||
dense
|
||||
filled
|
||||
@update:model-value="value => onRowInputPriceChange(props.row, value)"
|
||||
@blur="normalizeRowPriceDisplay(props.row)"
|
||||
input-class="text-right"
|
||||
inputmode="decimal"
|
||||
placeholder="0,00"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-inputPricePrBr="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row.inputPricePrBr"
|
||||
class="pcd-inline-input pcd-entry-input"
|
||||
:options="priceCurrencyOptions"
|
||||
color="primary"
|
||||
dense
|
||||
filled
|
||||
@update:model-value="value => onRowInputPriceCurrencyChange(props.row, value)"
|
||||
emit-value
|
||||
map-options
|
||||
options-dense
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-sBirim="props">
|
||||
<q-td :props="props">
|
||||
<span>{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
<div class="text-negative text-subtitle1">
|
||||
Bu module erisim yetkiniz yok.
|
||||
</div>
|
||||
</q-page>
|
||||
|
||||
<q-dialog v-model="rowEditorDialogOpen" persistent>
|
||||
<q-card class="pcd-row-editor-dialog">
|
||||
<q-card-section class="row items-center justify-between q-pb-sm">
|
||||
<div class="text-subtitle1 text-weight-bold">
|
||||
{{ rowEditorMode === 'edit' ? 'Satir Duzenle' : 'Yeni Satir Ekle' }}
|
||||
</div>
|
||||
<q-btn flat round dense icon="close" v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="q-pa-md">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-2">
|
||||
<q-input dense filled readonly label="No" v-model="rowEditorForm.nOnMLDetNo" />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="Parca Adi" v-model="rowEditorForm.sParcaAdi" />
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="Parca Grubu" v-model="rowEditorForm.sAciklama3" />
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select
|
||||
v-model="rowEditorForm.nHammaddeTuruNo"
|
||||
class="pcd-row-editor-entry"
|
||||
:options="rowEditorHammaddeOptions"
|
||||
:loading="rowEditorHammaddeLoading"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="250"
|
||||
dense
|
||||
filled
|
||||
label="Hammadde Turu"
|
||||
placeholder="Aramak icin yazin"
|
||||
@filter="filterRowEditorHammaddeOptions"
|
||||
@update:model-value="onRowEditorHammaddeChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select
|
||||
v-model="rowEditorForm.sKodu"
|
||||
class="pcd-row-editor-entry"
|
||||
:options="rowEditorItemOptions"
|
||||
:loading="rowEditorItemLoading"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="350"
|
||||
dense
|
||||
filled
|
||||
label="Kod / Aciklama"
|
||||
placeholder="En az 2 karakter yazin"
|
||||
@filter="filterRowEditorItemOptions"
|
||||
@update:model-value="onRowEditorItemChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select
|
||||
v-model="rowEditorForm.ColorCode"
|
||||
class="pcd-row-editor-entry"
|
||||
:options="rowEditorColorOptions"
|
||||
:loading="rowEditorColorLoading"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
use-input
|
||||
fill-input
|
||||
hide-selected
|
||||
input-debounce="250"
|
||||
dense
|
||||
filled
|
||||
label="Renk"
|
||||
placeholder="Aramak icin yazin"
|
||||
@filter="filterRowEditorColorOptions"
|
||||
@update:model-value="onRowEditorColorChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<q-input v-model="rowEditorForm.miktarInput" class="pcd-row-editor-entry" dense filled label="Miktar" input-class="text-right" inputmode="decimal" />
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<q-input v-model="rowEditorForm.inputPrice" class="pcd-row-editor-entry" dense filled label="Fiyat Giris" input-class="text-right" inputmode="decimal" />
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<q-select
|
||||
v-model="rowEditorForm.inputPricePrBr"
|
||||
class="pcd-row-editor-entry"
|
||||
:options="priceCurrencyOptions"
|
||||
emit-value
|
||||
map-options
|
||||
options-dense
|
||||
dense
|
||||
filled
|
||||
label="Pr.Br."
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<q-input dense filled readonly label="Birim" v-model="rowEditorForm.sBirim" />
|
||||
</div>
|
||||
<div class="col-12 col-md-2 pcd-row-editor-flag" style="display:flex;align-items:center;">
|
||||
<q-checkbox v-model="rowEditorForm.maliyeteDahil" label="Maliyete Dahil" color="primary" keep-color />
|
||||
</div>
|
||||
<div v-if="isCMGroupName(rowEditorForm.sAciklama3)" class="col-12 col-md-3 pcd-row-editor-flag" style="display:flex;align-items:center;">
|
||||
<q-checkbox v-model="rowEditorForm.cmPriceTypeChecked" label="CMT Malzeme Dahil" color="secondary" keep-color />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="q-pa-md">
|
||||
<q-btn
|
||||
v-if="rowEditorMode === 'edit'"
|
||||
flat
|
||||
color="negative"
|
||||
icon="delete"
|
||||
label="Satiri Sil"
|
||||
class="q-mr-auto"
|
||||
@click="deleteRowEditor"
|
||||
/>
|
||||
<q-btn flat label="Vazgec" color="grey-7" v-close-popup />
|
||||
<q-btn color="secondary" icon="save" label="Satira Uygula" @click="saveRowEditor" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="lineHistoryDialogOpen" maximized>
|
||||
<q-card class="pcd-history-dialog">
|
||||
<q-card-section class="row items-center justify-between q-gutter-sm">
|
||||
<div>
|
||||
<div class="text-subtitle1 text-weight-bold">Eski Satinalma ve Recete Fiyatlari</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
{{ lineHistoryTargetSummary }}
|
||||
</div>
|
||||
</div>
|
||||
<q-btn flat round dense icon="close" v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="q-pa-md">
|
||||
<q-banner v-if="lineHistoryError" class="bg-red text-white q-mb-md">
|
||||
Hata: {{ lineHistoryError }}
|
||||
</q-banner>
|
||||
|
||||
<q-banner v-if="lineHistoryShowingFallback" class="bg-amber-1 text-amber-9 q-mb-md rounded-borders border-amber">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="info" color="amber-9" />
|
||||
</template>
|
||||
Tam eşleşme bulunamadı. Benzer kodlara (prefix) ait geçmiş fiyatlar gösteriliyor.
|
||||
</q-banner>
|
||||
|
||||
<div v-if="lineHistoryCanFetchSimilar || lineHistoryCanFetchAlternative" class="row q-col-gutter-sm q-mb-md">
|
||||
<div v-if="lineHistorySearchMode !== 'exact'" class="col-auto">
|
||||
<q-btn
|
||||
label="Geri (Tam Eslesme)"
|
||||
icon="arrow_back"
|
||||
color="grey-7"
|
||||
outline
|
||||
no-caps
|
||||
@click="fetchSimilarItemHistory('exact')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="lineHistoryCanFetchSimilar" class="col-auto">
|
||||
<q-btn
|
||||
label="Benzer Kodları Göster"
|
||||
icon="travel_explore"
|
||||
color="secondary"
|
||||
outline
|
||||
no-caps
|
||||
:loading="lineHistoryLoading && lineHistorySearchMode === 'prefix'"
|
||||
@click="fetchSimilarItemHistory('prefix')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="lineHistoryCanFetchAlternative" class="col-auto">
|
||||
<q-btn
|
||||
label="Diğer Alternatifleri Göster"
|
||||
icon="alt_route"
|
||||
color="primary"
|
||||
outline
|
||||
no-caps
|
||||
:loading="lineHistoryLoading && lineHistorySearchMode === 'alternative'"
|
||||
@click="fetchSimilarItemHistory('alternative')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="lineHistoryLoading" class="row justify-center q-pa-lg">
|
||||
<q-spinner color="primary" size="32px" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="lineHistoryRows.length === 0" class="column items-center q-pa-xl">
|
||||
<q-icon name="history" size="64px" color="grey-4" />
|
||||
<div class="text-grey-7 q-mt-md text-center">
|
||||
Bu kalem için geçmiş hareket bulunamadı.
|
||||
<div v-if="lineHistoryTargetHammaddeTuruNo" class="q-mt-sm">
|
||||
<q-btn
|
||||
label="Hammadde Türü Bazında Benzer Ürünleri Göster"
|
||||
icon="travel_explore"
|
||||
color="secondary"
|
||||
outline
|
||||
no-caps
|
||||
:loading="lineHistoryLoading"
|
||||
@click="fetchSimilarItemHistory('prefix')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<q-table
|
||||
class="pcd-history-table"
|
||||
dense
|
||||
flat
|
||||
bordered
|
||||
row-key="__historyKey"
|
||||
:rows="lineHistoryRows"
|
||||
:columns="lineHistoryColumns"
|
||||
:table-row-class-fn="resolveLineHistoryRowClass"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-bottom
|
||||
>
|
||||
<template v-slot:body-cell-sourceLabel="props">
|
||||
<q-td :props="props">
|
||||
<span class="pcd-history-source-chip" :class="`pcd-history-source-chip--${String(props.row.sourceType || '').toLowerCase()}`">
|
||||
{{ props.row.sourceLabel }}
|
||||
</span>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-price="props">
|
||||
<q-td :props="props" class="text-right">
|
||||
{{ formatMoney(props.row.price) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-quantity="props">
|
||||
<q-td :props="props" class="text-right">
|
||||
{{ formatMoney(props.row.quantity) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-amount="props">
|
||||
<q-td :props="props" class="text-right">
|
||||
{{ formatMoney(props.row.amount) }}
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-select="props">
|
||||
<q-td :props="props" class="text-right">
|
||||
<q-btn
|
||||
label="Sec"
|
||||
color="primary"
|
||||
dense
|
||||
unelevated
|
||||
@click="applyLineHistorySelection(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { get, post, extractApiErrorDetail } from 'src/services/api'
|
||||
import { createTraceId, slog } from 'src/utils/slog'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const $q = useQuasar()
|
||||
const { canRead } = usePermission()
|
||||
const canReadOrder = canRead('order')
|
||||
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref('')
|
||||
const detailGroups = ref([])
|
||||
const detailHeader = ref(null)
|
||||
const costDate = ref('')
|
||||
const exchangeRates = ref(createEmptyExchangeRates())
|
||||
const deletedDetailRows = ref([])
|
||||
const localDraftTimer = ref(0)
|
||||
const initialHeaderSnapshot = ref('')
|
||||
const saveLoading = ref(false)
|
||||
const bulkPriceLoading = ref(false)
|
||||
const newRowSequence = ref(0)
|
||||
const ROW_EDITOR_OPTIONS_LIMIT = 100
|
||||
const ROW_EDITOR_ITEM_MIN_SEARCH_LENGTH = 2
|
||||
const LINE_HISTORY_ROW_LIMIT = 500
|
||||
const LINE_HISTORY_COMBINED_ROW_LIMIT = LINE_HISTORY_ROW_LIMIT * 2
|
||||
const rowEditorDialogOpen = ref(false)
|
||||
const rowEditorMode = ref('new')
|
||||
const rowEditorTargetRowKey = ref('')
|
||||
const rowEditorForm = ref(createRowEditorForm())
|
||||
const rowEditorHammaddeOptions = ref([])
|
||||
const rowEditorHammaddeAllOptions = ref([])
|
||||
const rowEditorHammaddeLoading = ref(false)
|
||||
const rowEditorItemOptions = ref([])
|
||||
const rowEditorItemAllOptions = ref([])
|
||||
const rowEditorItemLoading = ref(false)
|
||||
const rowEditorColorOptions = ref([])
|
||||
const rowEditorColorAllOptions = ref([])
|
||||
const rowEditorColorLoading = ref(false)
|
||||
const lineHistoryDialogOpen = ref(false)
|
||||
const lineHistoryLoading = ref(false)
|
||||
const lineHistoryError = ref('')
|
||||
const lineHistoryRows = ref([])
|
||||
const lineHistoryTargetRowKey = ref('')
|
||||
const lineHistoryTargetHammaddeTuruNo = ref('')
|
||||
const lineHistoryTargetItemCode = ref('')
|
||||
const lineHistoryTargetSummary = ref('')
|
||||
const lineHistorySearchMode = ref('exact')
|
||||
const lineHistoryLastPurchaseMatchStage = ref('')
|
||||
const lineHistoryLastRecipeMatchStage = ref('')
|
||||
const headerInfoCollapsed = ref(false)
|
||||
const subHeaderTop = ref(140)
|
||||
const stickyStackRef = ref(null)
|
||||
const saveToolbarRef = ref(null)
|
||||
const groupOpenState = ref({})
|
||||
const productionTypes = ref([])
|
||||
|
||||
const onMLNo = computed(() => String(route.query?.n_onml_no || '').trim())
|
||||
const productCode = computed(() => String(route.query?.urun_kodu || '').trim())
|
||||
const recipeCode = computed(() => String(route.query?.recete_kodu || '').trim())
|
||||
const detailSource = computed(() => String(route.query?.detail_source || '').trim().toLowerCase())
|
||||
const isNoCostDetail = computed(() => detailSource.value === 'no-cost')
|
||||
const generatedTraceId = ref(createTraceId('pcd-detail'))
|
||||
const traceId = computed(() => String(route.query?.trace_id || generatedTraceId.value).trim())
|
||||
const pageMode = computed(() => (isNoCostDetail.value ? 'new' : 'edit'))
|
||||
const pageVars = computed(() => ({
|
||||
'--pcd-subheader-top': `${subHeaderTop.value}px`
|
||||
}))
|
||||
const lineHistoryCanFetchSimilar = computed(() => (
|
||||
Boolean(lineHistoryTargetItemCode.value) &&
|
||||
lineHistorySearchMode.value !== 'prefix'
|
||||
))
|
||||
const lineHistoryCanFetchAlternative = computed(() => (
|
||||
Boolean(lineHistoryTargetHammaddeTuruNo.value) &&
|
||||
lineHistorySearchMode.value !== 'alternative'
|
||||
))
|
||||
const lineHistoryShowingFallback = computed(() => (
|
||||
lineHistorySearchMode.value === 'exact' &&
|
||||
lineHistoryRows.value.some(r => r.priceType === 'BNZ')
|
||||
))
|
||||
const priceCurrencyOptions = [
|
||||
{ label: 'USD', value: 'USD' },
|
||||
{ label: 'TRY', value: 'TRY' },
|
||||
{ label: 'EUR', value: 'EUR' },
|
||||
{ label: 'GBP', value: 'GBP' }
|
||||
]
|
||||
const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : []))
|
||||
|
||||
// no-cost: required parca slots (from Maliyet Parca Eslestirme)
|
||||
const requiredParcaMappings = ref([])
|
||||
const requiredAttentionRowKeys = ref({})
|
||||
|
||||
const draftStorageKey = computed(() => {
|
||||
if (isNoCostDetail.value) {
|
||||
if (!recipeCode.value) return ''
|
||||
return `pcd-costing:no-cost:${String(productCode.value || '').trim()}:${String(recipeCode.value || '').trim()}`
|
||||
}
|
||||
if (!onMLNo.value) return ''
|
||||
return `pcd-costing:has-cost:${String(onMLNo.value).trim()}`
|
||||
})
|
||||
|
||||
const currentHeaderSnapshot = computed(() => JSON.stringify({
|
||||
UretimSekliID: String(detailHeader.value?.UretimSekliID || '').trim(),
|
||||
UretimSekli: String(detailHeader.value?.UretimSekli || '').trim()
|
||||
}))
|
||||
|
||||
const headerHasUnsavedChanges = computed(() => {
|
||||
if (!initialHeaderSnapshot.value) return false
|
||||
return currentHeaderSnapshot.value !== initialHeaderSnapshot.value
|
||||
})
|
||||
|
||||
const hasUnsavedChanges = computed(() => {
|
||||
if (deletedDetailRows.value.length > 0) return true
|
||||
if (headerHasUnsavedChanges.value) return true
|
||||
return flatDetailRows.value.some(row => Boolean(row?.draftChanged))
|
||||
})
|
||||
|
||||
function persistLocalDraftNow () {
|
||||
const key = draftStorageKey.value
|
||||
if (!key) return
|
||||
try {
|
||||
const payload = {
|
||||
version: 1,
|
||||
savedAt: new Date().toISOString(),
|
||||
mode: pageMode.value,
|
||||
detail_source: detailSource.value || 'has-cost',
|
||||
n_onml_no: onMLNo.value,
|
||||
urun_kodu: productCode.value,
|
||||
recete_kodu: recipeCode.value,
|
||||
header: detailHeader.value ? {
|
||||
UretimSekliID: String(detailHeader.value?.UretimSekliID || '').trim(),
|
||||
UretimSekli: String(detailHeader.value?.UretimSekli || '').trim()
|
||||
} : null,
|
||||
deletedDetailRows: Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : [],
|
||||
detailGroups: Array.isArray(detailGroups.value) ? detailGroups.value : []
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(payload))
|
||||
} catch (err) {
|
||||
slog.error('production-product-costing.detail', 'draft:persist:error', {
|
||||
trace_id: traceId.value,
|
||||
key: draftStorageKey.value,
|
||||
error: String(err?.message || err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function schedulePersistLocalDraft () {
|
||||
try {
|
||||
if (localDraftTimer.value) window.clearTimeout(localDraftTimer.value)
|
||||
localDraftTimer.value = window.setTimeout(() => {
|
||||
localDraftTimer.value = 0
|
||||
persistLocalDraftNow()
|
||||
}, 400)
|
||||
} catch {
|
||||
persistLocalDraftNow()
|
||||
}
|
||||
}
|
||||
|
||||
function clearLocalDraft () {
|
||||
const key = draftStorageKey.value
|
||||
if (!key) return
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function tryHydrateFromLocalDraft () {
|
||||
const key = draftStorageKey.value
|
||||
if (!key) return false
|
||||
try {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return false
|
||||
const payload = JSON.parse(raw)
|
||||
if (!payload || typeof payload !== 'object') return false
|
||||
if (Array.isArray(payload.deletedDetailRows)) deletedDetailRows.value = payload.deletedDetailRows
|
||||
if (Array.isArray(payload.detailGroups)) detailGroups.value = normalizeDetailGroups(payload.detailGroups)
|
||||
if (payload.header && detailHeader.value) {
|
||||
detailHeader.value.UretimSekliID = String(payload.header.UretimSekliID || '').trim()
|
||||
detailHeader.value.UretimSekli = String(payload.header.UretimSekli || '').trim()
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBeforeUnloadGuard (enabled) {
|
||||
if (!enabled) {
|
||||
window.onbeforeunload = null
|
||||
return
|
||||
}
|
||||
window.onbeforeunload = (e) => {
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const toolbarSummary = computed(() => flatDetailRows.value.reduce((acc, row) => {
|
||||
if (!row?.maliyeteDahil) return acc
|
||||
acc.tryTotal += resolveRowTRYTutar(row)
|
||||
acc.usdTotal += resolveRowUSDTutar(row)
|
||||
acc.eurTotal += resolveRowEURTutar(row)
|
||||
acc.gbpTotal += resolveRowGBPTutar(row)
|
||||
return acc
|
||||
}, {
|
||||
tryTotal: 0,
|
||||
usdTotal: 0,
|
||||
eurTotal: 0,
|
||||
gbpTotal: 0
|
||||
}))
|
||||
|
||||
const partSummary = computed(() => {
|
||||
const summary = {}
|
||||
flatDetailRows.value.forEach(row => {
|
||||
if (!row?.maliyeteDahil) return
|
||||
const part = String(row?.sParcaAdi || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||||
if (!summary[part]) {
|
||||
summary[part] = { try: 0, usd: 0, eur: 0, gbp: 0 }
|
||||
}
|
||||
summary[part].try += resolveRowTRYTutar(row)
|
||||
summary[part].usd += resolveRowUSDTutar(row)
|
||||
summary[part].eur += resolveRowEURTutar(row)
|
||||
summary[part].gbp += resolveRowGBPTutar(row)
|
||||
})
|
||||
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%' }
|
||||
]
|
||||
|
||||
function resolveLineHistoryRowClass (row) {
|
||||
if (row?.priceType === 'BNZ') return 'pcd-history-row-similar'
|
||||
if (row?.sourceType === 'recipe') return 'pcd-history-row-recipe'
|
||||
if (row?.sourceType === 'purchase') return 'pcd-history-row-purchase'
|
||||
return ''
|
||||
}
|
||||
|
||||
const detailColumns = [
|
||||
{ name: 'actions', label: '', field: 'actions', align: 'left', style: 'width:60px', headerStyle: 'width:60px' },
|
||||
{ name: 'nOnMLDetNo', label: 'No', field: 'nOnMLDetNo', align: 'left', sortable: true },
|
||||
{ name: 'sParcaAdi', label: 'Parça Adı', field: 'sParcaAdi', align: 'left', sortable: true },
|
||||
{ name: 'nHammaddeTuruNo', label: 'Hammadde Türü', field: 'nHammaddeTuruNo', align: 'left', sortable: true },
|
||||
{ name: 'sKodu', label: 'Kod', field: 'sKodu', align: 'left', sortable: true },
|
||||
{ name: 'sAciklama', label: 'Açıklama', field: 'sAciklama', align: 'left', sortable: true },
|
||||
{ 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: '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) },
|
||||
{ name: 'lTutar', label: 'lTutar', field: 'lTutar', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||||
{ name: 'sFiyatTipi', label: 'sFiyatTipi', field: 'sFiyatTipi', align: 'left', sortable: true },
|
||||
{ name: 'sDovizCinsi', label: 'Döviz', field: 'sDovizCinsi', align: 'left', sortable: true },
|
||||
{ name: 'lDovizKuru', label: 'Kur', field: 'lDovizKuru', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||||
{ name: 'lDovizFiyati', label: 'Döviz Fiyatı', field: 'lDovizFiyati', align: 'right', sortable: true, format: val => formatMoney(val) },
|
||||
{ name: 'priceUpdateState', label: 'Fiyat Tipi', field: 'priceUpdateState', align: 'left', sortable: true },
|
||||
{
|
||||
name: 'usdTutar',
|
||||
label: 'USD TUTAR',
|
||||
field: row => resolveRowUSDTutar(row),
|
||||
align: 'right',
|
||||
sortable: true,
|
||||
format: val => formatMoney(val)
|
||||
},
|
||||
{ name: 'sBirim', label: 'Birim', field: 'sBirim', align: 'left', sortable: true }
|
||||
]
|
||||
|
||||
function formatDateTR (value) {
|
||||
const s = String(value || '').trim()
|
||||
if (!s) return ''
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}):(\d{2}))?/.exec(s)
|
||||
if (!m) return s
|
||||
const datePart = `${m[3]}.${m[2]}.${m[1]}`
|
||||
if (m[4] && m[5]) return `${datePart} ${m[4]}:${m[5]}`
|
||||
return datePart
|
||||
}
|
||||
|
||||
function formatUretimSekli (header) {
|
||||
const id = String(header?.UretimSekliID || '').trim()
|
||||
const aciklama = String(header?.UretimSekli || '').trim()
|
||||
|
||||
if (id && aciklama) return `${id}-${aciklama}`
|
||||
return id || aciklama || '-'
|
||||
}
|
||||
|
||||
function normalizeDateInput (value) {
|
||||
const s = String(value || '').trim()
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s)
|
||||
if (!m) return ''
|
||||
return `${m[1]}-${m[2]}-${m[3]}`
|
||||
}
|
||||
|
||||
function isCMGroupName (value) {
|
||||
const normalizedValue = String(value || '').trim().toUpperCase()
|
||||
return normalizedValue.includes('CM1') || normalizedValue.includes('CM2')
|
||||
}
|
||||
|
||||
function createEmptyExchangeRates () {
|
||||
return {
|
||||
rateDate: '',
|
||||
tryRate: 1,
|
||||
usdRate: 0,
|
||||
eurRate: 0,
|
||||
gbpRate: 0
|
||||
}
|
||||
}
|
||||
|
||||
function createRowEditorForm (seed = {}) {
|
||||
const defaultCurrency = normalizePriceCurrency(seed?.inputPricePrBr || seed?.fiyat_doviz || detailHeader.value?.sDovizCinsi) || 'USD'
|
||||
const cmPriceTypeId = normalizeCMPriceTypeId(seed?.cmPriceTypeId ?? seed?.cm_price_type_id, seed?.sAciklama3 ?? seed?.sParcaAdi)
|
||||
return {
|
||||
__rowKey: String(seed?.__rowKey || '').trim(),
|
||||
isNew: Boolean(seed?.isNew),
|
||||
nStokID: String(seed?.nStokID || '').trim(),
|
||||
sModel: String(seed?.sModel || '').trim(),
|
||||
nOnMLDetNo: String(seed?.nOnMLDetNo || '').trim(),
|
||||
sParcaAdi: String(seed?.sParcaAdi || seed?.sAciklama3 || '').trim(),
|
||||
nHammaddeTuruNo: String(seed?.nHammaddeTuruNo || '').trim(),
|
||||
sHammaddeTuruAdi: String(seed?.sHammaddeTuruAdi || '').trim(),
|
||||
sAciklama3: String(seed?.sAciklama3 || seed?.sParcaAdi || '').trim(),
|
||||
sKodu: String(seed?.sKodu || '').trim(),
|
||||
sAciklama: String(seed?.sAciklama || '').trim(),
|
||||
sRenk: String(seed?.sRenk || seed?.ColorCode || '').trim(),
|
||||
ColorCode: String(seed?.ColorCode || seed?.sRenk || '').trim(),
|
||||
ColorDescription: String(seed?.ColorDescription || '').trim(),
|
||||
miktarInput: normalizeQuantityInput(seed?.miktarInput ?? seed?.lMiktar ?? 1),
|
||||
inputPrice: normalizeInputPrice(seed?.inputPrice ?? seed?.fiyat_girilen ?? 0),
|
||||
inputPricePrBr: defaultCurrency,
|
||||
maliyeteDahil: seed?.maliyeteDahil ?? normalizeBooleanFlag(seed?.maliyete_dahil ?? true),
|
||||
cmPriceTypeChecked: cmPriceTypeId === 2,
|
||||
sBirim: extractPrimaryUnitValue(seed?.sBirim || 'AD') || 'AD'
|
||||
}
|
||||
}
|
||||
|
||||
function formatMoney (value) {
|
||||
return parseMoneyInput(value).toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
function formatQuantity (value) {
|
||||
return parseMoneyInput(value).toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: 4,
|
||||
maximumFractionDigits: 4
|
||||
})
|
||||
}
|
||||
|
||||
function formatBarMoney (value) {
|
||||
const roundedValue = Math.round((Number(value || 0) + Number.EPSILON) * 100) / 100
|
||||
return formatMoney(roundedValue)
|
||||
}
|
||||
|
||||
function normalizePriceCurrency (value) {
|
||||
const normalizedValue = String(value || '').trim().toUpperCase()
|
||||
return ['USD', 'TRY', 'EUR', 'GBP'].includes(normalizedValue) ? normalizedValue : ''
|
||||
}
|
||||
|
||||
function normalizeCodeValue (value) {
|
||||
return String(value ?? '').trim().toUpperCase()
|
||||
}
|
||||
|
||||
function extractPrimaryUnitValue (value) {
|
||||
const normalizedValue = String(value || '').trim()
|
||||
if (!normalizedValue) return ''
|
||||
return normalizedValue.split(/[\s,;|/]+/).filter(Boolean)[0] || normalizedValue
|
||||
}
|
||||
|
||||
function normalizeInputPrice (value) {
|
||||
const numericValue = parseMoneyInput(value)
|
||||
if (value === null || value === undefined || value === '' || !Number.isFinite(numericValue)) {
|
||||
return formatMoney(0)
|
||||
}
|
||||
return formatMoney(numericValue)
|
||||
}
|
||||
|
||||
function normalizeQuantityInput (value) {
|
||||
const numericValue = parseMoneyInput(value)
|
||||
if (value === null || value === undefined || value === '' || !Number.isFinite(numericValue)) {
|
||||
return formatQuantity(0)
|
||||
}
|
||||
return formatQuantity(numericValue)
|
||||
}
|
||||
|
||||
function normalizeSingleSeparatorNumber (rawValue, separator) {
|
||||
const parts = String(rawValue || '').split(separator)
|
||||
if (parts.length <= 1) return String(rawValue || '')
|
||||
if (parts.length === 2) {
|
||||
return separator === ',' ? `${parts[0]}.${parts[1]}` : `${parts[0]}.${parts[1]}`
|
||||
}
|
||||
|
||||
const headParts = parts.slice(0, -1)
|
||||
const lastPart = parts[parts.length - 1]
|
||||
const looksLikeGroupedInteger = lastPart.length === 3 && headParts.every((part, index) => {
|
||||
if (index === 0) return part.length >= 1 && part.length <= 3
|
||||
return part.length === 3
|
||||
})
|
||||
|
||||
if (looksLikeGroupedInteger) {
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
return `${headParts.join('')}.${lastPart}`
|
||||
}
|
||||
|
||||
function parseMoneyInput (value) {
|
||||
const rawValue = String(value ?? '').trim()
|
||||
if (!rawValue) return 0
|
||||
|
||||
const compactValue = rawValue.replace(/\s+/g, '')
|
||||
const lastDotIndex = compactValue.lastIndexOf('.')
|
||||
const lastCommaIndex = compactValue.lastIndexOf(',')
|
||||
const hasDot = lastDotIndex >= 0
|
||||
const hasComma = lastCommaIndex >= 0
|
||||
|
||||
let normalizedValue = compactValue
|
||||
|
||||
if (hasDot && hasComma) {
|
||||
const decimalSeparator = lastDotIndex > lastCommaIndex ? '.' : ','
|
||||
const thousandSeparator = decimalSeparator === '.' ? ',' : '.'
|
||||
normalizedValue = compactValue.replace(new RegExp(`\\${thousandSeparator}`, 'g'), '')
|
||||
if (decimalSeparator === ',') {
|
||||
normalizedValue = normalizedValue.replace(',', '.')
|
||||
}
|
||||
} else if (hasComma) {
|
||||
normalizedValue = normalizeSingleSeparatorNumber(compactValue, ',')
|
||||
} else if (hasDot) {
|
||||
normalizedValue = normalizeSingleSeparatorNumber(compactValue, '.')
|
||||
}
|
||||
|
||||
const numericValue = Number(normalizedValue)
|
||||
return Number.isFinite(numericValue) ? numericValue : 0
|
||||
}
|
||||
|
||||
function normalizeBooleanFlag (value) {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'number') return value === 1
|
||||
|
||||
const normalizedValue = String(value ?? '').trim().toLowerCase()
|
||||
return normalizedValue === '1' || normalizedValue === 'true' || normalizedValue === 'evet'
|
||||
}
|
||||
|
||||
function normalizeCMPriceTypeId (value, groupName) {
|
||||
if (!isCMGroupName(groupName)) return null
|
||||
if (value === null || value === undefined || value === '') return 1
|
||||
|
||||
const numericValue = Number(value)
|
||||
if (numericValue === 2) return 2
|
||||
return 1
|
||||
}
|
||||
|
||||
function resolveCMPriceTypeChecked (row) {
|
||||
return normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, row?.sAciklama3) === 2
|
||||
}
|
||||
|
||||
function firstDefinedValue (source, keys) {
|
||||
for (const key of keys) {
|
||||
const value = source?.[key]
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeExchangeRatesPayload (payload, fallbackDate = '') {
|
||||
return {
|
||||
rateDate: normalizeDateInput(firstDefinedValue(payload, ['rateDate', 'RateDate', 'date', 'Date'])) || fallbackDate,
|
||||
tryRate: 1,
|
||||
usdRate: parseMoneyInput(firstDefinedValue(payload, ['usdRate', 'USDRate', 'usd', 'USD'])),
|
||||
eurRate: parseMoneyInput(firstDefinedValue(payload, ['eurRate', 'EURRate', 'eur', 'EUR'])),
|
||||
gbpRate: parseMoneyInput(firstDefinedValue(payload, ['gbpRate', 'GBPRate', 'gbp', 'GBP']))
|
||||
}
|
||||
}
|
||||
|
||||
function buildRowEditorHammaddeOption (source) {
|
||||
const value = String(source?.value || source?.nHammaddeTuruNo || '').trim()
|
||||
const name = String(source?.sHammaddeTuruAdi || '').trim()
|
||||
const groupName = String(source?.sAciklama3 || '').trim()
|
||||
return {
|
||||
kind: 'hammadde',
|
||||
value,
|
||||
label: String(source?.label || `${value}${name ? ` - ${name}` : ''}`).trim(),
|
||||
nHammaddeTuruNo: value,
|
||||
sHammaddeTuruAdi: name,
|
||||
sAciklama3: groupName,
|
||||
sParcaAdi: String(source?.sParcaAdi || groupName).trim()
|
||||
}
|
||||
}
|
||||
|
||||
function buildRowEditorItemOption (source) {
|
||||
const value = String(source?.value || source?.sKodu || '').trim()
|
||||
const desc = String(source?.sAciklama || '').trim()
|
||||
return {
|
||||
kind: 'item',
|
||||
value,
|
||||
nStokID: String(source?.nStokID || '').trim(),
|
||||
sModel: String(source?.sModel || '').trim(),
|
||||
label: String(source?.label || `${value}${desc ? ` - ${desc}` : ''}`).trim(),
|
||||
sKodu: value,
|
||||
sAciklama: desc,
|
||||
sBirim: extractPrimaryUnitValue(source?.sBirim)
|
||||
}
|
||||
}
|
||||
|
||||
function buildRowEditorColorOption (source) {
|
||||
const value = String(source?.value || source?.colorCode || source?.ColorCode || source?.sRenk || '').trim()
|
||||
const desc = String(source?.colorDescription || source?.ColorDescription || '').trim()
|
||||
return {
|
||||
kind: 'color',
|
||||
value,
|
||||
label: String(source?.label || `${value}${desc ? ` - ${desc}` : ''}`).trim(),
|
||||
colorCode: value,
|
||||
colorDescription: desc
|
||||
}
|
||||
}
|
||||
|
||||
function upsertEditorOption (optionsRef, option) {
|
||||
if (!option?.value) return
|
||||
const nextOptions = [...optionsRef.value]
|
||||
const idx = nextOptions.findIndex(x => String(x?.value || '').trim() === option.value)
|
||||
if (idx >= 0) {
|
||||
nextOptions[idx] = { ...nextOptions[idx], ...option }
|
||||
} else {
|
||||
nextOptions.unshift(option)
|
||||
}
|
||||
optionsRef.value = nextOptions
|
||||
}
|
||||
|
||||
async function fetchRowEditorOptions (kind, search = '', extraParams = {}) {
|
||||
const response = await get('/pricing/production-product-costing/detail-editor-options', {
|
||||
kind,
|
||||
search,
|
||||
limit: ROW_EDITOR_OPTIONS_LIMIT,
|
||||
trace_id: traceId.value,
|
||||
...extraParams
|
||||
})
|
||||
return Array.isArray(response) ? response : []
|
||||
}
|
||||
|
||||
function setRowEditorOptionsByKind (kind, rows) {
|
||||
if (kind === 'hammadde') {
|
||||
rowEditorHammaddeAllOptions.value = rows
|
||||
rowEditorHammaddeOptions.value = rows
|
||||
return
|
||||
}
|
||||
if (kind === 'item') {
|
||||
rowEditorItemAllOptions.value = rows
|
||||
rowEditorItemOptions.value = rows
|
||||
return
|
||||
}
|
||||
rowEditorColorAllOptions.value = rows
|
||||
rowEditorColorOptions.value = rows
|
||||
}
|
||||
|
||||
function setRowEditorLookupLoading (kind, isLoading) {
|
||||
if (kind === 'hammadde') {
|
||||
rowEditorHammaddeLoading.value = isLoading
|
||||
return
|
||||
}
|
||||
if (kind === 'item') {
|
||||
rowEditorItemLoading.value = isLoading
|
||||
return
|
||||
}
|
||||
rowEditorColorLoading.value = isLoading
|
||||
}
|
||||
|
||||
async function refreshRowEditorOptions (kind, search = '') {
|
||||
const extraParams = kind === 'color'
|
||||
? { model_code: String(rowEditorForm.value.sModel || '').trim() }
|
||||
: {}
|
||||
|
||||
if (kind === 'color' && !extraParams.model_code) {
|
||||
setRowEditorOptionsByKind(kind, [])
|
||||
primeRowEditorOptionsFromForm()
|
||||
return []
|
||||
}
|
||||
|
||||
setRowEditorLookupLoading(kind, true)
|
||||
try {
|
||||
const rows = await fetchRowEditorOptions(kind, search, extraParams)
|
||||
setRowEditorOptionsByKind(kind, rows)
|
||||
primeRowEditorOptionsFromForm()
|
||||
return rows
|
||||
} finally {
|
||||
setRowEditorLookupLoading(kind, false)
|
||||
}
|
||||
}
|
||||
|
||||
async function filterRowEditorOptions (kind, val, update, abort) {
|
||||
const normalizedSearch = String(val || '').trim()
|
||||
if (kind === 'item' && normalizedSearch.length < ROW_EDITOR_ITEM_MIN_SEARCH_LENGTH) {
|
||||
update(() => {
|
||||
rowEditorItemAllOptions.value = []
|
||||
rowEditorItemOptions.value = []
|
||||
primeRowEditorOptionsFromForm()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await refreshRowEditorOptions(kind, normalizedSearch)
|
||||
update(() => {
|
||||
setRowEditorOptionsByKind(kind, rows)
|
||||
primeRowEditorOptionsFromForm()
|
||||
})
|
||||
} catch (err) {
|
||||
abort()
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: `Lookup getirilemedi: ${await extractApiErrorDetail(err)}`,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function filterRowEditorHammaddeOptions (val, update, abort) {
|
||||
return filterRowEditorOptions('hammadde', val, update, abort)
|
||||
}
|
||||
|
||||
function filterRowEditorItemOptions (val, update, abort) {
|
||||
return filterRowEditorOptions('item', val, update, abort)
|
||||
}
|
||||
|
||||
function filterRowEditorColorOptions (val, update, abort) {
|
||||
const modelCode = String(rowEditorForm.value.sModel || '').trim()
|
||||
if (!modelCode) {
|
||||
update(() => {
|
||||
rowEditorColorAllOptions.value = []
|
||||
rowEditorColorOptions.value = []
|
||||
})
|
||||
return
|
||||
}
|
||||
return filterRowEditorOptions('color', val, update, abort)
|
||||
}
|
||||
|
||||
async function loadRowEditorColorOptions () {
|
||||
const modelCode = String(rowEditorForm.value.sModel || '').trim()
|
||||
if (!modelCode) return []
|
||||
try {
|
||||
return await fetchRowEditorOptions('color', '', { model_code: modelCode })
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: `Renk lookup getirilemedi: ${await extractApiErrorDetail(err)}`,
|
||||
position: 'top-right'
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function primeRowEditorOptionsFromForm () {
|
||||
upsertEditorOption(rowEditorHammaddeAllOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorHammaddeOptions, buildRowEditorHammaddeOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorItemAllOptions, buildRowEditorItemOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorItemOptions, buildRowEditorItemOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorColorAllOptions, buildRowEditorColorOption(rowEditorForm.value))
|
||||
upsertEditorOption(rowEditorColorOptions, buildRowEditorColorOption(rowEditorForm.value))
|
||||
}
|
||||
|
||||
function getNextDetailNo () {
|
||||
const maxNo = flatDetailRows.value.reduce((acc, row) => {
|
||||
const rowNo = parseInt(String(row?.nOnMLDetNo || '').trim(), 10)
|
||||
return Number.isFinite(rowNo) ? Math.max(acc, rowNo) : acc
|
||||
}, 0)
|
||||
return String(maxNo + 1)
|
||||
}
|
||||
|
||||
function resolveRowColorCode (row) {
|
||||
return normalizeCodeValue(firstDefinedValue(row, [
|
||||
'ColorCode',
|
||||
'colorCode',
|
||||
'sRenk',
|
||||
's_renk',
|
||||
'renk'
|
||||
]))
|
||||
}
|
||||
|
||||
function resolveRowItemDim1Code (row) {
|
||||
return normalizeCodeValue(firstDefinedValue(row, [
|
||||
'ItemDim1Code',
|
||||
'itemDim1Code',
|
||||
'sBeden',
|
||||
's_beden'
|
||||
]))
|
||||
}
|
||||
|
||||
function extractNamedArray (payload, keys) {
|
||||
for (const key of keys) {
|
||||
if (Array.isArray(payload?.[key])) return payload[key]
|
||||
if (Array.isArray(payload?.data?.[key])) return payload.data[key]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function parseDateSortValue (value) {
|
||||
const rawValue = String(value || '').trim()
|
||||
if (!rawValue) return 0
|
||||
const normalizedValue = rawValue.replace(' ', 'T')
|
||||
const timeValue = Date.parse(normalizedValue)
|
||||
return Number.isFinite(timeValue) ? timeValue : 0
|
||||
}
|
||||
|
||||
function sortHistoryRowsByDateDesc (rows) {
|
||||
return [...rows].sort((a, b) => parseDateSortValue(b?.dateValue) - parseDateSortValue(a?.dateValue))
|
||||
}
|
||||
|
||||
function resolveHistorySourceType (item, forcedType = '') {
|
||||
const normalizedType = String(forcedType || firstDefinedValue(item, [
|
||||
'sourceType',
|
||||
'source',
|
||||
'kaynak',
|
||||
'historyType',
|
||||
'tip'
|
||||
])).trim().toLowerCase()
|
||||
|
||||
if (['recipe', 'recete', 'uretim'].includes(normalizedType)) return 'recipe'
|
||||
if (['purchase', 'invoice', 'satinalma', 'baggi_v3', 'baggi-v3'].includes(normalizedType)) return 'purchase'
|
||||
if (firstDefinedValue(item, ['MasrafKodu', 'FaturaKodu', 'EvrakFiyat', 'ItemDim1Code']) !== '') return 'purchase'
|
||||
if (firstDefinedValue(item, ['nOnMLNo', 'dteIslemTarihi', 'lDovizFiyati', 'lDovizTutari']) !== '') return 'recipe'
|
||||
return normalizedType || 'history'
|
||||
}
|
||||
|
||||
function resolveHistorySourceLabel (sourceType) {
|
||||
if (sourceType === 'purchase') return 'BAGGI_V3'
|
||||
if (sourceType === 'recipe') return 'URETIM'
|
||||
return 'HISTORY'
|
||||
}
|
||||
|
||||
function normalizeHistoryRowFromItem (item, index, forcedType = '') {
|
||||
const sourceType = resolveHistorySourceType(item, forcedType)
|
||||
const price = parseMoneyInput(firstDefinedValue(item, [
|
||||
'EvrakFiyat',
|
||||
'evrakFiyat',
|
||||
'fiyat_girilen',
|
||||
'fiyatGirilen',
|
||||
'price',
|
||||
'fiyat',
|
||||
'birimFiyat',
|
||||
'unitPrice',
|
||||
'lDovizFiyati',
|
||||
'lFiyat'
|
||||
]))
|
||||
const quantity = parseMoneyInput(firstDefinedValue(item, [
|
||||
'Miktar',
|
||||
'miktar',
|
||||
'quantity',
|
||||
'qty',
|
||||
'adet',
|
||||
'lMiktar'
|
||||
]))
|
||||
const amountValue = firstDefinedValue(item, [
|
||||
'EvrakTutar',
|
||||
'evrakTutar',
|
||||
'tutar',
|
||||
'amount',
|
||||
'toplamTutar',
|
||||
'lTutar',
|
||||
'lDovizTutari'
|
||||
])
|
||||
const currency = normalizePriceCurrency(firstDefinedValue(item, [
|
||||
'EvrakDoviz',
|
||||
'evrakDoviz',
|
||||
'fiyat_doviz',
|
||||
'fiyatDoviz',
|
||||
'currency',
|
||||
'priceCurrency',
|
||||
'prBr',
|
||||
'pb',
|
||||
'sDovizCinsi',
|
||||
'USD'
|
||||
]))
|
||||
const dateValue = String(firstDefinedValue(item, [
|
||||
'Tarih',
|
||||
'tarih',
|
||||
'InvoiceDate',
|
||||
'invoiceDate',
|
||||
'dteIslemTarihi',
|
||||
'islemTarihi',
|
||||
'dteKayitTarihi',
|
||||
'date'
|
||||
])).trim()
|
||||
const amount = amountValue === ''
|
||||
? price * quantity
|
||||
: parseMoneyInput(amountValue)
|
||||
|
||||
return {
|
||||
__historyKey: String(firstDefinedValue(item, ['id', '__historyKey', 'historyKey'])).trim() || `${sourceType}-${index}`,
|
||||
raw: item,
|
||||
sourceType,
|
||||
sourceLabel: resolveHistorySourceLabel(sourceType),
|
||||
dateValue,
|
||||
dateLabel: formatDateTR(dateValue),
|
||||
invoiceCode: String(firstDefinedValue(item, [
|
||||
'FaturaKodu',
|
||||
'faturaKodu',
|
||||
'InvoiceNumber',
|
||||
'invoiceCode',
|
||||
'FaturaNo',
|
||||
'nOnMLNo',
|
||||
'n_onml_no'
|
||||
])).trim(),
|
||||
companyCode: String(firstDefinedValue(item, [
|
||||
'FirmaKodu',
|
||||
'firmaKodu',
|
||||
'CurrAccCode',
|
||||
'currAccCode',
|
||||
'accountCode',
|
||||
'vendorCode'
|
||||
])).trim(),
|
||||
companyDescription: String(firstDefinedValue(item, [
|
||||
'FirmaAciklama',
|
||||
'firmaAciklama',
|
||||
'CurrAccDescription',
|
||||
'currAccDescription',
|
||||
'companyDescription',
|
||||
'vendorDescription'
|
||||
])).trim(),
|
||||
itemCode: String(firstDefinedValue(item, [
|
||||
'MasrafKodu',
|
||||
'masrafKodu',
|
||||
'sKodu',
|
||||
's_kodu',
|
||||
'ItemCode',
|
||||
'itemCode'
|
||||
])).trim(),
|
||||
itemDescription: String(firstDefinedValue(item, [
|
||||
'MasrafDetay',
|
||||
'masrafDetay',
|
||||
'sAciklama',
|
||||
's_aciklama',
|
||||
'ItemDescription',
|
||||
'itemDescription',
|
||||
'description'
|
||||
])).trim(),
|
||||
colorCode: String(firstDefinedValue(item, [
|
||||
'ColorCode',
|
||||
'colorCode',
|
||||
'sRenk',
|
||||
's_renk'
|
||||
])).trim(),
|
||||
colorDescription: String(firstDefinedValue(item, [
|
||||
'ColorDescription',
|
||||
'colorDescription',
|
||||
'sRenkAdi',
|
||||
's_renk_adi'
|
||||
])).trim(),
|
||||
itemDim1Code: String(firstDefinedValue(item, [
|
||||
'ItemDim1Code',
|
||||
'itemDim1Code',
|
||||
'sBeden',
|
||||
's_beden'
|
||||
])).trim(),
|
||||
itemDim1Description: String(firstDefinedValue(item, [
|
||||
'ItemDim1Description',
|
||||
'itemDim1Description',
|
||||
'sAciklama2',
|
||||
's_aciklama2'
|
||||
])).trim(),
|
||||
priceType: String(item?.priceType || '').trim(),
|
||||
quantity,
|
||||
unit: String(firstDefinedValue(item, [
|
||||
'BIRIM',
|
||||
'birim',
|
||||
'unit',
|
||||
'Unit',
|
||||
'sBirim',
|
||||
's_birim'
|
||||
])).trim(),
|
||||
price,
|
||||
amount,
|
||||
currency: currency || 'USD',
|
||||
recipeInfo: String(firstDefinedValue(item, [
|
||||
'recipeInfo',
|
||||
'receteBilgisi',
|
||||
'recete',
|
||||
'DUMMY',
|
||||
'nUrtReceteID',
|
||||
'sAciklama3',
|
||||
'Description',
|
||||
'Aciklama',
|
||||
'Aciklama1',
|
||||
'LineDescription',
|
||||
'note'
|
||||
])).trim()
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBulkPriceItems (payload) {
|
||||
const list = Array.isArray(payload)
|
||||
? payload
|
||||
: extractNamedArray(payload, ['items', 'data', 'updates', 'matchedItems', 'rows'])
|
||||
|
||||
return list.map((item, index) => {
|
||||
const price = parseMoneyInput(firstDefinedValue(item, [
|
||||
'EvrakFiyat',
|
||||
'evrakFiyat',
|
||||
'fiyat_girilen',
|
||||
'fiyatGirilen',
|
||||
'price',
|
||||
'fiyat',
|
||||
'unitPrice',
|
||||
'birimFiyat',
|
||||
'lDovizFiyati',
|
||||
'lFiyat'
|
||||
]))
|
||||
const currency = normalizePriceCurrency(firstDefinedValue(item, [
|
||||
'EvrakDoviz',
|
||||
'evrakDoviz',
|
||||
'fiyat_doviz',
|
||||
'fiyatDoviz',
|
||||
'currency',
|
||||
'priceCurrency',
|
||||
'prBr',
|
||||
'pb',
|
||||
'sDovizCinsi'
|
||||
]))
|
||||
|
||||
return {
|
||||
__updateKey: `upd-${index}`,
|
||||
__rowKey: String(firstDefinedValue(item, ['__rowKey', 'rowKey'])).trim(),
|
||||
nOnMLDetNo: String(firstDefinedValue(item, ['nOnMLDetNo', 'n_onml_det_no'])).trim(),
|
||||
nHammaddeTuruNo: String(firstDefinedValue(item, ['nHammaddeTuruNo', 'n_hammadde_turu_no'])).trim(),
|
||||
sKodu: normalizeCodeValue(firstDefinedValue(item, ['sKodu', 's_kodu', 'stokKodu', 'MasrafKodu', 'masrafKodu', 'ItemCode', 'itemCode'])),
|
||||
colorCode: normalizeCodeValue(firstDefinedValue(item, ['ColorCode', 'colorCode', 'sRenk', 's_renk', 'renk'])),
|
||||
itemDim1Code: normalizeCodeValue(firstDefinedValue(item, ['ItemDim1Code', 'itemDim1Code', 'item_dim1_code', 'sBeden', 's_beden'])),
|
||||
inputPrice: normalizeInputPrice(price),
|
||||
fiyat_girilen: price,
|
||||
inputPricePrBr: currency || 'USD',
|
||||
fiyat_doviz: currency || 'USD'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeLineHistoryRows (payload) {
|
||||
const purchaseRows = extractNamedArray(payload, [
|
||||
'purchaseRows',
|
||||
'purchase_items',
|
||||
'purchaseItems',
|
||||
'invoiceRows',
|
||||
'invoice_items',
|
||||
'invoiceItems',
|
||||
'baggiV3Rows',
|
||||
'baggi_v3_rows',
|
||||
'baggiRows'
|
||||
])
|
||||
const recipeRows = extractNamedArray(payload, [
|
||||
'recipeRows',
|
||||
'recipe_items',
|
||||
'recipeItems',
|
||||
'uretimRows',
|
||||
'uretim_rows',
|
||||
'productionRows'
|
||||
])
|
||||
|
||||
if (purchaseRows.length > 0 || recipeRows.length > 0) {
|
||||
const normalizedPurchaseRows = sortHistoryRowsByDateDesc(
|
||||
purchaseRows.map((item, index) => normalizeHistoryRowFromItem(item, index, 'purchase'))
|
||||
)
|
||||
const normalizedRecipeRows = sortHistoryRowsByDateDesc(
|
||||
recipeRows.map((item, index) => normalizeHistoryRowFromItem(item, index, 'recipe'))
|
||||
)
|
||||
return sortHistoryRowsByDateDesc([
|
||||
...normalizedPurchaseRows,
|
||||
...normalizedRecipeRows
|
||||
]).slice(0, LINE_HISTORY_COMBINED_ROW_LIMIT)
|
||||
}
|
||||
|
||||
const flatList = Array.isArray(payload)
|
||||
? payload
|
||||
: extractNamedArray(payload, ['items', 'data', 'rows'])
|
||||
|
||||
const normalizedRows = flatList.map((item, index) => normalizeHistoryRowFromItem(item, index))
|
||||
const purchaseHistoryRows = sortHistoryRowsByDateDesc(normalizedRows.filter(row => row.sourceType === 'purchase'))
|
||||
const recipeHistoryRows = sortHistoryRowsByDateDesc(normalizedRows.filter(row => row.sourceType === 'recipe'))
|
||||
const otherHistoryRows = sortHistoryRowsByDateDesc(normalizedRows.filter(row => !['purchase', 'recipe'].includes(row.sourceType)))
|
||||
|
||||
return sortHistoryRowsByDateDesc([
|
||||
...purchaseHistoryRows,
|
||||
...recipeHistoryRows,
|
||||
...otherHistoryRows
|
||||
]).slice(0, LINE_HISTORY_COMBINED_ROW_LIMIT)
|
||||
}
|
||||
|
||||
function rowMatchesBulkUpdate (row, update) {
|
||||
if (update.__rowKey && update.__rowKey === row.__rowKey) return true
|
||||
if (update.nOnMLDetNo && String(row?.nOnMLDetNo || '').trim() === update.nOnMLDetNo) return true
|
||||
|
||||
const rowKodu = normalizeCodeValue(row?.sKodu)
|
||||
const rowRenk = resolveRowColorCode(row)
|
||||
const rowTur = String(row?.nHammaddeTuruNo || '').trim()
|
||||
const rowItemDim1Code = resolveRowItemDim1Code(row)
|
||||
|
||||
if (update.nHammaddeTuruNo && update.sKodu) {
|
||||
if (rowTur === update.nHammaddeTuruNo && rowKodu === update.sKodu) return true
|
||||
}
|
||||
|
||||
if (update.sKodu && update.colorCode && update.itemDim1Code) {
|
||||
if (rowKodu === update.sKodu && rowRenk === update.colorCode && rowItemDim1Code === update.itemDim1Code) return true
|
||||
}
|
||||
|
||||
if (update.sKodu && update.colorCode) {
|
||||
if (rowKodu === update.sKodu && rowRenk === update.colorCode) return true
|
||||
}
|
||||
|
||||
if (update.sKodu && update.itemDim1Code) {
|
||||
if (rowKodu === update.sKodu && rowItemDim1Code === update.itemDim1Code) return true
|
||||
}
|
||||
|
||||
if (update.sKodu) {
|
||||
return rowKodu === update.sKodu
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveExchangeRateValue (currency) {
|
||||
const normalizedCurrency = normalizePriceCurrency(currency)
|
||||
if (normalizedCurrency === 'TRY') return 1
|
||||
if (normalizedCurrency === 'USD') return parseMoneyInput(exchangeRates.value?.usdRate)
|
||||
if (normalizedCurrency === 'EUR') return parseMoneyInput(exchangeRates.value?.eurRate)
|
||||
if (normalizedCurrency === 'GBP') return parseMoneyInput(exchangeRates.value?.gbpRate)
|
||||
return 0
|
||||
}
|
||||
|
||||
function resolveInputCurrency (row) {
|
||||
return normalizePriceCurrency(row?.inputPricePrBr || row?.fiyat_doviz) || 'USD'
|
||||
}
|
||||
|
||||
function resolveNumericRowQuantity (row) {
|
||||
return parseMoneyInput(row?.miktarInput ?? row?.lMiktar)
|
||||
}
|
||||
|
||||
function resolveNumericRowInputPrice (row) {
|
||||
return parseMoneyInput(row?.inputPrice ?? row?.fiyat_girilen)
|
||||
}
|
||||
|
||||
function resolveTRYUnitPriceByInput (inputPrice, inputCurrency, usdRate, eurRate, gbpRate) {
|
||||
if (inputPrice <= 0) return 0
|
||||
if (inputCurrency === 'TRY') return inputPrice
|
||||
if (inputCurrency === 'USD') return usdRate > 0 ? inputPrice * usdRate : 0
|
||||
if (inputCurrency === 'EUR') return eurRate > 0 ? inputPrice * eurRate : 0
|
||||
if (inputCurrency === 'GBP') return gbpRate > 0 ? inputPrice * gbpRate : 0
|
||||
return 0
|
||||
}
|
||||
|
||||
function resolveUSDUnitPriceByInput (inputPrice, inputCurrency, usdRate, eurRate, gbpRate) {
|
||||
if (inputPrice <= 0) return 0
|
||||
if (inputCurrency === 'USD') return inputPrice
|
||||
if (inputCurrency === 'TRY') return usdRate > 0 ? inputPrice / usdRate : 0
|
||||
if (inputCurrency === 'EUR' || inputCurrency === 'GBP') {
|
||||
const tryEquivalent = resolveTRYUnitPriceByInput(inputPrice, inputCurrency, usdRate, eurRate, gbpRate)
|
||||
return usdRate > 0 ? tryEquivalent / usdRate : 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function normalizeDetailRows (items, groupName = '') {
|
||||
const list = Array.isArray(items) ? items : []
|
||||
return list.map((x, i) => ({
|
||||
...x,
|
||||
__rowKey: x?.__rowKey || `${x?.nOnMLNo || ''}-${x?.nOnMLDetNo || ''}-${i}`,
|
||||
miktarInput: x?.miktarInput ?? normalizeQuantityInput(x?.lMiktar),
|
||||
inputPrice: x?.inputPrice ?? normalizeInputPrice(x?.fiyat_girilen),
|
||||
inputPricePrBr: normalizePriceCurrency(x?.inputPricePrBr || x?.fiyat_doviz || x?.sDovizCinsi) || 'USD',
|
||||
maliyeteDahil: x?.maliyeteDahil ?? normalizeBooleanFlag(x?.maliyete_dahil ?? x?.Maliyete_dahil),
|
||||
cmPriceTypeId: normalizeCMPriceTypeId(x?.cmPriceTypeId ?? x?.cm_price_type_id, groupName || x?.sAciklama3),
|
||||
draftChanged: Boolean(x?.draftChanged),
|
||||
priceUpdateState: String(x?.priceUpdateState || '').trim()
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizeDetailGroups (groups) {
|
||||
const list = Array.isArray(groups) ? groups : []
|
||||
return list.map(grp => {
|
||||
const groupName = String(grp?.sAciklama3 || '').trim()
|
||||
const items = normalizeDetailRows(grp?.items, groupName).map(row => ({
|
||||
...row,
|
||||
sAciklama3: String(grp?.sAciklama3 || row?.sAciklama3 || '').trim(),
|
||||
cmPriceTypeId: normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, groupName || row?.sAciklama3)
|
||||
}))
|
||||
// USD TUTAR (DESC) sıralama
|
||||
items.sort((a, b) => {
|
||||
const valA = resolveRowUSDTutar(a)
|
||||
const valB = resolveRowUSDTutar(b)
|
||||
return valB - valA
|
||||
})
|
||||
return {
|
||||
...grp,
|
||||
items
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function recalculateDetailRow (row, options = {}) {
|
||||
const quantity = resolveNumericRowQuantity(row)
|
||||
const inputPrice = resolveNumericRowInputPrice(row)
|
||||
const inputCurrency = resolveInputCurrency(row)
|
||||
const usdRate = resolveExchangeRateValue('USD')
|
||||
const eurRate = resolveExchangeRateValue('EUR')
|
||||
const gbpRate = resolveExchangeRateValue('GBP')
|
||||
const unitTRY = resolveTRYUnitPriceByInput(inputPrice, inputCurrency, usdRate, eurRate, gbpRate)
|
||||
const unitUSD = resolveUSDUnitPriceByInput(inputPrice, inputCurrency, usdRate, eurRate, gbpRate)
|
||||
const unitEUR = eurRate > 0 ? unitTRY / eurRate : 0
|
||||
const unitGBP = gbpRate > 0 ? unitTRY / gbpRate : 0
|
||||
const maliyeteDahil = normalizeBooleanFlag(row?.maliyeteDahil ?? row?.maliyete_dahil ?? row?.Maliyete_dahil)
|
||||
const cmPriceTypeId = normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, row?.sAciklama3)
|
||||
|
||||
row.lMiktar = quantity
|
||||
row.fiyat_girilen = inputPrice
|
||||
row.fiyat_doviz = inputCurrency
|
||||
row.inputPricePrBr = inputCurrency
|
||||
row.maliyeteDahil = maliyeteDahil
|
||||
row.maliyete_dahil = maliyeteDahil ? 1 : 0
|
||||
row.Maliyete_dahil = maliyeteDahil ? 1 : 0
|
||||
row.cmPriceTypeId = cmPriceTypeId
|
||||
row.cm_price_type_id = cmPriceTypeId
|
||||
row.sDovizCinsi = 'USD'
|
||||
row.lDovizKuru = usdRate
|
||||
row.lDovizFiyati = unitUSD
|
||||
row.lFiyat = unitTRY
|
||||
row.lTutar = unitTRY * quantity
|
||||
row.usdTutar = unitUSD * quantity
|
||||
row.eurTutar = unitEUR * quantity
|
||||
row.gbpTutar = unitGBP * quantity
|
||||
row.lTutarUSD = row.usdTutar
|
||||
row.lTutarEURO = row.eurTutar
|
||||
row.lTutarGBP = row.gbpTutar
|
||||
row.lTutarTL = row.lTutar
|
||||
|
||||
if (!options.preserveInputs) {
|
||||
row.miktarInput = normalizeQuantityInput(quantity)
|
||||
row.inputPrice = normalizeInputPrice(inputPrice)
|
||||
}
|
||||
|
||||
if (options.markChanged) {
|
||||
row.draftChanged = true
|
||||
}
|
||||
|
||||
if (options.priceType !== undefined) {
|
||||
row.sFiyatTipi = options.priceType
|
||||
}
|
||||
|
||||
if (options.updateState !== undefined) {
|
||||
row.priceUpdateState = options.updateState
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
function recalculateAllDetailRows () {
|
||||
detailGroups.value = detailGroups.value.map(grp => ({
|
||||
...grp,
|
||||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => recalculateDetailRow({ ...row }, { preserveInputs: true }))
|
||||
}))
|
||||
}
|
||||
|
||||
function resolveRowTRYTutar (row) {
|
||||
const tryAmount = Number(row?.lTutar || 0)
|
||||
return Number.isFinite(tryAmount) ? tryAmount : 0
|
||||
}
|
||||
|
||||
function resolveRowEURTutar (row) {
|
||||
const eurAmount = Number(row?.eurTutar || 0)
|
||||
return Number.isFinite(eurAmount) ? eurAmount : 0
|
||||
}
|
||||
|
||||
function resolveRowGBPTutar (row) {
|
||||
const gbpAmount = Number(row?.gbpTutar || 0)
|
||||
return Number.isFinite(gbpAmount) ? gbpAmount : 0
|
||||
}
|
||||
|
||||
function resolveRowUSDTutar (row) {
|
||||
const usdAmount = Number(row?.usdTutar || 0)
|
||||
if (Number.isFinite(usdAmount)) return usdAmount
|
||||
|
||||
const miktar = resolveNumericRowQuantity(row)
|
||||
const dovizFiyati = Number(row?.lDovizFiyati || 0)
|
||||
const calc = miktar * dovizFiyati
|
||||
return Number.isFinite(calc) ? calc : 0
|
||||
}
|
||||
|
||||
function shouldIgnoreGroupMaliyeteDahil (grp) {
|
||||
return isCMGroupName(grp?.sAciklama3)
|
||||
}
|
||||
|
||||
function shouldIncludeRowInGroupTotal (grp, row) {
|
||||
return shouldIgnoreGroupMaliyeteDahil(grp) || normalizeBooleanFlag(row?.maliyeteDahil)
|
||||
}
|
||||
|
||||
function resolveGroupTRYTutar (grp) {
|
||||
const items = Array.isArray(grp?.items) ? grp.items : []
|
||||
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowTRYTutar(row) : 0), 0)
|
||||
}
|
||||
|
||||
function resolveGroupUSDTutar (grp) {
|
||||
const items = Array.isArray(grp?.items) ? grp.items : []
|
||||
return items.reduce((acc, row) => acc + (shouldIncludeRowInGroupTotal(grp, row) ? resolveRowUSDTutar(row) : 0), 0)
|
||||
}
|
||||
|
||||
function groupKey (grp, gi) {
|
||||
return `${String(grp?.sAciklama3 || 'TANIMSIZ')}-${gi}`
|
||||
}
|
||||
|
||||
function isGroupOpen (grp, gi) {
|
||||
return groupOpenState.value[groupKey(grp, gi)] !== false
|
||||
}
|
||||
|
||||
function toggleGroup (grp, gi) {
|
||||
const key = groupKey(grp, gi)
|
||||
groupOpenState.value = {
|
||||
...groupOpenState.value,
|
||||
[key]: !isGroupOpen(grp, gi)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveElHeight (refVal) {
|
||||
const el = refVal?.$el || refVal
|
||||
return Number(el?.offsetHeight || 0)
|
||||
}
|
||||
|
||||
function updateStickyTop () {
|
||||
const stackH = resolveElHeight(stickyStackRef.value)
|
||||
// Quasar default header height is usually around 50px
|
||||
// If we are in a sub-layout or context where top header is not 50px, this might need adjustment
|
||||
const layoutHeader = document.querySelector('.q-header')
|
||||
const layoutHeaderH = layoutHeader ? layoutHeader.offsetHeight : 50
|
||||
subHeaderTop.value = (stackH || 0) + layoutHeaderH
|
||||
}
|
||||
|
||||
function toggleHeaderInfo () {
|
||||
headerInfoCollapsed.value = !headerInfoCollapsed.value
|
||||
nextTick(() => {
|
||||
updateStickyTop()
|
||||
})
|
||||
}
|
||||
|
||||
function onUretimSekliChange (newId) {
|
||||
if (!detailHeader.value) return
|
||||
const found = productionTypes.value.find(t => String(t.id) === String(newId))
|
||||
if (found) {
|
||||
detailHeader.value.UretimSekliID = found.id
|
||||
detailHeader.value.UretimSekli = found.aciklama
|
||||
schedulePersistLocalDraft()
|
||||
}
|
||||
}
|
||||
|
||||
function buildDetailFetchParams () {
|
||||
if (isNoCostDetail.value) {
|
||||
return {
|
||||
detail_source: 'no-cost',
|
||||
urun_kodu: productCode.value,
|
||||
recete_kodu: recipeCode.value,
|
||||
trace_id: traceId.value
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
n_onml_no: onMLNo.value,
|
||||
urun_kodu: productCode.value,
|
||||
trace_id: traceId.value
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchExchangeRatesForCostDate (targetDate = costDate.value) {
|
||||
const normalizedDate = normalizeDateInput(targetDate)
|
||||
if (!normalizedDate) {
|
||||
exchangeRates.value = createEmptyExchangeRates()
|
||||
recalculateAllDetailRows()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await get('/pricing/production-product-costing/has-cost-detail-exchange-rates', {
|
||||
maliyet_tarihi: normalizedDate,
|
||||
trace_id: traceId.value
|
||||
})
|
||||
exchangeRates.value = normalizeExchangeRatesPayload(response, normalizedDate)
|
||||
} catch (err) {
|
||||
exchangeRates.value = {
|
||||
...createEmptyExchangeRates(),
|
||||
rateDate: normalizedDate
|
||||
}
|
||||
slog.error('production-product-costing.detail', 'exchange-rates:error', {
|
||||
trace_id: traceId.value,
|
||||
maliyet_tarihi: normalizedDate,
|
||||
detail: await extractApiErrorDetail(err)
|
||||
})
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: await extractApiErrorDetail(err),
|
||||
position: 'top-right'
|
||||
})
|
||||
} finally {
|
||||
recalculateAllDetailRows()
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDetail () {
|
||||
if (isNoCostDetail.value && !recipeCode.value) {
|
||||
detailError.value = 'Recete kodu bulunamadi'
|
||||
detailGroups.value = []
|
||||
detailHeader.value = null
|
||||
exchangeRates.value = createEmptyExchangeRates()
|
||||
return
|
||||
}
|
||||
|
||||
if (!isNoCostDetail.value && !onMLNo.value) {
|
||||
detailError.value = 'nOnMLNo bulunamadi'
|
||||
detailGroups.value = []
|
||||
detailHeader.value = null
|
||||
exchangeRates.value = createEmptyExchangeRates()
|
||||
return
|
||||
}
|
||||
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
detailGroups.value = []
|
||||
detailHeader.value = null
|
||||
costDate.value = ''
|
||||
exchangeRates.value = createEmptyExchangeRates()
|
||||
newRowSequence.value = 0
|
||||
rowEditorDialogOpen.value = false
|
||||
rowEditorMode.value = 'new'
|
||||
rowEditorTargetRowKey.value = ''
|
||||
rowEditorForm.value = createRowEditorForm()
|
||||
rowEditorHammaddeAllOptions.value = []
|
||||
rowEditorHammaddeOptions.value = []
|
||||
rowEditorHammaddeLoading.value = false
|
||||
rowEditorItemAllOptions.value = []
|
||||
rowEditorItemOptions.value = []
|
||||
rowEditorItemLoading.value = false
|
||||
rowEditorColorAllOptions.value = []
|
||||
rowEditorColorOptions.value = []
|
||||
rowEditorColorLoading.value = false
|
||||
lineHistoryDialogOpen.value = false
|
||||
lineHistoryLoading.value = false
|
||||
lineHistoryError.value = ''
|
||||
lineHistoryRows.value = []
|
||||
lineHistoryTargetRowKey.value = ''
|
||||
lineHistoryTargetHammaddeTuruNo.value = ''
|
||||
lineHistoryTargetItemCode.value = ''
|
||||
lineHistoryTargetSummary.value = ''
|
||||
lineHistorySearchMode.value = 'exact'
|
||||
lineHistoryLastPurchaseMatchStage.value = ''
|
||||
lineHistoryLastRecipeMatchStage.value = ''
|
||||
|
||||
try {
|
||||
const detailParams = buildDetailFetchParams()
|
||||
slog.info('production-product-costing.detail', 'fetch-detail:start', {
|
||||
trace_id: traceId.value,
|
||||
detail_source: detailSource.value || 'has-cost',
|
||||
n_onml_no: onMLNo.value,
|
||||
urun_kodu: productCode.value,
|
||||
recete_kodu: recipeCode.value
|
||||
})
|
||||
const [headerData, groupsData, typesData] = await Promise.all([
|
||||
get('/pricing/production-product-costing/has-cost-detail-header', detailParams),
|
||||
get('/pricing/production-product-costing/has-cost-detail-groups', detailParams),
|
||||
get('/pricing/production-product-costing/production-types', {
|
||||
trace_id: traceId.value
|
||||
})
|
||||
])
|
||||
detailHeader.value = headerData && typeof headerData === 'object' ? headerData : null
|
||||
productionTypes.value = Array.isArray(typesData) ? typesData : []
|
||||
costDate.value = normalizeDateInput(detailHeader.value?.dteKayitTarihi)
|
||||
detailGroups.value = normalizeDetailGroups(groupsData)
|
||||
initialHeaderSnapshot.value = currentHeaderSnapshot.value
|
||||
// Optional: hydrate local draft after base data load.
|
||||
tryHydrateFromLocalDraft()
|
||||
// ensure required placeholder rows exist (based on mapping screen)
|
||||
try {
|
||||
const mappings = await fetchRequiredParcaMappings()
|
||||
ensureNoCostRequiredRowsFromMappings(mappings)
|
||||
} catch (err) {
|
||||
slog.error('production-product-costing.detail', 'required-mapping:error', {
|
||||
trace_id: traceId.value,
|
||||
detail: await extractApiErrorDetail(err)
|
||||
})
|
||||
}
|
||||
const initialOpen = {}
|
||||
detailGroups.value.forEach((grp, gi) => {
|
||||
initialOpen[groupKey(grp, gi)] = true
|
||||
})
|
||||
groupOpenState.value = initialOpen
|
||||
slog.info('production-product-costing.detail', 'fetch-detail:ok', {
|
||||
trace_id: traceId.value,
|
||||
detail_source: detailSource.value || 'has-cost',
|
||||
group_count: detailGroups.value.length,
|
||||
item_count: flatDetailRows.value.length,
|
||||
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
||||
n_urt_recete_id: detailHeader.value?.nUrtReceteID || ''
|
||||
})
|
||||
} catch (err) {
|
||||
detailError.value = await extractApiErrorDetail(err)
|
||||
slog.error('production-product-costing.detail', 'fetch-detail:error', {
|
||||
trace_id: traceId.value,
|
||||
detail_source: detailSource.value || 'has-cost',
|
||||
n_onml_no: onMLNo.value,
|
||||
urun_kodu: productCode.value,
|
||||
recete_kodu: recipeCode.value,
|
||||
detail: detailError.value
|
||||
})
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack () {
|
||||
if (isNoCostDetail.value) {
|
||||
router.push({ name: 'production-product-costing-no-cost' })
|
||||
return
|
||||
}
|
||||
|
||||
const urunKodu = productCode.value
|
||||
if (!urunKodu) {
|
||||
router.push({ name: 'production-product-costing-has-cost' })
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
name: 'production-product-costing-has-cost-history',
|
||||
query: { urun_kodu: urunKodu }
|
||||
})
|
||||
}
|
||||
|
||||
function buildDetailItemRequestPayload (row) {
|
||||
return {
|
||||
__rowKey: row.__rowKey,
|
||||
n_onml_no: String(row?.nOnMLNo || detailHeader.value?.nOnMLNo || onMLNo.value || '').trim(),
|
||||
n_onml_det_no: String(row?.nOnMLDetNo || '').trim(),
|
||||
n_hammadde_turu_no: String(row?.nHammaddeTuruNo || '').trim(),
|
||||
s_kodu: String(row?.sKodu || '').trim(),
|
||||
s_aciklama: String(row?.sAciklama || '').trim(),
|
||||
s_renk: String(row?.sRenk || '').trim(),
|
||||
color_code: String(firstDefinedValue(row, ['ColorCode', 'colorCode', 'sRenk', 's_renk'])).trim(),
|
||||
color_description: String(firstDefinedValue(row, ['ColorDescription', 'colorDescription', 'sRenkAdi', 's_renk_adi'])).trim(),
|
||||
item_dim1_code: String(firstDefinedValue(row, ['ItemDim1Code', 'itemDim1Code', 'sBeden', 's_beden'])).trim(),
|
||||
item_dim1_description: String(firstDefinedValue(row, ['ItemDim1Description', 'itemDim1Description', 'sAciklama2', 's_aciklama2'])).trim(),
|
||||
s_birim: String(row?.sBirim || '').trim(),
|
||||
l_miktar: resolveNumericRowQuantity(row),
|
||||
fiyat_girilen: resolveNumericRowInputPrice(row),
|
||||
fiyat_doviz: resolveInputCurrency(row),
|
||||
maliyete_dahil: row?.maliyeteDahil ? 1 : 0,
|
||||
cm_price_type_id: normalizeCMPriceTypeId(row?.cmPriceTypeId ?? row?.cm_price_type_id, row?.sAciklama3)
|
||||
}
|
||||
}
|
||||
|
||||
function applyPriceSelectionToRow (targetRowKey, price, currency, priceType) {
|
||||
const normalizedCurrency = normalizePriceCurrency(currency) || 'USD'
|
||||
const normalizedPrice = parseMoneyInput(price)
|
||||
const finalPriceType = priceType || 'SAF'
|
||||
|
||||
detailGroups.value = detailGroups.value.map(grp => ({
|
||||
...grp,
|
||||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
|
||||
if (row.__rowKey !== targetRowKey) return row
|
||||
return recalculateDetailRow({
|
||||
...row,
|
||||
inputPrice: normalizeInputPrice(normalizedPrice),
|
||||
fiyat_girilen: normalizedPrice,
|
||||
inputPricePrBr: normalizedCurrency,
|
||||
fiyat_doviz: normalizedCurrency
|
||||
}, {
|
||||
priceType: finalPriceType,
|
||||
updateState: 'selection',
|
||||
markChanged: true
|
||||
})
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
async function fetchSimilarItemHistory (mode = 'prefix') {
|
||||
if (mode === 'exact') {
|
||||
const row = flatDetailRows.value.find(r => r.__rowKey === lineHistoryTargetRowKey.value)
|
||||
if (row) {
|
||||
await openLineHistory(row)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!lineHistoryTargetHammaddeTuruNo.value && !lineHistoryTargetItemCode.value) return
|
||||
|
||||
const normalizedMode = mode === 'alternative' ? 'alternative' : 'prefix'
|
||||
lineHistorySearchMode.value = normalizedMode
|
||||
lineHistoryLoading.value = true
|
||||
lineHistoryError.value = ''
|
||||
|
||||
try {
|
||||
slog.info('production-product-costing.detail', 'line-history-similar:start', {
|
||||
trace_id: traceId.value,
|
||||
search_mode: normalizedMode,
|
||||
n_hammadde_turu_no: lineHistoryTargetHammaddeTuruNo.value,
|
||||
s_kodu: lineHistoryTargetItemCode.value,
|
||||
maliyet_tarihi: costDate.value,
|
||||
limit: LINE_HISTORY_ROW_LIMIT
|
||||
})
|
||||
|
||||
const response = await get('/pricing/production-product-costing/has-cost-detail-similar-history', {
|
||||
n_hammadde_turu_no: lineHistoryTargetHammaddeTuruNo.value,
|
||||
s_kodu: lineHistoryTargetItemCode.value,
|
||||
maliyet_tarihi: costDate.value,
|
||||
limit: LINE_HISTORY_ROW_LIMIT,
|
||||
search_mode: normalizedMode,
|
||||
trace_id: traceId.value
|
||||
})
|
||||
|
||||
lineHistoryRows.value = normalizeLineHistoryRows(response)
|
||||
lineHistoryLastPurchaseMatchStage.value = String(response?.purchase_match_stage || '').trim()
|
||||
lineHistoryLastRecipeMatchStage.value = String(response?.recipe_match_stage || '').trim()
|
||||
slog.info('production-product-costing.detail', 'line-history-similar:ok', {
|
||||
trace_id: traceId.value,
|
||||
search_mode: normalizedMode,
|
||||
purchase_match_stage: response?.purchase_match_stage || '',
|
||||
recipe_match_stage: response?.recipe_match_stage || '',
|
||||
similar_code_prefix: response?.similar_code_prefix || '',
|
||||
row_count: lineHistoryRows.value.length
|
||||
})
|
||||
|
||||
if (lineHistoryRows.value.length === 0) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Bu hammadde türü için de benzer ürün kaydı bulunamadı.',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
lineHistoryError.value = await extractApiErrorDetail(err)
|
||||
slog.error('production-product-costing.detail', 'line-history-similar:error', {
|
||||
trace_id: traceId.value,
|
||||
search_mode: normalizedMode,
|
||||
n_hammadde_turu_no: lineHistoryTargetHammaddeTuruNo.value,
|
||||
s_kodu: lineHistoryTargetItemCode.value,
|
||||
error: lineHistoryError.value
|
||||
})
|
||||
} finally {
|
||||
lineHistoryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onRowQuantityInput (row, value) {
|
||||
row.miktarInput = value
|
||||
recalculateDetailRow(row, {
|
||||
preserveInputs: true,
|
||||
markChanged: true
|
||||
})
|
||||
schedulePersistLocalDraft()
|
||||
triggerUIUpdate()
|
||||
}
|
||||
|
||||
function triggerUIUpdate () {
|
||||
detailGroups.value = [...detailGroups.value]
|
||||
}
|
||||
|
||||
function normalizeRowQuantityDisplay (row) {
|
||||
row.miktarInput = normalizeQuantityInput(row?.miktarInput)
|
||||
recalculateDetailRow(row, {
|
||||
preserveInputs: true
|
||||
})
|
||||
schedulePersistLocalDraft()
|
||||
triggerUIUpdate()
|
||||
}
|
||||
|
||||
function onRowInputPriceChange (row, value) {
|
||||
row.inputPrice = value
|
||||
recalculateDetailRow(row, {
|
||||
preserveInputs: true,
|
||||
priceType: 'MAN',
|
||||
updateState: 'manual',
|
||||
markChanged: true
|
||||
})
|
||||
schedulePersistLocalDraft()
|
||||
triggerUIUpdate()
|
||||
}
|
||||
|
||||
function normalizeRowPriceDisplay (row) {
|
||||
row.inputPrice = normalizeInputPrice(row?.inputPrice)
|
||||
recalculateDetailRow(row, {
|
||||
preserveInputs: true
|
||||
})
|
||||
schedulePersistLocalDraft()
|
||||
triggerUIUpdate()
|
||||
}
|
||||
|
||||
function onRowInputPriceCurrencyChange (row, value) {
|
||||
row.inputPricePrBr = normalizePriceCurrency(value) || 'USD'
|
||||
recalculateDetailRow(row, {
|
||||
preserveInputs: true,
|
||||
priceType: 'MAN',
|
||||
updateState: 'manual',
|
||||
markChanged: true
|
||||
})
|
||||
schedulePersistLocalDraft()
|
||||
triggerUIUpdate()
|
||||
}
|
||||
|
||||
function onRowMaliyeteDahilChange (row, value) {
|
||||
row.maliyeteDahil = Boolean(value)
|
||||
row.maliyete_dahil = value ? 1 : 0
|
||||
row.Maliyete_dahil = value ? 1 : 0
|
||||
row.draftChanged = true
|
||||
schedulePersistLocalDraft()
|
||||
triggerUIUpdate()
|
||||
}
|
||||
|
||||
function onRowCMPriceTypeChange (row, value) {
|
||||
row.cmPriceTypeId = isCMGroupName(row?.sAciklama3) ? (value ? 2 : 1) : null
|
||||
row.cm_price_type_id = row.cmPriceTypeId
|
||||
row.draftChanged = true
|
||||
schedulePersistLocalDraft()
|
||||
triggerUIUpdate()
|
||||
}
|
||||
|
||||
async function fetchBulkItemPrices () {
|
||||
const flatRows = detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])
|
||||
if (flatRows.length === 0) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Toplu fiyat cagrisi icin satir bulunamadi.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
bulkPriceLoading.value = true
|
||||
try {
|
||||
const response = await post('/pricing/production-product-costing/has-cost-detail-bulk-prices', {
|
||||
n_onml_no: onMLNo.value,
|
||||
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
||||
n_urt_recete_id: detailHeader.value?.nUrtReceteID || '',
|
||||
maliyet_tarihi: costDate.value,
|
||||
items: flatRows.map(buildDetailItemRequestPayload)
|
||||
}, {
|
||||
params: {
|
||||
trace_id: traceId.value
|
||||
}
|
||||
})
|
||||
|
||||
const updates = normalizeBulkPriceItems(response)
|
||||
if (updates.length === 0) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Toplu fiyat cagrisindan uygulanacak veri donmedi.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let appliedCount = 0
|
||||
detailGroups.value = detailGroups.value.map(grp => ({
|
||||
...grp,
|
||||
items: (Array.isArray(grp?.items) ? grp.items : []).map(row => {
|
||||
const match = updates.find(update => rowMatchesBulkUpdate(row, update))
|
||||
if (!match) return row
|
||||
appliedCount += 1
|
||||
return recalculateDetailRow({
|
||||
...row,
|
||||
inputPrice: match.inputPrice,
|
||||
fiyat_girilen: match.fiyat_girilen,
|
||||
inputPricePrBr: match.inputPricePrBr,
|
||||
fiyat_doviz: match.fiyat_doviz
|
||||
}, {
|
||||
priceType: match.priceType || 'SAF',
|
||||
updateState: 'bulk',
|
||||
markChanged: true
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
$q.notify({
|
||||
type: appliedCount > 0 ? 'positive' : 'warning',
|
||||
message: appliedCount > 0
|
||||
? `${appliedCount} kalemin fiyat ve pr. br. bilgisi guncellendi.`
|
||||
: 'Donen veriler satirlarla eslestirilemedi.',
|
||||
position: 'top-right'
|
||||
})
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: await extractApiErrorDetail(err),
|
||||
position: 'top-right'
|
||||
})
|
||||
} finally {
|
||||
bulkPriceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openLineHistory (row) {
|
||||
const rowCode = String(row?.sKodu || '').trim()
|
||||
if (!rowCode) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'History acmak icin satirda kod bilgisi olmali.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
lineHistoryDialogOpen.value = true
|
||||
lineHistoryLoading.value = true
|
||||
lineHistoryError.value = ''
|
||||
lineHistoryRows.value = []
|
||||
lineHistoryTargetRowKey.value = row.__rowKey
|
||||
lineHistoryTargetHammaddeTuruNo.value = String(row?.nHammaddeTuruNo || '').trim()
|
||||
lineHistoryTargetItemCode.value = rowCode
|
||||
lineHistoryTargetSummary.value = `${rowCode} | ${String(row?.sAciklama || '').trim() || 'TANIMSIZ'}`
|
||||
lineHistorySearchMode.value = 'exact'
|
||||
lineHistoryLastPurchaseMatchStage.value = ''
|
||||
lineHistoryLastRecipeMatchStage.value = ''
|
||||
|
||||
try {
|
||||
const response = await get('/pricing/production-product-costing/has-cost-detail-line-history', {
|
||||
n_onml_no: onMLNo.value,
|
||||
urun_kodu: detailHeader.value?.UrunKodu || productCode.value,
|
||||
n_urt_recete_id: detailHeader.value?.nUrtReceteID || '',
|
||||
n_onml_det_no: String(row?.nOnMLDetNo || '').trim(),
|
||||
n_hammadde_turu_no: String(row?.nHammaddeTuruNo || '').trim(),
|
||||
s_kodu: rowCode,
|
||||
s_renk: String(row?.sRenk || '').trim(),
|
||||
color_code: String(firstDefinedValue(row, ['ColorCode', 'colorCode', 'sRenk', 's_renk'])).trim(),
|
||||
item_dim1_code: String(firstDefinedValue(row, ['ItemDim1Code', 'itemDim1Code', 'sBeden', 's_beden'])).trim(),
|
||||
maliyet_tarihi: costDate.value,
|
||||
trace_id: traceId.value
|
||||
})
|
||||
lineHistoryRows.value = normalizeLineHistoryRows(response)
|
||||
} catch (err) {
|
||||
lineHistoryError.value = await extractApiErrorDetail(err)
|
||||
} finally {
|
||||
lineHistoryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onDetailRowClick (evt, row) {
|
||||
// Grid satırı tıklanabilir değil, artık ikonlar ve butonlar kullanılıyor.
|
||||
}
|
||||
|
||||
function applyLineHistorySelection (historyRow) {
|
||||
applyPriceSelectionToRow(lineHistoryTargetRowKey.value, historyRow?.price, historyRow?.currency, historyRow?.priceType)
|
||||
lineHistoryDialogOpen.value = false
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Secilen history fiyati satira uygulandi.',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
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'
|
||||
return row?.draftChanged ? 'pcd-detail-row-secondary' : ''
|
||||
}
|
||||
|
||||
async function bootstrapRowEditorOptions () {
|
||||
try {
|
||||
const hammaddeSearch = String(rowEditorForm.value.nHammaddeTuruNo || '').trim()
|
||||
const itemSearch = String(rowEditorForm.value.sKodu || '').trim()
|
||||
const colorSearch = String(rowEditorForm.value.ColorCode || '').trim()
|
||||
const [hammaddeRows, itemRows] = await Promise.all([
|
||||
refreshRowEditorOptions('hammadde', hammaddeSearch),
|
||||
itemSearch.length >= ROW_EDITOR_ITEM_MIN_SEARCH_LENGTH
|
||||
? refreshRowEditorOptions('item', itemSearch)
|
||||
: Promise.resolve([])
|
||||
])
|
||||
if (itemSearch.length < ROW_EDITOR_ITEM_MIN_SEARCH_LENGTH) {
|
||||
rowEditorItemAllOptions.value = []
|
||||
rowEditorItemOptions.value = []
|
||||
primeRowEditorOptionsFromForm()
|
||||
}
|
||||
const selectedHammadde = hammaddeRows.find(opt => String(opt?.value || '') === hammaddeSearch)
|
||||
if (selectedHammadde) {
|
||||
rowEditorForm.value.sHammaddeTuruAdi = String(selectedHammadde.sHammaddeTuruAdi || rowEditorForm.value.sHammaddeTuruAdi || '').trim()
|
||||
rowEditorForm.value.sAciklama3 = String(selectedHammadde.sAciklama3 || rowEditorForm.value.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||||
rowEditorForm.value.sParcaAdi = String(selectedHammadde.sParcaAdi || selectedHammadde.sAciklama3 || rowEditorForm.value.sParcaAdi || '').trim()
|
||||
}
|
||||
const selectedItem = itemRows.find(opt => String(opt?.value || '') === itemSearch)
|
||||
if (selectedItem) {
|
||||
rowEditorForm.value.nStokID = String(selectedItem.nStokID || rowEditorForm.value.nStokID || '').trim()
|
||||
rowEditorForm.value.sModel = String(selectedItem.sModel || rowEditorForm.value.sModel || '').trim()
|
||||
rowEditorForm.value.sAciklama = String(selectedItem.sAciklama || rowEditorForm.value.sAciklama || '').trim()
|
||||
rowEditorForm.value.sBirim = extractPrimaryUnitValue(selectedItem.sBirim || rowEditorForm.value.sBirim || 'AD') || 'AD'
|
||||
}
|
||||
await refreshRowEditorOptions('color', colorSearch)
|
||||
primeRowEditorOptionsFromForm()
|
||||
} catch (err) {
|
||||
rowEditorHammaddeAllOptions.value = []
|
||||
rowEditorHammaddeOptions.value = []
|
||||
rowEditorItemAllOptions.value = []
|
||||
rowEditorItemOptions.value = []
|
||||
rowEditorColorAllOptions.value = []
|
||||
rowEditorColorOptions.value = []
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: await extractApiErrorDetail(err),
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onRowEditorHammaddeChange (value) {
|
||||
const selected = rowEditorHammaddeOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||||
rowEditorForm.value.nHammaddeTuruNo = String(value || '').trim()
|
||||
if (!selected) return
|
||||
rowEditorForm.value.sHammaddeTuruAdi = String(selected.sHammaddeTuruAdi || '').trim()
|
||||
rowEditorForm.value.sAciklama3 = String(selected.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||||
rowEditorForm.value.sParcaAdi = String(selected.sParcaAdi || selected.sAciklama3 || '').trim()
|
||||
if (!isCMGroupName(rowEditorForm.value.sAciklama3)) {
|
||||
rowEditorForm.value.cmPriceTypeChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onRowEditorItemChange (value) {
|
||||
const selected = rowEditorItemOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||||
rowEditorForm.value.sKodu = String(value || '').trim()
|
||||
if (!selected) return
|
||||
const previousColorCode = String(rowEditorForm.value.ColorCode || '').trim()
|
||||
rowEditorForm.value.nStokID = String(selected.nStokID || '').trim()
|
||||
rowEditorForm.value.sModel = String(selected.sModel || '').trim()
|
||||
rowEditorForm.value.sAciklama = String(selected.sAciklama || '').trim()
|
||||
rowEditorForm.value.sBirim = extractPrimaryUnitValue(selected.sBirim || rowEditorForm.value.sBirim || 'AD') || 'AD'
|
||||
rowEditorForm.value.ColorCode = ''
|
||||
rowEditorForm.value.sRenk = ''
|
||||
rowEditorForm.value.ColorDescription = ''
|
||||
try {
|
||||
await refreshRowEditorOptions('color', previousColorCode)
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: `Renk lookup getirilemedi: ${await extractApiErrorDetail(err)}`,
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
const matchedColor = rowEditorColorOptions.value.find(opt => String(opt?.value || '') === previousColorCode)
|
||||
if (matchedColor) {
|
||||
rowEditorForm.value.ColorCode = previousColorCode
|
||||
rowEditorForm.value.sRenk = previousColorCode
|
||||
rowEditorForm.value.ColorDescription = String(matchedColor.colorDescription || '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
function onRowEditorColorChange (value) {
|
||||
const selected = rowEditorColorOptions.value.find(opt => String(opt?.value || '') === String(value || ''))
|
||||
const normalizedValue = String(value || '').trim()
|
||||
rowEditorForm.value.ColorCode = normalizedValue
|
||||
rowEditorForm.value.sRenk = normalizedValue
|
||||
if (!selected) return
|
||||
rowEditorForm.value.ColorDescription = String(selected.colorDescription || '').trim()
|
||||
}
|
||||
|
||||
function buildRowFromEditorForm () {
|
||||
const form = rowEditorForm.value
|
||||
const existingRow = flatDetailRows.value.find(row => row.__rowKey === rowEditorTargetRowKey.value)
|
||||
const cmPriceTypeId = normalizeCMPriceTypeId(form.cmPriceTypeChecked ? 2 : 1, form.sAciklama3)
|
||||
if (!existingRow) {
|
||||
newRowSequence.value += 1
|
||||
}
|
||||
|
||||
return recalculateDetailRow({
|
||||
...(existingRow || {}),
|
||||
__rowKey: existingRow?.__rowKey || `new-editor-row-${newRowSequence.value}`,
|
||||
isNew: Boolean(existingRow?.isNew) || rowEditorMode.value === 'new',
|
||||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||||
nStokID: String(form.nStokID || '').trim(),
|
||||
sModel: String(form.sModel || '').trim(),
|
||||
nOnMLDetNo: String(form.nOnMLDetNo || '').trim(),
|
||||
sParcaAdi: String(form.sParcaAdi || form.sAciklama3 || '').trim(),
|
||||
sAciklama3: String(form.sAciklama3 || form.sParcaAdi || 'TANIMSIZ').trim() || 'TANIMSIZ',
|
||||
nHammaddeTuruNo: String(form.nHammaddeTuruNo || '').trim(),
|
||||
sHammaddeTuruAdi: String(form.sHammaddeTuruAdi || '').trim(),
|
||||
sKodu: String(form.sKodu || '').trim(),
|
||||
sAciklama: String(form.sAciklama || '').trim(),
|
||||
sRenk: String(form.sRenk || form.ColorCode || '').trim(),
|
||||
ColorCode: String(form.ColorCode || form.sRenk || '').trim(),
|
||||
ColorDescription: String(form.ColorDescription || '').trim(),
|
||||
lMiktar: parseMoneyInput(form.miktarInput),
|
||||
miktarInput: normalizeQuantityInput(form.miktarInput),
|
||||
inputPrice: normalizeInputPrice(form.inputPrice),
|
||||
inputPricePrBr: normalizePriceCurrency(form.inputPricePrBr) || 'USD',
|
||||
fiyat_girilen: parseMoneyInput(form.inputPrice),
|
||||
fiyat_doviz: normalizePriceCurrency(form.inputPricePrBr) || 'USD',
|
||||
maliyeteDahil: Boolean(form.maliyeteDahil),
|
||||
maliyete_dahil: form.maliyeteDahil ? 1 : 0,
|
||||
Maliyete_dahil: form.maliyeteDahil ? 1 : 0,
|
||||
cmPriceTypeId,
|
||||
cm_price_type_id: cmPriceTypeId,
|
||||
sBirim: String(form.sBirim || 'AD').trim() || 'AD',
|
||||
draftChanged: true
|
||||
}, {
|
||||
preserveInputs: true,
|
||||
priceType: 'MAN',
|
||||
updateState: 'editor',
|
||||
markChanged: true
|
||||
})
|
||||
}
|
||||
|
||||
function applyEditorRowToGroups (nextRow) {
|
||||
const targetGroupName = String(nextRow?.sAciklama3 || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||||
const nextGroups = detailGroups.value
|
||||
.map(grp => ({
|
||||
...grp,
|
||||
items: (Array.isArray(grp?.items) ? grp.items : []).filter(row => row.__rowKey !== nextRow.__rowKey)
|
||||
}))
|
||||
.filter(grp => (Array.isArray(grp?.items) ? grp.items.length : 0) > 0 || String(grp?.sAciklama3 || '').trim() === targetGroupName)
|
||||
|
||||
const targetIndex = nextGroups.findIndex(grp => String(grp?.sAciklama3 || '').trim() === targetGroupName)
|
||||
if (targetIndex >= 0) {
|
||||
nextGroups[targetIndex] = {
|
||||
...nextGroups[targetIndex],
|
||||
items: [...(Array.isArray(nextGroups[targetIndex].items) ? nextGroups[targetIndex].items : []), nextRow]
|
||||
.sort((left, right) => parseInt(String(left?.nOnMLDetNo || '0'), 10) - parseInt(String(right?.nOnMLDetNo || '0'), 10))
|
||||
}
|
||||
} else {
|
||||
nextGroups.push({
|
||||
sAciklama3: targetGroupName,
|
||||
totalTutar: 0,
|
||||
totalUSDTutar: 0,
|
||||
items: [nextRow]
|
||||
})
|
||||
}
|
||||
|
||||
detailGroups.value = nextGroups
|
||||
syncAllGroupsOpen()
|
||||
schedulePersistLocalDraft()
|
||||
}
|
||||
|
||||
function syncAllGroupsOpen () {
|
||||
const openState = {}
|
||||
detailGroups.value.forEach((grp, gi) => {
|
||||
openState[groupKey(grp, gi)] = true
|
||||
})
|
||||
groupOpenState.value = openState
|
||||
}
|
||||
|
||||
function normalizeHammaddeNo (value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function normalizeGroupName (value) {
|
||||
return String(value || 'TANIMSIZ').trim() || 'TANIMSIZ'
|
||||
}
|
||||
|
||||
async function fetchRequiredParcaMappings () {
|
||||
const ana = String(detailHeader.value?.UrunAnaGrubu || '').trim()
|
||||
const alt = String(detailHeader.value?.UrunAltGrubu || '').trim()
|
||||
if (!ana || !alt) return []
|
||||
|
||||
const data = await get('/pricing/production-product-costing/maliyet-parca-eslestirme', {
|
||||
trace_id: traceId.value,
|
||||
only_active: 1,
|
||||
urun_ana_grubu: ana,
|
||||
urun_alt_grubu: alt
|
||||
})
|
||||
return Array.isArray(data) ? data : []
|
||||
}
|
||||
|
||||
function ensureNoCostRequiredRowsFromMappings (mappings) {
|
||||
const list = Array.isArray(mappings) ? mappings : []
|
||||
requiredParcaMappings.value = list
|
||||
if (list.length === 0) return
|
||||
|
||||
// Add missing placeholder rows (qty=1, price=0) to remind user
|
||||
list.forEach(mapping => {
|
||||
const groupName = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || mapping?.sAciklama3)
|
||||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||||
hList.forEach(hNoRaw => {
|
||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||
if (!hNo) return
|
||||
|
||||
const exists = flatDetailRows.value.some(r =>
|
||||
normalizeGroupName(r?.sAciklama3) === groupName &&
|
||||
normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo
|
||||
)
|
||||
if (exists) return
|
||||
|
||||
newRowSequence.value += 1
|
||||
const rowKey = `req-auto-row-${newRowSequence.value}`
|
||||
const placeholder = recalculateDetailRow({
|
||||
__rowKey: rowKey,
|
||||
isNew: true,
|
||||
nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '',
|
||||
nOnMLDetNo: '',
|
||||
sParcaAdi: groupName,
|
||||
sAciklama3: groupName,
|
||||
nHammaddeTuruNo: hNo,
|
||||
sHammaddeTuruAdi: '',
|
||||
sKodu: '',
|
||||
sAciklama: '',
|
||||
sRenk: '',
|
||||
ColorCode: '',
|
||||
ColorDescription: '',
|
||||
lMiktar: 1,
|
||||
miktarInput: '1',
|
||||
inputPrice: '0',
|
||||
inputPricePrBr: 'USD',
|
||||
fiyat_girilen: 0,
|
||||
fiyat_doviz: 'USD',
|
||||
maliyeteDahil: true,
|
||||
maliyete_dahil: 1,
|
||||
Maliyete_dahil: 1,
|
||||
cmPriceTypeId: normalizeCMPriceTypeId(1, groupName),
|
||||
cm_price_type_id: normalizeCMPriceTypeId(1, groupName),
|
||||
sBirim: 'AD',
|
||||
draftChanged: true,
|
||||
requiredPlaceholder: true
|
||||
}, {
|
||||
preserveInputs: true,
|
||||
priceType: 'REQ',
|
||||
updateState: 'required',
|
||||
markChanged: true
|
||||
})
|
||||
|
||||
applyEditorRowToGroups(placeholder)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function computeMissingRequiredSlots () {
|
||||
const list = Array.isArray(requiredParcaMappings.value) ? requiredParcaMappings.value : []
|
||||
const missing = []
|
||||
if (list.length === 0) return missing
|
||||
|
||||
list.forEach(mapping => {
|
||||
const groupName = normalizeGroupName(mapping?.parcaBolumAdi || mapping?.mtBolumAdi || mapping?.sAciklama3)
|
||||
const hList = Array.isArray(mapping?.nHammaddeTurleri) ? mapping.nHammaddeTurleri : []
|
||||
hList.forEach(hNoRaw => {
|
||||
const hNo = normalizeHammaddeNo(hNoRaw)
|
||||
if (!hNo) return
|
||||
|
||||
const match = flatDetailRows.value.find(r =>
|
||||
normalizeGroupName(r?.sAciklama3) === groupName &&
|
||||
normalizeHammaddeNo(r?.nHammaddeTuruNo) === hNo
|
||||
)
|
||||
const price = resolveNumericRowInputPrice(match)
|
||||
if (!match || !(price > 0)) {
|
||||
missing.push({ groupName, nHammaddeTuruNo: hNo, rowKey: match?.__rowKey || '' })
|
||||
}
|
||||
})
|
||||
})
|
||||
return missing
|
||||
}
|
||||
|
||||
function removeDetailRowByKey (rowKey) {
|
||||
const normalizedRowKey = String(rowKey || '').trim()
|
||||
if (!normalizedRowKey) return false
|
||||
|
||||
const existingRow = flatDetailRows.value.find(r => String(r?.__rowKey || '').trim() === normalizedRowKey)
|
||||
|
||||
const nextGroups = detailGroups.value
|
||||
.map(grp => ({
|
||||
...grp,
|
||||
items: (Array.isArray(grp?.items) ? grp.items : []).filter(row => row.__rowKey !== normalizedRowKey)
|
||||
}))
|
||||
.filter(grp => (Array.isArray(grp?.items) ? grp.items.length : 0) > 0)
|
||||
|
||||
const hasChanged = nextGroups.length !== detailGroups.value.length ||
|
||||
nextGroups.some((grp, index) => (grp?.items?.length || 0) !== (detailGroups.value[index]?.items?.length || 0))
|
||||
|
||||
if (!hasChanged) return false
|
||||
|
||||
detailGroups.value = nextGroups
|
||||
syncAllGroupsOpen()
|
||||
if (existingRow && !existingRow?.isNew) {
|
||||
deletedDetailRows.value = [
|
||||
...(Array.isArray(deletedDetailRows.value) ? deletedDetailRows.value : []),
|
||||
{
|
||||
__rowKey: normalizedRowKey,
|
||||
nOnMLNo: String(existingRow?.nOnMLNo || '').trim(),
|
||||
nOnMLDetNo: String(existingRow?.nOnMLDetNo || '').trim(),
|
||||
nHammaddeTuruNo: String(existingRow?.nHammaddeTuruNo || '').trim(),
|
||||
sKodu: String(existingRow?.sKodu || '').trim(),
|
||||
sRenk: String(existingRow?.sRenk || '').trim()
|
||||
}
|
||||
]
|
||||
}
|
||||
schedulePersistLocalDraft()
|
||||
return true
|
||||
}
|
||||
|
||||
function openNewRowDialog () {
|
||||
if (!detailHeader.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Yeni satir eklemek icin once detay verisi olmali.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rowEditorMode.value = 'new'
|
||||
rowEditorTargetRowKey.value = ''
|
||||
rowEditorForm.value = createRowEditorForm({
|
||||
isNew: true,
|
||||
nOnMLDetNo: getNextDetailNo(),
|
||||
inputPricePrBr: normalizePriceCurrency(detailHeader.value?.sDovizCinsi) || 'USD',
|
||||
maliyeteDahil: true,
|
||||
sBirim: 'AD'
|
||||
})
|
||||
primeRowEditorOptionsFromForm()
|
||||
rowEditorDialogOpen.value = true
|
||||
void bootstrapRowEditorOptions()
|
||||
}
|
||||
|
||||
function openRowEditorForEdit (row) {
|
||||
rowEditorMode.value = 'edit'
|
||||
rowEditorTargetRowKey.value = String(row?.__rowKey || '').trim()
|
||||
rowEditorForm.value = createRowEditorForm({
|
||||
...row,
|
||||
sAciklama3: row?.sAciklama3 || ''
|
||||
})
|
||||
primeRowEditorOptionsFromForm()
|
||||
rowEditorDialogOpen.value = true
|
||||
void bootstrapRowEditorOptions()
|
||||
}
|
||||
|
||||
function deleteRowEditor () {
|
||||
const targetRowKey = String(rowEditorTargetRowKey.value || rowEditorForm.value.__rowKey || '').trim()
|
||||
if (!targetRowKey) return
|
||||
|
||||
$q.dialog({
|
||||
title: 'Satiri Sil',
|
||||
message: 'Bu satir gridden kaldirilacak. Devam edilsin mi?',
|
||||
cancel: {
|
||||
flat: true,
|
||||
color: 'grey-7',
|
||||
label: 'Vazgec'
|
||||
},
|
||||
ok: {
|
||||
unelevated: true,
|
||||
color: 'negative',
|
||||
label: 'Sil'
|
||||
},
|
||||
persistent: true
|
||||
}).onOk(() => {
|
||||
if (!removeDetailRowByKey(targetRowKey)) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Silinecek satir bulunamadi.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rowEditorDialogOpen.value = false
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Satir gridden kaldirildi.',
|
||||
position: 'top-right'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveRowEditor () {
|
||||
if (!String(rowEditorForm.value.nOnMLDetNo || '').trim()) {
|
||||
rowEditorForm.value.nOnMLDetNo = getNextDetailNo()
|
||||
}
|
||||
if (!String(rowEditorForm.value.nHammaddeTuruNo || '').trim()) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Hammadde Turu secilmeden satir kaydedilemez.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!String(rowEditorForm.value.sKodu || '').trim()) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Kod secilmeden satir kaydedilemez.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const nextRow = buildRowFromEditorForm()
|
||||
applyEditorRowToGroups(nextRow)
|
||||
rowEditorDialogOpen.value = false
|
||||
}
|
||||
|
||||
async function saveChanges () {
|
||||
saveLoading.value = true
|
||||
try {
|
||||
requiredAttentionRowKeys.value = {}
|
||||
if (isNoCostDetail.value) {
|
||||
const missing = computeMissingRequiredSlots()
|
||||
if (missing.length > 0) {
|
||||
const ok = await new Promise(resolve => {
|
||||
$q.dialog({
|
||||
title: 'Eksik Maliyet Parcalari',
|
||||
message: `Eslestirilen parcalarda (fiyat > 0) girilmemis satirlar var. Devam etmek istiyor musunuz? (Eksik: ${missing.length})`,
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(() => resolve(true)).onCancel(() => resolve(false))
|
||||
})
|
||||
if (!ok) {
|
||||
const next = {}
|
||||
missing.forEach(x => {
|
||||
if (x.rowKey) next[String(x.rowKey)] = true
|
||||
})
|
||||
requiredAttentionRowKeys.value = next
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Eksik parcalar isaretlendi. Fiyat girip tekrar deneyin.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Kaydetme endpointi henuz eklenmedi. Buton hazir, backend baglantisi bir sonraki adimda eklenebilir.',
|
||||
position: 'top-right'
|
||||
})
|
||||
} finally {
|
||||
saveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [onMLNo.value, productCode.value, recipeCode.value, detailSource.value],
|
||||
() => {
|
||||
fetchDetail()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => costDate.value,
|
||||
value => {
|
||||
const normalizedDate = normalizeDateInput(value)
|
||||
if (detailHeader.value) {
|
||||
detailHeader.value.dteKayitTarihi = normalizedDate
|
||||
}
|
||||
fetchExchangeRatesForCostDate(normalizedDate)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!canReadOrder.value) return
|
||||
fetchDetail()
|
||||
nextTick(() => {
|
||||
updateStickyTop()
|
||||
})
|
||||
window.addEventListener('resize', updateStickyTop)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateStickyTop)
|
||||
ensureBeforeUnloadGuard(false)
|
||||
if (localDraftTimer.value) window.clearTimeout(localDraftTimer.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => hasUnsavedChanges.value,
|
||||
(value) => {
|
||||
ensureBeforeUnloadGuard(Boolean(value))
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
$q.dialog({
|
||||
title: 'Sayfadan ayriliyorsunuz',
|
||||
message: pageMode.value === 'edit'
|
||||
? 'Yaptiginiz degisiklikler kaybolacak. Devam edilsin mi?'
|
||||
: 'Taslak korunacak. Sayfadan cikmak istiyor musunuz?',
|
||||
ok: { label: 'Evet', color: 'negative' },
|
||||
cancel: { label: 'Hayir' },
|
||||
persistent: true
|
||||
})
|
||||
.onOk(() => {
|
||||
// NEW (no-cost): always persist draft so it can be resumed
|
||||
if (pageMode.value === 'new') {
|
||||
persistLocalDraftNow()
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// EDIT (has-cost): allow exit, keep draft for now (later we can clear it after successful save)
|
||||
next()
|
||||
})
|
||||
.onCancel(() => next(false))
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [detailLoading.value, detailError.value, !!detailHeader.value, headerInfoCollapsed.value],
|
||||
() => {
|
||||
nextTick(() => {
|
||||
updateStickyTop()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pcd-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pcd-content-body {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pcd-sticky-stack {
|
||||
position: sticky !important;
|
||||
top: 50px !important;
|
||||
z-index: 1000 !important;
|
||||
background: #fff;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pcd-save-toolbar {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.pcd-toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pcd-toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pcd-toolbar-title {
|
||||
flex: 0 0 auto;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #223048;
|
||||
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-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
padding: 5px 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d7e0ea;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pcd-toolbar-pill-emphasis {
|
||||
background: var(--q-primary);
|
||||
border-color: #145ea8;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.pcd-toolbar-pill-neutral {
|
||||
background: #f5f7fa;
|
||||
color: #2b3c54;
|
||||
}
|
||||
|
||||
.pcd-toolbar-pill-label {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.pcd-toolbar-pill-value {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pcd-toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.pcd-toolbar-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pcd-toolbar-btn :deep(.q-btn__content) {
|
||||
min-height: 34px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.pcd-add-row-dialog {
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.pcd-row-editor-dialog {
|
||||
width: min(960px, 96vw);
|
||||
max-width: 96vw;
|
||||
}
|
||||
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__control) {
|
||||
background: color-mix(in srgb, var(--q-secondary) 18%, white) !important;
|
||||
border: 1px solid var(--q-secondary) !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__label),
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__native),
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__input),
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__marginal),
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-select__dropdown-icon),
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-field__selection) {
|
||||
color: var(--q-secondary) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry.q-field--focused .q-field__control) {
|
||||
box-shadow: 0 0 0 1px var(--q-secondary), 0 0 0 3px color-mix(in srgb, var(--q-secondary) 24%, white);
|
||||
}
|
||||
|
||||
.pcd-row-editor-dialog :deep(.pcd-row-editor-entry .q-placeholder) {
|
||||
color: var(--q-secondary) !important;
|
||||
opacity: 0.76;
|
||||
}
|
||||
|
||||
.pcd-row-editor-flag {
|
||||
background: color-mix(in srgb, var(--q-secondary) 10%, white);
|
||||
border: 1px solid color-mix(in srgb, var(--q-secondary) 35%, white);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.pcd-history-dialog {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pcd-detail-header-bar {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pcd-emphasis-field :deep(.q-field__control) {
|
||||
background: var(--q-primary) !important;
|
||||
border: 1px solid #145ea8;
|
||||
}
|
||||
|
||||
.pcd-emphasis-field :deep(.q-field__label) {
|
||||
color: #eaf3ff !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pcd-emphasis-field :deep(.q-field__native),
|
||||
.pcd-emphasis-field :deep(.q-field__input) {
|
||||
color: #ffffff !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pcd-emphasis-field-alt :deep(.q-field__control) {
|
||||
background: color-mix(in srgb, var(--q-secondary) 25%, white) !important;
|
||||
border: 1px solid var(--q-secondary);
|
||||
}
|
||||
|
||||
.pcd-emphasis-field-alt :deep(.q-field__label) {
|
||||
color: var(--q-primary) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pcd-emphasis-field-alt :deep(.q-field__native),
|
||||
.pcd-emphasis-field-alt :deep(.q-field__input),
|
||||
.pcd-emphasis-field-alt :deep(.q-field__selection) {
|
||||
color: var(--q-primary) !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pcd-readonly-header-input :deep(.q-field__control) {
|
||||
background: #f8f9fa !important;
|
||||
}
|
||||
|
||||
.pcd-group-card {
|
||||
border: 1px solid #dfe3e8;
|
||||
border-radius: 6px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
margin: 0 10px 15px;
|
||||
}
|
||||
|
||||
.pcd-sub-header {
|
||||
position: sticky !important;
|
||||
top: var(--pcd-subheader-top) !important;
|
||||
z-index: 990 !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
min-height: 42px;
|
||||
height: 42px;
|
||||
border-top: 1px solid #d6c06a;
|
||||
border-bottom: 1px solid #d6c06a;
|
||||
background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.pcd-sub-header .sub-left {
|
||||
font-weight: 800;
|
||||
color: #2b1f05;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pcd-sub-header .sub-right {
|
||||
font-weight: 900;
|
||||
color: #3b2f09;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
text-align: right;
|
||||
}
|
||||
.pcd-sub-right-clickable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table__middle) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table thead) {
|
||||
display: table-header-group !important;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table thead tr:first-child th) {
|
||||
position: sticky !important;
|
||||
top: calc(var(--pcd-subheader-top) + 42px) !important;
|
||||
z-index: 980 !important;
|
||||
background: #f8f9fa !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table thead th) {
|
||||
font-size: 11px;
|
||||
padding: 3px 4px;
|
||||
white-space: normal;
|
||||
line-height: 1.2;
|
||||
vertical-align: bottom;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table tbody td) {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.q-table tbody tr) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-detail-row-secondary td) {
|
||||
background: color-mix(in srgb, var(--q-secondary) 14%, white) !important;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-detail-row-required td) {
|
||||
background: color-mix(in srgb, #ff9800 18%, white) !important;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-entry-header) {
|
||||
background: color-mix(in srgb, var(--q-primary) 16%, #f8f9fa) !important;
|
||||
color: var(--q-primary) !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-secondary-header) {
|
||||
background: color-mix(in srgb, var(--q-secondary) 18%, #f8f9fa) !important;
|
||||
color: var(--q-secondary) !important;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-secondary-cell) {
|
||||
background: color-mix(in srgb, var(--q-secondary) 10%, white) !important;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-entry-input .q-field__control) {
|
||||
background: color-mix(in srgb, var(--q-primary) 12%, white) !important;
|
||||
border: 1px solid var(--q-primary) !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-entry-input .q-field__native),
|
||||
.pcd-detail-table :deep(.pcd-entry-input .q-field__input),
|
||||
.pcd-detail-table :deep(.pcd-entry-input .q-field__marginal),
|
||||
.pcd-detail-table :deep(.pcd-entry-input .q-select__dropdown-icon) {
|
||||
color: var(--q-primary) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-entry-input.q-field--focused .q-field__control) {
|
||||
box-shadow: 0 0 0 1px var(--q-primary), 0 0 0 3px color-mix(in srgb, var(--q-primary) 28%, white);
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-entry-input .q-placeholder) {
|
||||
color: var(--q-primary) !important;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-inline-input .q-field__control) {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.pcd-detail-table :deep(.pcd-inline-input .q-field__native),
|
||||
.pcd-detail-table :deep(.pcd-inline-input .q-field__input) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.pcd-detail-table {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pcd-part-summary-card {
|
||||
margin-top: 8px;
|
||||
border: 1px solid #d7e0ea;
|
||||
border-radius: 8px;
|
||||
background: #fcfdfe;
|
||||
padding: 8px;
|
||||
}
|
||||
.pcd-part-summary-title {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #2b3c54;
|
||||
margin-bottom: 6px;
|
||||
padding-left: 4px;
|
||||
border-left: 3px solid var(--q-primary);
|
||||
}
|
||||
.pcd-part-summary-table {
|
||||
background: transparent !important;
|
||||
}
|
||||
.pcd-part-summary-table thead th {
|
||||
background: #f5f7fa;
|
||||
font-weight: 800;
|
||||
color: #44556c;
|
||||
}
|
||||
.pcd-part-summary-table tbody td {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pcd-history-table :deep(.q-table thead th) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pcd-history-table :deep(.q-table tbody td) {
|
||||
font-size: 12px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.pcd-history-table :deep(.pcd-history-row-purchase td) {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.pcd-history-table :deep(.pcd-history-row-recipe td) {
|
||||
background: color-mix(in srgb, var(--q-secondary) 10%, white);
|
||||
}
|
||||
|
||||
.pcd-history-table :deep(.pcd-history-row-similar td) {
|
||||
background: #fff8e1 !important;
|
||||
border-bottom: 1px solid #ffe082 !important;
|
||||
}
|
||||
|
||||
.pcd-history-source-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 72px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.pcd-history-source-chip--purchase {
|
||||
background: color-mix(in srgb, var(--q-primary) 12%, white);
|
||||
color: var(--q-primary);
|
||||
}
|
||||
|
||||
.pcd-history-source-chip--bnz-v3,
|
||||
.pcd-history-source-chip--bnz-rec {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pcd-history-source-chip--recipe {
|
||||
background: color-mix(in srgb, var(--q-secondary) 18%, white);
|
||||
color: color-mix(in srgb, var(--q-secondary) 82%, black);
|
||||
}
|
||||
|
||||
.pcd-history-source-chip--history {
|
||||
background: #eceff3;
|
||||
color: #44556c;
|
||||
}
|
||||
</style>
|
||||
259
ui/src/pages/ProductionProductCostingHasCostHistory.vue
Normal file
259
ui/src/pages/ProductionProductCostingHasCostHistory.vue
Normal file
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<q-page v-if="canReadOrder" class="pch-page">
|
||||
<div class="sticky-stack pch-sticky-stack">
|
||||
<div class="ol-filter-bar filter-bar pch-filter-bar">
|
||||
<div class="row items-center q-col-gutter-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
:model-value="productCode || '-'"
|
||||
label="Urun Kodu"
|
||||
dense
|
||||
filled
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<q-input
|
||||
:model-value="rows.length"
|
||||
label="Toplam Maliyet Kaydi"
|
||||
dense
|
||||
filled
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-toolbar pch-save-toolbar">
|
||||
<div class="text-subtitle2 text-weight-bold">Secili Urunun Tum Maliyetleri</div>
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-btn
|
||||
icon="arrow_back"
|
||||
label="Geri"
|
||||
flat
|
||||
color="grey-8"
|
||||
@click="goBack"
|
||||
/>
|
||||
<q-btn
|
||||
label="Yenile"
|
||||
icon="refresh"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="fetchRows"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
title=""
|
||||
class="ol-table pch-table"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
separator="cell"
|
||||
row-key="__rowKey"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
no-data-label="Kayit bulunamadi"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-bottom
|
||||
>
|
||||
<template #body-cell="props">
|
||||
<q-td v-if="props.col.name === 'open'" :props="props" class="text-center">
|
||||
<q-btn
|
||||
icon="open_in_new"
|
||||
color="primary"
|
||||
flat
|
||||
round
|
||||
dense
|
||||
@click="openRow(props.row)"
|
||||
>
|
||||
<q-tooltip>Ac</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-else :props="props" class="pch-wrap-col">
|
||||
<div class="pch-wrap-2" :title="String(props.value ?? '')">{{ props.value }}</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-banner v-if="error" class="bg-red text-white q-mt-sm">
|
||||
Hata: {{ error }}
|
||||
</q-banner>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
<div class="text-negative text-subtitle1">
|
||||
Bu module erisim yetkiniz yok.
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { get, extractApiErrorDetail } from 'src/services/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { canRead } = usePermission()
|
||||
const canReadOrder = canRead('order')
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const allRows = ref([])
|
||||
|
||||
const productCode = computed(() => String(route.query?.urun_kodu || '').trim())
|
||||
|
||||
const columns = [
|
||||
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' },
|
||||
{ name: 'nOnMLNo', label: 'nOnMLNo', field: 'nOnMLNo', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'UrunKodu', label: 'UrunKodu', field: 'UrunKodu', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'UrunAdi', label: 'UrunAdi', field: 'UrunAdi', align: 'left', sortable: true, style: 'width:12%', headerStyle: 'width:12%' },
|
||||
{ name: 'Tarihi', label: 'Tarihi', field: 'Tarihi', align: 'center', sortable: true, format: val => formatDateTR(val), style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'sKullaniciAdi', label: 'sKullaniciAdi', field: 'sKullaniciAdi', align: 'left', sortable: true, style: 'width:8%', headerStyle: 'width:8%' },
|
||||
{ name: 'lTutarUSD', label: 'lTutarUSD', field: 'lTutarUSD', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'lTutarTL', label: 'lTutarTL', field: 'lTutarTL', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'lTutarEURO', label: 'lTutarEURO', field: 'lTutarEURO', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'sDovizCinsi', label: 'sDovizCinsi', field: 'sDovizCinsi', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'lTutarDoviz', label: 'lTutarDoviz', field: 'lTutarDoviz', align: 'right', sortable: true, format: val => formatMoney(val), style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'dteGuncellemeTarihi', label: 'dteGuncellemeTarihi', field: 'dteGuncellemeTarihi', align: 'center', sortable: true, format: val => formatDateTR(val), style: 'width:9%', headerStyle: 'width:9%' },
|
||||
{ name: 'sGuncellemeKullaniciAdi', label: 'sGuncellemeKullaniciAdi', field: 'sGuncellemeKullaniciAdi', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' },
|
||||
{ name: 'nUrtReceteID', label: 'nUrtReceteID', field: 'nUrtReceteID', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'sAciklama', label: 'sAciklama', field: 'sAciklama', align: 'left', sortable: true, style: 'width:10%', headerStyle: 'width:10%' }
|
||||
]
|
||||
|
||||
const rows = computed(() => allRows.value)
|
||||
|
||||
function formatDateTR (value) {
|
||||
const s = String(value || '').trim()
|
||||
if (!s) return ''
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}):(\d{2}))?/.exec(s)
|
||||
if (!m) return s
|
||||
const datePart = `${m[3]}.${m[2]}.${m[1]}`
|
||||
if (m[4] && m[5]) return `${datePart} ${m[4]}:${m[5]}`
|
||||
return datePart
|
||||
}
|
||||
|
||||
function formatMoney (value) {
|
||||
return Number(value || 0).toLocaleString('tr-TR', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 4
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchRows () {
|
||||
const urunKodu = productCode.value
|
||||
if (!urunKodu) {
|
||||
error.value = 'Urun kodu bulunamadi'
|
||||
allRows.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await get('/pricing/production-product-costing/has-cost-history', {
|
||||
urun_kodu: urunKodu
|
||||
})
|
||||
|
||||
const list = Array.isArray(data) ? data : []
|
||||
allRows.value = list.map((x, i) => ({
|
||||
__rowKey: `${x?.nOnMLNo || ''}-${x?.UrunKodu || ''}-${i}`,
|
||||
...x
|
||||
}))
|
||||
} catch (err) {
|
||||
error.value = await extractApiErrorDetail(err)
|
||||
allRows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack () {
|
||||
router.push({ name: 'production-product-costing-has-cost' })
|
||||
}
|
||||
|
||||
function openRow (row) {
|
||||
const onMLNo = String(row?.nOnMLNo || '').trim()
|
||||
if (!onMLNo) return
|
||||
|
||||
router.push({
|
||||
name: 'production-product-costing-has-cost-detail',
|
||||
query: {
|
||||
n_onml_no: onMLNo,
|
||||
urun_kodu: productCode.value || String(row?.UrunKodu || '').trim()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => productCode.value,
|
||||
() => {
|
||||
fetchRows()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!canReadOrder.value) return
|
||||
fetchRows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pch-page {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.pch-sticky-stack {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pch-filter-bar {
|
||||
min-height: auto !important;
|
||||
padding-top: 8px !important;
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.pch-save-toolbar {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.pch-table :deep(.q-table thead th) {
|
||||
font-size: 11px;
|
||||
padding: 3px 4px;
|
||||
white-space: normal !important;
|
||||
vertical-align: top !important;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.pch-table :deep(.q-table tbody td) {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
white-space: normal !important;
|
||||
vertical-align: top !important;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.pch-table :deep(.q-table) {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.pch-wrap-col {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.pch-wrap-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
744
ui/src/pages/ProductionProductCostingMTBolumMapping.vue
Normal file
744
ui/src/pages/ProductionProductCostingMTBolumMapping.vue
Normal file
@@ -0,0 +1,744 @@
|
||||
<template>
|
||||
<q-page v-if="canReadOrder" class="pcmm-page q-pa-md">
|
||||
<div class="pcmm-header row items-center q-col-gutter-md">
|
||||
<div class="col">
|
||||
<div class="text-h6">Maliyet Parca Eslestirme</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
V3 Urun Ilk Grubu (42. ozellik) + Urun Ana/Alt Grup + URETIM Parca Bolum + Hammadde Turleri eslestirmesi (URETIM mk_ tablolarinda tutulur)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="loading"
|
||||
@click="refreshAll"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
|
||||
<q-table
|
||||
class="ol-table pcmm-table"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
separator="cell"
|
||||
row-key="__key"
|
||||
:rows="mappings"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
no-data-label="Kayit bulunamadi"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-bottom
|
||||
>
|
||||
<template #top-right>
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-input
|
||||
v-model="filters.search"
|
||||
dense
|
||||
filled
|
||||
clearable
|
||||
debounce="250"
|
||||
label="Ara (Ana/Alt)"
|
||||
style="min-width: 220px"
|
||||
@update:model-value="fetchSheet"
|
||||
@clear="onSearchCleared"
|
||||
/>
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="content_copy"
|
||||
label="Kopyala"
|
||||
:disable="loading || saving || !canCopySelected"
|
||||
@click="copySelectedToSelected"
|
||||
/>
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="save"
|
||||
label="Secilenleri Kaydet"
|
||||
:loading="saving"
|
||||
:disable="loading || saving || !canSaveSelected"
|
||||
@click="saveSelected"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Degisenleri Kaydet"
|
||||
:loading="saving"
|
||||
:disable="loading || saving || dirtyCount === 0"
|
||||
@click="saveAll"
|
||||
/>
|
||||
<div class="text-caption text-grey-7 q-pl-sm">
|
||||
Satir: {{ mappings.length }} | Degisen: {{ dirtyCount }} | Kopya: {{ copySelectedCount }} | Secili: {{ saveSelectedCount }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body-cell-copy_select="props">
|
||||
<q-td :props="props" style="width: 54px">
|
||||
<div class="row items-center no-wrap">
|
||||
<q-checkbox
|
||||
:model-value="isCopySelected(props.row.__key)"
|
||||
@update:model-value="(v) => toggleCopySelected(props.row.__key, v)"
|
||||
/>
|
||||
<q-badge
|
||||
v-if="copyRoleLabel(props.row.__key)"
|
||||
color="grey-8"
|
||||
class="q-ml-xs"
|
||||
style="max-width: 110px"
|
||||
>
|
||||
{{ copyRoleLabel(props.row.__key) }}
|
||||
<q-tooltip anchor="center left" self="center right">
|
||||
{{ copyRoleLabel(props.row.__key) }}
|
||||
</q-tooltip>
|
||||
</q-badge>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-save_select="props">
|
||||
<q-td :props="props" style="width: 54px">
|
||||
<q-checkbox
|
||||
:model-value="isSaveSelected(props.row.__key)"
|
||||
@update:model-value="(v) => toggleSaveSelected(props.row.__key, v)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-parcaBolumAdi="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
:model-value="bolumByKey[props.row.__key] || []"
|
||||
dense
|
||||
filled
|
||||
multiple
|
||||
use-chips
|
||||
clearable
|
||||
use-input
|
||||
input-debounce="0"
|
||||
:options="mtBolumOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
map-options
|
||||
class="pcmm-multi-select"
|
||||
behavior="menu"
|
||||
@filter="onFilterMTBolum"
|
||||
@update:model-value="(val) => { updateBolumSelection(props.row.__key, val); markDirty(props.row) }"
|
||||
style="min-width: 260px"
|
||||
>
|
||||
<template #before-options>
|
||||
<q-item clickable @click="selectAllMTBolum(props.row.__key)">
|
||||
<q-item-section>Tumunu Sec</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="clearMTBolum(props.row.__key)">
|
||||
<q-item-section>Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
|
||||
<template #selected-item="scope">
|
||||
<q-chip
|
||||
class="q-mr-xs"
|
||||
dense
|
||||
removable
|
||||
@remove="scope.removeAtIndex(scope.index)"
|
||||
>
|
||||
{{ scope.opt.label }}
|
||||
</q-chip>
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="scope.selected"
|
||||
tabindex="-1"
|
||||
@update:model-value="() => scope.toggleOption(scope.opt)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-nHammaddeTurleri="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
:model-value="hammaddeByKey[props.row.__key] || []"
|
||||
dense
|
||||
filled
|
||||
multiple
|
||||
use-chips
|
||||
clearable
|
||||
use-input
|
||||
input-debounce="0"
|
||||
:options="hammaddeOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
emit-value
|
||||
map-options
|
||||
class="pcmm-multi-select"
|
||||
behavior="menu"
|
||||
@filter="onFilterHammadde"
|
||||
@update:model-value="(val) => { updateHammaddeSelection(props.row.__key, val); markDirty(props.row) }"
|
||||
style="min-width: 320px"
|
||||
>
|
||||
<template #before-options>
|
||||
<q-item clickable @click="selectAllHammadde(props.row.__key)">
|
||||
<q-item-section>Tumunu Sec</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="clearHammadde(props.row.__key)">
|
||||
<q-item-section>Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
|
||||
<template #selected-item="scope">
|
||||
<q-chip
|
||||
class="q-mr-xs"
|
||||
dense
|
||||
removable
|
||||
@remove="scope.removeAtIndex(scope.index)"
|
||||
>
|
||||
{{ scope.opt.label }}
|
||||
</q-chip>
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="scope.selected"
|
||||
tabindex="-1"
|
||||
@update:model-value="() => scope.toggleOption(scope.opt)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
<div class="text-negative text-subtitle1">
|
||||
Bu module erisim yetkiniz yok.
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { get, post, del, extractApiErrorDetail } from 'src/services/api'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { slog } from 'src/utils/slog'
|
||||
|
||||
const $q = useQuasar()
|
||||
const { canRead } = usePermission()
|
||||
const canReadOrder = canRead('order')
|
||||
|
||||
const traceId = `pcd-mtbolum-map-${crypto?.randomUUID?.() || String(Date.now())}`
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
const mappings = ref([])
|
||||
const copySelectedKeys = ref([]) // ordered
|
||||
const saveSelectedKeyMap = ref({}) // key -> true
|
||||
|
||||
const filters = ref({
|
||||
search: ''
|
||||
})
|
||||
|
||||
const mtBolumOptions = ref([])
|
||||
const hammaddeOptions = ref([])
|
||||
const mtBolumLoading = ref(false)
|
||||
const hammaddeLoading = ref(false)
|
||||
|
||||
const bolumByKey = ref({})
|
||||
const hammaddeByKey = ref({})
|
||||
|
||||
const columns = [
|
||||
{ name: 'copy_select', label: 'Kopya', field: 'copy_select', align: 'center' },
|
||||
{ name: 'save_select', label: 'Sec', field: 'save_select', align: 'center' },
|
||||
{ name: 'urunIlkGrubu', label: 'Urun Ilk Grubu', field: 'urunIlkGrubu', align: 'left', sortable: true },
|
||||
{ name: 'urunAnaGrubu', label: 'Urun Ana Grubu', field: 'urunAnaGrubu', align: 'left', sortable: true },
|
||||
{ name: 'urunAltGrubu', label: 'Urun Alt Grubu', field: 'urunAltGrubu', align: 'left', sortable: true },
|
||||
{ name: 'parcaBolumAdi', label: 'Parca Bolum', field: row => (Array.isArray(row?.nUrtMTBolumIDs) ? row.nUrtMTBolumIDs.join(', ') : ''), align: 'left' },
|
||||
{ name: 'nHammaddeTurleri', label: 'Hammadde Turleri', field: row => (Array.isArray(row?.nHammaddeTurleri) ? row.nHammaddeTurleri.join(', ') : ''), align: 'left' }
|
||||
]
|
||||
|
||||
const dirtyMap = ref({})
|
||||
const dirtyCount = computed(() => Object.keys(dirtyMap.value || {}).length)
|
||||
const copySelectedCount = computed(() => (Array.isArray(copySelectedKeys.value) ? copySelectedKeys.value.length : 0))
|
||||
const canCopySelected = computed(() => copySelectedCount.value >= 2)
|
||||
const saveSelectedCount = computed(() => Object.keys(saveSelectedKeyMap.value || {}).length)
|
||||
const canSaveSelected = computed(() => saveSelectedCount.value > 0)
|
||||
|
||||
function markDirty (row) {
|
||||
const key = String(row?.__key || '').trim()
|
||||
if (!key) return
|
||||
dirtyMap.value = { ...(dirtyMap.value || {}), [key]: true }
|
||||
}
|
||||
|
||||
function clearDirty () {
|
||||
dirtyMap.value = {}
|
||||
}
|
||||
|
||||
function isCopySelected (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return false
|
||||
return (Array.isArray(copySelectedKeys.value) ? copySelectedKeys.value : []).includes(key)
|
||||
}
|
||||
|
||||
function toggleCopySelected (rowKey, enabled) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return
|
||||
const current = Array.isArray(copySelectedKeys.value) ? [...copySelectedKeys.value] : []
|
||||
const idx = current.indexOf(key)
|
||||
if (enabled) {
|
||||
if (idx === -1) current.push(key)
|
||||
} else {
|
||||
if (idx >= 0) current.splice(idx, 1)
|
||||
}
|
||||
copySelectedKeys.value = current
|
||||
}
|
||||
|
||||
function copyRoleLabel (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return ''
|
||||
const keys = Array.isArray(copySelectedKeys.value) ? copySelectedKeys.value : []
|
||||
const idx = keys.indexOf(key)
|
||||
if (idx === 0) return 'Kopyalanacak'
|
||||
if (idx > 0) return 'Yapistirilacak'
|
||||
return ''
|
||||
}
|
||||
|
||||
function isSaveSelected (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return false
|
||||
return !!saveSelectedKeyMap.value?.[key]
|
||||
}
|
||||
|
||||
function toggleSaveSelected (rowKey, enabled) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return
|
||||
const next = { ...(saveSelectedKeyMap.value || {}) }
|
||||
if (enabled) next[key] = true
|
||||
else delete next[key]
|
||||
saveSelectedKeyMap.value = next
|
||||
}
|
||||
|
||||
function copySelectedToSelected () {
|
||||
const keys = Array.isArray(copySelectedKeys.value) ? copySelectedKeys.value : []
|
||||
if (keys.length < 2) return
|
||||
const srcKey = String(keys[0] || '').trim()
|
||||
if (!srcKey) return
|
||||
|
||||
const srcBolum = bolumByKey.value?.[srcKey] || []
|
||||
const srcHam = hammaddeByKey.value?.[srcKey] || []
|
||||
|
||||
for (let i = 1; i < keys.length; i++) {
|
||||
const key = String(keys[i] || '').trim()
|
||||
if (!key) continue
|
||||
updateBolumSelection(key, srcBolum)
|
||||
updateHammaddeSelection(key, srcHam)
|
||||
const row = (Array.isArray(mappings.value) ? mappings.value : []).find(r => String(r?.__key || '') === key)
|
||||
if (row) markDirty(row)
|
||||
}
|
||||
|
||||
$q.notify({ type: 'positive', message: 'Kopyala / Yapistir tamamlandi' })
|
||||
}
|
||||
|
||||
async function saveSelected () {
|
||||
const keys = Object.keys(saveSelectedKeyMap.value || {})
|
||||
await saveKeys(keys)
|
||||
}
|
||||
|
||||
function selectAllMTBolum (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return
|
||||
const all = (Array.isArray(mtBolumOptions.value) ? mtBolumOptions.value : [])
|
||||
.map(o => Number(o?.value))
|
||||
.filter(n => Number.isFinite(n) && n > 0)
|
||||
updateBolumSelection(key, all)
|
||||
const row = (Array.isArray(mappings.value) ? mappings.value : []).find(r => String(r?.__key || '') === key)
|
||||
if (row) markDirty(row)
|
||||
}
|
||||
|
||||
function clearMTBolum (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return
|
||||
updateBolumSelection(key, [])
|
||||
const row = (Array.isArray(mappings.value) ? mappings.value : []).find(r => String(r?.__key || '') === key)
|
||||
if (row) markDirty(row)
|
||||
}
|
||||
|
||||
function selectAllHammadde (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return
|
||||
const all = (Array.isArray(hammaddeOptions.value) ? hammaddeOptions.value : [])
|
||||
.map(o => Number(o?.value))
|
||||
.filter(n => Number.isFinite(n) && n > 0)
|
||||
updateHammaddeSelection(key, all)
|
||||
const row = (Array.isArray(mappings.value) ? mappings.value : []).find(r => String(r?.__key || '') === key)
|
||||
if (row) markDirty(row)
|
||||
}
|
||||
|
||||
function clearHammadde (rowKey) {
|
||||
const key = String(rowKey || '').trim()
|
||||
if (!key) return
|
||||
updateHammaddeSelection(key, [])
|
||||
const row = (Array.isArray(mappings.value) ? mappings.value : []).find(r => String(r?.__key || '') === key)
|
||||
if (row) markDirty(row)
|
||||
}
|
||||
|
||||
function normalizeIntList (list) {
|
||||
const arr = Array.isArray(list) ? list : []
|
||||
const nums = arr
|
||||
.map(x => Number(String(x).trim()))
|
||||
.filter(n => Number.isFinite(n) && n > 0)
|
||||
return Array.from(new Set(nums))
|
||||
}
|
||||
|
||||
function initEditableStateFromRows (rows) {
|
||||
const bolum = {}
|
||||
const ham = {}
|
||||
;(Array.isArray(rows) ? rows : []).forEach(r => {
|
||||
const key = String(r?.__key || '').trim()
|
||||
if (!key) return
|
||||
bolum[key] = normalizeIntList(r?.nUrtMTBolumIDs || [])
|
||||
ham[key] = normalizeIntList(r?.nHammaddeTurleri || [])
|
||||
})
|
||||
bolumByKey.value = bolum
|
||||
hammaddeByKey.value = ham
|
||||
}
|
||||
|
||||
function updateBolumSelection (key, newValue) {
|
||||
const k = String(key || '').trim()
|
||||
if (!k) return
|
||||
bolumByKey.value = {
|
||||
...(bolumByKey.value || {}),
|
||||
[k]: normalizeIntList(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
function updateHammaddeSelection (key, newValue) {
|
||||
const k = String(key || '').trim()
|
||||
if (!k) return
|
||||
hammaddeByKey.value = {
|
||||
...(hammaddeByKey.value || {}),
|
||||
[k]: normalizeIntList(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// label resolution now handled by options' `label` field + selected-item slot (see UserDetail.vue "Piyasalar").
|
||||
async function fetchMappings () {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await get('/pricing/production-product-costing/maliyet-parca-eslestirme', {
|
||||
trace_id: traceId,
|
||||
only_active: 1
|
||||
})
|
||||
return Array.isArray(data) ? data : []
|
||||
} catch (e) {
|
||||
const detail = await extractApiErrorDetail(e)
|
||||
$q.notify({ type: 'negative', message: detail || 'Liste okunamadi' })
|
||||
return []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSheet () {
|
||||
loading.value = true
|
||||
try {
|
||||
// Search changes should not carry over row-level selections from a previous sheet.
|
||||
copySelectedKeys.value = []
|
||||
saveSelectedKeyMap.value = {}
|
||||
|
||||
const [combos, existing] = await Promise.all([
|
||||
get('/pricing/production-product-costing/options/urun-ana-alt-combos', {
|
||||
trace_id: traceId,
|
||||
search: String(filters.value.search || '').trim(),
|
||||
limit: 5000
|
||||
}),
|
||||
fetchMappings()
|
||||
])
|
||||
|
||||
const existingByKey = new Map()
|
||||
;(Array.isArray(existing) ? existing : []).forEach(x => {
|
||||
const k = `${String(x?.urunIlkGrubu || '').trim()}|${String(x?.urunAltGrubu || '').trim()}|${String(x?.urunAnaGrubu || '').trim()}`
|
||||
const prev = existingByKey.get(k)
|
||||
const nextList = Array.isArray(prev) ? prev : (prev ? [prev] : [])
|
||||
nextList.push(x)
|
||||
existingByKey.set(k, nextList)
|
||||
})
|
||||
|
||||
const rows = (Array.isArray(combos) ? combos : []).map((c, idx) => {
|
||||
const ilk = String(c?.urunIlkGrubu || '').trim()
|
||||
const ana = String(c?.urunAnaGrubu || '').trim()
|
||||
const alt = String(c?.urunAltGrubu || '').trim()
|
||||
const k = `${ilk}|${alt}|${ana}`
|
||||
const hitList = existingByKey.get(k)
|
||||
const list = Array.isArray(hitList) ? hitList : []
|
||||
const mtIds = list
|
||||
.map(x => String(x?.nUrtMTBolumID || '').trim())
|
||||
.filter(Boolean)
|
||||
const uniqMt = Array.from(new Set(mtIds))
|
||||
|
||||
// If multiple mappings exist, we merge hammadde types (union) for editing convenience.
|
||||
const hSet = new Set()
|
||||
list.forEach(x => {
|
||||
if (Array.isArray(x?.nHammaddeTurleri)) {
|
||||
x.nHammaddeTurleri.forEach(v => {
|
||||
v = String(v || '').trim()
|
||||
if (v) hSet.add(v)
|
||||
})
|
||||
}
|
||||
})
|
||||
return {
|
||||
__key: k,
|
||||
__rowIndex: idx,
|
||||
__existingIDs: list.map(x => Number(x?.id || 0)).filter(n => n > 0),
|
||||
urunIlkGrubu: ilk,
|
||||
urunAnaGrubu: ana,
|
||||
urunAltGrubu: alt,
|
||||
nUrtMTBolumIDs: uniqMt,
|
||||
nHammaddeTurleri: Array.from(hSet)
|
||||
}
|
||||
})
|
||||
mappings.value = rows
|
||||
initEditableStateFromRows(rows)
|
||||
clearDirty()
|
||||
} catch (e) {
|
||||
const detail = await extractApiErrorDetail(e)
|
||||
$q.notify({ type: 'negative', message: detail || 'Carsaf yuklenemedi' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearchCleared () {
|
||||
filters.value.search = ''
|
||||
copySelectedKeys.value = []
|
||||
saveSelectedKeyMap.value = {}
|
||||
clearDirty()
|
||||
fetchSheet()
|
||||
}
|
||||
|
||||
async function refreshAll () {
|
||||
await Promise.all([
|
||||
fetchMTBolumOptions(''),
|
||||
fetchHammaddeOptions('')
|
||||
])
|
||||
await fetchSheet()
|
||||
}
|
||||
|
||||
async function fetchMTBolumOptions (search) {
|
||||
mtBolumLoading.value = true
|
||||
try {
|
||||
const data = await get('/pricing/production-product-costing/options/mtbolum', {
|
||||
trace_id: traceId,
|
||||
search: search || '',
|
||||
limit: 200
|
||||
})
|
||||
mtBolumOptions.value = Array.isArray(data)
|
||||
? data.map(x => ({
|
||||
value: Number(x?.value ?? x?.nUrtMTBolumID ?? x?.id ?? 0),
|
||||
label: (() => {
|
||||
const v = Number(x?.value ?? x?.nUrtMTBolumID ?? x?.id ?? 0)
|
||||
const name = String(x?.label || x?.sAdi || '').trim()
|
||||
if (Number.isFinite(v) && v > 0 && name) return `${v} - ${name}`
|
||||
if (Number.isFinite(v) && v > 0) return String(v)
|
||||
return name
|
||||
})()
|
||||
}))
|
||||
.filter(x => Number.isFinite(x.value) && x.value > 0)
|
||||
.sort((a, b) => a.value - b.value)
|
||||
: []
|
||||
} finally {
|
||||
mtBolumLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHammaddeOptions (search) {
|
||||
hammaddeLoading.value = true
|
||||
try {
|
||||
const data = await get('/pricing/production-product-costing/detail-editor-options', {
|
||||
trace_id: traceId,
|
||||
kind: 'hammadde',
|
||||
search: search || '',
|
||||
limit: 200
|
||||
})
|
||||
hammaddeOptions.value = Array.isArray(data)
|
||||
? data.map(x => ({
|
||||
value: Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim()),
|
||||
label: (() => {
|
||||
const v = Number(String(x?.nHammaddeTuruNo ?? x?.value ?? '').trim())
|
||||
const name = String(x?.sHammaddeTuruAdi || x?.label || '').trim()
|
||||
if (Number.isFinite(v) && v > 0 && name) return `${v} - ${name}`
|
||||
if (Number.isFinite(v) && v > 0) return String(v)
|
||||
return name
|
||||
})()
|
||||
}))
|
||||
.filter(x => Number.isFinite(x.value) && x.value > 0)
|
||||
.sort((a, b) => a.value - b.value)
|
||||
: []
|
||||
} finally {
|
||||
hammaddeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onFilterMTBolum (val, update) {
|
||||
update(async () => {
|
||||
await fetchMTBolumOptions(val)
|
||||
})
|
||||
}
|
||||
|
||||
function onFilterHammadde (val, update) {
|
||||
update(async () => {
|
||||
await fetchHammaddeOptions(val)
|
||||
})
|
||||
}
|
||||
|
||||
async function saveAll () {
|
||||
const dirtyKeys = Object.keys(dirtyMap.value || {})
|
||||
await saveKeys(dirtyKeys)
|
||||
}
|
||||
|
||||
async function saveKeys (keys) {
|
||||
const list = Array.isArray(keys) ? keys.map(k => String(k || '').trim()).filter(Boolean) : []
|
||||
if (list.length === 0) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const rowsToSave = mappings.value.filter(r => list.includes(String(r.__key || '').trim()))
|
||||
for (const row of rowsToSave) {
|
||||
const key = String(row.__key || '').trim()
|
||||
const mtIds = normalizeIntList(bolumByKey.value?.[key] || [])
|
||||
const hList = normalizeIntList(hammaddeByKey.value?.[key] || [])
|
||||
|
||||
// If hammadde cleared OR no bölüm selected: delete all existing mappings for this combo
|
||||
const existingIDs = Array.isArray(row.__existingIDs) ? row.__existingIDs : []
|
||||
if (existingIDs.length > 0 && (mtIds.length === 0 || hList.length === 0)) {
|
||||
for (const id of existingIDs) {
|
||||
await del('/pricing/production-product-costing/maliyet-parca-eslestirme', { trace_id: traceId, id })
|
||||
}
|
||||
row.__existingIDs = []
|
||||
bolumByKey.value = { ...(bolumByKey.value || {}), [key]: [] }
|
||||
hammaddeByKey.value = { ...(hammaddeByKey.value || {}), [key]: [] }
|
||||
continue
|
||||
}
|
||||
|
||||
if (mtIds.length > 0 && hList.length > 0) {
|
||||
const existing = await fetchMappings()
|
||||
const ilk = String(row.urunIlkGrubu || '').trim()
|
||||
const ana = String(row.urunAnaGrubu || '').trim()
|
||||
const alt = String(row.urunAltGrubu || '').trim()
|
||||
const existingForCombo = (Array.isArray(existing) ? existing : []).filter(x =>
|
||||
String(x?.urunIlkGrubu || '').trim() === ilk &&
|
||||
String(x?.urunAnaGrubu || '').trim() === ana &&
|
||||
String(x?.urunAltGrubu || '').trim() === alt
|
||||
)
|
||||
|
||||
const selectedSet = new Set(mtIds.map(String))
|
||||
for (const ex of existingForCombo) {
|
||||
const exMt = String(ex?.nUrtMTBolumID || '').trim()
|
||||
if (exMt && !selectedSet.has(exMt) && Number(ex?.id || 0) > 0) {
|
||||
await del('/pricing/production-product-costing/maliyet-parca-eslestirme', { trace_id: traceId, id: Number(ex.id) })
|
||||
}
|
||||
}
|
||||
|
||||
const idsAfter = []
|
||||
for (const mtId of mtIds) {
|
||||
const payload = {
|
||||
urunIlkGrubu: ilk,
|
||||
urunAnaGrubu: ana,
|
||||
urunAltGrubu: alt,
|
||||
nUrtMTBolumID: mtId,
|
||||
nHammaddeTurleri: hList,
|
||||
bAktif: true
|
||||
}
|
||||
const resp = await post('/pricing/production-product-costing/maliyet-parca-eslestirme/upsert', payload, { trace_id: traceId })
|
||||
if (resp?.id) idsAfter.push(Number(resp.id))
|
||||
}
|
||||
row.__existingIDs = idsAfter
|
||||
}
|
||||
}
|
||||
|
||||
$q.notify({ type: 'positive', message: 'Degisiklikler kaydedildi' })
|
||||
clearDirty()
|
||||
// after saving, clear save selection to avoid accidental re-save
|
||||
saveSelectedKeyMap.value = {}
|
||||
await refreshAll()
|
||||
} catch (e) {
|
||||
const detail = await extractApiErrorDetail(e)
|
||||
$q.notify({ type: 'negative', message: detail || 'Kaydetme basarisiz' })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
fetchMTBolumOptions(''),
|
||||
fetchHammaddeOptions(''),
|
||||
fetchSheet()
|
||||
])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pcmm-page {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.pcmm-header {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pcmm-form {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pcmm-table {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Allow multi-select chips to wrap and grow vertically (PowerBI-like) */
|
||||
.pcmm-table :deep(.pcmm-multi-select .q-field__control) {
|
||||
height: auto !important;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.pcmm-table :deep(.pcmm-multi-select .q-field__native) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.pcmm-table :deep(.pcmm-multi-select .q-select__chips) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pcmm-table :deep(.pcmm-multi-select .q-chip) {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
516
ui/src/pages/ProductionProductCostingNoCost.vue
Normal file
516
ui/src/pages/ProductionProductCostingNoCost.vue
Normal file
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<q-page v-if="canReadOrder" class="npc-page">
|
||||
<div class="ol-filter-bar npc-filter-bar">
|
||||
<div class="npc-filter-row">
|
||||
<q-input
|
||||
v-model="filters.search"
|
||||
class="npc-filter-input npc-search"
|
||||
dense
|
||||
filled
|
||||
clearable
|
||||
debounce="300"
|
||||
label="Arama (Model Kodu / Firma / Veren)"
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="filters.fromDate"
|
||||
class="npc-filter-input"
|
||||
dense
|
||||
filled
|
||||
type="date"
|
||||
label="Baslangic Tarihi"
|
||||
/>
|
||||
|
||||
<div class="ol-filter-actions npc-filter-actions">
|
||||
<q-btn
|
||||
label="Temizle"
|
||||
icon="clear"
|
||||
color="grey-7"
|
||||
flat
|
||||
:disable="loading"
|
||||
@click="clearFilters"
|
||||
/>
|
||||
<q-btn
|
||||
label="Kolon Filtreleri"
|
||||
icon="filter_alt_off"
|
||||
color="grey-7"
|
||||
flat
|
||||
:disable="loading"
|
||||
@click="clearAllColumnFilters"
|
||||
/>
|
||||
<q-btn
|
||||
label="Yenile"
|
||||
icon="refresh"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="fetchRows"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
title="Maliyeti Olmayan Urunler"
|
||||
class="ol-table npc-table"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
separator="cell"
|
||||
row-key="__rowKey"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
no-data-label="Kayit bulunamadi"
|
||||
:rows-per-page-options="[0]"
|
||||
hide-bottom
|
||||
>
|
||||
<template #header-cell="props">
|
||||
<q-th :props="props">
|
||||
<div class="npc-header-cell">
|
||||
<div class="npc-head-wrap-3">{{ props.col.label }}</div>
|
||||
<q-btn
|
||||
v-if="props.col.name !== 'open'"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
icon="filter_alt"
|
||||
:color="isColumnFilterActive(props.col.name) ? 'primary' : 'grey-6'"
|
||||
>
|
||||
<q-menu class="npc-filter-menu" fit>
|
||||
<div class="npc-filter-menu-content">
|
||||
<div class="text-caption text-weight-bold q-mb-sm">{{ props.col.label }}</div>
|
||||
<q-input
|
||||
v-model="getColumnFilter(props.col.name).text"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Icerir"
|
||||
/>
|
||||
<q-select
|
||||
v-model="getColumnFilter(props.col.name).selected"
|
||||
class="q-mt-sm"
|
||||
dense
|
||||
outlined
|
||||
multiple
|
||||
use-chips
|
||||
use-input
|
||||
emit-value
|
||||
map-options
|
||||
:options="getColumnDistinctOptions(props.col.name)"
|
||||
label="Deger Sec"
|
||||
/>
|
||||
<div class="row justify-end q-gutter-sm q-mt-sm">
|
||||
<q-btn dense flat color="grey-7" label="Temizle" @click="clearColumnFilter(props.col.name)" />
|
||||
</div>
|
||||
</div>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-th>
|
||||
</template>
|
||||
|
||||
<template #body-cell="props">
|
||||
<q-td v-if="props.col.name === 'open'" :props="props" class="text-center">
|
||||
<q-btn
|
||||
icon="open_in_new"
|
||||
color="primary"
|
||||
flat
|
||||
round
|
||||
dense
|
||||
@click="openRow(props.row)"
|
||||
>
|
||||
<q-tooltip>Ac</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-else :props="props" class="npc-wrap-col">
|
||||
<div class="npc-wrap-3">{{ props.value }}</div>
|
||||
<q-tooltip v-if="props.value">{{ props.value }}</q-tooltip>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
<q-banner v-if="error" class="bg-red text-white q-mt-sm">
|
||||
Hata: {{ error }}
|
||||
</q-banner>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
<div class="text-negative text-subtitle1">
|
||||
Bu module erisim yetkiniz yok.
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { get, extractApiErrorDetail } from 'src/services/api'
|
||||
import { createTraceId, slog } from 'src/utils/slog'
|
||||
|
||||
const { canRead } = usePermission()
|
||||
const canReadOrder = canRead('order')
|
||||
const $q = useQuasar()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const allRows = ref([])
|
||||
|
||||
const filters = reactive({
|
||||
search: '',
|
||||
fromDate: '2025-06-01'
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ name: 'open', label: '', field: 'open', align: 'center', sortable: false, style: 'width:3%', headerStyle: 'width:3%' },
|
||||
{
|
||||
name: 'UretimSekli',
|
||||
label: 'Uretim Sekli',
|
||||
field: 'UretimSekli',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
classes: 'npc-wrap-col',
|
||||
headerClasses: 'npc-wrap-col',
|
||||
style: 'width:12%',
|
||||
headerStyle: 'width:12%'
|
||||
},
|
||||
{ name: 'nUrtSiparisNo', label: 'Uretim Siparis No', field: 'nUrtSiparisNo', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{
|
||||
name: 'dteIslemTarihi',
|
||||
label: 'Islem Tarihi',
|
||||
field: 'dteIslemTarihi',
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
format: val => formatDateTR(val),
|
||||
style: 'width:7%',
|
||||
headerStyle: 'width:7%'
|
||||
},
|
||||
{
|
||||
name: 'FirmaKodu',
|
||||
label: 'Firma Kodu',
|
||||
field: 'FirmaKodu',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
classes: 'npc-wrap-col',
|
||||
headerClasses: 'npc-wrap-col',
|
||||
style: 'width:8%',
|
||||
headerStyle: 'width:8%'
|
||||
},
|
||||
{ name: 'FirmaAdi', label: 'Firma Adi', field: 'FirmaAdi', align: 'left', sortable: true, style: 'width:10%', headerStyle: 'width:10%' },
|
||||
{ name: 'SonIsEmriVeren', label: '2.Firma', field: 'SonIsEmriVeren', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' },
|
||||
{
|
||||
name: 'lMMiktar_G',
|
||||
label: 'Miktar (G)',
|
||||
field: 'lMMiktar_G',
|
||||
align: 'right',
|
||||
sortable: true,
|
||||
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
|
||||
style: 'width:7%',
|
||||
headerStyle: 'width:7%'
|
||||
},
|
||||
{
|
||||
name: 'sMModelKodu',
|
||||
label: 'Model Kodu',
|
||||
field: 'sMModelKodu',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
classes: 'npc-wrap-col',
|
||||
headerClasses: 'npc-wrap-col',
|
||||
style: 'width:8%',
|
||||
headerStyle: 'width:8%'
|
||||
},
|
||||
{ name: 'sAdi', label: 'Model Adi', field: 'sAdi', align: 'left', sortable: true, style: 'width:9%', headerStyle: 'width:9%' },
|
||||
{ name: 'sKodu', label: 'Recete Kodu', field: 'sKodu', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'sKullaniciAdi', label: 'Receteyi Acan Kullanici', field: 'sKullaniciAdi', align: 'left', sortable: true, style: 'width:7%', headerStyle: 'width:7%' },
|
||||
{ name: 'sKullaniciAdiGunc', label: 'Receteyi Son Guncelleyen Kullanici', field: 'sKullaniciAdiGunc', align: 'left', sortable: true, style: 'width:6%', headerStyle: 'width:6%' }
|
||||
]
|
||||
|
||||
const columnFilters = reactive({})
|
||||
|
||||
function getColumnFilter (name) {
|
||||
if (!columnFilters[name]) {
|
||||
columnFilters[name] = {
|
||||
text: '',
|
||||
selected: []
|
||||
}
|
||||
}
|
||||
return columnFilters[name]
|
||||
}
|
||||
|
||||
function formatDateTR (value) {
|
||||
const s = String(value || '').trim()
|
||||
if (!s) return ''
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s)
|
||||
if (!m) return s
|
||||
return `${m[3]}.${m[2]}.${m[1]}`
|
||||
}
|
||||
|
||||
const rows = computed(() => {
|
||||
let result = allRows.value
|
||||
|
||||
for (const col of columns) {
|
||||
if (col.name === 'open') continue
|
||||
|
||||
const cf = getColumnFilter(col.name)
|
||||
const text = String(cf.text || '').trim().toLowerCase()
|
||||
const selected = Array.isArray(cf.selected) ? cf.selected : []
|
||||
|
||||
if (!text && selected.length === 0) continue
|
||||
|
||||
result = result.filter((row) => {
|
||||
const value = getColumnComparableValue(row, col.name)
|
||||
const valueLC = value.toLowerCase()
|
||||
|
||||
if (text && !valueLC.includes(text)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (selected.length > 0 && !selected.includes(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
function getColumnComparableValue (row, colName) {
|
||||
if (colName === 'dteIslemTarihi') {
|
||||
return formatDateTR(row?.dteIslemTarihi)
|
||||
}
|
||||
return String(row?.[colName] ?? '').trim()
|
||||
}
|
||||
|
||||
function getColumnDistinctOptions (colName) {
|
||||
const set = new Set()
|
||||
for (const row of allRows.value) {
|
||||
const val = getColumnComparableValue(row, colName)
|
||||
if (val) set.add(val)
|
||||
}
|
||||
return Array.from(set)
|
||||
.sort((a, b) => a.localeCompare(b, 'tr'))
|
||||
.map(v => ({ label: v, value: v }))
|
||||
}
|
||||
|
||||
function isColumnFilterActive (name) {
|
||||
const cf = getColumnFilter(name)
|
||||
return !!String(cf.text || '').trim() || (Array.isArray(cf.selected) && cf.selected.length > 0)
|
||||
}
|
||||
|
||||
function clearColumnFilter (name) {
|
||||
const cf = getColumnFilter(name)
|
||||
cf.text = ''
|
||||
cf.selected = []
|
||||
}
|
||||
|
||||
function clearAllColumnFilters () {
|
||||
for (const col of columns) {
|
||||
if (col.name === 'open') continue
|
||||
clearColumnFilter(col.name)
|
||||
}
|
||||
}
|
||||
|
||||
let searchTimer = null
|
||||
watch(
|
||||
() => filters.search,
|
||||
() => {
|
||||
clearTimeout(searchTimer)
|
||||
searchTimer = setTimeout(() => {
|
||||
fetchRows()
|
||||
}, 400)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => filters.fromDate,
|
||||
() => {
|
||||
fetchRows()
|
||||
}
|
||||
)
|
||||
|
||||
async function fetchRows () {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await get('/pricing/production-product-costing/no-cost-products', {
|
||||
search: filters.search || '',
|
||||
from_date: filters.fromDate || ''
|
||||
})
|
||||
|
||||
const list = Array.isArray(data) ? data : []
|
||||
allRows.value = list.map((x, i) => ({
|
||||
__rowKey: `${x?.sMModelKodu || ''}-${x?.nUrtSiparisNo || 0}-${i}`,
|
||||
...x
|
||||
}))
|
||||
} catch (err) {
|
||||
error.value = await extractApiErrorDetail(err)
|
||||
allRows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilters () {
|
||||
filters.search = ''
|
||||
filters.fromDate = '2025-06-01'
|
||||
clearAllColumnFilters()
|
||||
fetchRows()
|
||||
}
|
||||
|
||||
function openRow (row) {
|
||||
const productCode = String(row?.sMModelKodu || '').trim()
|
||||
const recipeCode = String(row?.sKodu || '').trim()
|
||||
const traceId = createTraceId('pcd-no-cost')
|
||||
if (!productCode || !recipeCode) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Detay acmak icin urun ve recete kodu gerekli.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
slog.info('production-product-costing.no-cost', 'navigate:detail', {
|
||||
trace_id: traceId,
|
||||
product_code: productCode,
|
||||
recipe_code: recipeCode
|
||||
})
|
||||
|
||||
router.push({
|
||||
name: 'production-product-costing-has-cost-detail',
|
||||
query: {
|
||||
detail_source: 'no-cost',
|
||||
urun_kodu: productCode,
|
||||
recete_kodu: recipeCode,
|
||||
trace_id: traceId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!canReadOrder.value) return
|
||||
fetchRows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.npc-page {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.npc-filter-bar {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.npc-filter-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.npc-filter-input {
|
||||
min-width: 118px;
|
||||
width: 136px;
|
||||
flex: 0 0 136px;
|
||||
}
|
||||
|
||||
.npc-search {
|
||||
min-width: 240px;
|
||||
max-width: 420px;
|
||||
flex: 1 1 360px;
|
||||
}
|
||||
|
||||
.npc-filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.npc-filter-menu {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.npc-filter-menu-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.npc-table :deep(.q-table thead th) {
|
||||
font-size: 11px;
|
||||
padding: 3px 4px;
|
||||
white-space: normal !important;
|
||||
vertical-align: top !important;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.npc-table :deep(.q-table tbody td) {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
white-space: normal !important;
|
||||
vertical-align: top !important;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.npc-table :deep(.q-table) {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.npc-wrap-col {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.npc-header-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.npc-head-wrap-3 {
|
||||
min-width: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.npc-wrap-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.npc-filter-row {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.npc-filter-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.npc-filter-input {
|
||||
flex: 1 1 140px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -324,6 +324,42 @@ const routes = [
|
||||
component: () => import('pages/ProductPricing.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
{
|
||||
path: 'pricing/production-product-costing',
|
||||
name: 'production-product-costing',
|
||||
component: () => import('pages/ProductionProductCosting.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
{
|
||||
path: 'pricing/production-product-costing/has-cost',
|
||||
name: 'production-product-costing-has-cost',
|
||||
component: () => import('pages/ProductionProductCostingHasCost.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
{
|
||||
path: 'pricing/production-product-costing/has-cost/history',
|
||||
name: 'production-product-costing-has-cost-history',
|
||||
component: () => import('pages/ProductionProductCostingHasCostHistory.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
{
|
||||
path: 'pricing/production-product-costing/has-cost/detail',
|
||||
name: 'production-product-costing-has-cost-detail',
|
||||
component: () => import('pages/ProductionProductCostingHasCostDetail.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
{
|
||||
path: 'pricing/production-product-costing/no-cost',
|
||||
name: 'production-product-costing-no-cost',
|
||||
component: () => import('pages/ProductionProductCostingNoCost.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
{
|
||||
path: 'pricing/production-product-costing/maliyet-parca-eslestirme',
|
||||
name: 'production-product-costing-maliyet-parca-eslestirme',
|
||||
component: () => import('pages/ProductionProductCostingMTBolumMapping.vue'),
|
||||
meta: { permission: 'order:view' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= PASSWORD ================= */
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from 'axios'
|
||||
import qs from 'qs'
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
import { DEFAULT_LOCALE, normalizeLocale } from 'src/i18n/languages'
|
||||
import { slog } from 'src/utils/slog'
|
||||
|
||||
const rawBaseUrl =
|
||||
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api'
|
||||
@@ -81,6 +82,20 @@ function getRequestLocale() {
|
||||
return normalizeLocale(window.localStorage.getItem(LOCALE_STORAGE_KEY))
|
||||
}
|
||||
|
||||
function extractTraceIdFromConfig(config) {
|
||||
const rawTraceId =
|
||||
config?.params?.trace_id ||
|
||||
config?.headers?.['X-Trace-ID'] ||
|
||||
config?.headers?.['x-trace-id'] ||
|
||||
''
|
||||
|
||||
return String(rawTraceId || '').trim()
|
||||
}
|
||||
|
||||
function shouldStructuredLogRequest(config) {
|
||||
return extractTraceIdFromConfig(config) !== ''
|
||||
}
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const auth = useAuthStore()
|
||||
const url = config.url || ''
|
||||
@@ -92,6 +107,22 @@ api.interceptors.request.use((config) => {
|
||||
config.headers ||= {}
|
||||
config.headers['Accept-Language'] = getRequestLocale()
|
||||
|
||||
const traceId = extractTraceIdFromConfig(config)
|
||||
config.__traceId = traceId
|
||||
config.__startedAt = Date.now()
|
||||
if (traceId) {
|
||||
config.headers['X-Trace-ID'] = traceId
|
||||
}
|
||||
|
||||
if (shouldStructuredLogRequest(config)) {
|
||||
slog.info('production-product-costing.api', 'request:start', {
|
||||
trace_id: traceId,
|
||||
method: String(config.method || 'GET').toUpperCase(),
|
||||
url,
|
||||
params: config.params || {}
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -148,7 +179,19 @@ function clearSessionAndRedirect() {
|
||||
}
|
||||
|
||||
api.interceptors.response.use(
|
||||
r => r,
|
||||
(response) => {
|
||||
const requestConfig = response?.config || {}
|
||||
if (shouldStructuredLogRequest(requestConfig)) {
|
||||
slog.info('production-product-costing.api', 'request:ok', {
|
||||
trace_id: requestConfig.__traceId || extractTraceIdFromConfig(requestConfig),
|
||||
method: String(requestConfig.method || 'GET').toUpperCase(),
|
||||
url: String(requestConfig.url || ''),
|
||||
status: response?.status || 200,
|
||||
duration_ms: Math.max(0, Date.now() - Number(requestConfig.__startedAt || Date.now()))
|
||||
})
|
||||
}
|
||||
return response
|
||||
},
|
||||
async (error) => {
|
||||
const requestConfig = error?.config || {}
|
||||
const status = error?.response?.status
|
||||
@@ -169,6 +212,22 @@ api.interceptors.response.use(
|
||||
console.error(`API ${status || '-'} ${method} ${requestUrl}: ${detail}`)
|
||||
}
|
||||
|
||||
if (shouldStructuredLogRequest(requestConfig)) {
|
||||
const detail = sanitizeApiErrorDetail(
|
||||
await extractApiErrorDetail(error),
|
||||
status
|
||||
)
|
||||
error.parsedMessage ||= detail
|
||||
slog.error('production-product-costing.api', 'request:error', {
|
||||
trace_id: requestConfig.__traceId || extractTraceIdFromConfig(requestConfig),
|
||||
method: String(requestConfig.method || 'GET').toUpperCase(),
|
||||
url: requestUrl,
|
||||
status: status || 0,
|
||||
duration_ms: Math.max(0, Date.now() - Number(requestConfig.__startedAt || Date.now())),
|
||||
detail
|
||||
})
|
||||
}
|
||||
|
||||
const shouldTryRefresh =
|
||||
status === 401 &&
|
||||
!requestConfig._retry &&
|
||||
|
||||
53
ui/src/utils/slog.js
Normal file
53
ui/src/utils/slog.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const isDev =
|
||||
typeof process !== 'undefined' &&
|
||||
Boolean(process.env?.DEV)
|
||||
|
||||
function emit(level, scope, message, data = {}) {
|
||||
const payload = {
|
||||
time: new Date().toISOString(),
|
||||
level,
|
||||
scope,
|
||||
message,
|
||||
...data
|
||||
}
|
||||
|
||||
const text = `[${scope}] ${message}`
|
||||
|
||||
if (level === 'error') {
|
||||
console.error(text, payload)
|
||||
return
|
||||
}
|
||||
if (level === 'warn') {
|
||||
console.warn(text, payload)
|
||||
return
|
||||
}
|
||||
if (level === 'debug' && !isDev) {
|
||||
return
|
||||
}
|
||||
|
||||
console.info(text, payload)
|
||||
}
|
||||
|
||||
export function createTraceId(prefix = 'trace') {
|
||||
const rawId =
|
||||
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
||||
|
||||
return `${prefix}-${rawId}`
|
||||
}
|
||||
|
||||
export const slog = {
|
||||
debug(scope, message, data = {}) {
|
||||
emit('debug', scope, message, data)
|
||||
},
|
||||
info(scope, message, data = {}) {
|
||||
emit('info', scope, message, data)
|
||||
},
|
||||
warn(scope, message, data = {}) {
|
||||
emit('warn', scope, message, data)
|
||||
},
|
||||
error(scope, message, data = {}) {
|
||||
emit('error', scope, message, data)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user