Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-02-13 20:58:38 +03:00
parent 572bab4134
commit 7edcd446d0
6 changed files with 1245 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
<template>
<q-page v-if="canUpdateOrder" class="bulk-close-page">
<div class="bulk-filter-bar">
<div class="bulk-filter-row">
<q-input
v-model="store.search"
dense
filled
clearable
class="bulk-search"
label="Arama (Sipariş No / Cari / Açıklama)"
>
<template #append>
<q-icon name="search" />
</template>
</q-input>
<div class="bulk-filter-actions">
<q-btn
label="Yenile"
icon="refresh"
color="primary"
:loading="store.loading"
@click="store.fetchOrders"
/>
<q-btn
label="Seçimi Temizle"
icon="clear"
flat
color="grey-7"
:disable="store.closing || !store.selectedCount"
@click="store.clearSelection"
/>
<q-btn
label="Seçilenleri Toplu Kapat"
icon="task_alt"
color="negative"
:loading="store.closing"
:disable="store.loading || store.closing || !store.selectedCount"
@click="onBulkClose"
/>
</div>
<div class="bulk-summary">
<div>Toplam: <strong>{{ store.totalCount }}</strong></div>
<div>Seçilen: <strong>{{ store.selectedCount }}</strong></div>
</div>
</div>
</div>
<q-table
class="bulk-table"
flat
bordered
dense
row-key="OrderNumber"
:rows="store.orders"
:columns="columns"
:loading="store.loading"
no-data-label="Kapatmaya uygun sipariş bulunamadı"
:rows-per-page-options="[0]"
hide-bottom
>
<template #header-cell-select="props">
<q-th :props="props" class="text-center">
<q-checkbox
:model-value="allSelected"
:disable="!store.totalCount || store.closing"
@update:model-value="toggleSelectAll"
/>
</q-th>
</template>
<template #body-cell-select="props">
<q-td :props="props" class="text-center">
<q-checkbox
:model-value="store.isSelected(props.row.OrderNumber)"
:disable="store.closing"
@update:model-value="(v) => store.setSelected(props.row.OrderNumber, v)"
/>
</q-td>
</template>
<template #body-cell-OrderDate="props">
<q-td :props="props">{{ formatDate(props.row.OrderDate) }}</q-td>
</template>
<template #body-cell-PackedRatePct="props">
<q-td
:props="props"
class="text-right text-weight-bold"
:class="packRateClass(props.row.PackedRatePct)"
>
{{ formatPct(props.row.PackedRatePct) }}
</q-td>
</template>
<template #body-cell-TotalAmountUSD="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.TotalAmountUSD) }}
</q-td>
</template>
<template #body-cell-PackedUSD="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.PackedUSD) }}
</q-td>
</template>
</q-table>
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
{{ store.error }}
</q-banner>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu module erisim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, watch } from 'vue'
import { Dialog, useQuasar } from 'quasar'
import { usePermission } from 'src/composables/usePermission'
import { useOrderBulkCloseStore } from 'src/stores/orderBulkCloseStore'
const $q = useQuasar()
const store = useOrderBulkCloseStore()
const { canUpdate } = usePermission()
const canUpdateOrder = canUpdate('order')
const columns = [
{ name: 'select', label: '', field: 'select', align: 'center' },
{ name: 'OrderNumber', label: 'Sipariş No', field: 'OrderNumber', align: 'left', sortable: true },
{ name: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'left', sortable: true },
{ name: 'CurrAccCode', label: 'Cari Kod', field: 'CurrAccCode', align: 'left', sortable: true },
{ name: 'CurrAccDescription', label: 'Cari Adı', field: 'CurrAccDescription', align: 'left', sortable: true },
{ name: 'DocCurrencyCode', label: 'PB', field: 'DocCurrencyCode', align: 'center', sortable: true },
{ name: 'TotalAmountUSD', label: 'Toplam USD', field: 'TotalAmountUSD', align: 'right', sortable: true },
{ name: 'PackedUSD', label: 'Paket USD', field: 'PackedUSD', align: 'right', sortable: true },
{ name: 'PackedRatePct', label: 'Paket %', field: 'PackedRatePct', align: 'right', sortable: true },
{ name: 'Description', label: 'Açıklama', field: 'Description', align: 'left' }
]
const allSelected = computed(() => {
if (!store.totalCount) return false
return store.orders.every((o) => store.isSelected(o.OrderNumber))
})
let searchTimer = null
watch(
() => store.search,
() => {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
if (canUpdateOrder.value) {
store.fetchOrders()
}
}, 350)
}
)
function toggleSelectAll (checked) {
const orderNumbers = store.orders.map((o) => o.OrderNumber)
store.toggleSelectMany(orderNumbers, checked)
}
function formatDate (value) {
if (!value) return ''
const [y, m, d] = String(value).split('-')
if (!y || !m || !d) return value
return `${d}.${m}.${y}`
}
function formatPct (value) {
return `${Number(value || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} %`
}
function packRateClass (value) {
const pct = Number(value || 0)
if (pct <= 50) return 'pack-rate-danger'
if (pct < 100) return 'pack-rate-warn'
return 'pack-rate-ok'
}
function formatMoney (value) {
return Number(value || 0).toLocaleString('tr-TR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
function onBulkClose () {
if (!store.selectedCount) return
Dialog.create({
title: 'Toplu Kapatma',
message: `${store.selectedCount} sipariş kapatılacak. Onaylıyor musunuz?`,
cancel: true,
persistent: true,
ok: { color: 'negative', label: 'Kapat' }
}).onOk(async () => {
try {
const { affected } = await store.closeSelectedOrders()
$q.notify({
type: 'positive',
message: `${affected} sipariş başarıyla kapatıldı.`,
position: 'top-right'
})
await store.fetchOrders()
} catch {
$q.notify({
type: 'negative',
message: store.error || 'Toplu kapatma başarısız',
position: 'top-right'
})
}
})
}
onMounted(() => {
if (canUpdateOrder.value) {
store.fetchOrders()
}
})
</script>
<style scoped>
.bulk-close-page {
padding: 10px;
}
.bulk-filter-bar {
margin-bottom: 8px;
}
.bulk-filter-row {
display: flex;
gap: 10px;
align-items: flex-start;
flex-wrap: wrap;
}
.bulk-search {
min-width: 320px;
max-width: 520px;
flex: 1 1 420px;
}
.bulk-filter-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.bulk-summary {
margin-left: auto;
display: flex;
flex-direction: column;
align-items: flex-end;
min-width: 140px;
font-size: 12px;
}
.bulk-table :deep(.q-table thead th) {
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.bulk-table :deep(.q-table tbody td) {
font-size: 11px;
white-space: nowrap;
}
.pack-rate-danger {
color: #c62828;
}
.pack-rate-warn {
color: #8a6d00;
}
.pack-rate-ok {
color: #1f7a4f;
}
@media (max-width: 1200px) {
.bulk-summary {
margin-left: 0;
align-items: flex-start;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<q-page v-if="canUpdateUser" class="perm-gateway flex flex-center column">
<div class="text-h5 text-primary q-mb-xl">
Rol + Departman Yetkileri
</div>
<div class="row q-gutter-lg q-mt-md">
<q-btn
color="secondary"
icon="folder_open"
label="MEVCUT YETKİLERİ GÖSTER"
@click="goList"
/>
<q-btn
color="primary"
icon="add_circle"
label="YETKİ EKLE / GÜNCELLE"
@click="goEditor"
/>
</div>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu modüle erişim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { usePermission } from 'src/composables/usePermission'
const router = useRouter()
const { canUpdate } = usePermission()
const canUpdateUser = canUpdate('user')
function goList () {
router.push({ name: 'role-dept-permissions-list' })
}
function goEditor () {
router.push({
name: 'role-dept-permissions-editor',
query: { mode: 'new' }
})
}
</script>
<style scoped>
.perm-gateway {
padding: 24px;
}
</style>

View File

@@ -0,0 +1,591 @@
<template>
<q-page v-if="canUpdateUser" class="rdp-list-page">
<div class="rdp-filter-bar">
<div class="rdp-filter-row">
<q-input
v-model="store.filters.search"
class="rdp-filter-input rdp-search"
dense
filled
clearable
debounce="300"
label="Arama (Rol / Departman)"
>
<template #append>
<q-icon name="search" />
</template>
</q-input>
<div class="rdp-filter-actions">
<q-btn
label="Temizle"
icon="clear"
color="grey-7"
flat
:disable="store.loading"
@click="clearFilters"
/>
<q-btn
label="Yenile"
icon="refresh"
color="primary"
:loading="store.loading"
@click="store.fetchRows"
/>
</div>
<div class="rdp-config-menus">
<q-btn-dropdown
color="secondary"
outline
icon="view_module"
label="Modüller"
:auto-close="false"
>
<q-list dense class="rdp-menu-list">
<q-item clickable @click="selectAllModules">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-separator />
<q-item
v-for="m in store.modules"
:key="m.value"
clickable
@click="onModuleRowClick(m.value)"
>
<q-item-section avatar>
<q-checkbox
:model-value="isModuleSelected(m.value)"
dense
@update:model-value="(v) => toggleModule(m.value, v)"
@click.stop
/>
</q-item-section>
<q-item-section>{{ m.label }}</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn-dropdown
color="secondary"
outline
icon="tune"
:label="`Aksiyonlar (${activeModuleLabel})`"
:disable="!activeModuleCode"
:auto-close="false"
>
<q-list dense class="rdp-menu-list">
<q-item clickable @click="selectAllActionsForActive">
<q-item-section>Tümünü Seç</q-item-section>
</q-item>
<q-separator />
<q-item
v-for="a in actionsForActiveModule"
:key="`${activeModuleCode}:${a}`"
clickable
>
<q-item-section avatar>
<q-checkbox
:model-value="isActionSelected(activeModuleCode, a)"
dense
@update:model-value="(v) => toggleAction(activeModuleCode, a, v)"
@click.stop
/>
</q-item-section>
<q-item-section>{{ actionLabel(a) }}</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<div class="rdp-summary">
<span>Toplam Kayıt: <strong>{{ store.totalCount }}</strong></span>
</div>
</div>
</div>
<q-table
title="Rol + Departman Yetki Setleri"
class="rdp-table"
flat
bordered
dense
row-key="row_key"
:rows="tableRows"
:columns="columns"
:loading="store.loading"
no-data-label="Kayıt bulunamadı"
:rows-per-page-options="[0]"
hide-bottom
>
<template #body-cell="props">
<q-td :props="props" :class="props.col.classes">
<template v-if="props.col.name === 'open'">
<div class="text-center">
<q-btn
icon="open_in_new"
color="primary"
flat
round
dense
@click="openEditor(props.row)"
>
<q-tooltip>Yetki setini aç</q-tooltip>
</q-btn>
</div>
</template>
<template v-else-if="isPermissionColumn(props.col.name)">
<div class="text-center">
<q-checkbox
:model-value="Boolean(props.value)"
disable
dense
/>
</div>
</template>
<template v-else>
{{ props.value }}
</template>
</q-td>
</template>
</q-table>
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
Hata: {{ store.error }}
</q-banner>
</q-page>
<q-page v-else class="q-pa-md flex flex-center">
<div class="text-negative text-subtitle1">
Bu modüle erişim yetkiniz yok.
</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useQuasar } from 'quasar'
import { usePermission } from 'src/composables/usePermission'
import { useRoleDeptPermissionListStore } from 'src/stores/RoleDeptPermissionListStore'
const router = useRouter()
const $q = useQuasar()
const store = useRoleDeptPermissionListStore()
const { canUpdate } = usePermission()
const canUpdateUser = canUpdate('user')
const selectedModules = ref([])
const selectedActionsByModule = ref({})
const activeModuleCode = ref('')
const actionLabelMap = {
update: 'Güncelleme',
view: 'Görüntüleme',
insert: 'Ekleme',
export: ıktı',
write: 'Yazma',
read: 'Okuma',
delete: 'Silme',
login: 'Giriş',
refresh: 'Yenileme',
'user.update': 'Kullanıcı Güncelle'
}
const fixedColumns = [
{
name: 'open',
label: '',
field: 'open',
align: 'center',
sortable: false,
classes: 'freeze-col freeze-1',
headerClasses: 'freeze-col freeze-1',
style: 'width:56px; min-width:56px; max-width:56px',
headerStyle: 'width:56px; min-width:56px; max-width:56px'
},
{
name: 'role_title',
label: 'Rol',
field: 'role_title',
align: 'left',
sortable: true,
classes: 'freeze-col freeze-2',
headerClasses: 'freeze-col freeze-2',
style: 'width:220px; min-width:220px; max-width:220px',
headerStyle: 'width:220px; min-width:220px; max-width:220px'
},
{
name: 'department_title',
label: 'Departman',
field: 'department_title',
align: 'left',
sortable: true,
classes: 'freeze-col freeze-3',
headerClasses: 'freeze-col freeze-3',
style: 'width:220px; min-width:220px; max-width:220px',
headerStyle: 'width:220px; min-width:220px; max-width:220px'
},
{
name: 'department_code',
label: 'Departman Kodu',
field: 'department_code',
align: 'left',
sortable: true,
style: 'width:140px; min-width:140px; max-width:140px',
headerStyle: 'width:140px; min-width:140px; max-width:140px'
}
]
const moduleLabelMap = computed(() => {
const map = {}
;(store.modules || []).forEach((m) => {
map[m.value] = m.label || m.value
})
return map
})
const actionsByModule = computed(() => {
const map = {}
;(store.moduleActions || []).forEach((x) => {
if (!map[x.module_code]) map[x.module_code] = []
if (!map[x.module_code].includes(x.action)) {
map[x.module_code].push(x.action)
}
})
Object.keys(map).forEach((k) => map[k].sort())
return map
})
const activeModuleLabel = computed(() => {
if (!activeModuleCode.value) return 'Seçim'
return moduleLabelMap.value[activeModuleCode.value] || activeModuleCode.value
})
const actionsForActiveModule = computed(() => {
if (!activeModuleCode.value) return []
return actionsByModule.value[activeModuleCode.value] || []
})
function actionLabel (action) {
const key = String(action || '').toLowerCase().trim()
return actionLabelMap[key] || key
}
function syncSelections () {
const availableModules = (store.modules || []).map((m) => m.value)
if (!availableModules.length) {
selectedModules.value = []
selectedActionsByModule.value = {}
activeModuleCode.value = ''
return
}
const selected = selectedModules.value.filter((m) => availableModules.includes(m))
selectedModules.value = selected.length ? selected : [...availableModules]
if (!selectedModules.value.includes(activeModuleCode.value)) {
activeModuleCode.value = selectedModules.value[0]
}
const next = {}
selectedModules.value.forEach((m) => {
const allActions = actionsByModule.value[m] || []
const prev = selectedActionsByModule.value[m] || []
const filtered = prev.filter((a) => allActions.includes(a))
next[m] = filtered.length ? filtered : [...allActions]
})
selectedActionsByModule.value = next
}
watch(
() => [store.modules, store.moduleActions],
() => {
syncSelections()
},
{ deep: true }
)
function isModuleSelected (moduleCode) {
return selectedModules.value.includes(moduleCode)
}
function toggleModule (moduleCode, checked) {
const set = new Set(selectedModules.value)
if (checked) {
set.add(moduleCode)
} else {
set.delete(moduleCode)
}
selectedModules.value = [...set]
if (!selectedModules.value.length) {
selectedModules.value = [moduleCode]
}
if (!selectedModules.value.includes(activeModuleCode.value)) {
activeModuleCode.value = selectedModules.value[0]
}
syncSelections()
}
function onModuleRowClick (moduleCode) {
activeModuleCode.value = moduleCode
}
function selectAllModules () {
selectedModules.value = (store.modules || []).map((m) => m.value)
syncSelections()
}
function isActionSelected (moduleCode, action) {
return (selectedActionsByModule.value[moduleCode] || []).includes(action)
}
function toggleAction (moduleCode, action, checked) {
const current = new Set(selectedActionsByModule.value[moduleCode] || [])
if (checked) {
current.add(action)
} else {
current.delete(action)
}
if (current.size === 0) {
current.add(action)
}
selectedActionsByModule.value = {
...selectedActionsByModule.value,
[moduleCode]: [...current]
}
}
function selectAllActionsForActive () {
if (!activeModuleCode.value) return
selectedActionsByModule.value = {
...selectedActionsByModule.value,
[activeModuleCode.value]: [...actionsForActiveModule.value]
}
}
const permissionColumns = computed(() => {
const cols = []
selectedModules.value.forEach((m) => {
const actions = selectedActionsByModule.value[m] || []
actions.forEach((a) => {
const key = `${m}|${a}`
cols.push({
name: `perm_${key}`,
label: `${moduleLabelMap.value[m] || m}\n${actionLabel(a)}`,
field: (row) => Boolean(row.module_flags?.[key]),
align: 'center',
sortable: true,
style: 'width:150px; min-width:150px; max-width:150px',
headerStyle: 'width:150px; min-width:150px; max-width:150px; white-space:pre-line; line-height:1.15'
})
})
})
return cols
})
const columns = computed(() => [
...fixedColumns,
...permissionColumns.value
])
const tableRows = computed(() =>
(store.rows || []).map((r) => ({
...r,
row_key: `${r.role_id}:${r.department_code}`
}))
)
function isPermissionColumn (name) {
return String(name || '').startsWith('perm_')
}
let searchTimer = null
watch(
() => store.filters.search,
() => {
clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
if (canUpdateUser.value) {
store.fetchRows()
}
}, 350)
}
)
function openEditor (row) {
if (!row?.role_id || !row?.department_code) {
$q.notify({ type: 'warning', message: 'Kayıt bilgisi eksik' })
return
}
router.push({
name: 'role-dept-permissions-editor',
query: {
mode: 'edit',
roleId: String(row.role_id),
deptCode: String(row.department_code)
}
})
}
function clearFilters () {
store.filters.search = ''
store.fetchRows()
}
onMounted(async () => {
if (canUpdateUser.value) {
await store.fetchRows()
syncSelections()
}
})
</script>
<style scoped>
.rdp-list-page {
--rdp-header-h: 56px;
--rdp-filter-h: 96px;
height: calc(100vh - var(--rdp-header-h));
overflow: auto;
background: #fff;
display: flex;
flex-direction: column;
padding: 10px;
}
.rdp-filter-bar {
position: sticky;
top: 0;
z-index: 600;
background: #fff;
border-bottom: 1px solid #ddd;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
min-height: var(--rdp-filter-h);
display: flex;
align-items: center;
padding: 10px 12px;
margin-bottom: 8px;
}
.rdp-filter-row {
display: flex;
align-items: flex-end;
gap: 12px;
flex-wrap: nowrap;
}
.rdp-filter-input {
min-width: 180px;
max-width: 300px;
}
.rdp-search {
min-width: 300px;
max-width: 520px;
flex: 1 1 360px;
}
.rdp-filter-actions {
display: flex;
gap: 8px;
align-items: center;
white-space: nowrap;
flex: 0 0 auto;
}
.rdp-config-menus {
display: flex;
gap: 8px;
align-items: center;
white-space: nowrap;
flex: 0 0 auto;
}
.rdp-menu-list {
min-width: 260px;
max-height: 420px;
}
.rdp-summary {
margin-left: auto;
background: #f9fafb;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 8px 12px;
white-space: nowrap;
font-size: 12px;
color: #4b5563;
align-self: center;
}
.rdp-table :deep(.q-table__middle) {
overflow: visible !important;
max-height: none !important;
}
.rdp-table :deep(.q-table thead th) {
position: sticky;
top: var(--rdp-filter-h);
z-index: 500;
background: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
font-size: 11px;
white-space: nowrap;
}
.rdp-table :deep(.q-table tbody td) {
font-size: 11px;
padding: 3px 6px;
}
.rdp-table :deep(.q-checkbox__inner) {
pointer-events: none;
}
.rdp-table :deep(.freeze-col) {
position: sticky;
background: #fff;
z-index: 510;
}
.rdp-table :deep(thead .freeze-col) {
z-index: 520;
background: #fff;
}
.rdp-table :deep(.freeze-1) {
left: 0;
}
.rdp-table :deep(.freeze-2) {
left: 56px;
}
.rdp-table :deep(.freeze-3) {
left: 276px;
}
@media (max-width: 1400px) {
.rdp-filter-row {
flex-wrap: wrap;
align-items: flex-start;
}
.rdp-filter-actions {
flex-wrap: wrap;
}
.rdp-config-menus {
flex-wrap: wrap;
}
.rdp-summary {
min-width: 100%;
margin-left: 0;
margin-top: 6px;
}
}
</style>

View File

@@ -0,0 +1,93 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
let lastRequestId = 0
export const useRoleDeptPermissionListStore = defineStore('roleDeptPermissionList', {
state: () => ({
modules: [],
moduleActions: [],
rows: [],
loading: false,
error: null,
filters: {
search: ''
}
}),
getters: {
totalCount (state) {
return state.rows.length
}
},
actions: {
async fetchRows () {
const rid = ++lastRequestId
this.loading = true
this.error = null
try {
const search = String(this.filters.search || '').trim()
const params = {}
if (search) params.search = search
const res = await api.get('/role-dept-permissions/list', { params })
if (rid !== lastRequestId) {
return
}
const payload = res?.data || {}
this.modules = Array.isArray(payload?.modules)
? payload.modules.map((m) => ({
value: String(m.value || '').toLowerCase().trim(),
label: String(m.label || '')
})).filter((m) => m.value)
: []
this.moduleActions = Array.isArray(payload?.module_actions)
? payload.module_actions.map((a) => ({
module_code: String(a.module_code || '').toLowerCase().trim(),
action: String(a.action || '').toLowerCase().trim()
})).filter((a) => a.module_code && a.action)
: []
const rawRows = Array.isArray(payload?.rows)
? payload.rows
: Array.isArray(res?.data) ? res.data : []
this.rows = rawRows.map((r) => {
const rawFlags = (r?.module_flags && typeof r.module_flags === 'object')
? r.module_flags
: {}
const flags = {}
Object.keys(rawFlags).forEach((k) => {
flags[String(k).toLowerCase().trim()] = Boolean(rawFlags[k])
})
return {
role_id: Number(r.role_id || 0),
role_title: r.role_title || '',
department_code: r.department_code || '',
department_title: r.department_title || '',
module_flags: flags
}
})
} catch (err) {
if (rid !== lastRequestId) return
this.modules = []
this.moduleActions = []
this.rows = []
this.error =
err?.response?.data ||
err?.message ||
'Yetki listesi alınamadı'
} finally {
if (rid === lastRequestId) {
this.loading = false
}
}
}
}
})

View File

@@ -0,0 +1,127 @@
import { defineStore } from 'pinia'
import api from 'src/services/api'
export const useOrderBulkCloseStore = defineStore('orderBulkClose', {
state: () => ({
orders: [],
selectedOrderNumbers: [],
search: '',
loading: false,
closing: false,
error: null
}),
getters: {
selectedCount (state) {
return state.selectedOrderNumbers.length
},
totalCount (state) {
return state.orders.length
}
},
actions: {
isSelected (orderNumber) {
return this.selectedOrderNumbers.includes(orderNumber)
},
setSelected (orderNumber, selected) {
if (!orderNumber) return
if (selected) {
if (!this.selectedOrderNumbers.includes(orderNumber)) {
this.selectedOrderNumbers.push(orderNumber)
}
return
}
this.selectedOrderNumbers = this.selectedOrderNumbers.filter(
(n) => n !== orderNumber
)
},
toggleSelectMany (orderNumbers, selected) {
const clean = Array.from(
new Set((orderNumbers || []).filter(Boolean))
)
if (selected) {
const current = new Set(this.selectedOrderNumbers)
clean.forEach((n) => current.add(n))
this.selectedOrderNumbers = Array.from(current)
return
}
const block = new Set(clean)
this.selectedOrderNumbers = this.selectedOrderNumbers.filter(
(n) => !block.has(n)
)
},
clearSelection () {
this.selectedOrderNumbers = []
},
async fetchOrders () {
this.loading = true
this.error = null
try {
const search = String(this.search || '').trim()
const params = {}
if (search) params.search = search
const res = await api.get('/orders/close-ready', { params })
this.orders = Array.isArray(res?.data) ? res.data : []
// new list disinda kalan secimleri temizle
const available = new Set(this.orders.map((o) => o.OrderNumber))
this.selectedOrderNumbers = this.selectedOrderNumbers.filter(
(n) => available.has(n)
)
} catch (err) {
this.orders = []
this.error =
err?.response?.data ||
err?.message ||
'Siparişler alınamadı'
} finally {
this.loading = false
}
},
async closeSelectedOrders () {
if (!this.selectedOrderNumbers.length) {
return { affected: 0 }
}
this.closing = true
this.error = null
try {
const payload = {
order_numbers: this.selectedOrderNumbers
}
const res = await api.post('/orders/bulk-close', payload)
const affected = Number(res?.data?.affected || 0)
// local optimizasyon: secilenleri listeden dus
const selected = new Set(this.selectedOrderNumbers)
this.orders = this.orders.filter((o) => !selected.has(o.OrderNumber))
this.selectedOrderNumbers = []
return { affected }
} catch (err) {
this.error =
err?.response?.data ||
err?.message ||
'Toplu kapatma başarısız'
throw err
} finally {
this.closing = false
}
}
}
})