Merge branch 'develop'

This commit is contained in:
Methapon2001 2025-01-10 16:49:09 +07:00
commit 55c15585f8
22 changed files with 403 additions and 115 deletions

View file

@ -57,7 +57,9 @@ api.interceptors.response.use(
persistent: true, persistent: true,
enablei18n: true, enablei18n: true,
message: `dialog.backend.${parseError(err.response.status, err.response.data)}`, message: `dialog.backend.${parseError(err.response.status, err.response.data)}`,
action: () => {}, action: () => {
if (err.response.status === 401) window.location.reload();
},
}); });
}, },
); );

View file

@ -32,6 +32,8 @@ const firstName = defineModel<string>('firstName');
const namePrefix = defineModel<string>('namePrefix'); const namePrefix = defineModel<string>('namePrefix');
const passportNumber = defineModel<string>('passportNumber'); const passportNumber = defineModel<string>('passportNumber');
const passportValidator = /[a-zA-Z]{1}[a-zA-Z0-9]{1}[0-9]{5,7}$/;
const genderOptions = ref<Record<string, unknown>[]>([]); const genderOptions = ref<Record<string, unknown>[]>([]);
let genderFilter: ( let genderFilter: (
value: string, value: string,
@ -275,7 +277,6 @@ watch(
</q-item> </q-item>
</template> </template>
</q-select> </q-select>
<q-input <q-input
:for="`${prefixId}-input-previous-passport-Number`" :for="`${prefixId}-input-previous-passport-Number`"
:dense="dense" :dense="dense"
@ -287,8 +288,9 @@ watch(
v-model="previousPassportRef" v-model="previousPassportRef"
:rules="[ :rules="[
(v) => (v) =>
(!!v && v.length === 6) || !v ||
$t('form.error.requireLength', { msg: 6 }), passportValidator.test(v) ||
$t('form.error.passportFormat'),
]" ]"
/> />
@ -484,7 +486,9 @@ watch(
:label="$t('customerEmployee.form.passportNo')" :label="$t('customerEmployee.form.passportNo')"
v-model="passportNumber" v-model="passportNumber"
:rules="[ :rules="[
(val) => (val && val.length > 0) || $t('form.error.required'), (val) => !!val || $t('form.error.required'),
(val) =>
passportValidator.test(val) || $t('form.error.passportFormat'),
]" ]"
/> />

View file

@ -0,0 +1,127 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getRole } from 'src/services/keycloak';
import { createSelect, SelectProps } from './select';
import SelectInput from '../SelectInput.vue';
import { User } from 'src/stores/user/types';
import useStore from 'src/stores/user';
type SelectOption = User;
const value = defineModel<string | null | undefined>('value', {
required: true,
});
const valueOption = defineModel<SelectOption>('valueOption', {
required: false,
});
const selectOptions = ref<SelectOption[]>([]);
const { fetchList: getList, fetchById: getById } = useStore();
defineEmits<{
(e: 'create'): void;
}>();
type ExclusiveProps = {
codeOnly?: boolean;
selectFirstValue?: boolean;
branchVirtual?: boolean;
checkRole?: string[];
};
const props = defineProps<SelectProps<typeof getList> & ExclusiveProps>();
const { getOptions, setFirstValue, getSelectedOption, filter } =
createSelect<SelectOption>(
{
value,
valueOption,
selectOptions,
getList: async (query) => {
const ret = await getList({
query,
...props.params,
includeBranch: true,
pageSize: 99999,
userType: 'DELEGATE',
status: 'ACTIVE',
});
if (ret) return ret.result;
},
getByValue: async (id) => {
const ret = await getById(id);
if (ret) return ret;
},
},
{ valueField: 'id' },
);
onMounted(async () => {
await getOptions();
if (props.autoSelectOnSingle && selectOptions.value.length === 1) {
setFirstValue();
}
if (props.selectFirstValue) {
setDefaultValue();
} else await getSelectedOption();
});
function setDefaultValue() {
setFirstValue();
}
</script>
<template>
<SelectInput
v-model="value"
incremental
option-value="id"
:label
:placeholder
:readonly
:disable="disabled"
:option="selectOptions"
:hide-selected="false"
:fill-input="false"
:rules="[
(v: string) => !props.required || !!v || $t('form.error.required'),
]"
@filter="filter"
>
<template #selected-item="{ opt }">
{{
$i18n.locale === 'tha'
? opt.firstName + ' ' + opt.lastName
: opt.firstNameEN + ' ' + opt.lastNameEN
}}
</template>
<template #option="{ opt, scope }">
<q-item v-bind="scope.itemProps">
<span class="row items-center">
{{
$i18n.locale === 'tha'
? opt.firstName + ' ' + opt.lastName
: opt.firstNameEN + ' ' + opt.lastNameEN
}}
</span>
</q-item>
<q-separator class="q-mx-sm" />
</template>
<template #append v-if="clearable">
<q-icon
v-if="!readonly && value"
name="mdi-close-circle"
@click.stop="value = ''"
class="cursor-pointer clear-btn"
/>
</template>
</SelectInput>
</template>

View file

@ -139,6 +139,7 @@ export default {
import: 'Import', import: 'Import',
numberOfDay: 'Number of days', numberOfDay: 'Number of days',
other: 'Other', other: 'Other',
agencyAddress: 'Agency Address',
}, },
menu: { menu: {
@ -299,6 +300,7 @@ export default {
branchNameField: "Only letters, numbers, or the characters . , - ' &.", branchNameField: "Only letters, numbers, or the characters . , - ' &.",
branchNameENField: branchNameENField:
"Only English letters, numbers, or the characters . , - ' &.", "Only English letters, numbers, or the characters . , - ' &.",
passportFormat: 'Please enter the passport number in the correct format.',
}, },
warning: { warning: {
title: 'Warning {msg}', title: 'Warning {msg}',
@ -958,6 +960,10 @@ export default {
Validate: 'Validate', Validate: 'Validate',
Complete: 'Complete', Complete: 'Complete',
Canceled: 'Canceled', Canceled: 'Canceled',
Restart: 'go proceed again',
receive: {
Canceled: 'Canceled',
},
}, },
receiveTask: 'Receive Task', receiveTask: 'Receive Task',
@ -977,7 +983,7 @@ export default {
noRequestAvailable: 'There is no request list available for processing', noRequestAvailable: 'There is no request list available for processing',
validate: 'Validate', validate: 'Validate',
done: 'Done', done: 'Done',
confirmValidate: 'Confirm Validate', confirmEndWork: 'Confirm end of work',
}, },
dialog: { dialog: {
@ -1012,6 +1018,10 @@ export default {
confirmValidate: 'Do you confirm the validation?', confirmValidate: 'Do you confirm the validation?',
warningSelectDeliveryStaff: warningSelectDeliveryStaff:
'You have not yet selected a document delivery staff.', 'You have not yet selected a document delivery staff.',
confirmEndWorkWarning:
"Do you want to end the work now? The current statuses 'Pending', 'In Progress', 'To Be Reprocessed' will be changed to 'Redo All'.",
confirmEndWork: 'Do you want to end the work?',
}, },
action: { action: {
ok: 'OK', ok: 'OK',

View file

@ -139,6 +139,7 @@ export default {
next: 'ถัดไป', next: 'ถัดไป',
numberOfDay: 'จำนวนวัน', numberOfDay: 'จำนวนวัน',
other: 'อื่นๆ', other: 'อื่นๆ',
agencyAddress: 'ที่อยู่หน่วยงาน ',
}, },
menu: { menu: {
@ -298,6 +299,7 @@ export default {
branchNameField: "โปรดใช้ตัวอักษร ตัวเลข หรือ . , - ' & เท่านั้น", branchNameField: "โปรดใช้ตัวอักษร ตัวเลข หรือ . , - ' & เท่านั้น",
branchNameENField: branchNameENField:
"โปรดใช้ตัวอักษรภาษาอังกฤษ ตัวเลข หรือ . , - ' & เท่านั้น", "โปรดใช้ตัวอักษรภาษาอังกฤษ ตัวเลข หรือ . , - ' & เท่านั้น",
passportFormat: 'กรุณากรอกหมายเลขพาสปอร์ตให้ถูกต้องตามรูปแบบ',
}, },
warning: { warning: {
title: 'แจ้งเตือน {msg}', title: 'แจ้งเตือน {msg}',
@ -941,12 +943,15 @@ export default {
InProgress: 'กำลังดำเนินการ ', InProgress: 'กำลังดำเนินการ ',
Success: 'ดำเนินการสำเร็จ', Success: 'ดำเนินการสำเร็จ',
Failed: 'ดำเนินการไม่สำเร็จ', Failed: 'ดำเนินการไม่สำเร็จ',
Redo: 'ทำใหม่', Redo: 'ยกเลิกรอดำเนินการใหม่',
Validate: 'ตรวจสอบความถูกต้อง', Validate: 'ตรวจสอบความถูกต้อง',
Complete: 'ดำเนินการเสร็จสิ้น', Complete: 'ดำเนินการเสร็จสิ้น',
Canceled: ' รายการคำขอถูกยกเลิก', Canceled: 'รายการคำขอถูกยกเลิก',
Restart: 'ไปดำเนินการใหม่',
receive: {
Canceled: 'ยกเลิก',
},
}, },
receiveTask: 'รับงาน', receiveTask: 'รับงาน',
receiveScan: 'รับงานแบบสแกน', receiveScan: 'รับงานแบบสแกน',
receiveCustom: 'รับงานแบบเลือกเอง', receiveCustom: 'รับงานแบบเลือกเอง',
@ -964,7 +969,7 @@ export default {
noRequestAvailable: 'ไม่มีใบรายการคำขอที่สามารถดำเนินการได้', noRequestAvailable: 'ไม่มีใบรายการคำขอที่สามารถดำเนินการได้',
validate: 'ตรวจสอบสินค้า', validate: 'ตรวจสอบสินค้า',
done: 'ดำเนินการแล้ว', done: 'ดำเนินการแล้ว',
confirmValidate: 'ยืนยันการตรวจสอบ', confirmEndWork: 'ยืนยันการจบงาน',
}, },
dialog: { dialog: {
@ -996,7 +1001,8 @@ export default {
confirmSavingStatus: confirmSavingStatus:
'คุณต้องการยืนยันการบันทึกข้อมูลการเปลี่ยนสถานะใช่หรือไม่', 'คุณต้องการยืนยันการบันทึกข้อมูลการเปลี่ยนสถานะใช่หรือไม่',
confirmSending: 'ยืนยันการส่งงานใช่หรือไม่', confirmSending: 'ยืนยันการส่งงานใช่หรือไม่',
confirmValidate: 'ยืนยันการตรวจสอบใช่หรือไม่', confirmEndWorkWarning: `ท่านต้องการให้ดำเนินการจบงานในขณะนี้หรือไม่? สถานะปัจจุบันที่แสดงว่า 'รอดำเนินการ' 'กำลังดำเนินการ' 'ไปดำเนินการใหม่' จะถูกเปลี่ยนเป็น 'ทำใหม่ทั้งหมด'`,
confirmEndWork: 'ท่านต้องการจบงานใช่หรือไม่',
}, },
action: { action: {
ok: 'ยืนยัน', ok: 'ยืนยัน',

View file

@ -4630,7 +4630,7 @@ const emptyCreateDialog = ref(false);
" "
class="q-mb-xl" class="q-mb-xl"
/> />
<FormPerso <FormPerson
id="drawer-form-personal" id="drawer-form-personal"
prefix-id="drawer-info-employee" prefix-id="drawer-info-employee"
dense dense

View file

@ -1,13 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import useUserStore from 'stores/user'; import SelectAgent from 'src/components/shared/select/SelectAgent.vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import SelectInput from 'src/components/shared/SelectInput.vue';
import { QSelect } from 'quasar';
const userStore = useUserStore();
const { locale } = useI18n({ useScope: 'global' });
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
@ -20,41 +12,6 @@ const officeTel = defineModel<string>('officeTel');
const agent = defineModel<string>('agent'); const agent = defineModel<string>('agent');
const agentUserId = defineModel<string>('agentUserId'); const agentUserId = defineModel<string>('agentUserId');
const agentOptions = ref<{ id: string; label: string; labelEN: string }[]>([]);
async function fetchAgentOptions(query: string) {
const res = await userStore.fetchList({
includeBranch: true,
query: query,
pageSize: 99999,
userType: 'DELEGATE',
status: 'ACTIVE',
});
if (res) {
agentOptions.value = res.result.map((v) => ({
id: v.id,
label: v.firstName + ' ' + v.lastName,
labelEN: v.firstNameEN + ' ' + v.lastNameEN,
}));
}
}
async function filter(val: string, update: (...args: unknown[]) => void) {
update(
async () => {
await fetchAgentOptions(val);
},
(ref: QSelect) => {
if (val !== '' && ref.options && ref.options?.length > 0) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
},
);
}
</script> </script>
<template> <template>
@ -153,18 +110,12 @@ async function filter(val: string, update: (...args: unknown[]) => void) {
</template> </template>
</q-input> </q-input>
<SelectInput <SelectAgent
@click="fetchAgentOptions" :label="$t('customer.form.agent')"
v-model:value="agentUserId"
:readonly :readonly
incremental
v-model="agentUserId"
id="quotation-branch" id="quotation-branch"
class="col-md-6 col-12" class="col-md-6 col-12"
:option="agentOptions"
:label="$t('customer.form.agent')"
option-value="id"
:option-label="locale === 'eng' ? 'labelEN' : 'label'"
@filter="(val: string, update) => filter(val, update)"
/> />
</div> </div>
</template> </template>

View file

@ -59,10 +59,12 @@ const { state: customerFormState, currentFormData: customerFormData } =
const { currentMyBranch } = storeToRefs(userBranch); const { currentMyBranch } = storeToRefs(userBranch);
const fieldSelectedOption = computed(() => { const fieldSelectedOption = computed(() => {
return columnQuotation.map((v) => ({ return columnQuotation
label: v.label, .filter((v) => v.name !== 'action')
value: v.name, .map((v) => ({
})); label: v.label,
value: v.name,
}));
}); });
const special = ref(false); const special = ref(false);
const branchId = ref(''); const branchId = ref('');

View file

@ -4,6 +4,7 @@ import { getInstance } from 'src/services/keycloak';
import { useNavigator } from 'src/stores/navigator'; import { useNavigator } from 'src/stores/navigator';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { initLang } from 'src/utils/ui';
const $q = useQuasar(); const $q = useQuasar();
const { locale } = useI18n(); const { locale } = useI18n();
@ -25,6 +26,8 @@ function sendMessage() {
} }
onMounted(() => { onMounted(() => {
initLang();
currentLocale.value = locale.value;
navigatorStore.current.title = 'menu.dms'; navigatorStore.current.title = 'menu.dms';
navigatorStore.current.path = [ navigatorStore.current.path = [
{ {
@ -47,12 +50,14 @@ watch(
watch([locale, $q.dark], () => { watch([locale, $q.dark], () => {
sendMessage(); sendMessage();
}); });
const currentLocale = ref(locale.value);
</script> </script>
<template> <template>
<iframe <iframe
ref="iframe" ref="iframe"
:src="`${EDM_SERVICE}/?at=${at}&rt=${rt}&lang=${locale}`" :src="`${EDM_SERVICE}/?at=${at}&rt=${rt}&lang=${currentLocale}`"
frameborder="0" frameborder="0"
class="full-width full-height rounded" class="full-width full-height rounded"
allowtransparency="true" allowtransparency="true"

View file

@ -70,6 +70,7 @@ async function fetchStats() {
function triggerCancel(id: string) { function triggerCancel(id: string) {
dialogWarningClose(t, { dialogWarningClose(t, {
message: t('form.warning.cancel'), message: t('form.warning.cancel'),
actionText: t('dialog.action.ok'),
action: async () => { action: async () => {
const res = await requestListStore.cancelRequest(id); const res = await requestListStore.cancelRequest(id);
if (res) { if (res) {
@ -222,6 +223,10 @@ watch([() => pageState.inputSearch, () => pageState.statusFilter], () =>
label: $t('requestList.status.Completed'), label: $t('requestList.status.Completed'),
value: RequestDataStatus.Completed, value: RequestDataStatus.Completed,
}, },
{
label: $t('requestList.status.Canceled'),
value: RequestDataStatus.Canceled,
},
]" ]"
/> />
<q-select <q-select

View file

@ -11,11 +11,14 @@ import {
PropString, PropString,
} from 'src/stores/product-service/types'; } from 'src/stores/product-service/types';
import { Attributes } from 'src/stores/request-list/types'; import { Attributes, RequestData } from 'src/stores/request-list/types';
import { getCustomerName, getEmployeeName } from 'src/stores/utils';
import useOptionStore from 'src/stores/options'; import useOptionStore from 'src/stores/options';
import { useRequestList } from 'src/stores/request-list'; import { useRequestList } from 'src/stores/request-list';
import { useI18n } from 'vue-i18n';
const { locale } = useI18n();
const optionStore = useOptionStore(); const optionStore = useOptionStore();
const requestListStore = useRequestList(); const requestListStore = useRequestList();
@ -24,6 +27,7 @@ const props = withDefaults(
id: string; id: string;
readonly?: boolean; readonly?: boolean;
propertiesToShow: (PropString | PropNumber | PropDate | PropOptions)[]; propertiesToShow: (PropString | PropNumber | PropDate | PropOptions)[];
requestListData: RequestData;
}>(), }>(),
{ {
id: '', id: '',
@ -31,6 +35,8 @@ const props = withDefaults(
}, },
); );
const readonlyField = ['quotationNo', 'contactPerson', 'telephone', 'employer'];
const formRemark = ref<string>(''); const formRemark = ref<string>('');
const formData = ref<{ const formData = ref<{
[field: string]: string | number | null | undefined; [field: string]: string | number | null | undefined;
@ -77,10 +83,22 @@ function triggerEdit() {
} }
function assignToForm() { function assignToForm() {
const requestData = props.requestListData;
// console.log(requestData);
formRemark.value = attributes.value?.remark || ''; formRemark.value = attributes.value?.remark || '';
formData.value = JSON.parse( formData.value = JSON.parse(
JSON.stringify(attributes.value?.properties || {}), JSON.stringify(attributes.value?.properties || {}),
); );
formData.value['quotationNo'] = requestData.quotation.code;
formData.value['contactPerson'] = requestData.quotation.contactName;
formData.value['telephone'] = requestData.quotation.contactTel;
formData.value['employee'] = getEmployeeName(requestData.employee, {
locale: locale.value,
});
formData.value['employer'] = getCustomerName(
requestData.quotation.customerBranch,
{ locale: locale.value },
);
} }
defineEmits<{ defineEmits<{
@ -143,7 +161,7 @@ defineEmits<{
{{ i + 1 }} {{ optionStore.mapOption(prop.fieldName) }} {{ i + 1 }} {{ optionStore.mapOption(prop.fieldName) }}
</article> </article>
<PropertiesToInput <PropertiesToInput
:readonly="!state.isEdit" :readonly="!state.isEdit || readonlyField.includes(prop.fieldName)"
:prop="prop" :prop="prop"
:placeholder="optionStore.mapOption(prop.fieldName)" :placeholder="optionStore.mapOption(prop.fieldName)"
v-model="formData[prop.fieldName]" v-model="formData[prop.fieldName]"

View file

@ -816,6 +816,7 @@ function goToQuotation(
" "
/> />
<PropertiesExpansion <PropertiesExpansion
:request-list-data="data"
:id="value.id" :id="value.id"
:readonly=" :readonly="
data.requestDataStatus === RequestDataStatus.Canceled data.requestDataStatus === RequestDataStatus.Canceled

View file

@ -69,6 +69,10 @@ function taskOrderStatus(value: TaskOrderStatus, type: 'status' | 'color') {
status: 'taskOrder.sentTask', status: 'taskOrder.sentTask',
color: '--blue-6-hsl', color: '--blue-6-hsl',
}, },
[TaskOrderStatus.Restart]: {
status: 'taskOrder.status.Restart',
color: '--red-5-hsl',
},
}[value][type]; }[value][type];
} }
@ -157,7 +161,13 @@ const emit = defineEmits<{
{{ props.row.code || '-' }} {{ props.row.code || '-' }}
</div> </div>
</q-td> </q-td>
<q-td v-if="visibleColumns.includes('issueBranch')">-</q-td> <q-td v-if="visibleColumns.includes('issueBranch')">
{{
$i18n.locale === 'eng'
? props.row.registeredBranch.nameEN || '-'
: props.row.registeredBranch.name || '-'
}}
</q-td>
<q-td v-if="visibleColumns.includes('institution')"> <q-td v-if="visibleColumns.includes('institution')">
{{ {{
$i18n.locale === 'eng' $i18n.locale === 'eng'
@ -251,7 +261,10 @@ const emit = defineEmits<{
:custom-data="[ :custom-data="[
{ {
label: $t('taskOrder.issueBranch'), label: $t('taskOrder.issueBranch'),
value: props.row.issueBranch, value:
$i18n.locale === 'eng'
? props.row.registeredBranch.nameEN || '-'
: props.row.registeredBranch.name || '-',
}, },
{ {
label: $t('general.agencies'), label: $t('general.agencies'),

View file

@ -75,7 +75,9 @@ function hideIcon() {
:label=" :label="
currStatus?.value === TaskStatus.Validate && type === 'order' currStatus?.value === TaskStatus.Validate && type === 'order'
? $t('taskOrder.done') ? $t('taskOrder.done')
: $t(`taskOrder.status.${status}`) : currStatus?.value === TaskStatus.Redo && type === 'receive'
? $t(`taskOrder.status.receive.Canceled`)
: $t(`taskOrder.status.${status}`)
" "
class="text-capitalize text-weight-regular product-status rounded" class="text-capitalize text-weight-regular product-status rounded"
:class="{ :class="{
@ -90,7 +92,8 @@ function hideIcon() {
negative: negative:
currStatus?.value === TaskStatus.Failed || currStatus?.value === TaskStatus.Failed ||
currStatus?.value === TaskStatus.Redo || currStatus?.value === TaskStatus.Redo ||
currStatus?.value === TaskStatus.Canceled, currStatus?.value === TaskStatus.Canceled ||
currStatus?.value === TaskStatus.Restart,
'pointer-events-none': { 'pointer-events-none': {
order: !['Success', 'Failed', 'Validate'].includes(status || ''), order: !['Success', 'Failed', 'Validate'].includes(status || ''),
receive: status !== TaskStatus.InProgress, receive: status !== TaskStatus.InProgress,
@ -110,7 +113,9 @@ function hideIcon() {
(v) => v.value === TaskStatus.Complete, (v) => v.value === TaskStatus.Complete,
), ),
Failed: taskStatusOrderToggle.filter( Failed: taskStatusOrderToggle.filter(
(v) => v.value === TaskStatus.Redo, (v) =>
v.value === TaskStatus.Redo ||
v.value === TaskStatus.Restart,
), ),
Validate: taskStatusOrderToggle.filter( Validate: taskStatusOrderToggle.filter(
(v) => v.value !== TaskStatus.InProgress, (v) => v.value !== TaskStatus.InProgress,

View file

@ -33,6 +33,10 @@ export const taskStatusOpts = [
status: TaskStatus.Complete, status: TaskStatus.Complete,
name: 'taskOrder.status.Complete', name: 'taskOrder.status.Complete',
}, },
{
status: TaskStatus.Restart,
name: 'taskOrder.status.Restart',
},
]; ];
export const pageTabs = [ export const pageTabs = [
@ -89,6 +93,11 @@ export const taskStatusOrderToggle = [
icon: 'mdi-file-remove-outline', icon: 'mdi-file-remove-outline',
color: 'negative', color: 'negative',
}, },
{
value: TaskStatus.Restart,
icon: 'mdi-file-remove-outline',
color: 'negative',
},
]; ];
export const column = [ export const column = [

View file

@ -69,7 +69,7 @@ defineProps<{
<span>{{ branch.webUrl }}</span> <span>{{ branch.webUrl }}</span>
</article> </article>
<article> <article>
<b>กค</b> <b>{{ $t('general.agencyAddress') }}</b>
<span> <span>
{{ {{
formatAddress({ formatAddress({

View file

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
// NOTE: Import stores // NOTE: Import stores
import { dateFormat } from 'src/utils/datetime'; import { dateFormatJS } from 'src/utils/datetime';
// NOTE Import Types // NOTE Import Types
import { import {
@ -19,8 +19,8 @@ defineProps<{
contactTel?: string; contactTel?: string;
contactUrl?: string; contactUrl?: string;
email?: string; email?: string;
receivedDate?: Date | string; acceptedAt?: Date | string;
deliveryDate?: Date | string; submittedAt?: Date | string;
status?: UserTaskStatus; status?: UserTaskStatus;
}>(); }>();
</script> </script>
@ -96,7 +96,11 @@ defineProps<{
:label="$t('taskOrder.workStartDate')" :label="$t('taskOrder.workStartDate')"
> >
<template #value> <template #value>
{{ receivedDate ? dateFormat(receivedDate) : '-' }} {{
acceptedAt
? dateFormatJS({ date: acceptedAt, withTime: true })
: '-'
}}
</template> </template>
</DataDisplay> </DataDisplay>
@ -105,7 +109,11 @@ defineProps<{
:label="$t('taskOrder.workSubmissionDate')" :label="$t('taskOrder.workSubmissionDate')"
> >
<template #value> <template #value>
{{ deliveryDate ? dateFormat(deliveryDate) : '-' }} {{
submittedAt
? dateFormatJS({ date: submittedAt, withTime: true })
: '-'
}}
</template> </template>
</DataDisplay> </DataDisplay>
@ -121,6 +129,7 @@ defineProps<{
Pending: 'waitReceive', Pending: 'waitReceive',
Accept: 'receiveTask', Accept: 'receiveTask',
Submit: 'sentTask', Submit: 'sentTask',
Restart: 'status.Restart',
}[status] ?? 'waitReceive' }[status] ?? 'waitReceive'
}`, }`,
) )
@ -130,6 +139,7 @@ defineProps<{
Pending: '--info-bg', Pending: '--info-bg',
Accept: '--info-bg', Accept: '--info-bg',
Submit: '--positive-bg', Submit: '--positive-bg',
Restart: '--negative-bg',
}[status] ?? '--info-bg' }[status] ?? '--info-bg'
" "
/> />

View file

@ -467,6 +467,7 @@ async function getFileList(taskId: string) {
async function remove(taskId: string, n: string) { async function remove(taskId: string, n: string) {
dialogWarningClose(t, { dialogWarningClose(t, {
message: t('dialog.message.confirmDelete'), message: t('dialog.message.confirmDelete'),
actionText: t('dialog.action.ok'),
action: async () => { action: async () => {
const res = await taskOrderStore.delAttachment({ const res = await taskOrderStore.delAttachment({
parentId: taskId, parentId: taskId,
@ -876,6 +877,16 @@ watch([currentFormData.value.taskStatus], () => {
(l) => l.userId === v.responsibleUser.id, (l) => l.userId === v.responsibleUser.id,
)?.userTaskStatus || UserTaskStatus.Pending )?.userTaskStatus || UserTaskStatus.Pending
" "
:accepted-at="
fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.acceptedAt
"
:submitted-at="
fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.submittedAt
"
> >
<template #product> <template #product>
<FormGroupHead> <FormGroupHead>
@ -977,12 +988,21 @@ watch([currentFormData.value.taskStatus], () => {
> >
<template #append="{ props: subProps }"> <template #append="{ props: subProps }">
<TaskStatusComponent <TaskStatusComponent
:key="subProps.row.id"
:no-action="view !== TaskOrderStatus.Validate" :no-action="view !== TaskOrderStatus.Validate"
type="order" type="order"
:readonly=" :readonly="
fullTaskOrder?.userTask.find( (() => {
(l) => l.userId === v.responsibleUser.id, const _userStatus =
)?.userTaskStatus !== UserTaskStatus.Submit fullTaskOrder?.userTask.find(
(l) => l.userId === v.responsibleUser.id,
)?.userTaskStatus;
console.log(_userStatus);
return (
_userStatus !== UserTaskStatus.Submit &&
_userStatus !== UserTaskStatus.Restart
);
})()
" "
:status="subProps.row.taskStatus" :status="subProps.row.taskStatus"
@click-failed=" @click-failed="
@ -1066,27 +1086,40 @@ watch([currentFormData.value.taskStatus], () => {
solid solid
/> />
</template> </template>
<SaveButton <SaveButton
v-if="state.mode !== 'create' && view === TaskOrderStatus.Validate" v-if="state.mode !== 'create' && view === TaskOrderStatus.Validate"
:disabled=" :disabled="
!fullTaskOrder?.taskList.every( !fullTaskOrder?.taskList.some((t) =>
(t) => [
t.taskStatus === TaskStatus.Complete || TaskStatus.Complete,
t.taskStatus === TaskStatus.Redo || TaskStatus.Redo,
t.taskStatus === TaskStatus.Canceled, TaskStatus.Canceled,
) || fullTaskOrder.taskOrderStatus === TaskOrderStatus.Complete TaskStatus.Validate,
].includes(t.taskStatus),
) || fullTaskOrder?.taskOrderStatus === TaskOrderStatus.Complete
" "
@click=" @click="
dialogWarningClose(t, { dialogWarningClose(t, {
message: t('dialog.message.confirmValidate'), message: $t(
`${
fullTaskOrder?.taskList.some(
(t) =>
t.taskStatus === TaskStatus.Pending ||
t.taskStatus === TaskStatus.InProgress ||
t.taskStatus === TaskStatus.Restart,
)
? 'dialog.message.confirmEndWorkWarning'
: 'dialog.message.confirmEndWork'
}`,
),
actionText: $t('dialog.action.ok'),
action: async () => { action: async () => {
await completeValidate(); await completeValidate();
}, },
cancel: () => {}, cancel: () => {},
}) })
" "
:label="$t('taskOrder.confirmValidate')" :label="$t('taskOrder.confirmEndWork')"
icon="mdi-check" icon="mdi-check"
solid solid
></SaveButton> ></SaveButton>

View file

@ -5,7 +5,7 @@ import { computed, onMounted, ref, watch } from 'vue';
import { getUserId } from 'src/services/keycloak'; import { getUserId } from 'src/services/keycloak';
// NOTE: Import Components // NOTE: Import Components
import { SaveButton, MainButton } from 'src/components/button'; import { SaveButton } from 'src/components/button';
import { StateButton } from 'components/button'; import { StateButton } from 'components/button';
import InfoMessengerExpansion from '../expansion/receive/InfoMessengerExpansion.vue'; import InfoMessengerExpansion from '../expansion/receive/InfoMessengerExpansion.vue';
import InfoProductExpansion from '../expansion/receive/InfoProductExpansion.vue'; import InfoProductExpansion from '../expansion/receive/InfoProductExpansion.vue';
@ -61,6 +61,7 @@ const statusTabForm = ref<
}, },
]); ]);
const failedDialog = ref(false); const failedDialog = ref(false);
const failedDialogReadonly = ref(false);
const taskStatusRecords = ref< const taskStatusRecords = ref<
{ {
requestWorkId: string; requestWorkId: string;
@ -72,6 +73,7 @@ const taskStatusRecords = ref<
>([]); >([]);
const selectedEmployee = ref< const selectedEmployee = ref<
(RequestWork & { (RequestWork & {
taskStatus: TaskStatus;
_template?: { _template?: {
id: string; id: string;
templateName: string; templateName: string;
@ -207,15 +209,6 @@ let taskListGroup = computed(() => {
// NOTE: Function // NOTE: Function
async function sendTask() { async function sendTask() {
if (!fullTaskOrder.value) return; if (!fullTaskOrder.value) return;
if (
!fullTaskOrder.value.taskList.every(
(t) =>
t.taskStatus === TaskStatus.Success ||
t.taskStatus === TaskStatus.Failed ||
t.taskStatus === TaskStatus.Canceled,
)
)
return;
await useTaskOrderStore().submitTaskOrder( await useTaskOrderStore().submitTaskOrder(
fullTaskOrder.value.id, fullTaskOrder.value.id,
getUserId(), getUserId(),
@ -477,6 +470,20 @@ watch([currentFormData.value.taskStatus], () => {
fullTaskOrder?.taskList[0].requestWorkStep.responsibleUserId, fullTaskOrder?.taskList[0].requestWorkStep.responsibleUserId,
)?.userTaskStatus || UserTaskStatus.Pending )?.userTaskStatus || UserTaskStatus.Pending
" "
:accepted-at="
fullTaskOrder.userTask.find(
(l) =>
l.userId ===
fullTaskOrder?.taskList[0].requestWorkStep.responsibleUserId,
)?.acceptedAt
"
:submitted-at="
fullTaskOrder.userTask.find(
(l) =>
l.userId ===
fullTaskOrder?.taskList[0].requestWorkStep.responsibleUserId,
)?.submittedAt
"
/> />
<InfoProductExpansion <InfoProductExpansion
@ -633,6 +640,15 @@ watch([currentFormData.value.taskStatus], () => {
}, },
]; ];
failedDialog = true; failedDialog = true;
failedDialogReadonly =
fullTaskOrder?.userTask.find(
(l) =>
l.userId ===
fullTaskOrder?.taskList[0].requestWorkStep
.responsibleUserId,
)?.userTaskStatus === UserTaskStatus.Submit
? true
: false;
} }
" "
@change-status=" @change-status="
@ -654,6 +670,7 @@ watch([currentFormData.value.taskStatus], () => {
</q-expansion-item> </q-expansion-item>
<FailRemarkDialog <FailRemarkDialog
:readonly="failedDialogReadonly"
:fail-task-option="list" :fail-task-option="list"
v-model:open="failedDialog" v-model:open="failedDialog"
v-model:set-task-status-list="taskStatusRecords" v-model:set-task-status-list="taskStatusRecords"
@ -688,6 +705,7 @@ watch([currentFormData.value.taskStatus], () => {
</article> </article>
<!-- SEC: footer --> <!-- SEC: footer -->
<footer <footer
v-if="fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending" v-if="fullTaskOrder.taskOrderStatus !== TaskOrderStatus.Pending"
class="surface-1 q-pa-md full-width" class="surface-1 q-pa-md full-width"
@ -695,16 +713,22 @@ watch([currentFormData.value.taskStatus], () => {
<nav class="row justify-end"> <nav class="row justify-end">
<SaveButton <SaveButton
:disabled=" :disabled="
!fullTaskOrder.taskList.every( fullTaskOrder.taskList.some(
(t) => (t) =>
t.taskStatus === TaskStatus.Success || t.taskStatus === TaskStatus.Pending ||
t.taskStatus === TaskStatus.Failed || t.taskStatus === TaskStatus.InProgress ||
t.taskStatus === TaskStatus.Canceled, t.taskStatus === TaskStatus.Restart,
) ) ||
fullTaskOrder?.userTask.find(
(l) =>
l.userId ===
fullTaskOrder?.taskList[0].requestWorkStep.responsibleUserId,
)?.userTaskStatus !== UserTaskStatus.Accept
" "
@click=" @click="
dialogWarningClose($t, { dialogWarningClose($t, {
message: $t('dialog.message.confirmSending'), message: $t('dialog.message.confirmSending'),
actionText: $t('dialog.action.ok'),
action: async () => { action: async () => {
await sendTask(); await sendTask();
}, },

View file

@ -12,6 +12,7 @@ export enum TaskOrderStatus {
Complete = 'Complete', Complete = 'Complete',
Accept = 'Accept', // messenger only Accept = 'Accept', // messenger only
Submit = 'Submit', // messenger only Submit = 'Submit', // messenger only
Restart = 'Restart',
} }
export enum TaskStatus { export enum TaskStatus {
@ -23,6 +24,7 @@ export enum TaskStatus {
Validate = 'Validate', Validate = 'Validate',
Complete = 'Complete', Complete = 'Complete',
Canceled = 'Canceled', Canceled = 'Canceled',
Restart = 'Restart',
} }
export interface TaskOrder { export interface TaskOrder {
@ -61,6 +63,8 @@ export interface UserTask {
taskOrderId: string; taskOrderId: string;
userId: string; userId: string;
userTaskStatus: UserTaskStatus; userTaskStatus: UserTaskStatus;
acceptedAt?: Date | string;
submittedAt?: Date | string;
} }
export interface Institution { export interface Institution {
@ -196,6 +200,7 @@ export enum UserTaskStatus {
Pending = 'Pending', Pending = 'Pending',
Accept = 'Accept', Accept = 'Accept',
Submit = 'Submit', Submit = 'Submit',
Restart = 'Restart',
} }
export interface SetTaskStatusPayload { export interface SetTaskStatusPayload {

View file

@ -14,6 +14,10 @@ import { getRole } from 'src/services/keycloak';
import GlobalDialog from 'components/GlobalDialog.vue'; import GlobalDialog from 'components/GlobalDialog.vue';
import DialogDuplicateData from 'components/DialogDuplicateData.vue'; import DialogDuplicateData from 'components/DialogDuplicateData.vue';
import useOptionStore from '../options';
import { CustomerBranch } from '../customer/types';
import { CustomerBranchRelation } from '../quotations';
import { Employee } from '../employee/types';
export const baseUrl = import.meta.env.VITE_API_BASE_URL; export const baseUrl = import.meta.env.VITE_API_BASE_URL;
@ -53,6 +57,7 @@ export function dialogWarningClose(
t: ComposerTranslation, t: ComposerTranslation,
opts: { opts: {
message?: string; message?: string;
actionText?: string;
action?: (...args: unknown[]) => unknown; action?: (...args: unknown[]) => unknown;
cancel?: (...args: unknown[]) => unknown; cancel?: (...args: unknown[]) => unknown;
}, },
@ -61,7 +66,7 @@ export function dialogWarningClose(
color: 'warning', color: 'warning',
icon: 'mdi-alert', icon: 'mdi-alert',
title: t('form.warning.title'), title: t('form.warning.title'),
actionText: t('dialog.action.close'), actionText: opts.actionText || t('dialog.action.close'),
message: opts.message || t('dialog.message.warningClose'), message: opts.message || t('dialog.message.warningClose'),
action: async () => { action: async () => {
if (opts.action) opts.action(); if (opts.action) opts.action();
@ -542,3 +547,42 @@ export function changeMode(mode: string) {
export function capitalizeFirstLetter(val: string) { export function capitalizeFirstLetter(val: string) {
return String(val).charAt(0).toUpperCase() + String(val).slice(1); return String(val).charAt(0).toUpperCase() + String(val).slice(1);
} }
export function getCustomerName(
record: CustomerBranch | CustomerBranchRelation,
opts?: {
locale?: string;
noCode?: boolean;
},
) {
const customer = record;
return (
{
['CORP']: {
['eng']: customer.registerNameEN,
['tha']: customer.registerName,
}[opts?.locale || 'eng'],
['PERS']:
{
['eng']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstNameEN} ${customer?.lastNameEN}`,
['tha']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstName} ${customer?.lastName}`,
}[opts?.locale || 'eng'] || '-',
}[customer.customer.customerType] +
(opts?.noCode ? '' : ' ' + `(${customer.code})`)
);
}
export function getEmployeeName(
record: Employee,
opts?: {
locale?: string;
},
) {
const employee = record;
return {
['eng']: `${useOptionStore().mapOption(employee.namePrefix)} ${employee.firstNameEN} ${employee.lastNameEN}`,
['tha']: `${useOptionStore().mapOption(employee.namePrefix)} ${employee.firstName} ${employee.lastName}`,
}[opts?.locale || 'eng'];
}

View file

@ -19,7 +19,8 @@ export function dateFormatJS(opts: {
noDay?: boolean; noDay?: boolean;
noMonth?: boolean; noMonth?: boolean;
noYear?: boolean; noYear?: boolean;
}) { withTime?: boolean;
}): string {
const dt = opts.date ? new Date(opts.date) : new Date(); const dt = opts.date ? new Date(opts.date) : new Date();
const { locale } = i18n.global; const { locale } = i18n.global;
@ -35,6 +36,17 @@ export function dateFormatJS(opts: {
opts.timeStyle = opts.timeStyle || 'short'; opts.timeStyle = opts.timeStyle || 'short';
} }
let timeText = opts.withTime
? ' ' +
dateFormatJS({
date: opts.date,
timeOnly: true,
timeStyle: opts.timeStyle,
})
: '';
if (timeText) opts.timeStyle = undefined;
let formatted = new Intl.DateTimeFormat(opts.locale, { let formatted = new Intl.DateTimeFormat(opts.locale, {
day: opts.noDay ? undefined : opts.dayStyle || 'numeric', day: opts.noDay ? undefined : opts.dayStyle || 'numeric',
month: opts.noMonth ? undefined : opts.monthStyle || 'short', month: opts.noMonth ? undefined : opts.monthStyle || 'short',
@ -46,11 +58,13 @@ export function dateFormatJS(opts: {
case Lang.Thai: case Lang.Thai:
case 'th-TH': case 'th-TH':
case 'th-Th': case 'th-Th':
return formatted.replace(/(\d{4})/, (year) => return (
(parseInt(year) - 543).toString(), formatted.replace(/(\d{4})/, (year) =>
(parseInt(year) - 543).toString(),
) + timeText
); );
default: default:
return formatted; return formatted + timeText;
} }
} }