Merge remote-tracking branch 'origin/master'

# Conflicts:
#	ui/src/pages/OrderList.vue
This commit is contained in:
2026-02-13 15:17:14 +03:00
parent c888ef9b3c
commit 03d6c61587
8 changed files with 576 additions and 133 deletions

View File

@@ -262,6 +262,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
rdPerm := "/api/roles/{roleId}/departments/{deptCode}/permissions" rdPerm := "/api/roles/{roleId}/departments/{deptCode}/permissions"
rdHandler := routes.NewRoleDepartmentPermissionHandler(pgDB) rdHandler := routes.NewRoleDepartmentPermissionHandler(pgDB)
bindV3(r, pgDB,
"/api/role-dept-permissions/list", "GET",
"user", "update",
wrapV3(http.HandlerFunc(rdHandler.List)),
)
bindV3(r, pgDB, bindV3(r, pgDB,
rdPerm, "GET", rdPerm, "GET",
"user", "update", "user", "update",
@@ -407,6 +412,8 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
{"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)}, {"/api/order/update", "POST", "update", http.HandlerFunc(routes.UpdateOrderHandler)},
{"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)}, {"/api/order/get/{id}", "GET", "view", routes.GetOrderByIDHandler(mssql)},
{"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)}, {"/api/orders/list", "GET", "view", routes.OrderListRoute(mssql)},
{"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)}, {"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},
{"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)}, {"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)},
{"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)}, {"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)},

View File

@@ -34,6 +34,67 @@ DO UPDATE SET
` `
// LIST (role+department sets with summary)
const ListRoleDepartmentPermissionSets = `
WITH role_dept AS (
SELECT DISTINCT
p.role_id,
p.department_code
FROM mk_sys_role_department_permissions p
),
base AS (
SELECT
rd.role_id,
COALESCE(NULLIF(r.title, ''), r.code, rd.role_id::text) AS role_title,
rd.department_code,
COALESCE(d.title, rd.department_code) AS department_title
FROM role_dept rd
LEFT JOIN dfrole r
ON r.id = rd.role_id
LEFT JOIN mk_dprt d
ON d.code = rd.department_code
WHERE
($1 = '' OR
COALESCE(NULLIF(r.title, ''), r.code, '') ILIKE '%' || $1 || '%' OR
COALESCE(d.title, '') ILIKE '%' || $1 || '%' OR
rd.department_code ILIKE '%' || $1 || '%' OR
rd.role_id::text ILIKE '%' || $1 || '%')
),
perm_agg AS (
SELECT
p.role_id,
p.department_code,
LOWER(p.module_code) AS module_code,
LOWER(p.action) AS action,
BOOL_OR(p.allowed) AS has_allowed
FROM mk_sys_role_department_permissions p
GROUP BY
p.role_id,
p.department_code,
LOWER(p.module_code),
LOWER(p.action)
)
SELECT
b.role_id,
b.role_title,
b.department_code,
b.department_title,
COALESCE(
(
SELECT jsonb_object_agg(pa.module_code || '|' || pa.action, pa.has_allowed)
FROM perm_agg pa
WHERE
pa.role_id = b.role_id
AND pa.department_code = b.department_code
),
'{}'::jsonb
) AS module_flags
FROM base b
ORDER BY
b.role_title,
b.department_title
`
// ====================================================== // ======================================================
// 📦 MODULES // 📦 MODULES
// ====================================================== // ======================================================
@@ -45,3 +106,20 @@ SELECT
FROM mk_sys_modules FROM mk_sys_modules
ORDER BY id ORDER BY id
` `
const GetModuleActionLookup = `
SELECT DISTINCT
LOWER(x.module_code) AS module_code,
LOWER(x.action) AS action
FROM (
SELECT module_code, action FROM mk_sys_routes
UNION ALL
SELECT module_code, action FROM mk_sys_role_department_permissions
) x
WHERE
x.module_code IS NOT NULL
AND x.action IS NOT NULL
ORDER BY
LOWER(x.module_code),
LOWER(x.action)
`

View File

@@ -11,6 +11,7 @@ import (
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@@ -24,6 +25,30 @@ type Row struct {
CanAccess bool `json:"can_access"` CanAccess bool `json:"can_access"`
} }
type RoleDeptPermissionSummary struct {
RoleID int `json:"role_id"`
RoleTitle string `json:"role_title"`
DepartmentCode string `json:"department_code"`
DepartmentTitle string `json:"department_title"`
ModuleFlags map[string]bool `json:"module_flags"`
}
type ModuleLookupOption struct {
Value string `json:"value"`
Label string `json:"label"`
}
type ModuleActionLookupOption struct {
ModuleCode string `json:"module_code"`
Action string `json:"action"`
}
type RoleDeptPermissionListResponse struct {
Modules []ModuleLookupOption `json:"modules"`
ModuleActions []ModuleActionLookupOption `json:"module_actions"`
Rows []RoleDeptPermissionSummary `json:"rows"`
}
type RoleDepartmentPermissionHandler struct { type RoleDepartmentPermissionHandler struct {
DB *sql.DB DB *sql.DB
Repo *permissions.RoleDepartmentPermissionRepo Repo *permissions.RoleDepartmentPermissionRepo
@@ -37,6 +62,109 @@ func NewRoleDepartmentPermissionHandler(db *sql.DB) *RoleDepartmentPermissionHan
} }
} }
/* ======================================================
LIST
====================================================== */
func (h *RoleDepartmentPermissionHandler) List(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
search := strings.TrimSpace(r.URL.Query().Get("search"))
modRows, err := h.DB.Query(queries.GetModuleLookup)
if err != nil {
http.Error(w, "module lookup error", http.StatusInternalServerError)
return
}
defer modRows.Close()
modules := make([]ModuleLookupOption, 0, 32)
for modRows.Next() {
var m ModuleLookupOption
if err := modRows.Scan(&m.Value, &m.Label); err != nil {
http.Error(w, "module lookup scan error", http.StatusInternalServerError)
return
}
modules = append(modules, m)
}
if err := modRows.Err(); err != nil {
http.Error(w, "module lookup rows error", http.StatusInternalServerError)
return
}
actionRows, err := h.DB.Query(queries.GetModuleActionLookup)
if err != nil {
http.Error(w, "module action lookup error", http.StatusInternalServerError)
return
}
defer actionRows.Close()
moduleActions := make([]ModuleActionLookupOption, 0, 128)
for actionRows.Next() {
var a ModuleActionLookupOption
if err := actionRows.Scan(&a.ModuleCode, &a.Action); err != nil {
http.Error(w, "module action scan error", http.StatusInternalServerError)
return
}
moduleActions = append(moduleActions, a)
}
if err := actionRows.Err(); err != nil {
http.Error(w, "module action rows error", http.StatusInternalServerError)
return
}
rows, err := h.DB.Query(queries.ListRoleDepartmentPermissionSets, search)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]RoleDeptPermissionSummary, 0, 128)
for rows.Next() {
var item RoleDeptPermissionSummary
var rawFlags []byte
if err := rows.Scan(
&item.RoleID,
&item.RoleTitle,
&item.DepartmentCode,
&item.DepartmentTitle,
&rawFlags,
); err != nil {
http.Error(w, "scan error", http.StatusInternalServerError)
return
}
item.ModuleFlags = map[string]bool{}
if len(rawFlags) > 0 {
if err := json.Unmarshal(rawFlags, &item.ModuleFlags); err != nil {
http.Error(w, "module flags parse error", http.StatusInternalServerError)
return
}
}
list = append(list, item)
}
if err := rows.Err(); err != nil {
http.Error(w, "rows error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(RoleDeptPermissionListResponse{
Modules: modules,
ModuleActions: moduleActions,
Rows: list,
})
}
/* ====================================================== /* ======================================================
GET GET
====================================================== */ ====================================================== */

View File

@@ -208,6 +208,11 @@ const menuItems = [
label: 'Siparişler', label: 'Siparişler',
to: '/app/order-gateway', to: '/app/order-gateway',
permission: 'order:view' permission: 'order:view'
},
{
label: 'Tamamlanan Siparişleri Toplu Kapatma',
to: '/app/order-bulk-close',
permission: 'order:update'
} }
] ]
}, },

View File

@@ -57,6 +57,13 @@
label="MEVCUT SİPARİŞİ AÇ" label="MEVCUT SİPARİŞİ AÇ"
@click="goOrderList" @click="goOrderList"
/> />
<q-btn
v-if="canUpdateOrder"
color="negative"
icon="task_alt"
label="TAMAMLANANLARI TOPLU KAPAT"
@click="goBulkClose"
/>
</div> </div>
@@ -80,10 +87,11 @@ import { useQuasar } from 'quasar'
import { useOrderEntryStore } from 'src/stores/orderentryStore' import { useOrderEntryStore } from 'src/stores/orderentryStore'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
const { canRead, canWrite } = usePermission() const { canRead, canWrite, canUpdate } = usePermission()
const canReadOrder = canRead('order') const canReadOrder = canRead('order')
const canWriteOrder = canWrite('order') const canWriteOrder = canWrite('order')
const canUpdateOrder = canUpdate('order')
const router = useRouter() const router = useRouter()
const $q = useQuasar() const $q = useQuasar()
@@ -182,6 +190,10 @@ function goOrderList () {
router.push({ name: 'order-list' }) router.push({ name: 'order-list' })
} }
function goBulkClose () {
router.push({ name: 'order-bulk-close' })
}
/* =========================================================== /* ===========================================================
🧹 NEW Taslağı Temizle (SADECE NEW) 🧹 NEW Taslağı Temizle (SADECE NEW)
=========================================================== */ =========================================================== */

View File

@@ -1,49 +1,40 @@
<template> <template>
<q-page class="ol-page"> <q-page v-if="canReadOrder" class="ol-page">
<!-- 🔍 Sticky Filter -->
<div class="ol-filter-bar"> <div class="ol-filter-bar">
<!-- 🔹 TEK SATIR FLEX -->
<div class="ol-filter-row"> <div class="ol-filter-row">
<!-- 🔍 Arama -->
<q-input <q-input
v-model="store.filters.search"
class="ol-filter-input ol-search" class="ol-filter-input ol-search"
dense dense
filled filled
v-model="store.filters.search"
label="Arama (Sipariş No / Cari / Açıklama)"
debounce="300" debounce="300"
clearable clearable
label="Arama (Sipariş No / Cari / Açıklama)"
> >
<template #append> <template #append>
<q-icon name="search" /> <q-icon name="search" />
</template> </template>
</q-input> </q-input>
<!-- 🧾 Cari Kodu -->
<q-input <q-input
class="ol-filter-input"
dense
filled
v-model="store.filters.CurrAccCode" v-model="store.filters.CurrAccCode"
label="Cari Kodu"
clearable
/>
<!-- 📅 Sipariş Tarihi -->
<q-input
class="ol-filter-input" class="ol-filter-input"
dense dense
filled filled
v-model="store.filters.OrderDate" clearable
label="Sipariş Tarihi" label="Cari Kodu"
type="date"
/> />
<!-- 🔘 Butonlar --> <q-input
<div class="ol-filter-actions"> v-model="store.filters.OrderDate"
class="ol-filter-input"
dense
filled
type="date"
label="Sipariş Tarihi"
/>
<div class="ol-filter-actions">
<q-btn <q-btn
label="Temizle" label="Temizle"
icon="clear" icon="clear"
@@ -52,9 +43,7 @@
:disable="store.loading" :disable="store.loading"
@click="clearFilters" @click="clearFilters"
> >
<q-tooltip> <q-tooltip>Tüm filtreleri temizle</q-tooltip>
Tüm filtreleri temizle
</q-tooltip>
</q-btn> </q-btn>
<q-btn <q-btn
@@ -73,25 +62,31 @@
:disable="store.loading || store.filteredOrders.length === 0" :disable="store.loading || store.filteredOrders.length === 0"
@click="exportExcel" @click="exportExcel"
/> />
</div> </div>
<!-- 💰 Toplam -->
<div class="ol-filter-total"> <div class="ol-filter-total">
Toplam Görünen Sipariş Tutarı (USD): <div class="ol-total-line">
<strong> <span class="ol-total-label">Toplam USD:</span>
{{ store.totalVisibleUSD.toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }} <strong class="ol-total-value">
USD {{ store.totalVisibleUSD.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</strong> </strong>
</div>
<div class="ol-total-line">
<span class="ol-total-label">Paketlenen USD:</span>
<strong class="ol-total-value">
{{ store.totalPackedVisibleUSD.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</strong>
</div>
<div class="ol-total-line">
<span class="ol-total-label">Paketlenme %:</span>
<strong class="ol-total-value" :class="packRateClass(store.packedVisibleRatePct)">
{{ store.packedVisibleRatePct.toLocaleString('tr-TR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</strong>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 📋 ORDER LIST TABLE -->
<q-table <q-table
title="Mevcut Siparişler" title="Mevcut Siparişler"
class="ol-table" class="ol-table"
@@ -107,11 +102,8 @@
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
hide-bottom hide-bottom
> >
<!-- 📄 PDF + DURUM -->
<template #body-cell-IsCreditableConfirmed="props"> <template #body-cell-IsCreditableConfirmed="props">
<q-td :props="props" class="text-center q-gutter-sm"> <q-td :props="props" class="text-center q-gutter-sm">
<q-btn <q-btn
icon="picture_as_pdf" icon="picture_as_pdf"
color="red" color="red"
@@ -132,11 +124,9 @@
{{ props.row.IsCreditableConfirmed ? 'Onaylı' : 'Onaysız' }} {{ props.row.IsCreditableConfirmed ? 'Onaylı' : 'Onaysız' }}
</q-tooltip> </q-tooltip>
</q-icon> </q-icon>
</q-td> </q-td>
</template> </template>
<!-- 📅 Tarih -->
<template #body-cell-OrderDate="props"> <template #body-cell-OrderDate="props">
<q-td :props="props" class="text-center"> <q-td :props="props" class="text-center">
{{ formatDate(props.row.OrderDate) }} {{ formatDate(props.row.OrderDate) }}
@@ -149,27 +139,65 @@
</q-td> </q-td>
</template> </template>
<!-- 🧾 Cari Adı 2 Satır --> <template #body-cell-PackedAmount="props">
<q-td :props="props" class="text-right text-weight-medium">
{{ Number(props.row.PackedAmount || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }}
{{ props.row.DocCurrencyCode }}
</q-td>
</template>
<template #body-cell-PackedUSD="props">
<q-td :props="props" class="text-right text-weight-medium">
{{ Number(props.row.PackedUSD || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }} USD
</q-td>
</template>
<template #body-cell-PackedRatePct="props">
<q-td
:props="props"
class="text-right text-weight-bold ol-pack-rate-cell"
:class="packRateClass(props.row.PackedRatePct)"
>
{{ Number(props.row.PackedRatePct || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) }} %
</q-td>
</template>
<template #body-cell-CurrAccDescription="props"> <template #body-cell-CurrAccDescription="props">
<q-td :props="props" class="ol-col-cari ol-col-multiline"> <q-td :props="props" class="ol-col-cari">
{{ props.value }} <div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-MusteriTemsilcisi="props">
<q-td :props="props" class="ol-col-short">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value">
{{ props.value }}
</q-tooltip>
</q-td>
</template>
<template #body-cell-Piyasa="props">
<q-td :props="props" class="ol-col-short">
<div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value"> <q-tooltip v-if="props.value">
{{ props.value }} {{ props.value }}
</q-tooltip> </q-tooltip>
</q-td> </q-td>
</template> </template>
<!-- 📝 ıklama 5 Satır -->
<template #body-cell-Description="props"> <template #body-cell-Description="props">
<q-td :props="props" class="ol-col-desc ol-col-multiline"> <q-td :props="props" class="ol-col-desc">
{{ props.value }} <div class="ol-col-multiline">{{ props.value }}</div>
<q-tooltip v-if="props.value"> <q-tooltip v-if="props.value">
{{ props.value }} {{ props.value }}
</q-tooltip> </q-tooltip>
</q-td> </q-td>
</template> </template>
<!-- 🔗 -->
<template #body-cell-select="props"> <template #body-cell-select="props">
<q-td :props="props" class="text-center"> <q-td :props="props" class="text-center">
<q-btn <q-btn
@@ -184,14 +212,17 @@
</q-btn> </q-btn>
</q-td> </q-td>
</template> </template>
</q-table> </q-table>
<!-- HATA -->
<q-banner v-if="store.error" class="bg-red text-white q-mt-sm"> <q-banner v-if="store.error" class="bg-red text-white q-mt-sm">
{{ store.error }} Hata: {{ store.error }}
</q-banner> </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> </q-page>
</template> </template>
@@ -199,47 +230,28 @@
import { onMounted, watch } from 'vue' import { onMounted, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { useOrderListStore } from 'src/stores/OrdernewListStore' import { useOrderListStore } from 'src/stores/OrdernewListStore'
import { useAuthStore } from 'src/stores/authStore' import { useAuthStore } from 'src/stores/authStore'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
const { canRead, canWrite, canUpdate } = usePermission() const { canRead } = usePermission()
const canReadOrder = canRead('order')
const canReadOrder = canRead('order')
const canWriteOrder = canWrite('order')
const canUpdateOrder = canUpdate('order')
/* =========================
INIT
========================= */
const router = useRouter() const router = useRouter()
const $q = useQuasar() const $q = useQuasar()
// ⚠️ ÖNCE store tanımlanır
const store = useOrderListStore() const store = useOrderListStore()
/* =========================
SEARCH DEBOUNCE
========================= */
let searchTimer = null let searchTimer = null
watch( watch(
() => store.filters.search, () => store.filters.search,
() => { () => {
clearTimeout(searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => { searchTimer = setTimeout(() => {
store.fetchOrders() store.fetchOrders()
}, 400) }, 400)
} }
) )
/* =========================
HELPERS
========================= */
function exportExcel () { function exportExcel () {
const auth = useAuthStore() const auth = useAuthStore()
@@ -276,80 +288,85 @@ function exportExcel () {
function formatDate (s) { function formatDate (s) {
if (!s) return '' if (!s) return ''
const [y, m, d] = s.split('-') const [y, m, d] = String(s).split('-')
if (!y || !m || !d) return s
return `${d}.${m}.${y}` return `${d}.${m}.${y}`
} }
/* ========================= function packRateClass (value) {
TABLE COLUMNS const pct = Number(value || 0)
========================= */ if (pct <= 50) return 'pack-rate-danger'
if (pct < 100) return 'pack-rate-warn'
return 'pack-rate-ok'
}
const columns = [ const columns = [
{ name: 'select', label: '', field: 'select', align: 'center', sortable: false }, { name: 'select', label: '', field: 'select', align: 'center', sortable: false },
{ name: 'OrderNumber', label: 'Sipariş No', field: 'OrderNumber', align: 'left', sortable: true, style: 'min-width:108px;white-space:nowrap', headerStyle: 'min-width:108px;white-space:nowrap' },
{ name: 'OrderNumber', label: 'Sipariş No', field: 'OrderNumber', align: 'left', sortable: true }, { name: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'center', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' },
{ name: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'center', sortable: true }, { name: 'CurrAccCode', label: 'Cari Kod', field: 'CurrAccCode', align: 'left', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' },
{ name: 'CurrAccDescription', label: 'Cari Adı', field: 'CurrAccDescription', align: 'left', sortable: true, classes: 'ol-col-cari', headerClasses: 'ol-col-cari', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
{ name: 'CurrAccCode', label: 'Cari Kod', field: 'CurrAccCode', align: 'left', sortable: true }, { name: 'MusteriTemsilcisi', label: 'Temsilci', field: 'MusteriTemsilcisi', align: 'left', sortable: true, classes: 'ol-col-short', headerClasses: 'ol-col-short', style: 'width:88px;max-width:88px', headerStyle: 'width:88px;max-width:88px' },
{ name: 'Piyasa', label: 'Piyasa', field: 'Piyasa', align: 'left', sortable: true, classes: 'ol-col-short', headerClasses: 'ol-col-short', style: 'width:72px;max-width:72px', headerStyle: 'width:72px;max-width:72px' },
{ { name: 'CreditableConfirmedDate', label: 'Onay', field: 'CreditableConfirmedDate', align: 'center', sortable: true, style: 'min-width:86px;white-space:nowrap', headerStyle: 'min-width:86px;white-space:nowrap' },
name: 'CurrAccDescription', { name: 'DocCurrencyCode', label: 'PB', field: 'DocCurrencyCode', align: 'center', sortable: true, style: 'min-width:46px;white-space:nowrap', headerStyle: 'min-width:46px;white-space:nowrap' },
label: 'Cari Adı',
field: 'CurrAccDescription',
align: 'left',
sortable: true,
classes: 'ol-col-cari',
headerClasses: 'ol-col-cari',
style: 'max-width:200px'
},
{ name: 'MusteriTemsilcisi', label: 'Temsilci', field: 'MusteriTemsilcisi', align: 'left', sortable: true },
{ name: 'Piyasa', label: 'Piyasa', field: 'Piyasa', align: 'left', sortable: true },
{ name: 'CreditableConfirmedDate', label: 'Onay', field: 'CreditableConfirmedDate', align: 'center', sortable: true },
{ name: 'DocCurrencyCode', label: 'PB', field: 'DocCurrencyCode', align: 'center', sortable: true },
{ {
name: 'TotalAmount', name: 'TotalAmount',
label: 'Tutar', label: 'Tutar',
field: 'TotalAmount', field: 'TotalAmount',
align: 'right', align: 'right',
sortable: true, sortable: true,
format: (val, row) => style: 'min-width:120px;white-space:nowrap',
Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + headerStyle: 'min-width:120px;white-space:nowrap',
' ' + row.DocCurrencyCode format: (val, row) => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' ' + row.DocCurrencyCode
}, },
{ {
name: 'TotalAmountUSD', name: 'TotalAmountUSD',
label: 'Tutar (USD)', label: 'Tutar (USD)',
field: 'TotalAmountUSD', field: 'TotalAmountUSD',
align: 'right', align: 'right',
sortable: true, sortable: true,
format: val => style: 'min-width:120px;white-space:nowrap',
Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD' headerStyle: 'min-width:120px;white-space:nowrap',
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD'
}, },
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
{ {
name: 'Description', name: 'PackedAmount',
label: 'ıklama', label: 'Paketlenen',
field: 'Description', field: 'PackedAmount',
align: 'left', align: 'right',
sortable: false, sortable: true,
classes: 'ol-col-desc', style: 'min-width:120px;white-space:nowrap',
headerClasses: 'ol-col-desc', headerStyle: 'min-width:120px;white-space:nowrap',
style: 'max-width:220px' format: (val, row) => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' ' + row.DocCurrencyCode
}, },
{
name: 'PackedUSD',
label: 'Paketlenen (USD)',
field: 'PackedUSD',
align: 'right',
sortable: true,
style: 'min-width:120px;white-space:nowrap',
headerStyle: 'min-width:120px;white-space:nowrap',
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD'
},
{
name: 'PackedRatePct',
label: 'Paketlenme %',
field: 'PackedRatePct',
align: 'right',
sortable: true,
classes: 'ol-pack-rate-cell',
headerClasses: 'ol-pack-rate-cell',
style: 'min-width:96px;white-space:nowrap',
headerStyle: 'min-width:96px;white-space:nowrap',
format: val => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' %'
},
{ name: 'IsCreditableConfirmed', label: 'Durum', field: 'IsCreditableConfirmed', align: 'center', sortable: true },
{ name: 'Description', label: 'Açıklama', field: 'Description', align: 'left', sortable: false, classes: 'ol-col-desc', headerClasses: 'ol-col-desc', style: 'width:160px;max-width:160px', headerStyle: 'width:160px;max-width:160px' },
{ name: 'pdf', label: 'PDF', field: 'pdf', align: 'center', sortable: false } { name: 'pdf', label: 'PDF', field: 'pdf', align: 'center', sortable: false }
] ]
/* =========================
ACTIONS
========================= */
function selectOrder (row) { function selectOrder (row) {
if (!row?.OrderHeaderID) { if (!row?.OrderHeaderID) {
$q.notify({ type: 'warning', message: 'OrderHeaderID bulunamadı' }) $q.notify({ type: 'warning', message: 'OrderHeaderID bulunamadı' })
@@ -397,12 +414,151 @@ function clearFilters () {
}) })
} }
/* =========================
INIT LOAD
========================= */
onMounted(() => { onMounted(() => {
store.fetchOrders() store.fetchOrders()
}) })
</script> </script>
<style scoped>
.ol-page {
padding: 10px;
}
.ol-filter-bar {
margin-bottom: 8px;
}
.ol-filter-row {
display: flex;
flex-wrap: nowrap;
gap: 10px;
align-items: center;
}
.ol-filter-input {
min-width: 118px;
width: 136px;
flex: 0 0 136px;
}
.ol-search {
min-width: 240px;
max-width: 420px;
flex: 1 1 360px;
}
.ol-filter-actions {
display: flex;
gap: 8px;
flex-wrap: nowrap;
flex: 0 0 auto;
}
.ol-filter-total {
margin-left: auto;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
gap: 2px;
min-width: 250px;
line-height: 1.2;
flex: 0 0 auto;
align-self: center;
}
.ol-total-line {
display: flex;
align-items: baseline;
gap: 8px;
white-space: nowrap;
}
.ol-total-label {
font-size: 12px;
color: #4b5563;
}
.ol-total-value {
font-size: 13px;
}
.ol-table :deep(.q-table thead th) {
font-size: 11px;
padding: 4px 6px;
white-space: nowrap;
}
.ol-table :deep(.q-table tbody td) {
font-size: 11px;
padding: 3px 6px;
}
.ol-col-multiline {
display: block;
white-space: normal !important;
word-break: break-word;
line-height: 1.15;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
max-height: 2.35em;
}
.ol-col-cari {
max-width: 160px;
}
.ol-col-short {
max-width: 88px;
}
.ol-col-desc {
max-width: 160px;
}
.ol-pack-rate-cell {
font-weight: 700;
}
.pack-rate-danger {
color: #c62828;
}
.pack-rate-warn {
color: #8a6d00;
}
.pack-rate-ok {
color: #1f7a4f;
}
@media (max-width: 1440px) {
.ol-filter-row {
flex-wrap: wrap;
align-items: flex-start;
}
.ol-filter-actions {
flex-wrap: wrap;
}
.ol-filter-input {
flex: 1 1 140px;
}
.ol-filter-total {
min-width: 100%;
margin-left: 0;
align-items: flex-start;
margin-top: 6px;
}
.ol-total-line {
width: 100%;
justify-content: space-between;
}
}
</style>

View File

@@ -68,6 +68,13 @@
Rol + Departman Yetkilendirme Rol + Departman Yetkilendirme
</div> </div>
<q-btn
flat
icon="list"
label="Liste"
@click="goList"
/>
<q-btn <q-btn
v-if="canUpdateUser" v-if="canUpdateUser"
color="primary" color="primary"
@@ -178,12 +185,15 @@
<script setup> <script setup>
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Notify } from 'quasar' import { Notify } from 'quasar'
import api from 'src/services/api' import api from 'src/services/api'
import { usePermission } from 'src/composables/usePermission' import { usePermission } from 'src/composables/usePermission'
const { canUpdate } = usePermission() const { canUpdate } = usePermission()
const canUpdateUser = canUpdate('user') const canUpdateUser = canUpdate('user')
const route = useRoute()
const router = useRouter()
/* ================= STATE ================= */ /* ================= STATE ================= */
@@ -237,6 +247,27 @@ const columns = [
let matrixLoading = false let matrixLoading = false
function goList () {
router.push({ name: 'role-dept-permissions-list' })
}
function applyRouteSelection () {
const qRole = String(route.query.roleId || '').trim()
const qDept = String(route.query.deptCode || '').trim()
if (/^\d+$/.test(qRole) && Number(qRole) > 0) {
roleId.value = qRole
}
if (qDept) {
deptCode.value = qDept
}
if (roleId.value && deptCode.value) {
loadMatrix()
}
}
/* ================= LOOKUPS ================= */ /* ================= LOOKUPS ================= */
@@ -425,14 +456,19 @@ function toggleColumn (key, val) {
/* ================= INIT ================= */ /* ================= INIT ================= */
onMounted(() => { onMounted(async () => {
loadLookups() await loadLookups()
applyRouteSelection()
}) })
watch(roleId, v => console.log('ROLE_ID >>>', v)) watch(roleId, v => console.log('ROLE_ID >>>', v))
watch(deptCode, v => console.log('DEPT >>>', v)) watch(deptCode, v => console.log('DEPT >>>', v))
watch(
() => [route.query.roleId, route.query.deptCode],
() => {
if (!lookupsLoaded.value) return
applyRouteSelection()
}
)
</script> </script>

View File

@@ -85,6 +85,20 @@ const routes = [
{ {
path: 'role-dept-permissions', path: 'role-dept-permissions',
name: 'role-dept-permissions', name: 'role-dept-permissions',
component: () => import('pages/RoleDepartmentPermissionGateway.vue'),
meta: { permission: 'user:update' }
},
{
path: 'role-dept-permissions/list',
name: 'role-dept-permissions-list',
component: () => import('pages/RoleDepartmentPermissionList.vue'),
meta: { permission: 'user:update' }
},
{
path: 'role-dept-permissions/editor',
name: 'role-dept-permissions-editor',
component: () => import('pages/RoleDepartmentPermissionPage.vue'), component: () => import('pages/RoleDepartmentPermissionPage.vue'),
meta: { permission: 'user:update' } meta: { permission: 'user:update' }
}, },
@@ -228,6 +242,13 @@ const routes = [
meta: { permission: 'order:view' } meta: { permission: 'order:view' }
}, },
{
path: 'order-bulk-close',
name: 'order-bulk-close',
component: () => import('pages/OrderBulkClose.vue'),
meta: { permission: 'order:update' }
},
{ {
path: 'order-pdf/:id', path: 'order-pdf/:id',
name: 'order-pdf', name: 'order-pdf',