Merge branch 'feat/handle-role' into develop
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 6s

This commit is contained in:
puriphatt 2025-07-07 12:44:10 +07:00
commit f08c83c98b
44 changed files with 430 additions and 237 deletions

View file

@ -89,15 +89,7 @@ defineProps<{
</div> </div>
</div> </div>
</div> </div>
<div <q-separator />
style="
display: block;
width: 100%;
height: 1px;
background: hsla(0 0% 0% / 0.1);
margin-bottom: var(--size-2);
"
/>
<slot name="data"></slot> <slot name="data"></slot>
<template v-if="!$slots.data"> <template v-if="!$slots.data">
<div <div

View file

@ -22,6 +22,8 @@ const prop = withDefaults(
inTable?: boolean; inTable?: boolean;
addButton?: boolean; addButton?: boolean;
prefixId?: string; prefixId?: string;
hideAction?: boolean;
hideDelete?: boolean;
}>(), }>(),
{ {
gridView: false, gridView: false,
@ -265,9 +267,10 @@ defineEmits<{
@click.stop="$emit('view', props.row)" @click.stop="$emit('view', props.row)"
/> />
<KebabAction <KebabAction
v-if="!inTable" v-if="!inTable && !hideAction"
:id-name="props.row.firstName" :id-name="props.row.firstName"
:status="props.row.status" :status="props.row.status"
:hide-delete="hideDelete"
@view="$emit('view', props.row)" @view="$emit('view', props.row)"
@edit="$emit('edit', props.row)" @edit="$emit('edit', props.row)"
@delete="$emit('delete', props.row)" @delete="$emit('delete', props.row)"
@ -280,9 +283,11 @@ defineEmits<{
<template v-slot:item="props"> <template v-slot:item="props">
<div class="col-12 col-md-3 col-sm-6"> <div class="col-12 col-md-3 col-sm-6">
<PersonCard <PersonCard
history
:hide-delete="hideDelete"
:hide-action="hideAction"
:id="`card-${props.row.firstNameEN}`" :id="`card-${props.row.firstNameEN}`"
:field-selected="fieldSelected" :field-selected="fieldSelected"
history
:prefix-id="props.row.firstNameEN ?? props.rowIndex" :prefix-id="props.row.firstNameEN ?? props.rowIndex"
:data="{ :data="{
code: props.row.code, code: props.row.code,

View file

@ -27,6 +27,7 @@ import { QField } from 'quasar';
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
onDrawer?: boolean; onDrawer?: boolean;
hideAction?: boolean;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -201,6 +202,7 @@ onMounted(async () => {
:class="{ 'q-ml-lg': $q.screen.gt.xs, 'q-mt-sm': $q.screen.lt.sm }" :class="{ 'q-ml-lg': $q.screen.gt.xs, 'q-mt-sm': $q.screen.lt.sm }"
> >
<ToggleButton <ToggleButton
:disable="hideAction"
class="q-mr-sm" class="q-mr-sm"
two-way two-way
:model-value="flowData.status !== 'INACTIVE'" :model-value="flowData.status !== 'INACTIVE'"

View file

@ -48,6 +48,7 @@ defineProps<{
readonly?: boolean; readonly?: boolean;
onDrawer?: boolean; onDrawer?: boolean;
inputOnly?: boolean; inputOnly?: boolean;
disableToggle?: boolean;
}>(); }>();
defineEmits<{ defineEmits<{
@ -76,6 +77,7 @@ defineEmits<{
<ToggleButton <ToggleButton
class="q-mr-sm" class="q-mr-sm"
two-way two-way
:disable="disableToggle"
:model-value="status !== 'INACTIVE'" :model-value="status !== 'INACTIVE'"
@click=" @click="
() => { () => {
@ -195,8 +197,8 @@ defineEmits<{
} }
:deep( :deep(
.q-item__section.column.q-item__section--side.justify-center.q-item__section--avatar.q-focusable.relative-position.cursor-pointer .q-item__section.column.q-item__section--side.justify-center.q-item__section--avatar.q-focusable.relative-position.cursor-pointer
) { ) {
justify-content: start !important; justify-content: start !important;
padding-right: 8px !important; padding-right: 8px !important;
padding-top: 16px; padding-top: 16px;
@ -208,15 +210,15 @@ defineEmits<{
} }
:deep( :deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) { ) {
color: var(--brand-1); color: var(--brand-1);
} }
:deep( :deep(
.q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.expansion-rounded.surface-2 .q-item.q-item-type.row.no-wrap.q-item--dense.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable.expansion-rounded.surface-2
.q-focus-helper .q-focus-helper
) { ) {
visibility: hidden; visibility: hidden;
} }

View file

@ -19,6 +19,7 @@ const props = withDefaults(
page?: number; page?: number;
pageSize?: number; pageSize?: number;
hideBtnPreview?: boolean; hideBtnPreview?: boolean;
hideAction?: boolean;
}>(), }>(),
{ {
row: () => [], row: () => [],
@ -149,6 +150,7 @@ defineEmits<{
/> />
<KebabAction <KebabAction
v-if="!hideAction"
:idName="`btn-kebab-${props.row.workName}`" :idName="`btn-kebab-${props.row.workName}`"
status="'ACTIVE'" status="'ACTIVE'"
hide-toggle hide-toggle

View file

@ -229,6 +229,7 @@ const smallBanner = ref(false);
<ToggleButton <ToggleButton
v-if="useToggle" v-if="useToggle"
:disable="readonly"
two-way two-way
:model-value="toggleStatus !== 'INACTIVE'" :model-value="toggleStatus !== 'INACTIVE'"
@click="$emit('update:toggleStatus', toggleStatus)" @click="$emit('update:toggleStatus', toggleStatus)"

View file

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { BranchWithChildren } from 'stores/branch/types'; import { BranchWithChildren } from 'stores/branch/types';
import KebabAction from './shared/KebabAction.vue'; import KebabAction from './shared/KebabAction.vue';
import { isRoleInclude } from 'stores/utils';
const nodes = defineModel<(any | BranchWithChildren)[]>('nodes', { const nodes = defineModel<(any | BranchWithChildren)[]>('nodes', {
default: [], default: [],
@ -120,11 +119,7 @@ defineEmits<{
/> />
<q-btn <q-btn
v-if=" v-if="node.isHeadOffice && typeTree === 'branch'"
node.isHeadOffice &&
typeTree === 'branch' &&
isRoleInclude(['head_of_admin', 'admin', 'system'])
"
:id="`create-sub-branch-btn-${node.name}`" :id="`create-sub-branch-btn-${node.name}`"
@click.stop="$emit('create', node)" @click.stop="$emit('create', node)"
icon="mdi-file-plus-outline" icon="mdi-file-plus-outline"

View file

@ -23,6 +23,8 @@ defineProps<{
history?: boolean; history?: boolean;
prefixId?: string; prefixId?: string;
separateEnter?: boolean; separateEnter?: boolean;
hideAction?: boolean;
hideDelete?: boolean;
}>(); }>();
defineEmits<{ defineEmits<{
@ -76,8 +78,10 @@ defineEmits<{
/> />
<KebabAction <KebabAction
v-if="!hideAction"
:id-name="prefixId" :id-name="prefixId"
:status="disabled ? 'INACTIVE' : 'ACTIVE'" :status="disabled ? 'INACTIVE' : 'ACTIVE'"
:hide-delete="hideDelete"
@view=" @view="
separateEnter separateEnter
? $emit('viewCard', 'INFO') ? $emit('viewCard', 'INFO')

View file

@ -7,7 +7,7 @@ import useMyBranch from 'stores/my-branch';
import { getUserId, getRole } from 'src/services/keycloak'; import { getUserId, getRole } from 'src/services/keycloak';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { isRoleInclude } from 'src/stores/utils'; import { canAccess } from 'src/stores/utils';
type Menu = { type Menu = {
label: string; label: string;
@ -71,82 +71,41 @@ function initMenu() {
{ {
label: 'branch', label: 'branch',
route: '/branch-management', route: '/branch-management',
hidden: !isRoleInclude([ hidden: !canAccess('branch'),
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
]),
}, },
{ {
label: 'personnel', label: 'personnel',
route: '/personnel-management', route: '/personnel-management',
hidden: !isRoleInclude([ hidden: !canAccess('personnel'),
'owner',
'system',
'head_of_admin',
'admin',
'branch_manager',
]),
}, },
{ {
label: 'workflow', label: 'workflow',
route: '/workflow', route: '/workflow',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']), hidden: !canAccess('workflow'),
}, },
{ {
label: 'property', label: 'property',
route: '/property', route: '/property',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']), hidden: !canAccess('workflow'),
}, },
{ {
label: 'productService', label: 'productService',
route: '/product-service', route: '/product-service',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'head_of_sale',
'sale',
]),
}, },
{ {
label: 'customer', label: 'customer',
route: '/customer-management', route: '/customer-management',
hidden: !isRoleInclude([ hidden: !canAccess('customer'),
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
}, },
{ {
label: 'agencies', label: 'agencies',
route: '/agencies-management', route: '/agencies-management',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
}, },
], ],
}, },
{ {
label: 'menu.sales', label: 'menu.sales',
icon: 'mdi-store-settings-outline', icon: 'mdi-store-settings-outline',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
children: [ children: [
{ label: 'quotation', route: '/quotation' }, { label: 'quotation', route: '/quotation' },
{ label: 'invoice', route: '/invoice' }, { label: 'invoice', route: '/invoice' },
@ -169,16 +128,6 @@ function initMenu() {
label: 'menu.account', label: 'menu.account',
icon: 'mdi-bank-outline', icon: 'mdi-bank-outline',
disabled: false, disabled: false,
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
children: [ children: [
{ label: 'receipt', route: '/receipt' }, { label: 'receipt', route: '/receipt' },
{ label: 'creditNote', route: '/credit-note' }, { label: 'creditNote', route: '/credit-note' },
@ -200,10 +149,13 @@ function initMenu() {
{ {
label: 'menu.overall', label: 'menu.overall',
icon: 'mdi-monitor-dashboard', icon: 'mdi-monitor-dashboard',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin', 'executive']),
children: [ children: [
{ label: 'report', route: '/report' }, { label: 'report', route: '/report' },
{ label: 'dashboard', route: '/dash-board' }, {
label: 'dashboard',
route: '/dash-board',
hidden: !canAccess('dashBoard'),
},
], ],
}, },

View file

@ -10,7 +10,7 @@ import type { QTableProps, QTableSlots } from 'quasar';
import { resetScrollBar } from 'src/stores/utils'; import { resetScrollBar } from 'src/stores/utils';
import useBranchStore from 'stores/branch'; import useBranchStore from 'stores/branch';
import useFlowStore from 'stores/flow'; import useFlowStore from 'stores/flow';
import { isRoleInclude } from 'stores/utils'; import { isRoleInclude, canAccess } from 'stores/utils';
import { import {
BranchWithChildren, BranchWithChildren,
BranchCreate, BranchCreate,
@ -1050,7 +1050,7 @@ watch(currentHq, () => {
{{ $t('branch.allBranch') }} {{ $t('branch.allBranch') }}
</div> </div>
<q-btn <q-btn
v-if="isRoleInclude(['head_of_admin', 'admin', 'system'])" v-if="isRoleInclude(['system'])"
round round
flat flat
size="md" size="md"
@ -1561,6 +1561,16 @@ watch(currentHq, () => {
</q-td> </q-td>
<q-td> <q-td>
<KebabAction <KebabAction
v-if="
isRoleInclude([
'system',
'head_of_admin',
'admin',
'executive',
'accountant',
]) ||
(canAccess('branch') && currentHq.id)
"
:status="props.row.status" :status="props.row.status"
:idName="props.row.name" :idName="props.row.name"
@view=" @view="
@ -1702,6 +1712,16 @@ watch(currentHq, () => {
> >
<template v-slot:action> <template v-slot:action>
<KebabAction <KebabAction
v-if="
isRoleInclude([
'system',
'head_of_admin',
'admin',
'executive',
'accountant',
]) ||
(canAccess('branch') && currentHq.id)
"
:status="props.row.status" :status="props.row.status"
:idName="props.row.name" :idName="props.row.name"
@view=" @view="

View file

@ -9,7 +9,7 @@ import { baseUrl } from 'src/stores/utils';
import useCustomerStore from 'stores/customer'; import useCustomerStore from 'stores/customer';
import useFlowStore from 'stores/flow'; import useFlowStore from 'stores/flow';
import useOptionStore from 'stores/options'; import useOptionStore from 'stores/options';
import { dialog } from 'stores/utils'; import { dialog, canAccess } from 'stores/utils';
import { Status } from 'stores/types'; import { Status } from 'stores/types';
import { Employee } from 'stores/employee/types'; import { Employee } from 'stores/employee/types';
@ -285,7 +285,7 @@ watch(
<template> <template>
<FloatingActionButton <FloatingActionButton
style="z-index: 999" style="z-index: 999"
v-if="$route.name !== 'CustomerManagement'" v-if="$route.name !== 'CustomerManagement' && canAccess('customer', 'edit')"
@click="openEmployerBranchForm('create')" @click="openEmployerBranchForm('create')"
hide-icon hide-icon
></FloatingActionButton> ></FloatingActionButton>
@ -615,7 +615,7 @@ watch(
<div class="text-center"> <div class="text-center">
<TableEmpoloyee <TableEmpoloyee
:prefix-id="props.row.registerName || props.row.firstName" :prefix-id="props.row.registerName || props.row.firstName"
add-button :add-button="canAccess('customer', 'edit')"
in-table in-table
:list-employee="listEmployee" :list-employee="listEmployee"
:columns-employee="columnsEmployee" :columns-employee="columnsEmployee"
@ -759,7 +759,10 @@ watch(
/> />
<DeleteButton <DeleteButton
icon-only icon-only
v-if="customerBranchFormState.dialogType === 'info'" v-if="
customerBranchFormState.dialogType === 'info' &&
canAccess('customer', 'edit')
"
@click=" @click="
() => { () => {
deleteBranchById(customerBranchFormData.id || ''); deleteBranchById(customerBranchFormData.id || '');

View file

@ -6,7 +6,7 @@ import { useRoute, useRouter } from 'vue-router';
import { getUserId, getRole } from 'src/services/keycloak'; import { getUserId, getRole } from 'src/services/keycloak';
import { baseUrl, setPrefixName, waitAll } from 'src/stores/utils'; import { baseUrl, setPrefixName, waitAll } from 'src/stores/utils';
import { dateFormat } from 'src/utils/datetime'; import { dateFormat } from 'src/utils/datetime';
import { dialogCheckData } from 'stores/utils'; import { dialogCheckData, canAccess } from 'stores/utils';
import useOcrStore from 'stores/ocr'; import useOcrStore from 'stores/ocr';
import useCustomerStore from 'stores/customer'; import useCustomerStore from 'stores/customer';
@ -397,7 +397,11 @@ async function fetchListEmployee(opt?: {
employeeStats.value = await employeeStore.getStatsEmployee(); employeeStats.value = await employeeStore.getStatsEmployee();
} }
async function triggerChangeStatus(id: string, status: string) { async function triggerChangeStatus(
id: string,
status: string,
employeeName?: string,
) {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
dialog({ dialog({
color: status !== 'INACTIVE' ? 'warning' : 'info', color: status !== 'INACTIVE' ? 'warning' : 'info',
@ -412,7 +416,11 @@ async function triggerChangeStatus(id: string, status: string) {
: t('dialog.message.confirmChangeStatusOn'), : t('dialog.message.confirmChangeStatusOn'),
action: async () => { action: async () => {
if (currentTab.value === 'employee') { if (currentTab.value === 'employee') {
await toggleStatusEmployee(id, status === 'INACTIVE' ? false : true) await toggleStatusEmployee(
id,
status === 'INACTIVE' ? false : true,
employeeName,
)
.then(resolve) .then(resolve)
.catch(reject); .catch(reject);
} else { } else {
@ -426,9 +434,14 @@ async function triggerChangeStatus(id: string, status: string) {
}); });
} }
async function toggleStatusEmployee(id: string, status: boolean) { async function toggleStatusEmployee(
id: string,
status: boolean,
employeeName: string,
) {
const res = await employeeStore.editById(id, { const res = await employeeStore.editById(id, {
status: !status ? 'ACTIVE' : 'INACTIVE', status: !status ? 'ACTIVE' : 'INACTIVE',
firstNameEN: employeeName,
}); });
if (res && employeeFormState.value.drawerModal) if (res && employeeFormState.value.drawerModal)
currentFromDataEmployee.value.status = res.status; currentFromDataEmployee.value.status = res.status;
@ -658,7 +671,7 @@ const emptyCreateDialog = ref(false);
<FloatingActionButton <FloatingActionButton
style="z-index: 999" style="z-index: 999"
:hide-icon="currentTab === 'employee'" :hide-icon="currentTab === 'employee'"
v-if="$route.name === 'CustomerManagement'" v-if="$route.name === 'CustomerManagement' && canAccess('customer', 'edit')"
@click=" @click="
() => { () => {
if (currentTab === 'employee') { if (currentTab === 'employee') {
@ -1391,6 +1404,7 @@ const emptyCreateDialog = ref(false);
/> />
<KebabAction <KebabAction
:hide-delete="!canAccess('customer', 'edit')"
:id-name=" :id-name="
props.row.branch[0].customerName || props.row.branch[0].customerName ||
props.row.branch[0].firstName props.row.branch[0].firstName
@ -1638,7 +1652,17 @@ const emptyCreateDialog = ref(false);
</div> </div>
</template> </template>
<template v-slot:action> <template v-slot:action>
<q-btn
icon="mdi-eye-outline"
:id="`btn-eye-${props.row.branch[0].customerName || props.row.branch[0].firstName}`"
size="sm"
dense
round
flat
@click.stop="editCustomerForm(props.row.id)"
/>
<KebabAction <KebabAction
:hide-delete="!canAccess('customer', 'edit')"
:status="props.row.status" :status="props.row.status"
:id-name="props.row.name" :id-name="props.row.name"
@view=" @view="
@ -1772,6 +1796,7 @@ const emptyCreateDialog = ref(false);
" "
> >
<TableEmpoloyee <TableEmpoloyee
:hide-delete="!canAccess('customer', 'edit')"
v-model:page-size="pageSize" v-model:page-size="pageSize"
v-model:current-page="currentPageEmployee" v-model:current-page="currentPageEmployee"
:grid-view="gridView" :grid-view="gridView"
@ -1819,7 +1844,11 @@ const emptyCreateDialog = ref(false);
" "
@toggle-status=" @toggle-status="
async (item: any) => { async (item: any) => {
triggerChangeStatus(item.id, item.status); triggerChangeStatus(
item.id,
item.status,
item.firstNameEN,
);
} }
" "
/> />
@ -1889,6 +1918,7 @@ const emptyCreateDialog = ref(false);
" "
> >
<TooltipComponent <TooltipComponent
v-if="canAccess('customer', 'edit')"
class="self-end q-ma-md" class="self-end q-ma-md"
:title="'general.noData'" :title="'general.noData'"
:caption="'general.clickToCreate'" :caption="'general.clickToCreate'"
@ -1900,6 +1930,7 @@ const emptyCreateDialog = ref(false);
style="flex-grow: 1" style="flex-grow: 1"
> >
<EmptyAddButton <EmptyAddButton
v-if="canAccess('customer', 'edit')"
:label="'general.add'" :label="'general.add'"
:i18n-args="{ :i18n-args="{
text: $t(`customer.${currentTab}`), text: $t(`customer.${currentTab}`),
@ -1914,6 +1945,7 @@ const emptyCreateDialog = ref(false);
} }
" "
/> />
<NoData v-else />
</div> </div>
</template> </template>
</div> </div>
@ -4423,6 +4455,7 @@ const emptyCreateDialog = ref(false);
(customerFormState.branchIndex !== -1 && (customerFormState.branchIndex !== -1 &&
customerFormState.branchIndex !== idx) customerFormState.branchIndex !== idx)
" "
:hide-delete="!canAccess('customer', 'edit')"
:readonly="customerFormState.branchIndex !== idx" :readonly="customerFormState.branchIndex !== idx"
@edit="() => (customerFormState.branchIndex = idx)" @edit="() => (customerFormState.branchIndex = idx)"
@cancel="() => customerFormUndo(false)" @cancel="() => customerFormUndo(false)"
@ -4463,11 +4496,18 @@ const emptyCreateDialog = ref(false);
} }
" "
:title=" :title="
employeeFormState.currentEmployee setPrefixName(
? $i18n.locale === 'eng' {
? `${employeeFormState.currentEmployee.firstNameEN} ${employeeFormState.currentEmployee.lastNameEN}` namePrefix: employeeFormState.currentEmployee.namePrefix,
: `${employeeFormState.currentEmployee.firstName} ${employeeFormState.currentEmployee.lastName}` firstName:
: '-' employeeFormState.currentEmployee.firstName ||
employeeFormState.currentEmployee.firstNameEN,
lastName: employeeFormState.currentEmployee.lastName,
firstNameEN: employeeFormState.currentEmployee.firstNameEN,
lastNameEN: employeeFormState.currentEmployee.lastNameEN,
},
{ locale },
)
" "
:badge-class=" :badge-class="
currentFromDataEmployee.gender === 'male' currentFromDataEmployee.gender === 'male'
@ -4562,7 +4602,11 @@ const emptyCreateDialog = ref(false);
@update:toggle-status=" @update:toggle-status="
(v) => { (v) => {
if (currentFromDataEmployee.id !== undefined) if (currentFromDataEmployee.id !== undefined)
triggerChangeStatus(currentFromDataEmployee.id, v); triggerChangeStatus(
currentFromDataEmployee.id,
v,
currentFromDataEmployee.firstNameEN,
);
} }
" "
:active="currentFromDataEmployee.status !== 'INACTIVE'" :active="currentFromDataEmployee.status !== 'INACTIVE'"
@ -4582,7 +4626,9 @@ const emptyCreateDialog = ref(false);
? setPrefixName( ? setPrefixName(
{ {
namePrefix: employeeFormState.currentEmployee.namePrefix, namePrefix: employeeFormState.currentEmployee.namePrefix,
firstName: employeeFormState.currentEmployee.firstName, firstName:
employeeFormState.currentEmployee.firstName ||
employeeFormState.currentEmployee.firstNameEN,
lastName: employeeFormState.currentEmployee.lastName, lastName: employeeFormState.currentEmployee.lastName,
firstNameEN: employeeFormState.currentEmployee.firstNameEN, firstNameEN: employeeFormState.currentEmployee.firstNameEN,
lastNameEN: employeeFormState.currentEmployee.lastNameEN, lastNameEN: employeeFormState.currentEmployee.lastNameEN,
@ -4878,7 +4924,10 @@ const emptyCreateDialog = ref(false);
type="button" type="button"
/> />
<DeleteButton <DeleteButton
v-if="!employeeFormState.isEmployeeEdit" v-if="
!employeeFormState.isEmployeeEdit &&
canAccess('customer', 'edit')
"
id="btn-info-basic-delete" id="btn-info-basic-delete"
icon-only icon-only
@click=" @click="

View file

@ -102,7 +102,13 @@ const telephoneNo = defineModel<string>('telephoneNo', { default: '' });
class="col-md-6" class="col-md-6"
:readonly :readonly
:disabled=" :disabled="
!isRoleInclude(['admin', 'system', 'head_of_admin']) && !readonly !isRoleInclude([
'admin',
'system',
'head_of_admin',
'executive',
'accountant',
]) && !readonly
" "
:label="$t('customer.form.registeredBranch')" :label="$t('customer.form.registeredBranch')"
select-first-value select-first-value

View file

@ -51,6 +51,7 @@ withDefaults(
actionDisabled?: boolean; actionDisabled?: boolean;
customerType?: 'CORP' | 'PERS'; customerType?: 'CORP' | 'PERS';
hideAction?: boolean; hideAction?: boolean;
hideDelete?: boolean;
}>(), }>(),
{ {
hideAction: false, hideAction: false,
@ -81,7 +82,7 @@ withDefaults(
/> />
<DeleteButton <DeleteButton
icon-only icon-only
v-if="readonly" v-if="readonly && !hideDelete"
@click="$emit('delete')" @click="$emit('delete')"
type="button" type="button"
:disabled="actionDisabled" :disabled="actionDisabled"

View file

@ -43,6 +43,7 @@ withDefaults(
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
isEdit?: boolean; isEdit?: boolean;
hideAction?: boolean;
}>(), }>(),
{ readonly: false, isEdit: false }, { readonly: false, isEdit: false },
); );
@ -207,7 +208,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
style="position: absolute; z-index: 999; top: 0; right: 0" style="position: absolute; z-index: 999; top: 0; right: 0"
> >
<div <div
v-if="flowData.status !== 'INACTIVE'" v-if="flowData.status !== 'INACTIVE' && !hideAction"
class="surface-1 row rounded" class="surface-1 row rounded"
> >
<UndoButton <UndoButton
@ -287,6 +288,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
> >
<template v-slot:btn-form-flow-step-drawer> <template v-slot:btn-form-flow-step-drawer>
<q-btn <q-btn
v-if="!hideAction"
dense dense
flat flat
icon="mdi-plus" icon="mdi-plus"
@ -315,6 +317,7 @@ function triggerPropertiesDialog(step: WorkFlowPayloadStep) {
<FormFlow <FormFlow
:readonly :readonly
onDrawer onDrawer
:hide-action="hideAction"
v-model:user-in-table="userInTable" v-model:user-in-table="userInTable"
v-model:flow-data="flowData" v-model:flow-data="flowData"
v-model:register-branch-id="registerBranchId" v-model:register-branch-id="registerBranchId"

View file

@ -11,7 +11,7 @@ import {
} from 'src/stores/workflow-template/types'; } from 'src/stores/workflow-template/types';
import { useWorkflowTemplate } from 'src/stores/workflow-template'; import { useWorkflowTemplate } from 'src/stores/workflow-template';
import { useNavigator } from 'src/stores/navigator'; import { useNavigator } from 'src/stores/navigator';
import { dialog } from 'src/stores/utils'; import { dialog, canAccess } from 'src/stores/utils';
import FloatingActionButton from 'components/FloatingActionButton.vue'; import FloatingActionButton from 'components/FloatingActionButton.vue';
import StatCardComponent from 'src/components/StatCardComponent.vue'; import StatCardComponent from 'src/components/StatCardComponent.vue';
@ -334,6 +334,7 @@ watch(
</script> </script>
<template> <template>
<FloatingActionButton <FloatingActionButton
v-if="canAccess('workflow', 'edit')"
style="z-index: 999" style="z-index: 999"
hide-icon hide-icon
@click="triggerDialog('add')" @click="triggerDialog('add')"
@ -682,6 +683,7 @@ watch(
" "
/> />
<KebabAction <KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.name" :id-name="props.row.name"
:status="props.row.status" :status="props.row.status"
@view=" @view="
@ -763,6 +765,7 @@ watch(
" "
/> />
<KebabAction <KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.name" :id-name="props.row.name"
:status="props.row.status" :status="props.row.status"
@view=" @view="
@ -846,6 +849,7 @@ watch(
@drawer-undo="undo" @drawer-undo="undo"
@close="resetForm" @close="resetForm"
@submit="submit" @submit="submit"
:hide-action="!canAccess('workflow', 'edit')"
:readonly="!pageState.isDrawerEdit" :readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit" :isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal" v-model="pageState.addModal"

View file

@ -41,7 +41,7 @@ import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import useFlowStore from 'stores/flow'; import useFlowStore from 'stores/flow';
import { dateFormat } from 'src/utils/datetime'; import { dateFormat } from 'src/utils/datetime';
import { formatNumberDecimal, isRoleInclude, notify } from 'stores/utils'; import { formatNumberDecimal, isRoleInclude, canAccess } from 'stores/utils';
const { getWorkflowTemplate } = useWorkflowTemplate(); const { getWorkflowTemplate } = useWorkflowTemplate();
import { Status } from 'stores/types'; import { Status } from 'stores/types';
@ -143,27 +143,25 @@ const { t } = useI18n();
const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL); const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL);
const priceDisplay = computed(() => ({ const priceDisplay = computed(() => ({
price: !isRoleInclude(['sale_agent']), // price: !isRoleInclude(['sale_agent']),
price: true,
agentPrice: isRoleInclude([ agentPrice: isRoleInclude([
'admin',
'head_of_admin',
'head_of_sale',
'system', 'system',
'owner', 'head_of_admin',
'admin',
'executive',
'accountant', 'accountant',
'sale_agent', 'head_of_sale',
]), ]),
serviceCharge: isRoleInclude([ serviceCharge: isRoleInclude([
'admin',
'head_of_admin',
'system', 'system',
'owner', 'head_of_admin',
'admin',
'executive',
'accountant', 'accountant',
]), ]),
})); }));
const actionDisplay = computed(() => const actionDisplay = computed(() => canAccess('product', 'edit'));
isRoleInclude(['admin', 'head_of_admin', 'system', 'owner', 'accountant']),
);
const splitterModel = computed(() => const splitterModel = computed(() =>
$q.screen.lt.md ? (productMode.value !== 'group' ? 0 : 100) : 25, $q.screen.lt.md ? (productMode.value !== 'group' ? 0 : 100) : 25,
); );

View file

@ -14,7 +14,7 @@ import { FloatingActionButton, PaginationComponent } from 'src/components';
import PaginationPageSize from 'src/components/PaginationPageSize.vue'; import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import PropertyDialog from './PropertyDialog.vue'; import PropertyDialog from './PropertyDialog.vue';
import { Property } from 'src/stores/property/types'; import { Property } from 'src/stores/property/types';
import { dialog, toCamelCase } from 'src/stores/utils'; import { dialog, toCamelCase, canAccess } from 'src/stores/utils';
import CreateButton from 'src/components/AddButton.vue'; import CreateButton from 'src/components/AddButton.vue';
import useOptionStore from 'stores/options'; import useOptionStore from 'stores/options';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue'; import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
@ -331,6 +331,7 @@ watch(
</script> </script>
<template> <template>
<FloatingActionButton <FloatingActionButton
v-if="canAccess('workflow', 'edit')"
style="z-index: 999" style="z-index: 999"
hide-icon hide-icon
@click="triggerDialog('add')" @click="triggerDialog('add')"
@ -536,11 +537,19 @@ watch(
class="col surface-2 flex items-center justify-center" class="col surface-2 flex items-center justify-center"
> >
<NoData <NoData
v-if="pageState.total !== 0 || pageState.searchDate.length > 0" v-if="
pageState.total !== 0 ||
pageState.searchDate.length > 0 ||
!canAccess('workflow', 'edit')
"
:not-found="!!pageState.inputSearch" :not-found="!!pageState.inputSearch"
/> />
<CreateButton <CreateButton
v-if="pageState.total === 0 && pageState.searchDate.length === 0" v-if="
pageState.total === 0 &&
pageState.searchDate.length === 0 &&
canAccess('workflow', 'edit')
"
@click="triggerDialog('add')" @click="triggerDialog('add')"
label="general.add" label="general.add"
:i18n-args="{ text: $t('flow.title') }" :i18n-args="{ text: $t('flow.title') }"
@ -698,6 +707,7 @@ watch(
" "
/> />
<KebabAction <KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.name" :id-name="props.row.name"
:status="props.row.status" :status="props.row.status"
@view=" @view="
@ -815,6 +825,7 @@ watch(
" "
/> />
<KebabAction <KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.id" :id-name="props.row.id"
:status="props.row.status" :status="props.row.status"
@view=" @view="
@ -906,6 +917,7 @@ watch(
@drawer-undo="() => undo()" @drawer-undo="() => undo()"
@close="() => resetForm()" @close="() => resetForm()"
@submit="() => submit()" @submit="() => submit()"
:hide-action="!canAccess('workflow', 'edit')"
:readonly="!pageState.isDrawerEdit" :readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit" :isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal" v-model="pageState.addModal"

View file

@ -30,6 +30,7 @@ withDefaults(
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
isEdit?: boolean; isEdit?: boolean;
hideAction?: boolean;
}>(), }>(),
{ readonly: false, isEdit: false }, { readonly: false, isEdit: false },
); );
@ -151,7 +152,7 @@ defineEmits<{
style="position: absolute; z-index: 999; top: 0; right: 0" style="position: absolute; z-index: 999; top: 0; right: 0"
> >
<div <div
v-if="propertyData.status !== 'INACTIVE'" v-if="propertyData.status !== 'INACTIVE' && !hideAction"
class="surface-1 row rounded" class="surface-1 row rounded"
> >
<UndoButton <UndoButton
@ -236,6 +237,7 @@ defineEmits<{
<FormProperty <FormProperty
onDrawer onDrawer
:readonly="!isEdit" :readonly="!isEdit"
:disable-toggle="hideAction"
v-model:name="formProperty.name" v-model:name="formProperty.name"
v-model:name-en="formProperty.nameEN" v-model:name-en="formProperty.nameEN"
v-model:type="formProperty.type" v-model:type="formProperty.type"

View file

@ -14,7 +14,7 @@ import useMyBranch from 'stores/my-branch';
import { useQuotationForm } from './form'; import { useQuotationForm } from './form';
import { hslaColors } from './constants'; import { hslaColors } from './constants';
import { pageTabs, columnQuotation } from './constants'; import { pageTabs, columnQuotation } from './constants';
import { toCamelCase } from 'stores/utils'; import { toCamelCase, canAccess } from 'stores/utils';
// NOTE Import Types // NOTE Import Types
import { CustomerBranchCreate, CustomerType } from 'stores/customer/types'; import { CustomerBranchCreate, CustomerType } from 'stores/customer/types';
@ -413,6 +413,7 @@ async function storeDataLocal(id: string) {
hide-icon hide-icon
style="z-index: 999" style="z-index: 999"
@click.stop="triggerAddQuotationDialog" @click.stop="triggerAddQuotationDialog"
v-if="canAccess('quotation', 'edit')"
/> />
<div class="column full-height no-wrap"> <div class="column full-height no-wrap">
@ -647,12 +648,20 @@ async function storeDataLocal(id: string) {
class="col surface-2 flex items-center justify-center" class="col surface-2 flex items-center justify-center"
> >
<NoData <NoData
v-if="pageState.inputSearch || pageState.currentTab !== 'Issued'" v-if="
pageState.inputSearch ||
!canAccess('quotation', 'edit') ||
pageState.currentTab !== 'Issued'
"
:not-found="!!pageState.inputSearch" :not-found="!!pageState.inputSearch"
/> />
<CreateButton <CreateButton
v-if="!pageState.inputSearch && pageState.currentTab === 'Issued'" v-if="
!pageState.inputSearch &&
pageState.currentTab === 'Issued' &&
canAccess('quotation', 'edit')
"
@click="triggerAddQuotationDialog" @click="triggerAddQuotationDialog"
label="general.add" label="general.add"
:i18n-args="{ text: $t('quotation.title') }" :i18n-args="{ text: $t('quotation.title') }"

View file

@ -29,6 +29,7 @@ const { data: config } = storeToRefs(configStore);
const prop = defineProps<{ const prop = defineProps<{
data?: Quotation | QuotationFull | DebitNote; data?: Quotation | QuotationFull | DebitNote;
readonly?: boolean;
isDebitNote?: boolean; isDebitNote?: boolean;
}>(); }>();
@ -479,7 +480,7 @@ onMounted(async () => {
</div> </div>
<!-- bill --> <!-- bill -->
<span class="app-text-muted-2 q-pt-md"> <span class="app-text-muted-2 q-pt-md" v-if="paymentData.length > 0">
{{ $t('quotation.receiptDialog.billOfPayment') }} {{ $t('quotation.receiptDialog.billOfPayment') }}
</span> </span>
@ -521,6 +522,7 @@ onMounted(async () => {
<div class="q-ml-auto row" style="gap: 10px"> <div class="q-ml-auto row" style="gap: 10px">
<q-btn <q-btn
:disable="readonly"
id="btn-payment" id="btn-payment"
@click.stop @click.stop
unelevated unelevated
@ -547,7 +549,11 @@ onMounted(async () => {
<span> <span>
{{ $t(`quotation.receiptDialog.${p.paymentStatus}`) }} {{ $t(`quotation.receiptDialog.${p.paymentStatus}`) }}
</span> </span>
<q-icon name="mdi-chevron-down" class="q-pl-xs" /> <q-icon
v-if="!readonly"
name="mdi-chevron-down"
class="q-pl-xs"
/>
<q-menu <q-menu
ref="refQMenu" ref="refQMenu"
fit fit
@ -634,6 +640,7 @@ onMounted(async () => {
}) })
}} }}
<q-btn <q-btn
v-if="!readonly"
unelevated unelevated
id="btn-upload-file" id="btn-upload-file"
:label="$t('general.upload')" :label="$t('general.upload')"
@ -762,10 +769,10 @@ onMounted(async () => {
} }
:deep( :deep(
.q-expansion-item .q-expansion-item
.q-item.q-item-type.row.no-wrap.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable .q-item.q-item-type.row.no-wrap.q-item--clickable.q-link.cursor-pointer.q-focusable.q-hoverable
.q-focus-helper .q-focus-helper
) { ) {
visibility: hidden; visibility: hidden;
} }
</style> </style>

View file

@ -8,6 +8,8 @@ import {
dialogCheckData, dialogCheckData,
dialogWarningClose, dialogWarningClose,
formatNumberDecimal, formatNumberDecimal,
canAccess,
isRoleInclude,
} from 'stores/utils'; } from 'stores/utils';
import { ProductTree, quotationProductTree } from './utils'; import { ProductTree, quotationProductTree } from './utils';
@ -213,17 +215,6 @@ const attachmentData = ref<
url?: string; url?: string;
}[] }[]
>([]); >([]);
const hideBtnApproveInvoice = computed(() => {
const role = getRole();
const allowedRoles = [
'system',
'head_of_admin',
'admin',
'head_of_accountant',
'accountant',
];
return !role || !role.some((r) => allowedRoles.includes(r));
});
const getToolbarConfig = computed(() => { const getToolbarConfig = computed(() => {
const toolbar = [['left', 'center', 'justify'], ['toggle'], ['clip']]; const toolbar = [['left', 'center', 'justify'], ['toggle'], ['clip']];
@ -1746,7 +1737,7 @@ function covertToNode() {
:readonly=" :readonly="
{ {
quotation: quotationFormState.mode !== 'edit', quotation: quotationFormState.mode !== 'edit',
invoice: false, invoice: isRoleInclude(['sale', 'head_of_sale']),
accepted: true, accepted: true,
}[view] }[view]
" "
@ -1950,6 +1941,7 @@ function covertToNode() {
view !== View.Receipt && view !== View.Receipt &&
view !== View.Complete view !== View.Complete
" "
:readonly="isRoleInclude(['sale', 'head_of_sale'])"
:data="quotationFormState.source" :data="quotationFormState.source"
v-model:first-code-payment="firstCodePayment" v-model:first-code-payment="firstCodePayment"
@fetch-status=" @fetch-status="
@ -2327,7 +2319,6 @@ function covertToNode() {
" "
> >
<MainButton <MainButton
v-if="!hideBtnApproveInvoice"
solid solid
icon="mdi-account-multiple-check-outline" icon="mdi-account-multiple-check-outline"
color="207 96% 32%" color="207 96% 32%"

View file

@ -66,21 +66,21 @@ const serviceList = defineModel<Partial<Record<ProductGroupId, Service[]>>>(
); );
const priceDisplay = computed(() => ({ const priceDisplay = computed(() => ({
price: !isRoleInclude(['sale_agent']), // price: !isRoleInclude(['sale_agent']),
price: true,
agentPrice: isRoleInclude([ agentPrice: isRoleInclude([
'admin',
'head_of_admin',
'head_of_sale',
'system', 'system',
'owner', 'head_of_admin',
'admin',
'executive',
'accountant', 'accountant',
'sale_agent', 'head_of_sale',
]), ]),
serviceCharge: isRoleInclude([ serviceCharge: isRoleInclude([
'admin',
'head_of_admin',
'system', 'system',
'owner', 'head_of_admin',
'admin',
'executive',
'accountant', 'accountant',
]), ]),
})); }));

View file

@ -62,6 +62,8 @@ const props = withDefaults(
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
isEdit?: boolean; isEdit?: boolean;
hideAction?: boolean;
hideDelete?: boolean;
dataId?: string; dataId?: string;
}>(), }>(),
@ -411,6 +413,7 @@ watch(
:prefix="data.name" :prefix="data.name"
hide-fade hide-fade
use-toggle use-toggle
:readonly="hideAction"
:active="data.status !== 'INACTIVE'" :active="data.status !== 'INACTIVE'"
:toggle-title="$t('status.title')" :toggle-title="$t('status.title')"
:icon="'ph-building-office'" :icon="'ph-building-office'"
@ -450,7 +453,7 @@ watch(
style="position: absolute; z-index: 999; top: 0; right: 0" style="position: absolute; z-index: 999; top: 0; right: 0"
> >
<div <div
v-if="data.status !== 'INACTIVE'" v-if="data.status !== 'INACTIVE' && !hideAction"
class="surface-1 row rounded" class="surface-1 row rounded"
> >
<UndoButton <UndoButton
@ -484,7 +487,7 @@ watch(
type="button" type="button"
/> />
<DeleteButton <DeleteButton
v-if="!isEdit" v-if="!isEdit && !hideDelete"
id="btn-info-basic-delete" id="btn-info-basic-delete"
icon-only icon-only
@click=" @click="
@ -597,6 +600,7 @@ watch(
v-model:on-create-data-list="imageListOnCreate" v-model:on-create-data-list="imageListOnCreate"
v-model:image-url="imageState.imageUrl" v-model:image-url="imageState.imageUrl"
v-model:data-list="imageList" v-model:data-list="imageList"
:changeDisabled="hideAction"
:on-create="model" :on-create="model"
:hiddenFooter="!imageState.isImageEdit" :hiddenFooter="!imageState.isImageEdit"
@add-image="addImage" @add-image="addImage"

View file

@ -7,7 +7,7 @@ import { Icon } from '@iconify/vue/dist/iconify.js';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { baseUrl } from 'src/stores/utils'; import { baseUrl, canAccess } from 'src/stores/utils';
import { useNavigator } from 'src/stores/navigator'; import { useNavigator } from 'src/stores/navigator';
import { useInstitution } from 'src/stores/institution'; import { useInstitution } from 'src/stores/institution';
import { Institution, InstitutionPayload } from 'src/stores/institution/types'; import { Institution, InstitutionPayload } from 'src/stores/institution/types';
@ -366,6 +366,7 @@ watch(
</script> </script>
<template> <template>
<FloatingActionButton <FloatingActionButton
v-if="canAccess('agencies', 'edit')"
style="z-index: 999" style="z-index: 999"
hide-icon hide-icon
@click="triggerDialog('add')" @click="triggerDialog('add')"
@ -750,6 +751,7 @@ watch(
" "
/> />
<KebabAction <KebabAction
:hide-delete="!canAccess('agencies', 'edit')"
:id-name="props.row.name" :id-name="props.row.name"
:status="props.row.status" :status="props.row.status"
@view=" @view="
@ -833,6 +835,7 @@ watch(
" "
/> />
<KebabAction <KebabAction
:hide-delete="!canAccess('agencies', 'edit')"
:id-name="props.row.id" :id-name="props.row.id"
:status="props.row.status" :status="props.row.status"
@view=" @view="
@ -973,6 +976,7 @@ watch(
@change-status="triggerChangeStatus" @change-status="triggerChangeStatus"
:readonly="!pageState.isDrawerEdit" :readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit" :isEdit="pageState.isDrawerEdit"
:hide-delete="!canAccess('agencies', 'edit')"
v-model="pageState.addModal" v-model="pageState.addModal"
v-model:drawer-model="pageState.viewDrawer" v-model:drawer-model="pageState.viewDrawer"
v-model:data="formData" v-model:data="formData"

View file

@ -21,7 +21,7 @@ import { column } from './constants';
import useFlowStore from 'src/stores/flow'; import useFlowStore from 'src/stores/flow';
import { useRequestList } from 'src/stores/request-list'; import { useRequestList } from 'src/stores/request-list';
import { RequestData, RequestDataStatus } from 'src/stores/request-list/types'; import { RequestData, RequestDataStatus } from 'src/stores/request-list/types';
import { dialogWarningClose } from 'src/stores/utils'; import { dialogWarningClose, canAccess } from 'src/stores/utils';
import { CancelButton, SaveButton } from 'src/components/button'; import { CancelButton, SaveButton } from 'src/components/button';
import { getRole } from 'src/services/keycloak'; import { getRole } from 'src/services/keycloak';
import FloatingActionButton from 'src/components/FloatingActionButton.vue'; import FloatingActionButton from 'src/components/FloatingActionButton.vue';
@ -473,6 +473,7 @@ watch(
" "
> >
<TableRequestList <TableRequestList
:no-link="!canAccess('customer', 'view')"
:columns="column" :columns="column"
:rows="data" :rows="data"
:grid="pageState.gridView" :grid="pageState.gridView"
@ -574,6 +575,7 @@ watch(
v-if="requestListActionData" v-if="requestListActionData"
v-model="pageState.requestListActionDialog" v-model="pageState.requestListActionDialog"
:request-list="requestListActionData" :request-list="requestListActionData"
:no-link="!canAccess('customer', 'view')"
@submit="submitRequestListAction" @submit="submitRequestListAction"
/> />
</div> </div>

View file

@ -86,7 +86,11 @@ function assignToForm() {
customerDutyCost: attributesForm.value.customerDutyCost ?? 30, customerDutyCost: attributesForm.value.customerDutyCost ?? 30,
companyDuty: attributesForm.value.companyDuty ?? false, companyDuty: attributesForm.value.companyDuty ?? false,
companyDutyCost: attributesForm.value.companyDutyCost ?? 30, companyDutyCost: attributesForm.value.companyDutyCost ?? 30,
responsibleUserLocal: attributesForm.value.responsibleUserLocal ?? true, responsibleUserLocal: attributesForm.value.responsibleUserLocal
? attributesForm.value.responsibleUserLocal
: props.responsibleAreaDistrictId
? false
: true,
responsibleUserId: responsibleUserId:
attributesForm.value.responsibleUserId || props.defaultMessenger, attributesForm.value.responsibleUserId || props.defaultMessenger,
individualDuty: attributesForm.value.individualDuty ?? false, individualDuty: attributesForm.value.individualDuty ?? false,

View file

@ -16,6 +16,7 @@ import useAddressStore from 'src/stores/address';
defineProps<{ defineProps<{
requestList: RequestData[]; requestList: RequestData[];
noLink?: boolean;
}>(); }>();
defineEmits<{ defineEmits<{
@ -99,6 +100,7 @@ watch(
hide-action hide-action
hide-view hide-view
checkable checkable
:no-link="noLink"
:list-same-area="listSameArea" :list-same-area="listSameArea"
:columns="column" :columns="column"
:rows="requestList" :rows="requestList"

View file

@ -26,6 +26,7 @@ import {
getEmployeeName, getEmployeeName,
getCustomerName, getCustomerName,
dialogWarningClose, dialogWarningClose,
canAccess,
} from 'src/stores/utils'; } from 'src/stores/utils';
import { dateFormatJS } from 'src/utils/datetime'; import { dateFormatJS } from 'src/utils/datetime';
import { useRequestList } from 'src/stores/request-list'; import { useRequestList } from 'src/stores/request-list';
@ -459,6 +460,7 @@ async function submitRejectCancel() {
} }
function toCustomer(customer: RequestData['quotation']['customerBranch']) { function toCustomer(customer: RequestData['quotation']['customerBranch']) {
if (!canAccess('customer', 'view')) return;
const url = new URL( const url = new URL(
`/customer-management?tab=customer&id=${customer.customerId}`, `/customer-management?tab=customer&id=${customer.customerId}`,
window.location.origin, window.location.origin,
@ -468,6 +470,7 @@ function toCustomer(customer: RequestData['quotation']['customerBranch']) {
} }
function toEmployee(employee: RequestData['employee']) { function toEmployee(employee: RequestData['employee']) {
if (!canAccess('customer', 'view')) return;
const url = new URL( const url = new URL(
`/customer-management?tab=employee&id=${employee.id}`, `/customer-management?tab=employee&id=${employee.id}`,
window.location.origin, window.location.origin,
@ -742,7 +745,7 @@ function toEmployee(employee: RequestData['employee']) {
}" }"
> >
<DataDisplay <DataDisplay
clickable :clickable="canAccess('customer', 'view')"
class="col" class="col"
icon="mdi-account-settings-outline" icon="mdi-account-settings-outline"
:label="$t('customer.employer')" :label="$t('customer.employer')"
@ -755,7 +758,7 @@ function toEmployee(employee: RequestData['employee']) {
@label-click="toCustomer(data.quotation.customerBranch)" @label-click="toCustomer(data.quotation.customerBranch)"
/> />
<DataDisplay <DataDisplay
clickable :clickable="canAccess('customer', 'view')"
class="col" class="col"
icon="mdi-account-settings-outline" icon="mdi-account-settings-outline"
:label="$t('customer.employee')" :label="$t('customer.employee')"
@ -864,7 +867,7 @@ function toEmployee(employee: RequestData['employee']) {
requestWorkId: value.id || '', requestWorkId: value.id || '',
}, },
value.stepStatus?.[pageState.currentStep - 1] value.stepStatus?.[pageState.currentStep - 1]
?.responsibleUserId, ?.responsibleUserId ?? data.defaultMessengerId,
); );
} }
" "

View file

@ -25,6 +25,7 @@ const props = withDefaults(
hideView?: boolean; hideView?: boolean;
checkable?: boolean; checkable?: boolean;
listSameArea?: string[]; listSameArea?: string[];
noLink?: boolean;
}>(), }>(),
{ {
row: () => [], row: () => [],
@ -119,6 +120,7 @@ function getEmployeeName(
} }
function toCustomer(customer: RequestData['quotation']['customerBranch']) { function toCustomer(customer: RequestData['quotation']['customerBranch']) {
if (props.noLink) return;
const url = new URL( const url = new URL(
`/customer-management?tab=customer&id=${customer.customerId}`, `/customer-management?tab=customer&id=${customer.customerId}`,
window.location.origin, window.location.origin,
@ -128,6 +130,7 @@ function toCustomer(customer: RequestData['quotation']['customerBranch']) {
} }
function toEmployee(employee: RequestData['employee']) { function toEmployee(employee: RequestData['employee']) {
if (props.noLink) return;
const url = new URL( const url = new URL(
`/customer-management?tab=employee&id=${employee.id}`, `/customer-management?tab=employee&id=${employee.id}`,
window.location.origin, window.location.origin,
@ -234,7 +237,7 @@ function handleCheckAll() {
</q-td> </q-td>
<q-td v-if="visibleColumns.includes('employer')" class="text-left"> <q-td v-if="visibleColumns.includes('employer')" class="text-left">
<span <span
class="link" :class="{ link: !noLink }"
@click="toCustomer(props.row.quotation.customerBranch)" @click="toCustomer(props.row.quotation.customerBranch)"
> >
{{ {{
@ -246,7 +249,10 @@ function handleCheckAll() {
</span> </span>
</q-td> </q-td>
<q-td v-if="visibleColumns.includes('employee')" class="text-left"> <q-td v-if="visibleColumns.includes('employee')" class="text-left">
<span class="link" @click="toEmployee(props.row.employee)"> <span
:class="{ link: !noLink }"
@click="toEmployee(props.row.employee)"
>
{{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }} {{ getEmployeeName(props.row, { locale: $i18n.locale }) || '-' }}
</span> </span>
</q-td> </q-td>
@ -403,6 +409,7 @@ function handleCheckAll() {
hide-kebab-delete hide-kebab-delete
:use-cancel="!hideAction" :use-cancel="!hideAction"
class="full-height" class="full-height"
:hide-action="hideAction"
:use-reject-cancel=" :use-reject-cancel="
props.row.customerRequestCancel && !props.row.rejectRequestCancel props.row.customerRequestCancel && !props.row.rejectRequestCancel
" "

View file

@ -5,6 +5,7 @@ import { storeToRefs } from 'pinia';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { canAccess } from 'src/stores/utils';
// NOTE: Components // NOTE: Components
import StatCardComponent from 'src/components/StatCardComponent.vue'; import StatCardComponent from 'src/components/StatCardComponent.vue';
@ -172,6 +173,7 @@ watch(
</script> </script>
<template> <template>
<FloatingActionButton <FloatingActionButton
v-if="canAccess('taskOrder', 'edit') || pageState.isMessenger"
style="z-index: 999" style="z-index: 999"
:hide-icon="!pageState.isMessenger" :hide-icon="!pageState.isMessenger"
@click.stop=" @click.stop="

View file

@ -4,6 +4,7 @@ import {
useRequestList, useRequestList,
RequestWork, RequestWork,
RequestWorkStatus, RequestWorkStatus,
RequestDataStatus,
} from 'src/stores/request-list'; } from 'src/stores/request-list';
import DialogHeader from 'src/components/dialog/DialogHeader.vue'; import DialogHeader from 'src/components/dialog/DialogHeader.vue';
import CancelButton from 'src/components/button/CancelButton.vue'; import CancelButton from 'src/components/button/CancelButton.vue';
@ -192,7 +193,8 @@ function submit() {
s.workStatus === s.workStatus ===
(props.creditNote (props.creditNote
? RequestWorkStatus.Canceled ? RequestWorkStatus.Canceled
: RequestWorkStatus.InProgress), : RequestWorkStatus.InProgress) ||
v.request.requestDataStatus === RequestDataStatus.Canceled,
); );
if (curr) { if (curr) {
const task: Task = { const task: Task = {
@ -387,8 +389,8 @@ function assignTempGroup() {
} }
:deep( :deep(
i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated i.q-icon.mdi.mdi-chevron-down-circle.q-expansion-item__toggle-icon.q-expansion-item__toggle-icon--rotated
) { ) {
color: var(--brand-1); color: var(--brand-1);
} }

View file

@ -79,7 +79,7 @@ function getEmployeeName(
return ( return (
{ {
['eng']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstNameEN} ${employee?.lastNameEN}`, ['eng']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstNameEN} ${employee?.lastNameEN}`,
['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName} ${employee?.lastName}`, ['tha']: `${useOptionStore().mapOption(employee?.namePrefix || '')} ${employee?.firstName || employee?.firstNameEN} ${employee?.lastName}`,
}[opts?.locale || 'eng'] || '-' }[opts?.locale || 'eng'] || '-'
); );
} }

View file

@ -2,6 +2,8 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { QTableProps, QTableSlots } from 'quasar'; import { QTableProps, QTableSlots } from 'quasar';
import { canAccess } from 'src/stores/utils';
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue'; import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
import BadgeComponent from 'src/components/BadgeComponent.vue'; import BadgeComponent from 'src/components/BadgeComponent.vue';
import KebabAction from 'src/components/shared/KebabAction.vue'; import KebabAction from 'src/components/shared/KebabAction.vue';
@ -216,6 +218,7 @@ const emit = defineEmits<{
v-if=" v-if="
!receive && props.row.taskOrderStatus === TaskOrderStatus.Pending !receive && props.row.taskOrderStatus === TaskOrderStatus.Pending
" "
:hide-delete="!canAccess('related', 'edit')"
:idName="`btn-kebab-${props.row.taskName}`" :idName="`btn-kebab-${props.row.taskName}`"
status="'ACTIVE'" status="'ACTIVE'"
hide-toggle hide-toggle
@ -264,6 +267,7 @@ const emit = defineEmits<{
:status="$t(taskOrderStatus(props.row.taskOrderStatus, 'status'))" :status="$t(taskOrderStatus(props.row.taskOrderStatus, 'status'))"
:badge-color="taskOrderStatus(props.row.taskOrderStatus, 'color')" :badge-color="taskOrderStatus(props.row.taskOrderStatus, 'color')"
hide-preview hide-preview
:hideKebabDelete="!canAccess('related', 'edit')"
:hide-action=" :hide-action="
receive || props.row.taskOrderStatus !== TaskOrderStatus.Pending receive || props.row.taskOrderStatus !== TaskOrderStatus.Pending
" "

View file

@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { api } from 'src/boot/axios'; import { api } from 'src/boot/axios';
import { Lang } from 'src/utils/ui'; import { Lang } from 'src/utils/ui';
import { baseUrl } from 'stores/utils'; import { baseUrl, canAccess } from 'stores/utils';
import TaskStatusComponent from '../TaskStatusComponent.vue'; import TaskStatusComponent from '../TaskStatusComponent.vue';
import StateButton from 'src/components/button/StateButton.vue'; import StateButton from 'src/components/button/StateButton.vue';
@ -1175,7 +1175,10 @@ watch(
<template #append="{ props: subProps }"> <template #append="{ props: subProps }">
<TaskStatusComponent <TaskStatusComponent
:key="subProps.row.id" :key="subProps.row.id"
:no-action="view !== TaskOrderStatus.Validate" :no-action="
view !== TaskOrderStatus.Validate &&
!canAccess('taskOrder', 'edit')
"
type="order" type="order"
:readonly=" :readonly="
(() => { (() => {

View file

@ -34,7 +34,7 @@ import {
import { RequestWork } from 'src/stores/request-list/types'; import { RequestWork } from 'src/stores/request-list/types';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import useOptionStore from 'src/stores/options'; import useOptionStore from 'src/stores/options';
import { dialogWarningClose } from 'src/stores/utils'; import { dialogWarningClose, canAccess, isRoleInclude } from 'src/stores/utils';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { QForm } from 'quasar'; import { QForm } from 'quasar';
import { getName } from 'src/services/keycloak'; import { getName } from 'src/services/keycloak';
@ -684,6 +684,7 @@ onMounted(async () => {
<RefundInformation <RefundInformation
v-if="view === CreditNoteStatus.Pending" v-if="view === CreditNoteStatus.Pending"
:readonly="!canAccess('related', 'edit')"
:total="creditNoteData?.value" :total="creditNoteData?.value"
:paid=" :paid="
creditNoteData?.paybackStatus === CreditNotePaybackStatus.Done creditNoteData?.paybackStatus === CreditNotePaybackStatus.Done
@ -727,7 +728,7 @@ onMounted(async () => {
<AdditionalFileExpansion <AdditionalFileExpansion
v-if="view !== CreditNoteStatus.Success" v-if="view !== CreditNoteStatus.Success"
:readonly="false" :readonly="isRoleInclude(['sale', 'head_of_sale'])"
v-model:file-data="attachmentData" v-model:file-data="attachmentData"
:transform-url=" :transform-url="
async (url: string) => { async (url: string) => {
@ -871,7 +872,8 @@ onMounted(async () => {
<SaveButton <SaveButton
v-if=" v-if="
!creditNoteData || !creditNoteData ||
creditNoteData?.creditNoteStatus === CreditNoteStatus.Waiting (creditNoteData?.creditNoteStatus === CreditNoteStatus.Waiting &&
canAccess('related', 'edit'))
" "
:disabled="taskListGroup.length === 0 || pageState.mode === 'edit'" :disabled="taskListGroup.length === 0 || pageState.mode === 'edit'"
type="submit" type="submit"

View file

@ -23,7 +23,7 @@ import useFlowStore from 'src/stores/flow';
import { pageTabs, columns, hslaColors } from './constants'; import { pageTabs, columns, hslaColors } from './constants';
import { CreditNoteStatus, useCreditNote } from 'src/stores/credit-note'; import { CreditNoteStatus, useCreditNote } from 'src/stores/credit-note';
import TableCreditNote from './TableCreditNote.vue'; import TableCreditNote from './TableCreditNote.vue';
import { dialogWarningClose } from 'src/stores/utils'; import { dialogWarningClose, canAccess } from 'src/stores/utils';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue'; import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
const $q = useQuasar(); const $q = useQuasar();
@ -146,6 +146,7 @@ watch(
</script> </script>
<template> <template>
<FloatingActionButton <FloatingActionButton
v-if="canAccess('related', 'edit')"
style="z-index: 999" style="z-index: 999"
hide-icon hide-icon
@click.stop="triggerCreateCreditNote()" @click.stop="triggerCreateCreditNote()"
@ -360,7 +361,10 @@ watch(
<TableCreditNote <TableCreditNote
:grid="pageState.gridView" :grid="pageState.gridView"
:visible-columns="pageState.fieldSelected" :visible-columns="pageState.fieldSelected"
:hide-delete="pageState.currentTab !== CreditNoteStatus.Waiting" :hide-delete="
pageState.currentTab !== CreditNoteStatus.Waiting ||
!canAccess('related', 'edit')
"
@view="(v) => navigateTo({ statusDialog: 'info', creditId: v.id })" @view="(v) => navigateTo({ statusDialog: 'info', creditId: v.id })"
@delete="(v) => triggerDelete(v.id)" @delete="(v) => triggerDelete(v.id)"
> >
@ -376,6 +380,10 @@ watch(
}) })
" "
@delete="() => triggerDelete(item.row.id)" @delete="() => triggerDelete(item.row.id)"
:hide-kebab-delete="
pageState.currentTab !== CreditNoteStatus.Waiting ||
!canAccess('related', 'edit')
"
:title="item.row.quotation.workName" :title="item.row.quotation.workName"
:code="item.row.code" :code="item.row.code"
:status="$t(`creditNote.status.${item.row.creditNoteStatus}`)" :status="$t(`creditNote.status.${item.row.creditNoteStatus}`)"

View file

@ -172,11 +172,13 @@ const refundOpts = ref<
> >
{{ $t('creditNote.label.refund') }} {{ $t('creditNote.label.refund') }}
<q-btn-dropdown <q-btn-dropdown
:disable="readonly"
dense dense
unelevated unelevated
:label="$t(`creditNote.status.payback.${paybackStatus}`)" :label="$t(`creditNote.status.payback.${paybackStatus}`)"
class="text-capitalize text-weight-regular product-status rounded" class="text-capitalize text-weight-regular product-status rounded"
:class="{ :class="{
'hide-dropdown q-pr-md': readonly,
warning: paybackStatus === CreditNotePaybackStatus.Pending, warning: paybackStatus === CreditNotePaybackStatus.Pending,
danger: paybackStatus === CreditNotePaybackStatus.Verify, danger: paybackStatus === CreditNotePaybackStatus.Verify,
'positive hide-dropdown q-pr-md': 'positive hide-dropdown q-pr-md':
@ -219,7 +221,6 @@ const refundOpts = ref<
<UploadFileSection <UploadFileSection
multiple multiple
:layout="$q.screen.gt.sm ? 'column' : 'row'" :layout="$q.screen.gt.sm ? 'column' : 'row'"
:readonly
:label="`${$t('general.upload', { msg: ' E-slip' })} ${$t( :label="`${$t('general.upload', { msg: ' E-slip' })} ${$t(
'general.or', 'general.or',
{ {
@ -281,9 +282,9 @@ const refundOpts = ref<
} }
:deep( :deep(
.hide-dropdown .hide-dropdown
i.q-icon.mdi.mdi-chevron-down.q-btn-dropdown__arrow.q-btn-dropdown__arrow-container i.q-icon.mdi.mdi-chevron-down.q-btn-dropdown__arrow.q-btn-dropdown__arrow-container
) { ) {
display: none; display: none;
} }
</style> </style>

View file

@ -46,7 +46,13 @@ import {
import { RequestWork } from 'src/stores/request-list/types'; import { RequestWork } from 'src/stores/request-list/types';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import useOptionStore from 'src/stores/options'; import useOptionStore from 'src/stores/options';
import { deleteItem, dialog, dialogWarningClose } from 'src/stores/utils'; import {
canAccess,
deleteItem,
dialog,
dialogWarningClose,
isRoleInclude,
} from 'src/stores/utils';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Employee } from 'src/stores/employee/types'; import { Employee } from 'src/stores/employee/types';
import QuotationFormWorkerSelect from '../05_quotation/QuotationFormWorkerSelect.vue'; import QuotationFormWorkerSelect from '../05_quotation/QuotationFormWorkerSelect.vue';
@ -1071,6 +1077,7 @@ async function submitAccepted() {
<PaymentForm <PaymentForm
v-if="view === QuotationStatus.PaymentPending" v-if="view === QuotationStatus.PaymentPending"
is-debit-note is-debit-note
:readonly="isRoleInclude(['sale', 'head_of_sale'])"
:data="debitNoteData" :data="debitNoteData"
@fetch-status=" @fetch-status="
() => { () => {
@ -1126,7 +1133,6 @@ async function submitAccepted() {
view === QuotationStatus.Issued || view === QuotationStatus.Issued ||
view === QuotationStatus.Accepted view === QuotationStatus.Accepted
" "
readonly
:total-price="summaryPrice.finalPrice" :total-price="summaryPrice.finalPrice"
class="q-mb-md" class="q-mb-md"
v-model:pay-type="currentFormData.payCondition" v-model:pay-type="currentFormData.payCondition"
@ -1144,7 +1150,7 @@ async function submitAccepted() {
view === QuotationStatus.Accepted || view === QuotationStatus.Accepted ||
view === QuotationStatus.PaymentPending view === QuotationStatus.PaymentPending
" "
:readonly :readonly="isRoleInclude(['sale', 'head_of_sale'])"
v-model:file-data="attachmentData" v-model:file-data="attachmentData"
:transform-url=" :transform-url="
async (url: string) => { async (url: string) => {

View file

@ -22,7 +22,7 @@ import { useNavigator } from 'src/stores/navigator';
import useFlowStore from 'src/stores/flow'; import useFlowStore from 'src/stores/flow';
import { pageTabs, columns, hslaColors } from './constants'; import { pageTabs, columns, hslaColors } from './constants';
import { DebitNoteStatus, useDebitNote } from 'src/stores/debit-note'; import { DebitNoteStatus, useDebitNote } from 'src/stores/debit-note';
import { dialogWarningClose } from 'src/stores/utils'; import { canAccess, dialogWarningClose } from 'src/stores/utils';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue'; import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
@ -163,6 +163,7 @@ watch(
<FloatingActionButton <FloatingActionButton
style="z-index: 999" style="z-index: 999"
hide-icon hide-icon
v-if="canAccess('related', 'edit')"
@click.stop="() => triggerCreateDebitNote()" @click.stop="() => triggerCreateDebitNote()"
></FloatingActionButton> ></FloatingActionButton>

View file

@ -6,6 +6,7 @@ import { DebitNote, useDebitNote } from 'src/stores/debit-note';
import { columns } from './constants'; import { columns } from './constants';
import KebabAction from 'src/components/shared/KebabAction.vue'; import KebabAction from 'src/components/shared/KebabAction.vue';
import { canAccess } from 'src/stores/utils';
const debitNote = useDebitNote(); const debitNote = useDebitNote();
const { data, page, pageSize } = storeToRefs(debitNote); const { data, page, pageSize } = storeToRefs(debitNote);
@ -80,6 +81,7 @@ const visible = computed(() =>
:id-name="`btn-kebab-${props.row.workName}`" :id-name="`btn-kebab-${props.row.workName}`"
hide-toggle hide-toggle
hide-edit hide-edit
:hide-delete="!canAccess('related', 'edit')"
@edit="$emit('edit', props.row)" @edit="$emit('edit', props.row)"
@delete="$emit('delete', props.row)" @delete="$emit('delete', props.row)"
@view="$emit('view', props.row)" @view="$emit('view', props.row)"

View file

@ -2,17 +2,15 @@
import MenuItem from 'components/00_home/MenuItem.vue'; import MenuItem from 'components/00_home/MenuItem.vue';
import { useNavigator } from 'src/stores/navigator'; import { useNavigator } from 'src/stores/navigator';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { getRole } from 'src/services/keycloak'; import { canAccess } from 'src/stores/utils';
const navigatorStore = useNavigator(); const navigatorStore = useNavigator();
const menu = ref<InstanceType<typeof MenuItem>['$props']['list']>([]); const menu = ref<InstanceType<typeof MenuItem>['$props']['list']>([]);
const role = ref();
onMounted(() => { onMounted(() => {
navigatorStore.current.title = ''; navigatorStore.current.title = '';
navigatorStore.current.path = [{ text: '' }]; navigatorStore.current.path = [{ text: '' }];
role.value = getRole();
menu.value = [ menu.value = [
{ {
value: 'branch-management', value: 'branch-management',
@ -20,14 +18,7 @@ onMounted(() => {
color: 'green', color: 'green',
title: 'menu.branch', title: 'menu.branch',
caption: 'menu.branchCaption', caption: 'menu.branchCaption',
hidden: hidden: !canAccess('branch'),
role.value.includes('admin') ||
role.value.includes('branch_admin') ||
role.value.includes('head_of_admin') ||
role.value.includes('system') ||
role.value.includes('owner')
? false
: true,
}, },
{ {
value: 'personnel-management', value: 'personnel-management',
@ -35,15 +26,7 @@ onMounted(() => {
color: 'cyan', color: 'cyan',
title: 'menu.user', title: 'menu.user',
caption: 'menu.userCaption', caption: 'menu.userCaption',
hidden: hidden: !canAccess('personnel'),
role.value.includes('admin') ||
role.value.includes('branch_admin') ||
role.value.includes('head_of_admin') ||
role.value.includes('system') ||
role.value.includes('owner') ||
role.value.includes('branch_manager')
? false
: true,
}, },
{ {
value: 'customer-management', value: 'customer-management',
@ -52,6 +35,7 @@ onMounted(() => {
title: 'menu.customer', title: 'menu.customer',
caption: 'menu.customerCaption', caption: 'menu.customerCaption',
isax: true, isax: true,
hidden: !canAccess('customer'),
}, },
{ {
value: 'product-service', value: 'product-service',
@ -115,6 +99,7 @@ onMounted(() => {
title: 'menu.dashboard', title: 'menu.dashboard',
caption: 'menu.dashboardCaption', caption: 'menu.dashboardCaption',
isax: true, isax: true,
hidden: !canAccess('dashBoard'),
}, },
]; ];
}); });

View file

@ -1,5 +1,5 @@
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { isRoleInclude } from 'stores/utils'; import { isRoleInclude, canAccess } from 'stores/utils';
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
@ -16,21 +16,8 @@ const routes: RouteRecordRaw[] = [
path: '/branch-management', path: '/branch-management',
name: 'BranchManagement', name: 'BranchManagement',
beforeEnter: (to, from, next) => { beforeEnter: (to, from, next) => {
if ( if (canAccess('branch')) next();
isRoleInclude([ else next('/');
'admin',
'branch_admin',
'head_of_admin',
'head_of_account',
'system',
'owner',
'branch_manager',
])
) {
next();
} else {
next('/');
}
}, },
component: () => import('pages/01_branch-management/MainPage.vue'), component: () => import('pages/01_branch-management/MainPage.vue'),
}, },
@ -38,36 +25,36 @@ const routes: RouteRecordRaw[] = [
path: '/personnel-management', path: '/personnel-management',
name: 'PersonnelManagement', name: 'PersonnelManagement',
beforeEnter: (to, from, next) => { beforeEnter: (to, from, next) => {
if ( if (canAccess('personnel')) next();
isRoleInclude([ else next('/');
'admin',
'branch_admin',
'head_of_admin',
'system',
'owner',
'branch_manager',
])
) {
next();
} else {
next('/');
}
}, },
component: () => import('pages/02_personnel-management/MainPage.vue'), component: () => import('pages/02_personnel-management/MainPage.vue'),
}, },
{ {
path: '/customer-management', path: '/customer-management',
name: 'CustomerManagement', name: 'CustomerManagement',
beforeEnter: (to, from, next) => {
if (canAccess('customer')) next();
else next('/');
},
component: () => import('pages/03_customer-management/MainPage.vue'), component: () => import('pages/03_customer-management/MainPage.vue'),
}, },
{ {
path: '/customer-management/:customerId', path: '/customer-management/:customerId',
name: 'CustomerSpecificManagement', name: 'CustomerSpecificManagement',
beforeEnter: (to, from, next) => {
if (canAccess('customer')) next();
else next('/');
},
component: () => import('pages/03_customer-management/MainPage.vue'), component: () => import('pages/03_customer-management/MainPage.vue'),
}, },
{ {
path: '/customer-management/:customerId/branch', path: '/customer-management/:customerId/branch',
name: 'CustomerBranchManagement', name: 'CustomerBranchManagement',
beforeEnter: (to, from, next) => {
if (canAccess('customer')) next();
else next('/');
},
component: () => import('pages/03_customer-management/MainPage.vue'), component: () => import('pages/03_customer-management/MainPage.vue'),
}, },
{ {
@ -78,11 +65,19 @@ const routes: RouteRecordRaw[] = [
{ {
path: '/workflow', path: '/workflow',
name: 'Workflow', name: 'Workflow',
beforeEnter: (to, from, next) => {
if (canAccess('workflow')) next();
else next('/');
},
component: () => import('pages/04_flow-managment/MainPage.vue'), component: () => import('pages/04_flow-managment/MainPage.vue'),
}, },
{ {
path: '/property', path: '/property',
name: 'Property', name: 'Property',
beforeEnter: (to, from, next) => {
if (canAccess('workflow')) next();
else next('/');
},
component: () => import('pages/04_property-managment/MainPage.vue'), component: () => import('pages/04_property-managment/MainPage.vue'),
}, },
{ {
@ -98,6 +93,10 @@ const routes: RouteRecordRaw[] = [
{ {
path: '/agencies-management', path: '/agencies-management',
name: 'agencies-management', name: 'agencies-management',
beforeEnter: (to, from, next) => {
if (canAccess('agencies')) next();
else next('/');
},
component: () => import('pages/07_agencies-management/MainPage.vue'), component: () => import('pages/07_agencies-management/MainPage.vue'),
}, },
{ {
@ -138,6 +137,10 @@ const routes: RouteRecordRaw[] = [
{ {
path: '/dash-board', path: '/dash-board',
name: 'dashBoard', name: 'dashBoard',
beforeEnter: (to, from, next) => {
if (canAccess('dashBoard')) next();
else next('/');
},
component: () => import('pages/15_dash-board/MainPage.vue'), component: () => import('pages/15_dash-board/MainPage.vue'),
}, },
{ {
@ -225,7 +228,7 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
path: '/receipt/:id', path: '/receipt/:id',
name: 'receiptform', name: 'receiptForm',
component: () => import('pages/13_receipt/MainPage.vue'), component: () => import('pages/13_receipt/MainPage.vue'),
}, },
{ {

View file

@ -221,11 +221,99 @@ export function checkTabBeforeAdd(data: unknown[], except?: string[]) {
export function isRoleInclude(role2check: string[]): boolean { export function isRoleInclude(role2check: string[]): boolean {
const roles = getRole() ?? []; const roles = getRole() ?? [];
const isIncluded = role2check.some((r) => roles.includes(r)); const filterRole = roles.filter(
(role: string) =>
role !== 'offline_access' &&
role !== 'uma_authorization' &&
!role.startsWith('default-roles'),
);
const isIncluded = role2check.some((r) => filterRole.includes(r));
return isIncluded; return isIncluded;
} }
const allRoles = [
'head_of_admin',
'admin',
'executive',
'accountant',
'branch_admin',
'branch_manager',
'branch_accountant',
'head_of_sale',
'sale',
'data_entry',
'document_checker',
'messenger',
'corporate_customer',
'agency',
];
const permissions = {
branch: {
edit: allRoles.slice(0, 7),
view: allRoles.slice(0, 7),
},
personnel: {
edit: allRoles.slice(0, 6).filter((r) => r !== 'accountant'),
view: allRoles.slice(0, 6).filter((r) => r !== 'accountant'),
},
product: {
edit: allRoles.slice(0, 7),
view: allRoles,
},
workflow: {
edit: allRoles.slice(0, 6),
view: allRoles.filter((r) => r !== 'branch_accountant'),
},
customer: {
edit: allRoles.slice(0, 9).filter((r) => r !== 'branch_accountant'),
view: allRoles.slice(0, 9),
},
agencies: {
edit: allRoles.slice(0, 7),
view: allRoles,
},
quotation: {
edit: allRoles.slice(0, 9).filter((r) => r !== 'branch_accountant'),
view: allRoles.slice(0, 9),
},
taskOrder: {
edit: [...allRoles.slice(0, 6), 'data_entry'],
view: allRoles,
},
related: {
// ใช้กับหลายเมนู
edit: allRoles.slice(0, 6),
view: allRoles,
},
// account: {
// edit: allRoles.slice(0, 6),
// view: allRoles.slice(0, 7),
// },
uploadSlip: {
edit: allRoles.slice(0, 6),
view: allRoles.filter((r) => r !== 'head_of_sale' && r !== 'sale'),
},
dashBoard: {
edit: allRoles.slice(0, 6).filter((r) => r !== 'admin'),
view: allRoles.filter((r) => r !== 'admin'),
},
};
export function canAccess(
menu: keyof typeof permissions,
action: 'edit' | 'view' = 'view',
): boolean {
// uma_authorization = all roles
const roles = getRole() ?? [];
if (roles.includes('system')) return true;
const allowedRoles = permissions[menu]?.[action] || [];
return allowedRoles.some((role: string) => roles.includes(role));
}
export function resetScrollBar(elementId: string) { export function resetScrollBar(elementId: string) {
const element = document.getElementById(elementId); const element = document.getElementById(elementId);
@ -606,7 +694,7 @@ export function getEmployeeName(
return { return {
['eng']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstNameEN} ${employee.lastNameEN}`, ['eng']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstNameEN} ${employee.lastNameEN}`,
['tha']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstName} ${employee.lastName}`, ['tha']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstName || employee.firstNameEN} ${employee.lastName}`,
}[opts?.locale || 'eng']; }[opts?.locale || 'eng'];
} }