Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-04-16 15:18:44 +03:00
parent 1831c45a0c
commit 2d369e7d7d
40 changed files with 3477 additions and 97 deletions

View File

@@ -148,6 +148,8 @@ createQuasarApp(createApp, quasarUserOptions)
import(/* webpackMode: "eager" */ 'boot/dayjs'),
import(/* webpackMode: "eager" */ 'boot/locale'),
import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard')
]).then(bootFiles => {

View File

@@ -15,7 +15,7 @@ export default defineConfig(() => {
/* =====================================================
BOOT FILES
===================================================== */
boot: ['dayjs', 'resizeObserverGuard'],
boot: ['dayjs', 'locale', 'resizeObserverGuard'],
/* =====================================================
GLOBAL CSS
@@ -70,7 +70,10 @@ export default defineConfig(() => {
context: ['/api'],
target: 'http://localhost:8080',
changeOrigin: true,
secure: false
secure: false,
ws: true,
timeout: 0,
proxyTimeout: 0
}
]
},

View File

@@ -27,7 +27,7 @@ var quasar_config_default = defineConfig(() => {
/* =====================================================
BOOT FILES
===================================================== */
boot: ["dayjs", "resizeObserverGuard"],
boot: ["dayjs", "locale", "resizeObserverGuard"],
/* =====================================================
GLOBAL CSS
===================================================== */
@@ -75,7 +75,10 @@ var quasar_config_default = defineConfig(() => {
context: ["/api"],
target: "http://localhost:8080",
changeOrigin: true,
secure: false
secure: false,
ws: true,
timeout: 0,
proxyTimeout: 0
}
]
},

View File

@@ -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'
// 🔹 Pluginleri 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
View 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)
})

View 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
}
}

View 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
View 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
View 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.'
}
}
}

View File

@@ -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>

View File

@@ -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 = {}
}

View File

@@ -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] Storedan 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)

View 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>

View File

@@ -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] Storedan 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)

View File

@@ -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 ================= */

View File

@@ -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
})

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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) {

View 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
}
})

View 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
}
}
}
})