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
style="
display: block;
width: 100%;
height: 1px;
background: hsla(0 0% 0% / 0.1);
margin-bottom: var(--size-2);
"
/>
<q-separator />
<slot name="data"></slot>
<template v-if="!$slots.data">
<div

View file

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

View file

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

View file

@ -48,6 +48,7 @@ defineProps<{
readonly?: boolean;
onDrawer?: boolean;
inputOnly?: boolean;
disableToggle?: boolean;
}>();
defineEmits<{
@ -76,6 +77,7 @@ defineEmits<{
<ToggleButton
class="q-mr-sm"
two-way
:disable="disableToggle"
:model-value="status !== 'INACTIVE'"
@click="
() => {
@ -195,8 +197,8 @@ defineEmits<{
}
: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;
padding-right: 8px !important;
padding-top: 16px;
@ -208,15 +210,15 @@ defineEmits<{
}
: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);
}
: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-focus-helper
) {
.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
) {
visibility: hidden;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import useMyBranch from 'stores/my-branch';
import { getUserId, getRole } from 'src/services/keycloak';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { isRoleInclude } from 'src/stores/utils';
import { canAccess } from 'src/stores/utils';
type Menu = {
label: string;
@ -71,82 +71,41 @@ function initMenu() {
{
label: 'branch',
route: '/branch-management',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
]),
hidden: !canAccess('branch'),
},
{
label: 'personnel',
route: '/personnel-management',
hidden: !isRoleInclude([
'owner',
'system',
'head_of_admin',
'admin',
'branch_manager',
]),
hidden: !canAccess('personnel'),
},
{
label: 'workflow',
route: '/workflow',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
hidden: !canAccess('workflow'),
},
{
label: 'property',
route: '/property',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
hidden: !canAccess('workflow'),
},
{
label: 'productService',
route: '/product-service',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'head_of_sale',
'sale',
]),
},
{
label: 'customer',
route: '/customer-management',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
hidden: !canAccess('customer'),
},
{
label: 'agencies',
route: '/agencies-management',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin']),
},
],
},
{
label: 'menu.sales',
icon: 'mdi-store-settings-outline',
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
children: [
{ label: 'quotation', route: '/quotation' },
{ label: 'invoice', route: '/invoice' },
@ -169,16 +128,6 @@ function initMenu() {
label: 'menu.account',
icon: 'mdi-bank-outline',
disabled: false,
hidden: !isRoleInclude([
'system',
'head_of_admin',
'admin',
'branch_manager',
'head_of_accountant',
'accountant',
'head_of_sale',
'sale',
]),
children: [
{ label: 'receipt', route: '/receipt' },
{ label: 'creditNote', route: '/credit-note' },
@ -200,10 +149,13 @@ function initMenu() {
{
label: 'menu.overall',
icon: 'mdi-monitor-dashboard',
hidden: !isRoleInclude(['system', 'head_of_admin', 'admin', 'executive']),
children: [
{ 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 useBranchStore from 'stores/branch';
import useFlowStore from 'stores/flow';
import { isRoleInclude } from 'stores/utils';
import { isRoleInclude, canAccess } from 'stores/utils';
import {
BranchWithChildren,
BranchCreate,
@ -1050,7 +1050,7 @@ watch(currentHq, () => {
{{ $t('branch.allBranch') }}
</div>
<q-btn
v-if="isRoleInclude(['head_of_admin', 'admin', 'system'])"
v-if="isRoleInclude(['system'])"
round
flat
size="md"
@ -1561,6 +1561,16 @@ watch(currentHq, () => {
</q-td>
<q-td>
<KebabAction
v-if="
isRoleInclude([
'system',
'head_of_admin',
'admin',
'executive',
'accountant',
]) ||
(canAccess('branch') && currentHq.id)
"
:status="props.row.status"
:idName="props.row.name"
@view="
@ -1702,6 +1712,16 @@ watch(currentHq, () => {
>
<template v-slot:action>
<KebabAction
v-if="
isRoleInclude([
'system',
'head_of_admin',
'admin',
'executive',
'accountant',
]) ||
(canAccess('branch') && currentHq.id)
"
:status="props.row.status"
:idName="props.row.name"
@view="

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,7 +41,7 @@ import PaginationPageSize from 'src/components/PaginationPageSize.vue';
import useFlowStore from 'stores/flow';
import { dateFormat } from 'src/utils/datetime';
import { formatNumberDecimal, isRoleInclude, notify } from 'stores/utils';
import { formatNumberDecimal, isRoleInclude, canAccess } from 'stores/utils';
const { getWorkflowTemplate } = useWorkflowTemplate();
import { Status } from 'stores/types';
@ -143,27 +143,25 @@ const { t } = useI18n();
const baseUrl = ref<string>(import.meta.env.VITE_API_BASE_URL);
const priceDisplay = computed(() => ({
price: !isRoleInclude(['sale_agent']),
// price: !isRoleInclude(['sale_agent']),
price: true,
agentPrice: isRoleInclude([
'admin',
'head_of_admin',
'head_of_sale',
'system',
'owner',
'head_of_admin',
'admin',
'executive',
'accountant',
'sale_agent',
'head_of_sale',
]),
serviceCharge: isRoleInclude([
'admin',
'head_of_admin',
'system',
'owner',
'head_of_admin',
'admin',
'executive',
'accountant',
]),
}));
const actionDisplay = computed(() =>
isRoleInclude(['admin', 'head_of_admin', 'system', 'owner', 'accountant']),
);
const actionDisplay = computed(() => canAccess('product', 'edit'));
const splitterModel = computed(() =>
$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 PropertyDialog from './PropertyDialog.vue';
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 useOptionStore from 'stores/options';
import AdvanceSearch from 'src/components/shared/AdvanceSearch.vue';
@ -331,6 +331,7 @@ watch(
</script>
<template>
<FloatingActionButton
v-if="canAccess('workflow', 'edit')"
style="z-index: 999"
hide-icon
@click="triggerDialog('add')"
@ -536,11 +537,19 @@ watch(
class="col surface-2 flex items-center justify-center"
>
<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"
/>
<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')"
label="general.add"
:i18n-args="{ text: $t('flow.title') }"
@ -698,6 +707,7 @@ watch(
"
/>
<KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.name"
:status="props.row.status"
@view="
@ -815,6 +825,7 @@ watch(
"
/>
<KebabAction
v-if="canAccess('workflow', 'edit')"
:id-name="props.row.id"
:status="props.row.status"
@view="
@ -906,6 +917,7 @@ watch(
@drawer-undo="() => undo()"
@close="() => resetForm()"
@submit="() => submit()"
:hide-action="!canAccess('workflow', 'edit')"
:readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal"

View file

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

View file

@ -14,7 +14,7 @@ import useMyBranch from 'stores/my-branch';
import { useQuotationForm } from './form';
import { hslaColors } from './constants';
import { pageTabs, columnQuotation } from './constants';
import { toCamelCase } from 'stores/utils';
import { toCamelCase, canAccess } from 'stores/utils';
// NOTE Import Types
import { CustomerBranchCreate, CustomerType } from 'stores/customer/types';
@ -413,6 +413,7 @@ async function storeDataLocal(id: string) {
hide-icon
style="z-index: 999"
@click.stop="triggerAddQuotationDialog"
v-if="canAccess('quotation', 'edit')"
/>
<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"
>
<NoData
v-if="pageState.inputSearch || pageState.currentTab !== 'Issued'"
v-if="
pageState.inputSearch ||
!canAccess('quotation', 'edit') ||
pageState.currentTab !== 'Issued'
"
:not-found="!!pageState.inputSearch"
/>
<CreateButton
v-if="!pageState.inputSearch && pageState.currentTab === 'Issued'"
v-if="
!pageState.inputSearch &&
pageState.currentTab === 'Issued' &&
canAccess('quotation', 'edit')
"
@click="triggerAddQuotationDialog"
label="general.add"
:i18n-args="{ text: $t('quotation.title') }"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import {
useRequestList,
RequestWork,
RequestWorkStatus,
RequestDataStatus,
} from 'src/stores/request-list';
import DialogHeader from 'src/components/dialog/DialogHeader.vue';
import CancelButton from 'src/components/button/CancelButton.vue';
@ -192,7 +193,8 @@ function submit() {
s.workStatus ===
(props.creditNote
? RequestWorkStatus.Canceled
: RequestWorkStatus.InProgress),
: RequestWorkStatus.InProgress) ||
v.request.requestDataStatus === RequestDataStatus.Canceled,
);
if (curr) {
const task: Task = {
@ -387,8 +389,8 @@ function assignTempGroup() {
}
: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);
}

View file

@ -79,7 +79,7 @@ function getEmployeeName(
return (
{
['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'] || '-'
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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