This commit is contained in:
2026-02-11 17:46:22 +03:00
commit eacfacb13b
266 changed files with 51337 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
// src/stores/OrdernewListStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
let lastRequestId = 0
export const useOrderListStore = defineStore('orderlist', {
state: () => ({
orders: [],
loading: false,
error: null,
filters: {
search: '',
CurrAccCode: '',
OrderDate: ''
}
}),
getters: {
filteredOrders (state) {
let result = state.orders
if (state.filters.CurrAccCode) {
result = result.filter(o => o.CurrAccCode === state.filters.CurrAccCode)
}
if (state.filters.OrderDate) {
result = result.filter(o =>
o.OrderDate?.startsWith(state.filters.OrderDate)
)
}
return result
},
totalVisibleUSD (state) {
return state.filteredOrders.reduce(
(sum, o) => sum + Number(o.TotalAmountUSD || 0),
0
)
}
},
actions: {
async fetchOrders () {
// ==============================
// 📌 REQUEST ID
// ==============================
const rid = ++lastRequestId
// ==============================
// 📌 SEARCH SNAPSHOT
// ==============================
const raw = this.filters.search ?? ''
const trimmed = String(raw).trim()
// ==============================
// 📌 REQUEST LOG
// ==============================
console.groupCollapsed(
`%c[orders] FETCH rid=${rid}`,
'color:#1976d2;font-weight:bold'
)
console.log('raw =', JSON.stringify(raw), 'len=', String(raw).length)
console.log('trimmed =', JSON.stringify(trimmed), 'len=', trimmed.length)
console.log('filters =', JSON.parse(JSON.stringify(this.filters)))
console.log('lastRID =', lastRequestId)
console.groupEnd()
this.loading = true
this.error = null
try {
// ==============================
// 📌 PARAMS
// ==============================
const params = {}
if (trimmed) params.search = trimmed
// ==============================
// 📌 API CALL
// ==============================
const res = await api.get('/orders/list', { params })
// ==============================
// 📌 STALE CHECK
// ==============================
if (rid !== lastRequestId) {
console.warn(
`[orders] IGNORE stale response rid=${rid} last=${lastRequestId}`
)
return
}
// ==============================
// 📌 DATA
// ==============================
const data = res?.data
this.orders = Array.isArray(data) ? data : []
// ==============================
// 📌 RESPONSE LOG
// ==============================
console.groupCollapsed(
`%c[orders] RESPONSE rid=${rid} count=${this.orders.length}`,
'color:#2e7d32;font-weight:bold'
)
console.log('status =', res?.status)
console.log(
'sample =',
this.orders.slice(0, 5).map(o => ({
id: o.OrderHeaderID,
no: o.OrderNumber,
code: o.CurrAccCode,
name: o.CurrAccDescription
}))
)
// ==============================
// 📌 DUPLICATE CHECK
// ==============================
const ids = this.orders.map(o => String(o.OrderHeaderID))
const dup = ids.filter((v, i) => ids.indexOf(v) !== i)
if (dup.length) {
console.warn(
'DUPLICATE OrderHeaderID sample =',
dup.slice(0, 10)
)
}
console.groupEnd()
} catch (err) {
if (rid !== lastRequestId) return
console.error(
'[orders] FETCH FAILED',
err?.response?.status,
err?.response?.data || err
)
this.orders = []
this.error =
err?.response?.data ||
err?.message ||
'Sipariş listesi alınamadı'
} finally {
if (rid === lastRequestId) {
this.loading = false
}
}
}
}
})

View File

@@ -0,0 +1,238 @@
// src/stores/userDetailStore.js
import { defineStore } from 'pinia'
import api, { get, post, put } from 'src/services/api'
export const useUserDetailStore = defineStore('userDetail', {
state: () => ({
sendingPasswordMail: false,
lastPasswordMailSentAt: null,
hasPassword: false,
/* ================= FLAGS ================= */
loading: false,
saving: false,
error: null,
/* ================= FORM ================= */
form: {
id: null,
code: '',
full_name: '',
email: '',
mobile: '',
is_active: true,
address: '',
roles: [],
departments: [],
piyasalar: [],
nebim_users: []
},
/* ================= LOOKUPS ================= */
roleOptions: [],
departmentOptions: [],
piyasaOptions: [],
nebimUserOptions: []
}),
actions: {
/* =====================================================
🔄 RESET (NEW MODE)
===================================================== */
resetForm () {
this.form = {
id: null,
code: '',
full_name: '',
email: '',
mobile: '',
is_active: true,
address: '',
roles: [],
departments: [],
piyasalar: [],
nebim_users: []
}
this.error = null
this.hasPassword = false
this.lastPasswordMailSentAt = null
},
/* =====================================================
🔐 ADMIN RESET PASSWORD
===================================================== */
async adminResetPassword (id, payload) {
// token otomatik (interceptor)
await post(`/users/${id}/admin-reset-password`, payload)
this.hasPassword = true
},
/* =====================================================
✉️ SEND PASSWORD MAIL
===================================================== */
async sendPasswordMail (id) {
this.sendingPasswordMail = true
this.error = null
try {
await post(`/users/${id}/send-password-mail`, {})
// UI takip (DBsiz): sadece “son gönderim” gösterir
this.lastPasswordMailSentAt = new Date().toLocaleString('tr-TR')
} catch (e) {
this.error = 'Parola maili gönderilemedi'
throw e
} finally {
this.sendingPasswordMail = false
}
},
/* =====================================================
📦 PAYLOAD BUILDER (BACKEND SÖZLEŞMESİYLE UYUMLU)
===================================================== */
buildPayload () {
return {
code: this.form.code,
full_name: this.form.full_name,
email: this.form.email,
mobile: this.form.mobile,
is_active: this.form.is_active,
address: this.form.address,
roles: this.form.roles,
// ✅ TEK DEPARTMAN (string → backend array)
departments: this.form.departments
? [{ code: this.form.departments }]
: [],
piyasalar: (this.form.piyasalar || []).map(code => ({ code })),
nebim_users: (this.form.nebim_users || []).map(username => {
const opt = (this.nebimUserOptions || []).find(x => x.value === username)
return {
username,
user_group_code: opt?.user_group_code || ''
}
})
}
},
/* =====================================================
📥 GET USER (EDIT MODE)
===================================================== */
async fetchUser (id) {
this.loading = true
this.error = null
try {
const data = await get(`/users/${id}`)
this.form.id = data.id
this.form.code = data.code || ''
this.form.full_name = data.full_name || ''
this.form.email = data.email || ''
this.form.mobile = data.mobile || ''
this.form.is_active = !!data.is_active
this.form.address = data.address || ''
this.form.roles = data.roles || []
this.form.departments = (data.departments || []).map(x => x.code)
this.form.piyasalar = (data.piyasalar || []).map(x => x.code)
this.form.nebim_users = (data.nebim_users || []).map(x => x.username)
this.hasPassword = !!data.has_password
} catch (e) {
this.error = 'Kullanıcı bilgileri alınamadı'
throw e
} finally {
this.loading = false
}
},
/* =====================================================
✍️ UPDATE USER (PUT)
===================================================== */
async saveUser (id) {
this.saving = true
this.error = null
try {
console.log('🟦 saveUser() START', id)
const payload = this.buildPayload()
console.log('📤 PUT payload', payload)
await put(`/users/${id}`, payload)
console.log('✅ PUT OK → REFETCH USER')
await this.fetchUser(id)
console.log('🔄 USER REFRESHED', {
hasPassword: this.hasPassword,
roles: this.form.roles,
departments: this.form.departments
})
} catch (e) {
console.error('❌ saveUser FAILED', e)
this.error = 'Kullanıcı güncellenemedi'
throw e
} finally {
this.saving = false
}
},
/* =====================================================
CREATE USER (POST)
===================================================== */
async createUser () {
this.saving = true
this.error = null
try {
console.log('🟢 createUser() START')
const payload = this.buildPayload()
console.log('📤 POST payload', payload)
const data = await post('/users', payload)
console.log('✅ CREATE OK response', data)
const newId = data?.id
if (!newId) {
throw new Error('CREATE response id yok')
}
console.log('🔁 FETCH NEW USER id=', newId)
await this.fetchUser(newId)
return newId
} catch (e) {
console.error('❌ createUser FAILED', e)
this.error = 'Kullanıcı oluşturulamadı'
throw e
} finally {
this.saving = false
}
},
/* =====================================================
📚 LOOKUPS (NEW + EDIT ORTAK)
===================================================== */
async fetchLookups () {
// token otomatik
const [roles, depts, piyasalar, nebims] = await Promise.all([
api.get('/lookups/roles'),
api.get('/lookups/departments'),
api.get('/lookups/piyasalar'),
api.get('/lookups/nebim-users')
])
this.roleOptions = roles?.data || roles || []
this.departmentOptions = depts?.data || depts || []
this.piyasaOptions = piyasalar?.data || piyasalar || []
this.nebimUserOptions = nebims?.data || nebims || []
}
}
})

View File

@@ -0,0 +1,72 @@
// src/stores/userListStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
export const useUserListStore = defineStore('userlist', {
state: () => ({
users: [],
loading: false,
error: null,
filters: {
search: '',
onlyActive: false
}
}),
getters: {
filteredUsers(state) {
let result = state.users
const term = state.filters.search?.toLowerCase() || ''
if (term) {
result = result.filter(u =>
u.code?.toLowerCase().includes(term) ||
u.nebim_username?.toLowerCase().includes(term) ||
u.role_names?.toLowerCase().includes(term) ||
u.department_names?.toLowerCase().includes(term) ||
u.piyasa_names?.toLowerCase().includes(term)
)
}
if (state.filters.onlyActive) {
result = result.filter(u => u.is_active)
}
return result
}
},
actions: {
async fetchUsers() {
this.loading = true
this.error = null
try {
const params = {}
if (this.filters.search) {
params.search = this.filters.search
}
const { data } = await api.get(
'/users/list',
{ params }
)
this.users = Array.isArray(data) ? data : []
console.log('✅ User listesi alındı:', this.users.length)
} catch (err) {
console.error('❌ User listesi alınamadı:', err)
this.users = []
this.error =
err?.message ||
'Kullanıcı listesi alınamadı'
} finally {
this.loading = false
}
}
}
})

View File

@@ -0,0 +1,43 @@
// src/stores/accountStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
export const useAccountStore = defineStore('account', {
state: () => ({
accountOptions: [],
loading: false,
error: null
}),
actions: {
async fetchAccounts () {
this.loading = true
this.error = null
try {
// 🔐 Token interceptor ile otomatik eklenir
const { data } = await api.get('/accounts')
this.accountOptions = (Array.isArray(data) ? data : []).map(acc => ({
label: `${acc.display_code || ''} ${acc.account_name || ''}`.trim(),
value: acc.account_code
}))
} catch (err) {
console.error('❌ Error fetching accounts:', err)
if (err?.response?.status === 401) {
this.error = 'Cari hesapları görüntüleme yetkiniz yok.'
} else {
this.error =
err?.response?.data?.message ||
err?.message ||
'Cari hesaplar yüklenemedi'
}
} finally {
this.loading = false
}
}
}
})

View File

@@ -0,0 +1,102 @@
import { defineStore } from 'pinia'
import { get } from 'src/services/api'
export const useActivityLogStore = defineStore('activityLogStore', {
state: () => ({
loading: false,
rows: [],
total: 0,
pagination: {
page: 1,
rowsPerPage: 0, // ✅ SINIRSIZ
sortBy: 'created_at',
descending: true
}
,
filters: {
username: '',
actionCategory: null,
actionType: '',
success: null,
dateFrom: '',
dateTo: ''
}
}),
actions: {
async fetchLogs () {
this.loading = true
try {
const params = {}
if (this.pagination.rowsPerPage > 0) {
params.page = this.pagination.page
params.limit = this.pagination.rowsPerPage
}
if (this.filters.username)
params.username = this.filters.username
if (this.filters.actionCategory)
params.action_category = this.filters.actionCategory
if (this.filters.actionType)
params.action_type = this.filters.actionType
if (this.filters.success !== null)
params.success = this.filters.success
if (this.filters.dateFrom)
params.date_from = this.filters.dateFrom
if (this.filters.dateTo)
params.date_to = this.filters.dateTo
const data = await get('/activity-logs', params)
this.rows = data.items || []
this.total = data.total || 0
} finally {
this.loading = false
}
},
quickRoleChange () {
this.filters.actionCategory = 'role_permission'
this.filters.actionType = 'role_department_permission_change'
this.pagination.page = 1
this.fetchLogs()
}
,
onTableRequest (props) {
const { page, rowsPerPage, sortBy, descending } = props.pagination
this.pagination.page = page
this.pagination.rowsPerPage = rowsPerPage
this.pagination.sortBy = sortBy
this.pagination.descending = descending
this.fetchLogs()
}
,
resetFilters () {
this.filters = {
username: '',
actionCategory: null,
actionType: '',
success: null,
dateFrom: '',
dateTo: ''
}
this.pagination.page = 1
this.fetchLogs()
}
}
})

117
ui/src/stores/authStore.js Normal file
View File

@@ -0,0 +1,117 @@
// src/stores/authStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
import { usePermissionStore } from 'stores/permissionStore'
export const useAuthStore = defineStore('auth', {
state: () => {
let user = null
try {
const raw = localStorage.getItem('user')
if (raw && raw !== 'undefined' && raw !== 'null') {
user = JSON.parse(raw)
}
} catch {
console.warn('⚠️ Invalid user in localStorage, cleared')
localStorage.removeItem('user')
}
return {
token: localStorage.getItem('token'),
user,
forcePasswordChange: localStorage.getItem('forcePasswordChange') === '1'
}
},
getters: {
isAuthenticated: s => !!s.token,
mustChangePassword: s => !!s.forcePasswordChange,
// 🔥 TEK ADMIN KURALI
isAdmin: s =>
String(s.user?.role_code || '').toLowerCase() === 'admin'
},
actions: {
/* =========================================================
🔐 SESSION
========================================================= */
setSession ({ token, user }) {
this.token = token
this.user = user ?? null
this.forcePasswordChange = !!user?.force_password_change
localStorage.setItem('token', token)
if (user) {
localStorage.setItem('user', JSON.stringify(user))
} else {
localStorage.removeItem('user')
}
localStorage.setItem(
'forcePasswordChange',
this.forcePasswordChange ? '1' : '0'
)
},
clearSession () {
this.token = null
this.user = null
this.forcePasswordChange = false
localStorage.removeItem('token')
localStorage.removeItem('user')
localStorage.removeItem('forcePasswordChange')
usePermissionStore().clear()
},
/* =========================================================
🔐 LOGIN
========================================================= */
async login (username, password) {
const res = await api.post('/auth/login', { username, password })
const token =
res?.token ||
res?.data?.token ||
res?.access_token ||
res?.data?.access_token
const user =
res?.user ||
res?.data?.user
// ✅ JWT doğrulama
const tokenStr = typeof token === 'string' ? token.trim() : ''
const looksLikeJwt = tokenStr.split('.').length === 3
if (!tokenStr || !looksLikeJwt) {
console.error('❌ LOGIN RESPONSE (unexpected):', res)
throw new Error('Invalid login token')
}
this.setSession({ token: tokenStr, user })
// 🔥 PERMISSIONS
const perm = usePermissionStore()
await perm.fetchPermissions()
// 🧪 DEBUG (istersen sonra kaldır)
console.log('🔐 AUTH DEBUG', {
isAdmin: this.isAdmin,
users: perm.hasPermission('/api/users/list'),
orders: perm.hasPermission('/api/orders/list'),
logs: perm.hasPermission('/api/activity-logs'),
permissions: perm.hasPermission('/api/permissions/matrix')
})
return true
}
}
})

3023
ui/src/stores/deneme Normal file
View File

@@ -0,0 +1,3023 @@
<template>
<!-- ===========================================================
🧾 ORDER ENTRY PAGE (BSSApp)
v22 — Model + Renk + 2. Renk + Beden/Stok Otomatik Eşleme
============================================================ -->
<q-page class="q-pa-md order-page">
<div class="order-scroll-x">
<div class="order-width">
<div class="row items-center q-mb-xs">
<q-chip
:color="isEditMode ? 'blue-7' : 'green-7'"
text-color="white"
icon="edit"
v-if="isEditMode"
>
Düzenleme Modu
</q-chip>
<q-chip
text-color="white"
icon="add_circle"
v-else
>
Yeni Sipariş
</q-chip>
</div>
<!-- =======================================================
🔹 FILTER BAR — OrderHeader Bilgileri
SQL eşleşmeli alanlar
======================================================= -->
<div class="filter-bar row q-col-gutter-md q-mb-sm">
<!-- 🧾 Cari Seçimi -->
<div class="col-5">
<q-select
v-model="form.CurrAccCode"
:options="filteredCariOptions"
label="Cari Seçimi"
filled
use-input
input-debounce="300"
emit-value
map-options
option-value="Cari_Kod"
:option-label="opt => `${opt.Cari_Kod} - ${opt.Cari_Ad}`"
@filter="filterCari"
@update:model-value="onCariChange"
:loading="loadingCari"
behavior="menu"
clearable
>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>{{ scope.opt.Cari_Ad }}</q-item-label>
<q-item-label caption>{{ scope.opt.Cari_Kod }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<!-- 🔢 Sipariş No -->
<div class="col-2">
<q-input
v-model="form.OrderNumber"
label="Sipariş No"
filled
dense
:readonly="isEditMode"
/>
</div>
<!-- 📅 Oluşturulma Tarihi -->
<div class="col-2">
<q-input
v-model="form.OrderDate"
label="Oluşturulma Tarihi"
type="date"
filled
dense
/>
</div>
<!-- 📅 Tahmini Termin Tarihi -->
<div class="col-2">
<q-input
v-model="form.AverageDueDate"
label="Tahmini Termin Tarihi"
type="date"
filled
dense
/>
</div>
<!-- 💰 TOPLAM TUTAR + KDV -->
<div class="col-12 row items-center q-gutter-sm q-mt-sm">
<div class="col-3">
<q-input
dense
filled
v-model.number="toplamTutar"
label="Toplam Tutar"
readonly
:suffix="form.pb"
/>
</div>
<div class="col-auto">
<q-checkbox
v-model="includeKDV"
label="KDV Dahil"
/>
</div>
<div class="col-2">
<q-input
dense
filled
v-model.number="manualKDV"
label="KDV"
readonly
suffix="%"
/>
</div>
<div class="col-3">
<q-input
dense
filled
:model-value="toplamKDVli.toFixed(2)"
label="KDV Dahil Toplam"
readonly
:suffix="form.pb"
/>
</div>
</div>
</div>
<!-- 🔸 Cari Bilgi Kutuları -->
<q-slide-transition>
<div v-if="cariInfo" class="row q-col-gutter-md q-mt-xs cari-info-bar">
<div class="col-3"><q-input :model-value="cariInfo.Musteri_Temsilcisi || '-'" label="Müşteri Temsilcisi" filled dense readonly /></div>
<div class="col-3"><q-input :model-value="cariInfo.Musteri_Ana_Grubu || '-'" label="Ana Grup" filled dense readonly /></div>
<div class="col-3"><q-input :model-value="cariInfo.Piyasa || '-'" label="Piyasa" filled dense readonly /></div>
<div class="col-3"><q-input :model-value="cariInfo.Ulke || '-'" label="Ülke" filled dense readonly /></div>
</div>
</q-slide-transition>
<!-- 🔹 SAVE TOOLBAR -->
<div class="save-toolbar">
<div class="text-subtitle2 text-weight-bold">Sipariş Formu</div>
<!-- =======================================================
🔹 KAYDET / GÜNCELLE BUTONU
======================================================= -->
<q-btn
:label="isEditMode ? 'TÜMÜNÜ GÜNCELLE' : 'TÜMÜNÜ KAYDET'"
color="primary"
icon="save"
class="q-ml-sm"
:loading="orderStore.loading"
@click="submitAll"
/>
<!-- =======================================================
🔹 YENİ SİPARİŞ / FORM SIFIRLA BUTONU
======================================================= -->
<q-btn
label="YENİ SİPARİŞ"
color="secondary"
icon="add_circle"
class="q-ml-sm"
@click="resetForm"
/>
</div>
<!-- 🔹 GRID HEADER -->
<div class="order-grid-header">
<div class="col-fixed model">MODEL</div>
<div class="col-fixed renk">RENK</div>
<div class="col-fixed ana">ÜRÜN ANA<br />GRUBU</div>
<div class="col-fixed alt">ÜRÜN ALT<br />GRUBU</div>
<div class="col-fixed aciklama-col">AÇIKLAMA</div>
<div class="beden-block">
<div v-for="grp in schema" :key="grp.key" class="grp-row" :class="{ 'hl-pan': grp.key === 'pan' && highlightPantolon }">
<div class="grp-title">{{ grp.title }}</div>
<div class="grp-body">
<div v-for="v in grp.values" :key="'b-' + grp.key + '-' + v" class="grp-cell hdr">
{{ v }}
</div>
</div>
</div>
</div>
<div class="total-row">
<div class="total-cell">ADET</div>
<div class="total-cell">FİYAT</div>
<div class="total-cell">PB</div>
<div class="total-cell">TUTAR</div>
<div class="total-cell">Tahmini Gönderim Tarihi</div>
</div>
</div>
</div>
<!-- =======================================================
🔹 GRID BODY
======================================================== -->
<div class="order-grid-body">
<template v-for="grp in groupedRows" :key="grp.name">
<div :class="['summary-group', grp.open ? 'open' : 'closed']">
<!-- 🟡 Sub-header -->
<div class="order-sub-header" @click="toggleGroup(grp.name)">
<div class="sub-left">{{ grp.name }}</div>
<div class="sub-center">
<div
v-for="v in (grp.bedenValues || [])"
:key="'hdr-' + v"
class="beden-cell"
>
{{ v }}
</div>
</div>
<div class="sub-right">
<div class="text-caption">
Toplam {{ grp.name }} Adet: {{ grp.toplamAdet }}
</div>
<div class="text-caption">
Toplam {{ grp.name }} Tutar:
{{ Number(grp.toplamTutar||0).toLocaleString('tr-TR',{minimumFractionDigits:2}) }} {{ form.pb || aktifPB }}
</div>
<q-icon :name="grp.open ? 'expand_less' : 'expand_more'" size="20px" class="cursor-pointer text-grey-8 q-ml-sm" />
</div>
</div>
<!-- 🧩 Grup satırları -->
<template v-if="grp.open">
<div
v-for="({ row }, i) in grp.rows"
:key="i"
class="summary-row"
:class="{
active: editingIndex === summaryRows.findIndex(r => r === row),
'is-editing': editingIndex === summaryRows.findIndex(r => r === row)
}"
@click="editRow(row, i)"
>
<!-- Sol kolonlar -->
<div class="cell model">{{ row.model }}</div>
<div class="cell renk">{{ row.renk }}{{ row.renk2 ? '-' + row.renk2 : '' }}</div>
<div class="cell ana">{{ row.urunAnaGrubu }}</div>
<div class="cell alt">{{ row.urunAltGrubu }}</div>
<div class="cell aciklama">{{ row.aciklama }}</div>
<!-- Beden kolonları -->
<div class="grp-area">
<div class="grp-row">
<div v-for="v in (schemaByKey[row.grpKey]?.values || [])" :key="'val-' + v" class="cell beden">
{{ row.bedenMap?.[row.grpKey]?.[v] ?? '' }}
</div>
<div v-for="i in (16 - (schemaByKey[row.grpKey]?.values?.length || 0))" :key="'empty-' + i" class="cell beden ghost"></div>
</div>
</div>
<!-- Sağ kolonlar -->
<div class="cell adet">{{ row.adet }}</div>
<div class="cell fiyat">{{ row.fiyat }}</div>
<div class="cell pb">{{ row.pb }}</div>
<div class="cell tutar">
{{ Number(row.tutar || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
</div>
<!-- ESKİ (inline düzenlenebilir) -->
<!-- 🔹 Termin Tarihi — sadece gösterge -->
<!-- 🔹 Termin Tarihi — sadece gösterge -->
<div class="cell termin">
<div class="termin-label text-center">
{{ formatDate(row.terminTarihi) }}
</div>
</div>
</div>
</template>
</div>
</template>
</div>
<!-- =======================================================
🔹 SATIR DÜZENLEYİCİ FORM (EDITOR)
======================================================== -->
<div class="editor q-mt-lg q-pa-sm">
<!-- 🔸 1. Satır: Model ve Ürün Bilgileri -->
<div class="row q-col-gutter-sm q-mb-sm">
<div class="col-3">
<!-- 🔹 Model Seçimi -->
<q-select
v-model="form.model"
:options="filteredModelOptions"
label="Model"
filled dense
use-input input-debounce="250"
emit-value map-options
option-value="value"
option-label="label"
clearable behavior="menu"
hint="Model kodu ile arayabilirsiniz"
:loading="loadingModels"
:disable="isEditing"
@filter="filterModel"
@update:model-value="(val) => useComboWatcher('model', onModelChange)(val)"
/>
<!-- 🔹 1. Renk Seçimi -->
<div class="q-mt-sm">
<q-select
ref="renkSelect"
v-model="form.renk"
:options="renkOptions"
label="Renk"
filled dense clearable
emit-value map-options
option-value="value"
option-label="label"
:disable="isEditing"
@update:model-value="(val) => useComboWatcher('renk', onColorChange)(val)"
/>
</div>
<!-- 🔹 2. Renk Seçimi -->
<div class="q-mt-sm">
<q-select
ref="renk2Select"
v-model="form.renk2"
:options="renkOptions2"
label="2. Renk"
filled dense clearable
emit-value map-options
option-value="value"
option-label="label"
:disable="!renkOptions2.length || isEditing"
@update:model-value="(val) => useComboWatcher('renk2', onColor2Change)(val)"
/>
</div>
</div>
<!-- Ürün teknik alanları -->
<div class="col-2">
<q-input v-model="form.urunAnaGrubu" label="Ürün Ana Grubu" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.urunAltGrubu" label="Alt Grup" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.fit" label="Fit" filled dense readonly />
</div>
<div class="col-2">
<q-input v-model="form.urunIcerik" label="İçerik" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.drop" label="Drop" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.askiliyan" label="ASKILI/YAN" filled dense readonly />
</div>
<div class="col-1">
<q-input v-model="form.kategori" label="Kategori" filled dense readonly />
</div>
</div>
<!-- 🔸 2. Satır: Seri Seçimi -->
<div class="row q-col-gutter-sm q-mt-xs">
<div class="col-3">
<q-select
v-if="activeSeriesOptions && activeSeriesOptions.length"
v-model="selectedSeriSet"
:options="activeSeriesOptions"
label="Beden Seti Seç"
filled dense
emit-value map-options
option-value="value"
option-label="label"
/>
</div>
<div class="col-2 q-mt-sm">
<q-input
v-if="selectedSeriSet"
v-model.number="seriMultiplier"
type="number"
label="Çarpan"
min="1"
filled dense
/>
</div>
<div class="col-2 q-mt-sm">
<q-btn
v-if="selectedSeriSet"
color="primary"
icon="add"
label="Seri Ekle"
@click="applySeriSet"
/>
</div>
</div>
<!-- =======================================================
🔹 BEDEN GİRİŞ ALANI + STOK ETİKETİ GÖRÜNÜMÜ
======================================================== -->
<div class="row q-mt-sm q-col-gutter-xs beden-grid">
<div
v-for="(lbl, i) in form.bedenLabels || []"
:key="'beden-'+i"
class="col-auto beden-wrap"
>
<div class="beden-label">{{ lbl }}</div>
<q-input
v-model.number="form.bedenler[i]"
dense outlined type="number" min="0"
style="width:60px"
@focus="activeBeden = i"
@blur="activeBeden = null"
@update:model-value="updateTotals(form)"
:class="{ 'beden-active': activeBeden === i }"
/>
<div
v-if="getStockFor(lbl) !== null"
class="stok-label text-caption text-center q-mt-xs"
:class="stockColorClass(getStockFor(lbl))"
>
Stok: {{ getStockFor(lbl) }}
</div>
</div>
</div>
<!-- 🔹 Aktif beden için küçük stok etiketi -->
<div
v-if="form.model && activeBeden !== null && getStockFor(form.bedenLabels[activeBeden]) !== null"
class="stok-label-sm"
:class="stockColorClass(getStockFor(form.bedenLabels[activeBeden]))"
>
Stok: {{ getStockFor(form.bedenLabels[activeBeden]) }}
</div>
<!-- =======================================================
🔹 ADET / FİYAT / PB / TUTAR
======================================================== -->
<div class="row q-mt-sm q-col-gutter-sm">
<div class="col-2">
<q-input v-model.number="form.adet" label="Adet" dense filled readonly />
</div>
<div class="col-2">
<q-input
v-model.number="form.fiyat"
label="Fiyat"
dense filled type="number" min="0"
@update:model-value="() => updateTotals(form)"
/>
</div>
<div class="col-2">
<q-select v-model="form.pb" :options="paraBirimOptions" label="PB" dense filled />
</div>
<div class="col-3">
<q-input v-model="form.tutar" label="Tutar" dense filled readonly />
</div>
</div>
<!-- =======================================================
🔹 SATIR BAZINDA TAHMİNİ TERMİN TARİHİ
======================================================== -->
<div class="row q-mt-sm">
<div class="col-4">
<q-input
v-model="form.terminTarihi"
type="date"
label="Tahmini Termin Tarihi"
filled dense
/>
</div>
</div>
<!-- =======================================================
🔹 AÇIKLAMA ALANI
======================================================== -->
<div class="row q-mt-sm">
<div class="col-12">
<q-input
v-model="form.aciklama"
label="Açıklama"
type="textarea"
filled dense autogrow
maxlength="1500" counter
/>
</div>
</div>
<!-- =======================================================
🔹 BUTONLAR (Kaydet / Güncelle / Sil / Temizle)
======================================================== -->
<div class="row justify-between items-center q-mt-md">
<div class="row q-gutter-sm">
<q-btn
:color="editingIndex === -1 ? 'primary' : 'positive'"
:label="editingIndex === -1 ? 'Kaydet' : 'Güncelle'"
@click="saveOrUpdate"
/>
<q-btn
v-if="editingIndex !== -1"
color="negative" flat label="Satırı Sil"
@click="removeSelected"
/>
<q-btn
flat color="grey-8"
label="Formu Temizle"
@click="resetForm"
/>
</div>
</div>
<!-- =======================================================
🔹 ALT BİLGİLENDİRME ALANI
======================================================== -->
<div class="q-mt-md text-caption text-grey-7 text-center">
<q-icon name="info" size="16px" class="q-mr-xs" />
Bu sayfada yapılan siparişler henüz gönderilmemiştir.
<br />
<span class="text-negative">"Tümünü Kaydet (Toplu Gönder)"</span>
butonuna basarak işlemleri kaydedebilirsiniz.
</div>
<!-- =======================================================
🔹 SİPARİŞ GENEL AÇIKLAMASI
======================================================== -->
<div class="row q-mt-md">
<div class="col-12">
<q-input
v-model="form.Description"
type="textarea"
label="Sipariş Genel Açıklaması"
filled dense autogrow
maxlength="1500"
counter
placeholder="Siparişe genel açıklama giriniz (örn. teslimat, üretim notu, müşteri isteği...)"
/>
</div>
</div>
</div> <!-- editor -->
</div> <!-- order-width -->
</div> <!-- order-scroll-x -->
</q-page>
</template>
<script setup>
/* ===========================================================
🧩 ORDER ENTRY (v22-final)
Tüm fonksiyonları kapsayan gelişmiş setup bloğu.
Bu dosya backend ile sıkı entegredir, axios ve Pinia store
ile veri alışverişi yapar.
=========================================================== */
// Vue çekirdek importları
import { ref, reactive, computed, onMounted, watch, onBeforeUnmount, nextTick } from 'vue'
import { api } from 'boot/axios'
import { useRoute, useRouter } from 'vue-router' // ✅ Buradan olmalı
import { useQuasar } from 'quasar'
import axios from 'axios'
import { useOrderentryStore } from 'src/stores/orderentryStore'
import dayjs from 'dayjs'
const route = useRoute()
const router = useRouter()
const $q = useQuasar()
const orderId =
route.params.id ||
route.query.id ||
route.query.orderId ||
null
console.log('🧩 Route parametresi alındı (setup başında):', orderId)
const isEditMode = ref(false)
// ===========================================================
// 🔹 Mode & Transaction State
// Yeni sipariş mi, düzenleme mi kontrolü için
// ===========================================================
const mode = ref('new') // 'new' | 'edit'
const txId = ref('')
const headerId = ref('') // Quasar bileşenleri ve $q.notify kullanımı için
const orderStore = useOrderentryStore() // Pinia store: siparişler, localStorage, API çağrıları
// 🔹 Genel reaktif değişkenler
const aktifPB = ref('USD') // Varsayılan para birimi (Cari seçimiyle değişebilir)
const siparisNo = ref('SP-2025-001') // Geçici sipariş numarası örneği
const allSeriesSets = ref([]) // Seri listeleri (seriMatrix'ten dinamik doldurulur)
/* ===========================================================
🗓️ SİPARİŞ TARİHLERİ — Varsayılan Değerler
Oluşturulma tarihi = bugünün tarihi
Tahmini termin tarihi = bugünden + 5 hafta (35 gün)
=========================================================== */
const today = new Date()
const terminDate = new Date(today)
terminDate.setDate(today.getDate() + 35) // 5 hafta sonrası
const defaultOlusturmaTarihi = today.toISOString().substring(0, 10)
const defaultTerminTarihi = terminDate.toISOString().substring(0, 10)
// 💰 KDV Hesaplama Alanları
const includeKDV = ref(false) // KDV kutusu işaretli mi?
const manualKDV = ref(0) // Kullanıcının elle girdiği KDV tutarı
const kdvOrani = 0.10 // Varsayılan %10 oran
/* ===========================================================
🔹 FORM TANIMI (reactive form)
Tüm giriş alanları ve hesaplanan değerler tek reactive obje içinde.
=========================================================== */
const form = reactive({
/* =========================================================
🔹 TEMEL ALANLAR
========================================================= */
OrderHeaderID: '', // string (GUID)
OrderTypeCode: 1, // int8
ProcessCode: 'WS', // string
OrderNumber: '', // string
OrderTime: dayjs().format('HH:mm:ss'), // saat formatı (örn. "14:35:22")
IsCancelOrder: false,
/* =========================================================
🔹 ADRES / REFERANS ALANLARI
========================================================= */
BillingPostalAddressID: '',
GuarantorContactID: '',
ApplicationCode: '',
ApplicationID: '',
/* =========================================================
🔹 TARİH / AÇIKLAMA
========================================================= */
OrderDate: dayjs().format('YYYY-MM-DD'),
AverageDueDate: dayjs().add(30, 'day').format('YYYY-MM-DD'),
Description: '',
InternalDescription: '',
/* =========================================================
🔹 CARİ BİLGİLERİ
========================================================= */
CurrAccTypeCode: 0,
CurrAccCode: '',
CurrAccDescription: '',
/* =========================================================
🔹 PARA BİRİMİ
========================================================= */
DocCurrencyCode: 'USD',
LocalCurrencyCode: 'TRY',
ExchangeRate: 1,
/* =========================================================
🔹 DURUM ALANLARI
========================================================= */
IsCreditSale: true,
IsCreditableConfirmed: false,
IsSalesViaInternet: false,
IsSuspended: false,
IsCompleted: false,
IsPrinted: false,
IsLocked: false,
IsClosed: false,
/* =========================================================
🔹 KULLANICI VE TARİH
========================================================= */
CreatedUserName: '', // backend dolduracak (auth user)
CreatedDate: dayjs().toISOString(),
LastUpdatedUserName: '',
LastUpdatedDate: dayjs().toISOString(),
CreditableConfirmedUser: '',
CreditableConfirmedDate: '',
/* =========================================================
🔹 SABİT / EK ALANLAR
========================================================= */
DocumentNumber: '',
PaymentTerm: '',
SubCurrAccID: '',
ShipmentMethodCode: '',
ContactID: '',
ShippingPostalAddressID: '',
GuarantorContactID2: '',
RoundsmanCode: '',
DeliveryCompanyCode: '',
TaxTypeCode: '',
WithHoldingTaxTypeCode: '',
DOVCode: '',
TaxExemptionCode: 0,
CompanyCode: 1,
OfficeCode: 101,
StoreTypeCode: 5,
StoreCode: 0,
POSTerminalID: 0,
WarehouseCode: '1-0-12',
ToWarehouseCode: '',
OrdererCompanyCode: 1,
OrdererOfficeCode: 101,
OrdererStoreCode: '',
GLTypeCode: '',
TDisRate1: 0,
TDisRate2: 0,
TDisRate3: 0,
TDisRate4: 0,
TDisRate5: 0,
DiscountReasonCode: 0,
SurplusOrderQtyToleranceRate: 0,
ImportFileNumber: '',
ExportFileNumber: '',
IncotermCode1: '',
IncotermCode2: '',
LettersOfCreditNumber: '',
PaymentMethodCode: '',
IsIncludedVat: 0,
UserLocked: 0,
IsProposalBased: 0,
model: '', // Ürün kodu
renk: '',
renk2: '',
urunAnaGrubu: '',
urunAltGrubu: '',
fit: '',
urunIcerik: '',
drop: '',
kategori: '',
askiliyan: '',
seri: '',
bedenLabels: [],
bedenler: [],
adet: 0,
fiyat: 0,
pb: aktifPB.value,
tutar: 0,
aciklama: '',
minFiyat: 0,
kur: 1,
minFiyatTRY: 0,
// 🗓️ Tarihler
olusturmaTarihi: defaultOlusturmaTarihi,
tahminiTerminTarihi: defaultTerminTarihi,
terminTarihi: defaultTerminTarihi
})
const orderLine = reactive({
// 🔹 Temel Bilgiler
OrderLineID: '', // GUID
OrderHeaderID: '', // foreign key
SortOrder: 1,
ItemTypeCode: 0,
ItemCode: '',
ColorCode: '',
ItemDim1Code: '',
ItemDim2Code: '',
ItemDim3Code: '',
// 🔹 Miktarlar
Qty1: 0,
Qty2: 0,
CancelQty1: 0,
CancelQty2: 0,
// 🔹 Tarihler
CancelDate: null, // null ya da ISO string
ClosedDate: null,
DeliveryDate: null,
PlannedDateOfLading: '',
// 🔹 Durum & Neden
OrderCancelReasonCode: '',
IsClosed: false,
// 🔹 Satış Planı
SalespersonCode: '',
PaymentPlanCode: '',
PurchasePlanCode: '',
// 🔹 Ürün Bilgisi
LineDescription: '',
UsedBarcode: '',
CostCenterCode: '',
VatCode: '',
VatRate: 0,
PCTCode: '',
PCTRate: 0,
// 🔹 Satır Bazlı İndirimler
LDisRate1: 0,
LDisRate2: 0,
LDisRate3: 0,
LDisRate4: 0,
LDisRate5: 0,
// 🔹 Para / Fiyat Bilgileri
DocCurrencyCode: 'USD',
PriceCurrencyCode: 'USD',
PriceExchangeRate: 1,
Price: 0,
// 🔹 Referans Bilgiler
PriceListLineID: '',
BaseProcessCode: '',
BaseOrderNumber: '',
BaseCustomerTypeCode: 0,
BaseCustomerCode: '',
BaseSubCurrAccID: '',
BaseStoreCode: '',
SupportRequestHeaderID: '',
// 🔹 Takip / Sistem Bilgileri
OrderLineSumID: 0,
OrderLineBOMID: 0,
// 🔹 Kullanıcı / Tarih
CreatedUserName: '',
CreatedDate: dayjs().toISOString(),
LastUpdatedUserName: '',
LastUpdatedDate: dayjs().toISOString(),
// 🔹 Vergi & Ek Kodlar
SurplusOrderQtyToleranceRate: 0,
PurchaseRequisitionLineID: '',
WithHoldingTaxTypeCode: '',
DOVCode: '',
OrderLineLinkedProductID: '',
// 🔹 Frontende özel ek alanlar (UI binding için)
selectedModel: '', // model (ürün kodu)
selectedColor: '', // renk
selectedColor2: '', // ikinci renk
bedenLabels: [], // beden başlıkları
bedenValues: {}, // {38:2, 40:1, 42:0} gibi
unitPrice: 0,
totalAmount: 0,
note: '',
})
const editingIndex = ref(-1) // aktif düzenlenen satırın indexi
const summaryRows = ref([]) // tüm satırların listesi (grid kaynağı)
// 🔹 Düzenleme durumunu hesaplayan computed
const isEditing = computed(() => editingIndex.value >= 0)
/* ===========================================================
🔹 1. ve 2. Renk Select Referansları
QSelect bileşenlerine erişmek için template refleri tutulur.
=========================================================== */
const renkSelect = ref(null)
const renk2Select = ref(null)
const renkOptions = ref([]) // 1. renk seçenekleri
const renkOptions2 = ref([]) // 2. renk seçenekleri
/* ===========================================================
🔹 Kombinasyon Anahtarı Fonksiyonları
Aynı model + renk + 2. renk kombinasyonunun gridde olup olmadığını bulmak için
bu yardımcı fonksiyonlar kullanılır.
=========================================================== */
const getComboKey = (o) => [o.model || '', o.renk || '', o.renk2 || ''].join('||')
// 99999 veya boş renkleri toleranslı eşleştirme
const isSameCombo = (row, form) => {
const sameModel = row.model === form.model
const renkOk =
(row.renk || '') === (form.renk || '') ||
(row.renk || '') === '99999' ||
(form.renk || '') === '99999'
const renk2Ok =
(row.renk2 || '') === (form.renk2 || '') ||
(row.renk2 || '') === '99999' ||
(form.renk2 || '') === '99999'
return sameModel && renkOk && renk2Ok
}
// Grid içinde aynı kombinasyonun indexini bulur
const findExistingIndexByForm = () =>
summaryRows.value.findIndex(r => isSameCombo(r, form))
/* ===========================================================
🔹 Ürün Ana Grubu Bazında Gruplanmış Satırlar
groupedRows computed fonksiyonu, satırları urunAnaGrubuna göre gruplar.
Her grup alt toplamları, açık/kapalı durumu ve beden setlerini içerir.
=========================================================== */
const groupOpen = reactive({}) // {"TAKIM ELBİSE": true, "GÖMLEK": false, ...}
const groupedRows = computed(() => {
const buckets = {}
const order = []
// Null veya hatalı satırları filtrele
const safeRows = (summaryRows.value || []).filter(r => r && r.urunAnaGrubu)
for (const [idx, row] of safeRows.entries()) {
const anaGrup = row.urunAnaGrubu.trim().toUpperCase()
if (!anaGrup) continue
// 🔹 Beden grubu anahtarını tespit et (schemaByKey üzerinden)
const bedenList = Object.keys(row.bedenMap?.[row.grpKey] || {})
const grpKey = detectBedenGroup(bedenList, row.urunAnaGrubu, row.kategori)
const grpSchema = schemaByKey.value[grpKey]
const bedenValues = grpSchema ? grpSchema.values : []
// 🔹 Eğer grup ilk kez görülüyorsa, yeni obje oluştur
if (!buckets[anaGrup]) {
buckets[anaGrup] = {
name: anaGrup,
key: grpKey,
rows: [],
toplamAdet: 0,
toplamTutar: 0,
bedenValues
}
order.push(anaGrup)
// ilk kez eklendiğinde default açık bırak
if (groupOpen[anaGrup] === undefined) groupOpen[anaGrup] = true
}
// 🔹 Satırları grup altına ekle
const adet = Number(row.adet || 0)
const tutar = Number(row.tutar || 0)
buckets[anaGrup].rows.push({ row, idx })
buckets[anaGrup].toplamAdet += adet
buckets[anaGrup].toplamTutar += tutar
// 🔹 Daha geniş beden seti varsa grup seviyesinde güncelle
if (buckets[anaGrup].bedenValues.length < bedenValues.length)
buckets[anaGrup].bedenValues = bedenValues
}
// 🔹 Sonuç sıralı dizi olarak döner (UI grid için)
return order.map(name => ({
...buckets[name],
open: groupOpen[name]
}))
})
/* ===========================================================
🔹 Grup Aç/Kapa Fonksiyonu
groupedRows içindeki grupOpen reactive objesini günceller.
Kullanıcı bir ürün grubunu kapattığında alt satırlar gizlenir.
=========================================================== */
function toggleGroup(groupName) {
if (!groupName) return
groupOpen[groupName] = !groupOpen[groupName]
console.log(`📂 Grup "${groupName}" artık ${groupOpen[groupName] ? 'açık' : 'kapalı'}`)
}
// ===========================================================
// 🔹 Grup Açık/Kapalı Durumunu summaryRowsa göre otomatik güncelle
// Eski sipariş çağrıldığında tüm gruplar açık gelsin
// ===========================================================
watch(summaryRows, rows => {
if (!Array.isArray(rows)) return
rows.forEach(r => {
if (r.urunAnaGrubu && groupOpen[r.urunAnaGrubu] === undefined) {
groupOpen[r.urunAnaGrubu] = true
}
})
})
// 🔹 Sipariş genel açıklaması (ör. “Yaz sezonu toplu sipariş”)
const siparisGenelAciklama = ref('')
const DRAFT_KEY = computed(() =>
mode.value === 'edit'
? `bssapp:order:draft:edit:${headerId.value}`
: `bssapp:order:draft:new:${txId.value || 'noguid'}`
)
// ===========================================================
// ✅ AXIOS INSTANCE TANIMI
// Tüm backend çağrıları bu instance üzerinden geçer.
// Token otomatik eklenir, 401 durumunda login sayfasına yönlendirir.
// ===========================================================
const API_BASE = 'http://localhost:8080'
// İstek öncesi interceptor — token ekleme
api.interceptors.request.use(cfg => {
const token = localStorage.getItem('token')
if (token) cfg.headers.Authorization = `Bearer ${token}`
return cfg
})
// Yanıt interceptor — 401 durumunda login'e yönlendir
api.interceptors.response.use(
r => r,
err => {
if (err?.response?.status === 401) {
localStorage.removeItem('token')
if (typeof window !== 'undefined') window.location.href = '/login'
}
return Promise.reject(err)
}
)
/* ===========================================================
🔹 detectBedenGroup — Beden Grubunu Otomatik Tespit Et
Bu fonksiyon, ürünün "ana grup" ve "kategori" bilgilerine
göre hangi beden setine (takım, gömlek, pantolon, ayakkabı vs.)
ait olduğunu belirler. Bu bilgi grid yapısını belirler.
=========================================================== */
function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
// Beden listesi normalize edilir (trim, uppercase, boşsa default ' ')
const list = Array.isArray(bedenList) && bedenList.length > 0
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
: [' ']
// Ana grup temizleme: parantez içlerini ve özel karakterleri kaldırır
const ana = (urunAnaGrubu || '')
.toUpperCase()
.trim()
.replace(/\(.*?\)/g, '') // (Regular Fit) gibi notları siler
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '') // özel karakterleri temizler
.replace(/\s+/g, ' ') // fazla boşlukları tek boşluk yapar
// Kategori de temizlenir
const kat = (urunKategori || '').toUpperCase().trim()
// ✅ Aksesuar gruplarının listesi
const aksesuarGruplari = [
'AKSESUAR', 'KRAVAT', 'PAPYON', 'KEMER',
'CORAP', 'ÇORAP', 'FULAR', 'MENDIL', 'MENDİL',
'KASKOL', 'ASKI', 'YAKA', 'KOL DUGMESI', 'KOL DÜĞMESİ'
]
// ✅ Giyim grupları
const giyimGruplari = ['GÖMLEK', 'CEKET', 'PANTOLON', 'MONT', 'YELEK', 'TAKIM', 'TSHIRT', 'TİŞÖRT']
// ✅ Harfli beden tespiti: XS, S, M, L, XL, XXL gibi
const harfliBedenler = ['XS', 'S', 'M', 'L', 'XL', 'XXL', '3XL', '4XL']
if (list.some(b => harfliBedenler.includes(b))) {
return 'gom' // gömlek / tişört tarzı gruplar
}
// ⚙️ Eğer aksesuar kelimesi geçiyor ama giyim grubu değilse
if (
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
!giyimGruplari.some(g => ana.includes(g))
) {
return 'aksbir'
}
// 🔢 Pantolon + Garson (yaş grubu) özel kuralları
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan'
if (kat.includes('GARSON')) return 'yas'
// 🔢 Tamamen sayısal bedenler (örneğin 3944)
const allNumeric = list.every(v => /^\d+$/.test(v))
if (allNumeric) {
const nums = list.map(v => parseInt(v, 10)).filter(Boolean)
const diffs = nums.slice(1).map((v, i) => v - nums[i])
if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk'
}
// 🧩 Eğer hiçbiri değilse:
// Harf içeriyorsa 'gom', değilse 'tak' (takım elbise)
const sample = list[0]
if (/[A-Z]/.test(sample)) return 'gom'
return 'tak'
}
/* ===========================================================
🔹 Seri Matrix — Excel benzeri çarpan tabloları
Her ürün tipi için (takım, gömlek, pantolon vs.)
önceden tanımlanmış seri setlerini tutar.
Örneğin “4658 seri” seçilirse 46=1, 48=1, … şeklinde çarpanlar oluşur.
=========================================================== */
const seriMatrix = {
tak: {
'46-58 seri': { 46:1, 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 },
'46-58 ara çift': { 46:1, 48:2, 50:2, 52:2, 54:1, 56:1, 58:1 },
'44-58 seri': { 44:1, 46:1, 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 },
'44-58 ara çift': { 44:1, 46:1, 48:2, 50:2, 52:2, 54:1, 56:1, 58:1 },
'60-64 seri': { 60:1, 62:1, 64:1 },
'66-70 seri': { 66:1, 68:1, 70:1 },
'48-58 seri': { 48:1, 50:1, 52:1, 54:1, 56:1, 58:1 }
},
gom: {
'XS-XXL': { XS:1, S:1, M:1, L:1, XL:1, XXL:1 },
'XS-XXL ara çift': { XS:1, S:1, M:2, L:2, XL:2, XXL:1 },
'3XL-5XL': { '3XL':1, '4XL':1, '5XL':1 }
},
ayk: {
'10\'lu seri': { 39:1, 40:2, 41:2, 42:2, 43:2, 44:1 },
'39-44': { 39:1, 40:1, 41:1, 42:1, 43:1, 44:1 },
'45-47': { 45:1, 46:1, 47:1 }
},
yas: {
'2-14Y': { 2:1, 4:1, 6:1, 8:1, 10:1, 12:1, 14:1 }
},
pan: {
'38-50 seri': { 38:1, 40:1, 42:1, 44:1, 46:1, 48:1, 50:1 },
'38-50 ara çift': { 38:1, 40:1, 42:2, 44:2, 46:2, 48:1, 50:1 },
'52-56 seri': { 52:1, 54:1, 56:1 },
'58-62 seri': { 58:1, 60:1, 62:1 }
}
}
// 🔹 Aktif ürün grubuna göre uygun seri setlerini dinamik hesaplar
const activeSeriesOptions = computed(() => {
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
const sets = seriMatrix[grpKey] || {}
return Object.keys(sets).map(k => ({ label: k, value: k, isActive: true }))
})
/* ===========================================================
🔹 Para Birimi ve Toplam Tutar Hesaplaması
Sipariş toplamları ve para birimi seçimi burada yönetilir.
=========================================================== */
const paraBirimOptions = ['USD', 'EUR', 'TRY'] // Kullanıcıya sunulacak döviz seçenekleri
// Grid altındaki “Toplam Tutar” alanı dinamik hesaplanır.
const toplamTutar = computed(() => {
const sum = orderStore.totalAmount ? Number(orderStore.totalAmount) : 0
return isNaN(sum) ? 0 : sum
})
/* ===========================================================
🔹 Cari Bilgileri
Cari (müşteri) listesi, arama filtresi ve seçim sonrası
para birimi kontrolü burada yapılır.
=========================================================== */
const selectedCari = ref(null) // Kullanıcının seçtiği cari kodu
const cariOptions = ref([]) // Tüm cari listesi (backend'den gelir)
const filteredCariOptions = ref([]) // Arama filtrelenmiş hali
const loadingCari = ref(false) // Yükleniyor göstergesi
const cariInfo = ref(null) // Seçilen carinin tüm bilgisi
/* ===========================================================
🔹 Cari Listesini Yükleme Fonksiyonu
Uygulama açıldığında veya cari seçimi değiştiğinde çağrılır.
Backend'den /api/customer-list endpoint'ini çağırır.
=========================================================== */
async function loadCariList() {
loadingCari.value = true
try {
ensureAuthOrRedirect() // Token kontrolü, gerekirse login yönlendirmesi
const res = await api.get('/customer-list')
const data = res?.data
// Gelen data farklı formatlarda olabilir, esnek parse yapılır
if (!data) {
cariOptions.value = []
} else if (Array.isArray(data)) {
cariOptions.value = data
} else if (Array.isArray(data?.data)) {
cariOptions.value = data.data
} else {
cariOptions.value = []
}
// Filtre listesi de aynı anda güncellenir
filteredCariOptions.value = cariOptions.value
console.log('🧾 Cari listesi yüklendi:', cariOptions.value.length)
} catch (err) {
console.error('❌ Cari listesi alınamadı:', err)
$q.notify({ type: 'negative', message: 'Cari listesi yüklenemedi ❌' })
} finally {
loadingCari.value = false
}
}
// ===========================================================
// 🔹 Local Draft Yönetimi
// ===========================================================
function saveDraft() {
const draft = {
header: {
OrderDate: form.olusturmaTarihi,
AverageDueDate: form.tahminiTerminTarihi,
CurrAccCode: selectedCari.value,
DocCurrencyCode: form.pb,
Description: siparisGenelAciklama.value
},
lines: summaryRows.value
}
localStorage.setItem(DRAFT_KEY.value, JSON.stringify(draft))
}
function loadDraft() {
const raw = localStorage.getItem(DRAFT_KEY.value)
if (!raw) return false
try {
const { header, lines } = JSON.parse(raw)
if (header) {
form.olusturmaTarihi = header.OrderDate || form.olusturmaTarihi
form.tahminiTerminTarihi = header.AverageDueDate || form.tahminiTerminTarihi
form.pb = header.DocCurrencyCode || form.pb
selectedCari.value = header.CurrAccCode || ''
siparisGenelAciklama.value = header.Description || ''
}
if (Array.isArray(lines)) summaryRows.value = [...lines]
return true
} catch {
return false
}
}
function clearDraft() {
localStorage.removeItem(DRAFT_KEY.value)
}
/* ===========================================================
🔹 onMounted — İlk Yüklemeler
Uygulama ilk açıldığında auth kontrolü, store restore,
cari ve model listesi yükleme işlemleri yapılır.
=========================================================== */
/* ===========================================================
🔹 onMounted — İlk Yüklemeler
Uygulama ilk açıldığında auth kontrolü, store restore,
cari ve model listesi yükleme işlemleri yapılır.
=========================================================== */
onMounted(async () => {
ensureAuthOrRedirect()
if (orderId) {
console.log('✏️ Düzenleme modu başlatılıyor:', orderId)
mode.value = 'edit'
isEditMode.value = true
headerId.value = orderId
await loadOrderById(orderId)
} else {
console.log('🆕 Yeni sipariş modu başlatılıyor')
mode.value = 'new'
isEditMode.value = false
form.OrderHeaderID = crypto.randomUUID()
txId.value = form.OrderHeaderID
resetForm()
}
await Promise.all([loadCariList(), loadModels()])
console.log('✅ OrderEntry ekranı hazır — mode:', mode.value)
})
/* ===========================================================
🔹 Cari Filtreleme (Arama Kutusu)
QSelect bileşeninde “use-input” aktif olduğunda çalışır.
Kullanıcının yazdığı değeri cari listesinde arar (kod + ad bazlı).
=========================================================== */
function filterCari(val, update) {
if (val === '') {
// Boş arama → tüm cari listesi geri yüklenir
update(() => (filteredCariOptions.value = cariOptions.value))
return
}
// Küçük harfe çevirip hem ad hem kod üzerinden arama yap
update(() => {
const needle = val.toLowerCase()
filteredCariOptions.value = cariOptions.value.filter(v =>
(v.Cari_Ad || '').toLowerCase().includes(needle) ||
(v.Cari_Kod || '').toLowerCase().includes(needle)
)
})
}
onMounted(() => {
// ♻️ Daha önce kaydedilmiş siparişler varsa geri yükle
orderStore.loadFromStorage()
// 💾 Order değişikliklerini izleyip her değişiklikte kaydet
orderStore.watchOrders()
// 💾 LocalStorage geri yüklendikten sonra grid senkronizasyonu
onMounted(async () => {
await nextTick()
if (orderStore.orders && orderStore.orders.length > 0) {
summaryRows.value = [...orderStore.orders]
console.log('💾 Grid satırları LocalStoragedan yüklendi:', summaryRows.value.length)
} else {
console.log(' LocalStorage boş, grid başlatılmadı.')
}
})
// 🔄 Store değişiklikleri anlık olarak gride yansıt
watch(
() => orderStore.orders,
newOrders => {
summaryRows.value = [...newOrders]
},
{ deep: true, immediate: true }
)
console.log(
'♻️ LocalStorage geri yükleme tamamlandı. Aktif transaction:',
orderStore.activeTransactionId || '—'
)
})
// 🧹 Sayfa kapanmadan önce tekrar yedekle (fail-safe)
onBeforeUnmount(() => {
orderStore.saveToStorage()
console.log('💾 Sayfa kapatılırken veriler son kez kaydedildi.')
})
// 🔄 Reaktif izleme: orderStore.orders değiştiğinde kalıcı yaz
watch(
() => orderStore.orders,
() => orderStore.saveToStorage(),
{ deep: true }
)
/* ===========================================================
🔹 onMounted: Header Gap Güncelleme
order-grid-header yüksekliğini ölçüp CSS değişkeni olarak kaydeder.
Böylece sticky header ile grid gövdesi arasında tam hizalama sağlanır.
=========================================================== */
onMounted(() => {
nextTick(() => { // DOM tamamen yüklensin
const hdr = document.querySelector('.order-grid-header')
if (!hdr) {
console.warn('⚠️ .order-grid-header bulunamadı, ölçüm atlandı.')
return
}
const updateHeaderGap = () => {
if (!hdr || !hdr.parentNode) return // ⚠️ güvenlik kontrolü eklendi
const rect = hdr.getBoundingClientRect()
const height = rect.height || 0
const fineAdjust = -height
document.documentElement.style.setProperty('--header-body-gap', `${fineAdjust}px`)
console.log('📏 Header yüksekliği ölçüldü:', height, 'gap:', fineAdjust)
}
updateHeaderGap()
const resizeObs = new ResizeObserver(() => {
if (hdr?.parentNode) updateHeaderGap()
})
resizeObs.observe(hdr)
const onResize = () => {
if (hdr?.parentNode) updateHeaderGap()
}
window.addEventListener('resize', onResize)
onBeforeUnmount(() => {
resizeObs.disconnect()
window.removeEventListener('resize', onResize)
})
})
})
/* ===========================================================
🔹 Cari Seçimi (onCariChange)
Kullanıcı cari seçtiğinde hem cari bilgisi yüklenir,
hem de ilgili para birimi (PB) otomatik olarak set edilir.
=========================================================== */
async function onCariChange(kod) {
// 1⃣ Cari bilgiyi lokal listeden bul
cariInfo.value = cariOptions.value.find(c => c.Cari_Kod === kod) || null
// 2⃣ Varsayılan PB USD (fallback)
let pb = cariInfo.value?.Doviz_Cinsi || 'USD'
try {
// 3⃣ Eğer local veride Doviz_Cinsi yoksa backend'den çağır
if (!cariInfo.value?.Doviz_Cinsi && kod) {
const res = await api.get('/customer-detail', { params: { code: kod } })
const data = res?.data || {}
// Backend farklı property isimleri döndürebileceği için esnek kontrol
if (data.Doviz_Cinsi || data.ParaBirimi || data.currency) {
pb = data.Doviz_Cinsi || data.ParaBirimi || data.currency
console.log(`💱 Cari (${kod}) para birimi backend'den alındı: ${pb}`)
} else {
console.log(`💵 Cari (${kod}) için PB bulunamadı, USD olarak atanıyor.`)
pb = 'USD'
}
}
} catch (err) {
console.warn('⚠️ Cari detay alınamadı, USD olarak devam ediliyor.', err)
pb = 'USD'
}
// 4⃣ Global ve form seviyesinde PB'yi güncelle
aktifPB.value = pb
form.pb = pb
// 5⃣ Eğer model seçiliyse, PB değiştiği için min fiyatı yeniden çek
if (form.model) {
try {
await fetchMinPrice()
} catch (e) {
console.warn('⚠️ Min fiyat yenilenemedi:', e)
}
}
// 6⃣ Bilgi logu
console.log(`💱 Aktif PB setlendi: ${pb}`)
}
/* ===========================================================
🔹 Model Listesi (Ürün Seçimi)
Backend'den /api/products endpointinden tüm ürün kodları çekilir.
=========================================================== */
const modelOptions = ref([]) // Model seçenekleri
const filteredModelOptions = ref([]) // Arama ile filtrelenmiş hali
const loadingModels = ref(false) // Spinner için flag
async function loadModels() {
loadingModels.value = true
try {
ensureAuthOrRedirect()
const res = await api.get('/products')
const arr = res?.data || []
// Backend'den ProductCode alanı çekilir
modelOptions.value = arr.map(x => ({
label: x.ProductCode,
value: x.ProductCode
}))
filteredModelOptions.value = modelOptions.value
console.log('✅ Model listesi yüklendi:', modelOptions.value.length)
} catch (err) {
console.error('❌ Model listesi alınamadı:', err)
$q.notify({ type: 'negative', message: 'Model listesi alınamadı ❌' })
} finally {
loadingModels.value = false
}
}
/* ===========================================================
🔹 Model Arama (QSelect içinde)
Kullanıcının yazdığı harflerle model kodlarını filtreler.
=========================================================== */
function filterModel(val, update) {
if (val === '') {
update(() => (filteredModelOptions.value = modelOptions.value))
return
}
update(() => {
const needle = val.toLowerCase()
filteredModelOptions.value = modelOptions.value.filter(v =>
(v.label || '').toLowerCase().includes(needle)
)
})
}
/* ===========================================================
🔹 MODEL SEÇİMİ (onModelChangeV2)
Yeni model seçildiğinde renkler, ürün bilgileri, min fiyat,
stok ve beden grubu eksiksiz yenilenir; açıklama ve adet korunur.
=========================================================== */
/* ===========================================================
🔹 MODEL SEÇİMİ (onModelChange)
Yeni model seçildiğinde renkler, ürün bilgileri, min fiyat,
stok ve beden grubu eksiksiz yenilenir; açıklama ve adet korunur.
=========================================================== */
async function onModelChange(modelCode) {
if (!modelCode) {
console.warn('⚠️ Model kodu boş, sorgu yapılmadı.')
return
}
// 🧩 Önceki değerleri yedekle (korunacak alanlar)
const keep = {
aciklama: form.aciklama,
bedenler: [...form.bedenler],
bedenLabels: [...form.bedenLabels],
fiyat: form.fiyat,
adet: form.adet,
tutar: form.tutar
}
try {
ensureAuthOrRedirect()
// 🎨 1⃣ Renk listesi
const resColors = await api.get('/product-colors', { params: { code: modelCode } })
renkOptions.value = (resColors?.data || []).map(x => ({
label: `${x.color_code || x.ColorCode} ${x.color_description || x.ColorDesc || ''}`,
value: x.color_code || x.ColorCode
}))
// 🧱 2⃣ Ürün detayları
const resDetail = await api.get('/product-detail', { params: { code: modelCode } })
const d = resDetail?.data || {}
Object.assign(form, {
model: modelCode,
urunAnaGrubu: d.UrunAnaGrubu || d.ProductGroup || '',
urunAltGrubu: d.UrunAltGrubu || d.ProductSubGroup || '',
fit: d.Fit1 || d.Fit || '',
urunIcerik: d.UrunIcerik || d.Fabric || '',
drop: d.Drop || '',
kategori: d.Kategori || '',
askiliyan: d.AskiliYan || '',
aciklama: keep.aciklama,
fiyat: keep.fiyat,
adet: keep.adet,
tutar: keep.tutar,
bedenLabels: keep.bedenLabels,
bedenler: keep.bedenler
})
console.log('📦 Model detayları yüklendi:', d.UrunAnaGrubu, d.Fit1)
// 💰 3⃣ Min fiyatı yükle
await fetchMinPrice()
// ⚙️ 4⃣ Renk bulunmazsa doğrudan bedenleri yükle
if (!renkOptions.value.length) {
await loadProductSizes(true)
}
// 🧮 5⃣ Gridde mevcut kombinasyon varsa düzenleme moduna al
await openExistingCombination()
// ✅ 6⃣ Yeni model sonrası otomatik stok/beden yükle (forceRefresh)
await handleNewCombination()
$q.notify({
type: 'info',
message: `Model "${modelCode}" yüklendi ✅`,
position: 'top-right'
})
} catch (err) {
console.error('❌ Model verileri alınamadı:', err)
$q.notify({
type: 'negative',
message: 'Model bilgileri alınamadı ❌',
position: 'top-right'
})
}
}
/* ===========================================================
🔹 RENK SEÇİMİ (1. Renk Değişimi)
Kullanıcı model seçtikten sonra 1. rengi seçtiğinde:
- 2. renk seçenekleri sıfırlanır
- 2. renk listesi backend'den yüklenir
- Eğer 2. renk tanımı yoksa doğrudan bedenler yüklenir
=========================================================== */
async function onColorChange(colorCode) {
form.renk = colorCode || ''
renkOptions2.value = []
form.renk2 = ''
// 2. renk QSelect bileşenini sıfırla
if (renk2Select.value) renk2Select.value.reset()
// ⚠️ Renk seçilmediyse işlemi iptal et
if (!form.renk) {
console.warn('⚠️ Renk seçilmedi, işlemler durduruldu.')
return
}
try {
ensureAuthOrRedirect()
// 🎨 2⃣ İkinci renk listesini yükle
const res = await api.get('/product-secondcolor', {
params: { code: form.model, color: colorCode }
})
const data = res?.data || []
if (Array.isArray(data) && data.length > 0) {
renkOptions2.value = data.map(x => ({
label: x.item_dim2_code,
value: x.item_dim2_code
}))
console.log('🎨 2. renk listesi yüklendi:', renkOptions2.value.length)
} else {
// 2. renk yoksa doğrudan beden/stok yükle
console.log('⚪ 2. renk yok, doğrudan beden/stok yükleniyor...')
await loadProductSizes(true)
}
await handleNewCombination()
} catch (err) {
console.error('❌ 1. renk sonrası hata:', err)
}
}
/* ===========================================================
🔹 2. RENK SEÇİMİ (onColor2Change)
Kullanıcı 2. renk seçtiğinde beden/stok sorgusu yeniden yapılır.
Ayrıca kombinasyon gridde varsa form otomatik doldurulur.
=========================================================== */
async function onColor2Change(colorCode2) {
if (typeof colorCode2 === 'object' && colorCode2?.value) {
colorCode2 = colorCode2.value
}
form.renk2 = colorCode2 || ''
try {
ensureAuthOrRedirect()
// 2. renk seçildikten sonra stok/beden yükle
await loadProductSizes(true)
// Aynı kombinasyon varsa düzenleme moduna al
await openExistingCombination()
await handleNewCombination()
} catch (err) {
console.error('❌ 2. renk sonrası hata:', err)
}
}
/* ===========================================================
🔹 MODEL + PB Bazlı Minimum Fiyat
Backendde her model + para birimi kombinasyonu için
minimum satış fiyatı tutulur. Kullanıcı fiyat girdiğinde
bu alt limitin altına inmemesi sağlanır.
=========================================================== */
async function fetchMinPrice() {
if (!form.model || !form.pb) {
console.warn('⚠️ Fiyat sorgusu için model veya PB eksik.')
return
}
try {
// Storedaki fetchMinPrice fonksiyonu backendden veri çeker
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
if (priceData) {
// Döviz bazlı fiyatlar + TL karşılığı (rateToTRY) setlenir
form.minFiyat = Number(priceData.price || 0)
form.kur = Number(priceData.rateToTRY || 1)
form.minFiyatTRY = Number(priceData.priceTRY || 0)
console.log(
`💰 Fiyatlar yüklendi: ${form.minFiyat} ${form.pb} (${form.minFiyatTRY.toFixed(2)} TRY)`
)
} else {
// Backend boş döndüyse default sıfırla
form.minFiyat = 0
form.kur = 1
form.minFiyatTRY = 0
}
} catch (err) {
console.error('❌ Min fiyat alınamadı:', err)
form.minFiyat = 0
}
}
/* ===========================================================
🔹 Beden / Stok Yükleyici (loadProductSizes)
Bu fonksiyon ERPden renk+model bazlı beden ve stok bilgisini çeker.
Ayrıca MSSQL stoklarıyla merge eder ve cacheler.
=========================================================== */
const sizeCache = ref({}) // Tekrar sorguları engellemek için cache
const bedenStock = ref([]) // Görsel tablo için stok listesi
const stockMap = ref({}) // { "48": 12, "50": 7, ... } şeklinde key-value map
/* ===========================================================
🔹 Beden / Stok Yükleme Fonksiyonu
forceRefresh = true → cache'i yok say, API'den güncel çek
=========================================================== */
async function loadProductSizes(forceRefresh = false) {
if (!form.model) {
console.warn('⚠️ Beden yüklenemedi: model seçilmemiş.')
return
}
const colorKey = (form.renk && form.renk.trim() !== '') ? form.renk.trim() : 'nocolor'
const color2Key = (form.renk2 && form.renk2.trim() !== '') ? form.renk2.trim() : 'no2color'
const key = `${form.model}_${colorKey}_${color2Key}`
console.log('🧩 loadProductSizes → key:', key, '| forceRefresh:', forceRefresh)
// 💾 Cacheden veri varsa ve forceRefresh=false ise cache kullan
if (!forceRefresh && sizeCache.value[key]) {
console.log(`💾 Cacheden yüklendi: ${key}`)
const cached = sizeCache.value[key]
// ✅ Mevcut adetleri koru
const previousMap = {}
form.bedenLabels?.forEach((lbl, i) => {
previousMap[lbl] = Number(form.bedenler?.[i] || 0)
})
form.bedenLabels = cached.labels
form.bedenler = form.bedenLabels.map(lbl => Number(previousMap[lbl] || 0))
bedenStock.value = cached.stockArray
stockMap.value = { ...cached.stockMap }
// ⚡ Cache yüklenmiş olsa bile MSSQL stoklarını güncelle (edit mode için)
console.log('🔄 Cache sonrası MSSQL stokları tazeleniyor...')
await loadOrderInventory(true)
return
}
try {
ensureAuthOrRedirect()
const params = { code: form.model }
if (form.renk?.trim()) params.color = form.renk.trim()
if (form.renk2?.trim()) params.color2 = form.renk2.trim()
console.log('📦 Beden/stok sorgusu gönderiliyor:', params)
const res = await api.get('/product-colorsize', { params })
const data = res?.data || []
console.log(`📦 Gelen beden/stok kayıt sayısı: ${data.length}`)
if (!Array.isArray(data) || data.length === 0) {
console.warn('⚪ Bu kombinasyon için tanımlı beden bulunamadı.')
form.bedenLabels = []
form.bedenler = []
bedenStock.value = []
stockMap.value = {}
return
}
// 🔹 Gelen beden kodlarını normalize et
const bedenList = Array.from(
new Set(
data.map(x => x.item_dim1_code?.trim() || x.ItemDim1Code?.trim()).filter(Boolean)
)
)
// 🔹 Önceki adetleri koru
const previousMap = {}
form.bedenLabels?.forEach((lbl, i) => {
previousMap[lbl] = Number(form.bedenler?.[i] || 0)
})
// 🔹 Aktif beden grubu belirle
const grpKey = detectBedenGroup(bedenList, form.urunAnaGrubu, form.kategori)
const grp = schemaByKey.value[grpKey] || schemaByKey.value.tak
form.bedenLabels = grp.values
form.bedenler = form.bedenLabels.map(lbl => Number(previousMap[lbl] || 0))
console.log(`✅ Aktif beden grubu: ${grp.title} (${grpKey})`)
// 🔹 ERP stoklarını işle
const stockArray = data.map(x => ({
beden: x.item_dim1_code?.trim() || x.ItemDim1Code?.trim(),
stok: Number(
x.kullanilabilir_envanter ||
x.Kullanilabilir_Envanter ||
x.stock ||
0
)
}))
const stockMapLocal = {}
for (const s of stockArray) stockMapLocal[s.beden] = s.stok
stockMap.value = { ...stockMapLocal }
bedenStock.value = [...stockArray]
console.log(`🧮 İlk stok verisi işlendi (${stockArray.length} beden)`)
// 🔹 MSSQL stoklarını merge et
await loadOrderInventory(true)
// 💾 Cache güncelle
sizeCache.value[key] = {
labels: [...form.bedenLabels],
stockArray: [...bedenStock.value],
stockMap: { ...stockMap.value }
}
console.log(`✅ Cache güncellendi: ${key}`)
} catch (err) {
console.error('❌ Beden/stok verisi yüklenirken hata oluştu:', err)
$q.notify({ type: 'negative', message: 'Beden/stok verisi alınamadı ❌' })
}
}
/* ===========================================================
🔹 loadOrderInventory (GÜNCELLENMİŞ)
MSSQL stok sorgusu — artık boş değerleri 0 yapmıyor.
merge=true ise sadece dolu değerleri günceller.
=========================================================== */
async function loadOrderInventory(merge = false) {
if (!form.model) {
console.warn('⚠️ Stok yüklenemedi: model seçilmemiş.')
return
}
try {
ensureAuthOrRedirect()
const params = { code: form.model }
if (form.renk?.trim()) params.color = form.renk.trim()
if (form.renk2?.trim()) params.color2 = form.renk2.trim()
console.log('📦 MSSQL stok sorgusu gönderiliyor:', params)
const res = await api.get('/order-inventory', { params })
const data = res?.data || []
console.log('📦 MSSQL stok verisi geldi:', data.length)
// 1⃣ Normalize et
const invMap = {}
for (const x of data) {
let beden = (x.Beden || x.beden || '').trim()
if (beden === '') beden = ' '
const stokDeger =
x.KullanilabilirEnvanter ??
x.kullanilabilir_envanter ??
x.Kullanilabilir_Envanter ??
null
if (stokDeger != null) invMap[beden] = Number(stokDeger)
}
// 2⃣ Form bedenlerine göre map oluştur
const newMap = {}
for (const lbl of form.bedenLabels || []) {
const key = lbl?.trim() === '' ? ' ' : lbl.trim()
if (invMap[key] != null) newMap[lbl] = invMap[key]
}
// 3⃣ Merge veya replace
if (merge && stockMap.value) {
for (const lbl of Object.keys(newMap)) {
// sadece yeni stok bilgisi varsa güncelle
if (newMap[lbl] != null && !isNaN(newMap[lbl])) {
stockMap.value[lbl] = newMap[lbl]
}
}
} else {
stockMap.value = { ...newMap }
}
// 4⃣ bedenStock listesini senkronize et
bedenStock.value = Object.keys(stockMap.value).map(k => ({
beden: k,
stok: stockMap.value[k]
}))
console.log('✅ Stok haritası güncellendi:', stockMap.value)
} catch (err) {
console.error('❌ Order inventory yüklenemedi:', err)
$q.notify({
type: 'negative',
message: 'Stok verisi alınamadı ❌',
position: 'top-right'
})
}
}
function formatDate(v) {
if (!v) return '—'
try {
// ISO tarih ise parçala
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
const [y, m, d] = v.split('-')
return `${d}.${m}.${y}`
}
// diğer olası formatlar
const date = new Date(v)
if (isNaN(date.getTime())) return '—'
const dd = String(date.getDate()).padStart(2, '0')
const mm = String(date.getMonth() + 1).padStart(2, '0')
const yyyy = date.getFullYear()
return `${dd}.${mm}.${yyyy}`
} catch {
return '—'
}
}
/* ===========================================================
🔹 applyTerminToRows — Tahmini Termin Tarihini Grid Satırlarına Aktar
Formdaki tahmini termin tarihi değiştiğinde,
griddeki tüm satırlara aynı tarih işlenir (boş olanlara veya yeni eklenenlere)
=========================================================== */
function applyTerminToRows(dateStr) {
if (!dateStr || !Array.isArray(summaryRows.value)) return
summaryRows.value = summaryRows.value.map(r => {
if (!r.terminTarihi || r.terminTarihi === '') {
return { ...r, terminTarihi: dateStr }
}
return r
})
}
// 🔹 Watcher: Formdaki tahmini termin tarihi değiştiğinde tüm satırlara uygula
// ÜST formdaki tahmini termin değişince:
watch(
() => form.tahminiTerminTarihi,
(yeni, eski) => {
// 1) Önce editördeki tarih alanını güncelle
// Eğer kullanıcı editörde manuel farklı bir tarih tutmuyorsa, senkronla.
if (!form.terminTarihi || form.terminTarihi === eski) {
form.terminTarihi = yeni || ''
}
// 2) İstersen griddeki BOŞ termin alanlarına da uygula
// (dolu satırlara dokunmaz)
applyTerminToRows(yeni)
}
)
/*=========================================================== */
const selectedSeriSet = ref(null) // Seçili seri seti (ör: “46-58 seri”)
const seriMultiplier = ref(1) // Çarpan (ör: 2 → her bedene 2 ekler)
/* ===========================================================
🔹 Seri Uygulama (Tüm bedenlere aynı değeri atar)
Kullanıcı “ALL1” veya “ALL2” gibi genel seri modunu seçerse
tüm bedenlere aynı miktar yazılır.
=========================================================== */
function applySeri(f) {
f.bedenler = (f.bedenLabels || []).map(() =>
f.seri === 'ALL2' ? 2 : f.seri === 'ALL1' ? 1 : 0
)
updateTotals(f)
}
/* ===========================================================
🔹 Toplam Güncelleme Fonksiyonu
Beden girişlerinde adet veya fiyat değiştiğinde toplam tutarı hesaplar.
=========================================================== */
function updateTotals(f) {
f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
const fiyat = Number(f.fiyat) || 0
f.tutar = (f.adet * fiyat).toFixed(2)
}
// dışarıya alınmalı:
watch([includeKDV, toplamTutar], ([aktif, toplam]) => {
manualKDV.value = aktif ? Number(toplam || 0) * kdvOrani : 0
})
const toplamKDVli = computed(() => {
return Number(toplamTutar.value || 0) + (includeKDV.value ? Number(manualKDV.value || 0) : 0)
})
/* ===========================================================
🔹 Seri Seti Uygulama (applySeriSet)
Kullanıcı bir seri seti seçtiğinde, o setin beden-adet oranlarını
ilgili gruptan alır ve formdaki bedenlere ekler.
=========================================================== */
function applySeriSet() {
if (!selectedSeriSet.value) return
// 🔹 Aktif beden grubu anahtarını tespit et
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
// 🔹 Seçilen seri setinin anahtarını al (object veya string olabilir)
const setKey =
typeof selectedSeriSet.value === 'object'
? (selectedSeriSet.value.value || selectedSeriSet.value.label)
: selectedSeriSet.value
// 🔹 Seri patternini bul
const pattern = seriMatrix[grpKey]?.[setKey] || {}
const mult = Number(seriMultiplier.value) || 1
// 🔸 EKLEYEREK uygula — mevcut değerlere çarpanlı ekleme yapar
form.bedenler = form.bedenLabels.map((lbl, idx) => {
const current = Number(form.bedenler[idx] || 0)
const inc = Number(pattern[lbl] || 0) * mult
return current + inc
})
// 🔹 Toplam adet ve tutarı güncelle
updateTotals(form)
// 🔔 Kullanıcıya bilgi bildirimi
$q.notify({
type: 'positive',
message: `Seri seti "${setKey}" başarıyla eklendi ✅`
})
}
/* ===========================================================
🔹 Editing ve Satır İşlemleri
summaryRows, ekranda gridde görülen satırların tam listesidir.
editingIndex aktif olarak düzenlenen satırı tutar.
=========================================================== */
/* ===========================================================
🔹 Form → Grid Satırı Dönüştürücü (v2)
Kullanıcının formda yaptığı girişleri griddeki summaryRows
formatına dönüştürür. Ayrıca formdaki tahmini termin tarihini
grid satırına "terminTarihi" olarak işler.
=========================================================== */
function toSummaryRowFromForm() {
// 🔸 Beden grubu anahtarını tespit et
const grpKey = detectBedenGroup(form.bedenLabels, form.urunAnaGrubu, form.kategori)
// 🔸 Beden map oluştur
const bedenMap = {}
for (const lbl of form.bedenLabels) {
const index = form.bedenLabels.indexOf(lbl)
bedenMap[lbl] = Number(form.bedenler[index] || 0)
}
// 🔸 Grid satır objesi
const row = {
id: Date.now(), // geçici ID
model: form.model,
renk: form.renk,
renk2: form.renk2,
urunAnaGrubu: form.urunAnaGrubu,
urunAltGrubu: form.urunAltGrubu,
aciklama: form.aciklama,
fiyat: Number(form.fiyat || 0),
pb: form.pb || aktifPB.value || 'USD',
adet: Number(form.adet || 0),
tutar: Number(form.tutar || 0),
// 🔹 Grup yapısı
grpKey,
bedenMap: { [grpKey]: { ...bedenMap } },
// 🔹 Yeni: Tahmini termin tarihi formdan alınır
terminTarihi: form.terminTarihi || form.tahminiTerminTarihi || ''
}
console.log('📄 Grid satırı oluşturuldu:', row)
return row
}
/* ===========================================================
🔹 updateRow (Grid satırını güncelle veya ekle)
- Edit mode aktifse mevcut satırı günceller
- Yeni kayıt ise summaryRowsa ekler
- Adet, fiyat, tutar, aciklama, pb, bedenMap alanlarını senkronize eder
=========================================================== */
async function updateRow() {
try {
ensureAuthOrRedirect()
// 1⃣ Temel doğrulama
if (!form.model) {
$q.notify({ type: 'warning', message: 'Lütfen model seçin ⚠️' })
return
}
// 2⃣ Toplamları güncelle
updateTotals(form)
// 3⃣ Formu summaryRow formatına çevir
const row = toSummaryRowFromForm()
// 4⃣ Edit mode aktifse → mevcut satırı güncelle
if (editingIndex.value !== -1 && summaryRows.value[editingIndex.value]) {
const idx = editingIndex.value
const existing = summaryRows.value[idx]
console.log(`✏️ updateRow çalıştı → ID: ${existing.id || '(geçici)'}`)
// 🔹 Mevcut satır verilerini koruyarak güncelle
summaryRows.value[idx] = {
...existing,
...row, // formdaki yeni değerleri uygula
id: existing.id || row.id, // ID aynı kalmalı
grpKey: existing.grpKey || row.grpKey,
bedenMap: row.bedenMap, // beden adetleri
fiyat: form.fiyat,
pb: form.pb,
tutar: form.tutar,
aciklama: form.aciklama,
adet: form.adet
}
// 🧮 Grup ve toplamlar yeniden hesaplanır (recomputed)
updateTotals(form)
$q.notify({
type: 'positive',
message: 'Satır başarıyla güncellendi ✅',
position: 'top-right'
})
console.log('✅ Satır güncellendi:', summaryRows.value[idx])
}
else {
// 5⃣ Yeni satır ekleme
summaryRows.value.push(row)
console.log('🆕 Yeni satır eklendi:', row)
$q.notify({
type: 'positive',
message: 'Yeni kombinasyon eklendi 🧩',
position: 'top-right'
})
}
// 6⃣ Formu temizle (sadece yeni ekleme sonrası)
if (editingIndex.value === -1) {
resetForm(true)
} else {
// Edit sonrası formu bırak ama editingi kapat
editingIndex.value = -1
orderStore.selected = null
}
console.log('🧾 Toplam satır sayısı:', summaryRows.value.length)
} catch (err) {
console.error('❌ Satır güncellenirken hata oluştu:', err)
$q.notify({ type: 'negative', message: 'Satır güncellenemedi ❌' })
}
}
/* ===========================================================
🔹 openExistingCombination
Formda seçilen model + renk + renk2 kombinasyonu zaten gridde varsa,
o satırı düzenleme moduna alır ve formu otomatik doldurur.
Yoksa formu temizleyip yeni girişe hazırlar.
=========================================================== */
/* ===========================================================
🔹 openExistingCombination (Zero-Stock Safe v3)
Edit modunda stokların 0 görünme hatası çözülmüş versiyon.
Reactive flush + parametre doğrulaması içerir.
=========================================================== */
async function openExistingCombination() {
if (!form.model) return
// 🔍 1⃣ Aynı kombinasyon gridde var mı?
const idx = findExistingIndexByForm()
if (idx === -1) {
// 🆕 Yeni kombinasyon
editingIndex.value = -1
orderStore.selected = null
Object.assign(form, {
adet: 0,
fiyat: 0,
tutar: 0,
pb: aktifPB.value || 'USD'
})
console.log('🆕 Yeni kombinasyon → Form temizlendi')
return
}
// 🔹 2⃣ Mevcut satırı bul
const r = summaryRows.value[idx]
editingIndex.value = idx
orderStore.selected = { ...r }
// 🔒 Edit modda aktif model ve renk seçeneklerini sabitle
renkOptions.value = [{ label: r.renk, value: r.renk }]
renkOptions2.value = r.renk2
? [{ label: r.renk2, value: r.renk2 }]
: []
// 🔹 3⃣ Formu doldur
Object.assign(form, {
model: r.model,
renk: r.renk,
renk2: r.renk2,
urunAnaGrubu: r.urunAnaGrubu,
urunAltGrubu: r.urunAltGrubu,
aciklama: r.aciklama,
fiyat: Number(r.fiyat || 0),
pb: r.pb || aktifPB.value || 'USD',
adet: Number(r.adet || 0),
tutar: Number(r.tutar || 0)
})
// ✅ 3B⃣ GRIDDEKİ TERMİN → EDİTÖR
form.terminTarihi = r.terminTarihi || form.terminTarihi || form.tahminiTerminTarihi || ''
// 🔹 4⃣ Beden grubunu geri yükle
const key = r.grpKey || activeGroupKeyForRow(r)
const grp = schemaByKey.value[key]
form.bedenLabels = grp?.values || []
const savedMap = r.bedenMap?.[key] || {}
form.bedenler = form.bedenLabels.map(lbl => Number(savedMap[lbl] || 0))
// ✅ 5⃣ Stokları yeniden getir (forceRefresh = true)
try {
console.log('🔄 Stoklar tazeleniyor (edit mode)...')
await nextTick()
await new Promise(resolve => setTimeout(resolve, 250))
await nextTick()
console.log('🧩 loadProductSizes çağrısı öncesi:', {
model: form.model,
renk: form.renk,
renk2: form.renk2
})
if (!form.model || !form.renk) {
console.warn('⚠️ Model veya renk eksik, stok çağrısı atlanıyor.', form)
} else {
await loadProductSizes(true)
const stoklar = Object.values(stockMap.value || {})
console.log(`📦 Güncel stoklar: ${stoklar.join(', ')}`)
if (stoklar.length && stoklar.every(v => Number(v) === 0)) {
console.warn('⚠️ Backend 0 stok döndürdü → form:', form)
$q.notify({
type: 'warning',
message: '⚠️ Bu kombinasyon için stok bulunamadı (0).',
position: 'top-right'
})
} else {
console.log('✅ Stoklar başarıyla yüklendi.')
}
}
} catch (err) {
console.warn('❌ Stok bilgisi yenilenemedi:', err)
}
// 💱 Para birimi doğrulaması
if (!form.pb || form.pb === '') {
form.pb = aktifPB.value || 'USD'
console.log('💱 Para birimi formda eksikti, otomatik setlendi:', form.pb)
}
// 💰 Minimum fiyat kontrolü
try {
if (form.model && form.pb) {
console.log('💰 Min fiyat kontrolü başlatılıyor...')
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
const minFiyat = Number(priceData?.price || 0)
if (minFiyat > 0 && form.fiyat < minFiyat) {
form.fiyat = minFiyat
form.tutar = (form.adet || 0) * (form.fiyat || 0)
$q.notify({
type: 'warning',
message: `Fiyat min. seviyeye güncellendi (${minFiyat.toLocaleString('tr-TR')} ${form.pb})`,
position: 'top-right'
})
}
}
} catch (e) {
console.warn('⚠️ Fiyat bilgisi yenilenemedi:', e)
}
// 🔢 Toplam güncelle
updateTotals(form)
// 💬 Kullanıcıya bilgi ver
$q.notify({
type: 'info',
message: 'Mevcut kombinasyon düzenleme moduna alındı ✏️',
position: 'top-right'
})
console.log(
`✏️ Editör aktif → model=${r.model}, renk=${r.renk || '-'}, renk2=${r.renk2 || '-'}, pb=${form.pb}, termin=${form.terminTarihi}`
)
}
/* ===========================================================
🔹 Yeni Kombinasyon Seçimi (Model / Renk / 2. Renk)
Kullanıcı yeni model veya renk seçtiğinde stokların 0 görünmesini önler.
Vue flush tamamlandıktan sonra loadProductSizes(true) çağrılır.
=========================================================== */
async function handleNewCombination() {
if (!form.model) {
console.warn('⚠️ Model seçilmeden stok yüklenemez.')
return
}
console.log('🆕 Yeni kombinasyon seçildi:', {
model: form.model,
renk: form.renk,
renk2: form.renk2
})
try {
// 🧠 Reaktivite flush bitene kadar bekle
await nextTick()
await new Promise(resolve => setTimeout(resolve, 250))
await nextTick()
// ⚙️ Gereksiz çağrıları önle
if (!form.model || !form.renk) {
console.warn('⚠️ Model veya renk eksik, loadProductSizes atlandı.')
return
}
console.log('🧩 loadProductSizes çağrılıyor (handleNewCombination)...')
await loadProductSizes(true)
const stoklar = Object.values(stockMap.value || {})
if (stoklar.length && stoklar.every(v => Number(v) === 0)) {
$q.notify({
type: 'warning',
message: '⚠️ Bu kombinasyon için stok bulunamadı (0)',
position: 'top-right'
})
} else {
console.log('✅ Stoklar başarıyla yüklendi (yeni kombinasyon).')
}
// 🔹 Gridde varsa düzenleme moduna al
await openExistingCombination()
} catch (err) {
console.error('❌ handleNewCombination hatası:', err)
$q.notify({
type: 'negative',
message: 'Stok bilgisi alınamadı ❌',
position: 'top-right'
})
}
}
/* ===========================================================
🔹 useComboWatcher — Model / Renk / Renk2 değişimlerini izler
Tek bir yardımcı fonksiyonla handleNewCombination çağrısını yönetir.
Parametre: hangi alan değişti (model, renk, renk2)
=========================================================== */
/* ===========================================================
🔹 useComboWatcher v3 (Yeni Satır Güvenli)
Model / Renk / Renk2 değişimlerinde reaktif flush sonrası
otomatik stok yükleme + mevcut satır koruma.
=========================================================== */
function useComboWatcher(type, handler) {
return async (val) => {
try {
console.log(`🎯 useComboWatcher tetiklendi → ${type}:`, val)
const isNewRow = editingIndex.value === -1
const prevModel = form.model
// 1⃣ İlgili handler'ı (ör. onModelChange) çalıştır
if (typeof handler === 'function') {
await handler(val)
}
// 2⃣ Vue flush tamamlanmasını bekle
await nextTick()
await new Promise(resolve => setTimeout(resolve, 200))
await nextTick()
// 3⃣ Türüne göre zinciri yürüt
if (type === 'model') {
if (isNewRow || form.model !== prevModel) {
// 🗓️ Yeni satır açılıyorsa, editörde termin boşsa default terminle doldur
if (!form.terminTarihi || form.terminTarihi === '') {
form.terminTarihi = form.tahminiTerminTarihi || ''
}
console.log('🆕 Yeni satır veya model değişimi → stok yükleniyor...')
await handleNewCombination()
}
} else if (type === 'renk' || type === 'renk2') {
// 🗓️ Editörde termin boşsa default terminle doldur
if (!form.terminTarihi || form.terminTarihi === '') {
form.terminTarihi = form.tahminiTerminTarihi || ''
}
await handleNewCombination()
}
console.log(`✅ useComboWatcher(${type}) tamamlandı.`)
} catch (err) {
console.error(`❌ useComboWatcher(${type}) hatası:`, err)
}
}
}
/* ===========================================================
🔹 Satır Düzenleme (Manuel Edit stok güncelleme dahil)
Griddeki bir satırı tıklayınca formu o satırla doldurur.
Termin: GRID → EDİTÖR senkronu eklendi.
=========================================================== */
const editRow = async (row, localIndex) => {
const globalIndex = summaryRows.value.findIndex(r =>
r.model === row.model &&
r.renk === row.renk &&
r.renk2 === row.renk2 &&
r.aciklama === row.aciklama
)
if (globalIndex === -1) {
console.warn('⚠️ Editlenecek satır bulunamadı.')
return
}
editingIndex.value = globalIndex
orderStore.selected = { ...row }
// 🔒 Edit modda seçili renkleri kilitle (seçenek olarak satırdaki değerleri koy)
renkOptions.value = row.renk ? [{ label: row.renk, value: row.renk }] : []
renkOptions2.value = row.renk2 ? [{ label: row.renk2, value: row.renk2 }] : []
// 🔄 Formu satır bilgileriyle doldur
Object.assign(form, {
model: row.model,
renk: row.renk,
renk2: row.renk2,
urunAnaGrubu: row.urunAnaGrubu,
urunAltGrubu: row.urunAltGrubu,
aciklama: row.aciklama,
fiyat: Number(row.fiyat || 0),
pb: row.pb,
adet: Number(row.adet || 0),
tutar: Number(row.tutar || 0)
})
// 🗓️ GRID → EDİTÖR termin senkronu
form.terminTarihi = row.terminTarihi || form.terminTarihi || form.tahminiTerminTarihi || ''
// 🔹 Beden şemasını geri yükle
const key = row.grpKey || activeGroupKeyForRow(row)
const grp = schemaByKey.value[key]
form.bedenLabels = grp?.values || []
const savedMap = row.bedenMap?.[key] || {}
form.bedenler = form.bedenLabels.map(lbl => Number(savedMap[lbl] || 0))
// 🔢 Toplamları yeniden hesapla
updateTotals(form)
form.tutar = (form.adet || 0) * (form.fiyat || 0)
// 🧩 Stokları forceRefresh ile yükle
if (form.model && form.renk) {
try {
await nextTick()
await loadProductSizes(true) // 💾 forceRefresh
console.log('📦 Edit mode stoklar yenilendi:', stockMap.value)
} catch (err) {
console.warn('⚠️ Edit modda stok yenileme başarısız:', err)
$q.notify({
type: 'warning',
message: 'Stok verisi yenilenemedi ⚠️',
position: 'top-right'
})
}
} else {
console.log('⚪ Model veya renk eksik, stok çağrısı atlandı.')
}
console.log(`✏️ Edit mode aktif → index: ${globalIndex}, model: ${row.model}, termin: ${form.terminTarihi}`)
}
// Bu fonksiyonu parent bileşenlerden çağırabilmek için export et
defineExpose({ editRow })
/* ===========================================================
🔹 Kaydet / Güncelle (saveOrUpdate)
- Aynı kombinasyon varsa güncelleme moduna geçer,
yoksa yeni satır ekler.
- Kaydetmeden hemen önce stokları forceRefresh ile tazeler.
- Stok ve Min Fiyat validasyonları içerir.
=========================================================== */
async function saveOrUpdate() {
// 0⃣ Kombinasyon kontrolü (99999 toleranslı)
const existingIndex = summaryRows.value.findIndex(r =>
r.model === form.model &&
((r.renk || '') === (form.renk || '') || (r.renk || '') === '99999' || (form.renk || '') === '99999') &&
((r.renk2 || '') === (form.renk2 || '') || (r.renk2 || '') === '99999' || (form.renk2 || '') === '99999')
)
if (existingIndex !== -1 && editingIndex.value === -1) {
console.log(`⚙️ Kombinasyon zaten var → index ${existingIndex} güncellenecek`)
editingIndex.value = existingIndex
}
// 1⃣ Model zorunlu alan
if (!form.model) {
$q.notify({ type: 'warning', message: '⚠️ Model seçimi gerekli!' })
return
}
// 2⃣ Para birimi doğrulaması (cari yoksa USD)
if (!form.pb || form.pb === '') {
form.pb = aktifPB.value || 'USD'
console.log('💱 Para birimi otomatik setlendi:', form.pb)
}
// 3⃣ Kaydetmeden önce stok verisini tazele (forceRefresh)
try {
await loadProductSizes(true)
console.log('📦 Stok bilgisi kaydetmeden önce yenilendi (forceRefresh).')
} catch (err) {
console.warn('⚠️ Stok bilgisi kaydetmeden önce yenilenemedi:', err)
}
// 4⃣ Stok kontrolü (beden bazında)
let stokOK = true
const overLimit = []
for (let i = 0; i < form.bedenLabels.length; i++) {
const lbl = form.bedenLabels[i]
const stok = Number(stockMap.value?.[lbl] || 0)
const girilen = Number(form.bedenler[i] || 0)
if (stok > 0 && girilen > stok) {
overLimit.push({ beden: lbl, stok, girilen })
}
}
if (overLimit.length > 0) {
const msgLines = overLimit
.map(x => `🟡 ${x.beden}: ${x.girilen} (Stok: ${x.stok})`)
.join('<br>')
stokOK = await new Promise(resolve => {
$q.dialog({
title: 'Stok Uyarısı',
message: `Bazı bedenlerde stoktan fazla giriş yaptınız:<br><br>${msgLines}`,
html: true,
ok: { label: 'Devam Et', color: 'primary' },
cancel: { label: 'İptal', color: 'negative' }
})
.onOk(() => resolve(true))
.onCancel(() => resolve(false))
})
}
if (!stokOK) return
// 5⃣ Min fiyat kontrolü (stok OK sonrası)
let fiyatOK = true
try {
const priceData = await orderStore.fetchMinPrice(form.model, form.pb)
const minFiyat = Number(priceData?.price || 0)
if (priceData && form.fiyat < minFiyat) {
fiyatOK = await new Promise(resolve => {
$q.dialog({
title: 'Fiyat Uyarısı',
message: `
<b>Min. Fiyat:</b> ${minFiyat.toLocaleString('tr-TR')} ${form.pb}<br>
<b>Girdiğin:</b> ${form.fiyat.toLocaleString('tr-TR')} ${form.pb}`,
html: true,
ok: { label: 'Devam Et', color: 'primary' },
cancel: { label: 'İptal', color: 'negative' }
})
.onOk(() => resolve(true))
.onCancel(() => resolve(false))
})
}
} catch (err) {
console.warn('⚠️ Fiyat kontrolü yapılamadı:', err)
}
if (!fiyatOK) return
// 6⃣ Satır objesi oluştur (form → grid row)
const row = toSummaryRowFromForm()
if (!row || !row.model) return
// 7⃣ Tutarlılık kontrolleri
row.adet = Number(row.adet || form.adet || 0)
row.fiyat = Number(row.fiyat || form.fiyat || 0)
row.tutar = Number(row.tutar || (row.adet * row.fiyat) || 0)
row.pb = form.pb || aktifPB.value || 'USD'
// 8⃣ Güncelle veya yeni satır ekle
if (editingIndex.value !== -1) {
const currentRow = summaryRows.value[editingIndex.value]
if (currentRow?.id) {
// 🔹 ID varsa doğrudan güncelle
row.id = currentRow.id
summaryRows.value.splice(editingIndex.value, 1, row)
orderStore.updateRow(currentRow.id, { ...row })
} else {
// 🔹 ID yoksa index bazlı güncelle
summaryRows.value.splice(editingIndex.value, 1, row)
if (orderStore.orders[editingIndex.value])
orderStore.orders[editingIndex.value] = { ...row }
}
orderStore.saveToStorage?.()
$q.notify({
type: 'positive',
message: 'Mevcut kombinasyon güncellendi ✏️',
position: 'top-right'
})
} else {
// 🆕 Yeni satır ekleme akışı
orderStore.addRow({ ...row })
summaryRows.value = [...orderStore.orders]
orderStore.saveToStorage?.()
$q.notify({
type: 'positive',
message: 'Yeni kombinasyon eklendi ✅',
position: 'top-right'
})
}
// 9⃣ Düzenleme durumunu sıfırla
editingIndex.value = -1
orderStore.selected = null
resetForm()
}
/* ===========================================================
🔹 Form Sıfırlama (Tam Temizleme)
Tüm form alanlarını, renk seçeneklerini, stok maplerini
ve select bileşenlerini sıfırlar.
=========================================================== */
/* ===========================================================
🔹 resetForm (Güvenli Sıfırlama)
Yeni sipariş başlatılırken formu temizler.
Edit modunda ise aktif satırı korur.
=========================================================== */
/* ===========================================================
🔹 Reset Form (Yeni Sipariş Başlatma)
=========================================================== */
function resetForm() {
form.olusturmaTarihi = dayjs().format('YYYY-MM-DD')
form.tahminiTerminTarihi = dayjs().add(30, 'day').format('YYYY-MM-DD')
form.pb = 'USD'
aktifPB.value = 'USD'
selectedCari.value = ''
siparisGenelAciklama.value = ''
summaryRows.value = []
orderStore.newOrderTemplate()
isEditMode.value = false
console.log('🧹 Form sıfırlandı.')
}
/* ===========================================================
🔹 Satır Silme (removeSelected)
Kullanıcı bir satırı düzenleme modundayken silmek isterse
güvenlik diyaloğu açılır. Onay verilirse hem gridden hem
storedan kaldırılır.
=========================================================== */
function removeSelected() {
if (editingIndex.value === -1) {
$q.notify({
type: 'info',
message: 'Silinecek satır seçili değil ⚠️'
})
return
}
const idx = editingIndex.value
const selectedRow = summaryRows.value[idx]
if (!selectedRow) {
$q.notify({
type: 'warning',
message: 'Geçersiz satır, silinemedi ⚠️'
})
return
}
// 🔹 Onay penceresi
$q.dialog({
title: 'Satırı Sil',
message: `
Bu satırı silmek istediğinizden emin misiniz?<br><br>
<b>Model:</b> ${selectedRow.model || '-'}<br>
<b>Renk:</b> ${selectedRow.renk || '-'}<br>
<b>PB:</b> ${selectedRow.pb || '-'}<br>
<b>Tutar:</b> ${Number(selectedRow.tutar || 0).toLocaleString(
'tr-TR',
{ minimumFractionDigits: 2 }
)}`,
html: true,
ok: { label: 'Evet, Sil', color: 'negative' },
cancel: { label: 'Vazgeç', flat: true }
}).onOk(() => {
// 🔹 Grid'den kaldır
summaryRows.value.splice(idx, 1)
// 🔹 Store'dan kaldır (id varsa idye göre, yoksa indexe göre)
if (selectedRow.id != null) {
orderStore.removeRow(selectedRow.id)
} else {
orderStore.orders.splice(idx, 1)
orderStore.saveToStorage?.()
}
// 🔹 Formu sıfırla
editingIndex.value = -1
orderStore.selected = null
resetForm()
// 🔹 Kullanıcıya bilgi
$q.notify({
type: 'positive',
message: 'Satır silindi ✅',
position: 'top-right'
})
})
}
/* ===========================================================
🔹 Grid Şeması (Beden Haritası)
Uygulamada hangi ürün grubunda hangi beden seti kullanılacak
burada tanımlanır. Key → kod, title → görünür ad, values → bedenler.
=========================================================== */
const schema = ref([
{
key: 'ayk',
title: 'AYAKKABI',
values: ['39','40','41','42','43','44','45']
},
{
key: 'yas',
title: 'YAŞ',
values: ['2','4','6','8','10','12','14']
},
{
key: 'pan',
title: 'PANTOLON',
values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68']
},
{
key: 'gom',
title: 'GÖMLEK',
values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL']
},
{
key: 'tak',
title: 'TAKIM ELBİSE',
values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74']
},
{
key: 'aksbir',
title: 'AKSESUAR',
values: [' ', '44', 'STD', '110CM', '115CM', '120CM', '125CM', '130CM', '135CM']
}
])
/* ===========================================================
🔹 Aktif Grup Anahtarı Tespiti
Ürün grubunun adından (ör. “TAKIM”, “PANTOLON”) hareketle
uygun schema.key değerini döndürür. Default: 'tak'
=========================================================== */
function activeGroupKeyForRow(row) {
const g = row.urunAnaGrubu?.toUpperCase() || ''
if (g.includes('TAKIM')) return 'tak'
if (g.includes('PANTOLON')) return 'pan'
if (g.includes('GÖMLEK')) return 'gom'
if (g.includes('AYAKKABI')) return 'ayk'
if (g.includes('YAŞ')) return 'yas'
return 'tak' // default fallback
}
/* ===========================================================
🔹 schemaByKey (Computed)
Reaktif olarak schema listesinden key → object map üretir.
Böylece örn. schemaByKey.value['pan'] diyerek doğrudan erişim sağlanır.
=========================================================== */
const schemaByKey = computed(() => {
const m = {}
schema.value.forEach(grp => (m[grp.key] = grp))
return m
})
/* ===========================================================
🔹 highlightPantolon (Computed)
Eğer formun aktif ürünü PANTOLON ise,
grid veya formda özel vurgu yapılabilir.
=========================================================== */
const highlightPantolon = computed(() =>
form.urunAnaGrubu?.toUpperCase()?.includes('PANTOLON')
)
/* ===========================================================
🔹 Sayfa Kapanırken Verileri Kaydet
onBeforeUnmount: sayfa kapatıldığında sipariş verileri
LocalStoragea otomatik yazılır.
=========================================================== */
onBeforeUnmount(() => {
orderStore.saveToStorage()
console.log('💾 Sayfa kapatılırken veriler kaydedildi.')
})
/* ===========================================================
🔹 Tümünü Kaydet (Toplu Gönder) — Store Entegrasyonlu Versiyon
=========================================================== */
async function submitAll() {
try {
ensureAuthOrRedirect()
const headerPayload = {
OrderDate: form.olusturmaTarihi,
AverageDueDate: form.tahminiTerminTarihi,
CurrAccCode: selectedCari.value,
DocCurrencyCode: form.pb,
Description: siparisGenelAciklama.value
}
const linesPayload = summaryRows.value
let res
if (mode.value === 'new') {
res = await api.post('/api/orders', { header: headerPayload, lines: linesPayload })
$q.notify({ type: 'positive', message: 'Yeni sipariş oluşturuldu ✅' })
mode.value = 'edit'
headerId.value = res?.data?.OrderHeaderID || headerId.value
form.OrderHeaderID = headerId.value
} else {
res = await api.put(`/api/orders/${form.OrderHeaderID}`, { header: headerPayload, lines: linesPayload })
$q.notify({ type: 'positive', message: 'Sipariş güncellendi ✅' })
}
saveDraft()
} catch (err) {
console.error('❌ submitAll hata:', err)
$q.notify({ type: 'negative', message: 'Kaydedilemedi ❌' })
}
}
// ===========================================================
// 🔹 Sipariş Yükleme Fonksiyonu (Güvenli Sürüm)
// ===========================================================
async function loadOrderById(orderId) {
console.log('📦 loadOrderById çağrıldı →', orderId)
const hasLoading = !!$q.loading
const hasNotify = !!$q.notify
try {
// 🔹 Loading başlat
if (hasLoading) {
$q.loading.show({
message: 'Sipariş yükleniyor...',
spinnerSize: 50,
spinnerColor: 'gold',
backgroundColor: 'rgba(255,255,255,0.6)',
})
} else {
console.warn('⚠️ $q.loading tanımlı değil (plugin aktif mi?).')
}
// 🔹 API çağrısı
const { data } = await api.get(`/order/get/${orderId}`)
console.log('📡 API yanıtı:', data)
if (!data || Object.keys(data).length === 0) {
if (hasNotify) {
$q.notify({
type: 'negative',
message: 'Sipariş verisi bulunamadı.',
position: 'top',
})
}
console.error('❌ Boş yanıt geldi.')
return
}
// 🔹 Header → forma aktar
if (data.header) {
Object.assign(form, data.header)
console.log('✅ Header yüklendi:', form.OrderNumber || '(numara yok)')
}
// 🔹 Detay satırlarını frontend modeline çevir
if (Array.isArray(data.lines)) {
summaryRows.value = data.lines.map(l => {
const grpKey = detectBedenGroup([l.ItemDim1Code], l.ProductGroup, '')
return {
model: l.ItemCode || '',
renk: l.ColorCode || '',
renk2: l.ItemDim2Code || '',
urunAnaGrubu: l.ProductGroup || l.UrunAnaGrubu || '',
urunAltGrubu: l.ProductSubGroup || l.UrunAltGrubu || '',
aciklama: l.LineDescription || l.Aciklama || '',
fiyat: Number(l.Price || 0),
pb: l.PriceCurrencyCode || l.DocCurrencyCode || 'USD',
adet: Number(l.Qty1 || 0),
tutar: Number((l.Qty1 || 0) * (l.Price || 0)),
grpKey,
bedenMap: {
[grpKey]: { [l.ItemDim1Code]: Number(l.Qty1 || 0) },
},
terminTarihi: l.DeliveryDate
? dayjs(l.DeliveryDate).format('YYYY-MM-DD')
: form.tahminiTerminTarihi,
}
})
console.log('📋 Grid satırları hazır:', summaryRows.value.length)
} else {
summaryRows.value = []
}
} catch (err) {
console.error('❌ loadOrderById hata:', err)
if (hasNotify) {
$q.notify({
type: 'negative',
message: 'Sipariş yüklenirken hata oluştu.',
position: 'top',
})
}
} finally {
// 🔹 Loading gizle
if (hasLoading) {
try {
$q.loading.hide()
} catch (hideErr) {
console.warn('⚠️ Loading kapatılamadı:', hideErr)
}
}
}
}
/* ===========================================================
🔹 Stok Bilgisi Görüntüleme Fonksiyonları
Her beden veya satır için stok bilgisini UIda göstermek için kullanılır.
=========================================================== */
function getStockFor(lbl) {
if (!lbl || !stockMap.value) return 0
const val = stockMap.value[lbl]
const num = Number(val)
return isNaN(num) ? 0 : num // 🎯 string "433" bile olsa 433 olarak döner
}
function getStockForRow(row, beden) {
if (!row || !beden) return 0
// 🔹 Eğer formda seçili model ve renk bu satıra aitse, bedenStock'tan getir
if (row.model === form.model && row.renk === form.renk) {
const f = bedenStock.value.find(x => x.beden === beden)
if (f) return Number(f.stok) || 0 // 🎯 string → number dönüşümü eklendi
}
// 🔹 Satırın kendi stokMapinde varsa oradan getir
if (row.stokMap && row.stokMap[beden] != null) {
const num = Number(row.stokMap[beden])
return isNaN(num) ? 0 : num
}
return 0
}
/* ===========================================================
🔹 Watchers — Model & Renk Değişimlerinde Otomatik Temizlik
Kullanıcı model veya renk değiştirdiğinde önceki form verileri
sıfırlanır, gereksiz stok veya beden bilgileri temizlenir.
=========================================================== */
watch(
() => form.model,
async (newVal, oldVal) => {
// MODEL TEMİZLENDİYSE: her şey sıfırlanır
if (!newVal) {
console.log('🧹 Model kaldırıldı, beden seti sıfırlanıyor...')
resetForm()
selectedSeriSet.value = null
seriMultiplier.value = 1
return
}
// MODEL DEĞİŞTİYSE: form yeniden başlatılır ve yeni model yüklenir
if (oldVal && newVal !== oldVal) {
console.log('🌀 Model değişti, form temizleniyor...')
resetForm()
await nextTick()
form.model = newVal
onModelChange(newVal)
}
}
)
watch(
() => form.renk,
(newVal, oldVal) => {
// Renk değiştiğinde ikinci renk ve ilgili stok bilgileri temizlenir
if (oldVal && newVal && newVal !== oldVal) {
console.log('🎨 Renk değişti, alt veriler sıfırlanıyor...')
form.renk2 = ''
renkOptions2.value = []
if (renk2Select.value) renk2Select.value.reset()
}
}
)
/* ===========================================================
🔹 Beden + Stok Etiketli Görünüm (Frontend Haritalama)
Grid veya tablo üzerinde bedenlerin yanında stok göstermek için
kullanılacak computed alan.
=========================================================== */
const bedenWithStock = computed(() => {
if (!form.bedenLabels.length) return []
return form.bedenLabels.map(lbl => ({
label: lbl,
stok: stockMap.value[lbl] ?? null,
value: form.bedenler[form.bedenLabels.indexOf(lbl)]
}))
})
/* ===========================================================
🔹 PB (Para Birimi) değişiminde min fiyat yenileme
Eğer kullanıcı para birimini değiştirirse, backendden yeniden
min fiyat sorgusu yapılır (örneğin USD → EUR dönüşüm).
=========================================================== */
watch(
() => form.pb,
async (newVal, oldVal) => {
if (newVal && newVal !== oldVal) {
console.log('💱 PB değişti:', newVal)
await fetchMinPrice()
}
}
)
/* ===========================================================
🔹 Aktif Beden Alanı ve Renkli Stok Etiketleri
Stok miktarına göre hücre arka planı veya yazı rengini belirler.
Kullanıcı stok durumunu görsel olarak hemen fark eder.
=========================================================== */
const activeBeden = ref(null)
function stockColorClass(qty) {
const n = Number(qty)
if (isNaN(n)) return ''
if (n === 0) return 'stok-red' // 🔴 Stok yok
if (n > 0 && n <= 2) return 'stok-yellow' // 🟡 Kritik stok (12)
return 'stok-green' // 🟢 Yeterli stok (3+)
}
/* ===========================================================
🔹 Yardımcı Fonksiyonlar — Auth ve Token Kontrolü
Kullanıcının tokenı yoksa login sayfasına yönlendirir.
Tüm backend çağrıları öncesi güvenlik katmanı sağlar.
=========================================================== */
function getToken() {
return localStorage.getItem('token')
}
function ensureAuthOrRedirect() {
const token = getToken()
if (!token) {
if (typeof window !== 'undefined') window.location.href = '/login'
throw new Error('🚫 Yetkilendirme gerekli.')
}
}
/* ===========================================================
🔹 Son Bilgilendirme ve Debug Log
Geliştirme aşamasında konsolda bileşenin başarıyla
yüklendiği bilgisini verir.
=========================================================== */
console.log('🧩 OrderEntry (v22-final) bileşeni başarıyla yüklendi.')
</script>

1128
ui/src/stores/deneme2 Normal file
View File

@@ -0,0 +1,1128 @@
/* ===========================================================
GLOBAL CUSTOM CSS
=========================================================== */
.with-bg {
position: relative;
min-height: 100%;
}
.with-bg::before {
content: "";
position: absolute;
inset: 0;
background: url('/images/Baggi-tekstilas-logolu.jpg') no-repeat center top;
background-size: 400px auto;
opacity: 0.15;
pointer-events: none;
z-index: 0;
}
.with-bg > * {
position: relative;
z-index: 1;
}
.q-page {
margin-top: 5px;
}
@media (max-width: 768px) {
.with-bg::before {
background-size: 260px auto;
}
}
/* ===== ÜST BLOKLAR (SABİT) ===== */
.filter-sticky {
position: sticky;
top: 56px; /* q-header yüksekliği */
z-index: 300;
background: #fff;
}
.filter-collapsible {
background: #fff;
}
/* ===== TABLO SCROLL ===== */
.table-scroll {
margin-top: 0; /* 🔹 Boşluğu kaldır */
height: calc(100vh - 56px); /* 🔹 Header yüksekliği kadar kısalt */
overflow-y: auto;
overflow-x: auto;
position: relative;
}
.sticky-table .q-table__middle {
overflow: visible !important;
max-height: none !important;
}
.sticky-table .q-table__top {
position: sticky;
top: 0;
z-index: 220;
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
.sticky-table thead th {
position: sticky;
top: 40px;
z-index: 210;
background: #fff;
}
/* 🔹 Toggle bar */
.sticky-bar {
position: sticky;
top: 0; /* tablo scroll başladığında en üstte kalsın */
z-index: 230;
background: #fff;
padding: 4px 8px;
border-bottom: 1px solid #ddd;
}
/* ===== KOLON DARALTMA + WRAP ===== */
.sticky-table thead th {
resize: horizontal;
overflow: auto;
min-width: 80px;
max-width: 400px;
}
.sticky-table td {
min-width: 80px;
max-width: 400px;
white-space: normal !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
line-height: 1.2rem;
padding: 4px 8px !important;
font-weight: 600;
font-size: 0.95rem;
}
/* ===== GÖRSEL ===== */
.baggi-ppct {
display: block;
margin: 30px auto 0;
max-width: 400px;
opacity: 0.4;
}
.col-desc {
white-space: normal !important;
word-break: break-word !important;
overflow-wrap: break-word;
font-size: 0.75rem !important;
line-height: 1.1rem;
width: 220px !important;
max-width: 220px !important;
min-width: 180px !important;
}
/* ===== TABLO GÖRÜNÜM ===== */
.custom-table { font-size: 0.8rem; }
.custom-table th { background: #fff; font-weight: 800; color: #222; }
.custom-table td { font-weight: 600; color: #333; }
.custom-subtable { font-size: 0.72rem; background: #fafafa; }
.custom-subtable th { background: #f9f9f9; font-weight: 500; color: #555; }
.custom-subtable td { font-weight: 400; color: #666; }
/* dar sütunlar için */
.col-narrow {
font-size: 0.72rem;
padding: 2px 6px !important;
max-width: 90px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ===== GRUP SATIRI ===== */
.group-row {
background: #f1f1f1 !important;
font-weight: 700 !important;
color: #222;
border-top: 2px solid #ccc;
border-bottom: 2px solid #ccc;
}
/* ===== BALANCE CARD ===== */
.balance-card {
width: 100%;
min-height: 120px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.q-table td[data-col="belge_no"],
.q-table td[data-col="Belge_No"],
.q-table td[data-col="BELGE_NO"] {
color: var(--q-primary) !important;
font-weight: 600 !important;
}
/* ===========================================================
PERMISSIONS PAGE (FINAL)
=========================================================== */
/* Toolbar */
.permissions-toolbar {
position: sticky;
top: 42px; /* q-header yüksekliği */
z-index: 300;
background: #fff;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
border-bottom: 1px solid #ddd;
}
/* Table scroll alanı */
.permissions-table-scroll {
height: calc(100vh - 112px); /* header (56) + toolbar (56) */
overflow-y: auto;
overflow-x: auto;
position: relative;
}
/* Tablo gövdesi */
.permissions-table .q-table__middle {
overflow: auto !important;
max-height: none !important;
padding-top: 0px; /* 🔑 Başlık yüksekliği kadar boşluk bırak */
}
/* Sticky başlıklar toolbarın altında */
.permissions-table thead th {
position: sticky;
top:10px; /* toolbar altında hizalanır */
z-index: 210;
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
/* Hücreler */
.permissions-table td {
min-width: 80px;
max-width: 400px;
white-space: normal !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
line-height: 1.2rem;
padding: 4px 8px !important;
font-weight: 600;
font-size: 0.95rem;
background: #fff;
}
/* İlk kolon (role) sabit */
.permissions-table .permissions-sticky-col {
position: sticky;
left: 0;
z-index: 205;
background: #fff;
box-shadow: 2px 0 4px rgba(0,0,0,0.04);
}
/* ===========================================================
1⃣ ROOT & GLOBAL RESET
=========================================================== */
:root {
--header-h: 0px;
--filter-h: 72px;
--save-h: 60px;
--grid-header-h: 172px;
--sub-header-h: 34px;
--drawer-w: 240px;
/* Grid kolon genişlikleri */
--col-model: 90px;
--col-renk: 80px;
--col-ana: 100px;
--col-alt: 100px;
--col-aciklama: 140px;
--col-adet: 70px;
--col-fiyat: 70px;
--col-pb: 70px;
--col-tutar: 70px;
--col-termin: 142px; /* 🔹 termin tarihi kolon genişliği */
/* Beden blok ölçüleri */
--grp-title-w: 90px;
--grp-title-gap: 4px;
--beden-w: 44px;
--beden-h: 28px;
--beden-count: 16;
/* Tema renkleri */
--baggi-gold: #c9a227;
--baggi-gold-pale: #fff9e6;
--baggi-gold-light: #fff7d2;
--baggi-cream: #fffef9;
--baggi-gray-border: #bbb;
}
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
background: #fff;
color: #222;
font-family: Inter, "Segoe UI", Arial, sans-serif;
font-size: 14px;
line-height: 1.4;
}
#q-app, .q-page-container { margin: 0; padding: 0; }
.q-layout__page { top: 0 !important; }
/* ===========================================================
2⃣ PAGE STRUCTURE & SCROLL
=========================================================== */
.order-page {
display: flex;
flex-direction: column;
height: calc(100vh - var(--header-h));
overflow-y: auto;
overflow-x: hidden;
background: #fff;
}
.body--drawer-left-open .q-page-container {
margin-left: var(--drawer-w);
width: calc(100% - var(--drawer-w));
}
.body--drawer-left-closed .q-page-container {
margin-left: 0; width: 100%;
}
/* 🔸 Yatay scroll sadece grid alanında */
.order-scroll-x {
flex: 1;
overflow-x: auto;
overflow-y: visible;
background: #fff;
}
/* 🔸 Scrollbar stili */
.order-page::-webkit-scrollbar,
.order-scroll-x::-webkit-scrollbar {
height: 8px; width: 8px;
}
.order-page::-webkit-scrollbar-thumb,
.order-scroll-x::-webkit-scrollbar-thumb {
background: #c0a75e;
border-radius: 4px;
}
.order-page::-webkit-scrollbar-track,
.order-scroll-x::-webkit-scrollbar-track {
background: #f9f5e6;
}
/* ===========================================================
3⃣ STICKY STACK (HEADER + TOOLBARS)
=========================================================== */
.q-header {
position: sticky;
top: 0;
z-index: 1000;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
.sticky-stack {
position: sticky;
top: var(--header-h);
margin-top: 0 !important;
z-index: 950;
display: flex;
flex-direction: column;
background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
/* 🔹 Filtre bar */
.filter-bar {
background: #fafafa;
border-bottom: 1px solid #ddd;
padding: 12px 24px;
margin-top:0 !important;
}
/* 🔹 Save toolbar */
.save-toolbar {
background: var(--baggi-gold-pale);
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 10px 16px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 940;
}
.save-toolbar .label { font-weight: 700; color: #6a5314; }
.save-toolbar .value { font-weight: 700; color: #000; }
.save-toolbar .q-btn {
font-weight: 600;
border-radius: 6px;
text-transform: none;
}
/* ===========================================================
4⃣ GRID HEADER (ANA BAŞLIK BLOKU)
=========================================================== */
.order-grid-header {
position: sticky;
top: calc(var(--header-h) + var(--filter-h) + var(--save-h));
z-index: 700;
display: grid;
grid-auto-flow: column;
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w)*var(--beden-count)))
var(--col-adet)
var(--col-fiyat)
var(--col-pb)
var(--col-tutar)
var(--col-termin);
background: var(--baggi-cream);
border-bottom: 2px solid var(--baggi-gray-border);
box-shadow: 0 2px 3px rgba(0,0,0,0.05);
}
/* Sabit kolonlar */
.order-grid-header .col-fixed {
display: flex;
justify-content: center;
align-items: center;
writing-mode: vertical-lr;
transform: rotate(180deg);
background: var(--baggi-gold-light);
border: 1px solid #aaa;
font-weight: 700;
font-size: 12.5px;
height: var(--grid-header-h);
}
.order-grid-header .aciklama-col {
background: #fff9c4;
border-right: 2px solid #a6a6a6;
}
/* ===========================================================
5⃣ BEDEN BLOKLARI & SAĞ TOPLAM
=========================================================== */
.order-grid-header .beden-block {
display: flex;
flex-direction: column;
height: var(--grid-header-h);
background: #fff;
border: 1px solid #ccc;
}
.order-grid-header .grp-row {
display: flex;
align-items: center;
height: var(--beden-h);
}
.order-grid-header .grp-title {
width: var(--grp-title-w);
text-align: right;
font-weight: 700;
font-size: 12px;
padding-right: 4px;
}
.order-grid-header .grp-body {
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
}
.order-grid-header .grp-cell.hdr {
width: var(--beden-w);
height: var(--beden-h);
border: 1px solid #bbb;
font-size: 11.5px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.order-grid-header .total-row {
display: flex;
align-items: stretch;
justify-content: space-between;
background: #fff59d;
}
.order-grid-header .total-cell {
width: var(--col-adet);
display: flex;
justify-content: center;
align-items: center;
writing-mode: vertical-lr;
transform: rotate(180deg);
border-right: 1px solid #bbb;
background: var(--baggi-gold-pale);
font-weight: 700;
font-size: 12px;
}
/* ===========================================================
6⃣ SUB-HEADER (ÜRÜN GRUBU BAR) — TAM HİZALANMIŞ
=========================================================== */
.order-sub-header {
padding-right: 0 !important; /* 🔹 Ekstra sağ boşluğu kaldır */
margin-right: 0 !important;
}
.order-sub-header {
position: sticky;
top: calc(
var(--header-h)
+ var(--filter-h)
+ var(--save-h)
+ var(--grid-header-h)
);
z-index: 650;
/* 🔹 Header ile birebir grid düzeni */
display: grid;
grid-auto-flow: column;
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
var(--col-adet)
var(--col-fiyat)
var(--col-pb)
var(--col-tutar)
var(--col-termin);
align-items: center;
justify-items: stretch;
height: var(--sub-header-h);
min-height: var(--sub-header-h);
/* 🔹 Görsel */
background: linear-gradient(90deg, #fffbe9 0%, #fff4c4 50%, #fff1b0 100%);
border-top: 1px solid #d6c06a;
border-bottom: 1px solid #d6c06a;
/* 🔹 Hatalı hizalamaları engelle */
box-sizing: border-box;
overflow: hidden;
padding: 0 !important;
margin: 0 !important;
padding-right: 0 !important; /* ✅ sağ taşmayı önler */
}
/* 🔹 Genişlik eşitleme */
:root {
--col-termin: 142px; /* ✅ q-input genişliğiyle birebir */
}
/* 🔹 Sub-header hover efekti */
.order-sub-header:hover {
background: linear-gradient(90deg, #fff9cf 0%, #fff3b0 70%, #ffe88f 100%);
}
/* 🔹 Sol taraf (MODELAÇIKLAMA alanı) */
.order-sub-header .sub-left {
grid-column: 1 / span 5;
font-weight: 800;
padding-left: 6px;
color: #2b1f05;
display: flex;
align-items: center;
}
/* 🔹 Orta beden bloğu (headerla aynı yapı) */
.order-sub-header .sub-center {
grid-column: 6 / 7;
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
justify-content: start;
align-items: center;
width: calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)));
padding-left: var(--grp-title-w);
margin-left: var(--grp-title-gap);
height: 100%;
box-sizing: border-box;
}
.order-sub-header .beden-cell {
width: var(--beden-w);
height: 100%;
border: 1px solid #d8c16b;
border-right: none;
background: #fffdf3;
font-size: 12px;
font-weight: 600;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.order-sub-header .beden-cell:last-child {
border-right: 1px solid #d8c16b;
}
/* 🔹 Sağ taraf (adetfiyatpbtutartermin toplamları) */
.order-sub-header .sub-right {
grid-column: 7 / -1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
text-align: right;
padding-right: 6px;
font-weight: 900;
color: #3b2f09;
line-height: 1.3;
text-transform: uppercase;
font-size: 13.5px;
}
.order-sub-header:hover {
background: linear-gradient(90deg,#fff9cf 0%,#fff3b0 70%,#ffe88f 100%);
}
:root {
--sub-header-h: 60px;
}
/* SUB-HEADER sağ yazıyı 3 beden kolonu sola kaydır */
.order-sub-header .sub-right {
transform: translateX(calc(-1 * var(--beden-w) * 4));
}
/* Taşmayı engelle (ihtiyaten) */
.order-sub-header {
overflow: hidden;
}
/* ===========================================================
7⃣ GRID BODY & SATIRLAR — TAM HİZALANMIŞ
=========================================================== */
.order-grid-body {
position: relative;
background: #fff;
margin-top: 0 !important;
padding-top: 0;
z-index: 100;
}
.summary-row {
display: grid;
grid-template-columns:
var(--col-model)
var(--col-renk)
var(--col-ana)
var(--col-alt)
var(--col-aciklama)
calc(var(--grp-title-w) + var(--grp-title-gap) + (var(--beden-w) * var(--beden-count)))
var(--col-adet)
var(--col-fiyat)
var(--col-pb)
var(--col-tutar)
var(--col-termin);}
.summary-row:hover {
background: #fffce0;
}
.summary-row.is-editing {
background: #fff3cd;
outline: 2px solid #caa83f;
}
.summary-row .cell {
display: flex;
align-items: center;
justify-content: center;
height: var(--beden-h);
padding: 4px 6px;
font-size: 13px;
color: #222;
box-sizing: border-box;
}
.summary-row:nth-child(odd) { background: #fffef9; }
/* 🔹 Beden blok hizalaması */
.summary-row .grp-area {
display: flex;
flex-direction: column;
justify-content: center;
transform: translateX(calc(var(--grp-title-w) - var(--beden-w)));
}
.summary-row .grp-row {
display: grid;
grid-auto-flow: column;
grid-auto-columns: var(--beden-w);
}
.summary-row .grp-row .cell.beden {
width: var(--beden-w);
height: var(--beden-h);
border: 1px solid #ddd;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.cell.beden.ghost {
opacity: 0;
pointer-events: none;
border: 1px solid transparent !important;
}
/* 🔹 Sağ kolonlar */
.summary-row .cell.adet,
.summary-row .cell.fiyat,
.summary-row .cell.pb,
.summary-row .cell.tutar,
.summary-row .cell.termin {
font-weight: 600;
color: #000;
border-left: none !important;
height: 100%;
}
.summary-row .cell.tutar {
text-align: right;
justify-content: flex-end;
padding-right: 8px;
border-right: none !important;
}
.summary-row .cell.termin {
background: #fffef9;
justify-content: center;
align-items: center;
min-width: var(--col-termin);
}
.summary-row .cell.termin .q-input {
width: 100%;
max-width: 142px !important;
box-sizing: border-box;
}
.summary-row .cell.termin input {
text-align: center;
font-size: 13px;
}
/* ===========================================================
9⃣ ORDER EDITOR (ALT FORM)
=========================================================== */
.editor {
position: relative;
z-index: 50;
background: #fffef9;
border-top: 1px solid #ddd;
margin-top: 24px;
padding: 16px;
}
.editor::before {
content: "";
display: block;
height: 4px;
background: linear-gradient(to right,#c9a227,#e5d28b,#fff7d2);
margin-bottom: 12px;
border-radius: 2px;
}
.editor .q-btn:hover { background: #d2b04d; }
.editor .q-input,
.editor .q-select { margin-bottom: 8px; font-size: 14px; }
.cell.termin .termin-label {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 13px;
color: #222;
background: #fffef9;
border-left: 1px solid #ccc;
box-sizing: border-box;
}
/* ===========================================================
🔟 RESPONSIVE + MİNÖR DÜZEN
=========================================================== */
@media (max-width: 1024px) {
:root { --beden-w: 40px; --col-aciklama: 120px; }
.order-grid-header .col-fixed { font-size: 11px; }
.order-sub-header { font-size: 12.5px; }
}
@media (max-width: 768px) {
:root {
--beden-w: 36px;
--col-model: 70px;
--col-renk: 60px;
--col-aciklama: 100px;
}
.order-page { font-size: 13px; }
.order-grid-header .total-cell { font-size: 10.5px; }
}
.summary-row .cell {
display: flex;
justify-content: center;
align-items: center;
padding: 4px 6px;
height: auto;
text-align: center;
white-space: normal;
word-wrap: break-word;
}
.summary-row .grp-area,
.summary-row .grp-row,
.summary-row .grp-row .cell.beden {
align-items: center;
height: 100%;
}
.summary-row .cell.aciklama {
grid-column: 5 / 6 !important; /* sadece 5. kolon */
position: relative !important;
width: calc(var(--col-aciklama) + 92px) !important; /* 🔹 74px genişletme */
margin-right: -92px !important; /* 🔹 bedenle tam hizalanır */
white-space: normal !important;
word-break: break-word !important;
overflow-wrap: break-word !important;
line-height: 1.4 !important;
padding: 6px 12px !important;
font-size: 13px !important;
text-align: left !important;
display: flex !important;
flex-direction: column !important;
align-items: flex-start !important;
justify-content: flex-start !important;
min-height: 36px !important;
background: #fff !important;
box-sizing: border-box !important;
border-right: 1px solid #ccc !important;
z-index: 10 !important;
}
/* 🧩 Grid çizgi kontrastı güçlendirme */
.summary-row .cell,
.order-grid-header .col-fixed,
.summary-row .grp-row .cell.beden {
border-color: #bbb !important; /* 🔹 daha belirgin çizgi */
}
.summary-row .cell:not(:last-child) {
border-right: 1px solid #bdbdbd !important;
}
/* ===========================================================
🧱 ALT GRID ÇİZGİLERİ TÜM SATIRLAR İÇİN
=========================================================== */
.summary-row {
border-bottom: 1px solid #ccc; /* 🔹 satır alt çizgisi */
}
.summary-row:last-child {
border-bottom: 2px solid #b7a33a; /* 🔹 son satırda Baggi gold tonu */
}
/* 🔹 Hücrelerin alt çizgisi (beden dahil) */
.summary-row .cell,
.summary-row .grp-row .cell.beden {
border-bottom: 1px solid #ddd !important;
}
/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */
.summary-row:hover .cell,
.summary-row:hover .grp-row .cell.beden {
border-bottom: 1px solid #ccc !important;
}
.summary-row:hover {
background: #fffce0;
}
.summary-row.is-editing {
background: #fff3cd;
outline: 2px solid #caa83f;
z-index: 2;
}
.editor .q-btn:hover {
background: #d2b04d;
color: #fff;
}
/* 🔹 Hover olduğunda grid çizgileri kaybolmasın */
.summary-row:hover .cell,
.summary-row:hover .grp-row .cell.beden {
border-bottom: 1px solid #ccc !important;
}
/* ===========================================================
🎨 STOK RENKLERİ (LOWMIDHIGH)
=========================================================== */
.stok-red {
color: #e53935; /* 🔴 Kırmızı */
font-weight: 600;
}
.stok-yellow {
color: #f9a825; /* 🟡 Sarı */
font-weight: 600;
}
.stok-green {
color: #43a047; /* 🟢 Yeşil */
font-weight: 600;
}
.q-banner.rounded-borders {
border-radius: 8px;
}
.order-gateway {
background: linear-gradient(145deg, #fff 0%, #fafafa 100%);
height: 100%;
}
.order-btn {
font-size: 1.2rem;
padding: 20px 40px;
border-radius: 12px;
min-width: 280px;
transition: all 0.2s ease;
}
.order-btn:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* ===========================================================
🧭 DRAWER AÇIKKEN GRID HİZALAMA FIX
=========================================================== */
/* Drawer açıkken içerik kaymaması */
.body--drawer-left-open .order-page {
width: calc(100vw - var(--drawer-w)); /* viewport'tan drawer genişliği kadar düş */
overflow-x: hidden; /* dış overflowu kes */
}
/* Scroll konteyner sadece grid içinde çalışsın */
.order-scroll-x {
max-width: 100%;
overflow-x: auto;
overflow-y: visible;
background: #fff;
box-sizing: border-box;
}
/* Scrollbar ve sağ boşluğu dengeler */
.order-grid-header,
.order-sub-header,
.order-grid-body {
min-width: fit-content;
width: 100%;
box-sizing: border-box;
}
/* ===========================================================
🧱 DRAWER AÇIKKEN TAM HİZALAMA FIX (v2)
=========================================================== */
/* Drawer açıkken tüm üst bloklar sağdan taşmasın */
.body--drawer-left-open .filter-bar,
.body--drawer-left-open .save-toolbar,
.body--drawer-left-open .order-grid-header,
.body--drawer-left-open .order-sub-header,
.body--drawer-left-open .order-grid-body {
width: calc(100vw - var(--drawer-w)); /* drawer genişliği kadar daralt */
margin-left: 0;
margin-right: 0;
overflow-x: hidden;
box-sizing: border-box;
}
/* Drawer kapalıyken tam genişlik */
.body--drawer-left-closed .filter-bar,
.body--drawer-left-closed .save-toolbar,
.body--drawer-left-closed .order-grid-header,
.body--drawer-left-closed .order-sub-header,
.body--drawer-left-closed .order-grid-body {
width: 100vw;
}
/* Order grid sağ sınırı altın kenarlıkla bitir (optik kapanış) */
.order-grid-header,
.order-sub-header,
.order-grid-body {
border-right: 2px solid var(--baggi-gold);
}
/* ===========================================================
🎯 SAĞ ALT BOŞLUK FİNAL FIX
=========================================================== */
/* Drawer açıkken tüm grid konteynerleri sağdan tam sıfırla */
.body--drawer-left-open .order-page,
.body--drawer-left-open .filter-bar,
.body--drawer-left-open .save-toolbar,
.body--drawer-left-open .order-grid-header,
.body--drawer-left-open .order-sub-header,
.body--drawer-left-open .order-grid-body {
width: calc(100vw - var(--drawer-w) - 8px); /* 🔹 scrollbar toleransı */
padding-right: 0 !important;
margin-right: 0 !important;
overflow-x: hidden !important;
}
/* Son altın kenarlık hizasını koru */
.order-grid-body {
border-right: 2px solid var(--baggi-gold);
}
/* ===========================================================
🎯 GRID SAĞ HİZALAMA (FILTER + SAVE + HEADER)
=========================================================== */
/* Ana scroll container referansı */
.order-scroll-x {
display: flex;
flex-direction: column;
align-items: flex-start; /* hizalama sola */
overflow-x: auto;
overflow-y: visible;
background: #fff;
}
/* Filter ve Save barlar grid genişliğini takip etsin */
.filter-bar,
.save-toolbar,
.order-grid-header,
.order-sub-header {
width: fit-content; /* içeriğe göre genişlik */
min-width: 100%; /* minimum ekran kadar */
box-sizing: border-box;
}
/* Grid bodynin genişliği kadar sağ hizalama */
.order-grid-body {
width: fit-content;
box-sizing: border-box;
}
/* Sağ kenarda taşma veya padding olmasın */
.filter-bar,
.save-toolbar,
.order-grid-header,
.order-sub-header,
.order-grid-body {
margin-right: 0 !important;
padding-right: 0 !important;
border-right: none !important; /* altın çizgi istemiyorsan kaldırılır */
}
/* Drawer açık/kapalı fark etmeden */
.body--drawer-left-open .order-scroll-x,
.body--drawer-left-closed .order-scroll-x {
width: 100%;
overflow-x: auto;
}
/* ===============================
ORDER LIST (ol-) — Sticky Stack
=============================== */
:root {
/* Quasar header yüksekliği */
--ol-header-h: 56px;
/* Filter bar yüksekliği (px) — inputlar tek satırsa 56 idealdir */
--ol-filter-h: 96px;
}
/* q-page tek scroller: header altından başlar */
.ol-page {
height: calc(100vh - var(--ol-header-h));
overflow: auto; /* 🔑 tek scroll container */
background: #fff;
display: flex;
flex-direction: column;
}
/* Filter bar: q-headerın altında sticky */
.ol-filter-bar {
position: sticky;
top: 0; /* 🔑 .ol-page scrollerında en üst */
z-index: 600;
background: #fff;
border-bottom: 1px solid #ddd;
padding: 10px 16px;
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
min-height: var(--ol-filter-h);
display: flex;
align-items: center;
}
/* QTable: sticky thead, zebra aktif ve çakışma yok */
.ol-table .q-table__middle {
overflow: visible !important; /* sticky thead için güvenli */
max-height: none !important;
}
/* thead sabitleme: filter barın ALTINA oturur */
.ol-table thead th {
position: sticky;
top: var(--ol-filter-h); /* 🔑 filter yüksekliği kadar boşluk */
z-index: 500;
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
font-weight: 700;
}
/* Zebra */
.ol-table .q-table__body .q-tr:nth-child(odd) {
background-color: #f7f7f7 !important;
}
.ol-table .q-table__body .q-tr:nth-child(even) {
background-color: #ffffff !important;
}
.ol-table .q-table__body .q-tr:hover {
background-color: #fff7d1 !important;
transition: background-color .15s ease;
}
/* Hücreler */
.ol-table .q-td {
font-size: .9rem;
line-height: 1.3;
padding: 6px 8px !important;
}
/* Güvenli z-index hiyerarşisi */
.q-header { z-index: 1000 !important; } /* header en üstte */
.q-drawer { z-index: 950 !important; } /* drawer headerın altında */
/* Mobile */
@media (max-width: 768px) {
:root { --ol-filter-h: 64px; } /* input kırılıyorsa biraz artır */
.ol-filter-bar { padding: 8px 12px; }
}
/* ===========================================================
🟡 ORDERLIST ZEBRA FIX (v3)
=========================================================== */
/* Her iki tr katmanını da hedefliyoruz (Quasar q-tr + native tr) */
.ol-table tbody tr:nth-child(odd),
.ol-table .q-table__body .q-tr:nth-child(odd) {
background-color: #faf8ef !important; /* açık krem tonu */
}
.ol-table tbody tr:nth-child(even),
.ol-table .q-table__body .q-tr:nth-child(even) {
background-color: #ffffff !important;
}
/* Hover tonu: hafif Baggi gold dokunuşu */
.ol-table tbody tr:hover,
.ol-table .q-table__body .q-tr:hover {
background-color: #fff4cc !important;
transition: background-color 0.2s ease;
}

653
ui/src/stores/deneme3 Normal file
View File

@@ -0,0 +1,653 @@
// src/stores/orderentryStore.js
import { defineStore } from 'pinia'
import axios from 'axios'
import qs from 'qs'
import { useAuthStore } from 'src/stores/authStore'
import dayjs from 'src/boot/dayjs'
import { ref, watch } from 'vue'
/* ==========================================================
Reaktif shared referanslar (bazı UI yardımcıları)
========================================================== */
const stockMap = ref({}) // { "48": 12, "50": 7, ... }
const bedenStock = ref([]) // [{beden:'48', stok:12}, ...]
const sizeCache = ref({}) // beden/stok cache (component tarafı çağırıyor)
/* ==========================================================
STORE
========================================================== */
export const useOrderentryStore = defineStore('orderentry', {
state: () => ({
/* 🔹 Ana durumlar */
orders: [], // grid kaynak arrayi (summaryRows ile senkron)
loading: false,
selected: null, // UIde seçili satır
error: null,
/* 🔹 Cari */
customers: [],
selectedCustomer: null,
/* 🔹 Ürün zinciri */
products: [],
colors: [],
secondColors: [],
inventory: [],
selectedProduct: null,
selectedColor: null,
selectedColor2: null,
/* 🔹 Transaction & Storage */
activeTransactionId: null,
persistKey: 'bss_orderentry_data', // ♻️ kalıcı depolama keyi
lastSnapshotKey: 'bss_orderentry_snapshot', // son-kaydedilen-sipariş
/* 🔹 Düzenleme durumu */
editingIndex: -1,
currentOrderId: null, // edit modunda header ID
header: {}, // backend header modeli
mode: 'new' // 'new' | 'edit'
}),
getters: {
/* 🔹 Toplam adet */
totalQty(state) {
return state.orders.reduce((sum, r) => sum + (Number(r.adet) || 0), 0)
},
/* 🔹 Toplam tutar (string fix2) */
totalAmount(state) {
const n = state.orders.reduce((s, r) => s + (Number(r.tutar) || 0), 0)
return isNaN(n) ? '0.00' : n.toFixed(2)
},
/* 🔹 Müşteri bazlı gruplanmış (opsiyonel) */
groupedByCustomer(state) {
const out = {}
for (const row of state.orders) {
const k = row.musteri || '—'
if (!out[k]) out[k] = []
out[k].push(row)
}
return out
},
/* 🔹 2. renk var mı? */
hasSecondColor(state) {
return Array.isArray(state.secondColors) && state.secondColors.length > 0
},
/* 🔹 Envanter toplamı */
totalInventoryQty(state) {
return state.inventory.reduce((s, r) => s + (Number(r.kullanilabilir) || 0), 0)
}
},
actions: {
/* ==========================================================
STORAGE — Kalıcı kayıt yardımcıları
========================================================== */
saveToStorage() {
try {
const payload = {
orders: this.orders,
header: this.header,
currentOrderId: this.currentOrderId,
selectedCustomer: this.selectedCustomer,
activeTransactionId: this.activeTransactionId,
mode: this.mode,
savedAt: dayjs().toISOString()
}
localStorage.setItem(this.persistKey, JSON.stringify(payload))
} catch (err) {
console.warn('⚠️ localStorage kaydı başarısız:', err)
}
},
/* Kayıt sonrası görüntülenecek "snapshot".
UIyi temizlesen bile sayfa yenilenince bu snapshot geri yüklenebilir. */
saveSnapshot(tag = 'post-submit') {
try {
const snap = {
tag,
orders: this.orders,
header: this.header,
currentOrderId: this.currentOrderId,
selectedCustomer: this.selectedCustomer,
mode: this.mode,
savedAt: dayjs().toISOString()
}
localStorage.setItem(this.lastSnapshotKey, JSON.stringify(snap))
} catch (e) {
console.warn('⚠️ saveSnapshot hatası:', e)
}
},
loadFromStorage() {
try {
const raw = localStorage.getItem(this.persistKey)
if (!raw) return
const data = JSON.parse(raw)
if (Array.isArray(data.orders)) this.orders = data.orders
this.header = data.header || {}
this.currentOrderId = data.currentOrderId || null
this.selectedCustomer = data.selectedCustomer || null
this.activeTransactionId = data.activeTransactionId || null
this.mode = data.mode || 'new'
console.log(`♻️ Storage yüklendi • mode:${this.mode} • rows:${this.orders.length}`)
} catch (err) {
console.warn('⚠️ localStorage okuma hatası:', err)
}
},
loadSnapshot() {
try {
const raw = localStorage.getItem(this.lastSnapshotKey)
if (!raw) return null
return JSON.parse(raw)
} catch (e) {
console.warn('⚠️ loadSnapshot hatası:', e)
return null
}
},
clearStorage() {
localStorage.removeItem(this.persistKey)
// snapshotı silmiyoruz → kullanıcı isterse elle siler
},
/* ==========================================================
TRANSACTION STATE
========================================================== */
setTransaction(id) {
this.activeTransactionId = id
this.saveToStorage()
},
async initTransaction() {
if (this.activeTransactionId) {
console.log('🔹 Aktif transaction:', this.activeTransactionId)
return this.activeTransactionId
}
try {
const dummyId = Math.floor(100000 + Math.random() * 900000)
this.activeTransactionId = dummyId
this.saveToStorage()
console.log('🧩 Dummy Transaction başlatıldı:', dummyId)
return dummyId
} catch (err) {
console.error('❌ Dummy transaction başlatılamadı:', err)
return null
}
},
clearTransaction() {
this.activeTransactionId = null
this.saveToStorage()
},
/* Ordersı otomatik kaydeden watcher (componentten çağrılır) */
watchOrders() {
watch(
() => this.orders,
() => {
// her değişimde full storage yaz
this.saveToStorage()
},
{ deep: true }
)
},
/* ==========================================================
CRUD — Frontend gridi ile senkron temel aksiyonlar
========================================================== */
addRow(row) {
if (!row) return
this.orders.push({ ...row })
this.saveToStorage()
},
updateRow(idOrIndex, patch) {
if (idOrIndex == null) return
let idx = -1
if (typeof idOrIndex === 'number') {
idx = idOrIndex
} else {
// id ile bul
idx = this.orders.findIndex(r => r.id === idOrIndex)
}
if (idx >= 0 && this.orders[idx]) {
this.orders[idx] = { ...this.orders[idx], ...patch }
this.saveToStorage()
}
},
removeRow(idOrIndex) {
let idx = -1
if (typeof idOrIndex === 'number') {
idx = idOrIndex
} else {
idx = this.orders.findIndex(r => r.id === idOrIndex)
}
if (idx >= 0) {
this.orders.splice(idx, 1)
this.saveToStorage()
}
},
/* ==========================================================
PRICE / LIMIT — Minimum fiyat sorgusu (model + PB)
Beklenen response: { price, priceTRY, rateToTRY }
========================================================== */
async fetchMinPrice(modelCode, pb) {
if (!modelCode || !pb) return null
try {
const baseURL = 'http://localhost:8080'
const res = await axios.get(`${baseURL}/api/min-price`, {
params: { code: modelCode, pb }
})
const d = res?.data || null
if (!d) return null
// normalize
return {
price: Number(d.price ?? d.Price ?? 0),
priceTRY: Number(d.priceTRY ?? d.PriceTRY ?? d.price_try ?? 0),
rateToTRY: Number(d.rateToTRY ?? d.RateToTRY ?? d.rate ?? 1)
}
} catch (e) {
console.warn('⚠️ fetchMinPrice hata:', e)
return null
}
},
/* ==========================================================
LOAD (EDIT MODE) — Sunucudan Siparişi Açma
========================================================== */
async openById(id) {
if (!id) return
this.loading = true
try {
const auth = useAuthStore()
const res = await axios.get(`http://localhost:8080/api/order/get/${id}`, {
headers: { Authorization: `Bearer ${auth.token}` }
})
const data = res.data || {}
// 🔹 sql.Null* flatten helper
const flat = (v) => {
if (v === null || v === undefined) return null
if (typeof v === 'object' && 'Valid' in v) {
return v.Valid
? v.String ?? v.Float64 ?? v.Int32 ?? v.Time ?? null
: null
}
return v
}
/* ============================================================
🧾 HEADER MAPPING (73 kolon)
============================================================ */
const h = data.header || {}
const header = {
// Görünen alanlar
OrderHeaderID: flat(h.OrderHeaderID) || '',
OrderNumber: flat(h.OrderNumber) || '',
OrderDate: flat(h.OrderDate)
? String(flat(h.OrderDate)).substring(0, 10)
: '',
AverageDueDate: flat(h.AverageDueDate)
? String(flat(h.AverageDueDate)).substring(0, 10)
: '',
Description: flat(h.Description) || '',
CurrAccCode: flat(h.CurrAccCode) || '',
DocCurrencyCode: flat(h.DocCurrencyCode) || 'TRY',
// Arka plan alanlar (backend roundtrip)
OrderTypeCode: flat(h.OrderTypeCode) || 1,
ProcessCode: flat(h.ProcessCode) || 'WS',
IsCancelOrder: flat(h.IsCancelOrder) || 0,
OrderTime: flat(h.OrderTime) || '',
DocumentNumber: flat(h.DocumentNumber) || '',
PaymentTerm: flat(h.PaymentTerm) || '',
InternalDescription: flat(h.InternalDescription) || '',
CurrAccTypeCode: flat(h.CurrAccTypeCode) || '',
SubCurrAccID: flat(h.SubCurrAccID) || '',
ContactID: flat(h.ContactID) || '',
ShipmentMethodCode: flat(h.ShipmentMethodCode) || '',
ShippingPostalAddressID: flat(h.ShippingPostalAddressID) || '',
BillingPostalAddressID: flat(h.BillingPostalAddressID) || '',
GuarantorContactID: flat(h.GuarantorContactID) || '',
GuarantorContactID2: flat(h.GuarantorContactID2) || '',
RoundsmanCode: flat(h.RoundsmanCode) || '',
DeliveryCompanyCode: flat(h.DeliveryCompanyCode) || '',
TaxTypeCode: flat(h.TaxTypeCode) || '',
WithHoldingTaxTypeCode: flat(h.WithHoldingTaxTypeCode) || '',
DOVCode: flat(h.DOVCode) || '',
TaxExemptionCode: flat(h.TaxExemptionCode) || 0,
CompanyCode: flat(h.CompanyCode) || 1,
OfficeCode: flat(h.OfficeCode) || '101',
StoreTypeCode: flat(h.StoreTypeCode) || 5,
StoreCode: flat(h.StoreCode) || 0,
POSTerminalID: flat(h.POSTerminalID) || 0,
WarehouseCode: flat(h.WarehouseCode) || '1-0-12',
ToWarehouseCode: flat(h.ToWarehouseCode) || '',
OrdererCompanyCode: flat(h.OrdererCompanyCode) || 1,
OrdererOfficeCode: flat(h.OrdererOfficeCode) || '101',
OrdererStoreCode: flat(h.OrdererStoreCode) || '',
GLTypeCode: flat(h.GLTypeCode) || '',
LocalCurrencyCode: flat(h.LocalCurrencyCode) || 'TRY',
ExchangeRate: flat(h.ExchangeRate) || 1,
DiscountReasonCode: flat(h.DiscountReasonCode) || 0,
SurplusOrderQtyToleranceRate: flat(h.SurplusOrderQtyToleranceRate) || 0,
IncotermCode1: flat(h.IncotermCode1) || '',
IncotermCode2: flat(h.IncotermCode2) || '',
PaymentMethodCode: flat(h.PaymentMethodCode) || '',
IsInclutedVat: flat(h.IsInclutedVat) || 0,
IsCreditSale: flat(h.IsCreditSale) || 1,
IsCreditableConfirmed: flat(h.IsCreditableConfirmed) || 1,
CreditableConfirmedUser: flat(h.CreditableConfirmedUser) || '',
CreditableConfirmedDate: flat(h.CreditableConfirmedDate) || '',
ApplicationCode: flat(h.ApplicationCode) || 'Order',
ApplicationID: flat(h.ApplicationID) || '',
CreatedUserName: flat(h.CreatedUserName) || '',
CreatedDate: flat(h.CreatedDate) || '',
LastUpdatedUserName: flat(h.LastUpdatedUserName) || '',
LastUpdatedDate: flat(h.LastUpdatedDate) || '',
IsProposalBased: flat(h.IsProposalBased) || 0
}
this.header = header
this.currentOrderId = header.OrderHeaderID || id
this.mode = 'edit'
// 🔹 Cari görünümü (QSelect)
this.selectedCustomer = {
value: header.CurrAccCode || '',
label: `${header.CurrAccCode || ''} - ${flat(h.CurrAccDescription) || ''}`
}
/* ============================================================
📦 LINES MAPPING (57 kolon)
============================================================ */
this.orders = (data.lines || []).map((l, idx) => ({
// Görünen alanlar
id: flat(l.OrderLineID) || `row-${idx + 1}`,
model: flat(l.ItemCode),
renk: flat(l.ColorCode),
renk2: flat(l.ItemDim2Code),
fiyat: Number(flat(l.Price) || 0),
pb: flat(l.DocCurrencyCode) || flat(l.PriceCurrencyCode) || 'USD',
adet: Number(flat(l.Qty1) || 0),
tutar: Number(flat(l.Price) || 0) * Number(flat(l.Qty1) || 0),
aciklama: flat(l.LineDescription) || '',
terminTarihi: flat(l.DeliveryDate)
? String(flat(l.DeliveryDate)).substring(0, 10)
: '',
urunAnaGrubu: flat(l.ProductGroup) || '',
urunAltGrubu: flat(l.ProductSubGroup) || '',
grpKey: l.grpKey || 'tak',
bedenMap: l.BedenMap || {},
// Backend roundtrip alanları
SortOrder: flat(l.SortOrder) || 0,
ItemTypeCode: flat(l.ItemTypeCode) || 1,
ItemDim1Code: flat(l.ItemDim1Code) || '',
ItemDim3Code: flat(l.ItemDim3Code) || '',
Qty2: flat(l.Qty2) || 0,
CancelQty1: flat(l.CancelQty1) || 0,
CancelQty2: flat(l.CancelQty2) || 0,
CancelDate: flat(l.CancelDate) || null,
OrderCancelReasonCode: flat(l.OrderCancelReasonCode) || '',
ClosedDate: flat(l.ClosedDate) || null,
IsClosed: flat(l.IsClosed) || false,
VatRate: flat(l.VatRate) || 10,
PCTRate: flat(l.PCTRate) || 0,
PriceCurrencyCode: flat(l.PriceCurrencyCode) || 'TRY',
PriceExchangeRate: flat(l.PriceExchangeRate) || header.ExchangeRate || 1,
CreatedUserName: flat(l.CreatedUserName) || '',
CreatedDate: flat(l.CreatedDate) || '',
LastUpdatedUserName: flat(l.LastUpdatedUserName) || '',
LastUpdatedDate: flat(l.LastUpdatedDate) || '',
SurplusOrderQtyToleranceRate:
flat(l.SurplusOrderQtyToleranceRate) || 0
}))
/* ============================================================
💾 LOCAL STORAGE
============================================================ */
localStorage.setItem(
`bssapp:order:last:${id}`,
JSON.stringify({ header, lines: this.orders })
)
console.log(`📦 Sipariş (${id}) yüklendi • rows:${this.orders.length}`)
} catch (err) {
console.error('❌ openById hatası:', err)
this.error = err.message
} finally {
this.loading = false
}
}
,
/* ==========================================================
NEW TEMPLATE — Yeni sipariş başlatma
========================================================== */
newOrderTemplate() {
const today = dayjs().format('YYYY-MM-DD')
const due = dayjs().add(30, 'day').format('YYYY-MM-DD')
this.header = {
OrderHeaderID: '',
OrderTypeCode: 1,
ProcessCode: 'WS',
OrderNumber: '',
OrderDate: today,
AverageDueDate: due,
Description: '',
CurrAccCode: '',
CurrAccDescription: '',
DocCurrencyCode: 'USD',
LocalCurrencyCode: 'TRY',
ExchangeRate: 1,
CompanyCode: 1,
OfficeCode: '101',
StoreTypeCode: 5,
WarehouseCode: '1-0-12',
IsCreditSale: true,
CreatedUserName: '',
CreatedDate: today,
LastUpdatedUserName: '',
LastUpdatedDate: today
}
this.orders = []
this.currentOrderId = null
this.activeTransactionId = null
this.selectedCustomer = null
this.mode = 'new'
this.error = null
// Temiz bir başlangıcı storagea yaz
this.saveToStorage()
console.log('🧾 Yeni sipariş template yüklendi.')
},
/* ==========================================================
SUBMIT — Create/Update (SQL tablo INSERT/UPDATE)
➜ Kayıt sonrası: transaction kapanır AMA snapshot tutulur.
========================================================== */
async submitAll() {
const auth = useAuthStore()
const baseURL = 'http://localhost:8080'
const toNullable = (v, type = 'string') => {
if (v === null || v === undefined || v === '') {
if (type === 'number') return { Float64: 0, Valid: false }
if (type === 'time') return { Time: null, Valid: false }
return { String: '', Valid: false }
}
if (type === 'number') return { Float64: Number(v), Valid: true }
if (type === 'time') return { Time: v, Valid: true }
return { String: String(v), Valid: true }
}
try {
this.loading = true
// Header payload (backendin beklediği Null* formatıyla)
const h = this.header || {}
const headerPayload = {
OrderHeaderID: h.OrderHeaderID || this.currentOrderId || '',
OrderTypeCode: toNullable(1, 'number'),
ProcessCode: toNullable('WS'),
OrderNumber: toNullable(h.OrderNumber),
OrderDate: toNullable(h.OrderDate || dayjs().format('YYYY-MM-DD'), 'time'),
AverageDueDate: toNullable(h.AverageDueDate || dayjs().add(30, 'day').format('YYYY-MM-DD'), 'time'),
Description: toNullable(h.Description || ''),
CurrAccCode: toNullable(h.CurrAccCode || this.selectedCustomer?.value || ''),
CurrAccDescription: toNullable(h.CurrAccDescription || this.selectedCustomer?.label || ''),
DocCurrencyCode: toNullable(h.DocCurrencyCode || 'USD'),
LocalCurrencyCode: toNullable(h.LocalCurrencyCode || 'TRY'),
ExchangeRate: toNullable(h.ExchangeRate || 1, 'number'),
CompanyCode: toNullable(1, 'number'),
OfficeCode: toNullable('101'),
StoreTypeCode: toNullable(5, 'number'),
WarehouseCode: toNullable(h.WarehouseCode || '1-0-12'),
IsCreditSale: true,
CreatedUserName: toNullable(auth.user?.Username || 'admin'),
CreatedDate: toNullable(h.CreatedDate || dayjs().format('YYYY-MM-DD'), 'time'),
LastUpdatedUserName: toNullable(auth.user?.Username || 'admin'),
LastUpdatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time')
}
// Lines payload
const linesPayload = this.orders.map((l, idx) => ({
OrderLineID: l.id || '',
SortOrder: idx + 1,
ItemTypeCode: toNullable(1, 'number'),
ItemCode: toNullable(l.model),
ColorCode: toNullable(l.renk),
ItemDim1Code: toNullable(Object.keys(l.bedenMap?.[l.grpKey] || {})[0] || ''),
ItemDim2Code: toNullable(l.renk2),
Qty1: toNullable(Number(l.adet || 0), 'number'),
Price: toNullable(Number(l.fiyat || 0), 'number'),
DocCurrencyCode: toNullable(l.pb || 'USD'),
VatRate: toNullable(10, 'number'),
PCTRate: toNullable(0, 'number'),
DeliveryDate: toNullable(l.terminTarihi || null, 'time'),
LineDescription: toNullable(l.aciklama || ''),
IsClosed: false,
CreatedUserName: toNullable(auth.user?.Username || 'admin'),
CreatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time'),
LastUpdatedUserName: toNullable(auth.user?.Username || 'admin'),
LastUpdatedDate: toNullable(dayjs().format('YYYY-MM-DD HH:mm:ss'), 'time')
}))
// Final payload
const payload = {
header: headerPayload,
lines: linesPayload,
user: auth.user?.Username || 'admin'
}
let res
if (this.currentOrderId) {
// UPDATE
res = await axios.post(`${baseURL}/api/order/update`, payload, {
headers: { Authorization: `Bearer ${auth.token}` }
})
console.log('✅ UPDATE ok:', res.data)
} else {
// CREATE
res = await axios.post(`${baseURL}/api/order/create`, payload, {
headers: { Authorization: `Bearer ${auth.token}` }
})
console.log('✅ CREATE ok:', res.data)
if (res.data?.orderID) {
this.currentOrderId = res.data.orderID
this.header.OrderHeaderID = res.data.orderID
this.mode = 'edit'
}
}
// 🟩 Kayıt sonrası: snapshotı al ve storagea da yaz
this.saveSnapshot('post-submit')
this.saveToStorage()
// 🧹 Transactionı kapat (UI temizliği ayrı fonksiyonda)
this.clearTransaction()
this.afterSubmit({ keepLocalStorage: true }) // 👈 önemli
} catch (err) {
console.error('❌ submitAll hatası:', err)
this.error = err.message
throw err
} finally {
this.loading = false
}
},
/* ==========================================================
AFTER SUBMIT — UI temizliği (snapshot kalır!)
keepLocalStorage=true → persistKey SİLİNMEZ
========================================================== */
afterSubmit(opts = { keepLocalStorage: true }) {
try {
// Snapshot zaten kaydedildi; istenirse persistKeyi bırak
if (!opts?.keepLocalStorage) {
localStorage.removeItem(this.persistKey)
} else {
// son hal zaten saveToStorage ile yazıldı — dokunma
}
// UI temizliği (hafızada formu boşaltalım)
// Ama edite dönmek istersen, snapshot/loadFromStorage ile geri getirirsin.
this.orders = []
// headerı hafızadan temizliyoruz ama snapshot yerinde.
this.header = {}
this.selectedCustomer = null
this.editingIndex = -1
// currentOrderIdyi istersen koruyabilirsin; biz editte geri yüklüyoruz.
// burada nulllıyoruz:
this.currentOrderId = null
this.mode = 'new'
this.loading = false
this.error = null
console.log('🧹 afterSubmit: UI temizlendi, snapshot storageda.')
} catch (err) {
console.warn('⚠️ afterSubmit temizleme hatası:', err)
}
},
/* ==========================================================
MANUAL UPDATE — mevcut header/lines yapılarına göre
(İsteğe bağlı kullanılır)
========================================================== */
async updateOrder() {
if (!this.currentOrderId) {
console.warn('⚠️ currentOrderId yok, update yapılamaz.')
return
}
try {
const auth = useAuthStore()
const payload = {
header: this.header,
lines: this.orders,
username: auth.user?.Username || 'admin'
}
const res = await axios.post(
'http://localhost:8080/api/order/update',
payload,
{ headers: { Authorization: `Bearer ${auth.token}` } }
)
console.log('✅ Güncelleme tamamlandı:', res.data)
// kayıt sonrası snapshot + persist
this.saveSnapshot('manual-update')
this.saveToStorage()
} catch (err) {
console.error('❌ updateOrder hatası:', err)
this.error = err.message
}
}
} // actions
}) // defineStore
// (opsiyonel) Bu referanslara component tarafından erişmek istersen:
export const sharedOrderEntryRefs = {
stockMap,
bedenStock,
sizeCache
}

View File

@@ -0,0 +1,50 @@
// src/stores/downloadstHeadStore.js
import { defineStore } from 'pinia'
import { download } from 'src/services/api'
export const useDownloadstHeadStore = defineStore('downloadstHead', {
actions: {
// 📄 Statement Header PDF indir / aç
async handlestHeadDownload (
accountCode,
startDate,
endDate,
parislemler
) {
try {
// ✅ Params (axios paramsSerializer array=repeat destekliyor)
const params = {
accountcode: accountCode,
startdate: startDate,
enddate: endDate
}
if (Array.isArray(parislemler) && parislemler.length > 0) {
params.parislemler = parislemler.filter(
p => p !== undefined && p !== null && p !== ''
)
}
// 🔥 API CALL (TOKEN + BLOB + ERROR HANDLING OTOMATİK)
const blob = await download(
'/exportstamentheaderreport-pdf',
params
)
const pdfUrl = window.URL.createObjectURL(blob)
window.open(pdfUrl, '_blank')
return { ok: true, message: '📄 PDF hazırlandı' }
} catch (err) {
console.error('❌ PDF açma hatası:', err)
return {
ok: false,
message:
err?.message ||
'❌ PDF açma hatası'
}
}
}
}
})

View File

@@ -0,0 +1,51 @@
// src/stores/downloadstpdfStore.js
import { defineStore } from 'pinia'
import { download } from 'src/services/api'
export const useDownloadstpdfStore = defineStore('downloadstpdf', {
actions: {
/* ==========================================================
📄 PDF İNDİR / AÇ
========================================================== */
async downloadPDF(accountCode, startDate, endDate, parislemler = []) {
try {
// 🔹 Query params
const params = {
accountcode: accountCode,
startdate: startDate,
enddate: endDate
}
if (Array.isArray(parislemler) && parislemler.length > 0) {
params.parislemler = parislemler.filter(
p => p !== undefined && p !== null && p !== ''
)
}
// 🔥 MERKEZİ API — BLOB
const blob = await download('/export-pdf', params)
// 🔹 Blob → URL
const pdfUrl = window.URL.createObjectURL(
new Blob([blob], { type: 'application/pdf' })
)
// 🔹 Yeni sekmede aç
window.open(pdfUrl, '_blank')
console.log('✅ PDF yeni sekmede açıldı')
return { ok: true, message: '📄 PDF hazırlandı' }
} catch (err) {
console.error('❌ PDF açma hatası:', err)
return {
ok: false,
message:
err?.message ||
'❌ PDF alınamadı'
}
}
}
}
})

View File

@@ -0,0 +1,15 @@
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
},
actions: {
increment() {
this.counter++;
},
},
});

20
ui/src/stores/index.js Normal file
View File

@@ -0,0 +1,20 @@
import { defineStore } from '#q-app/wrappers'
import { createPinia } from 'pinia'
/*
* If not building with SSR mode, you can
* directly export the Store instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Store instance.
*/
export default defineStore((/* { ssrContext } */) => {
const pinia = createPinia()
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})

View File

@@ -0,0 +1,29 @@
import { defineStore } from 'pinia'
import { post } from 'src/services/api'
export const useMailTestStore = defineStore('mailTest', {
state: () => ({
loading: false,
lastResult: null
}),
actions: {
async sendTestMail (to) {
this.loading = true
try {
const data = await post('/test-mail', {
to
})
this.lastResult = data
return true
} catch (err) {
this.lastResult = err
throw err
} finally {
this.loading = false
}
}
}
})

View File

@@ -0,0 +1,38 @@
// src/stores/mePasswordStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
export const useMePasswordStore = defineStore('mePassword', {
state: () => ({
loading: false,
error: null
}),
actions: {
async changePassword (currentPassword, newPassword) {
this.loading = true
this.error = null
try {
// 🔐 Token interceptor ile otomatik
await api.post('/me/password', {
current_password: currentPassword,
new_password: newPassword
})
return true
} catch (e) {
// 🔥 api.js normalize error
this.error =
e?.message ||
'Şifre güncellenemedi'
throw e
} finally {
this.loading = false
}
}
}
})

View File

@@ -0,0 +1,3340 @@
/* ===========================================================
📦 orderentryStore.js (v3.4 CLEAN — AUTH + LOCAL PERSIST + AUTO RESUME)
=========================================================== */
import { defineStore } from 'pinia'
import api from 'src/services/api'
import dayjs from 'src/boot/dayjs'
import { ref, toRaw, nextTick } from 'vue' // ✅ düzeltildi
import { useAuthStore } from 'src/stores/authStore'
// ===========================================================
// 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler)
// ===========================================================
/* ===========================================================
🔹 BEDEN ŞEMALARI — STORE SOURCE OF TRUTH
=========================================================== */
// ⬆️ orderentryStore.js EN ÜSTÜNE
// ===========================================================
// 🔑 COMBO KEY CONTRACT (Frontend ↔ Backend) — v1
// - trim + UPPER
// - dim1 boşsa " "
// - dim2 boşsa ""
// ===========================================================
const BEDEN_EMPTY = '_'
const norm = (v) => (v == null ? '' : String(v)).trim()
const normUpper = (v) => norm(v).toUpperCase()
export function buildComboKey(row, beden) {
const model = normUpper(row?.model || row?.ItemCode)
const renk = normUpper(row?.renk || row?.ColorCode)
const renk2 = normUpper(row?.renk2 || row?.ItemDim2Code)
const bdn = normUpper(beden)
const bedenFinal = bdn === '' ? BEDEN_EMPTY : bdn
// 🔒 KANONİK SIRA
return `${model}||${renk}||${renk2}||${bedenFinal}`
}
export const BEDEN_SCHEMA = [
{ key: 'ayk', title: 'AYAKKABI', values: ['39','40','41','42','43','44','45'] },
{ key: 'yas', title: 'YAŞ', values: ['2','4','6','8','10','12','14'] },
{ key: 'pan', title: 'PANTOLON', values: ['38','40','42','44','46','48','50','52','54','56','58','60','62','64','66','68'] },
{ key: 'gom', title: 'GÖMLEK', values: ['XS','S','M','L','XL','2XL','3XL','4XL','5XL','6XL','7XL'] },
{ key: 'tak', title: 'TAKIM ELBİSE', values: ['44','46','48','50','52','54','56','58','60','62','64','66','68','70','72','74'] },
{ key: 'aksbir', title: 'AKSESUAR', values: [' ', '44', 'STD', '110CM', '115CM', '120CM', '125CM', '130CM', '135CM'] }
]
export const schemaByKey = BEDEN_SCHEMA.reduce((m, g) => {
m[g.key] = g
return m
}, {})
export const stockMap = ref({})
export const bedenStock = ref([])
export const sizeCache = ref({})
// ===========================================================
// 🔹 Shared Reactive Referanslar (Global, Reaktif Nesneler)
// ===========================================================
// ========================
// 🧰 GLOBAL DATE NORMALIZER
// ========================
function newGuid() {
return crypto.randomUUID()
}
// 🔑 Her beden satırı için deterministik clientKey üretimi
function makeLineClientKey(row, grpKey, beden) {
const base =
row.clientRowKey ||
row.clientKey ||
row.id ||
row._id ||
row.tmpId ||
`${row.model || ''}|${row.renk || ''}|${row.renk2 || ''}`
return `${base}::${grpKey}::${beden}`
}
// ===========================================================
// 🧩 Pinia Store — ORDER ENTRY STORE (REV 2025-11-03.2)
// ===========================================================
export const useOrderEntryStore = defineStore('orderentry', {
state: () => ({
isControlledSubmit: false,
allowRouteLeaveOnce: false,
schemaMap: {},
productCache: {},
_lastSavedFingerprint: null,
activeNewHeaderId: localStorage.getItem("bss_active_new_header") || null,
loading: false,
selected: null,
error: null,
customers: [],
selectedCustomer: null,
products: [],
colors: [],
secondColors: [],
inventory: [],
selectedProduct: null,
selectedColor: null,
selectedColor2: null,
OrderHeaderID: null,
// Persist config
persistKey: 'bss_orderentry_data',
lastSnapshotKey: 'bss_orderentry_snapshot',
// Editor state
editingKey: null,
currentOrderId: null,
mode: 'new',
// Grid state
orders: [],
header: {},
summaryRows: [],
lastSavedAt: null,
// Guards
preventPersist: false,
_uiBusy: false,
_unsavedChanges: false,
}),
getters: {
getDraftKey() {
// NEW taslak → GLOBAL ama tekil
return 'bss_orderentry_new_draft'
},
getEditKey() {
// EDIT → OrderHeaderIDye bağlı
const id = this.header?.OrderHeaderID
return id ? `bss_orderentry_edit:${id}` : null
}
,
hasUnsavedChanges(state) {
try {
return (
state._lastSavedFingerprint !==
state._persistFingerprint?.()
)
} catch {
return false
}
},
getPersistKey: (state) =>
state.header?.OrderHeaderID
? `${state.persistKey}:${state.header.OrderHeaderID}`
: state.persistKey,
getSnapshotKey: (state) =>
state.header?.OrderHeaderID
? `${state.lastSnapshotKey}:${state.header.OrderHeaderID}`
: state.lastSnapshotKey,
totalQty: (state) =>
(state.orders || []).reduce((sum, r) => sum + (Number(r?.adet) || 0), 0),
hasAnyClosedLine(state) {
return Array.isArray(state.summaryRows) &&
state.summaryRows.some(r => r?.isClosed === true)
},
totalAmount(state) {
if (!Array.isArray(state.summaryRows)) return 0
return state.summaryRows.reduce(
(sum, r) => sum + Number(r?.tutar || 0),
0
)
}
},
actions: {
normalizeComboUI(row) {
return buildComboKey(row, BEDEN_EMPTY)
}
,
/* ===========================================================
🧩 initSchemaMap — BEDEN ŞEMA İNİT
- TEK SOURCE OF TRUTH: BEDEN_SCHEMA
=========================================================== */
initSchemaMap() {
if (this.schemaMap && Object.keys(this.schemaMap).length > 0) {
return
}
const map = {}
for (const g of BEDEN_SCHEMA) {
map[g.key] = {
key: g.key,
title: g.title,
values: [...g.values]
}
}
this.schemaMap = map
console.log(
'🧩 schemaMap INIT edildi:',
Object.keys(this.schemaMap)
)
},
getRowKey(row) {
if (!row) return null
return row.OrderLineID || row.id || null
}
,
updateHeaderTotals() {
try {
if (!Array.isArray(this.summaryRows)) return 0
const total = this.summaryRows.reduce(
(sum, r) => sum + Number(r?.tutar || 0),
0
)
// Header sadece GÖSTERİM / BACKEND için
if (this.header) {
this.header.TotalAmount = Number(total.toFixed(2))
}
return total
} catch (err) {
console.error('❌ updateHeaderTotals hata:', err)
return 0
}
}
,
/* ===========================================================
🚨 showInvalidVariantDialog — FINAL
-----------------------------------------------------------
✔ prItemVariant olmayan satırları listeler
✔ Satıra tıkla → scroll + highlight
✔ Kaydı BLOKLAYAN tek UI noktası
=========================================================== */
async showInvalidVariantDialog($q, invalidList = []) {
if (!Array.isArray(invalidList) || invalidList.length === 0) return
return new Promise(resolve => {
$q.dialog({
title: '🚨 Tanımsız Ürün Kombinasyonları',
message: `
<div style="max-height:60vh;overflow:auto">
${invalidList.map((v, i) => `
<div
class="invalid-row"
data-clientkey="${v.clientKey}"
style="
padding:8px 10px;
margin-bottom:6px;
border-left:4px solid #c10015;
background:#fff5f5;
cursor:pointer;
"
>
<div style="font-weight:600">
#${i + 1} | Item: ${v.itemCode}
</div>
<div style="font-size:13px">
Beden: ${v.dim1 || '(boş)'} |
Renk: ${v.colorCode || '-'} |
Qty: ${v.qty1}
</div>
<div style="font-size:12px;color:#c10015">
Sebep: ${v.reason || 'Tanımsız ürün kombinasyonu'}
</div>
</div>
`).join('')}
</div>
`,
html: true,
ok: {
label: 'Düzelt',
color: 'negative'
},
cancel: false,
persistent: true
})
.onOk(() => resolve())
.onDismiss(() => resolve())
.onShown(() => {
// Satıra tıklama → scroll + highlight
const nodes = document.querySelectorAll('.invalid-row')
nodes.forEach(n => {
n.addEventListener('click', () => {
const ck = n.getAttribute('data-clientkey')
this.scrollToInvalidRow?.(ck)
})
})
})
})
}
,
/* ===========================================================
🎯 scrollToInvalidRow — FINAL
-----------------------------------------------------------
✔ ClientKey bazlı scroll
✔ Hem summaryRows hem orders destekli
✔ Highlight otomatik kalkar
=========================================================== */
scrollToInvalidRow(clientKey) {
if (!clientKey) return
// 1⃣ Store içindeki satırı bul
const idx = this.summaryRows?.findIndex(
r => r.clientKey === clientKey
)
if (idx === -1) {
console.warn('❌ Satır bulunamadı:', clientKey)
return
}
// 2⃣ DOM node
const el = document.querySelector(
`[data-clientkey="${clientKey}"]`
)
if (!el) {
console.warn('❌ DOM satırı bulunamadı:', clientKey)
return
}
// 3⃣ Scroll
el.scrollIntoView({
behavior: 'smooth',
block: 'center'
})
// 4⃣ Highlight
el.classList.add('invalid-highlight')
setTimeout(() => {
el.classList.remove('invalid-highlight')
}, 2500)
}
,
async checkHeaderExists(orderHeaderID) {
try {
if (!orderHeaderID) return false
const res = await api.get(`/orders/check/${orderHeaderID}`)
// Backend “true/false” döner varsayımı
return res?.data?.exists === true
} catch (err) {
console.warn("⚠ checkHeaderExists hata:", err)
return false
}
}
,
async fetchOrderPdf(orderId) {
try {
const resp = await api.get(`/order/pdf/${orderId}`, {
responseType: 'blob'
})
return resp.data
} catch (err) {
console.error("❌ fetchOrderPdf hata:", err)
throw err
}
}
,
async downloadOrderPdf(id = null) {
try {
const orderId = id || this.header?.OrderHeaderID
if (!orderId) {
console.error('❌ PDF ID bulunamadı')
return
}
const res = await api.get(`/order/pdf/${orderId}`, {
responseType: 'blob'
})
const blob = new Blob([res.data], { type: 'application/pdf' })
const url = URL.createObjectURL(blob)
window.open(url, '_blank')
setTimeout(() => URL.revokeObjectURL(url), 60_000)
} catch (err) {
console.error('❌ PDF açma hatası:', err)
throw err
}
}
,
setActiveNewHeader(id) {
this.activeNewHeaderId = id || null
if (id) localStorage.setItem("bss_active_new_header", id)
else localStorage.removeItem("bss_active_new_header")
},
getActiveNewHeaderId() {
return this.activeNewHeaderId || localStorage.getItem("bss_active_new_header")
},
/* ===========================================================
🧩 initFromRoute (v5.6 — groupedRows TOUCH YOK)
-----------------------------------------------------------
- Route ID ve bss_last_txn arasında en dolu snapshot'ı seçer
- header + orders + summaryRows restore edilir
- groupedRows hydrate edilmez / resetlenmez / dokunulmaz
- Route ID farklıysa router.replace ile URL düzeltilir
=========================================================== */
async initFromRoute(orderId, router = null) { // ✅ NEW MODE → SADECE global draft
if (this.mode === 'new') {
const raw = localStorage.getItem(this.getDraftKey)
if (raw) {
try {
const payload = JSON.parse(raw)
this.header = payload.header || {}
this.orders = payload.orders || []
this.summaryRows = payload.summaryRows || this.orders
console.log('♻️ NEW draft restore edildi (global)')
return
} catch {}
}
console.log('⚪ NEW draft yok, boş başlatılıyor')
return
}
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
this.initSchemaMap()
}
try {
console.log('🧩 [initFromRoute] orderId:', orderId)
const lastTxn = localStorage.getItem('bss_last_txn') || null
const readPayload = (id) => {
if (!id) return null
const raw = localStorage.getItem(`bss_orderentry_data:${id}`)
if (!raw) return null
try {
return JSON.parse(raw)
} catch {
return null
}
}
const fromRoute = readPayload(orderId)
const fromLast = readPayload(lastTxn)
const hasData = (p) =>
!!p && (
(Array.isArray(p.orders) && p.orders.length > 0) ||
(Array.isArray(p.summaryRows) && p.summaryRows.length > 0)
)
let chosenId = null
let chosenPayload = null
if (hasData(fromRoute)) {
chosenId = orderId
chosenPayload = fromRoute
console.log('✅ [initFromRoute] Route ID snapshot seçildi:', chosenId)
} else if (hasData(fromLast)) {
chosenId = lastTxn
chosenPayload = fromLast
console.log('✅ [initFromRoute] lastTxn snapshot seçildi:', chosenId)
}
/* -------------------------------------------------------
🚫 SNAPSHOT YOK → BOŞ BAŞLA
-------------------------------------------------------- */
if (!chosenId || !chosenPayload) {
console.log('⚪ [initFromRoute] Snapshot yok, boş başlatılıyor')
this.header = {
...(this.header || {}),
OrderHeaderID: orderId || lastTxn || crypto.randomUUID()
}
this.orders = []
this.summaryRows = []
// ❗ groupedRows'a DOKUNMA
return
}
/* -------------------------------------------------------
✅ SNAPSHOT RESTORE (SAFE CLONE)
-------------------------------------------------------- */
this.header = {
...(chosenPayload.header || {}),
OrderHeaderID: chosenId
}
const orders = Array.isArray(chosenPayload.orders)
? [...chosenPayload.orders]
: []
const summaryRows = Array.isArray(chosenPayload.summaryRows)
? [...chosenPayload.summaryRows]
: orders
this.orders = orders
this.summaryRows = summaryRows
// ❗ groupedRows hydrate edilmez, resetlenmez
/* -------------------------------------------------------
🔁 lastTxn SENKRON
-------------------------------------------------------- */
try {
localStorage.setItem('bss_last_txn', chosenId)
} catch (e) {
console.warn('⚠️ bss_last_txn yazılamadı:', e)
}
/* -------------------------------------------------------
🔁 ROUTE DÜZELTME (GEREKİRSE)
-------------------------------------------------------- */
if (router && orderId && orderId !== chosenId) {
console.log('🔁 [initFromRoute] Route ID düzeltiliyor →', chosenId)
await router.replace({
name: 'order-entry',
params: { orderHeaderID: chosenId }
})
}
console.log(
'✅ [initFromRoute] Restore tamam. Satır sayısı:',
this.summaryRows.length
)
} catch (err) {
console.error('❌ [initFromRoute] hata:', err)
}
}
,
/* ===========================================================
🆕 startNewOrder (v8.3 — FINAL & STABLE)
=========================================================== */
async startNewOrder({ $q }) {
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
this.initSchemaMap()
}
const headerId = crypto.randomUUID()
let orderNumber = `LOCAL-${dayjs().format("YYMMDD-HHmmss")}`
try {
const res = await api.get("/order/new-number")
if (res?.data?.OrderNumber) {
orderNumber = res.data.OrderNumber
}
} catch {
console.info(' Backend order number yok, LOCAL kullanıldı')
}
this.mode = 'new'
this.isControlledSubmit = false
this.allowRouteLeaveOnce = false
this.header = {
OrderHeaderID: headerId,
OrderNumber: orderNumber,
OrderDate: new Date().toISOString().slice(0, 10),
CurrAccCode: null,
DocCurrencyCode: 'USD',
PriceCurrencyCode: 'USD',
PriceExchangeRate: 1
}
this.orders = []
this.summaryRows = []
// ✅ fingerprint bazlı sistem için reset
this._lastSavedFingerprint = null
// ✅ NEW draft hemen yazılır
this.persistLocalStorage?.()
return this.header
}
,
dedupeActiveLinesByCombo(lines) {
const map = new Map()
for (const ln of lines) {
const key = buildComboKey({
model: ln.ItemCode,
renk: ln.ColorCode,
renk2: ln.ItemDim2Code
}, ln.ItemDim1Code)
if (!map.has(key)) {
ln.ComboKey = key
map.set(key, ln)
continue
}
const ex = map.get(key)
ex.Qty1 = (Number(ex.Qty1) || 0) + (Number(ln.Qty1) || 0)
// OrderLineID boşsa doldur (editte önemli)
if (!ex.OrderLineID && ln.OrderLineID) ex.OrderLineID = ln.OrderLineID
}
return Array.from(map.values())
}
,
/* ===========================================================
🧹 Core reset helper — sadece state'i sıfırlar
=========================================================== */
resetCoreState() {
this.orders = []
this.summaryRows = []
this.groupedRows = []
this.header = {}
this.editingKey = null
this.currentOrderId = null
},resetForNewOrder() {
// mevcut her şeyi temizle
this.header = {
OrderHeaderID: this.header?.OrderHeaderID || null,
OrderDate: new Date().toISOString().slice(0,10),
CurrAccCode: null,
DocCurrencyCode: 'TRY',
PriceCurrencyCode: 'TRY',
// ihtiyaç duyduğun diğer default header alanları
}
this.orders = []
this.summaryRows = []
this.productCache = {}
this.stockMap = {}
this.setMode('new')
}
,
resetForEdit() {
// EDIT modda grid temizlenmez — sadece UI state resetlenir
this.editingKey = null
this.groupedRows = []
this.mode = 'edit'
}
,markAsSaved() {
try {
this._lastSavedFingerprint = this._persistFingerprint()
console.log('✅ markAsSaved → fingerprint senkron')
} catch (e) {
console.warn('⚠️ markAsSaved hata:', e)
}
}
,clearLocalSnapshot() {
try {
const id = this.header?.OrderHeaderID
if (!id) return
localStorage.removeItem(`bss_orderentry_data:${id}`)
console.log('🧹 Local snapshot temizlendi:', id)
} catch (e) {
console.warn('⚠️ clearLocalSnapshot hata:', e)
}
},/* ===========================================================
🧹 HARD CLEAN — ALL ORDERENTRY SNAPSHOTS
=========================================================== */
clearAllOrderSnapshots () {
Object.keys(localStorage)
.filter(k =>
k.startsWith('bss_orderentry_data:') ||
k.startsWith('bss_orderentry_edit:')
)
.forEach(k => {
console.log('🧹 snapshot silindi:', k)
localStorage.removeItem(k)
})
localStorage.removeItem('bss_last_txn')
}
,
/* ===========================================================
🧹 Store Hard Reset — Submit Sonrası Temizlik (FIXED)
- Grid, header, toplamlar, local state'ler sıfırlanır
- persistKey / lastSnapshotKey NULL yapılmaz (config sabit kalır)
- localStorage txn/snapshot temizliği güvenli yapılır
=========================================================== */
hardResetAfterSubmit() {
try {
// 🔑 mevcut idyi yakala (local temizliği için)
const id = this.header?.OrderHeaderID || null
/* -------------------------------------------------------
1) Grid ve satırlar
-------------------------------------------------------- */
this.orders = []
this.summaryRows = []
this.groupedRows = []
/* -------------------------------------------------------
2) Header & meta
-------------------------------------------------------- */
this.header = {}
/* -------------------------------------------------------
3) Mode & edit state
-------------------------------------------------------- */
this.mode = 'new'
this.editingKey = null
this.currentOrderId = null
/* -------------------------------------------------------
4) Snapshot / transaction meta
⚠️ persistKey / lastSnapshotKey store config → NULL YAPMA
-------------------------------------------------------- */
this.activeTransactionId = null
this.submitted = false
// fingerprint / debounce meta varsa sıfırla
this._lastSavedFingerprint = null
this._lastPersistFingerprint = null
if (this._persistTimeout) {
clearTimeout(this._persistTimeout)
this._persistTimeout = null
}
/* -------------------------------------------------------
5) LocalStorage temizlik (opsiyonel ama submit sonrası doğru)
-------------------------------------------------------- */
try {
if (id) {
localStorage.removeItem(`bss_orderentry_data:${id}`)
localStorage.removeItem(`bss_orderentry_snapshot:${id}`)
}
localStorage.removeItem('bss_last_txn')
localStorage.removeItem('bss_active_new_header')
} catch (e) {
console.warn('⚠️ hardResetAfterSubmit localStorage temizliği hata:', e)
}
console.log('🧹 Store resetlendi (submit sonrası).')
} catch (err) {
console.error('❌ hardResetAfterSubmit hata:', err)
}
}
,
/* ===========================================================
✏️ openExistingForEdit (v12 — FINAL & CLEAN)
-----------------------------------------------------------
✔ Backend authoritative (orderlist açılışı local'i dikkate almaz)
✔ mode=new → backend çağrısı YOK
✔ normalizeOrderLines → grpKey + bedenMap garanti
✔ isClosed varsa → view, yoksa → edit
✔ Form sync opsiyonel
✔ İlk açılışta snapshot yazılır (edit boyunca persist ile güncellenir)
=========================================================== */
async openExistingForEdit(
orderId,
{ $q = null, form = null, productCache = null } = {}
) {
// 🔑 schemaMap garanti
if (!this.schemaMap || !Object.keys(this.schemaMap).length) {
this.initSchemaMap?.()
}
if (!orderId) return false
/* =======================================================
🟦 NEW MODE — ASLA backend çağrısı yok
======================================================= */
if (this.mode === 'new') {
console.log('⚪ openExistingForEdit skip (mode=new)')
return false
}
// productCache hem ref hem reactive olabilir → güvenli oku
const pc =
productCache?.value
? productCache.value
: (productCache && typeof productCache === 'object' ? productCache : {})
try {
// geçici varsayım (sonra isClosed durumuna göre set edilecek)
this.setMode?.('edit')
/* =======================================================
🔹 BACKEND — authoritative load
======================================================= */
const res = await api.get(`/order/get/${orderId}`)
const backend = res?.data
if (!backend?.header) {
throw new Error('Backend header yok')
}
/* =======================================================
🔹 HEADER — SADECE BACKEND
(orderlist açılışında local merge YOK)
======================================================= */
this.header = {
...backend.header,
OrderHeaderID: backend.header.OrderHeaderID || orderId
}
/* =======================================================
🔹 NORMALIZE LINES (TEK KAYNAK)
normalizeOrderLines şu alanları üretmeli:
✔ row.grpKey
✔ row.bedenMap[grpKey]
✔ row.isClosed (boolean)
======================================================= */
const normalized = this.normalizeOrderLines(
backend.lines || [],
this.header.DocCurrencyCode || 'USD',
pc
)
this.orders = Array.isArray(normalized) ? normalized : []
this.summaryRows = [...this.orders]
/* =======================================================
🔹 MODE KARARI (BACKEND SATIRLARI ÜZERİNDEN)
- herhangi bir isClosed=true → view
- değilse → edit
======================================================= */
const hasClosedLine = (this.summaryRows || []).some(r => r?.isClosed === true)
this.setMode?.(hasClosedLine ? 'view' : 'edit')
/* =======================================================
🔹 FORM SYNC (opsiyonel)
======================================================= */
if (form) {
Object.assign(form, this.header)
}
/* =======================================================
🔹 LOCAL SNAPSHOT (edit boyunca tutulacak temel)
- Açılışta snapshot yaz
- Sonraki değişikliklerde zaten persistLocalStorage çağrıları var
======================================================= */
this.persistLocalStorage?.()
try {
localStorage.setItem('bss_last_txn', String(orderId))
} catch {}
console.log('✅ openExistingForEdit OK:', {
id: orderId,
rows: this.summaryRows.length,
mode: this.mode,
hasClosedLine
})
return true
} catch (err) {
console.error('❌ openExistingForEdit hata:', err)
// new değilse uyar
if (this.mode !== 'new') {
$q?.notify?.({
type: 'negative',
message: 'Sipariş yüklenemedi'
})
}
return false
}
}
,
/* ===========================================================
♻️ hydrateFromLocalStorage (v5.5 — FIXED & CLEAN)
-----------------------------------------------------------
- Tek assign (double overwrite YOK)
- groupedRows hydrate edilmez
- mode ASLA set edilmez
- header + rows güvenli restore
=========================================================== */
async hydrateFromLocalStorage(orderId, log = false) {if (this.mode === 'new') {
return this.hydrateFromLocalStorageIfExists()
}
try {
const key = `bss_orderentry_data:${orderId}`
const payload = JSON.parse(localStorage.getItem(key) || 'null')
if (!payload) {
log && console.log(' hydrate → snapshot yok:', orderId)
return null
}
// 🔑 source bilgisi (mode set edilmez)
this.source = payload.source || 'local'
/* -------------------------------------------------------
MSSQL tarih helperları
-------------------------------------------------------- */
const safeDateTime = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : null
}
const safeDateOnly = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format('YYYY-MM-DD') : null
}
const safeTimeOnly = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format('HH:mm:ss') : null
}
/* -------------------------------------------------------
HEADER
-------------------------------------------------------- */
this.header = {
...(payload.header || {}),
OrderHeaderID: payload.header?.OrderHeaderID ?? orderId,
OrderNumber : payload.header?.OrderNumber ?? null
}
const h = this.header
h.CreatedDate = safeDateTime(h.CreatedDate)
h.LastUpdatedDate = safeDateTime(h.LastUpdatedDate)
h.CreditableConfirmedDate = safeDateTime(h.CreditableConfirmedDate)
h.OrderDate = safeDateOnly(h.OrderDate)
h.OrderTime = safeTimeOnly(h.OrderTime)
this.header = h
/* -------------------------------------------------------
ROWS (TEK KAYNAK)
-------------------------------------------------------- */
const orders = Array.isArray(payload.orders)
? payload.orders
: []
this.orders = orders
this.summaryRows = Array.isArray(payload.summaryRows)
? payload.summaryRows
: orders
// ❌ groupedRows hydrate edilmez (computed olmalı)
this.groupedRows = []
/* -------------------------------------------------------
SNAPSHOT ÖZET
-------------------------------------------------------- */
const output = {
type : payload.submitted === true ? 'submitted' : 'draft',
source : this.source,
headerId : orderId,
orderNumber: this.header?.OrderNumber ?? null,
rows : this.summaryRows.length,
submitted :
payload.submitted === true ||
payload.header?.IsSubmitted === true
}
log && console.log('♻️ hydrate sonuc (FIXED):', output)
return output
} catch (err) {
console.warn('⚠️ hydrateFromLocalStorage hata:', err)
return null
}
}
,
hydrateFromLocalStorageIfExists() {
try {
let raw = null
if (this.mode === 'new') {
raw = localStorage.getItem(this.getDraftKey) // ✅
}
if (this.mode === 'edit') {
const key = this.getEditKey // ✅
if (key) raw = localStorage.getItem(key)
}
if (!raw) return false
const payload = JSON.parse(raw)
this.header = payload.header || {}
this.orders = payload.orders || []
this.summaryRows = payload.summaryRows || this.orders
console.log('♻️ hydrate OK:', this.mode)
return true
} catch (err) {
console.warn('hydrateFromLocalStorageIfExists hata:', err)
return false
}
}
,
/* ===========================================================
🔀 mergeOrders (local + backend)
normalizeISO → kaldırıldı
safe MSSQL helpers eklendi
=========================================================== */
mergeOrders(local, backend, preferLocal = true) {
if (!backend && !local) return { header: {}, orders: [] }
const safeMerge = (base = {}, override = {}) => {
const out = { ...base }
for (const [k, v] of Object.entries(override || {})) {
if (v === undefined || v === null) continue
if (typeof v === 'string' && v.trim() === '') continue
out[k] = v
}
return out
}
// Header merge
const header = safeMerge(backend?.header || {}, local?.header || {})
header.OrderHeaderID =
backend?.header?.OrderHeaderID ||
local?.header?.OrderHeaderID ||
header.OrderHeaderID ||
null
const getKey = (r) =>
(r.OrderLineID ||
`${r.model || r.ItemCode}_${r.renk || r.ColorCode}_${r.renk2 || r.ColorCode2}`
).toString().toUpperCase()
const map = new Map()
// Backend satırları
for (const b of (backend?.lines || backend?.orders || [])) {
map.set(getKey(b), { ...b, _src: 'backend' })
}
// Local satırları merge et
for (const l of (local?.orders || [])) {
const k = getKey(l)
if (map.has(k)) {
const merged = safeMerge(map.get(k), l)
merged._src = preferLocal ? 'local' : 'backend'
map.set(k, merged)
} else {
map.set(k, { ...l, _src: 'local-only' })
}
}
const mergedOrders = Array.from(map.values())
console.log(`🧩 mergeOrders → ${mergedOrders.length} satır birleşti (ID:${header.OrderHeaderID})`)
// ====================================================
// 🕒 HEADER TARİHLERİNİ MSSQL FORMATINA NORMALİZE ET
// ====================================================
const safeDateTime = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : null
}
const safeDateOnly = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format("YYYY-MM-DD") : null
}
const safeTimeOnly = v => {
if (!v) return null
const d = dayjs(v)
return d.isValid() ? d.format("HH:mm:ss") : null
}
header.CreatedDate = safeDateTime(header.CreatedDate)
header.LastUpdatedDate = safeDateTime(header.LastUpdatedDate)
header.CreditableConfirmedDate = safeDateTime(header.CreditableConfirmedDate)
header.OrderDate = safeDateOnly(header.OrderDate)
header.OrderTime = safeTimeOnly(header.OrderTime)
return { header, orders: mergedOrders }
}
,
markRowSource(row) {
if (row._src === 'local-only') return '🟠 Offline'
if (row._src === 'local') return '🔵 Local'
return '⚪ Backend'
}
,
/* ===========================================================
🔄 mergeAndPersistBackendOrder (edit mode)
=========================================================== */
mergeAndPersistBackendOrder(orderId, backendPayload) {
const key = `bss_orderentry_data:${orderId}`
const localPayload = JSON.parse(localStorage.getItem(key) || 'null')
const merged = this.mergeOrders(localPayload, backendPayload, true)
localStorage.setItem(key, JSON.stringify({
...merged,
source: 'db',
mode: 'edit',
updatedAt: new Date().toISOString()
}))
console.log(`💾 mergeAndPersistBackendOrder → ${orderId} localStoragea yazıldı`)
}
,
persistLocalStorage() {
try {
if (this.preventPersist || this._uiBusy) return
const payload = {
mode: this.mode,
header: toRaw(this.header || {}),
orders: toRaw(this.orders || []),
summaryRows: toRaw(this.summaryRows || []),
updatedAt: new Date().toISOString()
}
/* ===============================
🟢 NEW MODE — GLOBAL TEK TASLAK
=============================== */
if (this.mode === 'new') {
localStorage.setItem(this.getDraftKey, JSON.stringify(payload))
// 🔒 sadece aktif new header bilgisi
this.setActiveNewHeader?.(this.header?.OrderHeaderID)
return
}
/* ===============================
🔵 EDIT MODE — ID BAZLI
=============================== */
if (this.mode === 'edit') {
const key = this.getEditKey
if (!key) return
localStorage.setItem(key, JSON.stringify(payload))
}
} catch (e) {
console.warn('persistLocalStorage error:', e)
}
}
,
clearEditSnapshotIfExists() {
if (this.mode !== 'edit') return
const key = this.getEditKey // ✅
if (!key) return
localStorage.removeItem(key)
console.log('🧹 EDIT snapshot silindi:', key)
}
,/* ===========================================================
🧠 _persistFingerprint — kritik stateleri tek stringe indirger
- X3: orders+header yetmez → mode, summaryRows, id/no, mapler dahil
=========================================================== */
_persistFingerprint() {
// 🔹 orders: çok büyürse pahalı olabilir ama snapshot tutarlılığı için önemli
// (istersen burada sadece length + rowKey listesi gibi optimize ederiz)
const ordersSnap = JSON.stringify(this.orders || [])
// 🔹 header: sadece kritik alanları al (tam header yerine daha stabil)
const h = this.header || {}
const headerSnap = JSON.stringify({
OrderHeaderID: h.OrderHeaderID || '',
OrderNumber: h.OrderNumber || '',
CurrAccCode: h.CurrAccCode || '',
DocCurrencyCode: h.DocCurrencyCode || '',
ExchangeRate: h.ExchangeRate ?? null
})
// 🔹 summaryRows: hash yerine şimdilik “length + rowKey listesi” (hafif + etkili)
const sr = Array.isArray(this.summaryRows) ? this.summaryRows : []
const summaryMeta = JSON.stringify({
len: sr.length,
keys: sr.map(r => this.getRowKey?.(r) || r?.key || r?.id || '').filter(Boolean)
})
// 🔹 comboLineIds / lineIdMap gibi kritik mapler
// (sende hangisi varsa onu otomatik topluyoruz)
const mapSnap = JSON.stringify({
lineIdMap: this.lineIdMap || null,
comboLineIds: this.comboLineIds || null,
comboLineIdMap: this.comboLineIdMap || null,
comboLineIdSet: this.comboLineIdSet ? Array.from(this.comboLineIdSet) : null
})
// 🔹 mode
const modeSnap = String(this.mode || 'new')
// ✅ Tek fingerprint
return `${modeSnap}|${headerSnap}|${summaryMeta}|${mapSnap}|${ordersSnap}`
}
,
/* ===========================================================
🕒 _safePersistDebounced — snapshot değişmediği sürece yazmaz (X3)
- fingerprint: mode + header(id/no) + summaryRows meta + lineIdMap/combo + orders
=========================================================== */
_safePersistDebounced(delay = 1200) {
clearTimeout(this._persistTimeout)
this._persistTimeout = setTimeout(() => {
try {
// ✅ Persist guardları (varsa)
if (this.preventPersist) return
if (this._uiBusy) return
const fp = this._persistFingerprint()
if (fp === this._lastPersistFingerprint) {
return
}
this._lastPersistFingerprint = fp
this.persistLocalStorage()
console.log(`🕒 Otomatik LocalStorage senkron (${this.orders?.length || 0} satır).`)
} catch (err) {
console.warn('⚠️ Debounce persist hata:', err)
}
}, delay)
}
,
/* ===========================================================
💰 fetchMinPrice — model/pb için min fiyat
=========================================================== */
async fetchMinPrice(model, currency, $q) {
try {
const res = await api.get('/min-price', {
params: { model, currency }
})
const data = res?.data || {}
console.log('💰 [store.fetchMinPrice] yanıt:', data)
return {
price: Number(data.price || 0),
rateToTRY: Number(data.rateToTRY || 1),
priceTRY: Number(data.priceTRY || 0)
}
} catch (err) {
console.error('❌ [store.fetchMinPrice] Min fiyat alınamadı:', err)
$q?.notify?.({
type: 'warning',
message: 'Min. fiyat bilgisi alınamadı, kontrol atlandı ⚠️',
position: 'top-right'
})
return { price: 0, rateToTRY: 1, priceTRY: 0 }
}
}
,
applyCurrencyToLines(newPB) {
if (!newPB) return
// 🔹 Header
if (this.header) {
this.header.DocCurrencyCode = newPB
this.header.PriceCurrencyCode = newPB
}
// 🔹 Lines
if (Array.isArray(this.orders)) {
this.orders = this.orders.map(r => ({
...r,
pb: newPB,
DocCurrencyCode: newPB,
PriceCurrencyCode: newPB
}))
}
// 🔹 Summary
if (Array.isArray(this.summaryRows)) {
this.summaryRows = this.summaryRows.map(r => ({
...r,
pb: newPB,
DocCurrencyCode: newPB,
PriceCurrencyCode: newPB
}))
}
// ❗ totalAmount SET ETME
// ✔️ TEK MERKEZ
this.updateHeaderTotals?.()
}
,
/* ===========================================================
💠 HEADER SET & CURRENCY PROPAGATION
=========================================================== */
setHeaderFields(fields, opts = {}) {
const {
applyCurrencyToLines = false,
immediatePersist = false
} = opts
// 1⃣ HEADER
this.header = {
...(this.header || {}),
...fields
}
// 2⃣ SATIRLARA GERÇEKTEN YAY
if (applyCurrencyToLines && Array.isArray(this.summaryRows)) {
this.summaryRows = this.summaryRows.map(r => ({
...r,
pb: fields.DocCurrencyCode ?? r.pb,
DocCurrencyCode: fields.DocCurrencyCode ?? r.DocCurrencyCode,
PriceCurrencyCode: fields.PriceCurrencyCode ?? fields.DocCurrencyCode ?? r.PriceCurrencyCode
}))
}
// 3⃣ STORE ORDERS REFERANSI
this.orders = [...this.summaryRows]
// 4⃣ PERSIST
if (immediatePersist) {
this.persistLocalStorage('header-change')
}
}
,
applyHeaderCurrencyToOrders() {
if (!Array.isArray(this.orders)) return
const doc = this.header?.DocCurrencyCode ?? null
const prc = this.header?.PriceCurrencyCode ?? null
const rate = this.header?.PriceExchangeRate ?? null
let cnt = 0
for (const r of this.orders) {
if (doc) r.DocCurrencyCode = doc
if (prc) r.PriceCurrencyCode = prc
if (rate != null) r.PriceExchangeRate = rate
cnt++
}
console.log(`💱 ${cnt} satırda PB güncellendi → Doc:${doc} Price:${prc} Rate:${rate}`)
}
,/* ===========================================================
📸 saveSnapshot — küçük debug snapshot
=========================================================== */
saveSnapshot(tag = 'snapshot') {
try {
const id = this.header?.OrderHeaderID
if (!id) return
const key = `bss_orderentry_snapshot:${id}`
const snap = {
tag,
mode: this.mode,
orders: toRaw(this.orders || []),
header: toRaw(this.header || {}),
savedAt: dayjs().toISOString()
}
localStorage.setItem(key, JSON.stringify(snap))
console.log(`📸 Snapshot kaydedildi [${key}]`)
} catch (err) {
console.warn('⚠️ saveSnapshot hata:', err)
}
}
,
/* ===========================================================
♻️ loadFromStorage — eski generic persist için
=========================================================== */
loadFromStorage(force = false) {
try {
const raw = localStorage.getItem(this.getPersistKey)
if (!raw) {
console.info(' LocalStorage boş, grid başlatılmadı.')
return false
}
if (!force && this.mode === 'edit') {
console.info('⚠️ Edit modda local restore atlandı (force=false).')
return false
}
const data = JSON.parse(raw)
this.orders = Array.isArray(data.orders) ? data.orders : []
this.header = data.header || {}
this.currentOrderId = data.currentOrderId || null
this.selectedCustomer = data.selectedCustomer || null
// 🔧 Temiz ID
this.header.OrderHeaderID = data.header?.OrderHeaderID || null
this.mode = data.mode || 'new'
this.lastSavedAt = data.savedAt || null
console.log(`♻️ Storage yüklendi • txn:${this.header.OrderHeaderID} (${this.orders.length} satır)`)
// Header PB -> satırlara
this.applyHeaderCurrencyToOrders()
this._safePersistDebounced(200)
return data
} catch (err) {
console.warn('⚠️ localStorage okuma hatası:', err)
return false
}
}
,
clearStorage() {
try {
localStorage.removeItem(this.getPersistKey)
console.log(`🗑️ LocalStorage temizlendi [${this.getPersistKey}]`)
} catch (err) {
console.warn('⚠️ clearStorage hatası:', err)
}
}
,
clearNewDraft() {
localStorage.removeItem(this.getDraftKey) // ✅
localStorage.removeItem('bss_last_txn')
console.log('🧹 NEW taslak temizlendi')
}
,
// ===========================================================
// 🔹 isSameCombo — STORE LEVEL (TEK KAYNAK)
// - model ZORUNLU eşleşir
// - renk / renk2 boşsa → joker
// ===========================================================
isSameCombo(a, b) {
if (!a || !b) return false
const n = v => (v == null ? '' : String(v).trim().toUpperCase())
const A = { model: n(a.model), renk: n(a.renk), renk2: n(a.renk2) }
const B = { model: n(b.model), renk: n(b.renk), renk2: n(b.renk2) }
if (!A.model || !B.model) return false
const renkOk = (A.renk === B.renk) || !A.renk || !B.renk
const renk2Ok = (A.renk2 === B.renk2) || !A.renk2 || !B.renk2
return A.model === B.model && renkOk && renk2Ok
},
// ===========================================================
// 🔹 saveOrUpdateRowUnified (v6.6 — COMBO SAFE + FIXED STOCK+PRICE + UI)
// - v6.5 korunur (stok+min fiyat + this.loadProductSizes)
// - ✅ NEW MODE: dupIdx artık _deleteSignal satırlarını BAŞTAN hariç tutar
// - EDIT MODE: sameCombo → update, combo değişti → delete + insert (korundu)
// - lineIdMap koruması korunur
// ===========================================================
async saveOrUpdateRowUnified({
form,
recalcVat = null,
resetEditor = null,
stockMap = null,
loadProductSizes = null,
$q = null
}) {
try {
console.log('🔥 saveOrUpdateRowUnified v6.6', {
model: form?.model,
mode: this.mode,
editingKey: this.editingKey
})
const getKey =
typeof this.getRowKey === 'function'
? this.getRowKey
: (r => r?.clientKey || r?.id || r?.OrderLineID)
const rows = Array.isArray(this.summaryRows)
? [...this.summaryRows]
: []
/* =======================================================
1⃣ ZORUNLU KONTROLLER
======================================================= */
if (!form?.model) {
$q?.notify?.({ type: 'warning', message: 'Model seçiniz' })
return false
}
if (!form.pb) {
form.pb = this.header?.DocCurrencyCode || 'USD'
}
/* =======================================================
2⃣ STOK KONTROLÜ (FIXED)
- stok guarddan önce this.loadProductSizes(form,true,$q)
- opsiyonel callback loadProductSizes(true)
- tek dialog + doğru await
======================================================= */
// ✅ store fonksiyonu
try {
if (typeof this.loadProductSizes === 'function') {
await this.loadProductSizes(form, true, $q)
}
} catch (err) {
console.warn('⚠ this.loadProductSizes hata:', err)
}
// ✅ dışarıdan callback geldiyse
try {
if (typeof loadProductSizes === 'function') {
await loadProductSizes(true)
}
} catch (err) {
console.warn('⚠ loadProductSizes hata:', err)
}
const stockMapLocal = stockMap?.value || stockMap || {}
const bedenLabels = form.bedenLabels || []
const bedenValues = form.bedenler || []
const overLimit = []
for (let i = 0; i < bedenLabels.length; i++) {
const lbl = String(bedenLabels[i] ?? '').trim()
const stok = Number(stockMapLocal?.[lbl] ?? 0)
const girilen = Number(bedenValues?.[i] ?? 0)
if (stok > 0 && girilen > stok) {
overLimit.push({ beden: lbl, stok, girilen })
}
}
if (overLimit.length && $q) {
const msg = overLimit
.map(x => `• <b>${x.beden}</b>: ${x.girilen} (Stok: ${x.stok})`)
.join('<br>')
const stokOK = await new Promise(resolve => {
$q.dialog({
title: 'Stok Uyarısı',
message: `Bazı bedenlerde stoktan fazla giriş yaptınız:<br><br>${msg}`,
html: true,
ok: { label: 'Devam', color: 'primary' },
cancel: { label: 'İptal', color: 'negative' }
})
.onOk(() => resolve(true))
.onCancel(() => resolve(false))
.onDismiss(() => resolve(false))
})
if (!stokOK) return false
}
/* =======================================================
3⃣ FİYAT (MIN) KONTROLÜ (FIXED)
======================================================= */
let fiyatOK = true
try {
let minFiyat = 0
if (typeof this.fetchMinPrice === 'function') {
const p = await this.fetchMinPrice(form.model, form.pb, $q)
minFiyat = Number(p?.price || 0)
} else if (Number(form.minFiyat || 0) > 0) {
minFiyat = Number(form.minFiyat)
}
const girilen = Number(form.fiyat || 0)
if (minFiyat > 0 && girilen > 0 && girilen < minFiyat && $q) {
fiyatOK = await new Promise(resolve => {
$q.dialog({
title: 'Fiyat Uyarısı',
message:
`<b>Min. Fiyat:</b> ${minFiyat} ${form.pb}<br>` +
`<b>Girdiğiniz:</b> ${girilen} ${form.pb}`,
html: true,
ok: { label: 'Devam', color: 'primary' },
cancel: { label: 'İptal', color: 'negative' }
})
.onOk(() => resolve(true))
.onCancel(() => resolve(false))
.onDismiss(() => resolve(false))
})
}
} catch (err) {
console.warn('⚠ Min fiyat hata:', err)
}
if (!fiyatOK) return false
/* =======================================================
4⃣ TOPLAM HESABI
======================================================= */
const adet = (form.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
form.adet = adet
form.tutar = Number((adet * Number(form.fiyat || 0)).toFixed(2))
const newRow = toSummaryRowFromForm(form)
/* =======================================================
5⃣ EDIT MODE (editingKey ZORUNLU)
======================================================= */
if (this.editingKey) {
const idx = rows.findIndex(r => getKey(r) === this.editingKey)
if (idx === -1) {
this.editingKey = null
resetEditor?.(true)
return false
}
const prev = rows[idx]
if (this.isRowLocked?.(prev)) {
$q?.notify?.({ type: 'warning', message: 'Satır kapalı' })
this.editingKey = null
resetEditor?.(true)
return false
}
// ✅ kritik: store-level
const sameCombo = this.isSameCombo(prev, newRow)
const preservedLineIdMap =
(prev?.lineIdMap && typeof prev.lineIdMap === 'object')
? { ...prev.lineIdMap }
: (newRow?.lineIdMap && typeof newRow.lineIdMap === 'object')
? { ...newRow.lineIdMap }
: {}
/* ===== SAME COMBO → UPDATE ===== */
if (sameCombo) {
rows[idx] = {
...prev,
...newRow,
id: prev.id,
OrderLineID: prev.OrderLineID || null,
lineIdMap: preservedLineIdMap
}
this.summaryRows = rows
this.orders = rows
this.updateHeaderTotals?.()
this.persistLocalStorage?.()
this.editingKey = null
resetEditor?.(true)
recalcVat?.()
$q?.notify?.({ type: 'positive', message: 'Satır güncellendi' })
return true
}
/* ===== COMBO CHANGED → DELETE + INSERT ===== */
const grpKey =
prev?.grpKey ||
Object.keys(prev?.bedenMap || {})[0] ||
'tak'
const emptyMap = {}
const srcMap =
(prev?.bedenMap?.[grpKey] && typeof prev.bedenMap[grpKey] === 'object')
? prev.bedenMap[grpKey]
: (preservedLineIdMap && typeof preservedLineIdMap === 'object')
? preservedLineIdMap
: null
if (srcMap) {
for (const beden of Object.keys(srcMap)) emptyMap[beden] = 0
} else {
emptyMap['STD'] = 0
}
const deleteRow = {
...prev,
id: `DEL::${prev.id || prev.OrderLineID || crypto.randomUUID()}`,
_deleteSignal: true,
adet: 0,
Qty1: 0,
tutar: 0,
ComboKey: '',
OrderLineID: prev.OrderLineID || null,
grpKey,
bedenMap: { [grpKey]: emptyMap },
lineIdMap: preservedLineIdMap,
comboLineIds: { ...(prev.comboLineIds || {}) }
}
const insertedRow = {
...newRow,
id: crypto.randomUUID(),
OrderLineID: null,
lineIdMap: {}
}
rows.splice(idx, 1, insertedRow)
this.summaryRows = rows
this.orders = [...rows, deleteRow]
this.updateHeaderTotals?.()
this.persistLocalStorage?.()
this.editingKey = null
resetEditor?.(true)
recalcVat?.()
$q?.notify?.({ type: 'positive', message: 'Kombinasyon değişti' })
return true
}
/* =======================================================
6⃣ NEW MODE (MERGE / INSERT) — COMBO SAFE
- aynı combo → bedenMap merge (satır sayısı artmaz)
- farklı combo → yeni satır
- ✅ FIX: _deleteSignal satırlarını dup aramasında hariç tut
======================================================= */
const dupIdx = rows.findIndex(r =>
!r?._deleteSignal &&
this.isSameCombo(r, newRow)
)
// helper: bedenMap çıkar (gruplu ya da düz)
const extractMap = (row) => {
const grpKey =
row?.grpKey ||
Object.keys(row?.bedenMap || {})[0] ||
'GENEL'
const grouped = row?.bedenMap?.[grpKey]
const flat = (row?.bedenMap && typeof row.bedenMap === 'object' && !grouped)
? row.bedenMap
: null
return { grpKey, map: (grouped || flat || {}) }
}
if (dupIdx !== -1) {
const prev = rows[dupIdx]
// delete satırına merge yapma (ek güvenlik)
if (prev?._deleteSignal !== true) {
const { grpKey: prevGrp, map: prevMap } = extractMap(prev)
const { grpKey: newGrp, map: newMap } = extractMap(newRow)
// hangi grpKey kullanılacak?
const grpKey = newRow?.grpKey || prevGrp || newGrp || 'GENEL'
// MERGE: bedenleri topluyoruz (override değil)
const merged = { ...(prevMap || {}) }
for (const [k, v] of Object.entries(newMap || {})) {
const beden = (k == null || String(k).trim() === '') ? ' ' : String(k).trim()
merged[beden] = Number(merged[beden] || 0) + Number(v || 0)
}
// toplam adet/tutar recalc
const totalAdet = Object.values(merged).reduce((a, b) => a + Number(b || 0), 0)
const price = Number(newRow?.fiyat ?? prev?.fiyat ?? 0)
const totalTutar = Number((totalAdet * price).toFixed(2))
rows[dupIdx] = {
...prev,
...newRow,
// kritik korumalar
id: prev.id,
OrderLineID: prev.OrderLineID || null,
lineIdMap: { ...(prev.lineIdMap || {}) },
// MERGED bedenMap
grpKey,
bedenMap: { [grpKey]: merged },
// adet/tutar
adet: totalAdet,
tutar: totalTutar,
updatedAt: dayjs().toISOString()
}
this.summaryRows = rows
this.orders = rows
this.updateHeaderTotals?.()
this.persistLocalStorage?.()
resetEditor?.(true)
recalcVat?.()
$q?.notify?.({ type: 'positive', message: 'Aynı kombinasyon bulundu, bedenler birleştirildi' })
return true
}
}
// dup yoksa (veya dup delete satırıydı) → yeni satır
rows.push({
...newRow,
id: newRow.id || crypto.randomUUID(),
OrderLineID: null,
lineIdMap: { ...(newRow.lineIdMap || {}) }
})
this.summaryRows = rows
this.orders = rows
this.updateHeaderTotals?.()
this.persistLocalStorage?.()
resetEditor?.(true)
recalcVat?.()
$q?.notify?.({ type: 'positive', message: 'Yeni satır eklendi' })
return true
} catch (err) {
console.error('❌ saveOrUpdateRowUnified:', err)
$q?.notify?.({ type: 'negative', message: 'Satır kaydı başarısız' })
return false
}
}
,
/* ===========================================================
🔄 setTransaction — yeni transaction ID set et
=========================================================== */
setTransaction(id, autoResume = true) {
if (!id) return
// 🔧 temiz ID
this.header.OrderHeaderID = id
localStorage.setItem('bss_last_txn', id)
console.log('🔄 Transaction değiştirildi:', id)
if (autoResume) {
const hasData = Array.isArray(this.orders) && this.orders.length > 0
if (!hasData) {
const ok = this.hydrateFromLocalStorage(id,true)
if (ok) console.info('📦 Local kayıt geri yüklendi (boş grid için).')
} else {
console.log('🚫 Grid dolu, auto-resume atlandı (mevcut satırlar korundu).')
}
}
}
,
/* ===========================================================
🧹 clearTransaction — sadece NEW MODE taslaklarını temizler
=========================================================== */
clearTransaction() {
try {
const id = this.header?.OrderHeaderID
if (id) {
localStorage.removeItem(`bss_orderentry_data:${id}`)
}
this.orders = []
this.summaryRows = []
this.groupedRows = []
this.header = {}
this.mode = 'new'
localStorage.removeItem('bss_last_txn')
console.log('🧹 Transaction temizlendi')
} catch (err) {
console.warn('⚠️ clearTransaction hata:', err)
}
}
,
// =======================================================
// 🔒 KİLİT KONTROLÜ — Sadece EDIT modunda, backend satırı
// =======================================================
isRowLocked(row) {
if (!row) return false
// Sadece edit modunda,
// ve backend'den gelen gerçek OrderLineID varsa,
// ve IsClosed=1 ise satır kilitli
return (
this.mode === 'edit' &&
!!row.OrderLineID &&
row.isClosed === true
)
},
findExistingIndexByForm(form) {
return this.orders.findIndex(r => this.isSameCombo(r, form))
},
addRow(row) {
if (!row) return
const existingIndex = this.orders.findIndex(r => {
const sameId = r.id && row.id && r.id === row.id
const sameCombo = this.isSameCombo(r, row)
return sameId || sameCombo
})
if (existingIndex !== -1) {
const old = this.orders[existingIndex]
this.orders[existingIndex] = {
...old,
adet: Number(row.adet ?? old.adet ?? 0),
fiyat: Number(row.fiyat ?? old.fiyat ?? 0),
tutar: Number(row.fiyat ?? old.fiyat ?? 0) * Number(row.adet ?? old.adet ?? 0),
ItemDim1Code: row.ItemDim1Code || old.ItemDim1Code,
aciklama: row.aciklama || old.aciklama,
updatedAt: dayjs().toISOString()
}
console.log(`⚠️ Aynı kombinasyon bulundu, satır güncellendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`)
} else {
this.orders.push(toRaw(row))
console.log(` Yeni kombinasyon eklendi: ${row.model} ${row.renk || ''} ${row.renk2 || ''}`)
}
this.persistLocalStorage()
this.saveSnapshot('after-add')
},
updateRow(index, patch) {
if (index < 0 || index >= this.orders.length) return
this.orders[index] = {
...this.orders[index],
...toRaw(patch),
updatedAt: dayjs().toISOString()
}
this.persistLocalStorage()
this.saveSnapshot('after-update')
console.log(`✏️ Satır güncellendi (store): #${index}`)
},
removeRow(index) {
if (index < 0 || index >= this.orders.length) return
const removed = this.orders.splice(index, 1)
if (Array.isArray(this.summaryRows)) {
this.summaryRows.splice(index, 1)
}
this.persistLocalStorage()
this.saveSnapshot('after-remove')
console.log(`🗑️ Satır silindi: ${removed[0]?.model || '(model yok)'}`)
},
removeSelectedRow(row, $q = null) {
if (!row) return
// 1) Kilitli satır silinemez
if (this.isRowLocked(row)) {
$q?.notify?.({
type: 'warning',
message: '🔒 Bu satır (IsClosed=1) kapatılmış. Silinemez.'
})
return false
}
// 2) Kullanıcıya onay sor
return new Promise(resolve => {
$q?.dialog({
title: 'Satır Sil',
message: `${row.model} / ${row.renk} / ${row.renk2} kombinasyonu silinsin mi?`,
ok: { label: 'Evet', color: 'negative' },
cancel: { label: 'Vazgeç' }
})
.onOk(() => {
this.removeRowInternal(row)
resolve(true)
})
.onCancel(() => resolve(false))
})
}
,
removeRowInternal(row) {
if (!row) return false
// 1⃣ Kilit kontrolü
if (this.isRowLocked(row)) {
console.warn('🔒 Kilitli satır silinemez.')
return false
}
const getKey =
typeof this.getRowKey === 'function'
? this.getRowKey
: (r => r?.clientKey || r?.id || r?.OrderLineID)
const rowKey = getKey(row)
if (!rowKey) return false
const idx = this.summaryRows.findIndex(r => getKey(r) === rowKey)
if (idx === -1) return false
console.log('🗑️ X2 removeRowInternal →', row)
// 🔐 UI BUSY
this._uiBusy = true
this.preventPersist = true
try {
// 2⃣ UIdan kaldır
this.summaryRows.splice(idx, 1)
// orders = UI satırları (temiz kopya)
this.orders = [...this.summaryRows]
// 3⃣ EDIT MODE → DELETE SİNYALİ
if (this.mode === 'edit') {
const grpKey =
row.grpKey ||
Object.keys(row.bedenMap || {})[0] ||
'tak'
// ✅ lineIdMap referansı (varsa)
const lineIdMap =
(row.lineIdMap && typeof row.lineIdMap === 'object')
? { ...row.lineIdMap }
: {}
const emptyMap = {}
// Öncelik: bedenMap[grpKey] → lineIdMap → fallback
if (row.bedenMap && row.bedenMap[grpKey]) {
for (const beden of Object.keys(row.bedenMap[grpKey] || {})) {
emptyMap[beden] = 0
}
} else if (Object.keys(lineIdMap).length) {
for (const beden of Object.keys(lineIdMap)) {
emptyMap[beden] = 0
}
} else {
emptyMap['STD'] = 0
}
const deleteSignalRow = {
...row,
// 🔴 UI KEY
id: `DEL::${row.id || row.OrderLineID || crypto.randomUUID()}`,
// 🔴 BACKEND DELETE SIGNAL
adet: 0,
Qty1: 0,
tutar: 0,
// 🔴 CRITICAL: duplicate guard'a girmesin
ComboKey: '',
// 🔴 legacy tekil alan (varsa kalsın)
OrderLineID: row.OrderLineID || null,
// ✅ CRITICAL
grpKey,
bedenMap: { [grpKey]: emptyMap },
lineIdMap,
comboLineIds: { ...(row.comboLineIds || {}) },
_deleteSignal: true
}
console.log('📡 DELETE sinyali üretildi:', deleteSignalRow)
this.orders.push(deleteSignalRow)
}
// 4⃣ Totals (persist YOK)
this.updateHeaderTotals?.()
} finally {
// 🔓 GUARD KAPAT
this.preventPersist = false
this._uiBusy = false
}
// 5⃣ TEK VE KONTROLLÜ persist
this.persistLocalStorage()
return true
}
,
/* ===========================================================
📦 normalizeOrderLines (v9 — lineIdMap FIXED + AKSBİR SAFE)
-----------------------------------------------------------
✔ grpKey SADECE burada set edilir
✔ detectBedenGroup SADECE storeda kullanılır
✔ aksbir → ' ' bedeni = GERÇEK adet
✔ backend satırlarında BEDEN → OrderLineID mapi üretilir
=========================================================== */
normalizeOrderLines(lines, pbFallback = 'USD') {
if (!Array.isArray(lines)) return []
const merged = Object.create(null)
const makeBaseKey = (model, renk, renk2) =>
`${model || ''}||${renk || ''}||${renk2 || ''}`
for (const raw of lines) {
if (!raw) continue
const isClosed =
raw.IsClosed === true ||
raw.isClosed === true ||
raw.IsClosed?.Bool === true
/* =======================================================
1⃣ UI / SNAPSHOT KAYNAKLI SATIR
-------------------------------------------------------
✔ ComboKey YOK
✔ Sadece model / renk / renk2 bazında gruplanır
======================================================= */
if (raw.bedenMap && Object.keys(raw.bedenMap).length) {
const model = (raw.model || raw.ItemCode || '').trim()
const renk = (raw.renk || raw.ColorCode || '').trim()
const renk2 = (raw.renk2 || raw.ItemDim2Code || '').trim()
// ❗ BEDEN YOK → bu SADECE üst seviye grup anahtarı
const modelKey = `${model}||${renk}||${renk2}`
const grpKey = raw.grpKey || 'tak'
const srcMap = raw.bedenMap[grpKey] || {}
const adet = Object.values(srcMap).reduce((a, b) => a + (Number(b) || 0), 0)
const fiyat = Number(raw.fiyat || 0)
const pb = raw.pb || raw.DocCurrencyCode || pbFallback
const tutar = Number(raw.tutar ?? adet * fiyat)
merged[modelKey] ??= []
merged[modelKey].push({
...raw,
grpKey,
bedenMap: { [grpKey]: { ...srcMap } },
adet,
fiyat,
pb,
tutar,
isClosed
})
continue
}
/* =======================================================
2⃣ BACKEND / LEGACY SATIR (FIXED)
-------------------------------------------------------
✔ ComboKey YOK
✔ Sadece model / renk / renk2 bazlı gruplanır
✔ BEDEN sadece bedenMap + lineIdMap için kullanılır
======================================================= */
const model = (raw.Model || raw.ItemCode || '').trim()
const renk = (raw.ColorCode || '').trim()
const renk2 = (raw.ItemDim2Code || '').trim()
// ❗ BEDEN HARİÇ — üst seviye grup anahtarı
const modelKey = `${model}||${renk}||${renk2}`
merged[modelKey] ??= []
const beden = (
raw.ItemDim1Code == null || String(raw.ItemDim1Code).trim() === ''
? ' '
: String(raw.ItemDim1Code).trim().toUpperCase()
)
const qty = Number(raw.Qty1 || raw.Qty || 0)
let entry = merged[modelKey][0]
if (!entry) {
entry = {
id: raw.OrderLineID || crypto.randomUUID(),
model,
renk,
renk2,
urunAnaGrubu: raw.UrunAnaGrubu || 'GENEL',
urunAltGrubu: raw.UrunAltGrubu || '',
kategori: raw.Kategori || '',
aciklama: raw.LineDescription || '',
fiyat: Number(raw.Price || 0),
pb: raw.DocCurrencyCode || pbFallback,
__tmpMap: {}, // beden → qty
lineIdMap: {}, // beden → OrderLineID
adet: 0,
tutar: 0,
terminTarihi: raw.DeliveryDate || null,
isClosed
}
merged[modelKey].push(entry)
}
/* -------------------------------------------------------
🔑 BEDEN → OrderLineID (DETERMINISTIC & SAFE)
-------------------------------------------------------- */
const rawLineId =
raw.OrderLineID ||
raw.OrderLineId ||
raw.orderLineID ||
null
if (rawLineId) {
entry.lineIdMap[beden] = String(rawLineId)
}
if (qty > 0) {
entry.__tmpMap[beden] = (entry.__tmpMap[beden] || 0) + qty
entry.adet += qty
entry.tutar += qty * entry.fiyat
}
}
/* =======================================================
3⃣ FINAL — grpKey KESİN + AKSBİR FIX
======================================================= */
const out = []
for (const rows of Object.values(merged)) {
for (const row of rows) {
if (!row.__tmpMap) {
out.push(row)
continue
}
const bedenList = Object.keys(row.__tmpMap)
// 🔒 TEK VE KESİN KARAR
const grpKey = detectBedenGroup(
bedenList,
row.urunAnaGrubu,
row.kategori
)
row.grpKey = grpKey
row.bedenMap = { [grpKey]: { ...row.__tmpMap } }
/* ===================================================
🔒 AKSBİR — BOŞLUK BEDEN GERÇEK ADETİ ALIR
❗ STDye dönme YOK
❗ 0 yazma YOK
=================================================== */
if (grpKey === 'aksbir') {
row.bedenMap[grpKey] ??= {}
row.bedenMap[grpKey][' '] = Number(row.adet || 0)
}
delete row.__tmpMap
out.push(row)
}
}
console.log(
`📦 normalizeOrderLines (v9 + lineIdMap) → ${out.length} satır`
)
return out
}
,
/**
* ===========================================================
* loadProductSizes — FINAL v4.2 (EDITOR SAFE)
* -----------------------------------------------------------
* ✔ grpKey SADECE form.grpKey
* ✔ schemaMap TEK OTORİTE
* ✔ edit modda BEDEN LABEL DOKUNULMAZ
* ✔ ' ' (boş beden) korunur
* ===========================================================
*/
async loadProductSizes(form, forceRefresh = false, $q = null) {
if (!form?.model) return
const store = this
const prevBusy = !!store._uiBusy
const prevPrevent = !!store.preventPersist
store._uiBusy = true
store.preventPersist = true
try {
const grpKey = form.grpKey
if (!grpKey) {
console.warn('⛔ loadProductSizes iptal → grpKey yok')
return
}
const colorKey = form.renk || 'nocolor'
const color2Key = form.renk2 || 'no2color'
const cacheKey = `${form.model}_${colorKey}_${color2Key}_${grpKey}`
/* =======================================================
♻️ CACHE (LABEL DOKUNMADAN)
======================================================= */
if (!forceRefresh && sizeCache.value?.[cacheKey]) {
const cached = sizeCache.value[cacheKey]
bedenStock.value = [...cached.stockArray]
stockMap.value = { ...cached.stockMap }
console.log(`♻️ loadProductSizes CACHE → ${grpKey}`)
return
}
/* =======================================================
📡 API
======================================================= */
const params = { code: form.model }
if (form.renk) params.color = form.renk
if (form.renk2) params.color2 = form.renk2
const res = await api.get('/product-colorsize', { params })
const data = Array.isArray(res?.data) ? res.data : []
if (!data.length) {
bedenStock.value = []
stockMap.value = {}
return
}
/* =======================================================
📦 STOK MAP (' ' KORUNUR)
======================================================= */
const apiStockMap = {}
for (const x of data) {
const key =
x.item_dim1_code === null || x.item_dim1_code === ''
? ' '
: String(x.item_dim1_code)
apiStockMap[key] = Number(x.kullanilabilir_envanter ?? 0)
}
const finalStockMap = {}
for (const lbl of form.bedenLabels) {
finalStockMap[lbl] = apiStockMap[lbl] ?? 0
}
stockMap.value = { ...finalStockMap }
bedenStock.value = Object.entries(stockMap.value).map(
([beden, stok]) => ({ beden, stok })
)
/* =======================================================
💾 CACHE
======================================================= */
sizeCache.value[cacheKey] = {
labels: [...form.bedenLabels],
stockArray: [...bedenStock.value],
stockMap: { ...stockMap.value }
}
console.log(`✅ loadProductSizes FINAL v4.2 → ${grpKey}`)
} catch (err) {
console.error('❌ loadProductSizes hata:', err)
$q?.notify?.({ type: 'negative', message: 'Beden / stok alınamadı' })
} finally {
store._uiBusy = prevBusy
store.preventPersist = prevPrevent
console.log('🧩 Editor beden hydrate', {
grpKey: form.grpKey,
labels: form.bedenLabels,
values: form.bedenler
})
}
}
,
// =======================================================
// 🔸 TOPLAM HESAPLAMA (store içi) — X3 SAFE
// -------------------------------------------------------
// ✔ f.adet / f.tutar hesaplanır
// ✔ store.totalAmount ASLA set edilmez
// ✔ gerçek toplam → header.TotalAmount
// =======================================================
updateTotals(f) {
// 1⃣ Satır adet
f.adet = (f.bedenler || []).reduce(
(a, b) => a + Number(b || 0),
0
)
// 2⃣ Satır tutar
const fiyat = Number(f.fiyat) || 0
f.tutar = Number((f.adet * fiyat).toFixed(2))
// 3⃣ Header toplam (tek gerçek state)
if (this.header) {
const total = (this.summaryRows || []).reduce(
(sum, r) => sum + Number(r?.tutar || 0),
0
)
this.header.TotalAmount = Number(total.toFixed(2))
}
return f
}
,
// =======================================================
// 🔸 GRUP ANAHTARI TESPİTİ
// =======================================================
activeGroupKeyForRow(row) {
const g = (row?.urunAnaGrubu || '').toUpperCase()
if (g.includes('TAKIM')) return 'tak'
if (g.includes('PANTOLON')) return 'pan'
if (g.includes('GÖMLEK')) return 'gom'
if (g.includes('AYAKKABI')) return 'ayk'
if (g.includes('YAŞ')) return 'yas'
return 'tak'
},
/* =======================================================
🔹 MODE YÖNETİMİ — new / edit arası geçiş
======================================================= */
setMode(mode) {
if (!['new', 'edit', 'view'].includes(mode)) {
console.warn('⚠️ Geçersiz mode:', mode)
return
}
this.mode = mode
console.log(`🧭 Order mode set edildi → ${mode}`)
}
,
/* ===========================================================
🟦 submitAllReal (v12.1c — FINAL / CLEAN + PRE-VALIDATE)
-----------------------------------------------------------
✔ NEW → INSERT, EDIT → UPDATE (tek karar noktası)
✔ Controlled submit → route guard SUSAR
✔ Snapshot temizliği route öncesi
✔ Kaydet → edit replace → backend reload
✔ Listeye giderken guard popup 1 kez bypass
✔ ✅ PRE-VALIDATE → prItemVariant olmayan kombinasyonlar kaydı DURDURUR
=========================================================== */
async submitAllReal($q, router, form, summaryRows, productCache) {
let serverOrderId = null
let serverOrderNo = null
try {
this.loading = true
// 🔒 Kontrollü submit → route leave guard susar
this.isControlledSubmit = true
const isNew = this.mode === 'new'
const { header, lines } = this.buildFinalOrderJson()
// =======================================================
// 🧾 DEBUG — FRONTEND → BACKEND GİDEN PAYLOAD
// =======================================================
console.groupCollapsed(
`%c📤 ORDER PAYLOAD (${this.mode})`,
'color:#c9a873;font-weight:bold'
)
console.log('HEADER:', JSON.parse(JSON.stringify(header)))
lines.forEach((l, i) => {
console.log(`LINE[${i}]`, {
OrderLineID: l.OrderLineID,
ClientKey: l.ClientKey,
ItemCode: l.ItemCode,
ColorCode: l.ColorCode,
ItemDim1Code: l.ItemDim1Code,
ItemDim2Code: l.ItemDim2Code,
ItemDim3Code: l.ItemDim3Code,
Qty1: l.Qty1,
ComboKey: l.ComboKey
})
})
console.groupEnd()
// =======================================================
// 🧾 DEBUG (opsiyonel helper)
// =======================================================
this.debugOrderPayload?.(header, lines, 'PRE-VALIDATE')
// =======================================================
// 🧪 PRE-VALIDATE — prItemVariant ön kontrol
// - invalid varsa CREATE/UPDATE ÇALIŞMAZ
// =======================================================
const v = await api.post('/order/validate', { header, lines })
const invalid = v?.data?.invalid || []
if (invalid.length > 0) {
await this.showInvalidVariantDialog?.($q, invalid)
return // ❌ create / update ÇALIŞMAZ
}
console.log('📤 submitAllReal payload', {
mode: this.mode,
lines: lines.length,
deletes: lines.filter(l => l._deleteSignal).length
})
/* =======================================================
🚀 API CALL — TEK NOKTA
======================================================= */
const resp = await api.post(
isNew ? '/order/create' : '/order/update',
{ header, lines }
)
const data = resp?.data || {}
serverOrderId =
data.orderID ||
data.orderHeaderID ||
data.id ||
header?.OrderHeaderID
serverOrderNo =
data.orderNumber ||
data.orderNo ||
header?.OrderNumber
if (!serverOrderId) {
throw new Error('OrderHeaderID backendden dönmedi')
}
/* =======================================================
🔁 MODE SWITCH → EDIT
======================================================= */
this.setMode('edit')
// Header patch (ID / No)
this.header = {
...this.header,
OrderHeaderID: serverOrderId,
OrderNumber: serverOrderNo
}
/* =======================================================
🧹 KRİTİK: Snapshot + Dirty temizliği
❗ ROUTE değişmeden ÖNCE
======================================================= */
this.updateHeaderTotals?.()
this.markAsSaved?.()
/* =======================================================
🧹 KRİTİK: NEW → EDIT geçişinde TÜM SNAPSHOT TEMİZLENİR
======================================================= */
this.clearAllOrderSnapshots()
$q.notify({
type: 'positive',
message: `Sipariş kaydedildi: ${serverOrderNo || ''}`.trim()
})
/* =======================================================
🔀 ROUTE REPLACE (EDIT MODE)
- aynı sayfa → param değişti
- guard 1 kez bypass
======================================================= */
this.allowRouteLeaveOnce = true
await router.replace({
name: 'order-entry',
params: { orderHeaderID: serverOrderId },
query: { mode: 'edit', source: 'backend' }
})
/* =======================================================
🔄 BACKEND RELOAD (TEK GERÇEK KAYNAK)
======================================================= */
await this.openExistingForEdit(serverOrderId, {
$q,
form,
summaryRowsRef: summaryRows,
productCache
})
/* =======================================================
❓ USER NEXT STEP
======================================================= */
const choice = await new Promise(resolve => {
$q.dialog({
title: 'Sipariş Kaydedildi',
options: {
type: 'radio',
model: 'continue',
items: [
{ label: '✏️ Düzenlemeye Devam', value: 'continue' },
{ label: '🖨 Yazdır', value: 'print' },
{ label: '📋 Listeye Dön', value: 'list' }
]
},
ok: { label: 'Seç' },
cancel: { label: 'Kapat' }
})
.onOk(v => resolve(v))
.onCancel(() => resolve('continue'))
})
/* =======================================================
🧭 USER ROUTING
======================================================= */
if (choice === 'print') {
const id = this.header?.OrderHeaderID || serverOrderId
if (id) await this.downloadOrderPdf(id)
return
}
if (choice === 'list') {
this.allowRouteLeaveOnce = true
await router.push({ name: 'order-list' })
return
}
// continue → sayfada kal (hiçbir şey yapma)
} catch (err) {
console.error('❌ submitAllReal:', err)
$q.notify({
type: 'negative',
message:
err?.response?.data?.message ||
err?.message ||
'Kayıt sırasında hata'
})
} finally {
// 🔓 Guardlar normale dönsün
this.isControlledSubmit = false
this.loading = false
}
}
,
/* =======================================================
🧪 SUBMIT ALL TEST
======================================================= */
async submitAllTest($q = null) {
try {
const { header, lines } = this.buildFinalOrderJson()
console.log('🧾 TEST HEADER', Object.keys(header).length, 'alan')
console.log(JSON.stringify(header, null, 2))
console.log('🧾 TEST LINES', lines.length, 'satır')
console.log(JSON.stringify(lines, null, 2))
$q?.notify?.({
type: 'info',
message: `Header (${Object.keys(header).length}) + Lines (${lines.length}) gösterildi`,
position: 'top'
})
} catch (err) {
console.error('❌ submitAllTest hata:', err)
$q?.notify?.({
type: 'negative',
message: 'Gösterimde hata oluştu ❌',
position: 'top'
})
}
},
/* =======================================================
🧹 KAYIT SONRASI TEMİZLİK
======================================================= */
afterSubmit(opts = {
keepLocalStorage: true,
backendPayload: null,
resetMode: true // 🔑 yeni
}) {
try {
console.log('🧹 afterSubmit başlatıldı', opts)
if (opts?.backendPayload?.header?.OrderHeaderID) {
this.mergeAndPersistBackendOrder(
opts.backendPayload.header.OrderHeaderID,
opts.backendPayload
)
}
if (!opts?.keepLocalStorage) {
this.clearStorage()
this.clearTransaction()
} else {
this.saveSnapshot()
}
this.orders = []
this.header = {}
this.editingKey = null
this.currentOrderId = null
// 🔐 MODE RESET OPSİYONEL
if (opts.resetMode === true) {
this.mode = 'new'
}
console.log('✅ afterSubmit tamamlandı.')
} catch (err) {
console.error('❌ afterSubmit hata:', err)
}
}
,
/* ===========================================================
🟦 BUILD FINAL ORDER JSON — SAFE v26.1 (FINAL)
-----------------------------------------------------------
✔ ComboKey TEK OTORİTE → buildComboKey (bedenKey ile)
✔ UI/Map placeholder: '_' (bedenKey)
✔ DB/payload: '' (bedenPayload) → "_" ASLA GİTMEZ
✔ payload içinde aynı ComboKey TEK satır
✔ backend duplicate guard %100 uyumlu (ComboKey stabil)
✔ Final assert: payloadda "_" yakalanırsa patlatır
=========================================================== */
buildFinalOrderJson () {
const auth = useAuthStore()
const u = auth?.user || {}
const now = dayjs()
/* =========================
HELPERS
========================== */
const toNum = v => Number(v) || 0
const safeStr = v => (v == null ? '' : String(v).trim())
const formatDateOnly = v => (v ? dayjs(v).format('YYYY-MM-DD') : null)
const formatTimeOnly = v => dayjs(v).format('HH:mm:ss')
const formatDateTime = v => (v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : null)
// ✅ Payload beden normalize: "_" / "-" / "" => ''
const normBeden = (v) => {
const s = safeStr(v)
if (s === '' || s === '_' || s === '-') return '' // payload empty
return s
}
/* =========================
USER META
========================== */
const group = safeStr(u?.v3usergroup)
const v3name = safeStr(u?.v3_username)
const who = (group && v3name) ? `${group} ${v3name}` : (v3name || 'BSS')
const PCT_CODE_ZERO = '%0'
const VAT_CODE_ZERO = '%0'
/* =========================
HEADER
========================== */
const headerId = this.header?.OrderHeaderID || crypto.randomUUID()
const docCurrency = safeStr(this.header?.DocCurrencyCode) || 'TRY'
const exRate = toNum(this.header?.ExchangeRate) || 1
const avgDueSource =
this.header?.AverageDueDate ||
dayjs(this.header?.OrderDate || now).add(14, 'day')
const header = {
...this.header,
OrderHeaderID: headerId,
OrderDate: formatDateOnly(this.header?.OrderDate || now),
OrderTime: formatTimeOnly(now),
AverageDueDate: formatDateOnly(avgDueSource),
DocCurrencyCode: docCurrency,
LocalCurrencyCode: safeStr(this.header?.LocalCurrencyCode) || 'TRY',
ExchangeRate: exRate,
CreatedUserName:
this.mode === 'edit'
? (this.header?.CreatedUserName || who)
: who,
CreatedDate:
this.mode === 'edit'
? formatDateTime(this.header?.CreatedDate || now)
: formatDateTime(now),
LastUpdatedUserName: who,
LastUpdatedDate: formatDateTime(now)
}
/* =======================================================
LINES — COMBOKEY AGGREGATE (TEK MAP)
======================================================= */
const lines = []
const lineByCombo = new Map() // 🔒 KEY = ComboKey
const pushOrMerge = (row, ctx) => {
const {
grpKey,
bedenKey, // ✅ sadece ComboKey / Map için ('_' olabilir)
bedenPayload, // ✅ DB için ('' / 'S' / 'M' ...)
qty,
orderLineId,
isDeleteSignal
} = ctx
if (qty <= 0 && !isDeleteSignal) return
// ComboKey stabil kalsın diye bedenKey kullan
const comboKey = buildComboKey(row, bedenKey)
const makeLine = () => ({
OrderLineID: orderLineId || '',
ClientKey: makeLineClientKey(row, grpKey, bedenKey),
ComboKey: comboKey,
SortOrder: 0,
ItemTypeCode: 1,
ItemCode: safeStr(row.model),
ColorCode: safeStr(row.renk),
// ✅ PAYLOAD: "_" ASLA YOK
ItemDim1Code: bedenPayload,
ItemDim2Code: safeStr(row.renk2),
ItemDim3Code: '',
Qty1: isDeleteSignal ? 0 : qty,
Qty2: 0,
CancelQty1: 0,
CancelQty2: 0,
DeliveryDate: row.terminTarihi
? formatDateTime(row.terminTarihi)
: null,
PlannedDateOfLading: row.terminTarihi
? formatDateOnly(row.terminTarihi)
: null,
LineDescription: safeStr(row.aciklama),
UsedBarcode: '',
CostCenterCode: '',
VatCode: VAT_CODE_ZERO,
VatRate: toNum(row.vatRate ?? row.VatRate ?? 0),
PCTCode: PCT_CODE_ZERO,
PCTRate: 0,
LDisRate1: 0,
LDisRate2: 0,
LDisRate3: 0,
LDisRate4: 0,
LDisRate5: 0,
DocCurrencyCode: header.DocCurrencyCode,
PriceCurrencyCode: header.DocCurrencyCode,
PriceExchangeRate: toNum(header.ExchangeRate),
Price: toNum(row.fiyat),
BaseProcessCode: 'WS',
BaseOrderNumber: header.OrderNumber,
BaseCustomerTypeCode: 0,
BaseCustomerCode: header.CurrAccCode,
BaseSubCurrAccID: null,
BaseStoreCode: '',
OrderHeaderID: headerId,
CreatedUserName: who,
CreatedDate: formatDateTime(row.CreatedDate || now),
LastUpdatedUserName: who,
LastUpdatedDate: formatDateTime(now),
SurplusOrderQtyToleranceRate: 0,
WithHoldingTaxTypeCode: '',
DOVCode: ''
})
const existing = lineByCombo.get(comboKey)
if (!existing) {
const ln = makeLine()
lineByCombo.set(comboKey, ln)
lines.push(ln)
return
}
/* DELETE */
if (isDeleteSignal) {
if (orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId
}
existing.Qty1 = 0
return
}
/* MERGE */
existing.Qty1 += qty
if (this.mode === 'edit' && orderLineId && !existing.OrderLineID) {
existing.OrderLineID = orderLineId
}
existing.Price = toNum(row.fiyat)
}
/* =======================================================
ORDER ROW LOOP
======================================================= */
for (const row of this.orders || []) {
if (row?.isClosed === true) continue
const grpKey =
row.grpKey ||
Object.keys(row.bedenMap || {})[0] ||
'GENEL'
const lineIdMap = row.lineIdMap || {}
const grouped = row.bedenMap?.[grpKey]
const flat =
(row.bedenMap && typeof row.bedenMap === 'object' && !grouped)
? row.bedenMap
: null
const map = grouped || flat
const hasAnyBeden =
map && typeof map === 'object' && Object.keys(map).length > 0
/* 🔹 BEDENSİZ / AKSBİR */
if (!hasAnyBeden) {
const qty = toNum(row.qty ?? row.Qty1 ?? row.miktar ?? 0)
// ✅ ComboKey stabil: bedenKey = '_'
const bedenKey = '_'
// ✅ Payload: boş string
const bedenPayload = ''
let orderLineId = ''
if (this.mode === 'edit') {
// lineIdMap burada '_' ile tutuluyorsa onu da oku
orderLineId =
safeStr(lineIdMap?.[bedenKey]) ||
safeStr(lineIdMap?.[bedenPayload]) ||
safeStr(row.OrderLineID)
}
pushOrMerge(row, {
grpKey,
bedenKey,
bedenPayload,
qty,
orderLineId,
isDeleteSignal: row._deleteSignal === true && !!orderLineId
})
continue
}
/* 🔹 BEDENLİ */
for (const [bedenRaw, qtyRaw] of Object.entries(map)) {
const qty = toNum(qtyRaw)
// ✅ payload beden: '' / 'S' / 'M' ...
const bedenPayload = normBeden(bedenRaw)
// ✅ combokey beden: boşsa '_' ile stabil kalsın
const bedenKey = bedenPayload || '_'
let orderLineId = ''
if (this.mode === 'edit') {
// lineIdMap anahtarı sizde hangi bedenle tutuluyorsa ikisini de dene
orderLineId =
safeStr(lineIdMap?.[bedenKey]) ||
safeStr(lineIdMap?.[bedenPayload]) ||
(Object.keys(map).length === 1
? safeStr(row.OrderLineID)
: '')
}
pushOrMerge(row, {
grpKey,
bedenKey,
bedenPayload,
qty,
orderLineId,
isDeleteSignal: row._deleteSignal === true && !!orderLineId
})
}
}
/* =======================================================
FINAL SORT
======================================================= */
lines.forEach((ln, i) => { ln.SortOrder = i + 1 })
/* =======================================================
ASSERT — payloadda "_" OLAMAZ
======================================================= */
if (lines.some(l => (l.ItemDim1Code || '') === '_' )) {
console.error('❌ Payloadda "_" yakalandı', lines.filter(l => l.ItemDim1Code === '_'))
throw new Error('Payload ItemDim1Code "_" olamaz')
}
/* =======================================================
🔍 DEBUG — BUILD FINAL ORDER JSON OUTPUT
======================================================= */
console.groupCollapsed('%c📦 BUILD FINAL ORDER JSON', 'color:#c9a873;font-weight:bold')
console.log('🧾 HEADER:', header)
console.table(
lines.map((l, i) => ({
i: i + 1,
OrderLineID: l.OrderLineID,
ClientKey: l.ClientKey,
ComboKey: l.ComboKey,
ItemCode: l.ItemCode,
ColorCode: l.ColorCode,
ItemDim1Code: JSON.stringify(l.ItemDim1Code), // <-- kritik
ItemDim2Code: l.ItemDim2Code,
Qty1: l.Qty1,
Price: l.Price
}))
)
console.groupEnd()
return { header, lines }
}
,/* ===========================================================
✅ STORE ACTIONS — FIXED HELPERS
- setRowErrorByClientKey
- clearRowErrorByClientKey
- applyTerminToRowsIfEmpty
=========================================================== */
setRowErrorByClientKey(clientKey, payload) {
if (!clientKey) return
if (!Array.isArray(this.summaryRows)) return
const row = this.summaryRows.find(r => r?.clientKey === clientKey)
if (!row) return
row._error = {
code: payload?.code,
message: payload?.message
}
},
clearRowErrorByClientKey(clientKey) {
if (!clientKey) return
if (!Array.isArray(this.summaryRows)) return
const row = this.summaryRows.find(r => r?.clientKey === clientKey)
if (!row) return
if (row._error) {
delete row._error
}
},
applyTerminToRowsIfEmpty(dateStr) {
if (!dateStr) return
if (!Array.isArray(this.summaryRows)) return
// ❗ reassign YOK — patch/mutate
for (const r of this.summaryRows) {
if (!r?.terminTarihi || r.terminTarihi === '') {
r.terminTarihi = dateStr
}
}
// opsiyonel ama genelde doğru:
this.persistLocalStorage?.()
}
} // actions sonu
}) // defineStore sonu
/* ===========================================================
🔹 BEDEN LABEL NORMALİZASYONU (exported helper)
=========================================================== */
export function normalizeBedenLabel(v) {
if (v === null || v === undefined) return ' '
let s = String(v).trim()
if (s === '') return ' '
// 44R, 50L vb. son ekleri at
s = s.replace(/(^\d+)\s*[A-Z]?$/i, '$1')
s = s.toUpperCase()
// harfli bedenlerin normalizasyonu
const map = {
'XS': 'XS', 'S': 'S', 'M': 'M', 'L': 'L', 'XL': 'XL',
'XXL': '2XL', '2XL': '2XL', '3XL': '3XL', '4XL': '4XL',
'5XL': '5XL', '6XL': '6XL', '7XL': '7XL', 'STD': 'STD'
}
if (map[s]) return map[s]
// tamamen sayıysa string olarak döndür
if (/^\d+$/.test(s)) return s
// virgüllü değer geldiyse ilkini al
if (s.includes(',')) return s.split(',')[0].trim()
return s
}
/* ===========================================================
🔹 BEDEN GRUBU ALGILAMA HELPERI
-----------------------------------------------------------
Gelen beden listesini, ürün grubu/kategori bilgisine göre
doğru grup anahtarına dönüştürür (ayk, yas, pan, gom, tak, aksbir).
-----------------------------------------------------------
=========================================================== */
export function detectBedenGroup(bedenList, urunAnaGrubu = '', urunKategori = '') {
const list = Array.isArray(bedenList) && bedenList.length > 0
? bedenList.map(v => (v || '').toString().trim().toUpperCase())
: [' ']
const ana = (urunAnaGrubu || '')
.toUpperCase()
.trim()
.replace(/\(.*?\)/g, '')
.replace(/[^A-ZÇĞİÖŞÜ0-9\s]/g, '')
.replace(/\s+/g, ' ')
const kat = (urunKategori || '').toUpperCase().trim()
// 🔸 Aksesuar ise "aksbir"
const aksesuarGruplari = [
'AKSESUAR','KRAVAT','PAPYON','KEMER','CORAP','ÇORAP',
'FULAR','MENDIL','MENDİL','KASKOL','ASKI',
'YAKA','KOL DUGMESI','KOL DÜĞMESİ'
]
const giyimGruplari = ['GÖMLEK','CEKET','PANTOLON','MONT','YELEK','TAKIM','TSHIRT','TİŞÖRT']
// 🔸 Pantolon özel durumu
if (
aksesuarGruplari.some(g => ana.includes(g) || kat.includes(g)) &&
!giyimGruplari.some(g => ana.includes(g))
) return 'aksbir'
if (ana.includes('PANTOLON') && kat.includes('YETİŞKİN')) return 'pan'
// 🔸 Tamamen numerik (örneğin 39-44 arası) → ayakkabı
const allNumeric = list.every(v => /^\d+$/.test(v))
if (allNumeric) {
const nums = list.map(v => parseInt(v, 10)).filter(Boolean)
const diffs = nums.slice(1).map((v, i) => v - nums[i])
if (diffs.every(d => d === 1) && nums[0] >= 35 && nums[0] <= 46) return 'ayk'
}
// 🔸 Yaş grubu (çocuk/garson)
if (kat.includes('GARSON') || kat.includes('ÇOCUK')) return 'yas'
// 🔸 Harfli beden varsa doğrudan "gom" (gömlek, üst giyim)
const harfliBedenler = ['XS','S','M','L','XL','XXL','3XL','4XL']
if (list.some(b => harfliBedenler.includes(b))) return 'gom'
// 🔸 Varsayılan: takım elbise
return 'tak'
}
export function toSummaryRowFromForm(form) {
if (!form) return null
const grpKey = form.grpKey || 'tak'
const bedenMap = {}
const labels = Array.isArray(form.bedenLabels) ? form.bedenLabels : []
const values = Array.isArray(form.bedenler) ? form.bedenler : []
for (let i = 0; i < labels.length; i++) {
const rawLbl = labels[i]
const lbl =
rawLbl == null || String(rawLbl).trim() === ''
? ' '
: String(rawLbl).trim()
const val = Number(values[i] || 0)
if (val > 0) {
bedenMap[lbl] = val
}
}
return {
id: form.id || newGuid(),
OrderLineID: form.OrderLineID || null,
model: form.model || '',
renk: form.renk || '',
renk2: form.renk2 || '',
urunAnaGrubu: form.urunAnaGrubu || '',
urunAltGrubu: form.urunAltGrubu || '',
aciklama: form.aciklama || '',
fiyat: Number(form.fiyat || 0),
pb: form.pb || 'USD',
adet: Number(form.adet || 0),
tutar: Number(form.tutar || 0),
grpKey,
bedenMap: {
[grpKey]: { ...bedenMap }
},
terminTarihi: (form.terminTarihi || '').substring(0, 10)
}
}
/* ===========================================================
🔹 TOPLAM HESAPLAMA (EXPORT)
-----------------------------------------------------------
Hem store içinde hem de component tarafında kullanılabilir.
=========================================================== */
export function updateTotals(f) {
f.adet = (f.bedenler || []).reduce((a, b) => a + Number(b || 0), 0)
const fiyat = Number(f.fiyat) || 0
f.tutar = (f.adet * fiyat).toFixed(2)
return f
}
/* ===========================================================
🔹 EXPORT SET — Tek Merkezli Dışa Aktarımlar
=========================================================== */
/**
* 🧩 Shared Reactive Refs
* -----------------------------------------------------------
* import { sharedOrderEntryRefs } from 'src/stores/orderentryStore'
* const { stockMap, bedenStock, sizeCache } = sharedOrderEntryRefs
*/
export const sharedOrderEntryRefs = {
stockMap,
bedenStock,
sizeCache,
}

View File

@@ -0,0 +1,182 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
import { useAuthStore } from 'stores/authStore'
export const usePermissionStore = defineStore('permission', {
state: () => ({
// API route yetkileri
routes: [],
// module+action matrix
matrix: [],
loaded: false
}),
getters: {
/* ================= ADMIN ================= */
isAdmin () {
const auth = useAuthStore()
return auth.isAdmin === true
},
/* ================= API ROUTE ================= */
hasApiPermission: (state) => (apiPathOrPerm) => {
const auth = useAuthStore()
if (auth.isAdmin) return true
if (!state.loaded) return false
if (!apiPathOrPerm) return true
// ============================
// 1⃣ MODULE:ACTION GELDİYSE
// ============================
if (apiPathOrPerm.includes(':')) {
const [module, action] = apiPathOrPerm.split(':')
return state.matrix.some(p =>
p.module === module &&
p.action === action &&
p.allowed === true
)
}
// ============================
// 2⃣ API PATH GELDİYSE
// ===========================
const apiPath = apiPathOrPerm
// exact match
if (state.routes.some(p =>
p.route === apiPath && p.can_access
)) {
return true
}
// /{id} normalize
const normalized = apiPath
.replace(/\/\d+/g, '/{id}')
if (state.routes.some(p =>
p.route === normalized && p.can_access
)) {
return true
}
// prefix
return state.routes.some(p =>
p.can_access && apiPath.startsWith(p.route)
)
},
/* ================= MODULE ================= */
hasModule: (state) => (module) => {
const auth = useAuthStore()
if (auth.isAdmin) return true
if (!state.loaded) return false
return state.matrix.some(p =>
p.module === module &&
p.allowed === true
)
},
/* ================= ACTION ================= */
hasPermission: (state) => (module, action) => {
const auth = useAuthStore()
if (auth.isAdmin) return true
if (!state.loaded) return false
return state.matrix.some(p =>
p.module === module &&
p.action === action &&
p.allowed === true
)
}
},
actions: {
async fetchPermissions () {
const auth = useAuthStore()
if (auth.isAdmin) {
this.routes = []
this.matrix = []
this.loaded = true
return
}
try {
// API ROUTES
const routesRes = await api.get('/permissions/routes')
this.routes = routesRes.data || []
// EFFECTIVE MATRIX
const effRes = await api.get('/permissions/effective')
this.matrix = effRes.data || []
console.group('🔐 PERMISSION DEBUG')
console.log('API ROUTES:', this.routes)
console.log('EFFECTIVE MATRIX:', this.matrix)
console.groupEnd()
} catch (err) {
console.error('❌ Permission load failed', err)
this.routes = []
this.matrix = []
} finally {
this.loaded = true
}
},
clear () {
this.routes = []
this.matrix = []
this.loaded = false
}
}
})

View File

@@ -0,0 +1,66 @@
// src/stores/statementdetailStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
export const useStatementdetailStore = defineStore('statementdetail', {
state: () => ({
details: [],
loading: false,
error: null
}),
actions: {
async loadDetails ({ accountCode, startDate, endDate, parislemler }) {
if (!accountCode) {
this.error = 'Geçerli bir cari kod seçilmedi.'
return
}
this.loading = true
this.error = null
try {
// ✅ Params (arrayFormat=repeat global)
const params = {
startdate: startDate,
enddate: endDate
}
if (Array.isArray(parislemler) && parislemler.length > 0) {
params.parislemler = parislemler.filter(
p => p !== undefined && p !== null && p !== ''
)
}
// 🔐 TOKEN + SERIALIZER + ERROR HANDLING OTOMATİK
const res = await api.get(
`/statements/${accountCode}/details`,
{ params }
)
this.details = res.data || []
} catch (err) {
console.error('❌ Details yüklenemedi:', err)
this.error =
err?.data?.message ||
err?.message ||
'Detaylar yüklenemedi'
} finally {
this.loading = false
}
},
getDetailsByBelge (belgeNo) {
return this.details.filter(
d => d.belge_ref_numarasi === belgeNo
)
},
reset () {
this.details = []
this.loading = false
this.error = null
}
}
})

View File

@@ -0,0 +1,163 @@
// src/stores/statementheaderStore.js
import { defineStore } from 'pinia'
import api from 'src/services/api'
import qs from 'qs'
import dayjs from 'src/boot/dayjs'
export const useStatementheaderStore = defineStore('statementheader', {
state: () => ({
headers: [], // Ana tablo verileri
details: {}, // Alt tablolar (belge bazlı)
loading: false, // Yükleme durumu
groupOpen: {} // Para birimi bazlı aç/kapa durumu
}),
getters: {
// 🔹 Benzersiz para birimleri listesi
currencies(state) {
const set = new Set()
for (const r of state.headers) {
set.add(r.para_birimi || '—')
}
return Array.from(set).sort()
},
// 🔹 Her para birimi için toplam borç / alacak / bakiye
totalsByCurrency(state) {
const out = {}
for (const r of state.headers) {
const k = r.para_birimi || '—'
if (!out[k]) {
out[k] = { borc: 0, alacak: 0, bakiye: 0, count: 0 }
}
out[k].borc += Number(r.borc) || 0
out[k].alacak += Number(r.alacak) || 0
out[k].bakiye += Number(r.bakiye) || 0
out[k].count += 1
}
return out
},
// 🔹 QTable için satırlar (group + data)
groupedRows: (state) => {
const grouped = {}
for (const row of state.headers) {
const k = row.para_birimi || '—'
if (!grouped[k]) grouped[k] = []
grouped[k].push(row)
}
const output = []
for (const [currency, rows] of Object.entries(grouped)) {
if (!rows.length) continue
// 📅 Tarihe göre sırala
const sorted = [...rows].sort(
(a, b) => new Date(a.belge_tarihi) - new Date(b.belge_tarihi)
)
const lastRow = sorted.at(-1)
const lastBalance =
lastRow && lastRow.bakiye != null
? Number(lastRow.bakiye)
: 0
// 🔹 Grup satırı
output.push({
_type: 'group',
para_birimi: currency,
sonBakiye: lastBalance
})
// 🔹 Alt satırlar
if (state.groupOpen[currency] !== false) {
sorted.forEach(r => {
output.push({ ...r, _type: 'data' })
})
}
}
return output
}
},
actions: {
/* ==========================================================
🔄 ANA STATEMENT LİSTESİ
========================================================== */
async loadStatements(params = {}) {
this.loading = true
try {
const { data } = await api.get(
'/statements',
{
params,
paramsSerializer: p =>
qs.stringify(p, { arrayFormat: 'repeat' })
}
)
this.headers = Array.isArray(data) ? data : []
// 🔹 Yeni gelen para birimleri default açık
for (const k of this.currencies) {
if (!(k in this.groupOpen)) {
this.groupOpen[k] = true
}
}
} catch (err) {
console.error('❌ Statements yüklenemedi:', err)
this.headers = []
} finally {
this.loading = false
}
},
/* ==========================================================
📄 BELGE DETAYLARI
========================================================== */
async loadDetails(belgeNo) {
if (!belgeNo || this.details[belgeNo]) return
try {
const { data } = await api.get(
`/statements/${belgeNo}/details`
)
this.details[belgeNo] = Array.isArray(data) ? data : []
} catch (err) {
console.error('❌ Details yüklenemedi:', err)
this.details[belgeNo] = []
}
},
/* ==========================================================
🔘 GRUP AÇ / KAPA
========================================================== */
toggleGroup(currency) {
const key = currency || '—'
this.groupOpen[key] = !this.groupOpen[key]
},
openAllGroups() {
for (const k of this.currencies) {
this.groupOpen[k] = true
}
},
closeAllGroups() {
for (const k of this.currencies) {
this.groupOpen[k] = false
}
}
}
})

13
ui/src/stores/store-flag.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
/*
WARNING: DO NOT MODIFY OR DELETE
This file is auto-generated by Quasar CLI
It's recommended to NOT .gitignore it
You don't have to use TypeScript in your project, don't worry
*/
import "quasar/dist/types/feature-flag.d.ts";
declare module "quasar/dist/types/feature-flag.d.ts" {
interface QuasarFeatureFlags {
store: true;
}
}

View File

@@ -0,0 +1,31 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
export const useUserPermissionStore = defineStore('userPerm', {
state: () => ({
rows: [],
loading: false,
saving: false
}),
actions: {
async fetch (id) {
this.loading = true
const res = await api.get(`/users/${id}/permissions`)
this.rows = res.data || []
this.loading = false
},
async save (id) {
this.saving = true
await api.post(`/users/${id}/permissions`, this.rows)
this.saving = false
}
}
})

View File

@@ -0,0 +1,53 @@
// src/stores/userSyncStore.js
import { defineStore } from 'pinia'
export const useUserSyncStore = defineStore('userSync', {
state: () => ({
pgUsers: [], // Postgre kullanıcıları
msUsers: [], // MSSQL kullanıcıları
loading: false,
selectedPg: null
}),
actions: {
// 🧩 Dummy veri ile başlat
async loadDummy() {
this.loading = true
await new Promise(r => setTimeout(r, 600)) // küçük gecikme
this.pgUsers = [
{ id: 1, code: 'mehmetk', full_name: 'Mehmet Keçeci', email: 'm@b.com', mssql_username: 'MKECECI', sync_status: 'synced', is_active: true },
{ id: 2, code: 'ayse', full_name: 'Ayşe Yılmaz', email: 'a@y.com', mssql_username: null, sync_status: 'pending', is_active: true },
{ id: 3, code: 'ali', full_name: 'Ali Demir', email: 'a@d.com', mssql_username: 'ALI.D', sync_status: 'blocked', is_active: false }
]
this.msUsers = [
{ username: 'MKECECI', first_name: 'Mehmet', last_name: 'Keçeci', email: 'm@b.com', is_blocked: 0 },
{ username: 'ALI.D', first_name: 'Ali', last_name: 'Demir', email: 'a@d.com', is_blocked: 1 },
{ username: 'YENIUSR', first_name: 'Yeni', last_name: 'Kullanıcı', email: 'y@b.com', is_blocked: 0 }
]
this.loading = false
},
async syncNow() {
this.loading = true
console.log('🔄 Dummy sync tetiklendi...')
await new Promise(r => setTimeout(r, 800))
this.loading = false
},
async map(pgId, msUsername) {
const user = this.pgUsers.find(u => u.id === pgId)
if (user) {
user.mssql_username = msUsername
user.sync_status = 'manual'
}
},
async unmap(pgId) {
const user = this.pgUsers.find(u => u.id === pgId)
if (user) {
user.mssql_username = null
user.sync_status = 'pending'
}
}
}
})