Files
bssapp/ui/src/pages/OrderList.vue
2026-02-20 08:50:05 +03:00

573 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<q-page v-if="canReadOrder" class="ol-page">
<div class="ol-filter-bar">
<div class="ol-filter-row">
<q-input
v-model="store.filters.search"
class="ol-filter-input ol-search"
dense
filled
debounce="300"
clearable
label="Arama (Sipariş No / Cari / Açıklama)"
>
<template #append>
<q-icon name="search" />
</template>
</q-input>
<q-input
v-model="store.filters.CurrAccCode"
class="ol-filter-input"
dense
filled
clearable
label="Cari Kodu"
/>
<q-input
v-model="store.filters.OrderDate"
class="ol-filter-input"
dense
filled
type="date"
label="Sipariş Tarihi"
/>
<div class="ol-filter-actions">
<q-btn
label="Temizle"
icon="clear"
color="grey-7"
flat
:disable="store.loading"
@click="clearFilters"
>
<q-tooltip>Tüm filtreleri temizle</q-tooltip>
</q-btn>
<q-btn
label="Yenile"
color="primary"
icon="refresh"
:loading="store.loading"
@click="store.fetchOrders"
/>
<q-btn
label="Excel'e Aktar"
icon="download"
color="primary"
outline
:disable="store.loading || store.filteredOrders.length === 0"
@click="exportExcel"
/>
</div>
<div class="ol-filter-total">
<div class="ol-total-line">
<span class="ol-total-label">Toplam USD:</span>
<strong class="ol-total-value">
{{ store.totalVisibleUSD.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</strong>
</div>
<div class="ol-total-line">
<span class="ol-total-label">Paketlenen USD:</span>
<strong class="ol-total-value">
{{ store.totalPackedVisibleUSD.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</strong>
</div>
<div class="ol-total-line">
<span class="ol-total-label">Paketlenme %:</span>
<strong class="ol-total-value" :class="packRateClass(store.packedVisibleRatePct)">
{{ store.packedVisibleRatePct.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</strong>
</div>
</div>
</div>
</div>
<q-table
title="Mevcut Siparişler"
class="ol-table"
flat
bordered
dense
separator="cell"
row-key="OrderHeaderID"
:rows="store.filteredOrders"
:columns="columns"
:loading="store.loading"
no-data-label="Sipariş bulunamadı"
:rows-per-page-options="[0]"
hide-bottom
>
<template #body-cell-IsCreditableConfirmed="props">
<q-td :props="props" class="text-center q-gutter-sm">
<q-btn
icon="picture_as_pdf"
color="red"
flat
round
dense
@click="printPDF(props.row)"
>
<q-tooltip>Siparişi PDF olarak </q-tooltip>
</q-btn>
<q-icon
:name="props.row.IsCreditableConfirmed ? 'check_circle' : 'cancel'"
:color="props.row.IsCreditableConfirmed ? 'green' : 'red'"
size="20px"
>
<q-tooltip>
{{ props.row.IsCreditableConfirmed ? 'Onaylı' : 'Onaysız' }}
</q-tooltip>
</q-icon>
</q-td>
</template>
<template #body-cell-HasUretimUrunu="props">
<q-td :props="props" class="text-left">
<span v-if="props.row.HasUretimUrunu" class="text-weight-bold text-negative">
ÜRETİME VERİLECEK ÜRÜNÜ VAR
</span>
</q-td>
</template>
<template #body-cell-OrderDate="props">
<q-td :props="props" class="text-center">
{{ formatDate(props.row.OrderDate) }}
</q-td>
</template>
<template #body-cell-CreditableConfirmedDate="props">
<q-td :props="props" class="text-center">
{{ formatDate(props.row.CreditableConfirmedDate) }}
</q-td>
</template>
<template #body-cell-PackedAmount="props">
<q-td :props="props" class="text-right text-weight-medium">
{{ Number(props.row.PackedAmount || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
{{ props.row.DocCurrencyCode }}
</q-td>
</template>
<template #body-cell-PackedUSD="props">
<q-td :props="props" class="text-right text-weight-medium">
{{ Number(props.row.PackedUSD || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }} USD
</q-td>
</template>
<template #body-cell-PackedRatePct="props">
<q-td
:props="props"
class="text-right text-weight-bold ol-pack-rate-cell"
:class="packRateClass(props.row.PackedRatePct)"
>
{{ Number(props.row.PackedRatePct || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }} %
</q-td>
</template>
<template #body-cell-CurrAccDescription="props">
<q-td :props="props" class="ol-col-cari">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-MusteriTemsilcisi="props">
<q-td :props="props" class="ol-col-short">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-Piyasa="props">
<q-td :props="props" class="ol-col-short">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-Description="props">
<q-td :props="props" class="ol-col-desc">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-select="props">
<q-td :props="props" class="text-center">
<q-btn
icon="open_in_new"
color="primary"
flat
round
dense
@click="selectOrder(props.row)"
>
<q-tooltip>Siparişi </q-tooltip>
</q-btn>
</q-td>
</template>
</q-table>
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
Hata: {{ store.error }}
</q-banner>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu modüle erişim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import { useOrderListStore } from 'src/stores/OrdernewListStore'
import { useAuthStore } from 'src/stores/authStore'
import { usePermission } from 'src/composables/usePermission'
import api, { extractApiErrorDetail } from 'src/services/api'
const { canRead } = usePermission()
const canReadOrder = canRead('order')
const router = useRouter()
const $q = useQuasar()
const store = useOrderListStore()
let searchTimer = null
watch(
() => store.filters.search,
() => {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
store.fetchOrders()
}, 400)
}
)
function exportExcel () {
const auth = useAuthStore()
if (!auth?.token) {
$q.notify({
type: 'negative',
message: 'Oturum bulunamadı',
position: 'top-right'
})
return
}
const params = new URLSearchParams({
search: store.filters.search || '',
CurrAccCode: store.filters.CurrAccCode || '',
OrderDate: store.filters.OrderDate || ''
})
api.get(`/orders/export?${params.toString()}`, {
responseType: 'blob'
})
.then(res => res.data)
.then(blob => {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = 'siparis_listesi.xlsx'
link.click()
})
.catch(() => {
$q.notify({
type: 'negative',
message: 'Excel dosyasi indirilemedi',
position: 'top-right'
})
})
}
function formatDate (s) {
if (!s) return ''
const [y, m, d] = String(s).split('-')
if (!y || !m || !d) return s
return `${d}.${m}.${y}`
}
function packRateClass (value) {
const pct = Number(value || 0)
if (pct <= 50) return 'pack-rate-danger'
if (pct < 100) return 'pack-rate-warn'
return 'pack-rate-ok'
}
const columns = [
{ name: 'select', label: '', field: 'select', align: 'center', sortable: false },
{ name: 'OrderNumber', label: 'Sipariş No', field: 'OrderNumber', align: 'left', sortable: true, style: 'min-width:108px;white-space:nowrap', headerStyle: 'min-width:108px;white-space:nowrap' },
{ name: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'center', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' },
{ name: 'CurrAccCode', label: 'Cari Kod', field: 'CurrAccCode', align: 'left', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' },
{ name: 'CurrAccDescription', label: 'Cari Adı', field: 'CurrAccDescription', align: 'left', sortable: true, classes: 'ol-col-cari', headerClasses: 'ol-col-cari', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
{ name: 'MusteriTemsilcisi', label: 'Temsilci', field: 'MusteriTemsilcisi', align: 'left', sortable: true, classes: 'ol-col-short', headerClasses: 'ol-col-short', style: 'width:88px;max-width:88px', headerStyle: 'width:88px;max-width:88px' },
{ name: 'Piyasa', label: 'Piyasa', field: 'Piyasa', align: 'left', sortable: true, classes: 'ol-col-short', headerClasses: 'ol-col-short', style: 'width:72px;max-width:72px', headerStyle: 'width:72px;max-width:72px' },
{ name: 'CreditableConfirmedDate', label: 'Onay', field: 'CreditableConfirmedDate', align: 'center', sortable: true, style: 'min-width:86px;white-space:nowrap', headerStyle: 'min-width:86px;white-space:nowrap' },
{ name: 'DocCurrencyCode', label: 'PB', field: 'DocCurrencyCode', align: 'center', sortable: true, style: 'min-width:46px;white-space:nowrap', headerStyle: 'min-width:46px;white-space:nowrap' },
{
name: 'TotalAmount',
label: 'Tutar',
field: 'TotalAmount',
align: 'right',
sortable: true,
style: 'min-width:120px;white-space:nowrap',
headerStyle: 'min-width:120px;white-space:nowrap',
format: (val, row) => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' ' + row.DocCurrencyCode
},
{
name: 'TotalAmountUSD',
label: 'Tutar (USD)',
field: 'TotalAmountUSD',
align: 'right',
sortable: true,
style: 'min-width:120px;white-space:nowrap',
headerStyle: 'min-width:120px;white-space:nowrap',
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD'
},
{
name: 'PackedAmount',
label: 'Paketlenen',
field: 'PackedAmount',
align: 'right',
sortable: true,
style: 'min-width:120px;white-space:nowrap',
headerStyle: 'min-width:120px;white-space:nowrap',
format: (val, row) => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' ' + row.DocCurrencyCode
},
{
name: 'PackedUSD',
label: 'Paketlenen (USD)',
field: 'PackedUSD',
align: 'right',
sortable: true,
style: 'min-width:120px;white-space:nowrap',
headerStyle: 'min-width:120px;white-space:nowrap',
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD'
},
{
name: 'PackedRatePct',
label: 'Paketlenme %',
field: 'PackedRatePct',
align: 'right',
sortable: true,
classes: 'ol-pack-rate-cell',
headerClasses: 'ol-pack-rate-cell',
style: 'min-width:96px;white-space:nowrap',
headerStyle: 'min-width:96px;white-space:nowrap',
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' %'
},
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
{ name: 'HasUretimUrunu', label: 'Üretim', field: 'HasUretimUrunu', align: 'left', sortable: true, style: 'min-width:190px;white-space:nowrap', headerStyle: 'min-width:190px;white-space:nowrap' },
{ name: 'Description', label: 'Açıklama', field: 'Description', align: 'left', sortable: false, classes: 'ol-col-desc', headerClasses: 'ol-col-desc', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
{ name: 'pdf', label: 'PDF', field: 'pdf', align: 'center', sortable: false }
]
function selectOrder (row) {
if (!row?.OrderHeaderID) {
$q.notify({ type: 'warning', message: 'OrderHeaderID bulunamadı' })
return
}
router.push({
name: 'order-edit',
params: { orderHeaderID: row.OrderHeaderID },
query: { mode: 'edit' }
})
}
async function printPDF (row) {
if (!row?.OrderHeaderID) return
try {
const res = await api.get(`/order/pdf/${row.OrderHeaderID}`, {
responseType: 'blob'
})
window.open(URL.createObjectURL(res.data), '_blank')
} catch (err) {
const detail = await extractApiErrorDetail(err)
const status = err?.status || err?.response?.status || '-'
console.error(`PDF load error [${status}] /order/pdf/${row.OrderHeaderID}: ${detail}`)
$q.notify({ type: 'negative', message: `PDF yuklenemedi (${status}): ${detail}` })
}
}
function clearFilters () {
store.filters.search = ''
store.filters.CurrAccCode = ''
store.filters.OrderDate = ''
store.fetchOrders()
$q.notify({
type: 'info',
message: 'Filtreler temizlendi',
position: 'top-right'
})
}
onMounted(() => {
store.fetchOrders()
})
</script>
<style scoped>
.ol-page {
padding: 10px;
}
.ol-filter-bar {
margin-bottom: 8px;
}
.ol-filter-row {
display: flex;
flex-wrap: nowrap;
gap: 10px;
align-items: center;
}
.ol-filter-input {
min-width: 118px;
width: 136px;
flex: 0 0 136px;
}
.ol-search {
min-width: 240px;
max-width: 420px;
flex: 1 1 360px;
}
.ol-filter-actions {
display: flex;
gap: 8px;
flex-wrap: nowrap;
flex: 0 0 auto;
}
.ol-filter-total {
margin-left: auto;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
gap: 2px;
min-width: 250px;
line-height: 1.2;
flex: 0 0 auto;
align-self: center;
}
.ol-total-line {
display: flex;
align-items: baseline;
gap: 8px;
white-space: nowrap;
}
.ol-total-label {
font-size: 12px;
color: #4b5563;
}
.ol-total-value {
font-size: 13px;
}
.ol-table :deep(.q-table thead th) {
font-size: 11px;
padding: 4px 6px;
white-space: nowrap;
}
.ol-table :deep(.q-table tbody td) {
font-size: 11px;
padding: 3px 6px;
}
.ol-col-multiline {
display: block;
white-space: normal !important;
word-break: break-word;
line-height: 1.15;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
max-height: 2.35em;
}
.ol-col-cari {
max-width: 160px;
}
.ol-col-short {
max-width: 88px;
}
.ol-col-desc {
max-width: 160px;
}
.ol-pack-rate-cell {
font-weight: 700;
}
.pack-rate-danger {
color: #c62828;
}
.pack-rate-warn {
color: #8a6d00;
}
.pack-rate-ok {
color: #1f7a4f;
}
@media (max-width: 1440px) {
.ol-filter-row {
flex-wrap: wrap;
align-items: flex-start;
}
.ol-filter-actions {
flex-wrap: wrap;
}
.ol-filter-input {
flex: 1 1 140px;
}
.ol-filter-total {
min-width: 100%;
margin-left: 0;
align-items: flex-start;
margin-top: 6px;
}
.ol-total-line {
width: 100%;
justify-content: space-between;
}
}
</style>