Merge remote-tracking branch 'origin/master'
This commit is contained in:
296
ui/src/pages/OrderBulkClose.vue
Normal file
296
ui/src/pages/OrderBulkClose.vue
Normal 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>
|
||||
55
ui/src/pages/RoleDepartmentPermissionGateway.vue
Normal file
55
ui/src/pages/RoleDepartmentPermissionGateway.vue
Normal 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>
|
||||
591
ui/src/pages/RoleDepartmentPermissionList.vue
Normal file
591
ui/src/pages/RoleDepartmentPermissionList.vue
Normal 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>
|
||||
93
ui/src/stores/RoleDeptPermissionListStore.js
Normal file
93
ui/src/stores/RoleDeptPermissionListStore.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
127
ui/src/stores/orderBulkCloseStore.js
Normal file
127
ui/src/stores/orderBulkCloseStore.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user