Files
bssapp/ui/src/pages/AgingCustomerBalancelist.go.vue
2026-03-17 12:22:52 +03:00

1167 lines
37 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.
Logo
Konular
Değişiklik İstekleri
Dönüm noktaları
Keşfet
M_Kececi
/
bssapp
Kod
Konular
Değişiklik İstekleri
İşlemler
Paketler
Projeler
Sürüm
Wiki
Aktivite
Ayarlar
Dosyalar
AccountAgingStatement.vue
ActivityLogs.vue
ChangePassword.vue
CustomerBalanceList.vue
Dashboard.vue
ErrorNotFound.vue
FirstPasswordChange.vue
MainPage.vue
MainPanel.vue
MePassword.vue
OrderBulkClose.vue
OrderEntry.vue
OrderGateway.vue
OrderList.vue
OrderPdf.vue
OrderProductionUpdate.vue
OrderProductionUpdateList.vue
PermissionMatrix.vue
ProductStockByAttributes.vue
ProductStockQuery.vue
ProductionWorker.vue
ProductionWorkerGateway.vue
ResetPassword.vue
RoleDepartmentPermissionGateway.vue
RoleDepartmentPermissionList.vue
RoleDepartmentPermissionPage.vue
StatementHeaderReport.vue
StatementReport.vue
TestMail.vue
UserDetail.vue
UserGateway.vue
UserList.vue
UserPermissionPage.vue
UserSync.vue
dummydata.vue
statementofaccount.vue
App.vue
-.editorconfig
.editorconfig
.env.development
.env.production
.gitignore
.npmrc
README.md
babel.config.js
index.html
jsconfig.json
package-lock.json
package.json
postcss.config.js
quasar.config.js
quasar.config.js.temporary.compiled.1772696562168.mjs
.gitignore
readme.md
bssapp
/
ui
/
src
/
pages
/
AccountAgingStatement.vue
M_Kececi
ce31aff645
Merge remote-tracking branch 'origin/master'
3 gün önce
1059 satır
35 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 (Bugün)"
filled
dense
readonly
disable
/>
</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="Verileri Yenile"
:loading="store.loading"
@click="store.fetchBalances()"
/>
</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">
Veriler bugünün cache tablosundan okunur. Verileri Yenile ile tekrar yükleyebilirsiniz.
</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="downloadAgingBalancePDF(true)">
<q-item-section class="text-primary">
Detaylı Cari Yaşlandırmalı Bakiye Listesi Yazdır
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="downloadAgingBalancePDF(false)">
<q-item-section class="text-secondary">
Detaysız Cari Yaşlandırmalı 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="downloadAgingBalanceExcel"
/>
<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 Yaşlandırmalı 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-if="dayFields.includes(col.name)" class="text-center block">
{{ formatDay(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>
<template #body-cell-vade_gun="scope">
<q-td :props="scope" class="text-center">
{{ formatDay(scope.row.vade_gun) }}
</q-td>
</template>
<template #body-cell-vade_belge_tarihi_gun="scope">
<q-td :props="scope" class="text-center">
{{ formatDay(scope.row.vade_belge_tarihi_gun) }}
</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 { onMounted } from 'vue'
import { useAccountAgingBalanceStore } from 'src/stores/accountAgingBalanceStore'
import { usePermission } from 'src/composables/usePermission'
import { download, extractApiErrorDetail } from 'src/services/api'
const store = useAccountAgingBalanceStore()
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']
const dayFields = ['vade_gun', 'vade_belge_tarihi_gun']
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]),
{ name: 'vade_gun', label: 'Vade Gun', field: 'vade_gun', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) },
{ name: 'vade_belge_tarihi_gun', label: 'Belge Tarihi Gun', field: 'vade_belge_tarihi_gun', align: 'center', sortable: true, sort: (a, b) => toNumericSortValue(a) - toNumericSortValue(b) }
]))
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
const vadeGun = Number(row.vade_gun) || 0
const vadeBelge = Number(row.vade_belge_tarihi_gun) || 0
if (vadeGun !== 0 || vadeBelge !== 0) {
acc.vade_count += 1
acc.vade_gun_sum += vadeGun
acc.vade_belge_sum += vadeBelge
}
return acc
}, {
usd_bakiye_1_2: 0,
tl_bakiye_1_2: 0,
usd_bakiye_1_3: 0,
tl_bakiye_1_3: 0,
vade_gun_sum: 0,
vade_belge_sum: 0,
vade_count: 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]),
{ name: 'vade_gun', label: 'Vade Gun', field: 'vade_gun', align: 'center' },
{ name: 'vade_belge_tarihi_gun', label: 'Belge Tarihi Gun', field: 'vade_belge_tarihi_gun', align: 'center' }
])
function onReset () {
store.resetFilters()
store.fetchBalances()
}
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 downloadAgingBalancePDF (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 = {
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/account-aging-statement/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 downloadAgingBalanceExcel () {
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 = {
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/account-aging-statement/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_yaslandirmali_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 formatDay (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 === 'vade_gun') return formatDay(liveTotals.value.vade_count > 0 ? liveTotals.value.vade_gun_sum / liveTotals.value.vade_count : 0)
if (colName === 'vade_belge_tarihi_gun') return formatDay(liveTotals.value.vade_count > 0 ? liveTotals.value.vade_belge_sum / liveTotals.value.vade_count : 0)
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)}`
}
onMounted(async () => {
store.filters.selectedDate = new Date().toISOString().slice(0, 10)
await store.fetchBalances()
})
</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>