Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-03 00:30:19 +03:00
parent ea27d34336
commit a4f4c2457f
29 changed files with 4522 additions and 752 deletions

View File

@@ -0,0 +1,75 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { Quasar } from 'quasar'
import { markRaw } from 'vue'
import RootComponent from 'app/src/App.vue'
import createStore from 'app/src/stores/index'
import createRouter from 'app/src/router/index'
export default async function (createAppFn, quasarUserOptions) {
// Create the app instance.
// Here we inject into it the Quasar UI, the router & possibly the store.
const app = createAppFn(RootComponent)
app.use(Quasar, quasarUserOptions)
const store = typeof createStore === 'function'
? await createStore({})
: createStore
app.use(store)
const router = markRaw(
typeof createRouter === 'function'
? await createRouter({store})
: createRouter
)
// make router instance available in store
store.use(({ store }) => { store.router = router })
// Expose the app, the router and the store.
// Note that we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}

View File

@@ -0,0 +1,154 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import { createApp } from 'vue'
import '@quasar/extras/roboto-font/roboto-font.css'
import '@quasar/extras/material-icons/material-icons.css'
// We load Quasar stylesheet file
import 'quasar/dist/quasar.sass'
import 'src/css/app.css'
import createQuasarApp from './app.js'
import quasarUserOptions from './quasar-user-options.js'
const publicPath = `/`
async function start ({
app,
router
, store
}, bootFiles) {
let hasRedirected = false
const getRedirectUrl = url => {
try { return router.resolve(url).href }
catch (err) {}
return Object(url) === url
? null
: url
}
const redirect = url => {
hasRedirected = true
if (typeof url === 'string' && /^https?:\/\//.test(url)) {
window.location.href = url
return
}
const href = getRedirectUrl(url)
// continue if we didn't fail to resolve the url
if (href !== null) {
window.location.href = href
window.location.reload()
}
}
const urlPath = window.location.href.replace(window.location.origin, '')
for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) {
try {
await bootFiles[i]({
app,
router,
store,
ssrContext: null,
redirect,
urlPath,
publicPath
})
}
catch (err) {
if (err && err.url) {
redirect(err.url)
return
}
console.error('[Quasar] boot error:', err)
return
}
}
if (hasRedirected === true) return
app.use(router)
app.mount('#q-app')
}
createQuasarApp(createApp, quasarUserOptions)
.then(app => {
// eventually remove this when Cordova/Capacitor/Electron support becomes old
const [ method, mapFn ] = Promise.allSettled !== void 0
? [
'allSettled',
bootFiles => bootFiles.map(result => {
if (result.status === 'rejected') {
console.error('[Quasar] boot error:', result.reason)
return
}
return result.value.default
})
]
: [
'all',
bootFiles => bootFiles.map(entry => entry.default)
]
return Promise[ method ]([
import(/* webpackMode: "eager" */ 'boot/dayjs')
]).then(bootFiles => {
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
start(app, boot)
})
})

View File

@@ -0,0 +1,116 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import App from 'app/src/App.vue'
let appPrefetch = typeof App.preFetch === 'function'
? App.preFetch
: (
// Class components return the component options (and the preFetch hook) inside __c property
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
? App.__c.preFetch
: false
)
function getMatchedComponents (to, router) {
const route = to
? (to.matched ? to : router.resolve(to).route)
: router.currentRoute.value
if (!route) { return [] }
const matched = route.matched.filter(m => m.components !== void 0)
if (matched.length === 0) { return [] }
return Array.prototype.concat.apply([], matched.map(m => {
return Object.keys(m.components).map(key => {
const comp = m.components[key]
return {
path: m.path,
c: comp
}
})
}))
}
export function addPreFetchHooks ({ router, store, publicPath }) {
// Add router hook for handling preFetch.
// Doing it after initial route is resolved so that we don't double-fetch
// the data that we already have. Using router.beforeResolve() so that all
// async components are resolved.
router.beforeResolve((to, from, next) => {
const
urlPath = window.location.href.replace(window.location.origin, ''),
matched = getMatchedComponents(to, router),
prevMatched = getMatchedComponents(from, router)
let diffed = false
const preFetchList = matched
.filter((m, i) => {
return diffed || (diffed = (
!prevMatched[i] ||
prevMatched[i].c !== m.c ||
m.path.indexOf('/:') > -1 // does it has params?
))
})
.filter(m => m.c !== void 0 && (
typeof m.c.preFetch === 'function'
// Class components return the component options (and the preFetch hook) inside __c property
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
))
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
if (appPrefetch !== false) {
preFetchList.unshift(appPrefetch)
appPrefetch = false
}
if (preFetchList.length === 0) {
return next()
}
let hasRedirected = false
const redirect = url => {
hasRedirected = true
next(url)
}
const proceed = () => {
if (hasRedirected === false) { next() }
}
preFetchList.reduce(
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
store,
currentRoute: to,
previousRoute: from,
redirect,
urlPath,
publicPath
})),
Promise.resolve()
)
.then(proceed)
.catch(e => {
console.error(e)
proceed()
})
})
}

View File

@@ -0,0 +1,23 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import lang from 'quasar/lang/tr.js'
import {Loading,Dialog,Notify} from 'quasar'
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }

View File

@@ -1,4 +1,4 @@
<template>
<template>
<q-layout view="hHh Lpr fFf">
<!-- HEADER -->
@@ -200,6 +200,16 @@ const menuItems = [
label: 'Cari Bakiye Listesi',
to: '/app/customer-balance-list',
permission: 'finance:view'
},
{
label: 'Cari Yaşlandırmalı Ekstre',
to: '/app/account-aging-statement',
permission: 'finance:view'
},
{
label: 'Cari Yaşlandırmalı Cari Bakiye Listesi',
to: '/app/aged-customer-balance-list',
permission: 'finance:view'
}
]
},
@@ -308,3 +318,4 @@ const filteredMenu = computed(() => {
.filter(Boolean)
})
</script>

View File

@@ -0,0 +1,669 @@
<template>
<q-page v-if="canReadFinance" class="q-px-md q-pb-md q-pt-xs page-col statement-page">
<div class="filter-sticky compact-filter q-pa-sm q-mb-xs">
<div class="row q-col-gutter-sm items-end">
<div class="col-12 col-md-5">
<q-select
v-model="selectedCari"
:options="filteredOptions"
label="Cari kod / isim"
filled
dense
clearable
use-input
input-debounce="300"
@filter="filterCari"
emit-value
map-options
:loading="accountStore.loading"
option-value="value"
option-label="label"
behavior="menu"
:keep-selected="true"
/>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-input v-model="dateTo" label="Son tarih" filled dense clearable readonly>
<template #append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="dateTo" mask="YYYY-MM-DD" locale="tr-TR" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="col-12 col-sm-6 col-md-3">
<q-select
v-model="selectedMonType"
:options="monetaryTypeOptions"
label="Parasal İşlem Tipi"
emit-value
map-options
filled
dense
/>
</div>
<div class="col-auto">
<q-btn color="primary" icon="filter_alt" label="Filtrele" @click="onFilterClick" />
</div>
<div class="col-auto">
<q-btn flat color="grey-8" icon="restart_alt" label="Sıfırla" @click="resetFilters" />
</div>
</div>
</div>
<div class="table-scroll">
<div class="sticky-bar row justify-end items-center q-pa-sm bg-grey-1">
<q-btn-dropdown
v-if="canExportFinance"
flat
color="red"
icon="picture_as_pdf"
label="Yazdır"
class="q-mr-sm"
>
<q-list style="min-width: 220px">
<q-item clickable v-close-popup @click="downloadAgingPDF">
<q-item-section class="text-primary">
Detaylı Yaşlandırma Ekstresi Yazdır
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn
flat
color="secondary"
icon="list"
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları Aç'"
@click="toggleAllDetails"
/>
</div>
<q-table
class="sticky-table statement-table"
title="Cari Yaşlandırmalı Ekstre"
:rows="agingStore.masterRows"
:columns="masterColumns"
row-key="group_key"
flat
bordered
dense
hide-bottom
wrap-cells
:rows-per-page-options="[0]"
:loading="agingStore.loading"
:table-style="{ tableLayout: 'fixed', width: '100%' }"
>
<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>
</template>
<template #body="props">
<q-tr :props="props" class="master-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="masterExpanded[props.row.group_key] ? 'expand_less' : 'expand_more'"
@click="toggleMaster(props.row.group_key)"
/>
<span
v-else-if="masterNumericCols.includes(col.name)"
:class="['block', masterCenteredCols.includes(col.name) ? 'text-center' : 'text-right']"
>
{{ masterDayCols.includes(col.name) ? formatDay(props.row[col.field]) : formatAmount(props.row[col.field]) }}
</span>
<span v-else>{{ props.row[col.field] ?? '-' }}</span>
</q-td>
</q-tr>
<q-tr v-if="masterExpanded[props.row.group_key]" class="master-sub-row">
<q-td colspan="100%" class="q-pa-none">
<div class="currency-groups">
<div class="currency-level-head">
<div class="cgh-cell cgh-expand"></div>
<div class="cgh-cell cgh-code">Ana Cari Kod</div>
<div class="cgh-cell cgh-code">Ana Cari Detay</div>
<div class="cgh-cell cgh-code">Döviz Cinsi</div>
<div class="cgh-cell cgh-num">ık Kalem Tutarı</div>
<div class="cgh-cell cgh-num">ık Kalem USD</div>
<div class="cgh-cell cgh-num">ık Kalem TRY</div>
<div class="cgh-cell cgh-center">Ort Gün</div>
<div class="cgh-cell cgh-center">Ort Belge Gün</div>
</div>
<div
v-for="currRow in agingStore.getCurrenciesByMaster(props.row.group_key)"
:key="currRow.group_key"
class="currency-group"
>
<div class="currency-group-header">
<div class="cgh-cell cgh-expand">
<q-btn
dense
flat
round
size="sm"
:icon="currencyExpanded[currRow.group_key] ? 'expand_less' : 'expand_more'"
@click="toggleCurrency(currRow.group_key)"
/>
</div>
<div class="cgh-cell cgh-code">{{ currRow.cari8 }}</div>
<div class="cgh-cell cgh-code">{{ currRow.cari_detay || '-' }}</div>
<div class="cgh-cell cgh-code">{{ currRow.doviz_cinsi }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.acik_kalem_tutari) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.acik_kalem_usd) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.acik_kalem_try) }}</div>
<div class="cgh-cell cgh-center">{{ formatDay(currRow.ort_gun) }}</div>
<div class="cgh-cell cgh-center">{{ formatDay(currRow.ort_belge_gun) }}</div>
</div>
<div v-if="currencyExpanded[currRow.group_key]" class="detail-host-row">
<q-table
:rows="agingStore.getDetailsByCurrency(currRow.group_key)"
:columns="detailColumns"
row-key="detail_key"
flat
dense
bordered
hide-bottom
:rows-per-page-options="[0]"
:pagination="{ rowsPerPage: 0 }"
class="detail-subtable"
:table-style="{ minWidth: '1500px' }"
>
<template #body-cell-eslesen_tutar="d">
<q-td :props="d" class="text-right">{{ formatAmount(d.row.eslesen_tutar) }}</q-td>
</template>
<template #body-cell-usd_tutar="d">
<q-td :props="d" class="text-right">{{ formatAmount(d.row.usd_tutar) }}</q-td>
</template>
<template #body-cell-try_tutar="d">
<q-td :props="d" class="text-right">{{ formatAmount(d.row.try_tutar) }}</q-td>
</template>
<template #body-cell-gun_sayisi="d">
<q-td :props="d" class="text-center">{{ formatDay(d.row.gun_sayisi) }}</q-td>
</template>
<template #body-cell-gun_sayisi_docdate="d">
<q-td :props="d" class="text-center">{{ formatDay(d.row.gun_sayisi_docdate) }}</q-td>
</template>
<template #body-cell-gun_kur="d">
<q-td :props="d" class="text-center">{{ formatAmount(d.row.gun_kur, 2) }}</q-td>
</template>
<template #body-cell-aciklama="d">
<q-td :props="d" class="text-center">{{ d.row.aciklama || '-' }}</q-td>
</template>
</q-table>
</div>
</div>
</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 { onMounted, ref } from 'vue'
import { useQuasar } from 'quasar'
import dayjs from 'dayjs'
import { usePermission } from 'src/composables/usePermission'
import { useAccountStore } from 'src/stores/accountStore'
import { useStatementAgingStore } from 'src/stores/statementAgingStore'
import { download, extractApiErrorDetail } from 'src/services/api'
const { canRead, canExport } = usePermission()
const canReadFinance = canRead('finance')
const canExportFinance = canExport('finance')
const $q = useQuasar()
const accountStore = useAccountStore()
const agingStore = useStatementAgingStore()
const selectedCari = ref(null)
const filteredOptions = ref([])
const dateTo = ref(dayjs().format('YYYY-MM-DD'))
const masterExpanded = ref({})
const currencyExpanded = ref({})
const allDetailsOpen = ref(false)
const monetaryTypeOptions = [
{ label: '1-2 hesap', value: ['1', '2'] },
{ label: '1-3 r hesap', value: ['1', '3'] }
]
const selectedMonType = ref(monetaryTypeOptions[0].value)
const masterColumns = [
{ name: 'expand', label: '', field: 'expand', align: 'center' },
{ name: 'cari8', label: 'Ana Cari Kod', field: 'cari8', align: 'left', sortable: true },
{ name: 'cari_detay', label: 'Ana Cari Detay', field: 'cari_detay', align: 'left', sortable: true },
{ name: 'acik_kalem_tutari_usd', label: 'Açık Kalem Tutarı USD', field: 'acik_kalem_tutari_usd', align: 'right', sortable: true },
{ name: 'acik_kalem_tutari_try', label: 'Açık Kalem Tutarı TRY', field: 'acik_kalem_tutari_try', align: 'right', sortable: true },
{ name: 'acik_kalem_ort_vade_gun', label: 'Açık Kalem Ort Vade Gün', field: 'acik_kalem_ort_vade_gun', align: 'center', sortable: true },
{ name: 'acik_kalem_ort_belge_gun', label: 'Açık Kalem Ort Belge Gün', field: 'acik_kalem_ort_belge_gun', align: 'center', sortable: true },
{ name: 'normal_usd_tutar', label: 'Normal USD Tutar', field: 'normal_usd_tutar', align: 'right', sortable: true },
{ name: 'normal_try_tutar', label: 'Normal TRY Tutar', field: 'normal_try_tutar', align: 'right', sortable: true },
{ name: 'ortalama_vade_gun', label: 'Ortalama Vade Gün', field: 'ortalama_vade_gun', align: 'center', sortable: true },
{ name: 'ortalama_belge_gun', label: 'Ortalama Belge Gün', field: 'ortalama_belge_gun', align: 'center', sortable: true }
]
const detailColumns = [
{ name: 'fatura_cari', label: 'Fatura Cari', field: 'fatura_cari', align: 'left' },
{ name: 'odeme_cari', label: 'Ödeme Cari', field: 'odeme_cari', align: 'left' },
{ name: 'doc_currency_code', label: 'Döviz Cinsi', field: 'doc_currency_code', align: 'left' },
{ name: 'fatura_ref', label: 'Fatura Ref', field: 'fatura_ref', align: 'left' },
{ name: 'odeme_ref', label: 'Ödeme Ref', field: 'odeme_ref', align: 'left' },
{ name: 'fatura_tarihi', label: 'Fatura Tarihi', field: 'fatura_tarihi', align: 'left' },
{ name: 'odeme_tarihi', label: 'Ödeme Vade', field: 'odeme_tarihi', align: 'left' },
{ name: 'odeme_doc_date', label: 'Ödeme DocDate', field: 'odeme_doc_date', align: 'left' },
{ name: 'eslesen_tutar', label: 'Eşleşen Tutar', field: 'eslesen_tutar', align: 'right' },
{ name: 'usd_tutar', label: 'USD Tutar', field: 'usd_tutar', align: 'right' },
{ name: 'try_tutar', label: 'TRY Tutar', field: 'try_tutar', align: 'right' },
{ name: 'aciklama', label: 'Açıklama', field: 'aciklama', align: 'center' },
{ name: 'gun_sayisi', label: 'Gün', field: 'gun_sayisi', align: 'center' },
{ name: 'gun_sayisi_docdate', label: 'Gün (DocDate)', field: 'gun_sayisi_docdate', align: 'center' },
{ name: 'gun_kur', label: 'Gün Kur', field: 'gun_kur', align: 'center' }
]
const masterNumericCols = ['acik_kalem_tutari_usd', 'acik_kalem_tutari_try', 'acik_kalem_ort_vade_gun', 'acik_kalem_ort_belge_gun', 'normal_usd_tutar', 'normal_try_tutar', 'ortalama_vade_gun', 'ortalama_belge_gun']
const masterDayCols = ['acik_kalem_ort_vade_gun', 'acik_kalem_ort_belge_gun', 'ortalama_vade_gun', 'ortalama_belge_gun']
const masterCenteredCols = ['acik_kalem_ort_vade_gun', 'acik_kalem_ort_belge_gun', 'ortalama_vade_gun', 'ortalama_belge_gun']
function normalizeText(str) {
return (str || '')
.toString()
.toLocaleLowerCase('tr-TR')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim()
}
function filterCari(val, update) {
const needle = normalizeText(val)
update(() => {
if (!needle) {
filteredOptions.value = accountStore.accountOptions
return
}
filteredOptions.value = accountStore.accountOptions.filter(o => {
const label = normalizeText(o.label)
const value = normalizeText(o.value)
return label.includes(needle) || value.includes(needle)
})
})
}
onMounted(async () => {
await accountStore.fetchAccounts()
filteredOptions.value = accountStore.accountOptions
})
async function onFilterClick() {
if (!selectedCari.value || !dateTo.value) {
$q.notify({
type: 'warning',
message: 'Lütfen cari ve son tarih seçiniz.',
position: 'top-right'
})
return
}
try {
await agingStore.load({
accountcode: selectedCari.value,
enddate: dateTo.value,
parislemler: selectedMonType.value
})
const m = {}
const c = {}
for (const row of agingStore.masterRows) {
m[row.group_key] = true
for (const cr of agingStore.getCurrenciesByMaster(row.group_key)) {
c[cr.group_key] = true
}
}
masterExpanded.value = m
currencyExpanded.value = c
allDetailsOpen.value = agingStore.masterRows.length > 0
} catch (err) {
const msg = await extractApiErrorDetail(err)
$q.notify({
type: 'negative',
message: msg || 'Veriler yüklenemedi',
position: 'top-right'
})
}
}
function resetFilters() {
selectedCari.value = null
dateTo.value = dayjs().format('YYYY-MM-DD')
selectedMonType.value = monetaryTypeOptions[0].value
masterExpanded.value = {}
currencyExpanded.value = {}
allDetailsOpen.value = false
agingStore.reset()
}
function toggleMaster(key) {
masterExpanded.value[key] = !masterExpanded.value[key]
}
function toggleCurrency(key) {
currencyExpanded.value[key] = !currencyExpanded.value[key]
}
function toggleAllDetails() {
allDetailsOpen.value = !allDetailsOpen.value
if (!allDetailsOpen.value) {
masterExpanded.value = {}
currencyExpanded.value = {}
return
}
const m = {}
const c = {}
for (const row of agingStore.masterRows) {
m[row.group_key] = true
for (const cr of agingStore.getCurrenciesByMaster(row.group_key)) {
c[cr.group_key] = true
}
}
masterExpanded.value = m
currencyExpanded.value = c
}
function formatAmount(value, fraction = 2) {
const n = Number(value || 0)
return new Intl.NumberFormat('tr-TR', {
minimumFractionDigits: fraction,
maximumFractionDigits: fraction
}).format(n)
}
function formatDay(value) {
const n = Number(value || 0)
const v = Number.isFinite(n) ? Math.ceil(n) : 0
return new Intl.NumberFormat('tr-TR', {
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(v)
}
async function downloadAgingPDF () {
if (!canExportFinance.value) {
$q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' })
return
}
if (!selectedCari.value || !dateTo.value) {
$q.notify({ type: 'warning', message: 'Lütfen cari ve son tarih seçiniz.', position: 'top-right' })
return
}
try {
const blob = await download('/finance/account-aging-statement/export-pdf', {
accountcode: selectedCari.value,
enddate: dateTo.value,
parislemler: selectedMonType.value
})
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'
})
}
}
</script>
<style scoped>
.statement-page {
--master-head-h: 34px;
--lvl2-head-h: 34px;
--lvl3-head-h: 34px;
height: calc(100vh - 56px);
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-scroll {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.compact-filter {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px;
background: #fafafa;
}
.sticky-bar {
position: sticky;
top: 0;
z-index: 30;
flex: 0 0 auto;
background: var(--q-secondary);
color: #fff;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.statement-table {
flex: 1;
min-height: 0;
}
.statement-table :deep(.q-table__container) {
height: 100%;
display: flex;
flex-direction: column;
}
.statement-table :deep(.q-table__top) {
flex: 0 0 auto;
position: static;
}
.statement-table :deep(.q-table__middle) {
flex: 1 1 auto;
min-height: 0;
overflow: auto !important;
max-height: none !important;
}
.statement-table :deep(.header-row th) {
position: sticky;
top: 0;
z-index: 30;
height: var(--master-head-h);
background: var(--q-primary);
color: #fff;
font-weight: 600;
border-bottom: 1px solid rgba(255, 255, 255, 0.22);
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.2), 0 1px 0 rgba(0, 0, 0, 0.2);
}
.statement-table :deep(.master-row td) {
background: color-mix(in srgb, var(--q-secondary) 12%, white);
border-bottom: 2px solid rgba(0, 0, 0, 0.18);
font-weight: 600;
}
.statement-table :deep(.master-row td:first-child) {
border-left: 3px solid var(--q-primary);
}
.statement-table :deep(.master-sub-row td) {
background: #f4f6fb;
border-bottom: 8px solid #fff;
vertical-align: top;
padding: 0 !important;
}
.currency-groups {
padding: 6px;
background: #f8faff;
}
.currency-group {
border-left: 4px solid var(--q-secondary);
border-top: 1px solid rgba(0, 0, 0, 0.08);
border-right: 1px solid rgba(0, 0, 0, 0.08);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
margin-bottom: 8px;
background: #fff;
position: relative;
}
.currency-level-head {
position: sticky;
top: var(--master-head-h);
z-index: 27;
display: grid;
grid-template-columns: 48px 120px 280px 110px 1fr 1fr 1fr 120px 150px;
align-items: center;
gap: 0;
background: var(--q-secondary);
color: #fff;
border: 1px solid rgba(0, 0, 0, 0.15);
margin-bottom: 6px;
min-height: var(--lvl2-head-h);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
}
.currency-group-header {
position: sticky;
top: calc(var(--master-head-h) + var(--lvl2-head-h));
z-index: 26;
display: grid;
grid-template-columns: 48px 120px 280px 110px 1fr 1fr 1fr 120px 150px;
align-items: center;
gap: 0;
background: #4c5f7a;
color: #fff;
min-height: var(--lvl3-head-h);
border-bottom: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.18);
}
.cgh-cell {
padding: 6px 8px;
border-right: 1px solid rgba(255, 255, 255, 0.2);
font-weight: 600;
font-size: 11px;
}
.cgh-cell:last-child {
border-right: none;
}
.cgh-num {
text-align: right;
}
.cgh-center {
text-align: center;
}
.cgh-expand {
text-align: center;
}
.detail-host-row :deep(td) {
background: #fdfdfd;
padding: 6px !important;
}
.detail-subtable {
border-left: 4px solid var(--q-primary);
border-top: 1px solid rgba(0, 0, 0, 0.08);
border-right: 1px solid rgba(0, 0, 0, 0.08);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
background: #fff;
}
.statement-table :deep(th),
.statement-table :deep(td),
.detail-subtable :deep(th),
.detail-subtable :deep(td) {
padding: 3px 6px !important;
font-size: 11px !important;
line-height: 1.2 !important;
}
.detail-subtable :deep(.q-table__top) {
position: static;
}
.detail-subtable :deep(.q-table__middle) {
overflow: visible !important;
max-height: none !important;
}
.detail-subtable :deep(thead th) {
position: sticky;
top: calc(var(--master-head-h) + var(--lvl2-head-h) + var(--lvl3-head-h));
z-index: 25;
background: #1f3b5b;
color: #fff;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
}
.detail-subtable :deep(td[data-col="gun_sayisi"]),
.detail-subtable :deep(td[data-col="gun_sayisi_docdate"]),
.detail-subtable :deep(td[data-col="gun_kur"]),
.detail-subtable :deep(td[data-col="aciklama"]),
.detail-subtable :deep(th[data-col="gun_sayisi"]),
.detail-subtable :deep(th[data-col="gun_sayisi_docdate"]),
.detail-subtable :deep(th[data-col="gun_kur"]),
.detail-subtable :deep(th[data-col="aciklama"]),
.statement-table :deep(td[data-col="acik_kalem_ort_vade_gun"]),
.statement-table :deep(td[data-col="acik_kalem_ort_belge_gun"]),
.statement-table :deep(td[data-col="ortalama_vade_gun"]),
.statement-table :deep(td[data-col="ortalama_belge_gun"]),
.statement-table :deep(th[data-col="acik_kalem_ort_vade_gun"]),
.statement-table :deep(th[data-col="acik_kalem_ort_belge_gun"]),
.statement-table :deep(th[data-col="ortalama_vade_gun"]),
.statement-table :deep(th[data-col="ortalama_belge_gun"]) {
text-align: center !important;
}
@media (max-width: 1366px) {
.statement-table :deep(th),
.statement-table :deep(td),
.detail-subtable :deep(th),
.detail-subtable :deep(td) {
font-size: 10px !important;
padding: 2px 4px !important;
}
.currency-group-header {
grid-template-columns: 44px 100px 220px 90px 1fr 1fr 1fr 90px 120px;
}
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<q-page class="q-pa-md">
<q-card flat bordered class="q-pa-lg">
<div class="text-h6">Cari Yaşlandırmalı Cari Bakiye Listesi</div>
<div class="text-subtitle2 q-mt-sm text-grey-7">
Dummy ekran hazır. Bu modülün filtre ve tablo kurgusu bir sonraki adımda eklenecek.
</div>
</q-card>
</q-page>
</template>
<script setup>
</script>

View File

@@ -0,0 +1,13 @@
<template>
<q-page class="q-pa-md">
<q-card flat bordered class="q-pa-lg">
<div class="text-h6">Cari Yaşlandırmalı Ekstre</div>
<div class="text-subtitle2 q-mt-sm text-grey-7">
Dummy ekran hazır. Bu modülün filtre ve tablo kurgusu bir sonraki adımda eklenecek.
</div>
</q-card>
</q-page>
</template>
<script setup>
</script>

View File

@@ -1,263 +1,43 @@
<template>
<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="top-actions row q-col-gutter-sm items-end q-mb-sm">
<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-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-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-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-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"
@@ -279,140 +59,472 @@
</div>
</div>
<div 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-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
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' : ''"
<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"
>
{{ totalCellValue(col.name) }}
</q-th>
</q-tr>
</template>
<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"
/>
</div>
</div>
<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-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>
<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"
<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
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>
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 module erisim yetkiniz yok.
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 $q = useQuasar()
const { canRead } = usePermission()
const { canRead, canExport } = usePermission()
const canReadFinance = canRead('finance')
const canExportFinance = canExport('finance')
const islemTipiOptions = [
{ label: '1_2 Bakiye Pr.Br', value: 'prbr_1_2' },
@@ -422,14 +534,49 @@ const islemTipiOptions = [
{ 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']
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 },
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 }
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(() => {
@@ -440,15 +587,14 @@ const selectedMetricKeys = computed(() => {
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 }
{ 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
@@ -468,23 +614,36 @@ 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: 'ozellik03', label: 'Risk Durumu', field: 'ozellik03', align: 'left' },
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', 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: '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: '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' }
...selectedMetricKeys.value.map((k) => metricDefs[k])
])
function onReset () {
store.resetFilters()
store.applyCariSearch()
}
function onToggle12Changed (val) {
if (val) {
store.filters.excludeZeroBalance13 = false
}
}
function onToggle13Changed (val) {
if (val) {
store.filters.excludeZeroBalance12 = false
}
}
function toggleGroup (key) {
expanded.value[key] = !expanded.value[key]
if (!expanded.value[key]) {
@@ -512,6 +671,97 @@ function toggleAllDetails () {
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',
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', {
@@ -538,7 +788,6 @@ function totalCellValue (colName) {
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 '-'
}
@@ -564,7 +813,7 @@ function formatCurrencyMap (mapObj) {
if (!entries.length) return '-'
return entries
.map(([curr, amount]) => `${curr}: ${formatAmount(amount)}`)
.join(' | ')
.join('\n')
}
function formatRowPrBr (row, tip) {
@@ -577,7 +826,6 @@ function formatRowPrBr (row, tip) {
if (amount === 0) return '-'
return `${curr} ${formatAmount(amount)}`
}
</script>
<style scoped>
@@ -596,6 +844,12 @@ function formatRowPrBr (row, tip) {
padding-bottom: 6px;
}
.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;
}
@@ -652,7 +906,7 @@ function formatRowPrBr (row, tip) {
background: var(--q-primary);
color: #fff;
font-weight: 600;
font-family: "Roboto", sans-serif;
font-family: 'Roboto', sans-serif;
}
.balance-table :deep(.totals-row th) {
@@ -662,7 +916,7 @@ function formatRowPrBr (row, tip) {
background: var(--q-secondary);
color: var(--q-dark);
font-weight: 700;
font-family: "Roboto", sans-serif;
font-family: 'Roboto', sans-serif;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
@@ -701,7 +955,7 @@ function formatRowPrBr (row, tip) {
}
.prbr-cell {
white-space: normal;
white-space: pre-line;
word-break: break-word;
line-height: 1.25;
}

View File

@@ -0,0 +1,13 @@
<template>
<q-page class="q-pa-md">
<q-card flat bordered class="q-pa-lg">
<div class="text-h6">Cari Bakiye Listesi</div>
<div class="text-subtitle2 q-mt-sm text-grey-7">
Dummy ekran hazır. Bu modülün filtre ve tablo kurgusu bir sonraki adımda eklenecek.
</div>
</q-card>
</q-page>
</template>
<script setup>
</script>

View File

@@ -1,7 +1,7 @@
<template>
<template>
<q-page v-if="canReadFinance" class="q-pa-md page-col statement-page">
<!-- 🔹 Cari Kod / İsim (sabit) -->
<!-- Cari Kod / İsim (sabit) -->
<div class="filter-sticky">
<q-select
v-model="selectedCari"
@@ -22,7 +22,7 @@
/>
</div>
<!-- 🔹 Filtre Alanı -->
<!-- Filtre Alanı -->
<div class="filter-collapsible">
<div class="row items-center justify-between q-pa-sm bg-grey-2">
<div class="text-subtitle1">Filtreler</div>
@@ -116,7 +116,7 @@
</q-slide-transition>
</div>
<!-- 🔹 Tablo Alanı -->
<!-- Tablo Alanı -->
<div class="table-scroll">
<!-- Toggle butonları (sticky üst bar) -->
@@ -438,7 +438,7 @@ function toggleRowDetails(row) {
expandedRows.value[row.belge_no] = !expandedRows.value[row.belge_no]
}
/* 🔹 Tüm detayları aç/kapat */
/* Tüm detayları aç/kapat */
function toggleAllDetails() {
allDetailsOpen.value = !allDetailsOpen.value
if (allDetailsOpen.value) {
@@ -454,7 +454,7 @@ function toggleAllDetails() {
function normalizeText (str) {
return (str || '')
.toString()
.toLocaleLowerCase('tr-TR') // 🔥 Türkçe uyumlu
.toLocaleLowerCase('tr-TR') // Türkçe uyumlu
.normalize('NFD') // aksan temizleme
.replace(/[\u0300-\u036f]/g, '')
.trim()
@@ -480,7 +480,7 @@ function formatAmount(n) {
const filtersOpen = ref(true)
/* 🔹 Kolon gizle/göster */
/* Kolon gizle/göster */
const visibleColumns = ref([])
const showLeftCols = ref(true)
@@ -501,7 +501,7 @@ function toggleLeftCols() {
showLeftCols.value = !showLeftCols.value
}
/* 🔹 PDF İndirme Butonuna bağla */
/* PDF İndirme Butonuna bağla */
async function handleDownload() {
if (!canExportFinance.value) {
$q.notify({
@@ -536,14 +536,14 @@ async function handleDownload() {
selectedMonType.value // <-- eklendi (['1','2'] veya ['1','3'])
)
console.log("📤 [DEBUG] Storedan gelen result:", result)
console.log("[DEBUG] Storedan gelen result:", result)
$q.notify({
type: result.ok ? 'positive' : 'negative',
message: result.message,
position: 'top-right'
})
}/* 🔹 Cari Hesap Ekstresi (2. seçenek) */
}/* Cari Hesap Ekstresi (2. seçenek) */
import { useDownloadstHeadStore } from 'src/stores/downloadstHeadStore'
const downloadstHeadStore = useDownloadstHeadStore()
@@ -582,7 +582,7 @@ async function CurrheadDownload() {
selectedMonType.value // parasal işlem tipi (parislemler)
)
console.log("📤 [DEBUG] CurrheadDownloadresult:", result)
console.log("[DEBUG] CurrheadDownloadresult:", result)
$q.notify({
type: result.ok ? 'positive' : 'negative',
@@ -689,3 +689,4 @@ async function CurrheadDownload() {
}
}
</style>

View File

@@ -140,6 +140,18 @@ const routes = [
component: () => import('pages/CustomerBalanceList.vue'),
meta: { permission: 'finance:view' }
},
{
path: 'account-aging-statement',
name: 'account-aging-statement',
component: () => import('pages/AccountAgingStatement.vue'),
meta: { permission: 'finance:view' }
},
{
path: 'aged-customer-balance-list',
name: 'aged-customer-balance-list',
component: () => import('pages/AgedCustomerBalanceListDummy.vue'),
meta: { permission: 'finance:view' }
},
/* ================= USERS ================= */

View File

@@ -5,14 +5,17 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
state: () => ({
filters: {
selectedDate: new Date().toISOString().slice(0, 10),
excludeZeroBalance12: false,
excludeZeroBalance13: false,
cariSearch: '',
appliedCariSearch: '',
cariIlkGrup: [],
piyasa: [],
temsilci: [],
riskDurumu: [],
islemTipi: [],
ulke: []
ulke: [],
il: [],
ilce: []
},
rows: [],
loading: false,
@@ -25,26 +28,47 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
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'),
riskDurumuOptions: (state) => uniqueOptions(state.rows, 'risk_durumu'),
ulkeOptions: (state) => uniqueOptions(state.rows, 'ozellik05'),
ilOptions: (state) => uniqueOptions(state.rows, 'il'),
ilceOptions: (state) => uniqueOptions(state.rows, 'ilce'),
filteredRows: (state) => {
const selectedCariIlkGrup = new Set((state.filters.cariIlkGrup || []).map(v => normalizeText(v)))
const selectedPiyasa = new Set((state.filters.piyasa || []).map(v => normalizeText(v)))
const selectedTemsilci = new Set((state.filters.temsilci || []).map(v => normalizeText(v)))
const selectedRiskDurumu = new Set((state.filters.riskDurumu || []).map(v => normalizeText(v)))
const selectedUlke = new Set((state.filters.ulke || []).map(v => normalizeText(v)))
const selectedIl = new Set((state.filters.il || []).map(v => normalizeText(v)))
const selectedIlce = new Set((state.filters.ilce || []).map(v => normalizeText(v)))
const matchMulti = (selectedSet, value) => {
if (!selectedSet.size) return true
const normalized = normalizeText(value)
if (!normalized) return true
return selectedSet.has(normalized)
}
return state.rows.filter((row) => {
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
const cariSearchNeedle = normalizeText(state.filters.cariSearch || '')
const cariIlkGrupOk =
!state.filters.cariIlkGrup.length ||
state.filters.cariIlkGrup.includes(row.cari_ilk_grup)
matchMulti(selectedCariIlkGrup, row.cari_ilk_grup)
const piyasaOk =
!state.filters.piyasa.length ||
state.filters.piyasa.includes(row.piyasa)
matchMulti(selectedPiyasa, row.piyasa)
const temsilciOk =
!state.filters.temsilci.length ||
state.filters.temsilci.includes(row.temsilci)
matchMulti(selectedTemsilci, row.temsilci)
const riskDurumuOk =
!state.filters.riskDurumu.length ||
state.filters.riskDurumu.includes(row.ozellik03)
matchMulti(selectedRiskDurumu, row.risk_durumu)
const cariText = normalizeText([
row.ana_cari_kodu || '',
@@ -52,25 +76,22 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
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)
matchMulti(selectedUlke, row.ozellik05)
const ilOk =
matchMulti(selectedIl, row.il)
const ilceOk =
matchMulti(selectedIlce, row.ilce)
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
@@ -80,7 +101,12 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
return false
})
return cariIlkGrupOk && piyasaOk && temsilciOk && riskDurumuOk && cariSearchOk && ulkeOk && islemTipiOk
const excludeZero12Ok = !state.filters.excludeZeroBalance12 || bak12 !== 0
const excludeZero13Ok = !state.filters.excludeZeroBalance13 || bak13 !== 0
return cariIlkGrupOk && piyasaOk && temsilciOk && riskDurumuOk &&
cariSearchOk && ulkeOk && ilOk && ilceOk && islemTipiOk &&
excludeZero12Ok && excludeZero13Ok
})
},
@@ -88,10 +114,10 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
const grouped = new Map()
for (const row of this.filteredRows) {
const key = `${row.ana_cari_kodu || ''}||${row.ana_cari_adi || ''}`
const key = String(row.ana_cari_kodu || '').trim()
const current = grouped.get(key) || {
group_key: key,
ana_cari_kodu: row.ana_cari_kodu || '',
ana_cari_kodu: key,
ana_cari_adi: row.ana_cari_adi || '',
piyasa: '',
piyasa_set: new Set(),
@@ -104,8 +130,7 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
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: ''
tl_bakiye_1_3: 0
}
current.usd_bakiye_1_2 += Number(row.usd_bakiye_1_2) || 0
@@ -113,6 +138,10 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
current.usd_bakiye_1_3 += Number(row.usd_bakiye_1_3) || 0
current.tl_bakiye_1_3 += Number(row.tl_bakiye_1_3) || 0
if (!String(current.ana_cari_adi || '').trim() && String(row.ana_cari_adi || '').trim()) {
current.ana_cari_adi = row.ana_cari_adi
}
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)
@@ -125,14 +154,7 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
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()
const risk = String(row.risk_durumu || row.ozellik03 || '').trim()
if (risk) current.risk_set.add(risk)
const riskValues = Array.from(current.risk_set)
@@ -172,7 +194,7 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
const { data } = await api.get('/finance/customer-balances', {
params: {
selected_date: this.filters.selectedDate,
cari_search: String(this.filters.appliedCariSearch || this.filters.cariSearch || '').trim()
cari_search: String(this.filters.cariSearch || '').trim()
}
})
this.rows = Array.isArray(data) ? data : []
@@ -195,26 +217,25 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
getDetailsByGroup (groupKey) {
return this.filteredRows.filter(r =>
`${r.ana_cari_kodu || ''}||${r.ana_cari_adi || ''}` === groupKey
String(r.ana_cari_kodu || '').trim() === String(groupKey || '').trim()
)
},
resetFilters () {
this.filters.excludeZeroBalance12 = false
this.filters.excludeZeroBalance13 = false
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.filters.il = []
this.filters.ilce = []
this.defaultsInitialized = false
},
applyCariSearch () {
this.filters.appliedCariSearch = String(this.filters.cariSearch || '').trim()
},
selectAll (field, options) {
this.filters[field] = options.map(o => o.value)
},
@@ -224,10 +245,14 @@ export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
},
applyInitialFilterDefaults () {
const transferKey = normalizeText('transfer')
const excludedCariIlkGrup = new Set([
normalizeText('transfer'),
normalizeText('perakende'),
normalizeText('dtf')
])
this.filters.cariIlkGrup = this.cariIlkGrupOptions
.map(o => o.value)
.filter(v => normalizeText(v) !== transferKey)
.filter(v => !excludedCariIlkGrup.has(normalizeText(v)))
const excludedRisk = new Set([
normalizeText('avukat'),
@@ -260,3 +285,4 @@ function normalizeText (str) {
.replace(/[\u0300-\u036f]/g, '')
.trim()
}

View File

@@ -0,0 +1,222 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
import qs from 'qs'
export const useStatementAgingStore = defineStore('statementAging', {
state: () => ({
rows: [],
masterRows: [],
currencyRowsByMaster: {},
detailByCurrency: {},
loading: false
}),
actions: {
async load(params = {}) {
this.loading = true
try {
const { data } = await api.get('/finance/account-aging-statement', {
params,
paramsSerializer: p => qs.stringify(p, { arrayFormat: 'repeat' })
})
const base = Array.isArray(data) ? data.map(normalizeRowKeys) : []
this.rows = base.map((r, idx) => ({
...r,
detail_key: `${idx}-${r.cari8 || ''}-${r.doc_currency_code || ''}-${r.fatura_ref || ''}-${r.odeme_ref || ''}`
}))
this.rebuildHierarchical()
} catch (err) {
console.error('Aging statement load failed:', err)
this.reset()
throw err
} finally {
this.loading = false
}
},
rebuildHierarchical() {
const masterMap = {}
const currencyMap = {}
const detailMap = {}
for (const row of this.rows) {
const cari8 = String(row?.cari8 || '').trim()
const curr = String(row?.doc_currency_code || '').trim().toUpperCase() || 'N/A'
if (!cari8) continue
const masterKey = cari8
const currencyKey = `${cari8}|${curr}`
const tutar = Number(row?.eslesen_tutar) || 0
const usd = Number(row?.usd_tutar) || 0
const trY = Number(row?.try_tutar) || 0
const absTry = Math.abs(trY)
const gun = Number(row?.gun_sayisi) || 0
const gunDoc = Number(row?.gun_sayisi_docdate) || 0
const aciklama = String(row?.aciklama || '').toUpperCase()
const isAcik = aciklama === 'ACIKKALEM'
if (!masterMap[masterKey]) {
masterMap[masterKey] = {
group_key: masterKey,
cari8: masterKey,
cari_detay: String(row?.cari_detay || '').trim(),
acik_kalem_tutari_usd: 0,
acik_kalem_tutari_try: 0,
acik_kalem_ort_vade_gun: 0,
acik_kalem_ort_belge_gun: 0,
normal_usd_tutar: 0,
normal_try_tutar: 0,
ortalama_vade_gun: 0,
ortalama_belge_gun: 0,
weighted_all_base: 0,
weighted_all_gun_sum: 0,
weighted_all_doc_sum: 0,
weighted_open_base: 0,
weighted_open_gun_sum: 0,
weighted_open_doc_sum: 0,
}
}
if (!currencyMap[currencyKey]) {
currencyMap[currencyKey] = {
group_key: currencyKey,
master_key: masterKey,
cari8,
cari_detay: String(row?.cari_detay || '').trim(),
doviz_cinsi: curr,
acik_kalem_tutari: 0,
acik_kalem_usd: 0,
acik_kalem_try: 0,
ort_gun: 0,
ort_belge_gun: 0,
weighted_open_base: 0,
weighted_open_gun_sum: 0,
weighted_open_doc_sum: 0
}
}
const m = masterMap[masterKey]
const c = currencyMap[currencyKey]
if (isAcik) {
m.acik_kalem_tutari_usd += usd
m.acik_kalem_tutari_try += trY
c.acik_kalem_tutari += tutar
c.acik_kalem_usd += usd
c.acik_kalem_try += trY
} else {
m.normal_usd_tutar += usd
m.normal_try_tutar += trY
}
if (absTry > 0) {
m.weighted_all_base += absTry
m.weighted_all_gun_sum += absTry * gun
m.weighted_all_doc_sum += absTry * gunDoc
if (isAcik) {
m.weighted_open_base += absTry
m.weighted_open_gun_sum += absTry * gun
m.weighted_open_doc_sum += absTry * gunDoc
c.weighted_open_base += absTry
c.weighted_open_gun_sum += absTry * gun
c.weighted_open_doc_sum += absTry * gunDoc
}
}
if (!detailMap[currencyKey]) detailMap[currencyKey] = []
detailMap[currencyKey].push(row)
}
this.masterRows = Object.values(masterMap)
.map((m) => ({
...m,
acik_kalem_ort_vade_gun: m.weighted_open_base > 0 ? ceilDay(m.weighted_open_gun_sum / m.weighted_open_base) : 0,
acik_kalem_ort_belge_gun: m.weighted_open_base > 0 ? ceilDay(m.weighted_open_doc_sum / m.weighted_open_base) : 0,
ortalama_vade_gun: m.weighted_all_base > 0 ? ceilDay(m.weighted_all_gun_sum / m.weighted_all_base) : 0,
ortalama_belge_gun: m.weighted_all_base > 0 ? ceilDay(m.weighted_all_doc_sum / m.weighted_all_base) : 0
}))
.sort((a, b) => String(a.cari8).localeCompare(String(b.cari8), 'tr', { sensitivity: 'base' }))
const currencyByMaster = {}
for (const c of Object.values(currencyMap)) {
const row = {
...c,
ort_gun: c.weighted_open_base > 0 ? ceilDay(c.weighted_open_gun_sum / c.weighted_open_base) : 0,
ort_belge_gun: c.weighted_open_base > 0 ? ceilDay(c.weighted_open_doc_sum / c.weighted_open_base) : 0
}
if (!currencyByMaster[row.master_key]) currencyByMaster[row.master_key] = []
currencyByMaster[row.master_key].push(row)
}
for (const key of Object.keys(currencyByMaster)) {
currencyByMaster[key].sort((a, b) => String(a.doviz_cinsi).localeCompare(String(b.doviz_cinsi), 'en', { sensitivity: 'base' }))
}
this.currencyRowsByMaster = currencyByMaster
for (const key of Object.keys(detailMap)) {
detailMap[key].sort((a, b) => {
const aEmpty = !a?.odeme_tarihi
const bEmpty = !b?.odeme_tarihi
if (aEmpty && !bEmpty) return -1
if (!aEmpty && bEmpty) return 1
if (aEmpty && bEmpty) return 0
const aTs = Date.parse(a.odeme_tarihi)
const bTs = Date.parse(b.odeme_tarihi)
const aNum = Number.isFinite(aTs) ? aTs : -Infinity
const bNum = Number.isFinite(bTs) ? bTs : -Infinity
return bNum - aNum
})
}
this.detailByCurrency = detailMap
},
reset() {
this.rows = []
this.masterRows = []
this.currencyRowsByMaster = {}
this.detailByCurrency = {}
this.loading = false
},
getCurrenciesByMaster(masterKey) {
return this.currencyRowsByMaster[String(masterKey || '').trim()] || []
},
getDetailsByCurrency(currencyKey) {
return this.detailByCurrency[String(currencyKey || '').trim()] || []
}
}
})
function normalizeRowKeys(row) {
if (!row || typeof row !== 'object') return {}
return {
cari8: row.Cari8 ?? row.cari8 ?? null,
cari_detay: row.CariDetay ?? row.cari_detay ?? null,
fatura_cari: row.FaturaCari ?? row.fatura_cari ?? null,
odeme_cari: row.OdemeCari ?? row.odeme_cari ?? null,
fatura_ref: row.FaturaRef ?? row.fatura_ref ?? null,
odeme_ref: row.OdemeRef ?? row.odeme_ref ?? null,
fatura_tarihi: row.FaturaTarihi ?? row.fatura_tarihi ?? null,
odeme_tarihi: row.OdemeTarihi ?? row.odeme_tarihi ?? null,
odeme_doc_date: row.OdemeDocDate ?? row.odeme_doc_date ?? null,
eslesen_tutar: Number(row.EslesenTutar ?? row.eslesen_tutar ?? 0),
usd_tutar: Number(row.UsdTutar ?? row.usd_tutar ?? 0),
try_tutar: Number(row.TryTutar ?? row.try_tutar ?? 0),
gun_sayisi: Number(row.GunSayisi ?? row.gun_sayisi ?? 0),
gun_sayisi_docdate: Number(row.GunSayisi_DocDate ?? row.gun_sayisi_docdate ?? 0),
gun_kur: Number(row.GunKur ?? row.gun_kur ?? 0),
aciklama: row.Aciklama ?? row.aciklama ?? null,
doc_currency_code: row.DocCurrencyCode ?? row.doc_currency_code ?? null
}
}
function ceilDay(value) {
const n = Number(value)
if (!Number.isFinite(n)) return 0
return Math.ceil(n)
}