feat: menu request list (#75)
* feat: i18n * feat: request list * refactor: hide stat transition on app.scss * feat: request list i18n * feat: request list => constants and main page * feat: add store * feat: add fetch data * feat: add utilities fn * feat: add store function / types * refactor: request list type * refactor: request list constants * refactor: quotation card => add customData and badge color props * feat: avatar group components * feat: request list group * refactor: request list => remove tab, add table data * feat: send search query * feat: add parameter * refactor: remove unused function * fix: rename component lits to list * feat: show stats from api * chore: cleanup * refactor: make it type safe * refactor: accept rotate flow id as parameter * feat: use page size component * feat: add component, data display & expansion product * feat: i18n * refactor: constants and request list table * refactor: type code, createdAt, updatedAt * refactor: utils function changThemeMode * feat: request list => view page * refactor: use type instead of infer from value * fix: function getEmployeeName att type * refactor: fetch work list * refactor: loop work list * feat: add i18n duty * feat: add form issue component * feat: add form issue section * fix: store error * refactor: edit by value * refactor: accept basic info from outside instead * feat: add status filter support on fetch * refactor: remove delete button * refactor: wording * feat/fix: request list i18n & constant * feat: document type * feat/refactor: request list => document expansion * refactor: doc expansion use FormGroupHead * refactor: fetch data based on id from route param * refactor: text area disable * feat: properties expansion display (mocking) * refactor: add document at product relation * refactor: edit get value product * feat: get workflow step to show on top * refactor: add type * refactor: add get attachment * refactor: add view attachment * refactor: edit file name * refactor: define props get hide icon * refactor: edit align row * refactor: by value table document * refactor: by value row table * feat: add independent ocr dialog * chore: clean up * refactor: accept more props and small adjustment * fix: error withDefault call * feat: accept default metadata when open * fix: typo * feat: add override hook when finish ocr * feat: reset state on open * feat: detect reader result is actually string * fix: variable name conflict * feat: properties to input component * feat: properties input in properties expansion * feat: properties expansion data (temporary) * refactor: add i18n status work * refactor: edit type work status and add step status * refactor: add edit status work * refactor: edit step work * refactor: properties data type * refactor: filter selected product & specific properties * feat: add emit event * refactor: change variable name for better understanding * refactor: hide step that no properties * refactor: work status type to validate * feat: work status color * refactor: key for filename * refactor: close expansion when change step * refactor: responsive meta data * refactor: product expansion responsive * fix: dark mode step text color * fix: document expansion table no data label * refactor: main page body bordered and overflow hidden * refactor: use utils function instead * refactor: add process * refactor: by value name * refactor: add upload file * refactor: upload file * refactor: by value * fix: option worker type * refactor: fetchRequestAttachment after edit * fix: metadata display * refactor: add class full-height * refactor: edit type * refactor: fetch file * refactor: by value visa * refactor: request list attributes type * fix: properties to input props (placeholder, readonly, disable) * feat: request list properties function * fix: error when no workflow * docs: update comment to fix indent * refactor: step type (attributes) * refactor: add attributes payload on editStatusRequestWork function * feat/refactor: functional form expansion/filter worklist * refactor: set attributes properties after submit * refactor: add request work ready status * feat: request list => form employee component * feat/refactor: form expansion select user/layout * fix: properties readonly --------- Co-authored-by: puriphatt <puriphat@frappet.com> Co-authored-by: Thanaphon Frappet <thanaphon@frappet.com>
This commit is contained in:
parent
9105dcf7fe
commit
972f6ba13e
36 changed files with 3653 additions and 57 deletions
576
src/pages/08_request-list/RequestListView.vue
Normal file
576
src/pages/08_request-list/RequestListView.vue
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
<script setup lang="ts">
|
||||
// NOTE: Library
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
// NOTE: Components
|
||||
import DataDisplay from 'src/components/08_request-list/DataDisplay.vue';
|
||||
import DocumentExpansion from './DocumentExpansion.vue';
|
||||
import FormExpansion from './FormExpansion.vue';
|
||||
import PropertiesExpansion from './PropertiesExpansion.vue';
|
||||
|
||||
// NOTE: Store
|
||||
import { dateFormatJS } from 'src/utils/datetime';
|
||||
import { useRequestList } from 'src/stores/request-list';
|
||||
import {
|
||||
RequestData,
|
||||
RequestWork,
|
||||
Attributes,
|
||||
DocStatus,
|
||||
Step,
|
||||
} from 'src/stores/request-list/types';
|
||||
import useOptionStore from 'src/stores/options';
|
||||
import ProductExpansion from './ProductExpansion.vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useWorkflowTemplate } from 'src/stores/workflow-template';
|
||||
import { WorkflowTemplate } from 'src/stores/workflow-template/types';
|
||||
import { initLang, initTheme, Lang } from 'src/utils/ui';
|
||||
import {
|
||||
EmployeePassportPayload,
|
||||
EmployeeVisaPayload,
|
||||
} from 'stores/employee/types';
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
// NOTE: Variable
|
||||
const route = useRoute();
|
||||
const optionStore = useOptionStore();
|
||||
const requestListStore = useRequestList();
|
||||
const flowTemplateStore = useWorkflowTemplate();
|
||||
|
||||
const workList = ref<RequestWork[]>();
|
||||
const statusFile = ref<Attributes>({
|
||||
customer: {},
|
||||
employee: {},
|
||||
});
|
||||
|
||||
const refDocumentExpansion = ref<InstanceType<typeof DocumentExpansion>[]>([]);
|
||||
const data = ref<RequestData>();
|
||||
const flow = ref<WorkflowTemplate>();
|
||||
const pageState = reactive({
|
||||
hideMetaData: false,
|
||||
currentStep: 1,
|
||||
});
|
||||
|
||||
// NOTE: Function
|
||||
|
||||
async function fetchRequestWorkList(opts: { requestDataId: string }) {
|
||||
const res = await requestListStore.getRequestWorkList({
|
||||
requestDataId: opts.requestDataId,
|
||||
pageSize: 9999,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
workList.value = res.result;
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomerName(
|
||||
record: RequestData,
|
||||
opts?: {
|
||||
locale?: string;
|
||||
noCode?: boolean;
|
||||
},
|
||||
) {
|
||||
const customer = record.quotation.customerBranch;
|
||||
|
||||
return (
|
||||
{
|
||||
['CORP']: {
|
||||
[Lang.English]: customer.registerNameEN,
|
||||
[Lang.Thai]: customer.registerName,
|
||||
}[opts?.locale || 'eng'],
|
||||
['PERS']:
|
||||
{
|
||||
[Lang.English]: `${optionStore.mapOption(customer.namePrefix)} ${customer.firstNameEN} ${customer.lastNameEN}`,
|
||||
[Lang.Thai]: `${optionStore.mapOption(customer.namePrefix)} ${customer.firstName} ${customer.lastName}`,
|
||||
}[opts?.locale || Lang.English] || '-',
|
||||
}[customer.customer.customerType] +
|
||||
(opts?.noCode ? '' : ' ' + `(${customer.code})`)
|
||||
);
|
||||
}
|
||||
|
||||
function getEmployeeName(
|
||||
record: RequestData,
|
||||
opts?: {
|
||||
locale?: string;
|
||||
},
|
||||
) {
|
||||
const employee = record.employee;
|
||||
|
||||
return (
|
||||
{
|
||||
[Lang.English]: `${optionStore.mapOption(employee.namePrefix)} ${employee.firstNameEN} ${employee.lastNameEN}`,
|
||||
[Lang.Thai]: `${optionStore.mapOption(employee.namePrefix)} ${employee.firstName} ${employee.lastName}`,
|
||||
}[opts?.locale || Lang.English] || '-'
|
||||
);
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
const current = route.params['requestListId'];
|
||||
|
||||
if (typeof current === 'string') {
|
||||
const res = await requestListStore.getRequestData(current);
|
||||
|
||||
if (res) {
|
||||
data.value = res;
|
||||
await fetchRequestWorkList({ requestDataId: current });
|
||||
await getFlow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getFlow() {
|
||||
if (!workList.value) return;
|
||||
|
||||
const attr = workList.value.find((v) => !!v.productService.work?.attributes)
|
||||
?.productService.work?.attributes;
|
||||
|
||||
if (attr && Object.hasOwn(attr, 'workflowId')) {
|
||||
const workflowId = attr['workflowId'];
|
||||
|
||||
const res = await flowTemplateStore.getWorkflowTemplate(workflowId);
|
||||
|
||||
if (res) flow.value = res;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
initTheme();
|
||||
initLang();
|
||||
|
||||
// get data
|
||||
await getData();
|
||||
});
|
||||
|
||||
watch(() => route.params['requestListId'], getData);
|
||||
|
||||
async function triggerChangeStatusWork(step: Step) {
|
||||
const res = await requestListStore.editStatusRequestWork(step);
|
||||
if (res) {
|
||||
const indexWork = workList.value?.findIndex(
|
||||
(v) => v.id === step.requestWorkId,
|
||||
);
|
||||
if (indexWork === -1 || indexWork === undefined) return;
|
||||
if (workList.value === undefined) return;
|
||||
|
||||
const indexStep = workList.value[indexWork].stepStatus.findIndex(
|
||||
(v) => v.step === step.step,
|
||||
);
|
||||
|
||||
if (indexStep === -1) {
|
||||
workList.value[indexWork].stepStatus.push(res);
|
||||
}
|
||||
if (indexStep !== -1) {
|
||||
workList.value[indexWork].stepStatus[indexStep].workStatus =
|
||||
res.workStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerChangeStatusFile(opt: {
|
||||
index: number;
|
||||
id: string;
|
||||
documentType: string;
|
||||
status: DocStatus;
|
||||
type: 'customer' | 'employee';
|
||||
}) {
|
||||
if (!workList.value) return;
|
||||
|
||||
const workItem = workList.value[opt.index];
|
||||
if (!workItem || !workItem.attributes) {
|
||||
if (workItem) {
|
||||
workItem.attributes = { customer: {}, employee: {} };
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const attributes = workItem.attributes;
|
||||
|
||||
statusFile.value = attributes;
|
||||
if (!statusFile.value[opt.type]) {
|
||||
statusFile.value[opt.type] = {};
|
||||
}
|
||||
|
||||
statusFile.value[opt.type]![opt.documentType] = opt.status;
|
||||
|
||||
const res = await requestListStore.editRequestWork({
|
||||
id: opt.id,
|
||||
attributes: statusFile.value,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
workList.value[opt.index].attributes = res.attributes;
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerUpload(opt: {
|
||||
id: string;
|
||||
type: 'customer' | 'employee';
|
||||
group: string;
|
||||
file: File;
|
||||
form?: EmployeePassportPayload | EmployeeVisaPayload;
|
||||
}) {
|
||||
const newName = `${opt.group}-${Date.now()}-${opt.file.name}`;
|
||||
|
||||
const res = await requestListStore.uploadAttachmentRequest({
|
||||
...opt,
|
||||
name: newName,
|
||||
});
|
||||
|
||||
return !!res;
|
||||
}
|
||||
|
||||
async function triggerViewFile(opt: {
|
||||
id: string;
|
||||
fileName: string;
|
||||
type: 'customer' | 'employee';
|
||||
group: string;
|
||||
download?: boolean;
|
||||
}) {
|
||||
let url;
|
||||
url = await requestListStore.viewAttachmentRequest({
|
||||
id: opt.id,
|
||||
name: opt.fileName,
|
||||
type: opt.type,
|
||||
group: opt.group,
|
||||
download: opt.download,
|
||||
});
|
||||
|
||||
if (!opt.download) window.open(url, '_blank');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="column surface-0 fullscreen" v-if="data">
|
||||
<!-- SEC: Header -->
|
||||
<header class="row q-px-md q-py-sm items-center full justify-between">
|
||||
<div style="flex: 1" class="row items-center">
|
||||
<RouterLink to="/quotation">
|
||||
<q-img src="/icons/favicon-512x512.png" width="3rem" />
|
||||
</RouterLink>
|
||||
<span class="column text-h6 text-bold q-ml-md">
|
||||
{{ $t('requestList.title') }}
|
||||
{{ data.code || '' }}
|
||||
<span class="text-caption text-regular app-text-muted">
|
||||
{{
|
||||
$t('quotation.processOn', {
|
||||
msg: dateFormatJS({ date: data.createdAt }),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- SEC: Body -->
|
||||
<main class="col full-width q-pa-md" style="flex-grow: 1; overflow-y: auto">
|
||||
<section class="col-sm col-12">
|
||||
<div class="col q-gutter-y-md" :key="pageState.currentStep">
|
||||
<!-- step -->
|
||||
<nav
|
||||
v-if="flow"
|
||||
class="surface-1 q-pa-sm row no-wrap full-width scroll rounded"
|
||||
style="gap: 10px"
|
||||
>
|
||||
<template v-for="(value, i) in flow.step" :key="value.id">
|
||||
<button
|
||||
v-if="
|
||||
workList?.every(
|
||||
(v) =>
|
||||
v.productService.work?.attributes.workflowStep[i]
|
||||
.attributes.properties.length > 0,
|
||||
)
|
||||
"
|
||||
class="status-color q-pa-sm bordered row items-center cursor-pointer no-wrap"
|
||||
style="text-wrap: nowrap"
|
||||
:class="{
|
||||
[`status-color-${'doing'}`]: true,
|
||||
'step-status-active': pageState.currentStep === value.order,
|
||||
}"
|
||||
@click="() => (pageState.currentStep = value.order)"
|
||||
>
|
||||
<!-- 'quotation-status-active': value.active?.(), -->
|
||||
<!-- @click="'waiting' !== 'waiting' && value.handler()" -->
|
||||
<div class="q-px-sm">
|
||||
<q-icon
|
||||
class="icon-color quotation-status"
|
||||
style="border-radius: 50%"
|
||||
:name="`${pageState.currentStep === value.order ? 'mdi-circle-slice-8' : 'mdi-checkbox-blank-circle-outline'}`"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-left">{{ value.name }}</div>
|
||||
</button>
|
||||
</template>
|
||||
</nav>
|
||||
|
||||
<!-- meta data -->
|
||||
<article class="surface-1 rounded q-pa-sm">
|
||||
<div
|
||||
class="text-weight-bold row items-center no-wrap"
|
||||
style="gap: 16px"
|
||||
>
|
||||
<q-img src="/images/quotation-avatar.png" width="42px" />
|
||||
<span class="ellipsis" style="font-size: 18px">
|
||||
{{ data.quotation.workName || '-' }}
|
||||
</span>
|
||||
<q-btn
|
||||
class="q-ml-sm"
|
||||
icon="mdi-pin-outline"
|
||||
color="primary"
|
||||
size="sm"
|
||||
flat
|
||||
dense
|
||||
rounded
|
||||
@click="pageState.hideMetaData = !pageState.hideMetaData"
|
||||
:style="pageState.hideMetaData ? 'rotate: 90deg' : ''"
|
||||
style="transition: 0.1s ease-in-out"
|
||||
/>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
<section
|
||||
v-if="!pageState.hideMetaData"
|
||||
class="q-pt-md"
|
||||
:class="{ row: $q.screen.gt.sm, column: $q.screen.lt.md }"
|
||||
>
|
||||
<div
|
||||
class="col q-gutter-y-md"
|
||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
||||
>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-file-document-outline"
|
||||
:label="$t('requestList.requestListCode')"
|
||||
:value="data.quotation.code || '-'"
|
||||
/>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-account-settings-outline"
|
||||
:label="$t('customer.employee')"
|
||||
:value="
|
||||
getEmployeeName(data, { locale: $i18n.locale }) || '-'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="col q-gutter-y-md"
|
||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
||||
>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-file-document-outline"
|
||||
:label="$t('requestList.quotationCode')"
|
||||
:value="data.code || '-'"
|
||||
/>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-account-settings-outline"
|
||||
:label="$t('customer.employer')"
|
||||
:value="
|
||||
getCustomerName(data, { locale: locale, noCode: true }) ||
|
||||
'-'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="col q-gutter-y-md"
|
||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
||||
>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-file-document-outline"
|
||||
:label="$t('requestList.invoiceCode')"
|
||||
:value="'-'"
|
||||
/>
|
||||
<span class="col"></span>
|
||||
</div>
|
||||
<div
|
||||
class="col q-gutter-y-md"
|
||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
||||
>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-file-document-outline"
|
||||
:label="$t('requestList.receiptCode')"
|
||||
:value="'-'"
|
||||
/>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-passport"
|
||||
:label="$t('customerEmployee.form.passportNo')"
|
||||
:value="'-'"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="col q-gutter-y-md"
|
||||
:class="{ column: $q.screen.gt.sm, row: $q.screen.lt.md }"
|
||||
>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-card-account-details-outline"
|
||||
:label="$t('requestList.alienIdCard')"
|
||||
:value="data.employee.nrcNo"
|
||||
/>
|
||||
<DataDisplay
|
||||
class="col"
|
||||
icon="mdi-account-settings-outline"
|
||||
:label="$t('flow.responsiblePerson')"
|
||||
:value="'-'"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</transition>
|
||||
</article>
|
||||
<!-- product -->
|
||||
<template
|
||||
v-for="(value, index) in workList?.filter((v) =>
|
||||
v.productService.work?.attributes.workflowStep[
|
||||
pageState.currentStep - 1
|
||||
].productsId.includes(v.productService.productId),
|
||||
)"
|
||||
:key="value"
|
||||
>
|
||||
<ProductExpansion
|
||||
:status="
|
||||
value.stepStatus.find((v) => v.step === pageState.currentStep)
|
||||
"
|
||||
v-model:product-id="value.productService.productId"
|
||||
:name="value.productService.product.name"
|
||||
:code="value.productService.product.code"
|
||||
:product="value.productService.product"
|
||||
@change-status="
|
||||
(v) => {
|
||||
triggerChangeStatusWork({
|
||||
workStatus: v.requestWorkStatus,
|
||||
step:
|
||||
v.step === undefined
|
||||
? pageState.currentStep
|
||||
: v.step.step,
|
||||
requestWorkId: value.id || '',
|
||||
});
|
||||
}
|
||||
"
|
||||
>
|
||||
<template v-slot="{ product }">
|
||||
<section class="column surface-1 q-pa-sm bordered-t">
|
||||
<DocumentExpansion
|
||||
ref="refDocumentExpansion"
|
||||
:attributes="value.attributes"
|
||||
@change-status="
|
||||
(opt) => {
|
||||
triggerChangeStatusFile({
|
||||
index,
|
||||
id: value.id || '',
|
||||
documentType: opt.key || '',
|
||||
status: opt.status,
|
||||
type: opt.type || 'customer',
|
||||
});
|
||||
}
|
||||
"
|
||||
@view-doc="
|
||||
(opt) => {
|
||||
triggerViewFile({
|
||||
id: opt.id,
|
||||
fileName: opt.data.fileName,
|
||||
type: opt.type,
|
||||
group: opt.data.documentType,
|
||||
});
|
||||
}
|
||||
"
|
||||
@upload="
|
||||
async (opt, done) => {
|
||||
await triggerUpload({ ...opt });
|
||||
await done(opt.type || 'customer');
|
||||
}
|
||||
"
|
||||
@download="
|
||||
(opt) => {
|
||||
console.log(opt);
|
||||
|
||||
triggerViewFile({
|
||||
id: opt.id,
|
||||
fileName: opt.data.fileName,
|
||||
type: opt.type,
|
||||
group: opt.data.documentType,
|
||||
download: true,
|
||||
});
|
||||
}
|
||||
"
|
||||
:current-id="{
|
||||
customer: value.request.quotation.customerBranchId,
|
||||
employee: value.request.employeeId,
|
||||
}"
|
||||
:listDocument="product?.document"
|
||||
/>
|
||||
<FormExpansion
|
||||
:step="{
|
||||
step: pageState.currentStep,
|
||||
requestWorkId: value.id || '',
|
||||
}"
|
||||
:id="value.id"
|
||||
:attributes-form="
|
||||
value.stepStatus[pageState.currentStep - 1]?.attributes
|
||||
?.form
|
||||
"
|
||||
/>
|
||||
<PropertiesExpansion
|
||||
:id="value.id"
|
||||
:properties-to-show="
|
||||
value.productService.work?.attributes.workflowStep[
|
||||
pageState.currentStep - 1
|
||||
].attributes.properties
|
||||
"
|
||||
:attributes="value.attributes"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</ProductExpansion>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.status-color {
|
||||
--_color: var(--gray-0);
|
||||
border-color: hsla(var(--_color));
|
||||
background: hsla(var(--_color) / 0.05);
|
||||
border-radius: 4px;
|
||||
|
||||
.icon-color {
|
||||
color: hsla(var(--_color));
|
||||
}
|
||||
|
||||
&.status-color-done {
|
||||
--_color: var(--green-5-hsl);
|
||||
}
|
||||
&.status-color-doing {
|
||||
--_color: var(--blue-5-hsl);
|
||||
color: var(--foreground);
|
||||
}
|
||||
&.status-color-waiting {
|
||||
--_color: var(--gray-4-hsl);
|
||||
color: hsla(var(--_color));
|
||||
}
|
||||
}
|
||||
|
||||
.step-status-active {
|
||||
opacity: 1;
|
||||
font-weight: 600;
|
||||
transition: 1s box-shadow ease-in-out;
|
||||
animation: status 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes status {
|
||||
0% {
|
||||
box-shadow: 0x 0px 0px hsla(var(--_color) / 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0px 0px 1px 4px hsla(var(--_color) / 0.2);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0px 0px 4px 7px hsla(var(--_color) / 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue