Files
bssapp/ui/src/layouts/MainLayout.vue
2026-06-19 15:44:21 +03:00

559 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<q-layout view="hHh Lpr fFf">
<!-- HEADER -->
<q-header elevated class="bg-primary text-white">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar class="bg-secondary q-mr-sm">
<img src="/images/Baggi-tekstilas-logolu.jpg" />
</q-avatar>
{{ t('app.title') }}
</q-toolbar-title>
<q-select
v-model="selectedLocale"
dense
outlined
emit-value
map-options
options-dense
class="q-mr-sm lang-select"
option-value="value"
option-label="label"
:options="languageOptions"
>
<template #selected-item="scope">
<div class="lang-item">
<span class="lang-flag">{{ scope.opt.flag }}</span>
<span class="lang-short">{{ scope.opt.short }}</span>
</div>
</template>
<template #option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<div class="lang-item">
<span class="lang-flag">{{ scope.opt.flag }}</span>
<span class="lang-short">{{ scope.opt.short }}</span>
<span>{{ scope.opt.label }}</span>
</div>
</q-item-section>
</q-item>
</template>
</q-select>
<q-btn flat dense round icon="logout" @click="confirmLogout" />
</q-toolbar>
</q-header>
<!-- DRAWER -->
<q-drawer
v-if="perm.loaded"
v-model="leftDrawerOpen"
:show-if-above="!$q.screen.lt.md"
:overlay="$q.screen.lt.md"
:behavior="$q.screen.lt.md ? 'mobile' : 'desktop'"
:breakpoint="1023"
bordered
class="bg-secondary text-white"
>
<div class="drawer-scroll">
<q-list padding>
<!-- DYNAMIC MENU -->
<template
v-for="(item, i) in filteredMenu"
:key="i"
>
<!-- GROUP -->
<q-expansion-item
v-if="item.children"
:icon="item.icon"
:label="item.label"
expand-separator
>
<q-item
v-for="(c, j) in item.children"
:key="j"
clickable
:to="c.to"
>
<q-item-section avatar>
<q-icon name="chevron_right" />
</q-item-section>
<q-item-section>
{{ c.label }}
</q-item-section>
</q-item>
</q-expansion-item>
<!-- SINGLE -->
<q-item
v-else
clickable
:to="item.to"
>
<q-item-section avatar>
<q-icon :name="item.icon" />
</q-item-section>
<q-item-section>
{{ item.label }}
</q-item-section>
</q-item>
<q-separator spaced />
</template>
<!-- PASSWORD -->
<q-item clickable to="/app/change-password">
<q-item-section avatar>
<q-icon name="vpn_key" />
</q-item-section>
<q-item-section>
{{ t('app.changePassword') }}
</q-item-section>
</q-item>
</q-list>
</div>
</q-drawer>
<!-- CONTENT -->
<q-page-container class="with-bg">
<router-view />
</q-page-container>
<!-- FOOTER -->
<q-footer class="bg-grey-8 text-white">
<q-toolbar class="bg-secondary">
<q-toolbar-title>
{{ t('app.title') }}
</q-toolbar-title>
</q-toolbar>
</q-footer>
</q-layout>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Dialog, useQuasar } from 'quasar'
import { useAuthStore } from 'stores/authStore'
import { usePermissionStore } from 'stores/permissionStore'
import { useI18n } from 'src/composables/useI18n'
import { UI_LANGUAGE_OPTIONS } from 'src/i18n/languages'
import { useLocaleStore } from 'src/stores/localeStore'
import { activityLogsMenuItem, getAuthUserId } from 'src/modules/activityLogs'
/* ================= STORES ================= */
const router = useRouter()
const route = useRoute()
const $q = useQuasar()
const auth = useAuthStore()
const perm = usePermissionStore()
const localeStore = useLocaleStore()
const { t } = useI18n()
const languageOptions = UI_LANGUAGE_OPTIONS
const selectedLocale = computed({
get: () => localeStore.locale,
set: (value) => {
localeStore.setLocale(value)
}
})
/* ================= UI ================= */
const leftDrawerOpen = ref(!$q.screen.lt.md)
function toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
function confirmLogout () {
Dialog.create({
title: t('app.logoutTitle'),
message: t('app.logoutConfirm'),
cancel: true,
persistent: true
}).onOk(() => {
auth.clearSession()
perm.clear()
router.push('/login')
})
}
/* ================= LOAD PERMISSIONS ================= */
onMounted(async () => {
if (!perm.loaded) {
await perm.fetchPermissions()
}
leftDrawerOpen.value = !$q.screen.lt.md
})
watch(
() => route.fullPath,
() => {
if ($q.screen.lt.md) {
leftDrawerOpen.value = false
}
}
)
watch(
() => $q.screen.lt.md,
(isMobile) => {
if (isMobile) {
leftDrawerOpen.value = false
}
}
)
/* ================= MENU CONFIG ================= */
const menuItems = [
{
label: 'Ana Panel',
icon: 'dashboard',
to: '/app',
permission: 'system:view'
},
{
label: 'Finans',
icon: 'account_balance',
children: [
{
label: 'Cari Ekstre',
to: '/app/statementofaccount',
permission: 'finance:view'
},
{
label: 'Cari Bakiye Listesi',
to: '/app/customer-balance-list',
permission: 'finance:view'
},
{
label: 'Cari Yaşlandırmalı Ekstre',
to: '/app/account-aging-statement',
permission: 'finance:view'
},
{
label: 'Cari Yaşlandırmalı Cari Bakiye Listesi',
to: '/app/aged-customer-balance-list',
permission: 'finance:view'
}
]
},
{
label: 'Sipariş',
icon: 'shopping_cart',
children: [
{
label: 'Siparişler',
to: '/app/order-gateway',
permission: 'order:view'
},
{
label: 'Fiyat Listesi',
to: '/app/order-price-list',
permission: 'order:view'
},
{
label: 'Üretime Verilen Siparişleri Güncelle',
to: '/app/orderproductionupdate',
permission: 'order:update'
},
{
label: 'Tamamlanan Siparişleri Toplu Kapatma',
to: '/app/order-bulk-close',
permission: 'order:update'
}
]
},
{
label: 'Ürün',
icon: 'inventory_2',
children: [
{
label: 'Ürün Kodundan Stok Sorgula',
to: '/app/product-stock-query',
permission: 'order:view'
},
{
label: 'Ürün Özelliklerinden Stok Bul',
to: '/app/product-stock-by-attributes',
permission: 'order:view'
}
]
},
{
label: 'Ürün Maliyetlendirme',
icon: 'request_quote',
children: [
{
label: "Üretim'den Ürün Maliyetlendirme",
to: '/app/costing/production-product-costing',
permission: 'costing:view'
},
{
label: 'Maliyet Parça Eşleştirme',
to: '/app/costing/production-product-costing/maliyet-parca-eslestirme',
permission: 'costing:view'
},
{
label: 'Maliyet Varsayilan Miktarlar',
to: '/app/costing/production-product-costing/default-quantities',
permission: 'costing:view'
}
]
},
{
label: 'Ürün Fiyatlandırma',
icon: 'sell',
children: [
{
label: 'Fiyatlandırma',
to: '/app/pricing/product-pricing',
permission: 'pricing:view'
},
{
label: 'Toptan Kampanya Yönetimi',
to: '/app/pricing/wholesale-campaigns',
permission: 'pricing:view'
},
{
label: 'Marka Sınıflandırma',
to: '/app/pricing/brand-classification',
permission: 'pricing:view'
},
{
label: 'Fiyat Çarpan Kuralları',
to: '/app/pricing/pricing-rules',
permission: 'pricing:view'
}
]
},
{
label: 'Sistem',
icon: 'settings',
children: [
{
label: 'Rol + Departman Yetkileri',
to: '/app/role-dept-permissions',
permission: 'system:update'
},
{
label: 'Kullanıcı Yetkileri',
to: '/app/user-permissions',
permission: 'system:update'
},
{
label: 'Piyasa Mail Eşleştirme',
to: '/app/market-mail-mapping',
permission: 'system:update'
},
{
label: 'Maliyet Mail Eşleştirme',
to: '/app/costing-mail-mapping',
permission: 'system:update'
},
{
label: 'Fiyatlandırma Mail Eşleştirme',
to: '/app/pricing-mail-mapping',
permission: 'system:update'
},
{
label: 'Fiyat Listesi Mail Eşleştirme',
to: '/app/order-price-list-mail-mapping',
permission: 'system:update'
},
{
label: 'Kullanıcı Fiyat Eşleştirme',
to: '/app/order-price-list-user-price-groups',
permission: 'system:update'
}
]
},
{
label: 'SüperAdmin',
icon: 'admin_panel_settings',
onlyUserIds: activityLogsMenuItem.onlyUserIds,
children: [
{
label: 'Log İzleme',
to: activityLogsMenuItem.to,
permission: activityLogsMenuItem.permission,
onlyUserIds: activityLogsMenuItem.onlyUserIds
},
{
label: 'Test Mail',
to: '/app/test-mail',
permission: 'system:update',
onlyUserIds: activityLogsMenuItem.onlyUserIds
}
]
},
{
label: 'Dil Çeviri',
icon: 'translate',
children: [
{
label: 'Çeviri Tablosu',
to: '/app/language/translations',
permission: 'language:update'
}
]
},
{
label: 'Kullanıcı Yönetimi',
icon: 'people',
children: [
{
label: 'Kullanıcılar',
to: '/app/users',
permission: 'system:read'
}
]
}
]
/* ================= FILTERED MENU ================= */
function isAllowedForUser (item) {
const only = item?.onlyUserIds
if (!Array.isArray(only) || only.length === 0) return true
const id = getAuthUserId(auth.user)
if (id == null) return false
return only.includes(id)
}
const filteredMenu = computed(() => {
if (!perm.loaded) return []
return menuItems
.map(item => {
if (item.children) {
if (!isAllowedForUser(item)) return null
const children = item.children.filter(c =>
isAllowedForUser(c) && perm.hasApiPermission(c.permission)
)
if (!children.length) return null
return {
...item,
children
}
}
if (!isAllowedForUser(item)) {
return null
}
if (!perm.hasApiPermission(item.permission)) {
return null
}
return item
})
.filter(Boolean)
})
</script>
<style scoped>
.drawer-scroll {
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
}
.lang-select {
width: 140px;
background: #fff;
border-radius: 6px;
}
.lang-item {
display: inline-flex;
align-items: center;
gap: 8px;
}
.lang-flag {
font-size: 15px;
line-height: 1;
}
.lang-short {
font-weight: 700;
letter-spacing: 0.3px;
}
</style>