Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -1,75 +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.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
|
||||
}
|
||||
}
|
||||
@@ -1,158 +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'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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'),
|
||||
|
||||
import(/* webpackMode: "eager" */ 'boot/locale'),
|
||||
|
||||
import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard')
|
||||
|
||||
]).then(bootFiles => {
|
||||
const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function')
|
||||
start(app, boot)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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} }
|
||||
|
||||
158
ui/src/pages/BrandGroupCurrency.vue
Normal file
158
ui/src/pages/BrandGroupCurrency.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<q-page class="q-pa-md">
|
||||
<div class="row items-center justify-between q-col-gutter-sm q-mb-md">
|
||||
<div class="col-12 col-md">
|
||||
<div class="text-h6">Marka Grubu Pr Br. Seçimi</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
Marka gruplarının varsayılan çalışma para birimi burada tanımlanır.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-auto row items-center q-gutter-sm">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
outline
|
||||
:loading="loading"
|
||||
label="Yenile"
|
||||
@click="reload"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
unelevated
|
||||
icon="save"
|
||||
:disable="!canUpdate || dirtyCount === 0 || saving"
|
||||
:loading="saving"
|
||||
:label="`Kaydet (${dirtyCount})`"
|
||||
@click="saveRows"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
row-key="id"
|
||||
:rows="rows"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
hide-bottom
|
||||
class="bg-white"
|
||||
>
|
||||
<template #body-cell-anchor_mode="props">
|
||||
<q-td :props="props">
|
||||
<q-select
|
||||
v-model="props.row._anchor_mode"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:options="anchorOptions"
|
||||
:disable="!canUpdate || saving"
|
||||
style="min-width: 120px"
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-status="props">
|
||||
<q-td :props="props" class="text-right">
|
||||
<q-badge v-if="props.row._dirty" color="orange-7">Degisti</q-badge>
|
||||
<q-badge v-else color="grey-6">Kayitli</q-badge>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import api from 'src/services/api'
|
||||
import { usePermissionStore } from 'stores/permissionStore'
|
||||
|
||||
const perm = usePermissionStore()
|
||||
const canUpdate = computed(() => perm.hasApiPermission('pricing:update'))
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const rows = ref([])
|
||||
|
||||
const anchorOptions = [
|
||||
{ label: 'USD', value: 'USD' },
|
||||
{ label: 'TRY', value: 'TRY' }
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ name: 'code', label: 'Kod', field: 'code', align: 'left', sortable: true },
|
||||
{ name: 'title', label: 'Marka Grubu', field: 'title', align: 'left', sortable: true },
|
||||
{ name: 'description', label: 'Açıklama', field: 'description', align: 'left', sortable: true },
|
||||
{ name: 'anchor_mode', label: 'Varsayılan Pr.Br.', field: 'anchor_mode', align: 'left', sortable: true },
|
||||
{ name: 'status', label: '', field: 'status', align: 'right' }
|
||||
]
|
||||
|
||||
const dirtyCount = computed(() => rows.value.filter(row => row?._dirty).length)
|
||||
|
||||
function normalizeRow (row) {
|
||||
const mode = String(row?.anchor_mode || 'USD').trim().toUpperCase() || 'USD'
|
||||
return {
|
||||
id: Number(row?.id || 0),
|
||||
code: String(row?.code || '').trim(),
|
||||
title: String(row?.title || '').trim(),
|
||||
description: String(row?.description || '').trim(),
|
||||
anchor_mode: mode,
|
||||
_anchor_mode: mode,
|
||||
_dirty: false
|
||||
}
|
||||
}
|
||||
|
||||
function markDirty (row) {
|
||||
row._dirty = String(row._anchor_mode || 'USD').trim().toUpperCase() !== String(row.anchor_mode || 'USD').trim().toUpperCase()
|
||||
}
|
||||
|
||||
async function reload () {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
url: '/pricing/brand-group-currency',
|
||||
timeout: 180000
|
||||
})
|
||||
rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeRow)
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Marka grubu para birimi listesi alinamadi' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRows () {
|
||||
const dirty = rows.value.filter(row => row?._dirty)
|
||||
if (dirty.length === 0) return
|
||||
saving.value = true
|
||||
try {
|
||||
await api.request({
|
||||
method: 'POST',
|
||||
url: '/pricing/brand-group-currency/bulk-save',
|
||||
data: {
|
||||
items: dirty.map(row => ({
|
||||
id: row.id,
|
||||
anchor_mode: String(row._anchor_mode || 'USD').trim().toUpperCase()
|
||||
}))
|
||||
},
|
||||
timeout: 180000
|
||||
})
|
||||
for (const row of dirty) {
|
||||
row.anchor_mode = String(row._anchor_mode || 'USD').trim().toUpperCase()
|
||||
row._dirty = false
|
||||
}
|
||||
Notify.create({ type: 'positive', message: `Kaydedildi: ${dirty.length} satir` })
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Marka grubu para birimi kaydedilemedi' })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload)
|
||||
</script>
|
||||
@@ -86,6 +86,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-banner
|
||||
v-if="csvImportStatus"
|
||||
dense
|
||||
class="q-mb-xs"
|
||||
:class="csvImportStatus.type === 'warning' ? 'bg-amber-2 text-amber-10' : 'bg-green-1 text-green-10'"
|
||||
>
|
||||
{{ csvImportStatus.message }}
|
||||
</q-banner>
|
||||
|
||||
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
||||
<q-table
|
||||
flat
|
||||
@@ -272,6 +281,34 @@
|
||||
dense
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
<q-toggle
|
||||
v-else-if="col.name === 'calc_enabled'"
|
||||
v-model="props.row.calc_enabled"
|
||||
dense
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
<q-toggle
|
||||
v-else-if="col.name === 'publish_postgres'"
|
||||
v-model="props.row.publish_postgres"
|
||||
dense
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
<q-toggle
|
||||
v-else-if="col.name === 'publish_nebim'"
|
||||
v-model="props.row.publish_nebim"
|
||||
dense
|
||||
@update:model-value="() => markDirty(props.row)"
|
||||
/>
|
||||
<q-select
|
||||
v-else-if="retailModeFields.has(col.name)"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:options="retailModeOptions.map(value => ({ label: value, value }))"
|
||||
:model-value="props.row[col.field]"
|
||||
@update:model-value="(value) => updateRetailMode(props.row, col.field, value)"
|
||||
/>
|
||||
<input
|
||||
v-else-if="numericFields.has(col.name)"
|
||||
class="native-cell-input text-right"
|
||||
@@ -287,6 +324,8 @@
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
|
||||
<q-inner-loading :showing="saving" label="Kaydediliyor..." />
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
@@ -306,6 +345,7 @@ const fileInputRef = ref(null)
|
||||
const selectedKeyMap = ref({})
|
||||
const copySelectedKeys = ref([])
|
||||
const tablePagination = ref({ rowsPerPage: 0, sortBy: 'urun_ilk_grubu', descending: false })
|
||||
const csvImportStatus = ref(null) // { type: 'positive'|'warning', message: string, at: string }
|
||||
let emptyRetryTimer = null
|
||||
|
||||
const numericFields = new Set([
|
||||
@@ -313,6 +353,8 @@ const numericFields = new Set([
|
||||
'usd_base', 'usd1', 'usd2', 'usd3', 'usd4', 'usd5', 'usd6', 'usd_wholesale_step', 'usd_retail_step',
|
||||
'eur_base', 'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6', 'eur_wholesale_step', 'eur_retail_step'
|
||||
])
|
||||
const retailModeFields = new Set(['try_retail_mode', 'usd_retail_mode', 'eur_retail_mode'])
|
||||
const retailModeOptions = ['STEP', 'END_99', 'END_49', 'BAND_99', 'BAND_49']
|
||||
|
||||
const importKeyFieldLabels = [
|
||||
['askili_yan', 'ASKILI YAN'],
|
||||
@@ -328,7 +370,12 @@ const importKeyFieldLabels = [
|
||||
|
||||
const importFieldMap = {
|
||||
AKTIF: 'is_active',
|
||||
'HESAP AKTIF': 'calc_enabled',
|
||||
'PG YAYIN': 'publish_postgres',
|
||||
'NEBIM YAYIN': 'publish_nebim',
|
||||
'TRY TOPTAN YUVARLAMA': 'try_wholesale_step',
|
||||
'TRY PERAKENDE MODU': 'try_retail_mode',
|
||||
'TRY PERAKENDE DEGERI': 'try_retail_step',
|
||||
'TRY PERAKENDE YUVARLAMA': 'try_retail_step',
|
||||
'TRY YUVARLAMA': 'try_wholesale_step',
|
||||
'TRY TABAN': 'try_base',
|
||||
@@ -339,6 +386,8 @@ const importFieldMap = {
|
||||
'TRY 5': 'try5',
|
||||
'TRY 6': 'try6',
|
||||
'USD TOPTAN YUVARLAMA': 'usd_wholesale_step',
|
||||
'USD PERAKENDE MODU': 'usd_retail_mode',
|
||||
'USD PERAKENDE DEGERI': 'usd_retail_step',
|
||||
'USD PERAKENDE YUVARLAMA': 'usd_retail_step',
|
||||
'USD YUVARLAMA': 'usd_wholesale_step',
|
||||
'USD TABAN': 'usd_base',
|
||||
@@ -349,6 +398,8 @@ const importFieldMap = {
|
||||
'USD 5': 'usd5',
|
||||
'USD 6': 'usd6',
|
||||
'EUR TOPTAN YUVARLAMA': 'eur_wholesale_step',
|
||||
'EUR PERAKENDE MODU': 'eur_retail_mode',
|
||||
'EUR PERAKENDE DEGERI': 'eur_retail_step',
|
||||
'EUR PERAKENDE YUVARLAMA': 'eur_retail_step',
|
||||
'EUR YUVARLAMA': 'eur_wholesale_step',
|
||||
'EUR TABAN': 'eur_base',
|
||||
@@ -362,7 +413,8 @@ const importFieldMap = {
|
||||
|
||||
const multiFilterFields = [
|
||||
'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu', 'urun_ana_grubu',
|
||||
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
||||
'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group',
|
||||
'try_retail_mode', 'usd_retail_mode', 'eur_retail_mode'
|
||||
]
|
||||
const multiSelectFilterFieldSet = new Set(multiFilterFields)
|
||||
const numberRangeFilterFieldSet = new Set(numericFields)
|
||||
@@ -387,56 +439,64 @@ function col (name, label, field, width, extra = {}) {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
col('copy_select', 'KOPYA', 'copy_select', 86, { sortable: false, classes: 'copy-selection-col', headerClasses: 'copy-selection-col' }),
|
||||
col('select', 'KAYDET', 'select', 72, { sortable: false, classes: 'save-selection-col', headerClasses: 'save-selection-col' }),
|
||||
col('has_rule', 'DURUM', 'has_rule', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('is_active', 'AKTIF', 'is_active', 48, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('askili_yan', 'ASKILI YAN', 'askili_yan', 86, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('kategori', 'KATEGORI', 'kategori', 92, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_ilk_grubu', 'URUN ILK GRUBU', 'urun_ilk_grubu', 100, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_ana_grubu', 'URUN ANA GRUBU', 'urun_ana_grubu', 110, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_alt_grubu', 'URUN ALT GRUBU', 'urun_alt_grubu', 110, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 90, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('marka', 'MARKA', 'marka', 100, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('brand_code', 'BRAND CODE', 'brand_code', 78, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('brand_group', 'MARKA GRUBU', 'brand_group', 88, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('copy_select', 'KOPYA', 'copy_select', 68, { sortable: false, classes: 'copy-selection-col', headerClasses: 'copy-selection-col' }),
|
||||
col('select', 'KAYDET', 'select', 58, { sortable: false, classes: 'save-selection-col', headerClasses: 'save-selection-col' }),
|
||||
col('has_rule', 'DURUM', 'has_rule', 54, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('is_active', 'AKTIF', 'is_active', 42, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('askili_yan', 'ASKILI YAN', 'askili_yan', 72, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('kategori', 'KATEGORI', 'kategori', 76, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_ilk_grubu', 'URUN ILK GRUBU', 'urun_ilk_grubu', 84, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_ana_grubu', 'URUN ANA GRUBU', 'urun_ana_grubu', 90, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('urun_alt_grubu', 'URUN ALT GRUBU', 'urun_alt_grubu', 90, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 72, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('marka', 'MARKA', 'marka', 80, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('brand_code', 'BRAND CODE', 'brand_code', 68, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('brand_group', 'MARKA GRUBU', 'brand_group', 76, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('anchor_mode', 'ANCHOR MODE', 'anchor_mode', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('calc_enabled', 'HESAP AKTIF', 'calc_enabled', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('publish_postgres', 'PG YAYIN', 'publish_postgres', 62, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
col('publish_nebim', 'NEBIM YAYIN', 'publish_nebim', 66, { classes: 'ps-col', headerClasses: 'ps-col' }),
|
||||
|
||||
col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 92, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_retail_step', 'TRY PERAKENDE YUVARLAMA', 'try_retail_step', 98, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_base', 'TRY TABAN', 'try_base', 70, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try1', 'TRY 1', 'try1', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try2', 'TRY 2', 'try2', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try3', 'TRY 3', 'try3', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try4', 'TRY 4', 'try4', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try5', 'TRY 5', 'try5', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try6', 'TRY 6', 'try6', 62, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_wholesale_step', 'TRY TOPTAN YUVARLAMA', 'try_wholesale_step', 76, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_retail_mode', 'TRY PERAKENDE MODU', 'try_retail_mode', 76, { classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_retail_step', 'TRY PERAKENDE DEGERI', 'try_retail_step', 78, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try_base', 'TRY TABAN', 'try_base', 58, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try1', 'TRY 1', 'try1', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try2', 'TRY 2', 'try2', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try3', 'TRY 3', 'try3', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try4', 'TRY 4', 'try4', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try5', 'TRY 5', 'try5', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
col('try6', 'TRY 6', 'try6', 52, { align: 'right', classes: 'try-col', headerClasses: 'try-col' }),
|
||||
|
||||
col('usd_wholesale_step', 'USD TOPTAN YUVARLAMA', 'usd_wholesale_step', 92, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_retail_step', 'USD PERAKENDE YUVARLAMA', 'usd_retail_step', 98, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_base', 'USD TABAN', 'usd_base', 70, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd1', 'USD 1', 'usd1', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd2', 'USD 2', 'usd2', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd3', 'USD 3', 'usd3', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd4', 'USD 4', 'usd4', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd5', 'USD 5', 'usd5', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd6', 'USD 6', 'usd6', 62, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_wholesale_step', 'USD TOPTAN YUVARLAMA', 'usd_wholesale_step', 76, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_retail_mode', 'USD PERAKENDE MODU', 'usd_retail_mode', 76, { classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_retail_step', 'USD PERAKENDE DEGERI', 'usd_retail_step', 78, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd_base', 'USD TABAN', 'usd_base', 58, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd1', 'USD 1', 'usd1', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd2', 'USD 2', 'usd2', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd3', 'USD 3', 'usd3', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd4', 'USD 4', 'usd4', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd5', 'USD 5', 'usd5', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
col('usd6', 'USD 6', 'usd6', 52, { align: 'right', classes: 'usd-col', headerClasses: 'usd-col' }),
|
||||
|
||||
col('eur_wholesale_step', 'EUR TOPTAN YUVARLAMA', 'eur_wholesale_step', 92, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_retail_step', 'EUR PERAKENDE YUVARLAMA', 'eur_retail_step', 98, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_base', 'EUR TABAN', 'eur_base', 70, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur1', 'EUR 1', 'eur1', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur2', 'EUR 2', 'eur2', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur3', 'EUR 3', 'eur3', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur4', 'EUR 4', 'eur4', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur5', 'EUR 5', 'eur5', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur6', 'EUR 6', 'eur6', 62, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' })
|
||||
col('eur_wholesale_step', 'EUR TOPTAN YUVARLAMA', 'eur_wholesale_step', 76, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_retail_mode', 'EUR PERAKENDE MODU', 'eur_retail_mode', 76, { classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_retail_step', 'EUR PERAKENDE DEGERI', 'eur_retail_step', 78, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur_base', 'EUR TABAN', 'eur_base', 58, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur1', 'EUR 1', 'eur1', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur2', 'EUR 2', 'eur2', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur3', 'EUR 3', 'eur3', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur4', 'EUR 4', 'eur4', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur5', 'EUR 5', 'eur5', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' }),
|
||||
col('eur6', 'EUR 6', 'eur6', 52, { align: 'right', classes: 'eur-col', headerClasses: 'eur-col' })
|
||||
]
|
||||
|
||||
const stickyColumnNames = [
|
||||
'copy_select', 'select', 'has_rule', 'is_active', 'askili_yan', 'kategori', 'urun_ilk_grubu',
|
||||
'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group'
|
||||
'urun_ana_grubu', 'urun_alt_grubu', 'icerik', 'marka', 'brand_code', 'brand_group',
|
||||
'anchor_mode', 'calc_enabled', 'publish_postgres', 'publish_nebim'
|
||||
]
|
||||
const stickyBoundaryColumnName = 'brand_group'
|
||||
const stickyBoundaryColumnName = 'publish_nebim'
|
||||
const stickyColumnNameSet = new Set(stickyColumnNames)
|
||||
|
||||
const stickyLeftMap = computed(() => {
|
||||
@@ -466,6 +526,7 @@ const tableStyle = computed(() => ({
|
||||
function filterDisplayValue (row, field) {
|
||||
if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
|
||||
if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
|
||||
if (retailModeFields.has(field)) return String(row?.[field] || 'STEP').trim() || 'STEP'
|
||||
return String(row?.[field] ?? '').trim()
|
||||
}
|
||||
|
||||
@@ -562,6 +623,10 @@ function normalizeWorksheetRow (source) {
|
||||
_row_key: String(source?.scope_key || source?.pricing_parameter_id || ''),
|
||||
has_rule: Boolean(source?.has_rule),
|
||||
id: String(rule?.id || ''),
|
||||
anchor_mode: String(rule?.anchor_mode || 'USD'),
|
||||
calc_enabled: rule?.calc_enabled !== false,
|
||||
publish_postgres: rule?.publish_postgres !== false,
|
||||
publish_nebim: rule?.publish_nebim !== false,
|
||||
is_active: rule?.is_active !== false,
|
||||
askili_yan: String(source?.askili_yan || ''),
|
||||
kategori: String(source?.kategori || ''),
|
||||
@@ -572,6 +637,9 @@ function normalizeWorksheetRow (source) {
|
||||
marka: String(source?.marka || ''),
|
||||
brand_code: String(source?.brand_code || ''),
|
||||
brand_group: String(source?.brand_group || ''),
|
||||
try_retail_mode: String(rule?.try_retail_mode || 'STEP'),
|
||||
usd_retail_mode: String(rule?.usd_retail_mode || 'STEP'),
|
||||
eur_retail_mode: String(rule?.eur_retail_mode || 'STEP'),
|
||||
_dirty: false
|
||||
}
|
||||
for (const key of numericFields) {
|
||||
@@ -607,6 +675,11 @@ function isRowSelected (row) {
|
||||
return !!selectedKeyMap.value?.[row._row_key]
|
||||
}
|
||||
|
||||
function clearSelections () {
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
}
|
||||
|
||||
function isCopySelected (row) {
|
||||
return copySelectedKeySet.value.has(row._row_key)
|
||||
}
|
||||
@@ -668,9 +741,17 @@ function updateNumber (row, field, value) {
|
||||
markDirty(row)
|
||||
}
|
||||
|
||||
function updateRetailMode (row, field, value) {
|
||||
row[field] = retailModeOptions.includes(String(value || '').trim()) ? String(value).trim() : 'STEP'
|
||||
markDirty(row)
|
||||
}
|
||||
|
||||
function exportSortValue (row, field) {
|
||||
if (field === 'has_rule') return row?.has_rule ? 1 : 0
|
||||
if (field === 'is_active') return row?.is_active ? 1 : 0
|
||||
if (field === 'calc_enabled') return row?.calc_enabled ? 1 : 0
|
||||
if (field === 'publish_postgres') return row?.publish_postgres ? 1 : 0
|
||||
if (field === 'publish_nebim') return row?.publish_nebim ? 1 : 0
|
||||
if (numericFields.has(field)) return finiteNumber(row?.[field], 0)
|
||||
return String(row?.[field] ?? '')
|
||||
}
|
||||
@@ -678,6 +759,16 @@ function exportSortValue (row, field) {
|
||||
function exportCellValue (row, field) {
|
||||
if (field === 'has_rule') return row?.has_rule ? 'Tanimli' : 'Yeni'
|
||||
if (field === 'is_active') return row?.is_active ? 'Aktif' : 'Pasif'
|
||||
if (field === 'calc_enabled') return row?.calc_enabled ? 'Aktif' : 'Pasif'
|
||||
if (field === 'publish_postgres') return row?.publish_postgres ? 'Evet' : 'Hayir'
|
||||
if (field === 'publish_nebim') return row?.publish_nebim ? 'Evet' : 'Hayir'
|
||||
// Excel often coerces numeric-looking codes/names; wrap to keep as text when opened/edited in Excel.
|
||||
if (field === 'brand_code' || field === 'marka') {
|
||||
const text = String(row?.[field] ?? '').trim()
|
||||
if (!text) return ''
|
||||
return `="${text.replaceAll('"', '""')}"`
|
||||
}
|
||||
if (retailModeFields.has(field)) return String(row?.[field] || 'STEP').trim() || 'STEP'
|
||||
if (numericFields.has(field)) {
|
||||
const value = row?.[field]
|
||||
if (value === '' || value === null || value === undefined) return '0'
|
||||
@@ -858,10 +949,17 @@ async function onImportFileChange (event) {
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
console.info('[pricing-rules][ui] csv-import:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
name: file?.name || '',
|
||||
size: file?.size || 0
|
||||
})
|
||||
const text = await file.text()
|
||||
const matrix = parseCsvRows(text).filter(row => row.some(cell => String(cell || '').trim() !== ''))
|
||||
if (matrix.length < 2) {
|
||||
Notify.create({ type: 'negative', message: 'CSV bos veya gecersiz' })
|
||||
csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: 'CSV bos veya gecersiz.' }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -883,6 +981,7 @@ async function onImportFileChange (event) {
|
||||
let matched = 0
|
||||
let updated = 0
|
||||
let skipped = 0
|
||||
const updatedRowKeys = []
|
||||
|
||||
for (let i = 1; i < matrix.length; i++) {
|
||||
const csvRow = matrix[i]
|
||||
@@ -912,7 +1011,22 @@ async function onImportFileChange (event) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (field === 'calc_enabled' || field === 'publish_postgres' || field === 'publish_nebim') {
|
||||
const next = parseImportedBoolean(rawValue)
|
||||
if (next !== null && Boolean(target[field]) !== next) {
|
||||
target[field] = next
|
||||
rowChanged = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (retailModeFields.has(field)) {
|
||||
const next = retailModeOptions.includes(normalizeImportText(rawValue)) ? normalizeImportText(rawValue) : 'STEP'
|
||||
if (String(target[field] || 'STEP') !== next) {
|
||||
target[field] = next
|
||||
rowChanged = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
const next = parseImportedNumber(rawValue)
|
||||
if (Number(target[field] ?? 0) !== next) {
|
||||
target[field] = next
|
||||
@@ -922,21 +1036,48 @@ async function onImportFileChange (event) {
|
||||
|
||||
if (rowChanged) {
|
||||
markDirty(target)
|
||||
updatedRowKeys.push(String(target._row_key || '').trim())
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
if (matched === 0) {
|
||||
Notify.create({ type: 'warning', message: 'CSV icindeki satirlar ekrandaki kayitlarla eslesmedi' })
|
||||
csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: 'CSV icindeki satirlar ekrandaki kayitlarla eslesmedi.' }
|
||||
return
|
||||
}
|
||||
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: `CSV yuklendi. Islenen: ${matrix.length - 1}, eslesen: ${matched}, guncellenen: ${updated}, atlanan: ${skipped}`
|
||||
// Ensure: CSV'den degisen satirlar hem dirty hem de "Kaydet" secimi (checkbox) olarak isaretlensin.
|
||||
// Bazı render edge-case'lerinde sadece sayac artip checkbox guncellenmiyor gibi gorunebiliyor;
|
||||
// burada selection map'i explicit guncelleyip senkronu garanti ediyoruz.
|
||||
if (updatedRowKeys.length > 0) {
|
||||
const next = { ...(selectedKeyMap.value || {}) }
|
||||
for (const key of updatedRowKeys) {
|
||||
if (!key) continue
|
||||
next[key] = true
|
||||
}
|
||||
selectedKeyMap.value = next
|
||||
}
|
||||
|
||||
const summary = `CSV yuklendi. Islenen: ${matrix.length - 1}, eslesen: ${matched}, guncellenen: ${updated}, atlanan: ${skipped}`
|
||||
if (updated === 0) {
|
||||
Notify.create({ type: 'warning', message: summary })
|
||||
csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: summary }
|
||||
} else {
|
||||
Notify.create({ type: 'positive', message: summary })
|
||||
csvImportStatus.value = { type: 'positive', at: new Date().toISOString(), message: summary }
|
||||
}
|
||||
|
||||
console.info('[pricing-rules][ui] csv-import:done', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
processed: matrix.length - 1,
|
||||
matched,
|
||||
updated,
|
||||
skipped
|
||||
})
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' })
|
||||
csvImportStatus.value = { type: 'warning', at: new Date().toISOString(), message: err?.message || 'CSV okunamadi' }
|
||||
} finally {
|
||||
if (input) input.value = ''
|
||||
}
|
||||
@@ -953,6 +1094,12 @@ function copySelectedToSelected () {
|
||||
const target = rows.value.find(row => row._row_key === keys[i])
|
||||
if (!target) continue
|
||||
target.is_active = Boolean(source.is_active)
|
||||
target.calc_enabled = Boolean(source.calc_enabled)
|
||||
target.publish_postgres = Boolean(source.publish_postgres)
|
||||
target.publish_nebim = Boolean(source.publish_nebim)
|
||||
target.try_retail_mode = String(source.try_retail_mode || 'STEP')
|
||||
target.usd_retail_mode = String(source.usd_retail_mode || 'STEP')
|
||||
target.eur_retail_mode = String(source.eur_retail_mode || 'STEP')
|
||||
for (const field of numericFields) {
|
||||
target[field] = source[field]
|
||||
}
|
||||
@@ -1056,8 +1203,7 @@ async function refreshRows () {
|
||||
}
|
||||
|
||||
clearAllFilters()
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
clearSelections()
|
||||
await loadRows()
|
||||
}
|
||||
|
||||
@@ -1067,6 +1213,7 @@ async function loadRows () {
|
||||
emptyRetryTimer = null
|
||||
}
|
||||
loading.value = true
|
||||
let ok = false
|
||||
try {
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
@@ -1074,16 +1221,17 @@ async function loadRows () {
|
||||
timeout: 180000
|
||||
})
|
||||
rows.value = (Array.isArray(res?.data) ? res.data : []).map(normalizeWorksheetRow)
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
clearSelections()
|
||||
if (rows.value.length === 0) {
|
||||
emptyRetryTimer = setTimeout(loadRows, 10000)
|
||||
}
|
||||
ok = true
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kural kombinasyonlari alinamadi' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
async function saveSelected () {
|
||||
@@ -1091,27 +1239,124 @@ async function saveSelected () {
|
||||
if (dirty.length === 0) return
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
items: dirty.map(row => {
|
||||
const item = {
|
||||
id: row.id,
|
||||
pricing_parameter_id: row.pricing_parameter_id,
|
||||
is_active: Boolean(row.is_active)
|
||||
}
|
||||
for (const key of numericFields) item[key] = finiteNumber(row[key], 0)
|
||||
return item
|
||||
})
|
||||
}
|
||||
await api.request({
|
||||
method: 'POST',
|
||||
url: '/pricing/pricing-rules/bulk-save',
|
||||
data: payload,
|
||||
timeout: 180000
|
||||
const startedAt = Date.now()
|
||||
console.info('[pricing-rules][ui] saveSelected:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
dirty_count: dirty.length
|
||||
})
|
||||
|
||||
const buildPayload = (list) => {
|
||||
return {
|
||||
items: list.map(row => {
|
||||
const item = {
|
||||
id: row.id,
|
||||
pricing_parameter_id: row.pricing_parameter_id,
|
||||
calc_enabled: Boolean(row.calc_enabled),
|
||||
publish_postgres: Boolean(row.publish_postgres),
|
||||
publish_nebim: Boolean(row.publish_nebim),
|
||||
is_active: Boolean(row.is_active),
|
||||
try_retail_mode: String(row.try_retail_mode || 'STEP'),
|
||||
usd_retail_mode: String(row.usd_retail_mode || 'STEP'),
|
||||
eur_retail_mode: String(row.eur_retail_mode || 'STEP')
|
||||
}
|
||||
for (const key of numericFields) item[key] = finiteNumber(row[key], 0)
|
||||
return item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const makeTraceId = () => `ui-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
const isTimeoutLikeError = (e) => {
|
||||
const status = e?.response?.status || null
|
||||
// With axios timeout disabled for bulk-save, treat only real upstream/proxy timeouts as retry-able.
|
||||
return status === 504
|
||||
}
|
||||
|
||||
let savedTotal = 0
|
||||
const failedKeys = []
|
||||
|
||||
const postBulkSave = async (list) => {
|
||||
const traceId = makeTraceId()
|
||||
const payload = buildPayload(list)
|
||||
await api.request({
|
||||
method: 'POST',
|
||||
url: '/pricing/pricing-rules/bulk-save',
|
||||
data: payload,
|
||||
// Disable axios timeout here: backend may legitimately run for several minutes on the first write after a full truncate/import.
|
||||
// Any upstream/proxy timeout will surface as 504 anyway.
|
||||
timeout: 0,
|
||||
headers: { 'X-Trace-ID': traceId }
|
||||
})
|
||||
return traceId
|
||||
}
|
||||
|
||||
// Prefer single request (fast path). Fallback to bisection only on proxy/timeout errors.
|
||||
try {
|
||||
const traceId = await postBulkSave(dirty)
|
||||
savedTotal = dirty.length
|
||||
console.info('[pricing-rules][ui] saveSelected:one-shot:done', { trace_id: traceId, total: dirty.length })
|
||||
} catch (e) {
|
||||
if (!isTimeoutLikeError(e)) throw e
|
||||
|
||||
const initialChunkSize = 50
|
||||
const queue = []
|
||||
for (let offset = 0; offset < dirty.length; offset += initialChunkSize) {
|
||||
queue.push(dirty.slice(offset, offset + initialChunkSize))
|
||||
}
|
||||
|
||||
while (queue.length > 0) {
|
||||
const batch = queue.shift()
|
||||
if (!batch || batch.length === 0) continue
|
||||
|
||||
console.info('[pricing-rules][ui] saveSelected:batch:start', {
|
||||
batch_size: batch.length,
|
||||
saved_total: savedTotal,
|
||||
total: dirty.length
|
||||
})
|
||||
|
||||
try {
|
||||
const traceId = await postBulkSave(batch)
|
||||
savedTotal += batch.length
|
||||
Notify.create({ type: 'positive', message: `Kaydedildi: ${savedTotal} / ${dirty.length}` })
|
||||
console.info('[pricing-rules][ui] saveSelected:batch:done', {
|
||||
trace_id: traceId,
|
||||
batch_size: batch.length,
|
||||
saved_total: savedTotal,
|
||||
total: dirty.length
|
||||
})
|
||||
} catch (err2) {
|
||||
if (isTimeoutLikeError(err2) && batch.length > 1) {
|
||||
const mid = Math.ceil(batch.length / 2)
|
||||
queue.unshift(batch.slice(mid))
|
||||
queue.unshift(batch.slice(0, mid))
|
||||
continue
|
||||
}
|
||||
throw err2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reloaded = await loadRows()
|
||||
if (!reloaded) {
|
||||
Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Kaydetme tamamlandi, ancak liste yenilenemedi. Sayfayi yenileyip (F5) kontrol edin.'
|
||||
})
|
||||
return
|
||||
}
|
||||
Notify.create({ type: 'positive', message: `Kaydedildi: ${dirty.length}` })
|
||||
await loadRows()
|
||||
csvImportStatus.value = null
|
||||
console.info('[pricing-rules][ui] saveSelected:done', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
dirty_count: dirty.length,
|
||||
reloaded: true
|
||||
})
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kurallar kaydedilemedi' })
|
||||
console.error('[pricing-rules][ui] saveSelected:error', {
|
||||
status: err?.response?.status || null,
|
||||
message: err?.response?.data || err?.message || 'Kurallar kaydedilemedi'
|
||||
})
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -1119,8 +1364,7 @@ async function saveSelected () {
|
||||
|
||||
function resetTransientState () {
|
||||
rows.value = []
|
||||
selectedKeyMap.value = {}
|
||||
copySelectedKeys.value = []
|
||||
clearSelections()
|
||||
}
|
||||
|
||||
onMounted(refreshRows)
|
||||
@@ -1133,10 +1377,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.pricing-rules-page {
|
||||
--rules-row-height: 31px;
|
||||
--rules-header-height: 72px;
|
||||
--rules-row-height: 27px;
|
||||
--rules-header-height: 58px;
|
||||
--rules-table-height: calc(100vh - 210px);
|
||||
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
@@ -1181,7 +1426,7 @@ onBeforeUnmount(() => {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin-right: var(--sticky-scroll-comp, 0px);
|
||||
@@ -1197,7 +1442,7 @@ onBeforeUnmount(() => {
|
||||
.rules-table :deep(th),
|
||||
.rules-table :deep(td) {
|
||||
box-sizing: border-box;
|
||||
padding: 0 4px;
|
||||
padding: 0 2px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1229,7 +1474,7 @@ onBeforeUnmount(() => {
|
||||
word-break: normal;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
line-height: 1.15;
|
||||
}
|
||||
@@ -1301,7 +1546,7 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
color: #bf5b04;
|
||||
}
|
||||
@@ -1375,7 +1620,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.rules-table :deep(.selection-col .q-checkbox__inner) {
|
||||
color: var(--q-primary);
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.copy-cell-wrap {
|
||||
@@ -1389,7 +1634,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.rules-table :deep(.rule-select-checkbox .q-checkbox__inner) {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.rules-table :deep(th.usd-col),
|
||||
@@ -1419,18 +1664,19 @@ onBeforeUnmount(() => {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.1;
|
||||
padding: 0 4px;
|
||||
padding: 0 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.native-cell-input {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 3px;
|
||||
padding: 1px 2px;
|
||||
border: 1px solid #cfd8dc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1440,7 +1686,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.action-legend :deep(.q-chip) {
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="top-bar row items-center justify-between q-mb-xs">
|
||||
<div class="text-subtitle1 text-weight-bold">Urun Fiyatlandirma</div>
|
||||
<div class="top-actions">
|
||||
<div class="row items-center q-gutter-xs top-actions-row">
|
||||
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--filters">
|
||||
<q-select
|
||||
v-model="topUrunIlkGrubu"
|
||||
dense
|
||||
@@ -51,79 +51,134 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row items-center q-gutter-xs top-actions-row">
|
||||
<q-btn-dropdown color="secondary" outline icon="view_module" label="Doviz Gorunumu" :auto-close="false">
|
||||
<q-list dense class="currency-menu-list">
|
||||
<q-item clickable @click="selectAllCurrencies">
|
||||
<q-item-section>Tumunu Sec</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="clearAllCurrencies">
|
||||
<q-item-section>Tumunu Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item v-for="option in currencyOptions" :key="option.value" clickable @click="toggleCurrencyRow(option.value)">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="isCurrencySelected(option.value)"
|
||||
dense
|
||||
@update:model-value="(val) => toggleCurrency(option.value, val)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>{{ option.label }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
<q-btn
|
||||
flat
|
||||
:color="showSelectedOnly ? 'primary' : 'grey-7'"
|
||||
:icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'"
|
||||
:label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'"
|
||||
:disable="!showSelectedOnly && selectedRowCount === 0"
|
||||
@click="toggleShowSelectedOnly"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
outline
|
||||
icon="edit_note"
|
||||
label="Secili Olanlari Toplu Degistir"
|
||||
:disable="selectedRowCount === 0"
|
||||
@click="bulkDialogOpen = true"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
flat
|
||||
icon="download"
|
||||
label="Sayfayi Excel'e Aktar"
|
||||
:disable="filteredRows.length === 0"
|
||||
@click="exportCurrentView"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
outline
|
||||
icon="download_for_offline"
|
||||
label="Tum Filtreyi Excel'e Aktar"
|
||||
:disable="filteredRows.length === 0 || exportAllLoading"
|
||||
:loading="exportAllLoading"
|
||||
@click="exportAllFiltered"
|
||||
/>
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
color="primary"
|
||||
:max="Math.max(1, store.totalPages || 1)"
|
||||
:max-pages="8"
|
||||
boundary-links
|
||||
direction-links
|
||||
@update:model-value="onPageChange"
|
||||
/>
|
||||
<div class="text-caption text-grey-8">
|
||||
Sayfa {{ currentPage }} / {{ Math.max(1, store.totalPages || 1) }} - Toplam {{ store.totalCount || 0 }} urun kodu
|
||||
<div class="row items-center q-gutter-xs top-actions-row top-actions-row--actions">
|
||||
<div class="toolbar-group">
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
color="grey-8"
|
||||
icon="view_sidebar"
|
||||
:label="leftDetailsExpanded ? 'Detaylari Gizle' : 'Detaylari Goster'"
|
||||
@click="leftDetailsExpanded = !leftDetailsExpanded"
|
||||
/>
|
||||
<q-btn-dropdown dense color="secondary" outline icon="view_module" label="Doviz Gorunumu" :auto-close="false">
|
||||
<q-list dense class="currency-menu-list">
|
||||
<q-item clickable @click="selectAllCurrencies">
|
||||
<q-item-section>Tumunu Sec</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="clearAllCurrencies">
|
||||
<q-item-section>Tumunu Temizle</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item v-for="option in currencyOptions" :key="option.value" clickable @click="toggleCurrencyRow(option.value)">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="isCurrencySelected(option.value)"
|
||||
dense
|
||||
@update:model-value="(val) => toggleCurrency(option.value, val)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>{{ option.label }}</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
:color="showSelectedOnly ? 'primary' : 'grey-7'"
|
||||
:icon="showSelectedOnly ? 'checklist_rtl' : 'list_alt'"
|
||||
:label="showSelectedOnly ? `Secililer (${selectedRowCount})` : 'Secili Olanlari Getir'"
|
||||
:disable="!showSelectedOnly && selectedRowCount === 0"
|
||||
@click="toggleShowSelectedOnly"
|
||||
/>
|
||||
<q-btn
|
||||
dense
|
||||
color="primary"
|
||||
outline
|
||||
icon="calculate"
|
||||
label="Secilileri Hesapla"
|
||||
:disable="selectedRowCount === 0 || bulkCalcLoading"
|
||||
:loading="bulkCalcLoading"
|
||||
@click="calculateSelectedRows"
|
||||
/>
|
||||
<q-btn
|
||||
dense
|
||||
color="primary"
|
||||
icon="save"
|
||||
:label="saveButtonLabel"
|
||||
:disable="selectedDirtyCount === 0 || saving"
|
||||
:loading="saving"
|
||||
@click="saveSelectedRows"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<q-btn-dropdown dense color="primary" outline icon="download" label="Cikti Al" :auto-close="true">
|
||||
<q-list dense style="min-width: 260px;">
|
||||
<q-item clickable :disable="filteredRows.length === 0" @click="exportCurrentView">
|
||||
<q-item-section avatar><q-icon name="grid_on" /></q-item-section>
|
||||
<q-item-section>Sayfayi Excel'e Aktar</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable :disable="filteredRows.length === 0 || exportAllLoading" @click="exportAllFiltered">
|
||||
<q-item-section avatar><q-icon name="download_for_offline" /></q-item-section>
|
||||
<q-item-section>Tum Filtreyi Excel'e Aktar</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable :disable="store.loading" @click="openPriceListExportDialog()">
|
||||
<q-item-section avatar><q-icon name="receipt_long" /></q-item-section>
|
||||
<q-item-section>Fiyat Listesi Ciktisi...</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</div>
|
||||
|
||||
<q-space />
|
||||
|
||||
<div class="toolbar-group toolbar-group--paging">
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
color="primary"
|
||||
:max="Math.max(1, store.totalPages || 1)"
|
||||
:max-pages="8"
|
||||
boundary-links
|
||||
direction-links
|
||||
@update:model-value="onPageChange"
|
||||
/>
|
||||
<div class="text-caption text-grey-8">
|
||||
Sayfa {{ currentPage }} / {{ Math.max(1, store.totalPages || 1) }} - Toplam {{ store.totalCount || 0 }} urun kodu
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap" :style="{ '--sticky-scroll-comp': `${stickyScrollComp}px` }">
|
||||
<q-inner-loading :showing="saving || bulkCalcLoading">
|
||||
<q-spinner-gears size="46px" color="primary" />
|
||||
</q-inner-loading>
|
||||
<div v-if="showGuidanceOverlay" class="empty-overlay">
|
||||
<div class="empty-overlay-inner">
|
||||
<div class="text-subtitle1 text-weight-bold">Calismaya Baslamak Icin</div>
|
||||
<div class="text-body2 q-mt-xs">
|
||||
Urun Ilk Grubu veya Urun Ana Grubu secin ve <b>GRUPLARI GETIR</b>'e basin.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="topScrollRef"
|
||||
class="top-x-scroll"
|
||||
@scroll.passive="onTopScroll"
|
||||
>
|
||||
<div
|
||||
ref="topScrollInnerRef"
|
||||
class="top-x-scroll-inner"
|
||||
:style="{ width: `${tableMinWidth}px` }"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
ref="mainTableRef"
|
||||
class="pane-table pricing-table"
|
||||
@@ -157,7 +212,15 @@
|
||||
@update:model-value="toggleSelectAllVisible"
|
||||
/>
|
||||
<div v-else class="header-with-filter">
|
||||
<span>{{ col.label }}</span>
|
||||
<span :title="col.label">{{ col.label }}</span>
|
||||
<q-tooltip
|
||||
v-if="col.label"
|
||||
anchor="top middle"
|
||||
self="bottom middle"
|
||||
:offset="[0, 6]"
|
||||
>
|
||||
{{ col.label }}
|
||||
</q-tooltip>
|
||||
<q-btn
|
||||
v-if="isHeaderFilterField(col.field)"
|
||||
dense
|
||||
@@ -350,7 +413,7 @@
|
||||
<q-checkbox
|
||||
size="sm"
|
||||
color="primary"
|
||||
:model-value="isRowSelected(props.row.productCode)"
|
||||
:model-value="isRowSelected(rowSelectionKey(props.row))"
|
||||
@update:model-value="(val) => onRowCheckboxChange(props.row, val)"
|
||||
@click.stop
|
||||
/>
|
||||
@@ -368,15 +431,41 @@
|
||||
size="sm"
|
||||
color="primary"
|
||||
label="Hesapla"
|
||||
:loading="!!calcLoadingMap[props.row.productCode]"
|
||||
@click="calculateRow(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-historyAction="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
round
|
||||
size="sm"
|
||||
color="grey-8"
|
||||
icon="history"
|
||||
:disable="!props.row?.productCode"
|
||||
@click="openPriceHistoryDialog(props.row)"
|
||||
>
|
||||
<q-tooltip anchor="top middle" self="bottom middle" :offset="[0, 6]">Fiyat gecmisi</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-productCode="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:class="{
|
||||
'sticky-col': isStickyCol(props.col.name),
|
||||
'sticky-boundary': isStickyBoundary(props.col.name),
|
||||
'selected-tone-cell': shouldToneSelectedCell(props.row, props.col.name)
|
||||
}"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="product-code-text" :title="String(props.value ?? '')">{{ props.value }}</span>
|
||||
@@ -386,7 +475,11 @@
|
||||
<template #body-cell-stockQty="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:class="{
|
||||
'sticky-col': isStickyCol(props.col.name),
|
||||
'sticky-boundary': isStickyBoundary(props.col.name),
|
||||
'selected-tone-cell': shouldToneSelectedCell(props.row, props.col.name)
|
||||
}"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="stock-qty-text">{{ formatStock(props.value) }}</span>
|
||||
@@ -396,7 +489,11 @@
|
||||
<template #body-cell-stockEntryDate="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:class="{
|
||||
'sticky-col': isStickyCol(props.col.name),
|
||||
'sticky-boundary': isStickyBoundary(props.col.name),
|
||||
'selected-tone-cell': shouldToneSelectedCell(props.row, props.col.name)
|
||||
}"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="date-cell-text">{{ formatDateDisplay(props.value) }}</span>
|
||||
@@ -408,6 +505,7 @@
|
||||
:props="props"
|
||||
:class="[
|
||||
{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) },
|
||||
{ 'selected-tone-cell': shouldToneSelectedCell(props.row, props.col.name) },
|
||||
{ 'cell-danger': needsCosting(props.row) }
|
||||
]"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
@@ -424,7 +522,11 @@
|
||||
<template #body-cell-lastPricingDate="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:class="{
|
||||
'sticky-col': isStickyCol(props.col.name),
|
||||
'sticky-boundary': isStickyBoundary(props.col.name),
|
||||
'selected-tone-cell': shouldToneSelectedCell(props.row, props.col.name)
|
||||
}"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span :class="['date-cell-text', { 'date-warning': needsRepricing(props.row) }]">
|
||||
@@ -436,7 +538,11 @@
|
||||
<template #body-cell-brandGroupSelection="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:class="{
|
||||
'sticky-col': isStickyCol(props.col.name),
|
||||
'sticky-boundary': isStickyBoundary(props.col.name),
|
||||
'selected-tone-cell': shouldToneSelectedCell(props.row, props.col.name)
|
||||
}"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<span class="cell-text" :title="props.row.brandGroupSelection || ''">
|
||||
@@ -448,75 +554,293 @@
|
||||
<template #body-cell="props">
|
||||
<q-td
|
||||
:props="props"
|
||||
:class="{ 'sticky-col': isStickyCol(props.col.name), 'sticky-boundary': isStickyBoundary(props.col.name) }"
|
||||
:class="{
|
||||
'sticky-col': isStickyCol(props.col.name),
|
||||
'sticky-boundary': isStickyBoundary(props.col.name),
|
||||
'selected-tone-cell': shouldToneSelectedCell(props.row, props.col.name)
|
||||
}"
|
||||
:style="getBodyCellStyle(props.col)"
|
||||
>
|
||||
<input
|
||||
v-if="editableColumnSet.has(props.col.name)"
|
||||
class="native-cell-input text-right"
|
||||
:value="formatPrice(props.row[props.col.field])"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
@change="(e) => onEditableCellChange(props.row, props.col.field, e.target.value)"
|
||||
/>
|
||||
<div v-if="editableColumnSet.has(props.col.name)" class="editable-price-cell">
|
||||
<input
|
||||
class="native-cell-input text-right price-edit-input"
|
||||
:value="formatPrice(props.row[props.col.field])"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
@change="(e) => onEditableCellChange(props.row, props.col.field, e.target.value)"
|
||||
/>
|
||||
<span class="old-price-label" :title="`Eski: ${formatPrice(getOriginalCellValue(props.row, props.col.field))}`">
|
||||
{{ formatPrice(getOriginalCellValue(props.row, props.col.field)) }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="cell-text" :title="String(props.value ?? '')">{{ props.value }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
|
||||
<q-banner v-if="store.error" class="bg-red text-white q-mt-xs">
|
||||
Hata: {{ store.error }}
|
||||
<q-banner v-if="store.error && !isGuidanceState" class="bg-red text-white q-mt-xs">
|
||||
{{ store.error }}
|
||||
</q-banner>
|
||||
|
||||
<q-dialog v-model="bulkDialogOpen">
|
||||
<q-card style="min-width: 420px; max-width: 95vw;">
|
||||
<q-card-section class="text-subtitle1 text-weight-bold">
|
||||
Secili Olanlari Toplu Degistir
|
||||
<q-dialog v-model="priceHistoryDialogOpen" persistent>
|
||||
<q-card class="price-history-card">
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div>
|
||||
<div class="text-subtitle1 text-weight-bold">Urun Fiyat Karti</div>
|
||||
<div class="text-caption text-grey-7">
|
||||
{{ priceHistoryRow?.productCode || '-' }} | {{ priceHistoryRow?.marka || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<q-btn flat round icon="close" color="grey-8" @click="priceHistoryDialogOpen = false" />
|
||||
</q-card-section>
|
||||
<q-card-section class="q-gutter-sm">
|
||||
<q-select
|
||||
v-model="bulkField"
|
||||
:options="bulkFieldOptions"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
emit-value
|
||||
map-options
|
||||
dense
|
||||
outlined
|
||||
label="Alan"
|
||||
/>
|
||||
<q-input
|
||||
v-model="bulkValue"
|
||||
dense
|
||||
outlined
|
||||
label="Deger"
|
||||
inputmode="decimal"
|
||||
/>
|
||||
<div class="text-caption text-grey-8">
|
||||
Uygulanacak satir sayisi: {{ selectedRowCount }}
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="q-pt-sm q-pb-none">
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-btn
|
||||
color="negative"
|
||||
icon="delete"
|
||||
label="Secilenleri Sil"
|
||||
:disable="selectedHistoryCount === 0 || priceHistoryLoading"
|
||||
@click="confirmDeleteSelectedHistory"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn
|
||||
outline
|
||||
color="primary"
|
||||
icon="refresh"
|
||||
label="Yenile"
|
||||
:loading="priceHistoryLoading"
|
||||
:disable="!priceHistoryRow?.productCode"
|
||||
@click="reloadPriceHistory()"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-sm">
|
||||
<q-inner-loading :showing="priceHistoryLoading">
|
||||
<q-spinner-gears size="46px" color="primary" />
|
||||
</q-inner-loading>
|
||||
|
||||
<q-tabs v-model="priceHistoryTab" dense inline-label class="text-grey-8" active-color="primary">
|
||||
<q-tab name="pg" icon="storefront" label="B2B/B2C" />
|
||||
<q-tab name="mssql" icon="dns" label="NEBIM_V3" />
|
||||
</q-tabs>
|
||||
<q-separator class="q-mt-xs q-mb-sm" />
|
||||
|
||||
<q-tab-panels v-model="priceHistoryTab" animated>
|
||||
<q-tab-panel name="pg" class="q-pa-none">
|
||||
<div v-if="pgHistoryGroups.length === 0" class="text-caption text-grey-7 q-pa-sm">
|
||||
Kayit bulunamadi.
|
||||
</div>
|
||||
<q-list v-else dense bordered separator>
|
||||
<q-expansion-item
|
||||
v-for="g in pgHistoryGroups"
|
||||
:key="g.key"
|
||||
expand-separator
|
||||
:label="`${g.currency} ${(g.levelNo <= 5) ? 'B2B' : 'B2C'} Level ${g.levelNo}`"
|
||||
:caption="`${g.rows.length} kayit | Son: ${formatMoney(g.latest?.price)} @ ${g.latest?.updated_at || '-'}`"
|
||||
>
|
||||
<q-item v-for="r in g.rows" :key="r.id">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="selectedPgIdSet.has(r.id)"
|
||||
dense
|
||||
@update:model-value="(val) => toggleSelectedPgId(r.id, val)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ formatMoney(r.price) }}</q-item-label>
|
||||
<q-item-label caption>{{ r.updated_at }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-badge color="grey-6" outline>{{ r.currency }} {{ r.level_no }}</q-badge>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="mssql" class="q-pa-none">
|
||||
<div v-if="mssqlHistoryGroups.length === 0" class="text-caption text-grey-7 q-pa-sm">
|
||||
Kayit bulunamadi.
|
||||
</div>
|
||||
<q-list v-else dense bordered separator>
|
||||
<q-expansion-item
|
||||
v-for="g in mssqlHistoryGroups"
|
||||
:key="g.key"
|
||||
expand-separator
|
||||
:label="`${g.currency} ${g.price_group_code}`"
|
||||
:caption="`${g.rows.length} kayit | Son: ${formatMoney(g.latest?.price)} @ ${formatMssqlStamp(g.latest)}`"
|
||||
>
|
||||
<q-item v-for="r in g.rows" :key="r.price_list_line_id">
|
||||
<q-item-section avatar>
|
||||
<q-checkbox
|
||||
:model-value="selectedMssqlIdSet.has(r.price_list_line_id)"
|
||||
dense
|
||||
@update:model-value="(val) => toggleSelectedMssqlId(r.price_list_line_id, val)"
|
||||
@click.stop
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ formatMoney(r.price) }}</q-item-label>
|
||||
<q-item-label caption>{{ formatMssqlStamp(r) }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-badge color="grey-6" outline>{{ r.currency }} {{ r.price_group_code }}</q-badge>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="priceListExportDialogOpen" persistent>
|
||||
<q-card style="min-width: 740px; max-width: 95vw;">
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div class="text-subtitle1 text-weight-bold">
|
||||
Fiyat Listesi Ciktisi
|
||||
</div>
|
||||
<q-btn flat round icon="close" color="grey-8" @click="priceListExportDialogOpen = false" />
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="q-gutter-sm">
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-btn-toggle
|
||||
v-model="priceListExportFormat"
|
||||
dense
|
||||
unelevated
|
||||
toggle-color="primary"
|
||||
color="grey-3"
|
||||
text-color="grey-9"
|
||||
:options="[
|
||||
{ label: 'PDF', value: 'pdf', icon: 'picture_as_pdf' },
|
||||
{ label: 'Excel', value: 'excel', icon: 'grid_on' }
|
||||
]"
|
||||
/>
|
||||
<q-toggle v-model="priceListInStockOnly" label="Sadece stogu olan urunler" />
|
||||
<q-space />
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="download"
|
||||
:label="priceListExportFormat === 'pdf' ? 'PDF Olustur' : 'Excel Olustur'"
|
||||
:loading="priceListExportLoading"
|
||||
@click="runPriceListExport"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select
|
||||
v-model="priceListUrunIlkGrubu"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
emit-value
|
||||
map-options
|
||||
:options="topUrunIlkGrubuOptions"
|
||||
:loading="Boolean(serverFilterLoading.urunIlkGrubu)"
|
||||
label="Urun Ilk Grubu"
|
||||
@filter="onTopFilterSearchUrunIlkGrubu"
|
||||
@update:model-value="onPriceListUrunIlkGrubuChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select
|
||||
v-model="priceListUrunAnaGrubu"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
multiple
|
||||
use-chips
|
||||
emit-value
|
||||
map-options
|
||||
:options="topUrunAnaGrubuOptions"
|
||||
:loading="Boolean(serverFilterLoading.urunAnaGrubu)"
|
||||
label="Urun Ana Grubu (max 3)"
|
||||
@filter="onTopFilterSearchUrunAnaGrubu"
|
||||
@update:model-value="onPriceListUrunAnaGrubuChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<q-select
|
||||
v-model="priceListUrunAltGrubu"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
multiple
|
||||
use-chips
|
||||
emit-value
|
||||
map-options
|
||||
:options="priceListUrunAltGrubuOptions"
|
||||
:loading="Boolean(serverFilterLoading.urunAltGrubu)"
|
||||
label="Urun Alt Grubu"
|
||||
@filter="onPriceListFilterSearchUrunAltGrubu"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-toggle v-model="priceListIncludeCost" label="Maliyet fiyati" />
|
||||
<q-toggle v-model="priceListIncludeBase" label="Taban fiyatlar (USD/TRY)" />
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="text-caption text-grey-8 q-mb-xs">USD seviyeleri</div>
|
||||
<q-option-group
|
||||
v-model="priceListUSDLevels"
|
||||
type="checkbox"
|
||||
dense
|
||||
:options="priceLevelOptionsUSD"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="text-caption text-grey-8 q-mb-xs">EUR seviyeleri</div>
|
||||
<q-option-group
|
||||
v-model="priceListEURLevels"
|
||||
type="checkbox"
|
||||
dense
|
||||
:options="priceLevelOptionsEUR"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="text-caption text-grey-8 q-mb-xs">TRY seviyeleri</div>
|
||||
<q-option-group
|
||||
v-model="priceListTRYLevels"
|
||||
type="checkbox"
|
||||
dense
|
||||
:options="priceLevelOptionsTRY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Iptal" v-close-popup />
|
||||
<q-btn color="primary" label="Uygula" @click="applyBulkUpdate" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { Notify } from 'quasar'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { Notify, useQuasar } from 'quasar'
|
||||
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||
import api, { download } from 'src/services/api'
|
||||
|
||||
const $q = useQuasar()
|
||||
const store = useProductPricingStore()
|
||||
const PAGE_LIMIT = 250
|
||||
const currentPage = ref(1)
|
||||
let reloadTimer = null
|
||||
|
||||
const GUIDANCE_MSG = "Calismak icin once Urun Ilk Grubu veya Urun Ana Grubu Secin ve GRUPLARI GETIR'e Basin."
|
||||
|
||||
const usdToTry = 38.25
|
||||
const eurToTry = 41.6
|
||||
const multipliers = [1, 1.03, 1.06, 1.09, 1.12, 1.15]
|
||||
@@ -558,7 +882,6 @@ const numberRangeFilterFields = ['stockQty']
|
||||
const dateRangeFilterFields = ['stockEntryDate', 'lastPricingDate']
|
||||
const valueFilterFields = [
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry',
|
||||
'usd1',
|
||||
@@ -615,6 +938,32 @@ const topUrunAnaGrubu = ref([])
|
||||
|
||||
const topUrunIlkGrubuOptions = computed(() => serverFilterOptionMap.value.urunIlkGrubu || [])
|
||||
const topUrunAnaGrubuOptions = computed(() => serverFilterOptionMap.value.urunAnaGrubu || [])
|
||||
const priceListUrunAltGrubuOptions = computed(() => serverFilterOptionMap.value.urunAltGrubu || [])
|
||||
|
||||
const priceLevelOptionsUSD = [
|
||||
{ label: 'USD 1', value: 1 },
|
||||
{ label: 'USD 2', value: 2 },
|
||||
{ label: 'USD 3', value: 3 },
|
||||
{ label: 'USD 4', value: 4 },
|
||||
{ label: 'USD 5', value: 5 },
|
||||
{ label: 'USD 6', value: 6 }
|
||||
]
|
||||
const priceLevelOptionsEUR = [
|
||||
{ label: 'EUR 1', value: 1 },
|
||||
{ label: 'EUR 2', value: 2 },
|
||||
{ label: 'EUR 3', value: 3 },
|
||||
{ label: 'EUR 4', value: 4 },
|
||||
{ label: 'EUR 5', value: 5 },
|
||||
{ label: 'EUR 6', value: 6 }
|
||||
]
|
||||
const priceLevelOptionsTRY = [
|
||||
{ label: 'TRY 1', value: 1 },
|
||||
{ label: 'TRY 2', value: 2 },
|
||||
{ label: 'TRY 3', value: 3 },
|
||||
{ label: 'TRY 4', value: 4 },
|
||||
{ label: 'TRY 5', value: 5 },
|
||||
{ label: 'TRY 6', value: 6 }
|
||||
]
|
||||
const canFetchByGroup = computed(() => {
|
||||
return Boolean(String(topUrunIlkGrubu.value || '').trim()) || (topUrunAnaGrubu.value?.length || 0) > 0
|
||||
})
|
||||
@@ -709,13 +1058,107 @@ function onTopUrunAnaGrubuChange () {
|
||||
applyTopGroupFiltersToColumnFilters()
|
||||
}
|
||||
|
||||
function onPriceListUrunIlkGrubuChange () {
|
||||
// cascade for export dialog
|
||||
priceListUrunAnaGrubu.value = []
|
||||
const ilk = String(priceListUrunIlkGrubu.value || '').trim()
|
||||
if (ilk) {
|
||||
// scope ana grubu options
|
||||
topUrunIlkGrubu.value = ilk
|
||||
void fetchServerFilterOptions('urunAnaGrubu', { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function onPriceListUrunAnaGrubuChange () {
|
||||
// enforce max 3
|
||||
const nextAna = Array.isArray(priceListUrunAnaGrubu.value) ? priceListUrunAnaGrubu.value.slice(0, 3) : []
|
||||
if (nextAna.length !== (priceListUrunAnaGrubu.value || []).length) priceListUrunAnaGrubu.value = nextAna
|
||||
}
|
||||
|
||||
function onPriceListFilterSearchUrunAltGrubu (val, update) {
|
||||
update(() => {
|
||||
columnFilterSearch.value = { ...columnFilterSearch.value, urunAltGrubu: String(val || '') }
|
||||
scheduleServerFilterOptionsFetch('urunAltGrubu')
|
||||
})
|
||||
}
|
||||
|
||||
function openPriceListExportDialog (format) {
|
||||
// format optional (default: pdf); dialog includes its own format selector.
|
||||
if (format === 'excel' || format === 'pdf') {
|
||||
priceListExportFormat.value = format
|
||||
} else {
|
||||
priceListExportFormat.value = 'pdf'
|
||||
}
|
||||
priceListExportLoading.value = false
|
||||
// default selections: mirror top group selections if present
|
||||
priceListUrunIlkGrubu.value = topUrunIlkGrubu.value
|
||||
priceListUrunAnaGrubu.value = Array.isArray(topUrunAnaGrubu.value) ? topUrunAnaGrubu.value.slice(0, 3) : []
|
||||
priceListUrunAltGrubu.value = []
|
||||
// preload alt group options
|
||||
void fetchServerFilterOptions('urunAltGrubu', { force: true })
|
||||
priceListExportDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function runPriceListExport () {
|
||||
priceListExportLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
in_stock_only: !!priceListInStockOnly.value,
|
||||
include_meta: true,
|
||||
include_cost: !!priceListIncludeCost.value,
|
||||
include_base: !!priceListIncludeBase.value,
|
||||
usd_levels: Array.isArray(priceListUSDLevels.value) ? priceListUSDLevels.value : [],
|
||||
eur_levels: Array.isArray(priceListEURLevels.value) ? priceListEURLevels.value : [],
|
||||
try_levels: Array.isArray(priceListTRYLevels.value) ? priceListTRYLevels.value : [],
|
||||
urun_ilk_grubu: String(priceListUrunIlkGrubu.value || '').trim() ? [String(priceListUrunIlkGrubu.value || '').trim()] : [],
|
||||
urun_ana_grubu: Array.isArray(priceListUrunAnaGrubu.value) ? priceListUrunAnaGrubu.value : [],
|
||||
urun_alt_grubu: Array.isArray(priceListUrunAltGrubu.value) ? priceListUrunAltGrubu.value : []
|
||||
}
|
||||
|
||||
const traceId = `ui-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
const url = priceListExportFormat.value === 'excel'
|
||||
? '/pricing/products/price-list/export-excel'
|
||||
: '/pricing/products/price-list/export-pdf'
|
||||
|
||||
const res = await api.request({
|
||||
method: 'POST',
|
||||
url,
|
||||
data: payload,
|
||||
responseType: 'blob',
|
||||
timeout: 0,
|
||||
headers: { 'X-Trace-ID': traceId }
|
||||
})
|
||||
const blob = res?.data instanceof Blob ? res.data : new Blob([res?.data || ''])
|
||||
const objUrl = URL.createObjectURL(blob)
|
||||
|
||||
if (priceListExportFormat.value === 'pdf') {
|
||||
window.open(objUrl, '_blank')
|
||||
setTimeout(() => URL.revokeObjectURL(objUrl), 120000)
|
||||
} else {
|
||||
const a = document.createElement('a')
|
||||
a.href = objUrl
|
||||
a.download = `baggi_guncel_fiyat_listesi_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(objUrl)
|
||||
}
|
||||
|
||||
priceListExportDialogOpen.value = false
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.parsedMessage || err?.message || 'Fiyat listesi olusturulamadi' })
|
||||
} finally {
|
||||
priceListExportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetGroupSelections () {
|
||||
topUrunIlkGrubu.value = null
|
||||
topUrunAnaGrubu.value = []
|
||||
applyTopGroupFiltersToColumnFilters()
|
||||
// Keep other local filters cleared too, so page is "clean render".
|
||||
store.rows = []
|
||||
store.error = 'Performans icin once Urun Ilk Grubu veya Urun Ana Grubu secin.'
|
||||
store.error = GUIDANCE_MSG
|
||||
store.totalCount = 0
|
||||
store.totalPages = 1
|
||||
store.page = 1
|
||||
@@ -749,6 +1192,8 @@ const headerFilterFieldSet = new Set([
|
||||
])
|
||||
|
||||
const mainTableRef = ref(null)
|
||||
const topScrollRef = ref(null)
|
||||
const topScrollInnerRef = ref(null)
|
||||
const tablePagination = ref({
|
||||
page: 1, // server-side paging var; q-table local paging kapali
|
||||
rowsPerPage: 0,
|
||||
@@ -756,16 +1201,37 @@ const tablePagination = ref({
|
||||
descending: true
|
||||
})
|
||||
const selectedMap = ref({})
|
||||
const bulkDialogOpen = ref(false)
|
||||
const bulkField = ref('expenseForBasePrice')
|
||||
const bulkValue = ref('')
|
||||
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||
const exportAllLoading = ref(false)
|
||||
const showSelectedOnly = ref(false)
|
||||
const leftDetailsExpanded = ref(true)
|
||||
const calcLoadingMap = ref({})
|
||||
const bulkCalcLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
const priceHistoryDialogOpen = ref(false)
|
||||
const priceHistoryRow = ref(null)
|
||||
const priceHistoryLoading = ref(false)
|
||||
const priceHistoryTab = ref('pg')
|
||||
const priceHistory = ref({ postgres: [], mssql: [] })
|
||||
const selectedPgIds = ref([])
|
||||
const selectedMssqlIds = ref([])
|
||||
|
||||
const priceListExportDialogOpen = ref(false)
|
||||
const priceListExportFormat = ref('pdf') // 'pdf' | 'excel'
|
||||
const priceListExportLoading = ref(false)
|
||||
const priceListInStockOnly = ref(true)
|
||||
const priceListUrunIlkGrubu = ref(null)
|
||||
const priceListUrunAnaGrubu = ref([])
|
||||
const priceListUrunAltGrubu = ref([])
|
||||
const priceListIncludeCost = ref(true)
|
||||
const priceListIncludeBase = ref(true)
|
||||
const priceListUSDLevels = ref([1, 2, 3, 4, 5, 6])
|
||||
const priceListEURLevels = ref([1, 2, 3, 4, 5, 6])
|
||||
const priceListTRYLevels = ref([1, 2, 3, 4, 5, 6])
|
||||
|
||||
const editableColumns = [
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry',
|
||||
'usd1',
|
||||
@@ -809,6 +1275,7 @@ const allColumns = [
|
||||
col('marka', 'MARKA', 'marka', 54, { sortable: true, classes: 'ps-col' }),
|
||||
col('productCode', 'URUN KODU', 'productCode', 108, { sortable: true, classes: 'ps-col product-code-col' }),
|
||||
col('calcAction', 'HESAPLA', 'calcAction', 72, { align: 'center', classes: 'ps-col' }),
|
||||
col('historyAction', '', 'historyAction', 40, { align: 'center', classes: 'ps-col text-center' }),
|
||||
col('stockQty', 'STOK ADET', 'stockQty', 72, { align: 'right', sortable: true, classes: 'ps-col stock-col' }),
|
||||
col('stockEntryDate', 'STOK GIRIS TARIHI', 'stockEntryDate', 92, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
col('lastCostingDate', 'SON MALIYETLENDIRME', 'lastCostingDate', 110, { align: 'center', sortable: true, classes: 'ps-col date-col' }),
|
||||
@@ -820,36 +1287,48 @@ const allColumns = [
|
||||
col('urunAltGrubu', 'URUN ALT GRUBU', 'urunAltGrubu', 66, { sortable: true, classes: 'ps-col' }),
|
||||
col('icerik', 'ICERIK', 'icerik', 62, { sortable: true, classes: 'ps-col' }),
|
||||
col('karisim', 'KARISIM', 'karisim', 62, { sortable: true, classes: 'ps-col' }),
|
||||
col('costPrice', 'MALIYET FIYATI', 'costPrice', 74, { align: 'right', sortable: true, classes: 'usd-col' }),
|
||||
col('expenseForBasePrice', 'TABAN FIYAT MASRAF', 'expenseForBasePrice', 86, { align: 'right', classes: 'usd-col' }),
|
||||
col('basePriceUsd', 'TABAN USD', 'basePriceUsd', 74, { align: 'right', classes: 'usd-col' }),
|
||||
col('basePriceTry', 'TABAN TRY', 'basePriceTry', 74, { align: 'right', classes: 'try-col' }),
|
||||
col('usd1', 'USD 1', 'usd1', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd2', 'USD 2', 'usd2', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd3', 'USD 3', 'usd3', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd4', 'USD 4', 'usd4', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd5', 'USD 5', 'usd5', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd6', 'USD 6', 'usd6', 62, { align: 'right', classes: 'usd-col' }),
|
||||
col('eur1', 'EUR 1', 'eur1', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur2', 'EUR 2', 'eur2', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur3', 'EUR 3', 'eur3', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur4', 'EUR 4', 'eur4', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur5', 'EUR 5', 'eur5', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur6', 'EUR 6', 'eur6', 62, { align: 'right', classes: 'eur-col' }),
|
||||
col('try1', 'TRY 1', 'try1', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try2', 'TRY 2', 'try2', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try3', 'TRY 3', 'try3', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try4', 'TRY 4', 'try4', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try5', 'TRY 5', 'try5', 62, { align: 'right', classes: 'try-col' }),
|
||||
col('try6', 'TRY 6', 'try6', 62, { align: 'right', classes: 'try-col' })
|
||||
col('costPrice', 'MALIYET FIYATI', 'costPrice', 88, { align: 'right', sortable: true, classes: 'usd-col' }),
|
||||
col('basePriceUsd', 'TABAN USD', 'basePriceUsd', 88, { align: 'right', classes: 'usd-col' }),
|
||||
col('basePriceTry', 'TABAN TRY', 'basePriceTry', 96, { align: 'right', classes: 'try-col' }),
|
||||
col('usd1', 'USD 1', 'usd1', 84, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd2', 'USD 2', 'usd2', 84, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd3', 'USD 3', 'usd3', 84, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd4', 'USD 4', 'usd4', 84, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd5', 'USD 5', 'usd5', 84, { align: 'right', classes: 'usd-col' }),
|
||||
col('usd6', 'USD 6', 'usd6', 84, { align: 'right', classes: 'usd-col' }),
|
||||
col('eur1', 'EUR 1', 'eur1', 84, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur2', 'EUR 2', 'eur2', 84, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur3', 'EUR 3', 'eur3', 84, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur4', 'EUR 4', 'eur4', 84, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur5', 'EUR 5', 'eur5', 84, { align: 'right', classes: 'eur-col' }),
|
||||
col('eur6', 'EUR 6', 'eur6', 84, { align: 'right', classes: 'eur-col' }),
|
||||
col('try1', 'TRY 1', 'try1', 96, { align: 'right', classes: 'try-col' }),
|
||||
col('try2', 'TRY 2', 'try2', 96, { align: 'right', classes: 'try-col' }),
|
||||
col('try3', 'TRY 3', 'try3', 96, { align: 'right', classes: 'try-col' }),
|
||||
col('try4', 'TRY 4', 'try4', 96, { align: 'right', classes: 'try-col' }),
|
||||
col('try5', 'TRY 5', 'try5', 96, { align: 'right', classes: 'try-col' }),
|
||||
col('try6', 'TRY 6', 'try6', 96, { align: 'right', classes: 'try-col' })
|
||||
]
|
||||
|
||||
const stickyColumnNames = [
|
||||
const hideableLeftDetailColumnNames = new Set([
|
||||
'stockEntryDate',
|
||||
'lastCostingDate',
|
||||
'lastPricingDate',
|
||||
'askiliYan',
|
||||
'kategori',
|
||||
'urunIlkGrubu',
|
||||
'urunAnaGrubu',
|
||||
'urunAltGrubu',
|
||||
'icerik',
|
||||
'karisim'
|
||||
])
|
||||
const stickyColumnNamesBase = [
|
||||
'select',
|
||||
'brandGroupSelection',
|
||||
'marka',
|
||||
'productCode',
|
||||
'calcAction',
|
||||
'historyAction',
|
||||
'stockQty',
|
||||
'stockEntryDate',
|
||||
'lastPricingDate',
|
||||
@@ -861,12 +1340,10 @@ const stickyColumnNames = [
|
||||
'icerik',
|
||||
'karisim',
|
||||
'costPrice',
|
||||
'expenseForBasePrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry'
|
||||
]
|
||||
const stickyBoundaryColumnName = 'basePriceTry'
|
||||
const stickyColumnNameSet = new Set(stickyColumnNames)
|
||||
|
||||
const visibleColumns = computed(() => {
|
||||
const selected = new Set(selectedCurrencies.value)
|
||||
@@ -874,16 +1351,61 @@ const visibleColumns = computed(() => {
|
||||
if (c.name.startsWith('usd')) return selected.has('USD')
|
||||
if (c.name.startsWith('eur')) return selected.has('EUR')
|
||||
if (c.name.startsWith('try')) return selected.has('TRY')
|
||||
if (!leftDetailsExpanded.value && hideableLeftDetailColumnNames.has(c.name)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const exportableColumns = computed(() => visibleColumns.value.filter((col) => col.name !== 'select' && col.name !== 'calcAction'))
|
||||
const stickyColumnNames = computed(() => {
|
||||
const visibleNameSet = new Set(visibleColumns.value.map((col) => col.name))
|
||||
return stickyColumnNamesBase.filter((name) => visibleNameSet.has(name))
|
||||
})
|
||||
const stickyColumnNameSet = computed(() => new Set(stickyColumnNames.value))
|
||||
|
||||
const exportableColumns = computed(() => visibleColumns.value.filter((col) => col.name !== 'select' && col.name !== 'calcAction' && col.name !== 'historyAction'))
|
||||
|
||||
const pgHistoryGroups = computed(() => {
|
||||
const list = Array.isArray(priceHistory.value?.postgres) ? priceHistory.value.postgres : []
|
||||
const map = new Map()
|
||||
for (const r of list) {
|
||||
const currency = String(r?.currency || '').toUpperCase().trim()
|
||||
const levelNo = Number(r?.level_no || 0)
|
||||
if (!currency || !(levelNo >= 1 && levelNo <= 6)) continue
|
||||
const key = `${currency}|${levelNo}`
|
||||
if (!map.has(key)) map.set(key, { key, currency, levelNo, rows: [] })
|
||||
map.get(key).rows.push(r)
|
||||
}
|
||||
const out = Array.from(map.values())
|
||||
for (const g of out) g.latest = g.rows?.[0] || null
|
||||
out.sort((a, b) => (a.currency + a.levelNo).localeCompare(b.currency + b.levelNo))
|
||||
return out
|
||||
})
|
||||
|
||||
const mssqlHistoryGroups = computed(() => {
|
||||
const list = Array.isArray(priceHistory.value?.mssql) ? priceHistory.value.mssql : []
|
||||
const map = new Map()
|
||||
for (const r of list) {
|
||||
const currency = String(r?.currency || '').toUpperCase().trim()
|
||||
const pgc = String(r?.price_group_code || '').trim()
|
||||
if (!currency || !pgc) continue
|
||||
const key = `${currency}|${pgc}`
|
||||
if (!map.has(key)) map.set(key, { key, currency, price_group_code: pgc, rows: [] })
|
||||
map.get(key).rows.push(r)
|
||||
}
|
||||
const out = Array.from(map.values())
|
||||
for (const g of out) g.latest = g.rows?.[0] || null
|
||||
out.sort((a, b) => (a.currency + a.price_group_code).localeCompare(b.currency + b.price_group_code))
|
||||
return out
|
||||
})
|
||||
|
||||
const selectedPgIdSet = computed(() => new Set(selectedPgIds.value || []))
|
||||
const selectedMssqlIdSet = computed(() => new Set(selectedMssqlIds.value || []))
|
||||
const selectedHistoryCount = computed(() => (selectedPgIds.value?.length || 0) + (selectedMssqlIds.value?.length || 0))
|
||||
|
||||
const stickyLeftMap = computed(() => {
|
||||
const map = {}
|
||||
let left = 0
|
||||
for (const colName of stickyColumnNames) {
|
||||
for (const colName of stickyColumnNames.value) {
|
||||
const c = allColumns.find((x) => x.name === colName)
|
||||
if (!c) continue
|
||||
map[colName] = left
|
||||
@@ -893,9 +1415,8 @@ const stickyLeftMap = computed(() => {
|
||||
})
|
||||
const stickyScrollComp = computed(() => {
|
||||
const boundaryCol = allColumns.find((x) => x.name === stickyBoundaryColumnName)
|
||||
return (stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundaryCol?.style)
|
||||
return ((stickyLeftMap.value[stickyBoundaryColumnName] || 0) + extractWidth(boundaryCol?.style)) * 1.2
|
||||
})
|
||||
|
||||
const tableMinWidth = computed(() => visibleColumns.value.reduce((sum, c) => sum + extractWidth(c.style), 0))
|
||||
const tableStyle = computed(() => ({
|
||||
width: `${tableMinWidth.value}px`,
|
||||
@@ -905,16 +1426,9 @@ const tableStyle = computed(() => ({
|
||||
|
||||
const rows = computed(() => store.rows || [])
|
||||
const tableLoading = computed(() => Boolean(store.loading) && rows.value.length === 0)
|
||||
const bulkFieldOptions = computed(() => {
|
||||
return editableColumns
|
||||
.map((name) => {
|
||||
const colDef = allColumns.find((c) => c.field === name)
|
||||
return {
|
||||
value: name,
|
||||
label: colDef?.label || name
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const isGuidanceState = computed(() => String(store.error || '').trim() === GUIDANCE_MSG)
|
||||
const showGuidanceOverlay = computed(() => isGuidanceState.value && !store.loading && rows.value.length === 0)
|
||||
const multiFilterOptionMap = computed(() => {
|
||||
const map = {}
|
||||
multiFilterColumns.forEach(({ field }) => {
|
||||
@@ -1136,7 +1650,7 @@ function extractWidth (style) {
|
||||
}
|
||||
|
||||
function isStickyCol (colName) {
|
||||
return stickyColumnNameSet.has(colName)
|
||||
return stickyColumnNameSet.value.has(colName)
|
||||
}
|
||||
|
||||
function isStickyBoundary (colName) {
|
||||
@@ -1226,6 +1740,28 @@ function formatDateDisplay (val) {
|
||||
return `${day}.${month}.${year}`
|
||||
}
|
||||
|
||||
function formatMoney (v) {
|
||||
const n = Number(v ?? 0)
|
||||
if (!Number.isFinite(n)) return '-'
|
||||
return n.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 6 })
|
||||
}
|
||||
|
||||
function formatMssqlStamp (row) {
|
||||
if (!row) return '-'
|
||||
const vd = String(row?.valid_date || '').trim()
|
||||
const vt = String(row?.valid_time || '').trim()
|
||||
const lud = String(row?.last_updated_date || '').trim()
|
||||
const parts = []
|
||||
if (vd) parts.push(vd)
|
||||
if (vt) parts.push(vt)
|
||||
if (lud) parts.push(`upd:${lud}`)
|
||||
return parts.length ? parts.join(' ') : '-'
|
||||
}
|
||||
|
||||
function getOriginalCellValue (row, field) {
|
||||
return row?.[`__orig_${field}`] ?? row?.[field] ?? 0
|
||||
}
|
||||
|
||||
function exportCellValue (row, field) {
|
||||
if (field === 'stockQty') return formatStock(row?.[field])
|
||||
if (field === 'stockEntryDate' || field === 'lastCostingDate' || field === 'lastPricingDate') return formatDateDisplay(row?.[field])
|
||||
@@ -1337,24 +1873,252 @@ function needsCosting (row) {
|
||||
}
|
||||
|
||||
function recalcByBasePrice (row) {
|
||||
row.basePriceTry = round2((row.basePriceUsd * usdToTry) + row.expenseForBasePrice)
|
||||
row.basePriceTry = round2(row.basePriceUsd * usdToTry)
|
||||
let prevUsd = row.basePriceUsd
|
||||
let prevTry = row.basePriceTry
|
||||
let prevEur = round2(row.basePriceUsd * usdToTry / eurToTry)
|
||||
multipliers.forEach((multiplier, index) => {
|
||||
row[`usd${index + 1}`] = round2(row.basePriceUsd * multiplier)
|
||||
row[`eur${index + 1}`] = round2((row.basePriceUsd * usdToTry * multiplier) / eurToTry)
|
||||
row[`try${index + 1}`] = round2(row.basePriceTry * multiplier)
|
||||
const nextUsd = round2(prevUsd * multiplier)
|
||||
const nextTry = round2(prevTry * multiplier)
|
||||
const nextEur = round2(prevEur * multiplier)
|
||||
row[`usd${index + 1}`] = nextUsd
|
||||
row[`eur${index + 1}`] = nextEur
|
||||
row[`try${index + 1}`] = nextTry
|
||||
prevUsd = nextUsd
|
||||
prevTry = nextTry
|
||||
prevEur = nextEur
|
||||
})
|
||||
}
|
||||
|
||||
function onEditableCellChange (row, field, val) {
|
||||
const parsed = parseNumber(val)
|
||||
store.updateCell(row, field, parsed)
|
||||
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') recalcByBasePrice(row)
|
||||
if (field === 'basePriceUsd') recalcByBasePrice(row)
|
||||
}
|
||||
|
||||
function calculateRow (row) {
|
||||
if (!row) return
|
||||
recalcByBasePrice(row)
|
||||
toggleRowSelection(rowSelectionKey(row), true)
|
||||
function setCalcLoading (productCode, value) {
|
||||
calcLoadingMap.value = {
|
||||
...calcLoadingMap.value,
|
||||
[productCode]: !!value
|
||||
}
|
||||
}
|
||||
|
||||
function applyPreviewRowToUiRow (row, preview) {
|
||||
row.basePriceUsd = round2(preview?.base_price_usd)
|
||||
row.basePriceTry = round2(preview?.base_price_try)
|
||||
row.usd1 = round2(preview?.usd1)
|
||||
row.usd2 = round2(preview?.usd2)
|
||||
row.usd3 = round2(preview?.usd3)
|
||||
row.usd4 = round2(preview?.usd4)
|
||||
row.usd5 = round2(preview?.usd5)
|
||||
row.usd6 = round2(preview?.usd6)
|
||||
row.eur1 = round2(preview?.eur1)
|
||||
row.eur2 = round2(preview?.eur2)
|
||||
row.eur3 = round2(preview?.eur3)
|
||||
row.eur4 = round2(preview?.eur4)
|
||||
row.eur5 = round2(preview?.eur5)
|
||||
row.eur6 = round2(preview?.eur6)
|
||||
row.try1 = round2(preview?.try1)
|
||||
row.try2 = round2(preview?.try2)
|
||||
row.try3 = round2(preview?.try3)
|
||||
row.try4 = round2(preview?.try4)
|
||||
row.try5 = round2(preview?.try5)
|
||||
row.try6 = round2(preview?.try6)
|
||||
}
|
||||
|
||||
async function calculateRow (row) {
|
||||
if (!row?.productCode) return
|
||||
const productCode = String(row.productCode).trim()
|
||||
if (!productCode) return
|
||||
|
||||
setCalcLoading(productCode, true)
|
||||
console.info('[product-pricing][ui] calc-row:start', { product_code: productCode })
|
||||
try {
|
||||
const res = await api.post('/pricing/products/calculate-snapshots', {
|
||||
preview_only: true,
|
||||
product_codes: [productCode]
|
||||
}, {
|
||||
timeout: 180000
|
||||
})
|
||||
const list = Array.isArray(res?.data?.rows) ? res.data.rows : []
|
||||
const preview = list.find((item) => String(item?.product_code || '').trim() === productCode)
|
||||
if (!preview) {
|
||||
Notify.create({ type: 'warning', message: 'Bu urun icin hesap sonucu donmedi.' })
|
||||
return
|
||||
}
|
||||
applyPreviewRowToUiRow(row, preview)
|
||||
toggleRowSelection(rowSelectionKey(row), true)
|
||||
console.info('[product-pricing][ui] calc-row:done', { product_code: productCode })
|
||||
} catch (err) {
|
||||
console.error('[product-pricing][ui] calc-row:error', {
|
||||
product_code: productCode,
|
||||
status: err?.response?.status ?? null,
|
||||
message: err?.response?.data || err?.message || 'calc-row failed'
|
||||
})
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Hesaplama onizlemesi alinamadi' })
|
||||
} finally {
|
||||
setCalcLoading(productCode, false)
|
||||
}
|
||||
}
|
||||
|
||||
async function openPriceHistoryDialog (row) {
|
||||
if (!row?.productCode) return
|
||||
priceHistoryRow.value = row
|
||||
priceHistoryTab.value = 'pg'
|
||||
priceHistory.value = { postgres: [], mssql: [] }
|
||||
selectedPgIds.value = []
|
||||
selectedMssqlIds.value = []
|
||||
priceHistoryDialogOpen.value = true
|
||||
await reloadPriceHistory()
|
||||
}
|
||||
|
||||
async function reloadPriceHistory () {
|
||||
const code = String(priceHistoryRow.value?.productCode || '').trim()
|
||||
if (!code) return
|
||||
priceHistoryLoading.value = true
|
||||
try {
|
||||
const traceId = `ui-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
url: `/pricing/products/${encodeURIComponent(code)}/price-history`,
|
||||
timeout: 180000,
|
||||
headers: { 'X-Trace-ID': traceId }
|
||||
})
|
||||
priceHistory.value = {
|
||||
postgres: Array.isArray(res?.data?.postgres) ? res.data.postgres : [],
|
||||
mssql: Array.isArray(res?.data?.mssql) ? res.data.mssql : []
|
||||
}
|
||||
// keep selection but drop ids that no longer exist
|
||||
const pgSet = new Set(priceHistory.value.postgres.map((r) => String(r?.id || '').trim()).filter(Boolean))
|
||||
const msSet = new Set(priceHistory.value.mssql.map((r) => String(r?.price_list_line_id || '').trim()).filter(Boolean))
|
||||
selectedPgIds.value = (selectedPgIds.value || []).filter((id) => pgSet.has(id))
|
||||
selectedMssqlIds.value = (selectedMssqlIds.value || []).filter((id) => msSet.has(id))
|
||||
} catch (err) {
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Fiyat gecmisi yuklenemedi' })
|
||||
} finally {
|
||||
priceHistoryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectedPgId (id, val) {
|
||||
const sid = String(id || '').trim()
|
||||
if (!sid) return
|
||||
const set = new Set(selectedPgIds.value || [])
|
||||
if (val) set.add(sid)
|
||||
else set.delete(sid)
|
||||
selectedPgIds.value = Array.from(set)
|
||||
}
|
||||
|
||||
function toggleSelectedMssqlId (id, val) {
|
||||
const sid = String(id || '').trim()
|
||||
if (!sid) return
|
||||
const set = new Set(selectedMssqlIds.value || [])
|
||||
if (val) set.add(sid)
|
||||
else set.delete(sid)
|
||||
selectedMssqlIds.value = Array.from(set)
|
||||
}
|
||||
|
||||
async function confirmDeleteSelectedHistory () {
|
||||
const code = String(priceHistoryRow.value?.productCode || '').trim()
|
||||
if (!code) return
|
||||
|
||||
const pgCount = selectedPgIds.value?.length || 0
|
||||
const msCount = selectedMssqlIds.value?.length || 0
|
||||
if (pgCount + msCount === 0) return
|
||||
|
||||
await $q.dialog({
|
||||
title: 'Secilenleri Sil',
|
||||
message: `Secili kayitlari silmek istiyor musunuz? (B2B/B2C: ${pgCount}, NEBIM_V3: ${msCount})`,
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
ok: { label: 'Sil', color: 'negative' },
|
||||
cancel: { label: 'Vazgec', color: 'grey-7', flat: true }
|
||||
}).onOk(async () => {
|
||||
const traceId = `ui-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
const payload = {
|
||||
pg_ids: (selectedPgIds.value || []).map((x) => String(x || '').trim()).filter(Boolean),
|
||||
mssql_ids: (selectedMssqlIds.value || []).map((x) => String(x || '').trim()).filter(Boolean)
|
||||
}
|
||||
await api.request({
|
||||
method: 'POST',
|
||||
url: `/pricing/products/${encodeURIComponent(code)}/price-history/delete-selected`,
|
||||
data: payload,
|
||||
timeout: 180000,
|
||||
headers: { 'X-Trace-ID': traceId }
|
||||
})
|
||||
Notify.create({ type: 'positive', message: 'Secilen kayitlar silindi.' })
|
||||
selectedPgIds.value = []
|
||||
selectedMssqlIds.value = []
|
||||
await reloadPriceHistory()
|
||||
await reloadData({ page: currentPage.value })
|
||||
})
|
||||
}
|
||||
|
||||
let tableMiddleScrollEl = null
|
||||
let horizontalResizeObserver = null
|
||||
let syncingTopScroll = false
|
||||
|
||||
function getTableMiddleScrollEl () {
|
||||
return mainTableRef.value?.$el?.querySelector('.q-table__middle') || null
|
||||
}
|
||||
|
||||
function syncTopScrollWidth () {
|
||||
const top = topScrollRef.value
|
||||
const inner = topScrollInnerRef.value
|
||||
const middle = getTableMiddleScrollEl()
|
||||
if (!top || !inner || !middle) return
|
||||
const scrollWidth = Math.max(middle.scrollWidth, tableMinWidth.value, top.clientWidth)
|
||||
inner.style.width = `${scrollWidth}px`
|
||||
if (top.scrollLeft !== middle.scrollLeft) {
|
||||
top.scrollLeft = middle.scrollLeft
|
||||
}
|
||||
}
|
||||
|
||||
function onTopScroll () {
|
||||
const top = topScrollRef.value
|
||||
const middle = getTableMiddleScrollEl()
|
||||
if (!top || !middle || syncingTopScroll) return
|
||||
syncingTopScroll = true
|
||||
middle.scrollLeft = top.scrollLeft
|
||||
requestAnimationFrame(() => {
|
||||
syncingTopScroll = false
|
||||
})
|
||||
}
|
||||
|
||||
function onTableMiddleScroll () {
|
||||
const top = topScrollRef.value
|
||||
const middle = getTableMiddleScrollEl()
|
||||
if (!top || !middle || syncingTopScroll) return
|
||||
syncingTopScroll = true
|
||||
top.scrollLeft = middle.scrollLeft
|
||||
requestAnimationFrame(() => {
|
||||
syncingTopScroll = false
|
||||
})
|
||||
}
|
||||
|
||||
async function bindHorizontalScrollSync () {
|
||||
await nextTick()
|
||||
const middle = getTableMiddleScrollEl()
|
||||
if (tableMiddleScrollEl && tableMiddleScrollEl !== middle) {
|
||||
tableMiddleScrollEl.removeEventListener('scroll', onTableMiddleScroll)
|
||||
}
|
||||
tableMiddleScrollEl = middle
|
||||
if (tableMiddleScrollEl) {
|
||||
tableMiddleScrollEl.removeEventListener('scroll', onTableMiddleScroll)
|
||||
tableMiddleScrollEl.addEventListener('scroll', onTableMiddleScroll, { passive: true })
|
||||
}
|
||||
if (horizontalResizeObserver) {
|
||||
horizontalResizeObserver.disconnect()
|
||||
horizontalResizeObserver = null
|
||||
}
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
horizontalResizeObserver = new ResizeObserver(() => {
|
||||
syncTopScrollWidth()
|
||||
})
|
||||
if (topScrollRef.value) horizontalResizeObserver.observe(topScrollRef.value)
|
||||
if (tableMiddleScrollEl) horizontalResizeObserver.observe(tableMiddleScrollEl)
|
||||
}
|
||||
syncTopScrollWidth()
|
||||
}
|
||||
|
||||
function onBrandGroupSelectionChange (row, val) {
|
||||
@@ -1362,7 +2126,35 @@ function onBrandGroupSelectionChange (row, val) {
|
||||
}
|
||||
|
||||
function isRowSelected (rowKey) {
|
||||
return !!selectedMap.value[rowKey]
|
||||
const k = String(rowKey ?? '').trim()
|
||||
if (!k) return false
|
||||
return !!selectedMap.value[k]
|
||||
}
|
||||
|
||||
const selectedToneColumnNameSet = new Set([
|
||||
// "Karisim"e kadar olan sol kolonlar (fiyat kolonlarini boyamayalim)
|
||||
'brandGroupSelection',
|
||||
'marka',
|
||||
'productCode',
|
||||
'stockQty',
|
||||
'stockEntryDate',
|
||||
'lastCostingDate',
|
||||
'lastPricingDate',
|
||||
'askiliYan',
|
||||
'kategori',
|
||||
'urunIlkGrubu',
|
||||
'urunAnaGrubu',
|
||||
'urunAltGrubu',
|
||||
'icerik',
|
||||
'karisim'
|
||||
])
|
||||
|
||||
function shouldToneSelectedCell (row, colName) {
|
||||
if (!selectedToneColumnNameSet.has(String(colName || '').trim())) return false
|
||||
if (!isRowSelected(rowSelectionKey(row))) return false
|
||||
// don't override critical warning coloring
|
||||
if (String(colName || '').trim() === 'lastCostingDate' && needsCosting(row)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function onRowCheckboxChange (row, val) {
|
||||
@@ -1371,7 +2163,142 @@ function onRowCheckboxChange (row, val) {
|
||||
}
|
||||
|
||||
function toggleRowSelection (rowKey, val) {
|
||||
selectedMap.value = { ...selectedMap.value, [rowKey]: !!val }
|
||||
const k = String(rowKey ?? '').trim()
|
||||
if (!k) return
|
||||
selectedMap.value = { ...selectedMap.value, [k]: !!val }
|
||||
}
|
||||
|
||||
function isRowDirty (row) {
|
||||
if (!row) return false
|
||||
const fields = [
|
||||
'basePriceUsd',
|
||||
'basePriceTry',
|
||||
'usd1', 'usd2', 'usd3', 'usd4', 'usd5', 'usd6',
|
||||
'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6',
|
||||
'try1', 'try2', 'try3', 'try4', 'try5', 'try6'
|
||||
]
|
||||
for (const f of fields) {
|
||||
const cur = Number(row?.[f] ?? 0)
|
||||
const orig = Number(row?.[`__orig_${f}`] ?? 0)
|
||||
if (Math.abs(cur - orig) > 1e-9) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const selectedRows = computed(() => {
|
||||
const map = selectedMap.value || {}
|
||||
return rows.value.filter((r) => !!map[rowSelectionKey(r)])
|
||||
})
|
||||
|
||||
const selectedDirtyRows = computed(() => selectedRows.value.filter(isRowDirty))
|
||||
const selectedDirtyCount = computed(() => selectedDirtyRows.value.length)
|
||||
const saveButtonLabel = computed(() => {
|
||||
if (selectedDirtyCount.value > 0) return `Kaydet (${selectedDirtyCount.value})`
|
||||
return 'Kaydet'
|
||||
})
|
||||
|
||||
async function calculateSelectedRows () {
|
||||
const list = selectedRows.value
|
||||
if (!Array.isArray(list) || list.length === 0) return
|
||||
const productCodes = list
|
||||
.map((r) => String(r?.productCode || '').trim())
|
||||
.filter(Boolean)
|
||||
if (productCodes.length === 0) return
|
||||
|
||||
bulkCalcLoading.value = true
|
||||
console.info('[product-pricing][ui] bulk-calc:start', { selected: productCodes.length })
|
||||
try {
|
||||
const res = await api.post('/pricing/products/calculate-snapshots', {
|
||||
preview_only: true,
|
||||
product_codes: productCodes
|
||||
}, {
|
||||
timeout: 180000
|
||||
})
|
||||
const previewRows = Array.isArray(res?.data?.rows) ? res.data.rows : []
|
||||
const byCode = new Map(previewRows.map((p) => [String(p?.product_code || '').trim(), p]))
|
||||
let applied = 0
|
||||
for (const row of rows.value) {
|
||||
const code = String(row?.productCode || '').trim()
|
||||
if (!code) continue
|
||||
if (!selectedMap.value?.[rowSelectionKey(row)]) continue
|
||||
const p = byCode.get(code)
|
||||
if (!p) continue
|
||||
applyPreviewRowToUiRow(row, p)
|
||||
applied++
|
||||
}
|
||||
Notify.create({ type: 'positive', message: `Hesaplandi: ${applied} / ${productCodes.length}` })
|
||||
console.info('[product-pricing][ui] bulk-calc:done', { applied, selected: productCodes.length })
|
||||
} catch (err) {
|
||||
console.error('[product-pricing][ui] bulk-calc:error', {
|
||||
status: err?.response?.status ?? null,
|
||||
message: err?.response?.data || err?.message || 'bulk-calc failed'
|
||||
})
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Toplu hesaplama basarisiz' })
|
||||
} finally {
|
||||
bulkCalcLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSelectedRows () {
|
||||
const list = selectedDirtyRows.value
|
||||
if (!Array.isArray(list) || list.length === 0) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const traceId = `ui-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
console.info('[product-pricing][ui] save:start', { trace_id: traceId, dirty_count: list.length })
|
||||
const payload = {
|
||||
items: list.map((r) => ({
|
||||
product_code: String(r?.productCode || '').trim(),
|
||||
base_price_usd: Number(r?.basePriceUsd ?? 0),
|
||||
base_price_try: Number(r?.basePriceTry ?? 0),
|
||||
usd1: Number(r?.usd1 ?? 0),
|
||||
usd2: Number(r?.usd2 ?? 0),
|
||||
usd3: Number(r?.usd3 ?? 0),
|
||||
usd4: Number(r?.usd4 ?? 0),
|
||||
usd5: Number(r?.usd5 ?? 0),
|
||||
usd6: Number(r?.usd6 ?? 0),
|
||||
eur1: Number(r?.eur1 ?? 0),
|
||||
eur2: Number(r?.eur2 ?? 0),
|
||||
eur3: Number(r?.eur3 ?? 0),
|
||||
eur4: Number(r?.eur4 ?? 0),
|
||||
eur5: Number(r?.eur5 ?? 0),
|
||||
eur6: Number(r?.eur6 ?? 0),
|
||||
try1: Number(r?.try1 ?? 0),
|
||||
try2: Number(r?.try2 ?? 0),
|
||||
try3: Number(r?.try3 ?? 0),
|
||||
try4: Number(r?.try4 ?? 0),
|
||||
try5: Number(r?.try5 ?? 0),
|
||||
try6: Number(r?.try6 ?? 0)
|
||||
}))
|
||||
}
|
||||
|
||||
await api.request({
|
||||
method: 'POST',
|
||||
url: '/pricing/products/save',
|
||||
data: payload,
|
||||
timeout: 0,
|
||||
headers: { 'X-Trace-ID': traceId }
|
||||
})
|
||||
|
||||
Notify.create({ type: 'positive', message: `Kaydedildi: ${list.length}` })
|
||||
console.info('[product-pricing][ui] save:done', { trace_id: traceId, dirty_count: list.length })
|
||||
|
||||
// After persisting, clear selection state and reload from backend.
|
||||
// This avoids "Kaydet(1) but checkbox not ticked" confusion and ensures UI reflects DB.
|
||||
selectedMap.value = {}
|
||||
showSelectedOnly.value = false
|
||||
await reloadData({ page: currentPage.value, useCache: false })
|
||||
} catch (err) {
|
||||
console.error('[product-pricing][ui] save:error', {
|
||||
status: err?.response?.status ?? null,
|
||||
trace_id: err?.response?.headers?.['x-trace-id'] || null,
|
||||
message: err?.response?.data || err?.message || 'save failed'
|
||||
})
|
||||
Notify.create({ type: 'negative', message: err?.response?.data || err?.message || 'Kaydedilemedi' })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAllVisible (val) {
|
||||
@@ -1380,20 +2307,6 @@ function toggleSelectAllVisible (val) {
|
||||
selectedMap.value = next
|
||||
}
|
||||
|
||||
function applyBulkUpdate () {
|
||||
const field = String(bulkField.value || '').trim()
|
||||
if (!field || !editableColumnSet.has(field)) return
|
||||
const parsed = parseNumber(bulkValue.value)
|
||||
rows.value.forEach((row) => {
|
||||
if (!isRowSelected(rowSelectionKey(row))) return
|
||||
store.updateCell(row, field, parsed)
|
||||
if (field === 'expenseForBasePrice' || field === 'basePriceUsd') {
|
||||
recalcByBasePrice(row)
|
||||
}
|
||||
})
|
||||
bulkDialogOpen.value = false
|
||||
}
|
||||
|
||||
function resetAll () {
|
||||
columnFilters.value = {
|
||||
productCode: [],
|
||||
@@ -1500,7 +2413,7 @@ function scheduleReload () {
|
||||
}, 180)
|
||||
}
|
||||
|
||||
async function fetchChunk ({ page = 1 } = {}) {
|
||||
async function fetchChunk ({ page = 1, useCache = true } = {}) {
|
||||
const filters = buildServerFilters()
|
||||
const hasAnyFilter = Object.values(filters).some((v) => Array.isArray(v) && v.length > 0)
|
||||
const hasPrimaryFilter = (filters.urun_ilk_grubu?.length || 0) > 0 || (filters.urun_ana_grubu?.length || 0) > 0
|
||||
@@ -1516,7 +2429,7 @@ async function fetchChunk ({ page = 1 } = {}) {
|
||||
}
|
||||
if (!hasPrimaryFilter) {
|
||||
store.rows = []
|
||||
store.error = 'Performans icin once Urun Ilk Grubu veya Urun Ana Grubu secin.'
|
||||
store.error = GUIDANCE_MSG
|
||||
store.totalCount = 0
|
||||
store.totalPages = 1
|
||||
store.page = 1
|
||||
@@ -1528,6 +2441,7 @@ async function fetchChunk ({ page = 1 } = {}) {
|
||||
page,
|
||||
append: false,
|
||||
silent: false,
|
||||
useCache,
|
||||
filters,
|
||||
sortBy: tablePagination.value.sortBy,
|
||||
descending: tablePagination.value.descending
|
||||
@@ -1536,13 +2450,13 @@ async function fetchChunk ({ page = 1 } = {}) {
|
||||
return Number(result?.fetched) || 0
|
||||
}
|
||||
|
||||
async function reloadData ({ page = 1 } = {}) {
|
||||
async function reloadData ({ page = 1, useCache = true } = {}) {
|
||||
const startedAt = Date.now()
|
||||
console.info('[product-pricing][ui] reload:start', {
|
||||
at: new Date(startedAt).toISOString()
|
||||
})
|
||||
try {
|
||||
await fetchChunk({ page })
|
||||
await fetchChunk({ page, useCache })
|
||||
} catch (err) {
|
||||
console.error('[product-pricing][ui] reload:error', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
@@ -1554,6 +2468,7 @@ async function reloadData ({ page = 1 } = {}) {
|
||||
row_count: Array.isArray(store.rows) ? store.rows.length : 0,
|
||||
has_error: Boolean(store.error)
|
||||
})
|
||||
await bindHorizontalScrollSync()
|
||||
}
|
||||
|
||||
// Full "fetch all pages" is intentionally avoided; keep server-side paging for performance.
|
||||
@@ -1572,18 +2487,34 @@ onMounted(async () => {
|
||||
void fetchServerFilterOptions('urunAnaGrubu')
|
||||
// Do not auto-fetch listing on mount; user must scope by group first.
|
||||
store.rows = []
|
||||
store.error = 'Performans icin once Urun Ilk Grubu veya Urun Ana Grubu secin.'
|
||||
store.error = GUIDANCE_MSG
|
||||
store.totalCount = 0
|
||||
store.totalPages = 1
|
||||
store.page = 1
|
||||
store.hasMore = false
|
||||
await bindHorizontalScrollSync()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [tableMinWidth.value, rows.value.length, selectedCurrencies.value.join(',')],
|
||||
() => {
|
||||
void bindHorizontalScrollSync()
|
||||
}
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (reloadTimer) {
|
||||
clearTimeout(reloadTimer)
|
||||
reloadTimer = null
|
||||
}
|
||||
if (tableMiddleScrollEl) {
|
||||
tableMiddleScrollEl.removeEventListener('scroll', onTableMiddleScroll)
|
||||
tableMiddleScrollEl = null
|
||||
}
|
||||
if (horizontalResizeObserver) {
|
||||
horizontalResizeObserver.disconnect()
|
||||
horizontalResizeObserver = null
|
||||
}
|
||||
})
|
||||
|
||||
// NOTE: Listing fetch is intentionally manual via "Gruplari Getir" for performance.
|
||||
@@ -1595,6 +2526,7 @@ onBeforeUnmount(() => {
|
||||
--pricing-header-height: 72px;
|
||||
--pricing-table-height: calc(100vh - 210px);
|
||||
|
||||
position: relative;
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1608,16 +2540,96 @@ onBeforeUnmount(() => {
|
||||
.top-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.top-actions-row {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.top-actions-row--filters {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.top-actions-row--actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* paging group is inside actions row now */
|
||||
|
||||
.toolbar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.toolbar-group--paging {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toolbar-group--paging :deep(.q-pagination) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn__content) {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn--dense .q-btn__content) {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn__content .q-icon) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn .q-icon) {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn__content span) {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn) {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn__content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toolbar-group :deep(.q-btn__wrapper) {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1240px) {
|
||||
.top-actions-row--filters,
|
||||
.top-actions-row--actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
@@ -1627,15 +2639,65 @@ onBeforeUnmount(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.empty-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.empty-overlay-inner {
|
||||
width: min(720px, 100%);
|
||||
border: 1px dashed rgba(0, 0, 0, 0.18);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
padding: 16px 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.price-history-card {
|
||||
width: 980px;
|
||||
max-width: 95vw;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.price-history-card :deep(.q-card__section) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.price-history-card :deep(.q-tab-panels) {
|
||||
max-height: 62vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.top-x-scroll {
|
||||
flex: 0 0 14px;
|
||||
height: 14px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.top-x-scroll-inner {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.pane-table {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.q-table__middle) {
|
||||
height: var(--pricing-table-height);
|
||||
min-height: var(--pricing-table-height);
|
||||
max-height: var(--pricing-table-height);
|
||||
height: calc(var(--pricing-table-height) - 14px);
|
||||
min-height: calc(var(--pricing-table-height) - 14px);
|
||||
max-height: calc(var(--pricing-table-height) - 14px);
|
||||
overflow: auto !important;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
overscroll-behavior: contain;
|
||||
@@ -1661,7 +2723,7 @@ onBeforeUnmount(() => {
|
||||
.pricing-table :deep(th),
|
||||
.pricing-table :deep(td) {
|
||||
box-sizing: border-box;
|
||||
padding: 0 4px;
|
||||
padding: 0 1px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1681,7 +2743,7 @@ onBeforeUnmount(() => {
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
padding: 0 4px !important;
|
||||
padding: 0 1px !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(th),
|
||||
@@ -1725,6 +2787,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.pricing-table :deep(tbody .sticky-col) {
|
||||
z-index: 12 !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(.sticky-boundary) {
|
||||
@@ -1732,6 +2795,20 @@ onBeforeUnmount(() => {
|
||||
box-shadow: 8px 0 12px -10px rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
|
||||
.pricing-table :deep(tbody td:not(.sticky-col)) {
|
||||
position: relative;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
.pricing-table :deep(tbody td.sticky-col)::after,
|
||||
.pricing-table :deep(thead th.sticky-col)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: inherit;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.header-with-filter {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 20px;
|
||||
@@ -1830,6 +2907,11 @@ onBeforeUnmount(() => {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.pricing-table :deep(td.selected-tone-cell) {
|
||||
/* "Secondary" tonlu secim vurgusu (yalnizca karisima kadar olan sol kolonlar) */
|
||||
background: color-mix(in srgb, var(--q-secondary) 12%, #ffffff);
|
||||
}
|
||||
|
||||
.stock-qty-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -1879,7 +2961,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.pricing-table :deep(.selection-col .q-checkbox__bg) {
|
||||
background: #fff;
|
||||
border-color: var(--q-primary);
|
||||
}
|
||||
|
||||
@@ -1922,17 +3003,49 @@ onBeforeUnmount(() => {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.editable-price-cell {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.old-price-label {
|
||||
display: block;
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
color: #7c3aed;
|
||||
text-align: right;
|
||||
margin: 0 auto;
|
||||
padding-right: 1px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.native-cell-input,
|
||||
.native-cell-select {
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
height: 22px;
|
||||
box-sizing: border-box;
|
||||
padding: 1px 3px;
|
||||
padding: 1px 1px;
|
||||
border: 1px solid #cfd8dc;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
margin: 0 auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.price-edit-input {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.native-cell-input:focus,
|
||||
|
||||
@@ -382,6 +382,13 @@ const routes = [
|
||||
component: () => import('pages/BrandClassification.vue'),
|
||||
meta: { permission: 'pricing:view' }
|
||||
},
|
||||
{
|
||||
path: 'pricing/brandgroupcurrency',
|
||||
alias: ['pricing/brandproupcurrency', 'pricing/brand-group-currency'],
|
||||
name: 'brandgroupcurrency',
|
||||
component: () => import('pages/BrandGroupCurrency.vue'),
|
||||
meta: { permission: 'pricing:view' }
|
||||
},
|
||||
{
|
||||
path: 'pricing/pricing-rules',
|
||||
name: 'pricing-rules',
|
||||
|
||||
@@ -119,7 +119,8 @@ api.interceptors.request.use((config) => {
|
||||
trace_id: traceId,
|
||||
method: String(config.method || 'GET').toUpperCase(),
|
||||
url,
|
||||
params: config.params || {}
|
||||
params: config.params || {},
|
||||
timeout_ms: typeof config.timeout === 'number' ? config.timeout : null
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ function parseFlexibleNumber (value) {
|
||||
}
|
||||
|
||||
function mapRow (raw, index, baseIndex = 0) {
|
||||
return {
|
||||
const row = {
|
||||
id: baseIndex + index + 1,
|
||||
productCode: toText(raw?.ProductCode),
|
||||
stockQty: toNumber(raw?.StockQty),
|
||||
@@ -76,6 +76,18 @@ function mapRow (raw, index, baseIndex = 0) {
|
||||
try5: toNumber(raw?.TRY5),
|
||||
try6: toNumber(raw?.TRY6)
|
||||
}
|
||||
const originalFields = [
|
||||
'costPrice',
|
||||
'basePriceUsd',
|
||||
'basePriceTry',
|
||||
'usd1', 'usd2', 'usd3', 'usd4', 'usd5', 'usd6',
|
||||
'eur1', 'eur2', 'eur3', 'eur4', 'eur5', 'eur6',
|
||||
'try1', 'try2', 'try3', 'try4', 'try5', 'try6'
|
||||
]
|
||||
originalFields.forEach((field) => {
|
||||
row[`__orig_${field}`] = row[field]
|
||||
})
|
||||
return row
|
||||
}
|
||||
|
||||
function cloneRows (rows = []) {
|
||||
|
||||
Reference in New Issue
Block a user