Merge remote-tracking branch 'origin/master'
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user