Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -3,12 +3,12 @@ import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime.js'
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat.js'
|
||||
import 'dayjs/locale/tr.js'
|
||||
import { applyDayjsLocale } from 'src/i18n/dayjsLocale'
|
||||
|
||||
// 🔹 Plugin’leri aktif et
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(localizedFormat)
|
||||
dayjs.locale('tr')
|
||||
applyDayjsLocale('tr')
|
||||
|
||||
export default dayjs
|
||||
|
||||
7
ui/src/boot/locale.js
Normal file
7
ui/src/boot/locale.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { boot } from 'quasar/wrappers'
|
||||
import { useLocaleStore } from 'src/stores/localeStore'
|
||||
|
||||
export default boot(() => {
|
||||
const localeStore = useLocaleStore()
|
||||
localeStore.setLocale(localeStore.locale)
|
||||
})
|
||||
42
ui/src/composables/useI18n.js
Normal file
42
ui/src/composables/useI18n.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { messages } from 'src/i18n/messages'
|
||||
import { DEFAULT_LOCALE } from 'src/i18n/languages'
|
||||
import { useLocaleStore } from 'src/stores/localeStore'
|
||||
|
||||
function lookup(obj, path) {
|
||||
return String(path || '')
|
||||
.split('.')
|
||||
.filter(Boolean)
|
||||
.reduce((acc, key) => (acc && acc[key] != null ? acc[key] : undefined), obj)
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const localeStore = useLocaleStore()
|
||||
|
||||
const currentLocale = computed(() => localeStore.locale)
|
||||
|
||||
function fallbackLocales(locale) {
|
||||
const normalized = String(locale || '').toLowerCase()
|
||||
if (normalized === 'tr') return ['tr']
|
||||
if (normalized === 'en') return ['en', 'tr']
|
||||
return [normalized, 'en', 'tr']
|
||||
}
|
||||
|
||||
function t(key) {
|
||||
for (const locale of fallbackLocales(currentLocale.value)) {
|
||||
const val = lookup(messages[locale] || {}, key)
|
||||
if (val != null) return val
|
||||
}
|
||||
|
||||
const byDefault = lookup(messages[DEFAULT_LOCALE] || {}, key)
|
||||
if (byDefault != null) return byDefault
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
return {
|
||||
locale: currentLocale,
|
||||
t
|
||||
}
|
||||
}
|
||||
30
ui/src/i18n/dayjsLocale.js
Normal file
30
ui/src/i18n/dayjsLocale.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/tr.js'
|
||||
import 'dayjs/locale/en.js'
|
||||
import 'dayjs/locale/de.js'
|
||||
import 'dayjs/locale/it.js'
|
||||
import 'dayjs/locale/es.js'
|
||||
import 'dayjs/locale/ru.js'
|
||||
import 'dayjs/locale/ar.js'
|
||||
|
||||
import { normalizeLocale } from './languages.js'
|
||||
|
||||
export const DATE_LOCALE_MAP = {
|
||||
tr: 'tr-TR',
|
||||
en: 'en-US',
|
||||
de: 'de-DE',
|
||||
it: 'it-IT',
|
||||
es: 'es-ES',
|
||||
ru: 'ru-RU',
|
||||
ar: 'ar'
|
||||
}
|
||||
|
||||
export function applyDayjsLocale(locale) {
|
||||
const normalized = normalizeLocale(locale)
|
||||
dayjs.locale(normalized)
|
||||
}
|
||||
|
||||
export function getDateLocale(locale) {
|
||||
const normalized = normalizeLocale(locale)
|
||||
return DATE_LOCALE_MAP[normalized] || DATE_LOCALE_MAP.tr
|
||||
}
|
||||
32
ui/src/i18n/languages.js
Normal file
32
ui/src/i18n/languages.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export const DEFAULT_LOCALE = 'tr'
|
||||
|
||||
export const SUPPORTED_LOCALES = ['tr', 'en', 'de', 'it', 'es', 'ru', 'ar']
|
||||
|
||||
export const UI_LANGUAGE_OPTIONS = [
|
||||
{ label: 'Türkçe', value: 'tr', short: 'TUR', flag: '🇹🇷' },
|
||||
{ label: 'English', value: 'en', short: 'ENG', flag: '🇬🇧' },
|
||||
{ label: 'Deutsch', value: 'de', short: 'DEU', flag: '🇩🇪' },
|
||||
{ label: 'Italiano', value: 'it', short: 'ITA', flag: '🇮🇹' },
|
||||
{ label: 'Español', value: 'es', short: 'ESP', flag: '🇪🇸' },
|
||||
{ label: 'Русский', value: 'ru', short: 'RUS', flag: '🇷🇺' },
|
||||
{ label: 'العربية', value: 'ar', short: 'ARA', flag: '🇸🇦' }
|
||||
]
|
||||
|
||||
export const BACKEND_LANG_MAP = {
|
||||
tr: 'TR',
|
||||
en: 'EN',
|
||||
de: 'DE',
|
||||
it: 'IT',
|
||||
es: 'ES',
|
||||
ru: 'RU',
|
||||
ar: 'AR'
|
||||
}
|
||||
|
||||
export function normalizeLocale(value) {
|
||||
const locale = String(value || '').trim().toLowerCase()
|
||||
return SUPPORTED_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
export function toBackendLangCode(locale) {
|
||||
return BACKEND_LANG_MAP[normalizeLocale(locale)] || BACKEND_LANG_MAP[DEFAULT_LOCALE]
|
||||
}
|
||||
28
ui/src/i18n/messages.js
Normal file
28
ui/src/i18n/messages.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export const messages = {
|
||||
tr: {
|
||||
app: {
|
||||
title: 'Baggi Software System',
|
||||
logoutTitle: 'Çıkış Yap',
|
||||
logoutConfirm: 'Oturumunuzu kapatmak istediğinize emin misiniz?',
|
||||
changePassword: 'Şifre Değiştir',
|
||||
language: 'Dil'
|
||||
},
|
||||
statement: {
|
||||
invalidDateRange: 'Başlangıç tarihi bitiş tarihinden sonra olamaz.',
|
||||
selectFilters: 'Lütfen cari ve tarih aralığını seçiniz.'
|
||||
}
|
||||
},
|
||||
en: {
|
||||
app: {
|
||||
title: 'Baggi Software System',
|
||||
logoutTitle: 'Log Out',
|
||||
logoutConfirm: 'Are you sure you want to end your session?',
|
||||
changePassword: 'Change Password',
|
||||
language: 'Language'
|
||||
},
|
||||
statement: {
|
||||
invalidDateRange: 'Start date cannot be later than end date.',
|
||||
selectFilters: 'Please select account and date range.'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,41 @@
|
||||
<q-avatar class="bg-secondary q-mr-sm">
|
||||
<img src="/images/Baggi-tekstilas-logolu.jpg" />
|
||||
</q-avatar>
|
||||
Baggi Software System
|
||||
{{ t('app.title') }}
|
||||
</q-toolbar-title>
|
||||
|
||||
<q-select
|
||||
v-model="selectedLocale"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
options-dense
|
||||
class="q-mr-sm lang-select"
|
||||
option-value="value"
|
||||
option-label="label"
|
||||
:options="languageOptions"
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div class="lang-item">
|
||||
<span class="lang-flag">{{ scope.opt.flag }}</span>
|
||||
<span class="lang-short">{{ scope.opt.short }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section>
|
||||
<div class="lang-item">
|
||||
<span class="lang-flag">{{ scope.opt.flag }}</span>
|
||||
<span class="lang-short">{{ scope.opt.short }}</span>
|
||||
<span>{{ scope.opt.label }}</span>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-btn flat dense round icon="logout" @click="confirmLogout" />
|
||||
|
||||
</q-toolbar>
|
||||
@@ -99,7 +131,7 @@
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
Şifre Değiştir
|
||||
{{ t('app.changePassword') }}
|
||||
</q-item-section>
|
||||
|
||||
</q-item>
|
||||
@@ -122,7 +154,7 @@
|
||||
<q-toolbar class="bg-secondary">
|
||||
|
||||
<q-toolbar-title>
|
||||
Baggi Software System
|
||||
{{ t('app.title') }}
|
||||
</q-toolbar-title>
|
||||
|
||||
</q-toolbar>
|
||||
@@ -138,6 +170,9 @@ import { Dialog, useQuasar } from 'quasar'
|
||||
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
import { usePermissionStore } from 'stores/permissionStore'
|
||||
import { useI18n } from 'src/composables/useI18n'
|
||||
import { UI_LANGUAGE_OPTIONS } from 'src/i18n/languages'
|
||||
import { useLocaleStore } from 'src/stores/localeStore'
|
||||
|
||||
|
||||
/* ================= STORES ================= */
|
||||
@@ -147,6 +182,16 @@ const route = useRoute()
|
||||
const $q = useQuasar()
|
||||
const auth = useAuthStore()
|
||||
const perm = usePermissionStore()
|
||||
const localeStore = useLocaleStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const languageOptions = UI_LANGUAGE_OPTIONS
|
||||
const selectedLocale = computed({
|
||||
get: () => localeStore.locale,
|
||||
set: (value) => {
|
||||
localeStore.setLocale(value)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
/* ================= UI ================= */
|
||||
@@ -159,8 +204,8 @@ function toggleLeftDrawer () {
|
||||
|
||||
function confirmLogout () {
|
||||
Dialog.create({
|
||||
title: 'Çıkış Yap',
|
||||
message: 'Oturumunuzu kapatmak istediğinize emin misiniz?',
|
||||
title: t('app.logoutTitle'),
|
||||
message: t('app.logoutConfirm'),
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(() => {
|
||||
@@ -330,6 +375,18 @@ const menuItems = [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Dil Çeviri',
|
||||
icon: 'translate',
|
||||
|
||||
children: [
|
||||
{
|
||||
label: 'Çeviri Tablosu',
|
||||
to: '/app/language/translations',
|
||||
permission: 'language:update'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Kullanıcı Yönetimi',
|
||||
@@ -387,5 +444,27 @@ const filteredMenu = computed(() => {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.lang-select {
|
||||
width: 140px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.lang-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lang-flag {
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.lang-short {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -841,7 +841,16 @@ function clearAllCurrencies () {
|
||||
}
|
||||
|
||||
async function reloadData () {
|
||||
const startedAt = Date.now()
|
||||
console.info('[product-pricing][ui] reload:start', {
|
||||
at: new Date(startedAt).toISOString()
|
||||
})
|
||||
await store.fetchRows()
|
||||
console.info('[product-pricing][ui] reload:done', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
row_count: Array.isArray(store.rows) ? store.rows.length : 0,
|
||||
has_error: Boolean(store.error)
|
||||
})
|
||||
selectedMap.value = {}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="dateFrom" mask="YYYY-MM-DD" locale="tr-TR"/>
|
||||
<q-date v-model="dateFrom" mask="YYYY-MM-DD" :locale="dateLocale"/>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
@@ -63,7 +63,7 @@
|
||||
<template #append>
|
||||
<q-icon name="event" class="cursor-pointer">
|
||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||
<q-date v-model="dateTo" mask="YYYY-MM-DD" locale="tr-TR" />
|
||||
<q-date v-model="dateTo" mask="YYYY-MM-DD" :locale="dateLocale" />
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
@@ -277,12 +277,16 @@ import { useDownloadstpdfStore } from 'src/stores/downloadstpdfStore'
|
||||
import dayjs from 'dayjs'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { normalizeSearchText } from 'src/utils/searchText'
|
||||
import { useLocaleStore } from 'src/stores/localeStore'
|
||||
import { getDateLocale } from 'src/i18n/dayjsLocale'
|
||||
|
||||
const { canRead, canExport } = usePermission()
|
||||
const canReadFinance = canRead('finance')
|
||||
const canExportFinance = canExport('finance')
|
||||
|
||||
const $q = useQuasar()
|
||||
const localeStore = useLocaleStore()
|
||||
const dateLocale = computed(() => getDateLocale(localeStore.locale))
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const statementheaderStore = useStatementheaderStore()
|
||||
@@ -363,7 +367,7 @@ async function onFilterClick() {
|
||||
startdate: dateFrom.value,
|
||||
enddate: dateTo.value,
|
||||
accountcode: selectedCari.value,
|
||||
langcode: 'TR',
|
||||
langcode: localeStore.backendLangCode,
|
||||
parislemler: selectedMonType.value
|
||||
})
|
||||
|
||||
@@ -411,7 +415,7 @@ function resetFilters() {
|
||||
/* Format */
|
||||
function formatAmount(n) {
|
||||
if (n == null || isNaN(n)) return '0,00'
|
||||
return new Intl.NumberFormat('tr-TR', {
|
||||
return new Intl.NumberFormat(dateLocale.value, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(n)
|
||||
@@ -467,7 +471,8 @@ async function handleDownload() {
|
||||
selectedCari.value, // accountCode
|
||||
dateFrom.value, // startDate
|
||||
dateTo.value, // endDate
|
||||
selectedMonType.value // <-- eklendi (['1','2'] veya ['1','3'])
|
||||
selectedMonType.value, // <-- eklendi (['1','2'] veya ['1','3'])
|
||||
localeStore.backendLangCode
|
||||
)
|
||||
|
||||
console.log("📤 [DEBUG] Store’dan gelen result:", result)
|
||||
@@ -508,7 +513,8 @@ async function CurrheadDownload() {
|
||||
selectedCari.value, // accountCode
|
||||
dateFrom.value, // startDate
|
||||
dateTo.value, // endDate
|
||||
selectedMonType.value // parasal işlem tipi (parislemler)
|
||||
selectedMonType.value, // parasal işlem tipi (parislemler)
|
||||
localeStore.backendLangCode
|
||||
)
|
||||
|
||||
console.log("📤 [DEBUG] CurrheadDownloadresult:", result)
|
||||
|
||||
633
ui/src/pages/TranslationTable.vue
Normal file
633
ui/src/pages/TranslationTable.vue
Normal file
@@ -0,0 +1,633 @@
|
||||
<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"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
<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
|
||||
flat
|
||||
bordered
|
||||
dense
|
||||
row-key="t_key"
|
||||
:loading="store.loading || store.saving"
|
||||
:rows="pivotRows"
|
||||
:columns="columns"
|
||||
:rows-per-page-options="[0]"
|
||||
:pagination="{ rowsPerPage: 0 }"
|
||||
>
|
||||
<template #body-cell-actions="props">
|
||||
<q-td :props="props">
|
||||
<q-btn
|
||||
dense
|
||||
color="primary"
|
||||
icon="save"
|
||||
label="Güncelle"
|
||||
:disable="!rowHasChanges(props.row.t_key)"
|
||||
:loading="store.saving"
|
||||
@click="saveRow(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-select="props">
|
||||
<q-td :props="props">
|
||||
<q-checkbox
|
||||
dense
|
||||
:model-value="selectedKeys.includes(props.row.t_key)"
|
||||
@update:model-value="(v) => toggleSelected(props.row.t_key, v)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<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)" />
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template #body-cell-source_type="props">
|
||||
<q-td :props="props" :class="cellClass(props.row.t_key, 'source_type')">
|
||||
<q-select
|
||||
v-model="rowDraft(props.row.t_key).source_type"
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:options="sourceTypeOptions"
|
||||
@update:model-value="() => queueAutoSave(props.row.t_key)"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<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-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-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-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-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-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-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-page>
|
||||
|
||||
<q-page v-else class="q-pa-md flex flex-center">
|
||||
<div class="text-negative text-subtitle1">
|
||||
Bu module erisim yetkiniz yok.
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { useTranslationStore } from 'src/stores/translationStore'
|
||||
|
||||
const $q = useQuasar()
|
||||
const store = useTranslationStore()
|
||||
const { canUpdate } = usePermission()
|
||||
const canUpdateLanguage = canUpdate('language')
|
||||
|
||||
const filters = ref({
|
||||
q: ''
|
||||
})
|
||||
const autoTranslate = ref(false)
|
||||
|
||||
const sourceTypeOptions = [
|
||||
{ label: 'dummy', value: 'dummy' },
|
||||
{ label: 'postgre', value: 'postgre' },
|
||||
{ label: 'mssql', value: 'mssql' }
|
||||
]
|
||||
|
||||
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' }
|
||||
]
|
||||
|
||||
const draftByKey = ref({})
|
||||
const originalByKey = ref({})
|
||||
const selectedKeys = ref([])
|
||||
const autoSaveTimers = new Map()
|
||||
|
||||
const pivotRows = computed(() => {
|
||||
const byKey = new Map()
|
||||
for (const row of store.rows) {
|
||||
const key = row.t_key
|
||||
if (!byKey.has(key)) {
|
||||
byKey.set(key, {
|
||||
t_key: key,
|
||||
source_text_tr: '',
|
||||
source_type: 'dummy',
|
||||
en: '',
|
||||
de: '',
|
||||
es: '',
|
||||
it: '',
|
||||
ru: '',
|
||||
ar: '',
|
||||
langs: {}
|
||||
})
|
||||
}
|
||||
|
||||
const target = byKey.get(key)
|
||||
target.langs[row.lang_code] = {
|
||||
id: row.id,
|
||||
status: row.status,
|
||||
is_manual: row.is_manual
|
||||
}
|
||||
|
||||
if (row.lang_code === 'tr') {
|
||||
target.source_text_tr = row.translated_text || row.source_text_tr || ''
|
||||
target.source_type = row.source_type || 'dummy'
|
||||
} else if (row.lang_code === 'en') {
|
||||
target.en = row.translated_text || ''
|
||||
} else if (row.lang_code === 'de') {
|
||||
target.de = row.translated_text || ''
|
||||
} else if (row.lang_code === 'es') {
|
||||
target.es = row.translated_text || ''
|
||||
} else if (row.lang_code === 'it') {
|
||||
target.it = row.translated_text || ''
|
||||
} else if (row.lang_code === 'ru') {
|
||||
target.ru = row.translated_text || ''
|
||||
} else if (row.lang_code === 'ar') {
|
||||
target.ar = row.translated_text || ''
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byKey.values()).sort((a, b) => a.t_key.localeCompare(b.t_key))
|
||||
})
|
||||
|
||||
function snapshotDrafts () {
|
||||
const draft = {}
|
||||
const original = {}
|
||||
for (const row of pivotRows.value) {
|
||||
draft[row.t_key] = {
|
||||
source_text_tr: row.source_text_tr || '',
|
||||
source_type: row.source_type || 'dummy',
|
||||
en: row.en || '',
|
||||
de: row.de || '',
|
||||
es: row.es || '',
|
||||
it: row.it || '',
|
||||
ru: row.ru || '',
|
||||
ar: row.ar || ''
|
||||
}
|
||||
original[row.t_key] = { ...draft[row.t_key] }
|
||||
}
|
||||
draftByKey.value = draft
|
||||
originalByKey.value = original
|
||||
selectedKeys.value = selectedKeys.value.filter(k => draft[k])
|
||||
}
|
||||
|
||||
function rowDraft (key) {
|
||||
if (!draftByKey.value[key]) {
|
||||
draftByKey.value[key] = {
|
||||
source_text_tr: '',
|
||||
source_type: 'dummy',
|
||||
en: '',
|
||||
de: '',
|
||||
es: '',
|
||||
it: '',
|
||||
ru: '',
|
||||
ar: ''
|
||||
}
|
||||
}
|
||||
return draftByKey.value[key]
|
||||
}
|
||||
|
||||
function buildFilters () {
|
||||
return {
|
||||
q: filters.value.q || undefined
|
||||
}
|
||||
}
|
||||
|
||||
function rowHasChanges (key) {
|
||||
const draft = draftByKey.value[key]
|
||||
const orig = originalByKey.value[key]
|
||||
if (!draft || !orig) return false
|
||||
return (
|
||||
draft.source_text_tr !== orig.source_text_tr ||
|
||||
draft.source_type !== orig.source_type ||
|
||||
draft.en !== orig.en ||
|
||||
draft.de !== orig.de ||
|
||||
draft.es !== orig.es ||
|
||||
draft.it !== orig.it ||
|
||||
draft.ru !== orig.ru ||
|
||||
draft.ar !== orig.ar
|
||||
)
|
||||
}
|
||||
|
||||
function isPending (key, lang) {
|
||||
const row = pivotRows.value.find(r => r.t_key === key)
|
||||
const meta = row?.langs?.[lang]
|
||||
return meta?.status === 'pending'
|
||||
}
|
||||
|
||||
function cellClass (key, field) {
|
||||
const draft = draftByKey.value[key]
|
||||
const orig = originalByKey.value[key]
|
||||
if (!draft || !orig) return ''
|
||||
|
||||
if (draft[field] !== orig[field]) return 'cell-dirty'
|
||||
|
||||
if (field === 'en' && isPending(key, 'en')) return 'cell-new'
|
||||
if (field === 'de' && isPending(key, 'de')) return 'cell-new'
|
||||
if (field === 'es' && isPending(key, 'es')) return 'cell-new'
|
||||
if (field === 'it' && isPending(key, 'it')) return 'cell-new'
|
||||
if (field === 'ru' && isPending(key, 'ru')) return 'cell-new'
|
||||
if (field === 'ar' && isPending(key, 'ar')) return 'cell-new'
|
||||
if (field === 'source_text_tr' && isPending(key, 'tr')) return 'cell-new'
|
||||
return ''
|
||||
}
|
||||
|
||||
function toggleSelected (key, checked) {
|
||||
if (checked) {
|
||||
if (!selectedKeys.value.includes(key)) {
|
||||
selectedKeys.value = [...selectedKeys.value, key]
|
||||
}
|
||||
return
|
||||
}
|
||||
selectedKeys.value = selectedKeys.value.filter(k => k !== key)
|
||||
}
|
||||
|
||||
function queueAutoSave (key) {
|
||||
if (!key) return
|
||||
const existing = autoSaveTimers.get(key)
|
||||
if (existing) {
|
||||
clearTimeout(existing)
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
autoSaveTimers.delete(key)
|
||||
if (rowHasChanges(key)) {
|
||||
void saveRow(key)
|
||||
}
|
||||
}, 250)
|
||||
autoSaveTimers.set(key, timer)
|
||||
}
|
||||
|
||||
async function loadRows () {
|
||||
try {
|
||||
await store.fetchRows(buildFilters())
|
||||
snapshotDrafts()
|
||||
} catch (err) {
|
||||
console.error('[translation-sync][ui] loadRows:error', {
|
||||
message: err?.message || 'Ceviri satirlari yuklenemedi'
|
||||
})
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: err?.message || 'Çeviri satırları yüklenemedi'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureMissingLangRows (key, draft, langs) {
|
||||
const missingLangs = []
|
||||
if (!langs.en && String(draft.en || '').trim() !== '') missingLangs.push('en')
|
||||
if (!langs.de && String(draft.de || '').trim() !== '') missingLangs.push('de')
|
||||
if (!langs.es && String(draft.es || '').trim() !== '') missingLangs.push('es')
|
||||
if (!langs.it && String(draft.it || '').trim() !== '') missingLangs.push('it')
|
||||
if (!langs.ru && String(draft.ru || '').trim() !== '') missingLangs.push('ru')
|
||||
if (!langs.ar && String(draft.ar || '').trim() !== '') missingLangs.push('ar')
|
||||
if (missingLangs.length === 0) return false
|
||||
|
||||
await store.upsertMissing([
|
||||
{
|
||||
t_key: key,
|
||||
source_text_tr: draft.source_text_tr || key
|
||||
}
|
||||
], missingLangs)
|
||||
return true
|
||||
}
|
||||
|
||||
function buildRowUpdates (row, draft, original, approveStatus = 'approved') {
|
||||
const items = []
|
||||
const langs = row.langs || {}
|
||||
const sourceTypeChanged = draft.source_type !== original.source_type
|
||||
|
||||
if (langs.tr?.id && (draft.source_text_tr !== original.source_text_tr || sourceTypeChanged)) {
|
||||
items.push({
|
||||
id: langs.tr.id,
|
||||
source_text_tr: draft.source_text_tr,
|
||||
translated_text: draft.source_text_tr,
|
||||
source_type: draft.source_type,
|
||||
status: approveStatus,
|
||||
is_manual: true
|
||||
})
|
||||
}
|
||||
if (langs.en?.id && (draft.en !== original.en || sourceTypeChanged)) {
|
||||
items.push({
|
||||
id: langs.en.id,
|
||||
translated_text: draft.en,
|
||||
source_type: draft.source_type,
|
||||
status: approveStatus,
|
||||
is_manual: true
|
||||
})
|
||||
}
|
||||
if (langs.de?.id && (draft.de !== original.de || sourceTypeChanged)) {
|
||||
items.push({
|
||||
id: langs.de.id,
|
||||
translated_text: draft.de,
|
||||
source_type: draft.source_type,
|
||||
status: approveStatus,
|
||||
is_manual: true
|
||||
})
|
||||
}
|
||||
if (langs.es?.id && (draft.es !== original.es || sourceTypeChanged)) {
|
||||
items.push({
|
||||
id: langs.es.id,
|
||||
translated_text: draft.es,
|
||||
source_type: draft.source_type,
|
||||
status: approveStatus,
|
||||
is_manual: true
|
||||
})
|
||||
}
|
||||
if (langs.it?.id && (draft.it !== original.it || sourceTypeChanged)) {
|
||||
items.push({
|
||||
id: langs.it.id,
|
||||
translated_text: draft.it,
|
||||
source_type: draft.source_type,
|
||||
status: approveStatus,
|
||||
is_manual: true
|
||||
})
|
||||
}
|
||||
if (langs.ru?.id && (draft.ru !== original.ru || sourceTypeChanged)) {
|
||||
items.push({
|
||||
id: langs.ru.id,
|
||||
translated_text: draft.ru,
|
||||
source_type: draft.source_type,
|
||||
status: approveStatus,
|
||||
is_manual: true
|
||||
})
|
||||
}
|
||||
if (langs.ar?.id && (draft.ar !== original.ar || sourceTypeChanged)) {
|
||||
items.push({
|
||||
id: langs.ar.id,
|
||||
translated_text: draft.ar,
|
||||
source_type: draft.source_type,
|
||||
status: approveStatus,
|
||||
is_manual: true
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
async function saveRow (key) {
|
||||
const row = pivotRows.value.find(r => r.t_key === key)
|
||||
const draft = draftByKey.value[key]
|
||||
const original = originalByKey.value[key]
|
||||
if (!row || !draft || !original || !rowHasChanges(key)) return
|
||||
|
||||
try {
|
||||
const insertedMissing = await ensureMissingLangRows(key, draft, row.langs || {})
|
||||
if (insertedMissing) {
|
||||
await loadRows()
|
||||
}
|
||||
|
||||
const refreshed = pivotRows.value.find(r => r.t_key === key)
|
||||
if (!refreshed) return
|
||||
const refreshDraft = draftByKey.value[key]
|
||||
const refreshOriginal = originalByKey.value[key]
|
||||
const items = buildRowUpdates(refreshed, refreshDraft, refreshOriginal)
|
||||
if (items.length > 0) {
|
||||
await store.bulkUpdate(items)
|
||||
}
|
||||
|
||||
await loadRows()
|
||||
$q.notify({ type: 'positive', message: 'Satır güncellendi' })
|
||||
} catch (err) {
|
||||
$q.notify({ type: 'negative', message: err?.message || 'Güncelleme hatası' })
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkApproveSelected () {
|
||||
try {
|
||||
const ids = []
|
||||
for (const key of selectedKeys.value) {
|
||||
const row = pivotRows.value.find(r => r.t_key === key)
|
||||
if (!row) continue
|
||||
for (const lang of ['tr', 'en', 'de', 'es', 'it', 'ru', 'ar']) {
|
||||
const meta = row.langs?.[lang]
|
||||
if (meta?.id && meta?.status === 'pending') {
|
||||
ids.push(meta.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
const unique = Array.from(new Set(ids))
|
||||
if (unique.length === 0) {
|
||||
$q.notify({ type: 'warning', message: 'Onaylanacak pending kayıt bulunamadı' })
|
||||
return
|
||||
}
|
||||
await store.bulkApprove(unique)
|
||||
await loadRows()
|
||||
$q.notify({ type: 'positive', message: `${unique.length} kayıt onaylandı` })
|
||||
} catch (err) {
|
||||
$q.notify({ type: 'negative', message: err?.message || 'Toplu onay hatası' })
|
||||
}
|
||||
}
|
||||
|
||||
async function translateSelectedRows () {
|
||||
try {
|
||||
const keys = Array.from(new Set(selectedKeys.value.filter(Boolean)))
|
||||
if (keys.length === 0) {
|
||||
$q.notify({ type: 'warning', message: 'Çevrilecek seçim bulunamadı' })
|
||||
return
|
||||
}
|
||||
|
||||
const response = await store.translateSelected({
|
||||
t_keys: keys,
|
||||
languages: ['en', 'de', 'it', 'es', 'ru', 'ar'],
|
||||
limit: Math.min(50000, keys.length * 6)
|
||||
})
|
||||
|
||||
const translated = Number(response?.translated_count || 0)
|
||||
const traceId = response?.trace_id || null
|
||||
|
||||
await loadRows()
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `Seçilenler çevrildi: ${translated}${traceId ? ` | Trace: ${traceId}` : ''}`
|
||||
})
|
||||
} catch (err) {
|
||||
$q.notify({ type: 'negative', message: err?.message || 'Seçili çeviri işlemi başarısız' })
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkSaveSelected () {
|
||||
try {
|
||||
const items = []
|
||||
for (const key of selectedKeys.value) {
|
||||
const row = pivotRows.value.find(r => r.t_key === key)
|
||||
const draft = draftByKey.value[key]
|
||||
const original = originalByKey.value[key]
|
||||
if (!row || !draft || !original) continue
|
||||
if (!rowHasChanges(key)) continue
|
||||
|
||||
const insertedMissing = await ensureMissingLangRows(key, draft, row.langs || {})
|
||||
if (insertedMissing) {
|
||||
await loadRows()
|
||||
}
|
||||
|
||||
const refreshed = pivotRows.value.find(r => r.t_key === key)
|
||||
if (!refreshed) continue
|
||||
const refreshDraft = draftByKey.value[key]
|
||||
const refreshOriginal = originalByKey.value[key]
|
||||
items.push(...buildRowUpdates(refreshed, refreshDraft, refreshOriginal))
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
$q.notify({ type: 'warning', message: 'Toplu güncellenecek değişiklik yok' })
|
||||
return
|
||||
}
|
||||
|
||||
await store.bulkUpdate(items)
|
||||
await loadRows()
|
||||
$q.notify({ type: 'positive', message: `${items.length} kayıt toplu güncellendi` })
|
||||
} catch (err) {
|
||||
$q.notify({ type: 'negative', message: err?.message || 'Toplu güncelleme hatası' })
|
||||
}
|
||||
}
|
||||
|
||||
async function syncSources () {
|
||||
const startedAt = Date.now()
|
||||
const beforeCount = pivotRows.value.length
|
||||
console.info('[translation-sync][ui] button:click', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
auto_translate: autoTranslate.value,
|
||||
only_new: true,
|
||||
before_row_count: beforeCount
|
||||
})
|
||||
try {
|
||||
const response = await store.syncSources({
|
||||
auto_translate: autoTranslate.value,
|
||||
languages: ['en', 'de', 'it', 'es', 'ru', 'ar'],
|
||||
limit: 1000,
|
||||
only_new: true
|
||||
})
|
||||
const result = response?.result || response || {}
|
||||
const traceId = result?.trace_id || response?.trace_id || null
|
||||
console.info('[translation-sync][ui] sync:response', {
|
||||
trace_id: traceId,
|
||||
seed_count: result.seed_count || 0,
|
||||
affected_count: result.affected_count || 0,
|
||||
auto_translated: result.auto_translated || 0,
|
||||
duration_ms: result.duration_ms || null
|
||||
})
|
||||
await loadRows()
|
||||
const afterCount = pivotRows.value.length
|
||||
console.info('[translation-sync][ui] chain:reload-complete', {
|
||||
trace_id: traceId,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
before_row_count: beforeCount,
|
||||
after_row_count: afterCount,
|
||||
delta_row_count: afterCount - beforeCount
|
||||
})
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `Tarama tamamlandı. Seed: ${result.seed_count || 0}, Oto çeviri: ${result.auto_translated || 0}`
|
||||
})
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: err?.message || 'Kaynak tarama hatası'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRows()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cell-dirty {
|
||||
background: #fff3cd;
|
||||
}
|
||||
|
||||
.cell-new {
|
||||
background: #d9f7e8;
|
||||
}
|
||||
</style>
|
||||
@@ -42,7 +42,7 @@
|
||||
<q-date
|
||||
v-model="dateFrom"
|
||||
mask="YYYY-MM-DD"
|
||||
locale="tr-TR"
|
||||
:locale="dateLocale"
|
||||
:options="isValidFromDate"
|
||||
/>
|
||||
</q-popup-proxy>
|
||||
@@ -65,7 +65,7 @@
|
||||
<q-date
|
||||
v-model="dateTo"
|
||||
mask="YYYY-MM-DD"
|
||||
locale="tr-TR"
|
||||
:locale="dateLocale"
|
||||
:options="isValidToDate"
|
||||
/>
|
||||
</q-popup-proxy>
|
||||
@@ -281,12 +281,18 @@ import { useStatementdetailStore } from 'src/stores/statementdetailStore'
|
||||
import { useDownloadstpdfStore } from 'src/stores/downloadstpdfStore'
|
||||
import dayjs from 'dayjs'
|
||||
import { usePermission } from 'src/composables/usePermission'
|
||||
import { useLocaleStore } from 'src/stores/localeStore'
|
||||
import { getDateLocale } from 'src/i18n/dayjsLocale'
|
||||
import { useI18n } from 'src/composables/useI18n'
|
||||
|
||||
const { canRead, canExport } = usePermission()
|
||||
const canReadFinance = canRead('finance')
|
||||
const canExportFinance = canExport('finance')
|
||||
|
||||
const $q = useQuasar()
|
||||
const localeStore = useLocaleStore()
|
||||
const { t } = useI18n()
|
||||
const dateLocale = computed(() => getDateLocale(localeStore.locale))
|
||||
|
||||
const accountStore = useAccountStore()
|
||||
const statementheaderStore = useStatementheaderStore()
|
||||
@@ -360,7 +366,7 @@ function hasInvalidDateRange () {
|
||||
function notifyInvalidDateRange () {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Başlangıç tarihi bitiş tarihinden sonra olamaz.',
|
||||
message: t('statement.invalidDateRange'),
|
||||
position: 'top-right'
|
||||
})
|
||||
}
|
||||
@@ -402,7 +408,7 @@ async function onFilterClick() {
|
||||
if (!selectedCari.value || !dateFrom.value || !dateTo.value) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: '⚠️ Lütfen cari ve tarih aralığını seçiniz.',
|
||||
message: t('statement.selectFilters'),
|
||||
position: 'top-right'
|
||||
})
|
||||
return
|
||||
@@ -417,7 +423,7 @@ async function onFilterClick() {
|
||||
startdate: dateFrom.value,
|
||||
enddate: dateTo.value,
|
||||
accountcode: selectedCari.value,
|
||||
langcode: 'TR',
|
||||
langcode: localeStore.backendLangCode,
|
||||
parislemler: selectedMonType.value,
|
||||
excludeopening: excludeOpening.value
|
||||
})
|
||||
@@ -483,7 +489,7 @@ function toggleFiltersCollapsed () {
|
||||
function normalizeText (str) {
|
||||
return (str || '')
|
||||
.toString()
|
||||
.toLocaleLowerCase('tr-TR') // Türkçe uyumlu
|
||||
.toLocaleLowerCase(dateLocale.value)
|
||||
.normalize('NFD') // aksan temizleme
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim()
|
||||
@@ -503,7 +509,7 @@ function resetFilters() {
|
||||
/* Format */
|
||||
function formatAmount(n) {
|
||||
if (n == null || isNaN(n)) return '0,00'
|
||||
return new Intl.NumberFormat('tr-TR', {
|
||||
return new Intl.NumberFormat(dateLocale.value, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(n)
|
||||
@@ -562,7 +568,8 @@ async function handleDownload() {
|
||||
selectedCari.value, // accountCode
|
||||
dateFrom.value, // startDate
|
||||
dateTo.value, // endDate
|
||||
selectedMonType.value // <-- eklendi (['1','2'] veya ['1','3'])
|
||||
selectedMonType.value, // <-- eklendi (['1','2'] veya ['1','3'])
|
||||
localeStore.backendLangCode
|
||||
)
|
||||
|
||||
console.log("[DEBUG] Store’dan gelen result:", result)
|
||||
@@ -608,7 +615,8 @@ async function CurrheadDownload() {
|
||||
selectedCari.value, // accountCode
|
||||
dateFrom.value, // startDate
|
||||
dateTo.value, // endDate
|
||||
selectedMonType.value // parasal işlem tipi (parislemler)
|
||||
selectedMonType.value, // parasal işlem tipi (parislemler)
|
||||
localeStore.backendLangCode
|
||||
)
|
||||
|
||||
console.log("[DEBUG] CurrheadDownloadresult:", result)
|
||||
|
||||
@@ -228,6 +228,12 @@ const routes = [
|
||||
component: () => import('../pages/MarketMailMapping.vue'),
|
||||
meta: { permission: 'system:update' }
|
||||
},
|
||||
{
|
||||
path: 'language/translations',
|
||||
name: 'translation-table',
|
||||
component: () => import('pages/TranslationTable.vue'),
|
||||
meta: { permission: 'language:update' }
|
||||
},
|
||||
|
||||
|
||||
/* ================= ORDERS ================= */
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import axios from 'axios'
|
||||
import qs from 'qs'
|
||||
import { useAuthStore } from 'stores/authStore'
|
||||
import { DEFAULT_LOCALE, normalizeLocale } from 'src/i18n/languages'
|
||||
|
||||
const rawBaseUrl =
|
||||
(typeof process !== 'undefined' && process.env?.VITE_API_BASE_URL) || '/api'
|
||||
|
||||
export const API_BASE_URL = String(rawBaseUrl).trim().replace(/\/+$/, '')
|
||||
const AUTH_REFRESH_PATH = '/auth/refresh'
|
||||
const LOCALE_STORAGE_KEY = 'bss.locale'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
@@ -74,6 +76,11 @@ function redirectToLogin() {
|
||||
window.location.hash = '/login'
|
||||
}
|
||||
|
||||
function getRequestLocale() {
|
||||
if (typeof window === 'undefined') return DEFAULT_LOCALE
|
||||
return normalizeLocale(window.localStorage.getItem(LOCALE_STORAGE_KEY))
|
||||
}
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const auth = useAuthStore()
|
||||
const url = config.url || ''
|
||||
@@ -82,6 +89,8 @@ api.interceptors.request.use((config) => {
|
||||
config.headers ||= {}
|
||||
config.headers.Authorization = `Bearer ${auth.token}`
|
||||
}
|
||||
config.headers ||= {}
|
||||
config.headers['Accept-Language'] = getRequestLocale()
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
@@ -62,14 +62,36 @@ export const useProductPricingStore = defineStore('product-pricing-store', {
|
||||
async fetchRows () {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
const startedAt = Date.now()
|
||||
console.info('[product-pricing][frontend] request:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
timeout_ms: 600000
|
||||
})
|
||||
try {
|
||||
const res = await api.get('/pricing/products')
|
||||
const res = await api.request({
|
||||
method: 'GET',
|
||||
url: '/pricing/products',
|
||||
timeout: 600000
|
||||
})
|
||||
const traceId = res?.headers?.['x-trace-id'] || null
|
||||
const data = Array.isArray(res?.data) ? res.data : []
|
||||
this.rows = data.map((x, i) => mapRow(x, i))
|
||||
console.info('[product-pricing][frontend] request:success', {
|
||||
trace_id: traceId,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
row_count: this.rows.length
|
||||
})
|
||||
} catch (err) {
|
||||
this.rows = []
|
||||
const msg = err?.response?.data || err?.message || 'Urun fiyatlandirma listesi alinamadi'
|
||||
this.error = toText(msg)
|
||||
console.error('[product-pricing][frontend] request:error', {
|
||||
trace_id: err?.response?.headers?.['x-trace-id'] || null,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
timeout_ms: err?.config?.timeout ?? null,
|
||||
status: err?.response?.status || null,
|
||||
message: this.error
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ export const useDownloadstHeadStore = defineStore('downloadstHead', {
|
||||
accountCode,
|
||||
startDate,
|
||||
endDate,
|
||||
parislemler
|
||||
parislemler,
|
||||
langcode = 'TR'
|
||||
) {
|
||||
try {
|
||||
// ✅ Params (axios paramsSerializer array=repeat destekliyor)
|
||||
const params = {
|
||||
accountcode: accountCode,
|
||||
startdate: startDate,
|
||||
enddate: endDate
|
||||
enddate: endDate,
|
||||
langcode: langcode || 'TR'
|
||||
}
|
||||
|
||||
if (Array.isArray(parislemler) && parislemler.length > 0) {
|
||||
|
||||
@@ -7,13 +7,14 @@ export const useDownloadstpdfStore = defineStore('downloadstpdf', {
|
||||
/* ==========================================================
|
||||
📄 PDF İNDİR / AÇ
|
||||
========================================================== */
|
||||
async downloadPDF(accountCode, startDate, endDate, parislemler = []) {
|
||||
async downloadPDF(accountCode, startDate, endDate, parislemler = [], langcode = 'TR') {
|
||||
try {
|
||||
// 🔹 Query params
|
||||
const params = {
|
||||
accountcode: accountCode,
|
||||
startdate: startDate,
|
||||
enddate: endDate
|
||||
enddate: endDate,
|
||||
langcode: langcode || 'TR'
|
||||
}
|
||||
|
||||
if (Array.isArray(parislemler) && parislemler.length > 0) {
|
||||
|
||||
35
ui/src/stores/localeStore.js
Normal file
35
ui/src/stores/localeStore.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { applyDayjsLocale } from 'src/i18n/dayjsLocale'
|
||||
import { DEFAULT_LOCALE, normalizeLocale, toBackendLangCode } from 'src/i18n/languages'
|
||||
|
||||
const STORAGE_KEY = 'bss.locale'
|
||||
|
||||
function readInitialLocale() {
|
||||
if (typeof window === 'undefined') return DEFAULT_LOCALE
|
||||
return normalizeLocale(window.localStorage.getItem(STORAGE_KEY))
|
||||
}
|
||||
|
||||
export const useLocaleStore = defineStore('locale', () => {
|
||||
const locale = ref(readInitialLocale())
|
||||
|
||||
function setLocale(nextLocale) {
|
||||
const normalized = normalizeLocale(nextLocale)
|
||||
locale.value = normalized
|
||||
applyDayjsLocale(normalized)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(STORAGE_KEY, normalized)
|
||||
}
|
||||
}
|
||||
|
||||
const backendLangCode = computed(() => toBackendLangCode(locale.value))
|
||||
|
||||
applyDayjsLocale(locale.value)
|
||||
|
||||
return {
|
||||
locale,
|
||||
backendLangCode,
|
||||
setLocale
|
||||
}
|
||||
})
|
||||
128
ui/src/stores/translationStore.js
Normal file
128
ui/src/stores/translationStore.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import api from 'src/services/api'
|
||||
|
||||
export const useTranslationStore = defineStore('translation', {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
saving: false,
|
||||
rows: [],
|
||||
count: 0
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchRows (filters = {}) {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await api.get('/language/translations', { params: filters })
|
||||
const payload = res?.data || {}
|
||||
this.rows = Array.isArray(payload.rows) ? payload.rows : []
|
||||
this.count = Number(payload.count) || this.rows.length
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async updateRow (id, payload) {
|
||||
this.saving = true
|
||||
try {
|
||||
const res = await api.put(`/language/translations/${id}`, payload)
|
||||
return res?.data || null
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
async upsertMissing (items, languages = ['en', 'de', 'it', 'es', 'ru', 'ar']) {
|
||||
this.saving = true
|
||||
try {
|
||||
const res = await api.post('/language/translations/upsert-missing', {
|
||||
items: Array.isArray(items) ? items : [],
|
||||
languages: Array.isArray(languages) ? languages : []
|
||||
})
|
||||
return res?.data || null
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
async syncSources (payload = {}) {
|
||||
this.saving = true
|
||||
const startedAt = Date.now()
|
||||
console.info('[translation-sync][frontend] request:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
payload
|
||||
})
|
||||
try {
|
||||
const res = await api.post('/language/translations/sync-sources', payload, { timeout: 0 })
|
||||
const data = res?.data || null
|
||||
const traceId = data?.trace_id || data?.result?.trace_id || res?.headers?.['x-trace-id'] || null
|
||||
console.info('[translation-sync][frontend] request:success', {
|
||||
trace_id: traceId,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
result: data?.result || null
|
||||
})
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('[translation-sync][frontend] request:error', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
message: err?.message || 'sync-sources failed'
|
||||
})
|
||||
throw err
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
async translateSelected (payload = {}) {
|
||||
this.saving = true
|
||||
const startedAt = Date.now()
|
||||
console.info('[translation-selected][frontend] request:start', {
|
||||
at: new Date(startedAt).toISOString(),
|
||||
payload
|
||||
})
|
||||
try {
|
||||
const res = await api.post('/language/translations/translate-selected', payload, { timeout: 0 })
|
||||
const data = res?.data || null
|
||||
const traceId = data?.trace_id || res?.headers?.['x-trace-id'] || null
|
||||
console.info('[translation-selected][frontend] request:success', {
|
||||
trace_id: traceId,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
translated_count: data?.translated_count || 0
|
||||
})
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('[translation-selected][frontend] request:error', {
|
||||
duration_ms: Date.now() - startedAt,
|
||||
message: err?.message || 'translate-selected failed'
|
||||
})
|
||||
throw err
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
async bulkApprove (ids = []) {
|
||||
this.saving = true
|
||||
try {
|
||||
const res = await api.post('/language/translations/bulk-approve', {
|
||||
ids: Array.isArray(ids) ? ids : []
|
||||
})
|
||||
return res?.data || null
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
async bulkUpdate (items = []) {
|
||||
this.saving = true
|
||||
try {
|
||||
const res = await api.post('/language/translations/bulk-update', {
|
||||
items: Array.isArray(items) ? items : []
|
||||
})
|
||||
return res?.data || null
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user