530 lines
13 KiB
Vue
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>
|