Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -847,6 +847,11 @@ func main() {
|
||||
auditlog.Init(pgDB, 1000)
|
||||
log.Println("🕵️ AuditLog sistemi başlatıldı (buffer=1000)")
|
||||
|
||||
// -------------------------------------------------------
|
||||
// 🚀 TRANSLATION QUERY PERFORMANCE INDEXES
|
||||
// -------------------------------------------------------
|
||||
routes.EnsureTranslationPerfIndexes(pgDB)
|
||||
|
||||
// -------------------------------------------------------
|
||||
// ✉️ MAILER INIT
|
||||
// -------------------------------------------------------
|
||||
|
||||
@@ -5,12 +5,20 @@ import (
|
||||
"bssapp-backend/models"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error) {
|
||||
const query = `
|
||||
func GetProductPricingList(ctx context.Context, limit int, offset int) ([]models.ProductPricing, error) {
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
query := `
|
||||
WITH base_products AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
||||
@@ -27,6 +35,13 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
AND IsBlocked = 0
|
||||
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
|
||||
),
|
||||
paged_products AS (
|
||||
SELECT
|
||||
bp.ProductCode
|
||||
FROM base_products bp
|
||||
ORDER BY bp.ProductCode
|
||||
OFFSET %d ROWS FETCH NEXT %d ROWS ONLY
|
||||
),
|
||||
latest_base_price AS (
|
||||
SELECT
|
||||
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
|
||||
@@ -42,8 +57,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(b.ItemCode))
|
||||
FROM paged_products pp
|
||||
WHERE pp.ProductCode = LTRIM(RTRIM(b.ItemCode))
|
||||
)
|
||||
),
|
||||
stock_entry_dates AS (
|
||||
@@ -61,8 +76,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||
FROM paged_products pp
|
||||
WHERE pp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||
),
|
||||
@@ -75,8 +90,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||
FROM paged_products pp
|
||||
WHERE pp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||
),
|
||||
@@ -89,8 +104,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(p.ItemCode))
|
||||
FROM paged_products pp
|
||||
WHERE pp.ProductCode = LTRIM(RTRIM(p.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(p.ItemCode))
|
||||
),
|
||||
@@ -103,8 +118,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(r.ItemCode))
|
||||
FROM paged_products pp
|
||||
WHERE pp.ProductCode = LTRIM(RTRIM(r.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(r.ItemCode))
|
||||
),
|
||||
@@ -117,29 +132,29 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM base_products bp
|
||||
WHERE bp.ProductCode = LTRIM(RTRIM(d.ItemCode))
|
||||
FROM paged_products pp
|
||||
WHERE pp.ProductCode = LTRIM(RTRIM(d.ItemCode))
|
||||
)
|
||||
GROUP BY LTRIM(RTRIM(d.ItemCode))
|
||||
),
|
||||
stock_totals AS (
|
||||
SELECT
|
||||
bp.ProductCode AS ItemCode,
|
||||
pp.ProductCode AS ItemCode,
|
||||
CAST(ROUND(
|
||||
ISNULL(sb.InventoryQty1, 0)
|
||||
- ISNULL(pb.PickingQty1, 0)
|
||||
- ISNULL(rb.ReserveQty1, 0)
|
||||
- ISNULL(db.DispOrderQty1, 0)
|
||||
, 2) AS DECIMAL(18, 2)) AS StockQty
|
||||
FROM base_products bp
|
||||
FROM paged_products pp
|
||||
LEFT JOIN stock_base sb
|
||||
ON sb.ItemCode = bp.ProductCode
|
||||
ON sb.ItemCode = pp.ProductCode
|
||||
LEFT JOIN pick_base pb
|
||||
ON pb.ItemCode = bp.ProductCode
|
||||
ON pb.ItemCode = pp.ProductCode
|
||||
LEFT JOIN reserve_base rb
|
||||
ON rb.ItemCode = bp.ProductCode
|
||||
ON rb.ItemCode = pp.ProductCode
|
||||
LEFT JOIN disp_base db
|
||||
ON db.ItemCode = bp.ProductCode
|
||||
ON db.ItemCode = pp.ProductCode
|
||||
)
|
||||
SELECT
|
||||
bp.ProductCode AS ProductCode,
|
||||
@@ -155,7 +170,9 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
bp.Icerik,
|
||||
bp.Karisim,
|
||||
bp.Marka
|
||||
FROM base_products bp
|
||||
FROM paged_products pp
|
||||
INNER JOIN base_products bp
|
||||
ON bp.ProductCode = pp.ProductCode
|
||||
LEFT JOIN latest_base_price lp
|
||||
ON lp.ItemCode = bp.ProductCode
|
||||
AND lp.rn = 1
|
||||
@@ -165,6 +182,7 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
||||
ON st.ItemCode = bp.ProductCode
|
||||
ORDER BY bp.ProductCode;
|
||||
`
|
||||
query = fmt.Sprintf(query, offset, limit)
|
||||
|
||||
var (
|
||||
rows *sql.Rows
|
||||
|
||||
@@ -29,7 +29,20 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 180*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := queries.GetProductPricingList(ctx)
|
||||
limit := 500
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 10000 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
offset := 0
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("offset")); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil && parsed >= 0 && parsed <= 1000000 {
|
||||
offset = parsed
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := queries.GetProductPricingList(ctx, limit+1, offset)
|
||||
if err != nil {
|
||||
if isPricingTimeoutLike(err, ctx.Err()) {
|
||||
log.Printf(
|
||||
@@ -54,16 +67,29 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
hasMore := len(rows) > limit
|
||||
if hasMore {
|
||||
rows = rows[:limit]
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[ProductPricing] trace=%s success user=%s id=%d count=%d duration_ms=%d",
|
||||
"[ProductPricing] trace=%s success user=%s id=%d limit=%d offset=%d count=%d has_more=%t duration_ms=%d",
|
||||
traceID,
|
||||
claims.Username,
|
||||
claims.ID,
|
||||
limit,
|
||||
offset,
|
||||
len(rows),
|
||||
hasMore,
|
||||
time.Since(started).Milliseconds(),
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if hasMore {
|
||||
w.Header().Set("X-Has-More", "true")
|
||||
} else {
|
||||
w.Header().Set("X-Has-More", "false")
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(rows)
|
||||
}
|
||||
|
||||
|
||||
41
svc/routes/translation_perf.go
Normal file
41
svc/routes/translation_perf.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnsureTranslationPerfIndexes creates helpful indexes for translation listing/search.
|
||||
// It is safe to run on each startup; failures are logged and do not stop the service.
|
||||
func EnsureTranslationPerfIndexes(db *sql.DB) {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
|
||||
statements := []string{
|
||||
`CREATE EXTENSION IF NOT EXISTS pg_trgm`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_mk_translator_t_key_lang ON mk_translator (t_key, lang_code)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_mk_translator_status_lang_updated ON mk_translator (status, lang_code, updated_at DESC)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_mk_translator_manual_status ON mk_translator (is_manual, status)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_mk_translator_source_type_expr ON mk_translator ((COALESCE(NULLIF(provider_meta->>'source_type',''),'dummy')))`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_mk_translator_source_text_trgm ON mk_translator USING gin (source_text_tr gin_trgm_ops)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_mk_translator_translated_text_trgm ON mk_translator USING gin (translated_text gin_trgm_ops)`,
|
||||
}
|
||||
|
||||
for _, stmt := range statements {
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
log.Printf("[TranslationPerf] index_setup_warn sql=%q err=%v", summarizeSQL(stmt), err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[TranslationPerf] index_ready sql=%q", summarizeSQL(stmt))
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeSQL(sqlText string) string {
|
||||
s := strings.TrimSpace(sqlText)
|
||||
if len(s) <= 100 {
|
||||
return s
|
||||
}
|
||||
return s[:100] + "..."
|
||||
}
|
||||
@@ -143,6 +143,12 @@ func GetTranslationRowsHandler(db *sql.DB) http.HandlerFunc {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
offset := 0
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("offset")); raw != "" {
|
||||
if parsed, err := strconv.Atoi(raw); err == nil && parsed >= 0 && parsed <= 1000000 {
|
||||
offset = parsed
|
||||
}
|
||||
}
|
||||
|
||||
clauses := []string{"1=1"}
|
||||
args := make([]any, 0, 8)
|
||||
@@ -202,6 +208,11 @@ ORDER BY t_key, lang_code
|
||||
if limit > 0 {
|
||||
query += fmt.Sprintf("LIMIT $%d", argIndex)
|
||||
args = append(args, limit)
|
||||
argIndex++
|
||||
}
|
||||
if offset > 0 {
|
||||
query += fmt.Sprintf(" OFFSET $%d", argIndex)
|
||||
args = append(args, offset)
|
||||
}
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
|
||||
75
ui/.quasar/prod-spa/app.js
Normal file
75
ui/.quasar/prod-spa/app.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
||||
* DO NOT EDIT.
|
||||
*
|
||||
* You are probably looking on adding startup/initialization code.
|
||||
* Use "quasar new boot <name>" and add it there.
|
||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
||||
*
|
||||
* Boot files are your "main.js"
|
||||
**/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { Quasar } from 'quasar'
|
||||
import { markRaw } from 'vue'
|
||||
import RootComponent from 'app/src/App.vue'
|
||||
|
||||
import createStore from 'app/src/stores/index'
|
||||
import createRouter from 'app/src/router/index'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default async function (createAppFn, quasarUserOptions) {
|
||||
|
||||
|
||||
// Create the app instance.
|
||||
// Here we inject into it the Quasar UI, the router & possibly the store.
|
||||
const app = createAppFn(RootComponent)
|
||||
|
||||
|
||||
|
||||
app.use(Quasar, quasarUserOptions)
|
||||
|
||||
|
||||
|
||||
|
||||
const store = typeof createStore === 'function'
|
||||
? await createStore({})
|
||||
: createStore
|
||||
|
||||
|
||||
app.use(store)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const router = markRaw(
|
||||
typeof createRouter === 'function'
|
||||
? await createRouter({store})
|
||||
: createRouter
|
||||
)
|
||||
|
||||
|
||||
// make router instance available in store
|
||||
|
||||
store.use(({ store }) => { store.router = router })
|
||||
|
||||
|
||||
|
||||
// Expose the app, the router and the store.
|
||||
// Note that we are not mounting the app here, since bootstrapping will be
|
||||
// different depending on whether we are in a browser or on the server.
|
||||
return {
|
||||
app,
|
||||
store,
|
||||
router
|
||||
}
|
||||
}
|
||||
158
ui/.quasar/prod-spa/client-entry.js
Normal file
158
ui/.quasar/prod-spa/client-entry.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/* 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)
|
||||
})
|
||||
})
|
||||
|
||||
116
ui/.quasar/prod-spa/client-prefetch.js
Normal file
116
ui/.quasar/prod-spa/client-prefetch.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
||||
* DO NOT EDIT.
|
||||
*
|
||||
* You are probably looking on adding startup/initialization code.
|
||||
* Use "quasar new boot <name>" and add it there.
|
||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
||||
*
|
||||
* Boot files are your "main.js"
|
||||
**/
|
||||
|
||||
|
||||
|
||||
import App from 'app/src/App.vue'
|
||||
let appPrefetch = typeof App.preFetch === 'function'
|
||||
? App.preFetch
|
||||
: (
|
||||
// Class components return the component options (and the preFetch hook) inside __c property
|
||||
App.__c !== void 0 && typeof App.__c.preFetch === 'function'
|
||||
? App.__c.preFetch
|
||||
: false
|
||||
)
|
||||
|
||||
|
||||
function getMatchedComponents (to, router) {
|
||||
const route = to
|
||||
? (to.matched ? to : router.resolve(to).route)
|
||||
: router.currentRoute.value
|
||||
|
||||
if (!route) { return [] }
|
||||
|
||||
const matched = route.matched.filter(m => m.components !== void 0)
|
||||
|
||||
if (matched.length === 0) { return [] }
|
||||
|
||||
return Array.prototype.concat.apply([], matched.map(m => {
|
||||
return Object.keys(m.components).map(key => {
|
||||
const comp = m.components[key]
|
||||
return {
|
||||
path: m.path,
|
||||
c: comp
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
export function addPreFetchHooks ({ router, store, publicPath }) {
|
||||
// Add router hook for handling preFetch.
|
||||
// Doing it after initial route is resolved so that we don't double-fetch
|
||||
// the data that we already have. Using router.beforeResolve() so that all
|
||||
// async components are resolved.
|
||||
router.beforeResolve((to, from, next) => {
|
||||
const
|
||||
urlPath = window.location.href.replace(window.location.origin, ''),
|
||||
matched = getMatchedComponents(to, router),
|
||||
prevMatched = getMatchedComponents(from, router)
|
||||
|
||||
let diffed = false
|
||||
const preFetchList = matched
|
||||
.filter((m, i) => {
|
||||
return diffed || (diffed = (
|
||||
!prevMatched[i] ||
|
||||
prevMatched[i].c !== m.c ||
|
||||
m.path.indexOf('/:') > -1 // does it has params?
|
||||
))
|
||||
})
|
||||
.filter(m => m.c !== void 0 && (
|
||||
typeof m.c.preFetch === 'function'
|
||||
// Class components return the component options (and the preFetch hook) inside __c property
|
||||
|| (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function')
|
||||
))
|
||||
.map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch)
|
||||
|
||||
|
||||
if (appPrefetch !== false) {
|
||||
preFetchList.unshift(appPrefetch)
|
||||
appPrefetch = false
|
||||
}
|
||||
|
||||
|
||||
if (preFetchList.length === 0) {
|
||||
return next()
|
||||
}
|
||||
|
||||
let hasRedirected = false
|
||||
const redirect = url => {
|
||||
hasRedirected = true
|
||||
next(url)
|
||||
}
|
||||
const proceed = () => {
|
||||
|
||||
if (hasRedirected === false) { next() }
|
||||
}
|
||||
|
||||
|
||||
|
||||
preFetchList.reduce(
|
||||
(promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({
|
||||
store,
|
||||
currentRoute: to,
|
||||
previousRoute: from,
|
||||
redirect,
|
||||
urlPath,
|
||||
publicPath
|
||||
})),
|
||||
Promise.resolve()
|
||||
)
|
||||
.then(proceed)
|
||||
.catch(e => {
|
||||
console.error(e)
|
||||
proceed()
|
||||
})
|
||||
})
|
||||
}
|
||||
23
ui/.quasar/prod-spa/quasar-user-options.js
Normal file
23
ui/.quasar/prod-spa/quasar-user-options.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* THIS FILE IS GENERATED AUTOMATICALLY.
|
||||
* DO NOT EDIT.
|
||||
*
|
||||
* You are probably looking on adding startup/initialization code.
|
||||
* Use "quasar new boot <name>" and add it there.
|
||||
* One boot file per concern. Then reference the file(s) in quasar.config file > boot:
|
||||
* boot: ['file', ...] // do not add ".js" extension to it.
|
||||
*
|
||||
* Boot files are your "main.js"
|
||||
**/
|
||||
|
||||
import lang from 'quasar/lang/tr.js'
|
||||
|
||||
|
||||
|
||||
import {Loading,Dialog,Notify} from 'quasar'
|
||||
|
||||
|
||||
|
||||
export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} }
|
||||
|
||||
@@ -53,9 +53,10 @@
|
||||
:virtual-scroll-sticky-size-start="headerHeight"
|
||||
:virtual-scroll-slice-size="36"
|
||||
:rows-per-page-options="[0]"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
v-model:pagination="tablePagination"
|
||||
hide-bottom
|
||||
:table-style="tableStyle"
|
||||
@virtual-scroll="onTableVirtualScroll"
|
||||
>
|
||||
<template #header="props">
|
||||
<q-tr :props="props" class="header-row-fixed">
|
||||
@@ -309,10 +310,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||
|
||||
const store = useProductPricingStore()
|
||||
const FETCH_LIMIT = 500
|
||||
const currentOffset = ref(0)
|
||||
const loadingMore = ref(false)
|
||||
|
||||
const usdToTry = 38.25
|
||||
const eurToTry = 41.6
|
||||
@@ -381,6 +385,12 @@ const headerFilterFieldSet = new Set([
|
||||
])
|
||||
|
||||
const mainTableRef = ref(null)
|
||||
const tablePagination = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 0,
|
||||
sortBy: 'productCode',
|
||||
descending: false
|
||||
})
|
||||
const selectedMap = ref({})
|
||||
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||
const showSelectedOnly = ref(false)
|
||||
@@ -570,6 +580,7 @@ const selectedRowCount = computed(() => Object.values(selectedMap.value).filter(
|
||||
const selectedVisibleCount = computed(() => visibleRowIds.value.filter((id) => !!selectedMap.value[id]).length)
|
||||
const allSelectedVisible = computed(() => visibleRowIds.value.length > 0 && selectedVisibleCount.value === visibleRowIds.value.length)
|
||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||
const hasMoreRows = computed(() => Boolean(store.hasMore))
|
||||
|
||||
function isHeaderFilterField (field) {
|
||||
return headerFilterFieldSet.has(field)
|
||||
@@ -840,12 +851,52 @@ function clearAllCurrencies () {
|
||||
selectedCurrencies.value = []
|
||||
}
|
||||
|
||||
async function fetchChunk ({ reset = false } = {}) {
|
||||
const offset = reset ? 0 : currentOffset.value
|
||||
const result = await store.fetchRows({
|
||||
limit: FETCH_LIMIT,
|
||||
offset,
|
||||
append: !reset
|
||||
})
|
||||
const fetched = Number(result?.fetched) || 0
|
||||
currentOffset.value = offset + fetched
|
||||
return fetched
|
||||
}
|
||||
|
||||
async function loadMoreRows () {
|
||||
if (loadingMore.value || store.loading || !hasMoreRows.value) return
|
||||
loadingMore.value = true
|
||||
try {
|
||||
await fetchChunk({ reset: false })
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onTableVirtualScroll (details) {
|
||||
const to = Number(details?.to || 0)
|
||||
if (!Number.isFinite(to)) return
|
||||
if (to >= filteredRows.value.length - 25) {
|
||||
void loadMoreRows()
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureEnoughVisibleRows (minRows = 80, maxBatches = 4) {
|
||||
let guard = 0
|
||||
while (hasMoreRows.value && filteredRows.value.length < minRows && guard < maxBatches) {
|
||||
await loadMoreRows()
|
||||
guard++
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData () {
|
||||
const startedAt = Date.now()
|
||||
console.info('[product-pricing][ui] reload:start', {
|
||||
at: new Date(startedAt).toISOString()
|
||||
})
|
||||
await store.fetchRows()
|
||||
currentOffset.value = 0
|
||||
await fetchChunk({ reset: true })
|
||||
await ensureEnoughVisibleRows(120, 6)
|
||||
console.info('[product-pricing][ui] reload:done', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
row_count: Array.isArray(store.rows) ? store.rows.length : 0,
|
||||
@@ -857,6 +908,19 @@ async function reloadData () {
|
||||
onMounted(async () => {
|
||||
await reloadData()
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
columnFilters,
|
||||
numberRangeFilters,
|
||||
dateRangeFilters,
|
||||
showSelectedOnly,
|
||||
() => tablePagination.value.sortBy,
|
||||
() => tablePagination.value.descending
|
||||
],
|
||||
() => { void ensureEnoughVisibleRows(80, 4) },
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,69 +1,76 @@
|
||||
<template>
|
||||
<q-page v-if="canUpdateLanguage" class="q-pa-md">
|
||||
<div class="row q-col-gutter-sm items-end q-mb-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
v-model="filters.q"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Kelime ara"
|
||||
<template>
|
||||
<q-page v-if="canUpdateLanguage" class="q-pa-md translation-page">
|
||||
<div class="translation-toolbar sticky-toolbar">
|
||||
<div class="row q-col-gutter-sm items-end q-mb-md">
|
||||
<div class="col-12 col-md-4">
|
||||
<q-input
|
||||
v-model="filters.q"
|
||||
dense
|
||||
outlined
|
||||
clearable
|
||||
label="Kelime ara"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn color="primary" icon="search" label="Getir" @click="loadRows" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="sync"
|
||||
label="YENİ KELİMELERİ GETİR"
|
||||
:loading="store.saving"
|
||||
@click="syncSources"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-toggle v-model="autoTranslate" dense color="primary" label="Oto Çeviri" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-gutter-sm q-mb-sm">
|
||||
<q-btn
|
||||
color="accent"
|
||||
icon="g_translate"
|
||||
label="Seçilenleri Çevir"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="translateSelectedRows"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn color="primary" icon="search" label="Getir" @click="loadRows" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="sync"
|
||||
label="YENİ KELİMELERİ GETİR"
|
||||
icon="done_all"
|
||||
label="Seçilenleri Onayla"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="syncSources"
|
||||
@click="bulkApproveSelected"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Seçilenleri Toplu Güncelle"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="bulkSaveSelected"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-toggle v-model="autoTranslate" dense color="primary" label="Oto Çeviri" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-gutter-sm q-mb-sm">
|
||||
<q-btn
|
||||
color="accent"
|
||||
icon="g_translate"
|
||||
label="Seçilenleri Çevir"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="translateSelectedRows"
|
||||
/>
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="done_all"
|
||||
label="Seçilenleri Onayla"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="bulkApproveSelected"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Seçilenleri Toplu Güncelle"
|
||||
:disable="selectedKeys.length === 0"
|
||||
:loading="store.saving"
|
||||
@click="bulkSaveSelected"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
ref="tableRef"
|
||||
class="translation-table"
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
virtual-scroll
|
||||
:virtual-scroll-sticky-size-start="56"
|
||||
row-key="t_key"
|
||||
:loading="store.loading || store.saving"
|
||||
:rows="pivotRows"
|
||||
:columns="columns"
|
||||
:rows-per-page-options="[0]"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
v-model:pagination="tablePagination"
|
||||
hide-bottom
|
||||
@virtual-scroll="onVirtualScroll"
|
||||
>
|
||||
<template #body-cell-actions="props">
|
||||
<q-td :props="props">
|
||||
@@ -91,7 +98,9 @@
|
||||
|
||||
<template #body-cell-source_text_tr="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'source_text_tr')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).source_text_tr" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<div class="source-text-label" :title="rowDraft(props.row.t_key).source_text_tr">
|
||||
{{ rowDraft(props.row.t_key).source_text_tr }}
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
@@ -111,37 +120,79 @@
|
||||
|
||||
<template #body-cell-en="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'en')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).en" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).en"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-de="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'de')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).de" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).de"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-es="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'es')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).es" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).es"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-it="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'it')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).it" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).it"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-ru="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'ru')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).ru" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).ru"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-ar="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'ar')">
|
||||
<q-input v-model="rowDraft(props.row.t_key).ar" dense outlined @blur="queueAutoSave(props.row.t_key)" />
|
||||
<q-input
|
||||
v-model="rowDraft(props.row.t_key).ar"
|
||||
type="textarea"
|
||||
autogrow
|
||||
:max-rows="8"
|
||||
outlined
|
||||
@blur="queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
@@ -155,7 +206,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { useTranslationStore } from 'src/stores/translationStore'
|
||||
@@ -169,6 +220,18 @@ const filters = ref({
|
||||
q: ''
|
||||
})
|
||||
const autoTranslate = ref(false)
|
||||
const tableRef = ref(null)
|
||||
const FETCH_LIMIT = 1400
|
||||
const loadedOffset = ref(0)
|
||||
const hasMoreRows = ref(true)
|
||||
const loadingMore = ref(false)
|
||||
const tablePagination = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 0,
|
||||
sortBy: 'source_text_tr',
|
||||
descending: false
|
||||
})
|
||||
let filterReloadTimer = null
|
||||
|
||||
const sourceTypeOptions = [
|
||||
{ label: 'dummy', value: 'dummy' },
|
||||
@@ -179,15 +242,14 @@ const sourceTypeOptions = [
|
||||
const columns = [
|
||||
{ name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' },
|
||||
{ name: 'select', label: 'Seç', field: 'select', align: 'left' },
|
||||
{ name: 't_key', label: 'Key', field: 't_key', align: 'left', sortable: true },
|
||||
{ name: 'source_text_tr', label: 'Türkçe kaynak', field: 'source_text_tr', align: 'left' },
|
||||
{ name: 'source_type', label: 'Veri tipi', field: 'source_type', align: 'left' },
|
||||
{ name: 'en', label: 'English', field: 'en', align: 'left' },
|
||||
{ name: 'de', label: 'Deutch', field: 'de', align: 'left' },
|
||||
{ name: 'es', label: 'Espanol', field: 'es', align: 'left' },
|
||||
{ name: 'it', label: 'Italiano', field: 'it', align: 'left' },
|
||||
{ name: 'ru', label: 'Русский', field: 'ru', align: 'left' },
|
||||
{ name: 'ar', label: 'العربية', field: 'ar', align: 'left' }
|
||||
{ name: 'source_text_tr', label: 'Türkçe Metin', field: 'source_text_tr', align: 'left', style: 'min-width: 340px' },
|
||||
{ name: 'source_type', label: 'Kaynak', field: 'source_type', align: 'left', style: 'min-width: 140px' },
|
||||
{ name: 'en', label: 'İngilizce', field: 'en', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'de', label: 'Almanca', field: 'de', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'es', label: 'İspanyolca', field: 'es', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'it', label: 'İtalyanca', field: 'it', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'ru', label: 'Rusça', field: 'ru', align: 'left', style: 'min-width: 220px' },
|
||||
{ name: 'ar', label: 'Arapça', field: 'ar', align: 'left', style: 'min-width: 220px' }
|
||||
]
|
||||
|
||||
const draftByKey = ref({})
|
||||
@@ -242,10 +304,33 @@ const pivotRows = computed(() => {
|
||||
return Array.from(byKey.values()).sort((a, b) => a.t_key.localeCompare(b.t_key))
|
||||
})
|
||||
|
||||
function snapshotDrafts () {
|
||||
function snapshotDrafts (options = {}) {
|
||||
const preserveDirty = Boolean(options?.preserveDirty)
|
||||
const draft = {}
|
||||
const original = {}
|
||||
for (const row of pivotRows.value) {
|
||||
const existingDraft = draftByKey.value[row.t_key]
|
||||
const existingOriginal = originalByKey.value[row.t_key]
|
||||
const keepExisting = preserveDirty &&
|
||||
existingDraft &&
|
||||
existingOriginal &&
|
||||
(
|
||||
existingDraft.source_text_tr !== existingOriginal.source_text_tr ||
|
||||
existingDraft.source_type !== existingOriginal.source_type ||
|
||||
existingDraft.en !== existingOriginal.en ||
|
||||
existingDraft.de !== existingOriginal.de ||
|
||||
existingDraft.es !== existingOriginal.es ||
|
||||
existingDraft.it !== existingOriginal.it ||
|
||||
existingDraft.ru !== existingOriginal.ru ||
|
||||
existingDraft.ar !== existingOriginal.ar
|
||||
)
|
||||
|
||||
if (keepExisting) {
|
||||
draft[row.t_key] = { ...existingDraft }
|
||||
original[row.t_key] = { ...existingOriginal }
|
||||
continue
|
||||
}
|
||||
|
||||
draft[row.t_key] = {
|
||||
source_text_tr: row.source_text_tr || '',
|
||||
source_type: row.source_type || 'dummy',
|
||||
@@ -280,8 +365,9 @@ function rowDraft (key) {
|
||||
}
|
||||
|
||||
function buildFilters () {
|
||||
const query = String(filters.value.q || '').trim()
|
||||
return {
|
||||
q: filters.value.q || undefined
|
||||
q: query || undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,10 +435,30 @@ function queueAutoSave (key) {
|
||||
autoSaveTimers.set(key, timer)
|
||||
}
|
||||
|
||||
async function fetchRowsChunk (append = false) {
|
||||
const params = {
|
||||
...buildFilters(),
|
||||
limit: FETCH_LIMIT,
|
||||
offset: append ? loadedOffset.value : 0
|
||||
}
|
||||
|
||||
await store.fetchRows(params, { append })
|
||||
const incomingCount = Number(store.count) || 0
|
||||
|
||||
if (append) {
|
||||
loadedOffset.value += incomingCount
|
||||
} else {
|
||||
loadedOffset.value = incomingCount
|
||||
}
|
||||
hasMoreRows.value = incomingCount === FETCH_LIMIT
|
||||
snapshotDrafts({ preserveDirty: append })
|
||||
}
|
||||
|
||||
async function loadRows () {
|
||||
try {
|
||||
await store.fetchRows(buildFilters())
|
||||
snapshotDrafts()
|
||||
loadedOffset.value = 0
|
||||
hasMoreRows.value = true
|
||||
await fetchRowsChunk(false)
|
||||
} catch (err) {
|
||||
console.error('[translation-sync][ui] loadRows:error', {
|
||||
message: err?.message || 'Çeviri satırları yüklenemedi'
|
||||
@@ -364,6 +470,42 @@ async function loadRows () {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreRows () {
|
||||
if (!hasMoreRows.value || loadingMore.value || store.loading || store.saving) return
|
||||
loadingMore.value = true
|
||||
try {
|
||||
await fetchRowsChunk(true)
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureEnoughVisibleRows (minRows = 120, maxBatches = 4) {
|
||||
let guard = 0
|
||||
while (hasMoreRows.value && pivotRows.value.length < minRows && guard < maxBatches) {
|
||||
await loadMoreRows()
|
||||
guard++
|
||||
}
|
||||
}
|
||||
|
||||
function onVirtualScroll (details) {
|
||||
const to = Number(details?.to || 0)
|
||||
if (!Number.isFinite(to)) return
|
||||
if (to >= pivotRows.value.length - 15) {
|
||||
void loadMoreRows()
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleFilterReload () {
|
||||
if (filterReloadTimer) {
|
||||
clearTimeout(filterReloadTimer)
|
||||
}
|
||||
filterReloadTimer = setTimeout(() => {
|
||||
filterReloadTimer = null
|
||||
void loadRows()
|
||||
}, 350)
|
||||
}
|
||||
|
||||
async function ensureMissingLangRows (key, draft, langs) {
|
||||
const missingLangs = []
|
||||
if (!langs.en && String(draft.en || '').trim() !== '') missingLangs.push('en')
|
||||
@@ -618,11 +760,81 @@ async function syncSources () {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRows()
|
||||
void loadRows()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (filterReloadTimer) {
|
||||
clearTimeout(filterReloadTimer)
|
||||
filterReloadTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => filters.value.q,
|
||||
() => { scheduleFilterReload() }
|
||||
)
|
||||
|
||||
watch(
|
||||
[() => tablePagination.value.sortBy, () => tablePagination.value.descending],
|
||||
() => { void ensureEnoughVisibleRows(120, 4) }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.translation-page {
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.translation-toolbar {
|
||||
background: #fff;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.sticky-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 35;
|
||||
}
|
||||
|
||||
.translation-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.translation-table :deep(.q-table__middle) {
|
||||
max-height: calc(100vh - 280px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.translation-table :deep(.q-table thead tr th) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.translation-table :deep(.q-table tbody td) {
|
||||
vertical-align: top;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.translation-table :deep(.q-field__native) {
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.source-text-label {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
max-height: 11.2em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cell-dirty {
|
||||
background: #fff3cd;
|
||||
}
|
||||
@@ -631,3 +843,4 @@ onMounted(() => {
|
||||
background: #d9f7e8;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ function toNumber (value) {
|
||||
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
|
||||
}
|
||||
|
||||
function mapRow (raw, index) {
|
||||
function mapRow (raw, index, offset = 0) {
|
||||
return {
|
||||
id: index + 1,
|
||||
id: offset + index + 1,
|
||||
productCode: toText(raw?.ProductCode),
|
||||
stockQty: toNumber(raw?.StockQty),
|
||||
stockEntryDate: toText(raw?.StockEntryDate),
|
||||
@@ -55,34 +55,65 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
state: () => ({
|
||||
rows: [],
|
||||
loading: false,
|
||||
error: ''
|
||||
error: '',
|
||||
hasMore: true
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchRows () {
|
||||
async fetchRows (options = {}) {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
|
||||
const offset = Number(options?.offset) >= 0 ? Number(options.offset) : 0
|
||||
const append = Boolean(options?.append)
|
||||
const startedAt = Date.now()
|
||||
console.info('[product-pricing][frontend] request:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
timeout_ms: 600000
|
||||
timeout_ms: 180000,
|
||||
limit,
|
||||
offset,
|
||||
append
|
||||
})
|
||||
try {
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
url: '/pricing/products',
|
||||
timeout: 600000
|
||||
params: { limit, offset },
|
||||
timeout: 180000
|
||||
})
|
||||
const traceId = res?.headers?.['x-trace-id'] || null
|
||||
const hasMoreHeader = String(res?.headers?.['x-has-more'] || '').toLowerCase()
|
||||
const data = Array.isArray(res?.data) ? res.data : []
|
||||
this.rows = data.map((x, i) => mapRow(x, i))
|
||||
const mapped = data.map((x, i) => mapRow(x, i, offset))
|
||||
if (append) {
|
||||
const merged = [...this.rows]
|
||||
const seen = new Set(this.rows.map((x) => x?.productCode))
|
||||
for (const row of mapped) {
|
||||
const key = row?.productCode
|
||||
if (key && seen.has(key)) continue
|
||||
merged.push(row)
|
||||
if (key) seen.add(key)
|
||||
}
|
||||
this.rows = merged
|
||||
} else {
|
||||
this.rows = mapped
|
||||
}
|
||||
this.hasMore = hasMoreHeader ? hasMoreHeader === 'true' : mapped.length === limit
|
||||
console.info('[product-pricing][frontend] request:success', {
|
||||
trace_id: traceId,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
row_count: this.rows.length
|
||||
row_count: this.rows.length,
|
||||
fetched_count: mapped.length,
|
||||
has_more: this.hasMore
|
||||
})
|
||||
return {
|
||||
traceId,
|
||||
fetched: mapped.length,
|
||||
hasMore: this.hasMore
|
||||
}
|
||||
} catch (err) {
|
||||
this.rows = []
|
||||
if (!append) this.rows = []
|
||||
this.hasMore = false
|
||||
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
||||
this.error = toText(msg)
|
||||
console.error('[product-pricing][frontend] request:error', {
|
||||
@@ -92,6 +123,7 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
status: err?.response?.status || null,
|
||||
message: this.error
|
||||
})
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@@ -10,12 +10,27 @@ export const useTranslationStore = defineStore('translation', {
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchRows (filters = {}) {
|
||||
async fetchRows (filters = {}, options = {}) {
|
||||
this.loading = true
|
||||
const append = Boolean(options?.append)
|
||||
try {
|
||||
const res = await api.get('/language/translations', { params: filters })
|
||||
const payload = res?.data || {}
|
||||
this.rows = Array.isArray(payload.rows) ? payload.rows : []
|
||||
const incoming = Array.isArray(payload.rows) ? payload.rows : []
|
||||
if (append) {
|
||||
const merged = [...this.rows]
|
||||
const seen = new Set(this.rows.map((x) => x?.id))
|
||||
for (const row of incoming) {
|
||||
const id = row?.id
|
||||
if (!seen.has(id)) {
|
||||
merged.push(row)
|
||||
seen.add(id)
|
||||
}
|
||||
}
|
||||
this.rows = merged
|
||||
} else {
|
||||
this.rows = incoming
|
||||
}
|
||||
this.count = Number(payload.count) || this.rows.length
|
||||
} finally {
|
||||
this.loading = false
|
||||
|
||||
Reference in New Issue
Block a user