Files
bssapp/ui/src/pages/ProductionProductCostingNoCost.vue
2026-05-20 16:24:46 +03:00

530 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 (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 class="npc-missing-count text-caption text-grey-7">
Maliyeti Girilmemis Satir Sayisi: <b>{{ missingCostRowCount }}</b>
</div>
</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
})
const missingCostRowCount = computed(() => {
return Array.isArray(rows.value) ? rows.value.length : 0
})
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-missing-count {
white-space: nowrap;
align-self: center;
margin-left: 6px;
}
.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>