Compare commits
6 Commits
2d369e7d7d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6bdf83f05 | ||
|
|
f9728b8a4c | ||
|
|
307282928c | ||
|
|
29909f3609 | ||
|
|
bb856cb082 | ||
|
|
b065e7192d |
@@ -847,6 +847,11 @@ func main() {
|
|||||||
auditlog.Init(pgDB, 1000)
|
auditlog.Init(pgDB, 1000)
|
||||||
log.Println("🕵️ AuditLog sistemi başlatıldı (buffer=1000)")
|
log.Println("🕵️ AuditLog sistemi başlatıldı (buffer=1000)")
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// 🚀 TRANSLATION QUERY PERFORMANCE INDEXES
|
||||||
|
// -------------------------------------------------------
|
||||||
|
routes.EnsureTranslationPerfIndexes(pgDB)
|
||||||
|
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
// ✉️ MAILER INIT
|
// ✉️ MAILER INIT
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
|
|||||||
@@ -5,12 +5,25 @@ import (
|
|||||||
"bssapp-backend/models"
|
"bssapp-backend/models"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error) {
|
func GetProductPricingList(ctx context.Context, limit int, afterProductCode string) ([]models.ProductPricing, error) {
|
||||||
const query = `
|
if limit <= 0 {
|
||||||
|
limit = 500
|
||||||
|
}
|
||||||
|
afterProductCode = strings.TrimSpace(afterProductCode)
|
||||||
|
|
||||||
|
cursorFilter := ""
|
||||||
|
args := make([]any, 0, 1)
|
||||||
|
if afterProductCode != "" {
|
||||||
|
cursorFilter = "WHERE bp.ProductCode > @p1"
|
||||||
|
args = append(args, afterProductCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
WITH base_products AS (
|
WITH base_products AS (
|
||||||
SELECT
|
SELECT
|
||||||
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
LTRIM(RTRIM(ProductCode)) AS ProductCode,
|
||||||
@@ -27,6 +40,13 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
AND IsBlocked = 0
|
AND IsBlocked = 0
|
||||||
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
|
AND LEN(LTRIM(RTRIM(ProductCode))) = 13
|
||||||
),
|
),
|
||||||
|
paged_products AS (
|
||||||
|
SELECT TOP (` + strconv.Itoa(limit) + `)
|
||||||
|
bp.ProductCode
|
||||||
|
FROM base_products bp
|
||||||
|
` + cursorFilter + `
|
||||||
|
ORDER BY bp.ProductCode
|
||||||
|
),
|
||||||
latest_base_price AS (
|
latest_base_price AS (
|
||||||
SELECT
|
SELECT
|
||||||
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
|
LTRIM(RTRIM(b.ItemCode)) AS ItemCode,
|
||||||
@@ -42,8 +62,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
|
AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD'
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM base_products bp
|
FROM paged_products pp
|
||||||
WHERE bp.ProductCode = LTRIM(RTRIM(b.ItemCode))
|
WHERE pp.ProductCode = LTRIM(RTRIM(b.ItemCode))
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
stock_entry_dates AS (
|
stock_entry_dates AS (
|
||||||
@@ -61,8 +81,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
)
|
)
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM base_products bp
|
FROM paged_products pp
|
||||||
WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
WHERE pp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||||
)
|
)
|
||||||
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||||
),
|
),
|
||||||
@@ -75,8 +95,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM base_products bp
|
FROM paged_products pp
|
||||||
WHERE bp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
WHERE pp.ProductCode = LTRIM(RTRIM(s.ItemCode))
|
||||||
)
|
)
|
||||||
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
GROUP BY LTRIM(RTRIM(s.ItemCode))
|
||||||
),
|
),
|
||||||
@@ -89,8 +109,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13
|
AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM base_products bp
|
FROM paged_products pp
|
||||||
WHERE bp.ProductCode = LTRIM(RTRIM(p.ItemCode))
|
WHERE pp.ProductCode = LTRIM(RTRIM(p.ItemCode))
|
||||||
)
|
)
|
||||||
GROUP BY LTRIM(RTRIM(p.ItemCode))
|
GROUP BY LTRIM(RTRIM(p.ItemCode))
|
||||||
),
|
),
|
||||||
@@ -103,8 +123,8 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13
|
AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM base_products bp
|
FROM paged_products pp
|
||||||
WHERE bp.ProductCode = LTRIM(RTRIM(r.ItemCode))
|
WHERE pp.ProductCode = LTRIM(RTRIM(r.ItemCode))
|
||||||
)
|
)
|
||||||
GROUP BY LTRIM(RTRIM(r.ItemCode))
|
GROUP BY LTRIM(RTRIM(r.ItemCode))
|
||||||
),
|
),
|
||||||
@@ -117,29 +137,29 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13
|
AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM base_products bp
|
FROM paged_products pp
|
||||||
WHERE bp.ProductCode = LTRIM(RTRIM(d.ItemCode))
|
WHERE pp.ProductCode = LTRIM(RTRIM(d.ItemCode))
|
||||||
)
|
)
|
||||||
GROUP BY LTRIM(RTRIM(d.ItemCode))
|
GROUP BY LTRIM(RTRIM(d.ItemCode))
|
||||||
),
|
),
|
||||||
stock_totals AS (
|
stock_totals AS (
|
||||||
SELECT
|
SELECT
|
||||||
bp.ProductCode AS ItemCode,
|
pp.ProductCode AS ItemCode,
|
||||||
CAST(ROUND(
|
CAST(ROUND(
|
||||||
ISNULL(sb.InventoryQty1, 0)
|
ISNULL(sb.InventoryQty1, 0)
|
||||||
- ISNULL(pb.PickingQty1, 0)
|
- ISNULL(pb.PickingQty1, 0)
|
||||||
- ISNULL(rb.ReserveQty1, 0)
|
- ISNULL(rb.ReserveQty1, 0)
|
||||||
- ISNULL(db.DispOrderQty1, 0)
|
- ISNULL(db.DispOrderQty1, 0)
|
||||||
, 2) AS DECIMAL(18, 2)) AS StockQty
|
, 2) AS DECIMAL(18, 2)) AS StockQty
|
||||||
FROM base_products bp
|
FROM paged_products pp
|
||||||
LEFT JOIN stock_base sb
|
LEFT JOIN stock_base sb
|
||||||
ON sb.ItemCode = bp.ProductCode
|
ON sb.ItemCode = pp.ProductCode
|
||||||
LEFT JOIN pick_base pb
|
LEFT JOIN pick_base pb
|
||||||
ON pb.ItemCode = bp.ProductCode
|
ON pb.ItemCode = pp.ProductCode
|
||||||
LEFT JOIN reserve_base rb
|
LEFT JOIN reserve_base rb
|
||||||
ON rb.ItemCode = bp.ProductCode
|
ON rb.ItemCode = pp.ProductCode
|
||||||
LEFT JOIN disp_base db
|
LEFT JOIN disp_base db
|
||||||
ON db.ItemCode = bp.ProductCode
|
ON db.ItemCode = pp.ProductCode
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
bp.ProductCode AS ProductCode,
|
bp.ProductCode AS ProductCode,
|
||||||
@@ -155,7 +175,9 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
bp.Icerik,
|
bp.Icerik,
|
||||||
bp.Karisim,
|
bp.Karisim,
|
||||||
bp.Marka
|
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
|
LEFT JOIN latest_base_price lp
|
||||||
ON lp.ItemCode = bp.ProductCode
|
ON lp.ItemCode = bp.ProductCode
|
||||||
AND lp.rn = 1
|
AND lp.rn = 1
|
||||||
@@ -172,7 +194,7 @@ func GetProductPricingList(ctx context.Context) ([]models.ProductPricing, error)
|
|||||||
)
|
)
|
||||||
for attempt := 1; attempt <= 3; attempt++ {
|
for attempt := 1; attempt <= 3; attempt++ {
|
||||||
var err error
|
var err error
|
||||||
rows, err = db.MssqlDB.QueryContext(ctx, query)
|
rows, err = db.MssqlDB.QueryContext(ctx, query, args...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rowsErr = nil
|
rowsErr = nil
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -29,7 +29,15 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, cancel := context.WithTimeout(r.Context(), 180*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 180*time.Second)
|
||||||
defer cancel()
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
afterProductCode := strings.TrimSpace(r.URL.Query().Get("after_product_code"))
|
||||||
|
|
||||||
|
rows, err := queries.GetProductPricingList(ctx, limit+1, afterProductCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isPricingTimeoutLike(err, ctx.Err()) {
|
if isPricingTimeoutLike(err, ctx.Err()) {
|
||||||
log.Printf(
|
log.Printf(
|
||||||
@@ -54,16 +62,37 @@ func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
hasMore := len(rows) > limit
|
||||||
|
if hasMore {
|
||||||
|
rows = rows[:limit]
|
||||||
|
}
|
||||||
|
nextCursor := ""
|
||||||
|
if hasMore && len(rows) > 0 {
|
||||||
|
nextCursor = strings.TrimSpace(rows[len(rows)-1].ProductCode)
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf(
|
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 after=%q count=%d has_more=%t next=%q duration_ms=%d",
|
||||||
traceID,
|
traceID,
|
||||||
claims.Username,
|
claims.Username,
|
||||||
claims.ID,
|
claims.ID,
|
||||||
|
limit,
|
||||||
|
afterProductCode,
|
||||||
len(rows),
|
len(rows),
|
||||||
|
hasMore,
|
||||||
|
nextCursor,
|
||||||
time.Since(started).Milliseconds(),
|
time.Since(started).Milliseconds(),
|
||||||
)
|
)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
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")
|
||||||
|
}
|
||||||
|
if nextCursor != "" {
|
||||||
|
w.Header().Set("X-Next-Cursor", nextCursor)
|
||||||
|
}
|
||||||
_ = json.NewEncoder(w).Encode(rows)
|
_ = 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
|
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"}
|
clauses := []string{"1=1"}
|
||||||
args := make([]any, 0, 8)
|
args := make([]any, 0, 8)
|
||||||
@@ -202,6 +208,11 @@ ORDER BY t_key, lang_code
|
|||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
query += fmt.Sprintf("LIMIT $%d", argIndex)
|
query += fmt.Sprintf("LIMIT $%d", argIndex)
|
||||||
args = append(args, limit)
|
args = append(args, limit)
|
||||||
|
argIndex++
|
||||||
|
}
|
||||||
|
if offset > 0 {
|
||||||
|
query += fmt.Sprintf(" OFFSET $%d", argIndex)
|
||||||
|
args = append(args, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.Query(query, args...)
|
rows, err := db.Query(query, args...)
|
||||||
@@ -1375,18 +1386,26 @@ func callAzureTranslate(sourceText, targetLang string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
endpoint = strings.TrimRight(endpoint, "/")
|
endpoint = strings.TrimRight(endpoint, "/")
|
||||||
baseURL, err := url.Parse(endpoint + "/translate")
|
normalizedEndpoint := strings.ToLower(endpoint)
|
||||||
|
translatePath := "/translate"
|
||||||
|
// Azure custom endpoint requires the translator path with version in URL.
|
||||||
|
if strings.Contains(normalizedEndpoint, ".cognitiveservices.azure.com") {
|
||||||
|
translatePath = "/translator/text/v3.0/translate"
|
||||||
|
}
|
||||||
|
baseURL, err := url.Parse(endpoint + translatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("invalid AZURE_TRANSLATOR_ENDPOINT: %w", err)
|
return "", fmt.Errorf("invalid AZURE_TRANSLATOR_ENDPOINT: %w", err)
|
||||||
}
|
}
|
||||||
q := baseURL.Query()
|
q := baseURL.Query()
|
||||||
q.Set("api-version", "3.0")
|
if translatePath == "/translate" {
|
||||||
|
q.Set("api-version", "3.0")
|
||||||
|
}
|
||||||
q.Set("from", sourceLang)
|
q.Set("from", sourceLang)
|
||||||
q.Set("to", targetLang)
|
q.Set("to", targetLang)
|
||||||
baseURL.RawQuery = q.Encode()
|
baseURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
payload := []map[string]string{
|
payload := []map[string]string{
|
||||||
{"Text": sourceText},
|
{"text": sourceText},
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
req, err := http.NewRequest(http.MethodPost, baseURL.String(), bytes.NewReader(body))
|
req, err := http.NewRequest(http.MethodPost, baseURL.String(), bytes.NewReader(body))
|
||||||
|
|||||||
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-sticky-size-start="headerHeight"
|
||||||
:virtual-scroll-slice-size="36"
|
:virtual-scroll-slice-size="36"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
:pagination="{ rowsPerPage: 0 }"
|
v-model:pagination="tablePagination"
|
||||||
hide-bottom
|
hide-bottom
|
||||||
:table-style="tableStyle"
|
:table-style="tableStyle"
|
||||||
|
@virtual-scroll="onTableVirtualScroll"
|
||||||
>
|
>
|
||||||
<template #header="props">
|
<template #header="props">
|
||||||
<q-tr :props="props" class="header-row-fixed">
|
<q-tr :props="props" class="header-row-fixed">
|
||||||
@@ -309,10 +310,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
import { useProductPricingStore } from 'src/stores/ProductPricingStore'
|
||||||
|
|
||||||
const store = useProductPricingStore()
|
const store = useProductPricingStore()
|
||||||
|
const FETCH_LIMIT = 500
|
||||||
|
const nextCursor = ref('')
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
|
||||||
const usdToTry = 38.25
|
const usdToTry = 38.25
|
||||||
const eurToTry = 41.6
|
const eurToTry = 41.6
|
||||||
@@ -381,6 +385,12 @@ const headerFilterFieldSet = new Set([
|
|||||||
])
|
])
|
||||||
|
|
||||||
const mainTableRef = ref(null)
|
const mainTableRef = ref(null)
|
||||||
|
const tablePagination = ref({
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 0,
|
||||||
|
sortBy: 'productCode',
|
||||||
|
descending: false
|
||||||
|
})
|
||||||
const selectedMap = ref({})
|
const selectedMap = ref({})
|
||||||
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
const selectedCurrencies = ref(['USD', 'EUR', 'TRY'])
|
||||||
const showSelectedOnly = ref(false)
|
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 selectedVisibleCount = computed(() => visibleRowIds.value.filter((id) => !!selectedMap.value[id]).length)
|
||||||
const allSelectedVisible = computed(() => visibleRowIds.value.length > 0 && selectedVisibleCount.value === visibleRowIds.value.length)
|
const allSelectedVisible = computed(() => visibleRowIds.value.length > 0 && selectedVisibleCount.value === visibleRowIds.value.length)
|
||||||
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
const someSelectedVisible = computed(() => selectedVisibleCount.value > 0)
|
||||||
|
const hasMoreRows = computed(() => Boolean(store.hasMore))
|
||||||
|
|
||||||
function isHeaderFilterField (field) {
|
function isHeaderFilterField (field) {
|
||||||
return headerFilterFieldSet.has(field)
|
return headerFilterFieldSet.has(field)
|
||||||
@@ -840,12 +851,52 @@ function clearAllCurrencies () {
|
|||||||
selectedCurrencies.value = []
|
selectedCurrencies.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchChunk ({ reset = false } = {}) {
|
||||||
|
const afterProductCode = reset ? '' : nextCursor.value
|
||||||
|
const result = await store.fetchRows({
|
||||||
|
limit: FETCH_LIMIT,
|
||||||
|
afterProductCode,
|
||||||
|
append: !reset
|
||||||
|
})
|
||||||
|
const fetched = Number(result?.fetched) || 0
|
||||||
|
nextCursor.value = String(result?.nextCursor || '')
|
||||||
|
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 () {
|
async function reloadData () {
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
console.info('[product-pricing][ui] reload:start', {
|
console.info('[product-pricing][ui] reload:start', {
|
||||||
at: new Date(startedAt).toISOString()
|
at: new Date(startedAt).toISOString()
|
||||||
})
|
})
|
||||||
await store.fetchRows()
|
nextCursor.value = ''
|
||||||
|
await fetchChunk({ reset: true })
|
||||||
|
await ensureEnoughVisibleRows(120, 6)
|
||||||
console.info('[product-pricing][ui] reload:done', {
|
console.info('[product-pricing][ui] reload:done', {
|
||||||
duration_ms: Date.now() - startedAt,
|
duration_ms: Date.now() - startedAt,
|
||||||
row_count: Array.isArray(store.rows) ? store.rows.length : 0,
|
row_count: Array.isArray(store.rows) ? store.rows.length : 0,
|
||||||
@@ -857,6 +908,19 @@ async function reloadData () {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await reloadData()
|
await reloadData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
columnFilters,
|
||||||
|
numberRangeFilters,
|
||||||
|
dateRangeFilters,
|
||||||
|
showSelectedOnly,
|
||||||
|
() => tablePagination.value.sortBy,
|
||||||
|
() => tablePagination.value.descending
|
||||||
|
],
|
||||||
|
() => { void ensureEnoughVisibleRows(80, 4) },
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,69 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-page v-if="canUpdateLanguage" class="q-pa-md">
|
<q-page v-if="canUpdateLanguage" class="q-pa-md translation-page">
|
||||||
<div class="row q-col-gutter-sm items-end q-mb-md">
|
<div class="translation-toolbar sticky-toolbar">
|
||||||
<div class="col-12 col-md-4">
|
<div class="row q-col-gutter-sm items-end q-mb-md">
|
||||||
<q-input
|
<div class="col-12 col-md-4">
|
||||||
v-model="filters.q"
|
<q-input
|
||||||
dense
|
v-model="filters.q"
|
||||||
outlined
|
dense
|
||||||
clearable
|
outlined
|
||||||
label="Kelime ara"
|
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
|
<q-btn
|
||||||
color="secondary"
|
color="secondary"
|
||||||
icon="sync"
|
icon="done_all"
|
||||||
label="YENİ KELİMELERİ GETİR"
|
label="Seçilenleri Onayla"
|
||||||
|
:disable="selectedKeys.length === 0"
|
||||||
:loading="store.saving"
|
: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>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
|
ref="tableRef"
|
||||||
|
class="translation-table"
|
||||||
flat
|
flat
|
||||||
bordered
|
bordered
|
||||||
dense
|
virtual-scroll
|
||||||
|
:virtual-scroll-sticky-size-start="56"
|
||||||
row-key="t_key"
|
row-key="t_key"
|
||||||
:loading="store.loading || store.saving"
|
:loading="store.loading || store.saving"
|
||||||
:rows="pivotRows"
|
:rows="pivotRows"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
:pagination="{ rowsPerPage: 0 }"
|
v-model:pagination="tablePagination"
|
||||||
|
hide-bottom
|
||||||
|
@virtual-scroll="onVirtualScroll"
|
||||||
>
|
>
|
||||||
<template #body-cell-actions="props">
|
<template #body-cell-actions="props">
|
||||||
<q-td :props="props">
|
<q-td :props="props">
|
||||||
@@ -91,7 +98,9 @@
|
|||||||
|
|
||||||
<template #body-cell-source_text_tr="props">
|
<template #body-cell-source_text_tr="props">
|
||||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'source_text_tr')">
|
<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>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -111,37 +120,79 @@
|
|||||||
|
|
||||||
<template #body-cell-en="props">
|
<template #body-cell-en="props">
|
||||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'en')">
|
<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>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body-cell-de="props">
|
<template #body-cell-de="props">
|
||||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'de')">
|
<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>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body-cell-es="props">
|
<template #body-cell-es="props">
|
||||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'es')">
|
<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>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body-cell-it="props">
|
<template #body-cell-it="props">
|
||||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'it')">
|
<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>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body-cell-ru="props">
|
<template #body-cell-ru="props">
|
||||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'ru')">
|
<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>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body-cell-ar="props">
|
<template #body-cell-ar="props">
|
||||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'ar')">
|
<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>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
@@ -149,13 +200,13 @@
|
|||||||
|
|
||||||
<q-page v-else class="q-pa-md flex flex-center">
|
<q-page v-else class="q-pa-md flex flex-center">
|
||||||
<div class="text-negative text-subtitle1">
|
<div class="text-negative text-subtitle1">
|
||||||
Bu module erisim yetkiniz yok.
|
Bu modüle erişim yetkiniz yok.
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import { usePermission } from 'src/composables/usePermission'
|
import { usePermission } from 'src/composables/usePermission'
|
||||||
import { useTranslationStore } from 'src/stores/translationStore'
|
import { useTranslationStore } from 'src/stores/translationStore'
|
||||||
@@ -169,6 +220,18 @@ const filters = ref({
|
|||||||
q: ''
|
q: ''
|
||||||
})
|
})
|
||||||
const autoTranslate = ref(false)
|
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 = [
|
const sourceTypeOptions = [
|
||||||
{ label: 'dummy', value: 'dummy' },
|
{ label: 'dummy', value: 'dummy' },
|
||||||
@@ -179,15 +242,14 @@ const sourceTypeOptions = [
|
|||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' },
|
{ name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' },
|
||||||
{ name: 'select', label: 'Seç', field: 'select', 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 Metin', field: 'source_text_tr', align: 'left', style: 'min-width: 340px' },
|
||||||
{ name: 'source_text_tr', label: 'Türkçe kaynak', field: 'source_text_tr', align: 'left' },
|
{ name: 'source_type', label: 'Kaynak', field: 'source_type', align: 'left', style: 'min-width: 140px' },
|
||||||
{ name: 'source_type', label: 'Veri tipi', field: 'source_type', align: 'left' },
|
{ name: 'en', label: 'İngilizce', field: 'en', align: 'left', style: 'min-width: 220px' },
|
||||||
{ name: 'en', label: 'English', field: 'en', align: 'left' },
|
{ name: 'de', label: 'Almanca', field: 'de', align: 'left', style: 'min-width: 220px' },
|
||||||
{ name: 'de', label: 'Deutch', field: 'de', align: 'left' },
|
{ name: 'es', label: 'İspanyolca', field: 'es', align: 'left', style: 'min-width: 220px' },
|
||||||
{ name: 'es', label: 'Espanol', field: 'es', align: 'left' },
|
{ name: 'it', label: 'İtalyanca', field: 'it', align: 'left', style: 'min-width: 220px' },
|
||||||
{ name: 'it', label: 'Italiano', field: 'it', align: 'left' },
|
{ name: 'ru', label: 'Rusça', field: 'ru', align: 'left', style: 'min-width: 220px' },
|
||||||
{ name: 'ru', label: 'Русский', field: 'ru', align: 'left' },
|
{ name: 'ar', label: 'Arapça', field: 'ar', align: 'left', style: 'min-width: 220px' }
|
||||||
{ name: 'ar', label: 'العربية', field: 'ar', align: 'left' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const draftByKey = ref({})
|
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))
|
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 draft = {}
|
||||||
const original = {}
|
const original = {}
|
||||||
for (const row of pivotRows.value) {
|
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] = {
|
draft[row.t_key] = {
|
||||||
source_text_tr: row.source_text_tr || '',
|
source_text_tr: row.source_text_tr || '',
|
||||||
source_type: row.source_type || 'dummy',
|
source_type: row.source_type || 'dummy',
|
||||||
@@ -280,8 +365,9 @@ function rowDraft (key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildFilters () {
|
function buildFilters () {
|
||||||
|
const query = String(filters.value.q || '').trim()
|
||||||
return {
|
return {
|
||||||
q: filters.value.q || undefined
|
q: query || undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,13 +435,33 @@ function queueAutoSave (key) {
|
|||||||
autoSaveTimers.set(key, timer)
|
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 () {
|
async function loadRows () {
|
||||||
try {
|
try {
|
||||||
await store.fetchRows(buildFilters())
|
loadedOffset.value = 0
|
||||||
snapshotDrafts()
|
hasMoreRows.value = true
|
||||||
|
await fetchRowsChunk(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[translation-sync][ui] loadRows:error', {
|
console.error('[translation-sync][ui] loadRows:error', {
|
||||||
message: err?.message || 'Ceviri satirlari yuklenemedi'
|
message: err?.message || 'Çeviri satırları yüklenemedi'
|
||||||
})
|
})
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
@@ -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) {
|
async function ensureMissingLangRows (key, draft, langs) {
|
||||||
const missingLangs = []
|
const missingLangs = []
|
||||||
if (!langs.en && String(draft.en || '').trim() !== '') missingLangs.push('en')
|
if (!langs.en && String(draft.en || '').trim() !== '') missingLangs.push('en')
|
||||||
@@ -513,7 +655,7 @@ async function translateSelectedRows () {
|
|||||||
try {
|
try {
|
||||||
const keys = Array.from(new Set(selectedKeys.value.filter(Boolean)))
|
const keys = Array.from(new Set(selectedKeys.value.filter(Boolean)))
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
$q.notify({ type: 'warning', message: 'Çevrilecek seçim bulunamadı' })
|
$q.notify({ type: 'warning', message: 'Çevrilecek seçim bulunamadı' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,10 +671,10 @@ async function translateSelectedRows () {
|
|||||||
await loadRows()
|
await loadRows()
|
||||||
$q.notify({
|
$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: `Seçilenler çevrildi: ${translated}${traceId ? ` | Trace: ${traceId}` : ''}`
|
message: `Seçilenler çevrildi: ${translated}${traceId ? ` | Trace: ${traceId}` : ''}`
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$q.notify({ type: 'negative', message: err?.message || 'Seçili çeviri işlemi başarısız' })
|
$q.notify({ type: 'negative', message: err?.message || 'Seçili çeviri işlemi başarısız' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,11 +760,81 @@ async function syncSources () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.cell-dirty {
|
||||||
background: #fff3cd;
|
background: #fff3cd;
|
||||||
}
|
}
|
||||||
@@ -631,3 +843,4 @@ onMounted(() => {
|
|||||||
background: #d9f7e8;
|
background: #d9f7e8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ function toNumber (value) {
|
|||||||
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
|
return Number.isFinite(n) ? Number(n.toFixed(2)) : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRow (raw, index) {
|
function mapRow (raw, index, baseIndex = 0) {
|
||||||
return {
|
return {
|
||||||
id: index + 1,
|
id: baseIndex + index + 1,
|
||||||
productCode: toText(raw?.ProductCode),
|
productCode: toText(raw?.ProductCode),
|
||||||
stockQty: toNumber(raw?.StockQty),
|
stockQty: toNumber(raw?.StockQty),
|
||||||
stockEntryDate: toText(raw?.StockEntryDate),
|
stockEntryDate: toText(raw?.StockEntryDate),
|
||||||
@@ -55,34 +55,71 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
rows: [],
|
rows: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: ''
|
error: '',
|
||||||
|
hasMore: true
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async fetchRows () {
|
async fetchRows (options = {}) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
const limit = Number(options?.limit) > 0 ? Number(options.limit) : 500
|
||||||
|
const afterProductCode = toText(options?.afterProductCode)
|
||||||
|
const append = Boolean(options?.append)
|
||||||
|
const baseIndex = append ? this.rows.length : 0
|
||||||
const startedAt = Date.now()
|
const startedAt = Date.now()
|
||||||
console.info('[product-pricing][frontend] request:start', {
|
console.info('[product-pricing][frontend] request:start', {
|
||||||
at: new Date(startedAt).toISOString(),
|
at: new Date(startedAt).toISOString(),
|
||||||
timeout_ms: 600000
|
timeout_ms: 180000,
|
||||||
|
limit,
|
||||||
|
after_product_code: afterProductCode || null,
|
||||||
|
append
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
|
const params = { limit }
|
||||||
|
if (afterProductCode) params.after_product_code = afterProductCode
|
||||||
const res = await api.request({
|
const res = await api.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/pricing/products',
|
url: '/pricing/products',
|
||||||
timeout: 600000
|
params,
|
||||||
|
timeout: 180000
|
||||||
})
|
})
|
||||||
const traceId = res?.headers?.['x-trace-id'] || null
|
const traceId = res?.headers?.['x-trace-id'] || null
|
||||||
|
const hasMoreHeader = String(res?.headers?.['x-has-more'] || '').toLowerCase()
|
||||||
|
const nextCursorHeader = toText(res?.headers?.['x-next-cursor'])
|
||||||
const data = Array.isArray(res?.data) ? res.data : []
|
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, baseIndex))
|
||||||
|
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', {
|
console.info('[product-pricing][frontend] request:success', {
|
||||||
trace_id: traceId,
|
trace_id: traceId,
|
||||||
duration_ms: Date.now() - startedAt,
|
duration_ms: Date.now() - startedAt,
|
||||||
row_count: this.rows.length
|
row_count: this.rows.length,
|
||||||
|
fetched_count: mapped.length,
|
||||||
|
has_more: this.hasMore,
|
||||||
|
next_cursor: nextCursorHeader || null
|
||||||
})
|
})
|
||||||
|
return {
|
||||||
|
traceId,
|
||||||
|
fetched: mapped.length,
|
||||||
|
hasMore: this.hasMore,
|
||||||
|
nextCursor: nextCursorHeader
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.rows = []
|
if (!append) this.rows = []
|
||||||
|
this.hasMore = false
|
||||||
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
||||||
this.error = toText(msg)
|
this.error = toText(msg)
|
||||||
console.error('[product-pricing][frontend] request:error', {
|
console.error('[product-pricing][frontend] request:error', {
|
||||||
@@ -92,6 +129,7 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
|||||||
status: err?.response?.status || null,
|
status: err?.response?.status || null,
|
||||||
message: this.error
|
message: this.error
|
||||||
})
|
})
|
||||||
|
throw err
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,27 @@ export const useTranslationStore = defineStore('translation', {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async fetchRows (filters = {}) {
|
async fetchRows (filters = {}, options = {}) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
const append = Boolean(options?.append)
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/language/translations', { params: filters })
|
const res = await api.get('/language/translations', { params: filters })
|
||||||
const payload = res?.data || {}
|
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
|
this.count = Number(payload.count) || this.rows.length
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
|||||||
Reference in New Issue
Block a user