Files
bssapp/ui/src/pages/ProductionProductCostingHasCost.vue
2026-05-06 11:08:31 +03:00

479 lines
13 KiB
Vue

<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>