1041 lines
34 KiB
Vue
1041 lines
34 KiB
Vue
<template>
|
||
<q-page v-if="canReadFinance" class="q-pa-md page-layout">
|
||
<div class="filter-sticky" :class="{ collapsed: filtersCollapsed }">
|
||
<div class="top-actions row q-col-gutter-sm items-end q-mb-sm" :class="{ 'single-line': filtersCollapsed }">
|
||
<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-3">
|
||
<q-toggle
|
||
v-model="store.filters.excludeZeroBalance12"
|
||
dense
|
||
label="1_2 Bakiyesi Sıfır Olanları Alma"
|
||
@update:model-value="onToggle12Changed"
|
||
/>
|
||
</div>
|
||
|
||
<div class="col-12 col-sm-6 col-md-3">
|
||
<q-toggle
|
||
v-model="store.filters.excludeZeroBalance13"
|
||
dense
|
||
label="1_3 Bakiyesi Sıfır Olanları Alma"
|
||
@update:model-value="onToggle13Changed"
|
||
/>
|
||
</div>
|
||
|
||
<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-slide-transition>
|
||
<div v-show="!filtersCollapsed" class="filters-panel q-pa-sm q-mb-md">
|
||
<div class="row q-col-gutter-sm">
|
||
<div class="col-12 col-sm-6 col-md-4">
|
||
<q-input
|
||
v-model="store.filters.cariSearch"
|
||
filled
|
||
dense
|
||
label="Cari Kodu / Cari Adı"
|
||
/>
|
||
</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"
|
||
: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 class="col-12 col-sm-6 col-md-2">
|
||
<q-select
|
||
v-model="store.filters.il"
|
||
:options="store.ilOptions"
|
||
multiple
|
||
emit-value
|
||
map-options
|
||
filled
|
||
dense
|
||
options-dense
|
||
class="compact-select"
|
||
label="İl"
|
||
:display-value="selectionLabel(store.filters.il, 'İl')"
|
||
>
|
||
<template #before-options>
|
||
<q-item clickable dense @click.stop="store.selectAll('il', store.ilOptions)">
|
||
<q-item-section>Tümünü Seç</q-item-section>
|
||
</q-item>
|
||
<q-item clickable dense @click.stop="store.clearAll('il')">
|
||
<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.ilce"
|
||
:options="store.ilceOptions"
|
||
multiple
|
||
emit-value
|
||
map-options
|
||
filled
|
||
dense
|
||
options-dense
|
||
class="compact-select"
|
||
label="İlçe"
|
||
:display-value="selectionLabel(store.filters.ilce, 'İlçe')"
|
||
>
|
||
<template #before-options>
|
||
<q-item clickable dense @click.stop="store.selectAll('ilce', store.ilceOptions)">
|
||
<q-item-section>Tümünü Seç</q-item-section>
|
||
</q-item>
|
||
<q-item clickable dense @click.stop="store.clearAll('ilce')">
|
||
<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>
|
||
</q-slide-transition>
|
||
|
||
<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-between items-center q-pa-sm bg-grey-1">
|
||
<div />
|
||
<div class="row items-center q-gutter-sm">
|
||
<q-btn
|
||
flat
|
||
color="secondary"
|
||
icon="list"
|
||
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları Aç'"
|
||
@click="toggleAllDetails"
|
||
/>
|
||
<q-btn-dropdown
|
||
v-if="canExportFinance"
|
||
flat
|
||
color="red"
|
||
icon="picture_as_pdf"
|
||
label="Yazdır"
|
||
>
|
||
<q-list style="min-width: 240px">
|
||
<q-item clickable v-close-popup @click="downloadCustomerBalancePDF(true)">
|
||
<q-item-section class="text-primary">
|
||
Detaylı Cari Bakiye Listesi Yazdır
|
||
</q-item-section>
|
||
</q-item>
|
||
<q-item clickable v-close-popup @click="downloadCustomerBalancePDF(false)">
|
||
<q-item-section class="text-secondary">
|
||
Detaysız Cari Bakiye Listesi Yazdır
|
||
</q-item-section>
|
||
</q-item>
|
||
</q-list>
|
||
</q-btn-dropdown>
|
||
<q-btn
|
||
v-if="canExportFinance"
|
||
flat
|
||
color="green-8"
|
||
icon="table_view"
|
||
label="Excel"
|
||
@click="downloadCustomerBalanceExcel"
|
||
/>
|
||
<q-btn
|
||
flat
|
||
color="primary"
|
||
:icon="filtersCollapsed ? 'unfold_more' : 'unfold_less'"
|
||
:label="filtersCollapsed ? 'Filtreleri Genişlet' : 'Filtreleri Daralt'"
|
||
@click="toggleFiltersCollapsed"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<q-table
|
||
title="Cari Bakiye Listesi"
|
||
:rows="store.summaryRows"
|
||
:columns="summaryColumns"
|
||
v-model:pagination="summaryPagination"
|
||
row-key="group_key"
|
||
:loading="store.loading"
|
||
flat
|
||
bordered
|
||
dense
|
||
wrap-cells
|
||
separator="cell"
|
||
hide-bottom
|
||
:rows-per-page-options="[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>{{ 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 modüle erişim yetkiniz yok.
|
||
</div>
|
||
</q-page>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from 'vue'
|
||
import { useQuasar } from 'quasar'
|
||
import { useCustomerBalanceListStore } from 'src/stores/customerBalanceListStore'
|
||
import { usePermission } from 'src/composables/usePermission'
|
||
import { download, extractApiErrorDetail } from 'src/services/api'
|
||
|
||
const store = useCustomerBalanceListStore()
|
||
const expanded = ref({})
|
||
const allDetailsOpen = ref(false)
|
||
const filtersCollapsed = ref(false)
|
||
const summaryPagination = ref({
|
||
page: 1,
|
||
rowsPerPage: 0,
|
||
sortBy: 'ana_cari_kodu',
|
||
descending: false
|
||
})
|
||
const $q = useQuasar()
|
||
|
||
const { canRead, canExport } = usePermission()
|
||
const canReadFinance = canRead('finance')
|
||
const canExportFinance = canExport('finance')
|
||
|
||
const islemTipiOptions = [
|
||
{ label: '1_2', value: '1_2' },
|
||
{ label: '1_3', value: '1_3' }
|
||
]
|
||
|
||
const staticMoneyFields = ['usd_bakiye_1_2', 'tl_bakiye_1_2', 'usd_bakiye_1_3', 'tl_bakiye_1_3']
|
||
|
||
function toNumericSortValue (value) {
|
||
if (typeof value === 'number') {
|
||
return Number.isFinite(value) ? value : 0
|
||
}
|
||
|
||
const s = String(value ?? '').trim()
|
||
if (!s) return 0
|
||
|
||
const hasComma = s.includes(',')
|
||
const hasDot = s.includes('.')
|
||
|
||
let normalized = s.replace(/\s+/g, '')
|
||
|
||
if (hasComma && hasDot) {
|
||
const lastComma = normalized.lastIndexOf(',')
|
||
const lastDot = normalized.lastIndexOf('.')
|
||
if (lastComma > lastDot) {
|
||
normalized = normalized.replace(/\./g, '').replace(',', '.')
|
||
} else {
|
||
normalized = normalized.replace(/,/g, '')
|
||
}
|
||
} else if (hasComma) {
|
||
normalized = normalized.replace(/\./g, '').replace(',', '.')
|
||
}
|
||
|
||
const n = Number.parseFloat(normalized)
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
function sortTextTr (a, b) {
|
||
return String(a ?? '').localeCompare(String(b ?? ''), 'tr', { sensitivity: 'base' })
|
||
}
|
||
|
||
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, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
|
||
try_1_2: { name: 'tl_bakiye_1_2', label: '1_2 TRY_BAKIYE', field: 'tl_bakiye_1_2', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
|
||
usd_1_3: { name: 'usd_bakiye_1_3', label: '1_3 USD_BAKIYE', field: 'usd_bakiye_1_3', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
|
||
try_1_3: { name: 'tl_bakiye_1_3', label: '1_3 TRY_BAKIYE', field: 'tl_bakiye_1_3', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) }
|
||
}
|
||
|
||
const selectedMetricKeys = computed(() => {
|
||
const selected = (store.filters.islemTipi || [])
|
||
.map(v => String(v).trim())
|
||
.filter(Boolean)
|
||
|
||
if (selected.length === 0) {
|
||
return Object.keys(metricDefs)
|
||
}
|
||
|
||
const show12 = selected.includes('1_2')
|
||
const show13 = selected.includes('1_3')
|
||
|
||
return Object.keys(metricDefs).filter((key) => {
|
||
if (!show12 && !show13) return true
|
||
if (key.endsWith('_1_2')) return show12
|
||
if (key.endsWith('_1_3')) return show13
|
||
return true
|
||
})
|
||
})
|
||
|
||
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, sort: sortTextTr },
|
||
{ name: 'ana_cari_adi', label: 'Ana Cari Detay', field: 'ana_cari_adi', align: 'left', sortable: true, sort: sortTextTr },
|
||
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left', sortable: true, sort: sortTextTr },
|
||
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left', sortable: true, sort: sortTextTr },
|
||
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left', sortable: true, sort: sortTextTr },
|
||
...selectedMetricKeys.value.map((k) => metricDefs[k])
|
||
]))
|
||
|
||
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: 'sirket_detay', label: 'Şirket Detayı', field: 'sirket_detay', align: 'left' },
|
||
{ name: 'muhasebe_kodu', label: 'Muhasebe Kodu', field: 'muhasebe_kodu', align: 'left' },
|
||
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left' },
|
||
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left' },
|
||
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left' },
|
||
{ name: 'ozellik05', label: 'Ülke', field: 'ozellik05', align: 'left' },
|
||
{ name: 'il', label: 'İl', field: 'il', align: 'left' },
|
||
{ name: 'ilce', label: 'İlçe', field: 'ilce', align: 'left' },
|
||
{ name: 'cari_doviz', label: 'Döviz', field: 'cari_doviz', align: 'left' },
|
||
...selectedMetricKeys.value.map((k) => metricDefs[k])
|
||
])
|
||
|
||
|
||
function onReset () {
|
||
store.resetFilters()
|
||
}
|
||
|
||
function onToggle12Changed (val) {
|
||
if (val) {
|
||
store.filters.excludeZeroBalance13 = false
|
||
}
|
||
}
|
||
|
||
function onToggle13Changed (val) {
|
||
if (val) {
|
||
store.filters.excludeZeroBalance12 = false
|
||
}
|
||
}
|
||
|
||
function toggleFiltersCollapsed () {
|
||
filtersCollapsed.value = !filtersCollapsed.value
|
||
}
|
||
|
||
|
||
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 = {}
|
||
}
|
||
|
||
async function downloadCustomerBalancePDF (detailed) {
|
||
if (!canExportFinance.value) {
|
||
$q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' })
|
||
return
|
||
}
|
||
|
||
if (!store.hasFetched) {
|
||
$q.notify({ type: 'warning', message: 'Önce Bakiyeleri Getir ile veri yükleyin.', position: 'top-right' })
|
||
return
|
||
}
|
||
|
||
try {
|
||
const params = {
|
||
selected_date: store.filters.selectedDate,
|
||
cari_search: String(store.filters.cariSearch || '').trim(),
|
||
cari_ilk_grup: (store.filters.cariIlkGrup || []).join(','),
|
||
piyasa: (store.filters.piyasa || []).join(','),
|
||
temsilci: (store.filters.temsilci || []).join(','),
|
||
risk_durumu: (store.filters.riskDurumu || []).join(','),
|
||
islem_tipi: (store.filters.islemTipi || []).join(','),
|
||
ulke: (store.filters.ulke || []).join(','),
|
||
il: (store.filters.il || []).join(','),
|
||
ilce: (store.filters.ilce || []).join(','),
|
||
exclude_zero_12: store.filters.excludeZeroBalance12 ? '1' : '0',
|
||
exclude_zero_13: store.filters.excludeZeroBalance13 ? '1' : '0',
|
||
sort_by: String(summaryPagination.value?.sortBy || ''),
|
||
sort_desc: summaryPagination.value?.descending ? '1' : '0',
|
||
detailed: detailed ? '1' : '0'
|
||
}
|
||
|
||
const blob = await download('/finance/customer-balances/export-pdf', params)
|
||
const pdfUrl = window.URL.createObjectURL(new Blob([blob], { type: 'application/pdf' }))
|
||
window.open(pdfUrl, '_blank')
|
||
} catch (err) {
|
||
const detail = await extractApiErrorDetail(err?.original || err)
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: detail || 'PDF oluşturulamadı',
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
}
|
||
|
||
async function downloadCustomerBalanceExcel () {
|
||
if (!canExportFinance.value) {
|
||
$q.notify({ type: 'negative', message: 'Excel export yetkiniz yok', position: 'top-right' })
|
||
return
|
||
}
|
||
|
||
if (!store.hasFetched) {
|
||
$q.notify({ type: 'warning', message: 'Önce Bakiyeleri Getir ile veri yükleyin.', position: 'top-right' })
|
||
return
|
||
}
|
||
|
||
try {
|
||
const params = {
|
||
selected_date: store.filters.selectedDate,
|
||
cari_search: String(store.filters.cariSearch || '').trim(),
|
||
cari_ilk_grup: (store.filters.cariIlkGrup || []).join(','),
|
||
piyasa: (store.filters.piyasa || []).join(','),
|
||
temsilci: (store.filters.temsilci || []).join(','),
|
||
risk_durumu: (store.filters.riskDurumu || []).join(','),
|
||
islem_tipi: (store.filters.islemTipi || []).join(','),
|
||
ulke: (store.filters.ulke || []).join(','),
|
||
il: (store.filters.il || []).join(','),
|
||
ilce: (store.filters.ilce || []).join(','),
|
||
exclude_zero_12: store.filters.excludeZeroBalance12 ? '1' : '0',
|
||
exclude_zero_13: store.filters.excludeZeroBalance13 ? '1' : '0'
|
||
}
|
||
|
||
const file = await download('/finance/customer-balances/export-excel', params)
|
||
const blob = new Blob(
|
||
[file],
|
||
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
|
||
)
|
||
const url = window.URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = 'cari_bakiye_listesi.xlsx'
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
a.remove()
|
||
window.URL.revokeObjectURL(url)
|
||
} catch (err) {
|
||
const detail = await extractApiErrorDetail(err?.original || err)
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: detail || 'Excel oluşturulamadı',
|
||
position: 'top-right'
|
||
})
|
||
}
|
||
}
|
||
|
||
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)
|
||
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('\n')
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
.filter-sticky.collapsed {
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.top-actions.single-line {
|
||
flex-wrap: nowrap;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
scrollbar-width: thin;
|
||
padding-bottom: 4px;
|
||
}
|
||
|
||
.top-actions.single-line > [class*='col-'],
|
||
.top-actions.single-line > .col-auto {
|
||
flex: 0 0 auto;
|
||
min-width: 220px;
|
||
}
|
||
|
||
.top-actions.single-line > .col-auto {
|
||
min-width: auto;
|
||
}
|
||
|
||
.filters-panel {
|
||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||
border-radius: 8px;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.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: pre-line;
|
||
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>
|
||
|
||
|