diff --git a/svc/main.go b/svc/main.go index ca41e3d..0894815 100644 --- a/svc/main.go +++ b/svc/main.go @@ -262,6 +262,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router rdPerm := "/api/roles/{roleId}/departments/{deptCode}/permissions" rdHandler := routes.NewRoleDepartmentPermissionHandler(pgDB) + bindV3(r, pgDB, + "/api/role-dept-permissions/list", "GET", + "user", "update", + wrapV3(http.HandlerFunc(rdHandler.List)), + ) bindV3(r, pgDB, rdPerm, "GET", "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/get/{id}", "GET", "view", routes.GetOrderByIDHandler(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/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)}, {"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)}, diff --git a/svc/queries/permission_role_dept.go b/svc/queries/permission_role_dept.go index 49f80ec..a3020ed 100644 --- a/svc/queries/permission_role_dept.go +++ b/svc/queries/permission_role_dept.go @@ -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 // ====================================================== @@ -45,3 +106,20 @@ SELECT FROM mk_sys_modules 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) +` diff --git a/svc/routes/role_department_permissions.go b/svc/routes/role_department_permissions.go index 1d9efbe..2299dd0 100644 --- a/svc/routes/role_department_permissions.go +++ b/svc/routes/role_department_permissions.go @@ -11,6 +11,7 @@ import ( "log" "net/http" "strconv" + "strings" "github.com/gorilla/mux" ) @@ -24,6 +25,30 @@ type Row struct { 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 { DB *sql.DB 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 ====================================================== */ diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index fbc5d65..313403f 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -208,6 +208,11 @@ const menuItems = [ label: 'Siparişler', to: '/app/order-gateway', permission: 'order:view' + }, + { + label: 'Tamamlanan Siparişleri Toplu Kapatma', + to: '/app/order-bulk-close', + permission: 'order:update' } ] }, diff --git a/ui/src/pages/OrderGateway.vue b/ui/src/pages/OrderGateway.vue index 8fe1305..62c2b88 100644 --- a/ui/src/pages/OrderGateway.vue +++ b/ui/src/pages/OrderGateway.vue @@ -57,6 +57,13 @@ label="MEVCUT SİPARİŞİ AÇ" @click="goOrderList" /> + @@ -80,10 +87,11 @@ import { useQuasar } from 'quasar' import { useOrderEntryStore } from 'src/stores/orderentryStore' import { usePermission } from 'src/composables/usePermission' -const { canRead, canWrite } = usePermission() +const { canRead, canWrite, canUpdate } = usePermission() const canReadOrder = canRead('order') const canWriteOrder = canWrite('order') +const canUpdateOrder = canUpdate('order') const router = useRouter() const $q = useQuasar() @@ -182,6 +190,10 @@ function goOrderList () { router.push({ name: 'order-list' }) } +function goBulkClose () { + router.push({ name: 'order-bulk-close' }) +} + /* =========================================================== 🧹 NEW Taslağı Temizle (SADECE NEW) =========================================================== */ diff --git a/ui/src/pages/OrderList.vue b/ui/src/pages/OrderList.vue index bc3f043..33f6d72 100644 --- a/ui/src/pages/OrderList.vue +++ b/ui/src/pages/OrderList.vue @@ -1,49 +1,40 @@ @@ -199,47 +230,28 @@ import { onMounted, watch } from 'vue' import { useRouter } from 'vue-router' import { useQuasar } from 'quasar' - import { useOrderListStore } from 'src/stores/OrdernewListStore' import { useAuthStore } from 'src/stores/authStore' import { usePermission } from 'src/composables/usePermission' -const { canRead, canWrite, canUpdate } = usePermission() - -const canReadOrder = canRead('order') -const canWriteOrder = canWrite('order') -const canUpdateOrder = canUpdate('order') - -/* ========================= - INIT -========================= */ +const { canRead } = usePermission() +const canReadOrder = canRead('order') const router = useRouter() const $q = useQuasar() - -// ⚠️ ÖNCE store tanımlanır const store = useOrderListStore() -/* ========================= - SEARCH DEBOUNCE -========================= */ - let searchTimer = null - watch( () => store.filters.search, () => { clearTimeout(searchTimer) - searchTimer = setTimeout(() => { store.fetchOrders() }, 400) } ) -/* ========================= - HELPERS -========================= */ function exportExcel () { const auth = useAuthStore() @@ -276,80 +288,85 @@ function exportExcel () { function formatDate (s) { 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}` } -/* ========================= - TABLE COLUMNS -========================= */ +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' +} const columns = [ { name: 'select', label: '', field: 'select', align: 'center', sortable: false }, - - { name: 'OrderNumber', label: 'Sipariş No', field: 'OrderNumber', align: 'left', sortable: true }, - { name: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'center', sortable: true }, - - { name: 'CurrAccCode', label: 'Cari Kod', field: 'CurrAccCode', align: 'left', sortable: true }, - - { - name: 'CurrAccDescription', - 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: '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: 'OrderDate', label: 'Tarih', field: 'OrderDate', align: 'center', sortable: true, style: 'min-width:82px;white-space:nowrap', headerStyle: 'min-width:82px;white-space:nowrap' }, + { 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: '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: 'DocCurrencyCode', label: 'PB', field: 'DocCurrencyCode', align: 'center', sortable: true, style: 'min-width:46px;white-space:nowrap', headerStyle: 'min-width:46px;white-space:nowrap' }, { name: 'TotalAmount', label: 'Tutar', field: 'TotalAmount', align: 'right', sortable: true, - format: (val, row) => - Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + - ' ' + row.DocCurrencyCode + style: 'min-width:120px;white-space:nowrap', + headerStyle: 'min-width:120px;white-space:nowrap', + format: (val, row) => Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' ' + row.DocCurrencyCode }, - { name: 'TotalAmountUSD', label: 'Tutar (USD)', field: 'TotalAmountUSD', align: 'right', sortable: true, - format: val => - Number(val || 0).toLocaleString('tr-TR', { minimumFractionDigits: 2 }) + ' USD' + 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: '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: 'max-width:220px' + name: 'PackedAmount', + label: 'Paketlenen', + field: 'PackedAmount', + align: 'right', + sortable: true, + style: 'min-width:120px;white-space:nowrap', + headerStyle: 'min-width:120px;white-space:nowrap', + 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 } ] -/* ========================= - ACTIONS -========================= */ - function selectOrder (row) { if (!row?.OrderHeaderID) { $q.notify({ type: 'warning', message: 'OrderHeaderID bulunamadı' }) @@ -397,12 +414,151 @@ function clearFilters () { }) } -/* ========================= - INIT LOAD -========================= */ - onMounted(() => { store.fetchOrders() }) + diff --git a/ui/src/pages/RoleDepartmentPermissionPage.vue b/ui/src/pages/RoleDepartmentPermissionPage.vue index 867153a..8d2b0ad 100644 --- a/ui/src/pages/RoleDepartmentPermissionPage.vue +++ b/ui/src/pages/RoleDepartmentPermissionPage.vue @@ -68,6 +68,13 @@ Rol + Departman Yetkilendirme + + import { ref, onMounted, watch } from 'vue' +import { useRoute, useRouter } from 'vue-router' import { Notify } from 'quasar' import api from 'src/services/api' import { usePermission } from 'src/composables/usePermission' const { canUpdate } = usePermission() const canUpdateUser = canUpdate('user') +const route = useRoute() +const router = useRouter() /* ================= STATE ================= */ @@ -237,6 +247,27 @@ const columns = [ 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 ================= */ @@ -425,14 +456,19 @@ function toggleColumn (key, val) { /* ================= INIT ================= */ -onMounted(() => { - loadLookups() +onMounted(async () => { + await loadLookups() + applyRouteSelection() }) watch(roleId, v => console.log('ROLE_ID >>>', v)) watch(deptCode, v => console.log('DEPT >>>', v)) +watch( + () => [route.query.roleId, route.query.deptCode], + () => { + if (!lookupsLoaded.value) return + applyRouteSelection() + } +) - - - diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 0f50c21..e2fa415 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -85,6 +85,20 @@ const routes = [ { path: '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'), meta: { permission: 'user:update' } }, @@ -228,6 +242,13 @@ const routes = [ 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', name: 'order-pdf',