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