Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-02-25 10:40:07 +03:00
parent 47848fc14d
commit 15e51e9c39
21 changed files with 1526 additions and 618 deletions

View File

@@ -1,77 +0,0 @@
/* 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.config.performance = true
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

@@ -1,156 +0,0 @@
/* 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'
console.info('[Quasar] Running SPA.')
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

@@ -1,116 +0,0 @@
/* 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

@@ -1,23 +0,0 @@
/* 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,8 +0,0 @@
/* eslint-disable */
import "quasar/dist/types/feature-flag.d.ts";
declare module "quasar/dist/types/feature-flag.d.ts" {
interface QuasarFeatureFlags {
store: true;
}
}

View File

@@ -1,8 +0,0 @@
/* eslint-disable */
import { Router } from 'vue-router';
declare module 'pinia' {
export interface PiniaCustomProperties {
readonly router: Router;
}
}

View File

@@ -1,4 +0,0 @@
/* eslint-disable */
/// <reference types="@quasar/app-webpack" />
/// <reference types="vite/client" />

View File

@@ -1,94 +0,0 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "esnext",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"module": "preserve",
"noEmit": true,
"lib": [
"esnext",
"dom",
"dom.iterable"
],
"paths": {
"src": [
"./../src"
],
"src/*": [
"./../src/*"
],
"app": [
"./.."
],
"app/*": [
"./../*"
],
"components": [
"./../src/components"
],
"components/*": [
"./../src/components/*"
],
"layouts": [
"./../src/layouts"
],
"layouts/*": [
"./../src/layouts/*"
],
"pages": [
"./../src/pages"
],
"pages/*": [
"./../src/pages/*"
],
"assets": [
"./../src/assets"
],
"assets/*": [
"./../src/assets/*"
],
"boot": [
"./../src/boot"
],
"boot/*": [
"./../src/boot/*"
],
"stores": [
"./../src/stores"
],
"stores/*": [
"./../src/stores/*"
],
"#q-app": [
"./../node_modules/@quasar/app-webpack/types/index.d.ts"
],
"#q-app/wrappers": [
"./../node_modules/@quasar/app-webpack/types/app-wrappers.d.ts"
],
"#q-app/bex/background": [
"./../node_modules/@quasar/app-webpack/types/bex/entrypoints/background.d.ts"
],
"#q-app/bex/content": [
"./../node_modules/@quasar/app-webpack/types/bex/entrypoints/content.d.ts"
],
"#q-app/bex/private/bex-bridge": [
"./../node_modules/@quasar/app-webpack/types/bex/bex-bridge.d.ts"
]
}
},
"include": [
"./**/*.d.ts",
"./../**/*"
],
"exclude": [
"./../dist",
"./../node_modules",
"./../src-capacitor",
"./../src-cordova",
"./../quasar.config.*.temporary.compiled*"
]
}

View File

@@ -1,125 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* 1. DO NOT edit this file directly as it won't do anything.
* 2. EDIT the original quasar.config file INSTEAD.
* 3. DO NOT git commit this file. It should be ignored.
*
* This file is still here because there was an error in
* the original quasar.config file and this allows you to
* investigate the Node.js stack error.
*
* After you fix the original file, this file will be
* deleted automatically.
**/
// quasar.config.js
import { defineConfig } from "@quasar/app-webpack/wrappers";
var quasar_config_default = defineConfig(() => {
const apiBaseUrl = (process.env.VITE_API_BASE_URL || "/api").trim();
return {
/* =====================================================
APP INFO
===================================================== */
productName: "Baggi BSS",
productDescription: "Baggi Tekstil Business Support System",
/* =====================================================
BOOT FILES
===================================================== */
boot: ["dayjs"],
/* =====================================================
GLOBAL CSS
===================================================== */
css: ["app.css"],
/* =====================================================
ICONS / FONTS
===================================================== */
extras: [
"roboto-font",
"material-icons"
],
/* =====================================================
BUILD (PRODUCTION)
===================================================== */
build: {
vueRouterMode: "hash",
env: {
VITE_API_BASE_URL: apiBaseUrl
},
esbuildTarget: {
browser: ["es2022", "firefox115", "chrome115", "safari14"],
node: "node20"
},
// Cache & performance
gzip: true,
preloadChunks: true
},
/* =====================================================
DEV SERVER (LOCAL)
===================================================== */
devServer: {
server: { type: "http" },
port: 9e3,
open: true,
// DEV proxy (CORS'suz)
proxy: [
{
context: ["/api"],
target: "http://localhost:8080",
changeOrigin: true,
secure: false
}
]
},
/* =====================================================
QUASAR FRAMEWORK
===================================================== */
framework: {
config: {
notify: {
position: "top",
timeout: 2500
}
},
lang: "tr",
plugins: [
"Loading",
"Dialog",
"Notify"
]
},
animations: [],
/* =====================================================
SSR / PWA (DISABLED)
===================================================== */
ssr: {
prodPort: 3e3,
middlewares: ["render"],
pwa: false
},
pwa: {
workboxMode: "GenerateSW"
},
/* =====================================================
MOBILE / DESKTOP
===================================================== */
capacitor: {
hideSplashscreen: true
},
electron: {
preloadScripts: ["electron-preload"],
inspectPort: 5858,
bundler: "packager",
builder: {
appId: "baggisowtfaresystem"
}
},
bex: {
extraScripts: []
}
};
});
export {
quasar_config_default as default
};

View File

@@ -195,6 +195,11 @@ const menuItems = [
label: 'Cari Ekstre',
to: '/app/statementofaccount',
permission: 'finance:view'
},
{
label: 'Cari Bakiye Listesi',
to: '/app/customer-balance-list',
permission: 'finance:view'
}
]
},

View File

@@ -0,0 +1,731 @@
<template>
<q-page v-if="canReadFinance" class="q-pa-md page-layout">
<div class="filter-sticky">
<div class="row q-col-gutter-sm q-mb-md">
<div class="col-12 col-sm-6 col-md-4">
<q-input
v-model="store.filters.cariSearch"
filled
dense
label="Cari Kodu / Cari Adı"
@keyup.enter="store.applyCariSearch()"
>
<template #append>
<q-btn
dense
flat
round
icon="search"
@click="store.applyCariSearch()"
/>
</template>
</q-input>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-input
v-model="store.filters.selectedDate"
label="Tarih"
filled
dense
readonly
>
<template #append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="store.filters.selectedDate" mask="YYYY-MM-DD" />
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.cariIlkGrup"
:options="store.cariIlkGrupOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Cari İlk Grup"
:display-value="selectionLabel(store.filters.cariIlkGrup, 'Cari İlk Grup')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('cariIlkGrup', store.cariIlkGrupOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('cariIlkGrup')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.piyasa"
:options="store.piyasaOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Piyasa"
:display-value="selectionLabel(store.filters.piyasa, 'Piyasa')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('piyasa', store.piyasaOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('piyasa')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.temsilci"
:options="store.temsilciOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Temsilci"
:display-value="selectionLabel(store.filters.temsilci, 'Temsilci')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('temsilci', store.temsilciOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('temsilci')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.riskDurumu"
:options="store.riskDurumuOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Risk Durumu"
:display-value="selectionLabel(store.filters.riskDurumu, 'Risk Durumu')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('riskDurumu', store.riskDurumuOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('riskDurumu')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.islemTipi"
:options="islemTipiOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="İşlem Tipi"
:display-value="selectionLabel(store.filters.islemTipi, 'İşlem Tipi')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('islemTipi', islemTipiOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('islemTipi')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div class="col-12 col-sm-6 col-md-2">
<q-select
v-model="store.filters.ulke"
:options="store.ulkeOptions"
multiple
emit-value
map-options
filled
dense
options-dense
class="compact-select"
label="Ülke (Özellik05)"
:display-value="selectionLabel(store.filters.ulke, 'Ülke')"
>
<template #before-options>
<q-item clickable dense @click.stop="store.selectAll('ulke', store.ulkeOptions)">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-item clickable dense @click.stop="store.clearAll('ulke')">
<q-item-section>Tümünü Temizle</q-item-section>
</q-item>
<q-separator />
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-checkbox :model-value="scope.selected" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
<div class="row q-col-gutter-sm q-mb-md">
<div class="col-auto">
<q-btn
color="primary"
icon="download"
label="Bakiyeleri Getir"
:loading="store.loading"
@click="store.fetchCustomerBalances()"
/>
</div>
<div class="col-auto">
<q-btn
flat
color="grey-8"
icon="restart_alt"
label="Sıfırla"
@click="onReset"
/>
</div>
</div>
<q-banner v-if="store.error" class="bg-red-1 text-negative q-mb-md rounded-borders">
{{ store.error }}
</q-banner>
<q-banner v-if="!store.hasFetched && !store.loading" class="bg-blue-1 text-primary q-mb-md rounded-borders">
Bakiyeleri Getir Tuşuna Basmadan Sistem Çalışmaz
</q-banner>
</div>
<div class="table-area">
<div class="sticky-bar row justify-end items-center q-pa-sm bg-grey-1">
<q-btn
flat
color="secondary"
icon="list"
:label="allDetailsOpen ? 'Tüm Detayları Kapat' : 'Tüm Detayları Aç'"
@click="toggleAllDetails"
/>
</div>
<q-table
title="Cari Bakiye Listesi"
:rows="store.summaryRows"
:columns="summaryColumns"
row-key="group_key"
:loading="store.loading"
flat
bordered
dense
wrap-cells
separator="cell"
hide-bottom
:rows-per-page-options="[0]"
:pagination="{ rowsPerPage: 0 }"
:table-style="{ tableLayout: 'fixed', width: '100%' }"
class="balance-table"
>
<template #header="props">
<q-tr :props="props" class="header-row">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
</q-tr>
<q-tr class="totals-row">
<q-th
v-for="col in props.cols"
:key="`tot-${col.name}`"
:class="col.align === 'right' ? 'text-right' : ''"
>
{{ totalCellValue(col.name) }}
</q-th>
</q-tr>
</template>
<template #body="props">
<q-tr :props="props" class="sub-header-row">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<q-btn
v-if="col.name === 'expand'"
dense
flat
round
size="sm"
:icon="expanded[props.row.group_key] ? 'expand_less' : 'expand_more'"
@click="toggleGroup(props.row.group_key)"
/>
<span v-else-if="col.name === 'prbr_1_2'" class="text-right block prbr-cell">
{{ formatCurrencyMap(props.row.bakiye_1_2_map) }}
</span>
<span v-else-if="col.name === 'prbr_1_3'" class="text-right block prbr-cell">
{{ formatCurrencyMap(props.row.bakiye_1_3_map) }}
</span>
<span v-else-if="staticMoneyFields.includes(col.name)" class="text-center block">
{{ formatAmount(props.row[col.field]) }}
</span>
<span v-else-if="col.name === 'hesap_alinmayan_gun'" class="text-right block">
-
</span>
<span v-else>
{{ props.row[col.field] || '-' }}
</span>
</q-td>
</q-tr>
<q-tr v-if="expanded[props.row.group_key]" class="detail-host-row">
<q-td colspan="100%">
<div class="detail-wrap">
<q-table
:rows="store.getDetailsByGroup(props.row.group_key)"
:columns="detailColumns"
row-key="cari_kodu"
dense
flat
bordered
hide-bottom
:table-style="{ tableLayout: 'fixed', width: '100%' }"
class="detail-table"
>
<template #body-cell-prbr_1_2="scope">
<q-td :props="scope" class="text-right prbr-cell">
{{ formatRowPrBr(scope.row, '1_2') }}
</q-td>
</template>
<template #body-cell-prbr_1_3="scope">
<q-td :props="scope" class="text-right prbr-cell">
{{ formatRowPrBr(scope.row, '1_3') }}
</q-td>
</template>
</q-table>
</div>
</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu module erisim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useCustomerBalanceListStore } from 'src/stores/customerBalanceListStore'
import { usePermission } from 'src/composables/usePermission'
const store = useCustomerBalanceListStore()
const expanded = ref({})
const allDetailsOpen = ref(false)
const { canRead } = usePermission()
const canReadFinance = canRead('finance')
const islemTipiOptions = [
{ label: '1_2 Bakiye Pr.Br', value: 'prbr_1_2' },
{ label: '1_3 Bakiye Pr.Br', value: 'prbr_1_3' },
{ label: '1_2 USD Bakiye', value: 'usd_1_2' },
{ label: '1_2 TRY Bakiye', value: 'try_1_2' },
{ label: '1_3 USD Bakiye', value: 'usd_1_3' },
{ label: '1_3 TRY Bakiye', value: 'try_1_3' }
]
const staticMoneyFields = ['usd_bakiye_1_2', 'tl_bakiye_1_2', 'usd_bakiye_1_3', 'tl_bakiye_1_3']
const metricDefs = {
prbr_1_2: { name: 'prbr_1_2', label: '1_2 Bakiye\nPr.Br', field: 'prbr_1_2', align: 'right', sortable: false },
prbr_1_3: { name: 'prbr_1_3', label: '1_3 Bakiye\nPr.Br', field: 'prbr_1_3', align: 'right', sortable: false },
usd_1_2: { name: 'usd_bakiye_1_2', label: '1_2 USD_BAKIYE', field: 'usd_bakiye_1_2', align: 'center', sortable: true },
try_1_2: { name: 'tl_bakiye_1_2', label: '1_2 TRY_BAKIYE', field: 'tl_bakiye_1_2', align: 'center', sortable: true },
usd_1_3: { name: 'usd_bakiye_1_3', label: '1_3 USD_BAKIYE', field: 'usd_bakiye_1_3', align: 'center', sortable: true },
try_1_3: { name: 'tl_bakiye_1_3', label: '1_3 TRY_BAKIYE', field: 'tl_bakiye_1_3', align: 'center', sortable: true }
}
const selectedMetricKeys = computed(() => {
const selected = store.filters.islemTipi || []
if (!selected.length) return Object.keys(metricDefs)
return selected.filter((k) => k in metricDefs)
})
const summaryColumns = computed(() => ([
{ name: 'expand', label: '', field: 'expand', align: 'center', sortable: false },
{ name: 'ana_cari_kodu', label: 'Ana Cari Kodu', field: 'ana_cari_kodu', align: 'left', sortable: true },
{ name: 'ana_cari_adi', label: 'Ana Cari Detay', field: 'ana_cari_adi', align: 'left', sortable: true },
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left', sortable: true },
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left', sortable: true },
{ name: 'risk_durumu', label: 'Risk Durumu', field: 'risk_durumu', align: 'left', sortable: true },
...selectedMetricKeys.value.map(k => metricDefs[k]),
{ name: 'hesap_alinmayan_gun', label: 'Hesap Alınmayan Gün', field: 'hesap_alinmayan_gun', align: 'right', sortable: false },
{ name: 'kalan_fatura_ortalama_vade_tarihi', label: 'Kalan Fatura Ortalama Vade Tarihi', field: 'kalan_fatura_ortalama_vade_tarihi', align: 'left', sortable: true }
]))
const liveTotals = computed(() => {
return store.filteredRows.reduce((acc, row) => {
acc.usd_bakiye_1_2 += Number(row.usd_bakiye_1_2) || 0
acc.tl_bakiye_1_2 += Number(row.tl_bakiye_1_2) || 0
acc.usd_bakiye_1_3 += Number(row.usd_bakiye_1_3) || 0
acc.tl_bakiye_1_3 += Number(row.tl_bakiye_1_3) || 0
return acc
}, {
usd_bakiye_1_2: 0,
tl_bakiye_1_2: 0,
usd_bakiye_1_3: 0,
tl_bakiye_1_3: 0
})
})
const detailColumns = computed(() => [
{ name: 'cari_kodu', label: 'Cari Kodu', field: 'cari_kodu', align: 'left' },
{ name: 'cari_detay', label: 'Cari Detay', field: 'cari_detay', align: 'left' },
{ name: 'sirket', label: 'Şirket', field: 'sirket', align: 'left' },
{ name: 'piyasa', label: 'Piyasa', field: 'piyasa', align: 'left' },
{ name: 'temsilci', label: 'Temsilci', field: 'temsilci', align: 'left' },
{ name: 'ozellik03', label: 'Risk Durumu', field: 'ozellik03', align: 'left' },
{ name: 'ozellik05', label: 'Ülke', field: 'ozellik05', align: 'left' },
{ name: 'ozellik06', label: 'Özellik06', field: 'ozellik06', align: 'left' },
{ name: 'ozellik07', label: 'Özellik07', field: 'ozellik07', align: 'left' },
{ name: 'cari_doviz', label: 'Döviz', field: 'cari_doviz', align: 'left' },
...selectedMetricKeys.value.map(k => metricDefs[k]),
{ name: 'hesap_alinmayan_gun', label: 'Hesap Alınmayan Gün', field: 'hesap_alinmayan_gun', align: 'right' },
{ name: 'kalan_fatura_ortalama_vade_tarihi', label: 'Kalan Fatura Ortalama Vade Tarihi', field: 'kalan_fatura_ortalama_vade_tarihi', align: 'left' }
])
function onReset () {
store.resetFilters()
store.applyCariSearch()
}
function toggleGroup (key) {
expanded.value[key] = !expanded.value[key]
if (!expanded.value[key]) {
allDetailsOpen.value = false
return
}
allDetailsOpen.value =
store.summaryRows.length > 0 &&
store.summaryRows.every(r => expanded.value[r.group_key])
}
function toggleAllDetails () {
allDetailsOpen.value = !allDetailsOpen.value
if (allDetailsOpen.value) {
const next = {}
for (const row of store.summaryRows) {
next[row.group_key] = true
}
expanded.value = next
return
}
expanded.value = {}
}
function formatAmount (value) {
const n = Number(value || 0)
return new Intl.NumberFormat('tr-TR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(n)
}
function selectionLabel (arr, label) {
const count = Array.isArray(arr) ? arr.length : 0
if (count === 0) return `Tümü (${label})`
if (count === 1) return '1 seçim'
return `${count} seçim`
}
function totalCellValue (colName) {
if (colName === 'expand') return 'Toplam'
if (colName === 'piyasa') return '-'
if (colName === 'temsilci') return '-'
if (colName === 'risk_durumu') return '-'
if (colName === 'prbr_1_2') return formatCurrencyMap(totalByCurrency('1_2'))
if (colName === 'prbr_1_3') return formatCurrencyMap(totalByCurrency('1_3'))
if (colName === 'usd_bakiye_1_2') return formatAmount(liveTotals.value.usd_bakiye_1_2)
if (colName === 'tl_bakiye_1_2') return formatAmount(liveTotals.value.tl_bakiye_1_2)
if (colName === 'usd_bakiye_1_3') return formatAmount(liveTotals.value.usd_bakiye_1_3)
if (colName === 'tl_bakiye_1_3') return formatAmount(liveTotals.value.tl_bakiye_1_3)
if (colName === 'hesap_alinmayan_gun') return '-'
return '-'
}
function totalByCurrency (tip) {
const key = tip === '1_2' ? 'bakiye_1_2_map' : 'bakiye_1_3_map'
const out = {}
for (const r of store.summaryRows) {
const m = r[key] || {}
for (const [curr, val] of Object.entries(m)) {
out[curr] = (Number(out[curr]) || 0) + (Number(val) || 0)
}
}
return out
}
function formatCurrencyMap (mapObj) {
const entries = Object.entries(mapObj || {})
.filter(([, amount]) => Number(amount) !== 0)
.sort((a, b) => a[0].localeCompare(b[0], 'en'))
if (!entries.length) return '-'
return entries
.map(([curr, amount]) => `${curr}: ${formatAmount(amount)}`)
.join(' | ')
}
function formatRowPrBr (row, tip) {
const curr = String(row.cari_doviz || '').trim().toUpperCase() || 'N/A'
const amount = tip === '1_2'
? (Number(row.bakiye_1_2) || 0)
: (Number(row.bakiye_1_3) || 0)
if (amount === 0) return '-'
return `${curr} ${formatAmount(amount)}`
}
</script>
<style scoped>
.page-layout {
height: calc(100vh - 110px);
display: flex;
flex-direction: column;
overflow: hidden;
}
.filter-sticky {
position: sticky;
top: 0;
z-index: 20;
background: #fff;
padding-bottom: 6px;
}
.compact-select :deep(.q-field__control) {
min-height: 40px;
}
.compact-select :deep(.q-field__native),
.compact-select :deep(.q-field__input) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-area {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.sticky-bar {
position: sticky;
top: 0;
z-index: 9;
}
.balance-table {
flex: 1;
min-height: 0;
}
.balance-table :deep(.q-table__container) {
height: 100%;
display: flex;
flex-direction: column;
}
.balance-table :deep(.q-table__top) {
position: sticky;
top: 0;
z-index: 6;
background: #fff;
}
.balance-table :deep(.q-table__middle) {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.balance-table :deep(.header-row th) {
position: sticky;
top: 0;
z-index: 8;
background: var(--q-primary);
color: #fff;
font-weight: 600;
font-family: "Roboto", sans-serif;
}
.balance-table :deep(.totals-row th) {
position: sticky;
top: 38px;
z-index: 7;
background: var(--q-secondary);
color: var(--q-dark);
font-weight: 700;
font-family: "Roboto", sans-serif;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.detail-table :deep(.q-table__middle) {
max-height: 320px;
}
.balance-table :deep(.sub-header-row td) {
background: #fff;
border-bottom: 2px solid rgba(0, 0, 0, 0.18);
font-weight: 600;
}
.balance-table :deep(.sub-header-row td:first-child) {
border-left: 3px solid var(--q-primary);
}
.balance-table :deep(.detail-host-row td) {
background: #f7f7f7;
border-bottom: 10px solid #fff;
padding-top: 10px;
padding-bottom: 12px;
}
.detail-wrap {
border: 1px solid rgba(0, 0, 0, 0.14);
border-left: 4px solid var(--q-secondary);
border-radius: 6px;
background: #fff;
padding: 6px;
}
.balance-table :deep(.header-row th) {
white-space: pre-line;
line-height: 1.15;
}
.prbr-cell {
white-space: normal;
word-break: break-word;
line-height: 1.25;
}
.balance-table :deep(th),
.balance-table :deep(td),
.detail-table :deep(th),
.detail-table :deep(td) {
white-space: normal !important;
word-break: break-word;
overflow-wrap: anywhere;
font-size: 11px;
line-height: 1.2;
padding: 4px 6px !important;
}
.balance-table :deep(.q-table__table),
.detail-table :deep(.q-table__table) {
width: 100% !important;
}
.detail-table :deep(.q-table__middle) {
overflow-x: hidden;
}
</style>

View File

@@ -724,6 +724,13 @@
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="canMutateRows"
color="secondary"
label="Kaydet ve Diğer Renge Geç"
@click="onSaveAndNextColor"
:disable="isClosedRow || isViewOnly || !canMutateRows"
/>
<q-btn
v-if="isEditing && canMutateRows"
color="negative"
@@ -2831,6 +2838,76 @@ const onSaveOrUpdateRow = async () => {
showEditor.value = false
}
function normalizeColorValue(val) {
return String(val || '').trim().toUpperCase()
}
function getNextColorValue() {
const options = Array.isArray(renkOptions.value) ? renkOptions.value : []
if (!options.length) return null
const current = normalizeColorValue(form.renk)
const idx = options.findIndex(o => normalizeColorValue(o.value) === current)
if (idx === -1) return null
const next = options[idx + 1]
return next ? next.value : null
}
const onSaveAndNextColor = async () => {
if (!hasRowMutationPermission()) {
notifyNoPermission(
isEditMode.value
? 'Siparis satiri guncelleme yetkiniz yok'
: 'Siparis satiri kaydetme yetkiniz yok'
)
return
}
if (!form.model) {
$q.notify({ type: 'warning', message: 'Model seçiniz' })
return
}
if (!form.renk) {
$q.notify({ type: 'warning', message: 'Renk seçiniz' })
return
}
const ok = await orderStore.saveOrUpdateRowUnified({
form,
recalcVat: typeof recalcVat === 'function' ? recalcVat : null,
resetEditor: () => {},
stockMap,
$q
})
if (!ok) return
// Edit state temizle: renk değişimi combo delete tetiklemesin
orderStore.editingKey = null
orderStore.selected = null
const nextColor = getNextColorValue()
if (!nextColor) {
$q.notify({
type: 'warning',
message: 'Son renktesiniz. Lütfen farklı bir renk seçin.',
position: 'top-right'
})
return
}
form.renk2 = ''
await onColorChange(nextColor)
$q.notify({
type: 'info',
message: 'Satır kaydedildi. Bir sonraki renge geçildi.',
position: 'top-right'
})
}

View File

@@ -134,6 +134,13 @@ const routes = [
meta: { permission: 'finance:view' }
},
{
path: 'customer-balance-list',
name: 'customer-balance-list',
component: () => import('pages/CustomerBalanceList.vue'),
meta: { permission: 'finance:view' }
},
/* ================= USERS ================= */

View File

@@ -0,0 +1,262 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
export const useCustomerBalanceListStore = defineStore('customerBalanceList', {
state: () => ({
filters: {
selectedDate: new Date().toISOString().slice(0, 10),
cariSearch: '',
appliedCariSearch: '',
cariIlkGrup: [],
piyasa: [],
temsilci: [],
riskDurumu: [],
islemTipi: [],
ulke: []
},
rows: [],
loading: false,
error: null,
hasFetched: false,
defaultsInitialized: false
}),
getters: {
cariIlkGrupOptions: (state) => uniqueOptions(state.rows, 'cari_ilk_grup'),
piyasaOptions: (state) => uniqueOptions(state.rows, 'piyasa'),
temsilciOptions: (state) => uniqueOptions(state.rows, 'temsilci'),
riskDurumuOptions: (state) => uniqueOptions(state.rows, 'ozellik03'),
ulkeOptions: (state) => uniqueOptions(state.rows, 'ozellik05'),
filteredRows: (state) => {
return state.rows.filter((row) => {
const cariIlkGrupOk =
!state.filters.cariIlkGrup.length ||
state.filters.cariIlkGrup.includes(row.cari_ilk_grup)
const piyasaOk =
!state.filters.piyasa.length ||
state.filters.piyasa.includes(row.piyasa)
const temsilciOk =
!state.filters.temsilci.length ||
state.filters.temsilci.includes(row.temsilci)
const riskDurumuOk =
!state.filters.riskDurumu.length ||
state.filters.riskDurumu.includes(row.ozellik03)
const cariText = normalizeText([
row.ana_cari_kodu || '',
row.ana_cari_adi || '',
row.cari_kodu || '',
row.cari_detay || ''
].join(' '))
const cariSearchNeedle = normalizeText(state.filters.appliedCariSearch || '')
const cariSearchOk =
!cariSearchNeedle ||
cariText.includes(cariSearchNeedle)
const ulkeOk =
!state.filters.ulke.length ||
state.filters.ulke.includes(row.ozellik05)
const islemTipiOk =
!state.filters.islemTipi.length ||
state.filters.islemTipi.some((t) => {
const bak12 = Number(row.bakiye_1_2) || 0
const bak13 = Number(row.bakiye_1_3) || 0
const usd12 = Number(row.usd_bakiye_1_2) || 0
const try12 = Number(row.tl_bakiye_1_2) || 0
const usd13 = Number(row.usd_bakiye_1_3) || 0
const try13 = Number(row.tl_bakiye_1_3) || 0
if (t === 'prbr_1_2') return bak12 !== 0
if (t === 'prbr_1_3') return bak13 !== 0
if (t === 'usd_1_2') return usd12 !== 0
if (t === 'try_1_2') return try12 !== 0
if (t === 'usd_1_3') return usd13 !== 0
if (t === 'try_1_3') return try13 !== 0
return false
})
return cariIlkGrupOk && piyasaOk && temsilciOk && riskDurumuOk && cariSearchOk && ulkeOk && islemTipiOk
})
},
summaryRows () {
const grouped = new Map()
for (const row of this.filteredRows) {
const key = `${row.ana_cari_kodu || ''}||${row.ana_cari_adi || ''}`
const current = grouped.get(key) || {
group_key: key,
ana_cari_kodu: row.ana_cari_kodu || '',
ana_cari_adi: row.ana_cari_adi || '',
piyasa: '',
piyasa_set: new Set(),
temsilci: '',
temsilci_set: new Set(),
risk_durumu: '',
risk_set: new Set(),
bakiye_1_2_map: {},
bakiye_1_3_map: {},
usd_bakiye_1_2: 0,
tl_bakiye_1_2: 0,
usd_bakiye_1_3: 0,
tl_bakiye_1_3: 0,
kalan_fatura_ortalama_vade_tarihi: ''
}
current.usd_bakiye_1_2 += Number(row.usd_bakiye_1_2) || 0
current.tl_bakiye_1_2 += Number(row.tl_bakiye_1_2) || 0
current.usd_bakiye_1_3 += Number(row.usd_bakiye_1_3) || 0
current.tl_bakiye_1_3 += Number(row.tl_bakiye_1_3) || 0
const curr = String(row.cari_doviz || '').trim().toUpperCase() || 'N/A'
current.bakiye_1_2_map[curr] =
(Number(current.bakiye_1_2_map[curr]) || 0) + (Number(row.bakiye_1_2) || 0)
current.bakiye_1_3_map[curr] =
(Number(current.bakiye_1_3_map[curr]) || 0) + (Number(row.bakiye_1_3) || 0)
const piyasa = String(row.piyasa || '').trim()
if (piyasa) current.piyasa_set.add(piyasa)
const temsilci = String(row.temsilci || '').trim()
if (temsilci) current.temsilci_set.add(temsilci)
if (
!current.kalan_fatura_ortalama_vade_tarihi &&
row.kalan_fatura_ortalama_vade_tarihi
) {
current.kalan_fatura_ortalama_vade_tarihi = row.kalan_fatura_ortalama_vade_tarihi
}
const risk = String(row.ozellik03 || '').trim()
if (risk) current.risk_set.add(risk)
const riskValues = Array.from(current.risk_set)
current.risk_durumu =
riskValues.length <= 1
? (riskValues[0] || '-')
: riskValues.join(', ')
const piyasaValues = Array.from(current.piyasa_set)
current.piyasa =
piyasaValues.length <= 1
? (piyasaValues[0] || '-')
: piyasaValues.join(', ')
const temsilciValues = Array.from(current.temsilci_set)
current.temsilci =
temsilciValues.length <= 1
? (temsilciValues[0] || '-')
: temsilciValues.join(', ')
grouped.set(key, current)
}
return Array.from(grouped.values()).map((r) => {
const { risk_set, piyasa_set, temsilci_set, ...rest } = r
return rest
})
}
},
actions: {
async fetchCustomerBalances () {
this.loading = true
this.error = null
try {
const { data } = await api.get('/finance/customer-balances', {
params: {
selected_date: this.filters.selectedDate,
cari_search: String(this.filters.appliedCariSearch || this.filters.cariSearch || '').trim()
}
})
this.rows = Array.isArray(data) ? data : []
if (!this.defaultsInitialized) {
this.applyInitialFilterDefaults()
this.defaultsInitialized = true
}
this.hasFetched = true
} catch (err) {
this.rows = []
this.hasFetched = false
this.error =
err?.response?.data?.message ||
err?.message ||
'Cari bakiye listesi getirilemedi.'
} finally {
this.loading = false
}
},
getDetailsByGroup (groupKey) {
return this.filteredRows.filter(r =>
`${r.ana_cari_kodu || ''}||${r.ana_cari_adi || ''}` === groupKey
)
},
resetFilters () {
this.filters.cariSearch = ''
this.filters.appliedCariSearch = ''
this.filters.cariIlkGrup = []
this.filters.piyasa = []
this.filters.temsilci = []
this.filters.riskDurumu = []
this.filters.islemTipi = []
this.filters.ulke = []
this.defaultsInitialized = false
},
applyCariSearch () {
this.filters.appliedCariSearch = String(this.filters.cariSearch || '').trim()
},
selectAll (field, options) {
this.filters[field] = options.map(o => o.value)
},
clearAll (field) {
this.filters[field] = []
},
applyInitialFilterDefaults () {
const transferKey = normalizeText('transfer')
this.filters.cariIlkGrup = this.cariIlkGrupOptions
.map(o => o.value)
.filter(v => normalizeText(v) !== transferKey)
const excludedRisk = new Set([
normalizeText('avukat'),
normalizeText('orta risk'),
normalizeText('yuksek risk')
])
this.filters.riskDurumu = this.riskDurumuOptions
.map(o => o.value)
.filter(v => !excludedRisk.has(normalizeText(v)))
}
}
})
function uniqueOptions (rows, field) {
const set = new Set()
for (const r of rows) {
const v = String(r[field] || '').trim()
if (v) set.add(v)
}
return Array.from(set)
.sort((a, b) => a.localeCompare(b, 'tr'))
.map(v => ({ label: v, value: v }))
}
function normalizeText (str) {
return String(str || '')
.toLocaleLowerCase('tr-TR')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim()
}