refactor: quotation

This commit is contained in:
puriphatt 2024-09-27 16:11:16 +07:00
parent 57b5392bfe
commit f2d9507668
3 changed files with 935 additions and 148 deletions

View file

@ -1,7 +1,11 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { onMounted, reactive, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { productTreeDecoration } from './constants';
import {
productTreeDecoration,
pageTabs,
fieldSelectedOption,
} from './constants';
import useProductServiceStore from 'src/stores/product-service';
import {
ProductGroup,
@ -29,10 +33,24 @@ import {
import { CustomerBranchCreate } from 'stores/customer/types';
import DialogForm from 'components/DialogForm.vue';
import useFlowStore from 'src/stores/flow';
import { Employee } from 'src/stores/employee/types';
import QuotationForm from './QuotationForm.vue';
import TreeView from 'src/components/shared/TreeView.vue';
import { AddButton } from 'src/components/button';
import MainButton from 'src/components/button/MainButton.vue';
import { AddButton } from 'src/components/button';
import StatCardComponent from 'src/components/StatCardComponent.vue';
import CreateButton from 'src/components/AddButton.vue';
import PaginationComponent from 'src/components/PaginationComponent.vue';
import QuotationCard from 'src/components/05_quotation/QuotationCard.vue';
import FormAbout from 'src/components/05_quotation/FormAbout.vue';
import SelectZone from 'src/components/shared/SelectZone.vue';
import PersonCard from 'src/components/shared/PersonCard.vue';
import { useRouter } from 'vue-router';
import ProductServiceForm from './ProductServiceForm.vue';
const flowStore = useFlowStore();
import {
useCustomerForm,
@ -114,6 +132,7 @@ const nodes = ref([
},
]);
const productServiceStore = useProductServiceStore();
const router = useRouter();
type ProductGroupId = string;
@ -129,14 +148,91 @@ const selectedGroup = ref<ProductGroup | null>(null);
const selectedGroupSub = ref<'product' | 'service' | null>(null);
const selectedProductServiceId = ref('');
onMounted(async () => {
const ret = await productServiceStore.fetchListProductService({
page: 1,
pageSize: 9999,
});
if (ret) productGroup.value = ret.result;
const selectedEmployee = ref<Employee[]>([]);
const pageState = reactive({
hideStat: false,
inputSearch: '',
statusFilter: 'all',
fieldSelected: [],
gridView: false,
currentTab: 'all',
addModal: false,
quotationModal: false,
employeeModal: false,
productServiceModal: false,
currentMaxPage: 1,
currentPage: 1,
pageSize: 30,
});
const statData = ref<
{
icon: string;
count: number;
label: string;
color:
| 'pink'
| 'purple'
| 'green'
| 'orange'
| 'cyan'
| 'yellow'
| 'red'
| 'magenta'
| 'blue'
| 'lime'
| 'light-purple';
}[]
>([
{
icon: 'mdi-cash',
count: 0,
label: 'quotation.type.fullAmountCash',
color: 'red',
},
{
icon: 'mdi-hand-coin-outline',
count: 0,
label: 'quotation.type.installmentsCash',
color: 'blue',
},
{
icon: 'mdi-receipt-text-outline',
count: 0,
label: 'quotation.type.fullAmountBill',
color: 'lime',
},
{
icon: 'mdi-receipt-text-send-outline',
count: 0,
label: 'quotation.type.installmentsBill',
color: 'light-purple',
},
]);
function triggerAddQuotationDialog() {
pageState.addModal = true;
// TODO: form and state controll
}
function triggerQuotationDialog() {
window.open('/quotation/add-quotation', '_blank');
// TODO: form and state controll
}
function triggerSelectEmployeeDialog() {
pageState.employeeModal = true;
// TODO: form and state controll
}
function triggerProductServiceDialog() {
pageState.productServiceModal = true;
// TODO: form and state controll
}
async function getAllProduct(
groupId: string,
opts?: { force?: false; page?: number; pageSize?: number },
@ -186,116 +282,450 @@ function convertToTree() {
// TODO: convert product or service into selectable tree
// NOTE: this is meant to be used inside getService() and getProduct() before return and after return
}
onMounted(async () => {
const ret = await productServiceStore.fetchListProductService({
page: 1,
pageSize: 9999,
});
if (ret) productGroup.value = ret.result;
});
</script>
<template>
<div v-for="item in productGroup" class="row items-center">
{{ item.name }}
{{ item.code }}
<AddButton icon-only @click="selectedGroup = item" />
</div>
<div class="column full-height no-wrap">
<!-- SEC: stat -->
<section class="text-body-2 q-mb-xs flex items-center">
{{ $t('general.dataSum') }}
<q-badge
rounded
class="q-ml-sm"
style="
background-color: hsla(var(--info-bg) / 0.15);
color: hsl(var(--info-bg));
"
>
{{ '0' }}
</q-badge>
<q-btn
class="q-ml-sm"
icon="mdi-pin-outline"
color="primary"
size="sm"
flat
dense
rounded
@click="pageState.hideStat = !pageState.hideStat"
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
style="transition: 0.1s ease-in-out"
/>
<q-btn
class="q-ml-sm"
label="add"
color="primary"
size="sm"
flat
dense
@click="triggerSelectEmployeeDialog"
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
style="transition: 0.1s ease-in-out"
/>
<q-btn
class="q-ml-sm"
label="quo"
color="primary"
size="sm"
flat
dense
@click="triggerQuotationDialog"
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
style="transition: 0.1s ease-in-out"
/>
<q-btn
class="q-ml-sm"
label="proserv"
color="primary"
size="sm"
flat
dense
@click="triggerProductServiceDialog"
:style="pageState.hideStat ? 'rotate: 90deg' : ''"
style="transition: 0.1s ease-in-out"
/>
</section>
<template v-if="selectedGroup">
<MainButton
icon="mdi-check"
color="var(--blue-6-hsl)"
@click="getAllProduct(selectedGroup.id)"
>
Product
</MainButton>
<MainButton
icon="mdi-check"
color="var(--blue-6-hsl)"
@click="getAllService(selectedGroup.id)"
>
Service
</MainButton>
<div
v-if="selectedGroupSub === 'product' && productList[selectedGroup.id]"
v-for="item in productList[selectedGroup.id]"
class="row items-center"
>
{{ item.name }}
{{ item.code }}
<AddButton icon-only @click="getProduct(item.id)" />
</div>
<div
v-if="selectedGroupSub === 'service' && serviceList[selectedGroup.id]"
v-for="item in serviceList[selectedGroup.id]"
class="row items-center"
>
{{ item.name }}
{{ item.code }}
<AddButton icon-only @click="getService(item.id)" />
</div>
<template
v-if="selectedGroupSub === 'service' && service[selectedProductServiceId]"
>
{{ service[selectedProductServiceId] }}
</template>
<template
v-if="selectedGroupSub === 'product' && product[selectedProductServiceId]"
>
{{ product[selectedProductServiceId] }}
</template>
</template>
<div class="surface-1 rounded full-height q-pa-md">
<TreeView
:decoration="productTreeDecoration"
v-model:nodes="nodes"
expandable
/>
</div>
<!-- <ImageUploadDialog
v-model:dialog-state="isOpen"
v-model:file="file"
ref="imageUploadDialog"
clear-button
>
<template #error>
<div style="display: flex; align-items: center; justify-content: center">
<h1>Fallback</h1>
<transition name="slide">
<div v-if="!pageState.hideStat" class="scroll q-mb-md">
<div style="display: inline-block">
<StatCardComponent
labelI18n
:branch="
pageState.currentTab === 'all'
? statData
: statData.filter((i) =>
i.label.split('.').pop()?.includes(pageState.currentTab),
)
"
:dark="$q.dark.isActive"
/>
</div>
</div>
</template>
</ImageUploadDialog>
</transition>
<button @click="isOpen = !isOpen">Open</button>
<button @click="imageUploadDialog?.browse()">Click Me</button>
<div
style="
height: 100vh;
background: green;
width: 100%;
color: white;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
"
id="aaa"
>
My AAA
<!-- SEC: header content -->
<header class="col surface-1 rounded">
<div class="column full-height">
<section
class="row surface-3 justify-between full-width items-center bordered-b"
style="z-index: 1"
>
<div class="row q-py-sm q-px-md justify-between full-width">
<q-input
for="input-search"
outlined
dense
:label="$t('general.search')"
class="q-mr-md col-12 col-md-3"
:bg-color="$q.dark.isActive ? 'dark' : 'white'"
v-model="pageState.inputSearch"
debounce="200"
>
<template #prepend>
<q-icon name="mdi-magnify" />
</template>
</q-input>
<div
class="row col-12 col-md-5"
:class="{ 'q-pt-xs': $q.screen.lt.md }"
style="white-space: nowrap"
>
<q-select
v-model="pageState.statusFilter"
outlined
dense
option-value="value"
option-label="label"
class="col"
:class="{ 'offset-md-5': pageState.gridView }"
map-options
emit-value
:for="'field-select-status'"
:hide-dropdown-icon="$q.screen.lt.sm"
:options="[
{ label: $t('general.all'), value: 'all' },
{ label: $t('general.active'), value: 'active' },
{ label: $t('general.inactive'), value: 'inactive' },
]"
></q-select>
<q-select
v-if="!pageState.gridView"
id="select-field"
for="select-field"
class="col q-ml-sm"
:options="
fieldSelectedOption.map((v) => ({
...v,
label: $t(v.label),
}))
"
:display-value="$t('general.displayField')"
:hide-dropdown-icon="$q.screen.lt.sm"
v-model="pageState.fieldSelected"
option-label="label"
option-value="value"
map-options
emit-value
outlined
multiple
dense
/>
<q-btn-toggle
id="btn-mode"
v-model="pageState.gridView"
dense
class="no-shadow bordered rounded surface-1 q-ml-sm"
:toggle-color="$q.dark.isActive ? 'grey-9' : 'grey-2'"
size="xs"
:options="[
{ value: true, slot: 'folder' },
{ value: false, slot: 'list' },
]"
>
<template v-slot:folder>
<q-icon
name="mdi-view-grid-outline"
size="16px"
class="q-px-sm q-py-xs rounded"
:style="{
color: $q.dark.isActive
? pageState.gridView
? '#C9D3DB '
: '#787B7C'
: pageState.gridView
? '#787B7C'
: '#C9D3DB',
}"
/>
</template>
<template v-slot:list>
<q-icon
name="mdi-format-list-bulleted"
class="q-px-sm q-py-xs rounded"
size="16px"
:style="{
color: $q.dark.isActive
? pageState.gridView === false
? '#C9D3DB'
: '#787B7C'
: pageState.gridView === false
? '#787B7C'
: '#C9D3DB',
}"
/>
</template>
</q-btn-toggle>
</div>
</div>
</section>
<nav class="surface-2 bordered-b q-px-md full-width">
<q-tabs
inline-label
mobile-arrows
dense
v-model="pageState.currentTab"
align="left"
class="full-width"
active-color="info"
>
<q-tab
v-for="tab in pageTabs"
:name="tab"
:key="tab"
@click="
async () => {
pageState.currentPage = 1;
pageState.currentTab = tab;
pageState.inputSearch = '';
pageState.statusFilter = 'all';
flowStore.rotate();
}
"
>
<div
class="row text-capitalize"
:class="
pageState.currentTab === tab ? 'text-bold' : 'app-text-muted'
"
>
{{ $t(`quotation.type.${tab}`) }}
</div>
</q-tab>
</q-tabs>
</nav>
<!-- SEC: body content -->
<body
v-if="true"
class="col surface-2 flex items-center justify-center"
>
<CreateButton
@click="triggerAddQuotationDialog"
label="general.add"
:i18n-args="{ text: $t('quotation.title') }"
/>
</body>
<body v-else class="col q-pa-md surface-2 scroll">
<div class="row q-col-gutter-md">
<div v-for="n in 5" :key="n" class="col-md-4 col-12">
<QuotationCard
:type="
pageState.currentTab !== 'all'
? pageState.currentTab
: 'fullAmountCash'
"
code="QT240120S0002"
title="ชื่อใบเสนอราคา"
date="20/01/2024 16:00:01"
:amount="2"
customer-name="เลด้า สวนลุม"
reporter="สตีเฟ่น สมัคร"
:total-price="1500"
@view="console.log('view')"
@edit="console.log('edit')"
@link="console.log('link')"
@upload="console.log('upload')"
@delete="console.log('delete')"
@change-status="console.log('change')"
/>
</div>
</div>
</body>
<!-- SEC: footer content -->
<footer
class="row justify-between items-center q-px-md q-py-sm surface-2"
v-if="pageState.currentMaxPage > 0"
>
<div class="col-4">
<div class="row items-center no-wrap">
<div class="app-text-muted q-mr-sm" v-if="$q.screen.gt.sm">
{{ $t('general.recordPerPage') }}
</div>
<div>
<q-btn-dropdown
dense
unelevated
:label="pageState.pageSize"
class="bordered q-pl-md"
>
<q-list>
<q-item
v-for="v in [10, 30, 50, 100, 500, 1000]"
:key="v"
clickable
v-close-popup
@click="
async () => {
pageState.pageSize = v;
}
"
>
<q-item-section>
<q-item-label>{{ v }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</div>
</div>
<div class="col-4 row justify-center app-text-muted">
{{
$t('general.recordsPage', {
resultcurrentPage: 0,
total: 0,
})
}}
</div>
<nav class="col-4 row justify-end">
<PaginationComponent
v-model:current-page="pageState.currentPage"
v-model:max-page="pageState.currentMaxPage"
/>
<!-- :fetch-data="async () => await fetchUserList()" -->
</nav>
</footer>
</div>
</header>
</div>
<div
style="
height: 100vh;
background: red;
width: 100%;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
"
id="my-anchor"
<!-- SEC: Dialog -->
<!-- add quotation -->
<DialogForm
:title="$t('general.add', { text: $t('quotation.title') })"
v-model:modal="pageState.addModal"
:submit-label="$t('general.add', { text: $t('quotation.title') })"
submit-icon="mdi-check"
height="auto"
width="60vw"
>
My Menu
</div> -->
<QuotationForm v-model:dialog-state="dialog" readonly />
<header class="q-mx-lg q-mt-lg">
<ProfileBanner
img="/images/quotation-bg-avatar.png"
fallback-cover="/images/quotation-banner.png"
bg-color="var(--surface-1)"
:title="$t('general.itemNo', { msg: $t('quotation.title') })"
hideActive
readonly
noImageAction
hideFade
/>
</header>
<section class="col surface-1 q-ma-lg rounded bordered row scroll">
<div
class="col-12 q-px-md q-py-lg"
id="customer-form-content"
style="height: 100%; max-height: 100%; overflow-y: auto"
>
<FormAbout prefixId="zxc" />
</div>
</section>
<template #footer>
<q-btn
style="
border-radius: var(--radius-2);
padding-block: var(--size-1);
padding-inline: var(--size-2);
"
color="negative"
outline
:label="$t('general.add', { text: $t('quotation.newCustomer') })"
/>
</template>
</DialogForm>
<!-- add employee -->
<DialogForm
:title="$t('general.select', { msg: $t('quotation.employeeList') })"
v-model:modal="pageState.employeeModal"
:submit-label="$t('general.select', { msg: $t('quotation.employee') })"
submit-icon="mdi-check"
height="75vh"
>
<section class="col row scroll">
<SelectZone
v-model:selected-item="selectedEmployee"
:items="[
{
name: 'มิเคล่า สุวรรณดี',
gender: 'female',
telephoneNo: '0621249602',
code: 'CORP000000-1-01-240001',
birthDate: '16 ปี 11 เดือน 5 วัน',
},
{
name: 'มิลิเซนต์ สุวรรณดี',
gender: 'female',
telephoneNo: '0621249666',
code: 'CORP000000-1-01-240002',
birthDate: '19 ปี 2 เดือน 2 วัน',
},
{
name: 'ไรคาร์ด พวงศรี',
gender: 'male',
telephoneNo: '0621249777',
code: 'CORP000000-1-01-240003',
birthDate: '39 ปี 5 เดือน 2 วัน',
},
]"
>
<template #top><AddButton icon-only /></template>
<template #data="{ item }">
<PersonCard
noAction
prefixId="asda"
class="full-width"
:data="{
name: item.name,
code: item.code,
female: item.gender === 'female',
male: item.gender === 'male',
img: 'images/employee-avatar.png',
detail: [
{ icon: 'mdi-phone-outline', value: item.telephoneNo },
{ icon: 'mdi-clock-outline', value: item.birthDate },
],
}"
></PersonCard>
</template>
</SelectZone>
</section>
</DialogForm>
<DialogContainer
:model-value="test"
@ -559,6 +989,10 @@ function convertToTree() {
</div>
</div>
</DialogForm>
<ProductServiceForm
v-model="pageState.productServiceModal"
></ProductServiceForm>
</template>
<style scoped></style>