Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -195,6 +195,11 @@ const menuItems = [
|
||||
label: 'Cari Ekstre',
|
||||
to: '/app/statementofaccount',
|
||||
permission: 'finance:view'
|
||||
},
|
||||
{
|
||||
label: 'Cari Bakiye Listesi',
|
||||
to: '/app/customer-balance-list',
|
||||
permission: 'finance:view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
731
ui/src/pages/CustomerBalanceList.vue
Normal file
731
ui/src/pages/CustomerBalanceList.vue
Normal file
@@ -0,0 +1,731 @@
|
||||
<template>
|
||||
<q-page v-if="canReadFinance" class="q-pa-md page-layout">
|
||||
<div class="filter-sticky">
|
||||
<div class="row q-col-gutter-sm q-mb-md">
|
||||
<div class="col-12 col-sm-6 col-md-4">
|
||||
<q-input
|
||||
v-model="store.filters.cariSearch"
|
||||
filled
|
||||
dense
|
||||
label="Cari Kodu / Cari Adı"
|
||||
@keyup.enter="store.applyCariSearch()"
|
||||
>
|
||||
<template #append>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
round
|
||||
icon="search"
|
||||
@click="store.applyCariSearch()"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-2">
|
||||
<q-input
|
||||
v-model="store.filters.selectedDate"
|
||||
label="Tarih"
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="store.filters.selectedDate" mask="YYYY-MM-DD" />
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-2">
|
||||
<q-select
|
||||
v-model="store.filters.cariIlkGrup"
|
||||
:options="store.cariIlkGrupOptions"
|
||||
multiple
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
class="compact-select"
|
||||
label="Cari İlk Grup"
|
||||
:display-value="selectionLabel(store.filters.cariIlkGrup, 'Cari İlk Grup')"
|
||||
>
|
||||
<template #before-options>
|
||||
<q-item clickable dense @click.stop="store.selectAll('cariIlkGrup', store.cariIlkGrupOptions)">
|
||||
<q-item-section>Tümünü Seç</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable dense @click.stop="store.clearAll('cariIlkGrup')">
|
||||
<q-item-section>Tümünü Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox :model-value="scope.selected" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-2">
|
||||
<q-select
|
||||
v-model="store.filters.piyasa"
|
||||
:options="store.piyasaOptions"
|
||||
multiple
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
class="compact-select"
|
||||
label="Piyasa"
|
||||
:display-value="selectionLabel(store.filters.piyasa, 'Piyasa')"
|
||||
>
|
||||
<template #before-options>
|
||||
<q-item clickable dense @click.stop="store.selectAll('piyasa', store.piyasaOptions)">
|
||||
<q-item-section>Tümünü Seç</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable dense @click.stop="store.clearAll('piyasa')">
|
||||
<q-item-section>Tümünü Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox :model-value="scope.selected" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-2">
|
||||
<q-select
|
||||
v-model="store.filters.temsilci"
|
||||
:options="store.temsilciOptions"
|
||||
multiple
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
class="compact-select"
|
||||
label="Temsilci"
|
||||
:display-value="selectionLabel(store.filters.temsilci, 'Temsilci')"
|
||||
>
|
||||
<template #before-options>
|
||||
<q-item clickable dense @click.stop="store.selectAll('temsilci', store.temsilciOptions)">
|
||||
<q-item-section>Tümünü Seç</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable dense @click.stop="store.clearAll('temsilci')">
|
||||
<q-item-section>Tümünü Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox :model-value="scope.selected" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-2">
|
||||
<q-select
|
||||
v-model="store.filters.riskDurumu"
|
||||
:options="store.riskDurumuOptions"
|
||||
multiple
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
class="compact-select"
|
||||
label="Risk Durumu"
|
||||
:display-value="selectionLabel(store.filters.riskDurumu, 'Risk Durumu')"
|
||||
>
|
||||
<template #before-options>
|
||||
<q-item clickable dense @click.stop="store.selectAll('riskDurumu', store.riskDurumuOptions)">
|
||||
<q-item-section>Tümünü Seç</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable dense @click.stop="store.clearAll('riskDurumu')">
|
||||
<q-item-section>Tümünü Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox :model-value="scope.selected" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-2">
|
||||
<q-select
|
||||
v-model="store.filters.islemTipi"
|
||||
:options="islemTipiOptions"
|
||||
multiple
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
class="compact-select"
|
||||
label="İşlem Tipi"
|
||||
:display-value="selectionLabel(store.filters.islemTipi, 'İşlem Tipi')"
|
||||
>
|
||||
<template #before-options>
|
||||
<q-item clickable dense @click.stop="store.selectAll('islemTipi', islemTipiOptions)">
|
||||
<q-item-section>Tümünü Seç</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable dense @click.stop="store.clearAll('islemTipi')">
|
||||
<q-item-section>Tümünü Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox :model-value="scope.selected" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-2">
|
||||
<q-select
|
||||
v-model="store.filters.ulke"
|
||||
:options="store.ulkeOptions"
|
||||
multiple
|
||||
emit-value
|
||||
map-options
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
class="compact-select"
|
||||
label="Ülke (Özellik05)"
|
||||
:display-value="selectionLabel(store.filters.ulke, 'Ülke')"
|
||||
>
|
||||
<template #before-options>
|
||||
<q-item clickable dense @click.stop="store.selectAll('ulke', store.ulkeOptions)">
|
||||
<q-item-section>Tümünü Seç</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable dense @click.stop="store.clearAll('ulke')">
|
||||
<q-item-section>Tümünü Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox :model-value="scope.selected" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-sm q-mb-md">
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="download"
|
||||
label="Bakiyeleri Getir"
|
||||
:loading="store.loading"
|
||||
@click="store.fetchCustomerBalances()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey-8"
|
||||
icon="restart_alt"
|
||||
label="Sıfırla"
|
||||
@click="onReset"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="store.error" class="bg-red-1 text-negative q-mb-md rounded-borders">
|
||||
{{ store.error }}
|
||||
</q-banner>
|
||||
|
||||
<q-banner v-if="!store.hasFetched && !store.loading" class="bg-blue-1 text-primary q-mb-md rounded-borders">
|
||||
Bakiyeleri Getir Tuşuna Basmadan Sistem Çalışmaz
|
||||
</q-banner>
|
||||
</div>
|
||||
|
||||
<div class="table-area">
|
||||
<div class="sticky-bar row justify-end items-center q-pa-sm bg-grey-1">
|
||||
<q-btn
|
||||
flat
|
||||
color="secondary"
|
||||
icon="list"
|
||||
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları Aç'"
|
||||
@click="toggleAllDetails"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
title="Cari Bakiye Listesi"
|
||||
:rows="store.summaryRows"
|
||||
:columns="summaryColumns"
|
||||
row-key="group_key"
|
||||
:loading="store.loading"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
wrap-cells
|
||||
separator="cell"
|
||||
hide-bottom
|
||||
:rows-per-page-options="[0]"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
:table-style="{ tableLayout: 'fixed', width: '100%' }"
|
||||
class="balance-table"
|
||||
>
|
||||
<template #header="props">
|
||||
<q-tr :props="props" class="header-row">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
<q-tr class="totals-row">
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="`tot-${col.name}`"
|
||||
:class="col.align === 'right' ? 'text-right' : ''"
|
||||
>
|
||||
{{ totalCellValue(col.name) }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template #body="props">
|
||||
<q-tr :props="props" class="sub-header-row">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-btn
|
||||
v-if="col.name === 'expand'"
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
:icon="expanded[props.row.group_key] ? 'expand_less' : 'expand_more'"
|
||||
@click="toggleGroup(props.row.group_key)"
|
||||
/>
|
||||
<span v-else-if="col.name === 'prbr_1_2'" class="text-right block prbr-cell">
|
||||
{{ formatCurrencyMap(props.row.bakiye_1_2_map) }}
|
||||
</span>
|
||||
<span v-else-if="col.name === 'prbr_1_3'" class="text-right block prbr-cell">
|
||||
{{ formatCurrencyMap(props.row.bakiye_1_3_map) }}
|
||||
</span>
|
||||
<span v-else-if="staticMoneyFields.includes(col.name)" class="text-center block">
|
||||
{{ formatAmount(props.row[col.field]) }}
|
||||
</span>
|
||||
<span v-else-if="col.name === 'hesap_alinmayan_gun'" class="text-right block">
|
||||
-
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ props.row[col.field] || '-' }}
|
||||
</span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
|
||||
<q-tr v-if="expanded[props.row.group_key]" class="detail-host-row">
|
||||
<q-td colspan="100%">
|
||||
<div class="detail-wrap">
|
||||
<q-table
|
||||
:rows="store.getDetailsByGroup(props.row.group_key)"
|
||||
:columns="detailColumns"
|
||||
row-key="cari_kodu"
|
||||
dense
|
||||
flat
|
||||
bordered
|
||||
hide-bottom
|
||||
:table-style="{ tableLayout: 'fixed', width: '100%' }"
|
||||
class="detail-table"
|
||||
>
|
||||
<template #body-cell-prbr_1_2="scope">
|
||||
<q-td :props="scope" class="text-right prbr-cell">
|
||||
{{ formatRowPrBr(scope.row, '1_2') }}
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-prbr_1_3="scope">
|
||||
<q-td :props="scope" class="text-right prbr-cell">
|
||||
{{ formatRowPrBr(scope.row, '1_3') }}
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</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, ref } from 'vue'
|
||||
import { useCustomerBalanceListStore } from 'src/stores/customerBalanceListStore'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
|
||||
const store = useCustomerBalanceListStore()
|
||||
const expanded = ref({})
|
||||
const allDetailsOpen = ref(false)
|
||||
|
||||
const { canRead } = usePermission()
|
||||
const canReadFinance = canRead('finance')
|
||||
|
||||
const islemTipiOptions = [
|
||||
{ label: '1_2 Bakiye Pr.Br', value: 'prbr_1_2' },
|
||||
{ label: '1_3 Bakiye Pr.Br', value: 'prbr_1_3' },
|
||||
{ label: '1_2 USD Bakiye', value: 'usd_1_2' },
|
||||
{ label: '1_2 TRY Bakiye', value: 'try_1_2' },
|
||||
{ label: '1_3 USD Bakiye', value: 'usd_1_3' },
|
||||
{ label: '1_3 TRY Bakiye', value: 'try_1_3' }
|
||||
]
|
||||
const staticMoneyFields = ['usd_bakiye_1_2', 'tl_bakiye_1_2', 'usd_bakiye_1_3', 'tl_bakiye_1_3']
|
||||
const metricDefs = {
|
||||
prbr_1_2: { name: 'prbr_1_2', label: '1_2 Bakiye\nPr.Br', field: 'prbr_1_2', align: 'right', sortable: false },
|
||||
prbr_1_3: { name: 'prbr_1_3', label: '1_3 Bakiye\nPr.Br', field: 'prbr_1_3', align: 'right', sortable: false },
|
||||
usd_1_2: { name: 'usd_bakiye_1_2', label: '1_2 USD_BAKIYE', field: 'usd_bakiye_1_2', align: 'center', sortable: true },
|
||||
try_1_2: { name: 'tl_bakiye_1_2', label: '1_2 TRY_BAKIYE', field: 'tl_bakiye_1_2', align: 'center', sortable: true },
|
||||
usd_1_3: { name: 'usd_bakiye_1_3', label: '1_3 USD_BAKIYE', field: 'usd_bakiye_1_3', align: 'center', sortable: true },
|
||||
try_1_3: { name: 'tl_bakiye_1_3', label: '1_3 TRY_BAKIYE', field: 'tl_bakiye_1_3', align: 'center', sortable: true }
|
||||
}
|
||||
|
||||
const selectedMetricKeys = computed(() => {
|
||||
const selected = store.filters.islemTipi || []
|
||||
if (!selected.length) return Object.keys(metricDefs)
|
||||
return selected.filter((k) => k in metricDefs)
|
||||
})
|
||||
|
||||
const summaryColumns = computed(() => ([
|
||||
{ name: 'expand', label: '', field: 'expand', align: 'center', sortable: false },
|
||||
{ name: 'ana_cari_kodu', label: 'Ana Cari Kodu', field: 'ana_cari_kodu', align: 'left', sortable: true },
|
||||
{ name: 'ana_cari_adi', label: 'Ana Cari Detay', field: 'ana_cari_adi', align: 'left', sortable: true },
|
||||
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left', sortable: true },
|
||||
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left', sortable: true },
|
||||
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left', sortable: true },
|
||||
...selectedMetricKeys.value.map(k => metricDefs[k]),
|
||||
{ name: 'hesap_alinmayan_gun', label: 'Hesap Alınmayan Gün', field: 'hesap_alinmayan_gun', align: 'right', sortable: false },
|
||||
{ name: 'kalan_fatura_ortalama_vade_tarihi', label: 'Kalan Fatura Ortalama Vade Tarihi', field: 'kalan_fatura_ortalama_vade_tarihi', align: 'left', sortable: true }
|
||||
]))
|
||||
const liveTotals = computed(() => {
|
||||
return store.filteredRows.reduce((acc, row) => {
|
||||
acc.usd_bakiye_1_2 += Number(row.usd_bakiye_1_2) || 0
|
||||
acc.tl_bakiye_1_2 += Number(row.tl_bakiye_1_2) || 0
|
||||
acc.usd_bakiye_1_3 += Number(row.usd_bakiye_1_3) || 0
|
||||
acc.tl_bakiye_1_3 += Number(row.tl_bakiye_1_3) || 0
|
||||
return acc
|
||||
}, {
|
||||
usd_bakiye_1_2: 0,
|
||||
tl_bakiye_1_2: 0,
|
||||
usd_bakiye_1_3: 0,
|
||||
tl_bakiye_1_3: 0
|
||||
})
|
||||
})
|
||||
|
||||
const detailColumns = computed(() => [
|
||||
{ name: 'cari_kodu', label: 'Cari Kodu', field: 'cari_kodu', align: 'left' },
|
||||
{ name: 'cari_detay', label: 'Cari Detay', field: 'cari_detay', align: 'left' },
|
||||
{ name: 'sirket', label: 'Şirket', field: 'sirket', align: 'left' },
|
||||
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left' },
|
||||
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left' },
|
||||
{ name: 'ozellik03', label: 'Risk Durumu', field: 'ozellik03', align: 'left' },
|
||||
{ name: 'ozellik05', label: 'Ülke', field: 'ozellik05', align: 'left' },
|
||||
{ name: 'ozellik06', label: 'Özellik06', field: 'ozellik06', align: 'left' },
|
||||
{ name: 'ozellik07', label: 'Özellik07', field: 'ozellik07', align: 'left' },
|
||||
{ name: 'cari_doviz', label: 'Döviz', field: 'cari_doviz', align: 'left' },
|
||||
...selectedMetricKeys.value.map(k => metricDefs[k]),
|
||||
{ name: 'hesap_alinmayan_gun', label: 'Hesap Alınmayan Gün', field: 'hesap_alinmayan_gun', align: 'right' },
|
||||
{ name: 'kalan_fatura_ortalama_vade_tarihi', label: 'Kalan Fatura Ortalama Vade Tarihi', field: 'kalan_fatura_ortalama_vade_tarihi', align: 'left' }
|
||||
])
|
||||
|
||||
function onReset () {
|
||||
store.resetFilters()
|
||||
store.applyCariSearch()
|
||||
}
|
||||
|
||||
function toggleGroup (key) {
|
||||
expanded.value[key] = !expanded.value[key]
|
||||
if (!expanded.value[key]) {
|
||||
allDetailsOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
allDetailsOpen.value =
|
||||
store.summaryRows.length > 0 &&
|
||||
store.summaryRows.every(r => expanded.value[r.group_key])
|
||||
}
|
||||
|
||||
function toggleAllDetails () {
|
||||
allDetailsOpen.value = !allDetailsOpen.value
|
||||
|
||||
if (allDetailsOpen.value) {
|
||||
const next = {}
|
||||
for (const row of store.summaryRows) {
|
||||
next[row.group_key] = true
|
||||
}
|
||||
expanded.value = next
|
||||
return
|
||||
}
|
||||
|
||||
expanded.value = {}
|
||||
}
|
||||
|
||||
function formatAmount (value) {
|
||||
const n = Number(value || 0)
|
||||
return new Intl.NumberFormat('tr-TR', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(n)
|
||||
}
|
||||
|
||||
function selectionLabel (arr, label) {
|
||||
const count = Array.isArray(arr) ? arr.length : 0
|
||||
if (count === 0) return `Tümü (${label})`
|
||||
if (count === 1) return '1 seçim'
|
||||
return `${count} seçim`
|
||||
}
|
||||
|
||||
function totalCellValue (colName) {
|
||||
if (colName === 'expand') return 'Toplam'
|
||||
if (colName === 'piyasa') return '-'
|
||||
if (colName === 'temsilci') return '-'
|
||||
if (colName === 'risk_durumu') return '-'
|
||||
if (colName === 'prbr_1_2') return formatCurrencyMap(totalByCurrency('1_2'))
|
||||
if (colName === 'prbr_1_3') return formatCurrencyMap(totalByCurrency('1_3'))
|
||||
if (colName === 'usd_bakiye_1_2') return formatAmount(liveTotals.value.usd_bakiye_1_2)
|
||||
if (colName === 'tl_bakiye_1_2') return formatAmount(liveTotals.value.tl_bakiye_1_2)
|
||||
if (colName === 'usd_bakiye_1_3') return formatAmount(liveTotals.value.usd_bakiye_1_3)
|
||||
if (colName === 'tl_bakiye_1_3') return formatAmount(liveTotals.value.tl_bakiye_1_3)
|
||||
if (colName === 'hesap_alinmayan_gun') return '-'
|
||||
return '-'
|
||||
}
|
||||
|
||||
function totalByCurrency (tip) {
|
||||
const key = tip === '1_2' ? 'bakiye_1_2_map' : 'bakiye_1_3_map'
|
||||
const out = {}
|
||||
|
||||
for (const r of store.summaryRows) {
|
||||
const m = r[key] || {}
|
||||
for (const [curr, val] of Object.entries(m)) {
|
||||
out[curr] = (Number(out[curr]) || 0) + (Number(val) || 0)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
function formatCurrencyMap (mapObj) {
|
||||
const entries = Object.entries(mapObj || {})
|
||||
.filter(([, amount]) => Number(amount) !== 0)
|
||||
.sort((a, b) => a[0].localeCompare(b[0], 'en'))
|
||||
|
||||
if (!entries.length) return '-'
|
||||
return entries
|
||||
.map(([curr, amount]) => `${curr}: ${formatAmount(amount)}`)
|
||||
.join(' | ')
|
||||
}
|
||||
|
||||
function formatRowPrBr (row, tip) {
|
||||
const curr = String(row.cari_doviz || '').trim().toUpperCase() || 'N/A'
|
||||
|
||||
const amount = tip === '1_2'
|
||||
? (Number(row.bakiye_1_2) || 0)
|
||||
: (Number(row.bakiye_1_3) || 0)
|
||||
|
||||
if (amount === 0) return '-'
|
||||
return `${curr} ${formatAmount(amount)}`
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-layout {
|
||||
height: calc(100vh - 110px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: #fff;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.compact-select :deep(.q-field__control) {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.compact-select :deep(.q-field__native),
|
||||
.compact-select :deep(.q-field__input) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.table-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sticky-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.balance-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.balance-table :deep(.q-table__container) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.balance-table :deep(.q-table__top) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 6;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.balance-table :deep(.q-table__middle) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.balance-table :deep(.header-row th) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 8;
|
||||
background: var(--q-primary);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
|
||||
.balance-table :deep(.totals-row th) {
|
||||
position: sticky;
|
||||
top: 38px;
|
||||
z-index: 7;
|
||||
background: var(--q-secondary);
|
||||
color: var(--q-dark);
|
||||
font-weight: 700;
|
||||
font-family: "Roboto", sans-serif;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.detail-table :deep(.q-table__middle) {
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.balance-table :deep(.sub-header-row td) {
|
||||
background: #fff;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.18);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.balance-table :deep(.sub-header-row td:first-child) {
|
||||
border-left: 3px solid var(--q-primary);
|
||||
}
|
||||
|
||||
.balance-table :deep(.detail-host-row td) {
|
||||
background: #f7f7f7;
|
||||
border-bottom: 10px solid #fff;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.detail-wrap {
|
||||
border: 1px solid rgba(0, 0, 0, 0.14);
|
||||
border-left: 4px solid var(--q-secondary);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.balance-table :deep(.header-row th) {
|
||||
white-space: pre-line;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.prbr-cell {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.balance-table :deep(th),
|
||||
.balance-table :deep(td),
|
||||
.detail-table :deep(th),
|
||||
.detail-table :deep(td) {
|
||||
white-space: normal !important;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
padding: 4px 6px !important;
|
||||
}
|
||||
|
||||
.balance-table :deep(.q-table__table),
|
||||
.detail-table :deep(.q-table__table) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.detail-table :deep(.q-table__middle) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -724,6 +724,13 @@
|
||||
:disable="isClosedRow || isViewOnly || !canMutateRows"
|
||||
|
||||
/>
|
||||
<q-btn
|
||||
v-if="canMutateRows"
|
||||
color="secondary"
|
||||
label="Kaydet ve Diğer Renge Geç"
|
||||
@click="onSaveAndNextColor"
|
||||
:disable="isClosedRow || isViewOnly || !canMutateRows"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="isEditing && canMutateRows"
|
||||
color="negative"
|
||||
@@ -2831,6 +2838,76 @@ const onSaveOrUpdateRow = async () => {
|
||||
showEditor.value = false
|
||||
}
|
||||
|
||||
function normalizeColorValue(val) {
|
||||
return String(val || '').trim().toUpperCase()
|
||||
}
|
||||
|
||||
function getNextColorValue() {
|
||||
const options = Array.isArray(renkOptions.value) ? renkOptions.value : []
|
||||
if (!options.length) return null
|
||||
|
||||
const current = normalizeColorValue(form.renk)
|
||||
const idx = options.findIndex(o => normalizeColorValue(o.value) === current)
|
||||
if (idx === -1) return null
|
||||
|
||||
const next = options[idx + 1]
|
||||
return next ? next.value : null
|
||||
}
|
||||
|
||||
const onSaveAndNextColor = async () => {
|
||||
if (!hasRowMutationPermission()) {
|
||||
notifyNoPermission(
|
||||
isEditMode.value
|
||||
? 'Siparis satiri guncelleme yetkiniz yok'
|
||||
: 'Siparis satiri kaydetme yetkiniz yok'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.model) {
|
||||
$q.notify({ type: 'warning', message: 'Model seçiniz' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.renk) {
|
||||
$q.notify({ type: 'warning', message: 'Renk seçiniz' })
|
||||
return
|
||||
}
|
||||
|
||||
const ok = await orderStore.saveOrUpdateRowUnified({
|
||||
form,
|
||||
recalcVat: typeof recalcVat === 'function' ? recalcVat : null,
|
||||
resetEditor: () => {},
|
||||
stockMap,
|
||||
$q
|
||||
})
|
||||
|
||||
if (!ok) return
|
||||
|
||||
// Edit state temizle: renk değişimi combo delete tetiklemesin
|
||||
orderStore.editingKey = null
|
||||
orderStore.selected = null
|
||||
|
||||
const nextColor = getNextColorValue()
|
||||
if (!nextColor) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Son renktesiniz. Lütfen farklı bir renk seçin.',
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
form.renk2 = ''
|
||||
await onColorChange(nextColor)
|
||||
|
||||
$q.notify({
|
||||
type: 'info',
|
||||
message: 'Satır kaydedildi. Bir sonraki renge geçildi.',
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -134,6 +134,13 @@ const routes = [
|
||||
meta: { permission: 'finance:view' }
|
||||
},
|
||||
|
||||
{
|
||||
path: 'customer-balance-list',
|
||||
name: 'customer-balance-list',
|
||||
component: () => import('pages/CustomerBalanceList.vue'),
|
||||
meta: { permission: 'finance:view' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= USERS ================= */
|
||||
|
||||
|
||||
262
ui/src/stores/customerBalanceListStore.js
Normal file
262
ui/src/stores/customerBalanceListStore.js
Normal file
@@ -0,0 +1,262 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
|
||||
state: () => ({
|
||||
filters: {
|
||||
selectedDate: new Date().toISOString().slice(0, 10),
|
||||
cariSearch: '',
|
||||
appliedCariSearch: '',
|
||||
cariIlkGrup: [],
|
||||
piyasa: [],
|
||||
temsilci: [],
|
||||
riskDurumu: [],
|
||||
islemTipi: [],
|
||||
ulke: []
|
||||
},
|
||||
rows: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
hasFetched: false,
|
||||
defaultsInitialized: false
|
||||
}),
|
||||
|
||||
getters: {
|
||||
cariIlkGrupOptions: (state) => uniqueOptions(state.rows, 'cari_ilk_grup'),
|
||||
piyasaOptions: (state) => uniqueOptions(state.rows, 'piyasa'),
|
||||
temsilciOptions: (state) => uniqueOptions(state.rows, 'temsilci'),
|
||||
riskDurumuOptions: (state) => uniqueOptions(state.rows, 'ozellik03'),
|
||||
ulkeOptions: (state) => uniqueOptions(state.rows, 'ozellik05'),
|
||||
|
||||
filteredRows: (state) => {
|
||||
return state.rows.filter((row) => {
|
||||
const cariIlkGrupOk =
|
||||
!state.filters.cariIlkGrup.length ||
|
||||
state.filters.cariIlkGrup.includes(row.cari_ilk_grup)
|
||||
|
||||
const piyasaOk =
|
||||
!state.filters.piyasa.length ||
|
||||
state.filters.piyasa.includes(row.piyasa)
|
||||
|
||||
const temsilciOk =
|
||||
!state.filters.temsilci.length ||
|
||||
state.filters.temsilci.includes(row.temsilci)
|
||||
|
||||
const riskDurumuOk =
|
||||
!state.filters.riskDurumu.length ||
|
||||
state.filters.riskDurumu.includes(row.ozellik03)
|
||||
|
||||
const cariText = normalizeText([
|
||||
row.ana_cari_kodu || '',
|
||||
row.ana_cari_adi || '',
|
||||
row.cari_kodu || '',
|
||||
row.cari_detay || ''
|
||||
].join(' '))
|
||||
const cariSearchNeedle = normalizeText(state.filters.appliedCariSearch || '')
|
||||
const cariSearchOk =
|
||||
!cariSearchNeedle ||
|
||||
cariText.includes(cariSearchNeedle)
|
||||
|
||||
const ulkeOk =
|
||||
!state.filters.ulke.length ||
|
||||
state.filters.ulke.includes(row.ozellik05)
|
||||
|
||||
const islemTipiOk =
|
||||
!state.filters.islemTipi.length ||
|
||||
state.filters.islemTipi.some((t) => {
|
||||
const bak12 = Number(row.bakiye_1_2) || 0
|
||||
const bak13 = Number(row.bakiye_1_3) || 0
|
||||
const usd12 = Number(row.usd_bakiye_1_2) || 0
|
||||
const try12 = Number(row.tl_bakiye_1_2) || 0
|
||||
const usd13 = Number(row.usd_bakiye_1_3) || 0
|
||||
const try13 = Number(row.tl_bakiye_1_3) || 0
|
||||
|
||||
if (t === 'prbr_1_2') return bak12 !== 0
|
||||
if (t === 'prbr_1_3') return bak13 !== 0
|
||||
if (t === 'usd_1_2') return usd12 !== 0
|
||||
if (t === 'try_1_2') return try12 !== 0
|
||||
if (t === 'usd_1_3') return usd13 !== 0
|
||||
if (t === 'try_1_3') return try13 !== 0
|
||||
return false
|
||||
})
|
||||
|
||||
return cariIlkGrupOk && piyasaOk && temsilciOk && riskDurumuOk && cariSearchOk && ulkeOk && islemTipiOk
|
||||
})
|
||||
},
|
||||
|
||||
summaryRows () {
|
||||
const grouped = new Map()
|
||||
|
||||
for (const row of this.filteredRows) {
|
||||
const key = `${row.ana_cari_kodu || ''}||${row.ana_cari_adi || ''}`
|
||||
const current = grouped.get(key) || {
|
||||
group_key: key,
|
||||
ana_cari_kodu: row.ana_cari_kodu || '',
|
||||
ana_cari_adi: row.ana_cari_adi || '',
|
||||
piyasa: '',
|
||||
piyasa_set: new Set(),
|
||||
temsilci: '',
|
||||
temsilci_set: new Set(),
|
||||
risk_durumu: '',
|
||||
risk_set: new Set(),
|
||||
bakiye_1_2_map: {},
|
||||
bakiye_1_3_map: {},
|
||||
usd_bakiye_1_2: 0,
|
||||
tl_bakiye_1_2: 0,
|
||||
usd_bakiye_1_3: 0,
|
||||
tl_bakiye_1_3: 0,
|
||||
kalan_fatura_ortalama_vade_tarihi: ''
|
||||
}
|
||||
|
||||
current.usd_bakiye_1_2 += Number(row.usd_bakiye_1_2) || 0
|
||||
current.tl_bakiye_1_2 += Number(row.tl_bakiye_1_2) || 0
|
||||
current.usd_bakiye_1_3 += Number(row.usd_bakiye_1_3) || 0
|
||||
current.tl_bakiye_1_3 += Number(row.tl_bakiye_1_3) || 0
|
||||
|
||||
const curr = String(row.cari_doviz || '').trim().toUpperCase() || 'N/A'
|
||||
current.bakiye_1_2_map[curr] =
|
||||
(Number(current.bakiye_1_2_map[curr]) || 0) + (Number(row.bakiye_1_2) || 0)
|
||||
current.bakiye_1_3_map[curr] =
|
||||
(Number(current.bakiye_1_3_map[curr]) || 0) + (Number(row.bakiye_1_3) || 0)
|
||||
|
||||
const piyasa = String(row.piyasa || '').trim()
|
||||
if (piyasa) current.piyasa_set.add(piyasa)
|
||||
|
||||
const temsilci = String(row.temsilci || '').trim()
|
||||
if (temsilci) current.temsilci_set.add(temsilci)
|
||||
|
||||
if (
|
||||
!current.kalan_fatura_ortalama_vade_tarihi &&
|
||||
row.kalan_fatura_ortalama_vade_tarihi
|
||||
) {
|
||||
current.kalan_fatura_ortalama_vade_tarihi = row.kalan_fatura_ortalama_vade_tarihi
|
||||
}
|
||||
|
||||
const risk = String(row.ozellik03 || '').trim()
|
||||
if (risk) current.risk_set.add(risk)
|
||||
|
||||
const riskValues = Array.from(current.risk_set)
|
||||
current.risk_durumu =
|
||||
riskValues.length <= 1
|
||||
? (riskValues[0] || '-')
|
||||
: riskValues.join(', ')
|
||||
|
||||
const piyasaValues = Array.from(current.piyasa_set)
|
||||
current.piyasa =
|
||||
piyasaValues.length <= 1
|
||||
? (piyasaValues[0] || '-')
|
||||
: piyasaValues.join(', ')
|
||||
|
||||
const temsilciValues = Array.from(current.temsilci_set)
|
||||
current.temsilci =
|
||||
temsilciValues.length <= 1
|
||||
? (temsilciValues[0] || '-')
|
||||
: temsilciValues.join(', ')
|
||||
|
||||
grouped.set(key, current)
|
||||
}
|
||||
|
||||
return Array.from(grouped.values()).map((r) => {
|
||||
const { risk_set, piyasa_set, temsilci_set, ...rest } = r
|
||||
return rest
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchCustomerBalances () {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const { data } = await api.get('/finance/customer-balances', {
|
||||
params: {
|
||||
selected_date: this.filters.selectedDate,
|
||||
cari_search: String(this.filters.appliedCariSearch || this.filters.cariSearch || '').trim()
|
||||
}
|
||||
})
|
||||
this.rows = Array.isArray(data) ? data : []
|
||||
if (!this.defaultsInitialized) {
|
||||
this.applyInitialFilterDefaults()
|
||||
this.defaultsInitialized = true
|
||||
}
|
||||
this.hasFetched = true
|
||||
} catch (err) {
|
||||
this.rows = []
|
||||
this.hasFetched = false
|
||||
this.error =
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
'Cari bakiye listesi getirilemedi.'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
getDetailsByGroup (groupKey) {
|
||||
return this.filteredRows.filter(r =>
|
||||
`${r.ana_cari_kodu || ''}||${r.ana_cari_adi || ''}` === groupKey
|
||||
)
|
||||
},
|
||||
|
||||
resetFilters () {
|
||||
this.filters.cariSearch = ''
|
||||
this.filters.appliedCariSearch = ''
|
||||
this.filters.cariIlkGrup = []
|
||||
this.filters.piyasa = []
|
||||
this.filters.temsilci = []
|
||||
this.filters.riskDurumu = []
|
||||
this.filters.islemTipi = []
|
||||
this.filters.ulke = []
|
||||
this.defaultsInitialized = false
|
||||
},
|
||||
|
||||
applyCariSearch () {
|
||||
this.filters.appliedCariSearch = String(this.filters.cariSearch || '').trim()
|
||||
},
|
||||
|
||||
selectAll (field, options) {
|
||||
this.filters[field] = options.map(o => o.value)
|
||||
},
|
||||
|
||||
clearAll (field) {
|
||||
this.filters[field] = []
|
||||
},
|
||||
|
||||
applyInitialFilterDefaults () {
|
||||
const transferKey = normalizeText('transfer')
|
||||
this.filters.cariIlkGrup = this.cariIlkGrupOptions
|
||||
.map(o => o.value)
|
||||
.filter(v => normalizeText(v) !== transferKey)
|
||||
|
||||
const excludedRisk = new Set([
|
||||
normalizeText('avukat'),
|
||||
normalizeText('orta risk'),
|
||||
normalizeText('yuksek risk')
|
||||
])
|
||||
this.filters.riskDurumu = this.riskDurumuOptions
|
||||
.map(o => o.value)
|
||||
.filter(v => !excludedRisk.has(normalizeText(v)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function uniqueOptions (rows, field) {
|
||||
const set = new Set()
|
||||
for (const r of rows) {
|
||||
const v = String(r[field] || '').trim()
|
||||
if (v) set.add(v)
|
||||
}
|
||||
|
||||
return Array.from(set)
|
||||
.sort((a, b) => a.localeCompare(b, 'tr'))
|
||||
.map(v => ({ label: v, value: v }))
|
||||
}
|
||||
|
||||
function normalizeText (str) {
|
||||
return String(str || '')
|
||||
.toLocaleLowerCase('tr-TR')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
}
|
||||
Reference in New Issue
Block a user