Create order page

This commit is contained in:
Kevin Martinez 2023-12-11 10:47:16 -04:00
parent f9fe504969
commit 9a477c2c25
17 changed files with 1186 additions and 5 deletions

View File

@ -55,6 +55,10 @@ const $props = defineProps({
description: description:
'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)', 'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)',
}, },
mapper: {
type: Function,
default: null,
}
}); });
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
@ -83,6 +87,7 @@ onUnmounted(() => {
const isLoading = ref(false); const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges); const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({...$props.formInitialData}); const originalData = ref({...$props.formInitialData});
const formData = computed(() => state.get($props.model)); const formData = computed(() => state.get($props.model));
@ -92,7 +97,10 @@ const startFormWatcher = () => {
watch( watch(
() => formData.value, () => formData.value,
(val) => { (val) => {
if (val) hasChanges.value = true; if (!isResetting.value && val) {
hasChanges.value = true;
}
isResetting.value = false;
}, },
{ deep: true } { deep: true }
); );
@ -121,11 +129,12 @@ async function save() {
isLoading.value = true; isLoading.value = true;
try { try {
const body = $props.mapper ? $props.mapper(formData.value) : formData.value
if ($props.urlCreate) { if ($props.urlCreate) {
await axios.post($props.urlCreate, formData.value); await axios.post($props.urlCreate, body);
notify('globals.dataCreated', 'positive'); notify('globals.dataCreated', 'positive');
} else { } else {
await axios.patch($props.urlUpdate || $props.url, formData.value); await axios.patch($props.urlUpdate || $props.url, body);
} }
} catch (err) { } catch (err) {
notify('errors.create', 'negative'); notify('errors.create', 'negative');
@ -143,6 +152,7 @@ function reset() {
emit('onFetch', state.get($props.model)); emit('onFetch', state.get($props.model));
if ($props.observeFormChanges) { if ($props.observeFormChanges) {
hasChanges.value = false; hasChanges.value = false;
isResetting.value = true;
} }
} }

View File

@ -31,7 +31,7 @@ const props = defineProps({
default: null, default: null,
}, },
order: { order: {
type: String, type: [String, Array],
default: '', default: '',
}, },
limit: { limit: {
@ -149,7 +149,7 @@ async function onLoad(...params) {
v-if="props.skeleton && props.autoLoad && !store.data" v-if="props.skeleton && props.autoLoad && !store.data"
class="card-list q-gutter-y-md" class="card-list q-gutter-y-md"
> >
<QCard class="card" v-for="$index in $props.limit" :key="$index"> <QCard class="card" v-for="$index in props.limit" :key="$index">
<QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable"> <QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<QItemSection class="q-pa-md"> <QItemSection class="q-pa-md">
<QSkeleton type="rect" class="q-mb-md" square /> <QSkeleton type="rect" class="q-mb-md" square />

View File

@ -473,6 +473,55 @@ export default {
recyclable: 'Recyclable', recyclable: 'Recyclable',
}, },
}, },
order: {
pageTitles: {
order: 'Orders',
orderList: 'List',
create: 'Create',
summary: 'Summary',
basicData: 'Basic Data',
},
field: {
salesPersonFk: 'Sales Person',
clientFk: 'Client',
isConfirmed: 'Confirmed',
created: 'Created',
landed: 'Landed',
hour: 'Hour',
agency: 'Agency',
total: 'Total'
},
form: {
clientFk: 'Client',
addressFk: 'Address',
landed: 'Landed',
agencyModeFk: 'Agency',
},
list: {
newOrder: 'New Order'
},
summary: {
basket: 'Basket',
nickname: 'Nickname',
company: 'Company',
confirmed: 'Confirmed',
notConfirmed: 'Not confirmed',
created: 'Created',
landed: 'Landed',
phone: 'Phone',
createdFrom: 'Created From',
address: 'Address',
notes: 'Notes',
subtotal: 'Subtotal',
total: 'Total',
vat: 'VAT',
state: 'State',
alias: 'Alias',
items: 'Items',
orderTicketList: 'Order Ticket List',
details: 'Details',
}
},
worker: { worker: {
pageTitles: { pageTitles: {
workers: 'Workers', workers: 'Workers',

View File

@ -0,0 +1,12 @@
<script setup>
import OrderForm from 'pages/Order/Card/OrderForm.vue';
</script>
<template>
<QToolbar>
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<OrderForm />
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,36 @@
<script setup>
import LeftMenu from 'components/LeftMenu.vue';
import { useStateStore } from 'stores/useStateStore';
import OrderSearchbar from 'pages/Order/Card/OrderSearchbar.vue';
import OrderDescriptor from "pages/Order/Card/OrderDescriptor.vue";
const stateStore = useStateStore();
</script>
<template>
<Teleport to="#searchbar" v-if="stateStore.isHeaderMounted()">
<OrderSearchbar />
</Teleport>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit">
<OrderDescriptor />
<QSeparator />
<LeftMenu source="card" />
</QScrollArea>
</QDrawer>
<QPageContainer>
<QPage>
<div class="q-pa-md">
<RouterView></RouterView>
</div>
</QPage>
</QPageContainer>
</template>
<i18n>
es:
Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
Details: Detalles
Notes: Notas
Action: Acción
</i18n>

View File

@ -0,0 +1,128 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { toCurrency, toDate } from 'src/filters';
import { useState } from 'src/composables/useState';
import useCardDescription from 'src/composables/useCardDescription';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import OrderDescriptorMenu from "pages/Order/Card/OrderDescriptorMenu.vue";
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const state = useState();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const filter = {
include: [
{ relation: 'agencyMode', scope: { fields: ['name'] } },
{
relation: 'address',
scope: { fields: ['nickname'] },
},
{ relation: 'rows', scope: { fields: ['id'] } },
{
relation: 'client',
scope: {
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked',
],
include: {
relation: 'salesPersonUser',
scope: { fields: ['id', 'name'] },
},
},
},
],
};
const data = ref(useCardDescription());
const setData = (entity) => {
if (!entity) return;
data.value = useCardDescription(entity.client.name, entity.id);
state.set('ClaimDescriptor', entity);
};
</script>
<template>
<CardDescriptor
ref="descriptor"
:url="`Orders/${entityId}`"
:filter="filter"
module="Order"
:title="data.title"
:subtitle="data.subtitle"
@on-fetch="setData"
data-key="orderData"
>
<template #menu="{ entity }">
<OrderDescriptorMenu :order="entity" />
</template>
<template #body="{ entity }">
<VnLv
:label="t('order.summary.state')"
:value="
t(
entity.isConfirmed
? 'order.summary.confirmed'
: 'order.summary.notConfirmed'
)
"
/>
<VnLv :label="t('order.field.salesPersonFk')">
<template #value>
<span class="link">
{{ entity?.client?.salesPersonUser?.name || '-' }}
<WorkerDescriptorProxy :id="entity?.client?.salesPersonFk" />
</span>
</template>
</VnLv>
<VnLv :label="t('order.summary.landed')" :value="toDate(entity?.landed)" />
<VnLv :label="t('order.field.agency')" :value="entity?.agencyMode?.name" />
<VnLv :label="t('order.summary.alias')" :value="entity?.address?.nickname" />
<VnLv :label="t('order.summary.items')" :value="entity?.rows?.length || 0" />
<VnLv :label="t('order.summary.total')" :value="toCurrency(entity?.total)" />
</template>
<template #actions="{ entity }">
<QCardActions>
<QBtn
size="md"
icon="vn:ticket"
color="primary"
:to="{
name: 'TicketList',
query: { params: JSON.stringify({ orderFk: entity.id }) },
}"
>
<QTooltip>{{ t('order.summary.orderTicketList') }}</QTooltip>
</QBtn>
<QBtn
size="md"
icon="vn:client"
color="primary"
:to="{ name: 'CustomerCard', params: { id: entity.clientFk } }"
>
<QTooltip>{{ t('claim.card.customerSummary') }}</QTooltip>
</QBtn>
</QCardActions>
</template>
</CardDescriptor>
</template>

View File

@ -0,0 +1,65 @@
<script setup>
import axios from 'axios';
import { ref } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import VnConfirm from 'components/ui/VnConfirm.vue';
const $props = defineProps({
order: {
type: Object,
required: true,
},
});
const router = useRouter();
const quasar = useQuasar();
const { t } = useI18n();
const order = ref($props.order);
function confirmRemove() {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('confirmDeletion'),
message: t('confirmDeletionMessage'),
promise: remove,
},
})
.onOk(async () => await router.push({ name: 'OrderList' }));
}
async function remove() {
const id = order.value.id;
await axios.delete(`Orders/${id}`);
quasar.notify({
message: t('globals.dataDeleted'),
type: 'positive',
});
}
</script>
<template>
<QItem @click="confirmRemove()" v-ripple clickable>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('deleteOrder') }}</QItemSection>
</QItem>
</template>
<i18n>
{
"en": {
"deleteOrder": "Delete order",
"confirmDeletion": "Confirm deletion",
"confirmDeletionMessage": "Are you sure you want to delete this order?"
},
"es": {
"deleteOrder": "Eliminar pedido",
"confirmDeletion": "Confirmar eliminación",
"confirmDeletionMessage": "Seguro que quieres eliminar este pedido?"
}
}
</i18n>

View File

@ -0,0 +1,245 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const workers = ref();
const states = ref();
// New
const agencyFilter = { fields: ['id', 'name'] };
const agencyList = ref(null);
const salesPersonFilter = {
fields: ['id', 'nickname'],
};
const salesPersonList = ref(null);
const sourceFilter = { fields: ['value'] };
const sourceList = ref(null)
</script>
<template>
<FetchData
url="AgencyMode/isActive"
:filter="agencyFilter"
limit="30"
sort-by="name ASC"
auto-load
@on-fetch="(data) => (agencyList = data)"
/>
<FetchData
url="Workers/search"
:filter="salesPersonFilter"
limit="30"
sort-by="nickname ASC"
@on-fetch="(data) => (salesPersonList = data)"
auto-load
/>
<FetchData
url="Orders/getSourceValues"
:filter="sourceFilter"
limit="30"
sort-by="value ASC"
@on-fetch="(data) => (sourceList = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QList dense>
<QItem>
<QItemSection>
<QInput
:label="t('Customer ID')"
v-model="params.clientFk"
lazy-rules
>
<template #prepend>
<QIcon name="badge" size="sm"></QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QInput
:label="t('Client Name')"
v-model="params.clientName"
lazy-rules
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelectFilter
:label="t('Salesperson')"
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelectFilter
:label="t('Attender')"
v-model="params.attenderFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelectFilter
:label="t('Responsible')"
v-model="params.claimResponsibleFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem class="q-mb-md">
<QItemSection v-if="!states">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="states">
<VnSelectFilter
:label="t('State')"
v-model="params.claimStateFk"
@update:model-value="searchFn()"
:options="states"
option-value="id"
option-label="description"
emit-value
map-options
/>
</QItemSection>
</QItem>
<QSeparator />
<QExpansionItem :label="t('More options')" expand-separator>
<!-- <QItem>
<QItemSection>
<qSelect
:label="t('Item')"
v-model="params.itemFk"
:options="items"
:loading="loading"
@filter="filterFn"
@virtual-scroll="onScroll"
option-value="id"
option-label="name"
emit-value
map-options
/>
</QItemSection>
</QItem> -->
<QItem>
<QItemSection>
<QInput
v-model="params.created"
:label="t('Created')"
autofocus
readonly
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="params.created">
<div class="row items-center justify-end">
<QBtn
v-close-popup
:label="t('globals.close')"
color="primary"
flat
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItemSection>
</QItem>
</QExpansionItem>
</QList>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: Contains
clientFk: Customer
clientName: Customer
salesPersonFk: Salesperson
attenderFk: Attender
claimResponsibleFk: Responsible
claimStateFk: State
created: Created
es:
params:
search: Contiene
clientFk: Cliente
clientName: Cliente
salesPersonFk: Comercial
attenderFk: Asistente
claimResponsibleFk: Responsable
claimStateFk: Estado
created: Creada
Customer ID: ID cliente
Client Name: Nombre del cliente
Salesperson: Comercial
Attender: Asistente
Responsible: Responsable
State: Estado
Item: Artículo
Created: Creada
More options: Más opciones
</i18n>

View File

@ -0,0 +1,209 @@
<script setup>
import { useRoute } from 'vue-router';
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useState } from 'composables/useState';
import FormModel from 'components/FormModel.vue';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
const { t } = useI18n();
const route = useRoute();
const state = useState();
const ORDER_MODEL = 'order';
const isNew = Boolean(!route.params.id);
const initialFormState = reactive({
clientFk: null,
addressFk: null,
agencyModeFk: null,
landed: null,
});
const clientList = ref([]);
const agencyList = ref([]);
const addressList = ref([]);
const fetchAddressList = async (addressId) => {
try {
const { data } = await axios.get('addresses', {
params: {
filter: JSON.stringify({
fields: ['id', 'nickname', 'street', 'city'],
where: { id: addressId },
}),
},
});
addressList.value = data;
// Set address by default
if (addressList.value?.length === 1) {
state.get(ORDER_MODEL).addressFk = addressList.value[0].id;
}
} catch (err) {
console.error(`Error fetching addresses`, err);
return err.response;
}
};
const fetchAgencyList = async (landed, addressFk) => {
if (!landed || !addressFk) {
return;
}
try {
const { data } = await axios.get('Agencies/landsThatDay', {
params: {
addressFk,
landed: new Date(landed).toISOString(),
},
});
agencyList.value = data;
} catch (err) {
console.error(`Error fetching agencies`, err);
return err.response;
}
};
const fetchOrderDetails = (order) => {
fetchAddressList(order?.addressFk)
fetchAgencyList(order?.landed, order?.addressFk)
};
const orderMapper = (order) => {
return {
addressId: order.addressFk,
agencyModeId: order.agencyModeFk,
landed: new Date(order.landed).toISOString(),
};
};
const orderFilter = {
include: [
{ relation: 'agencyMode', scope: { fields: ['name'] } },
{
relation: 'address',
scope: { fields: ['nickname'] },
},
{ relation: 'rows', scope: { fields: ['id'] } },
{
relation: 'client',
scope: {
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked',
],
include: {
relation: 'salesPersonUser',
scope: { fields: ['id', 'name'] },
},
},
},
],
};
</script>
<template>
<QToolbar>
<div id="st-data"></div>
<QSpace />
<div id="st-actions"></div>
</QToolbar>
<FetchData
url="Clients"
@on-fetch="(data) => (clientList = data)"
:filter="{ fields: ['id', 'name', 'defaultAddressFk'] }"
auto-load
/>
<FormModel
:url="!isNew ? `Orders/${route.params.id}` : null"
:url-create="isNew ? 'Orders/new' : null"
:model="ORDER_MODEL"
:form-initial-data="isNew ? initialFormState : null"
:observe-form-changes="!isNew"
:mapper="isNew ? orderMapper : null"
:filter="orderFilter"
@on-fetch="fetchOrderDetails"
>
<template #form="{ data }">
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('order.form.clientFk')"
v-model="data.clientFk"
:options="clientList"
option-value="id"
option-label="name"
hide-selected
@update:model-value="
(client) => fetchAddressList(client.defaultAddressFk)
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
<div class="col">
<VnSelectFilter
:label="t('order.form.addressFk')"
v-model="data.addressFk"
:options="addressList"
option-value="id"
option-label="nickname"
hide-selected
:disable="!addressList?.length"
@update:model-value="
() => fetchAgencyList(data.landed, data.addressFk)
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{
`${scope.opt.nickname}: ${scope.opt.street},${scope.opt.city}`
}}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnInputDate
placeholder="dd-mm-aaa"
:label="t('order.form.landed')"
v-model="data.landed"
@update:model-value="
() => fetchAgencyList(data.landed, data.addressFk)
"
/>
</div>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelectFilter
:label="t('order.form.agencyModeFk')"
v-model="data.agencyModeFk"
:options="agencyList"
option-value="agencyModeFk"
option-label="agencyMode"
hide-selected
:disable="!agencyList?.length"
>
</VnSelectFilter>
</div>
</VnRow>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,25 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
const { t } = useI18n();
</script>
<template>
<VnSearchbar
data-key="OrderList"
url="Orders/filter"
:label="t('search-order')"
:info="t('search-order-info')"
/>
</template>
<style scoped lang="scss"></style>
<i18n>
en:
search-order: Search order
search-order-info: You can search orders by reference
es:
Search shelving: Buscar orden
You can search by shelving reference: Puedes buscar por referencia de la orden
</i18n>

View File

@ -0,0 +1,161 @@
<script setup>
import {computed, ref} from 'vue';
import { useRoute } from 'vue-router';
import CardSummary from 'components/ui/CardSummary.vue';
import { useI18n } from 'vue-i18n';
import VnLv from 'components/ui/VnLv.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import { toCurrency, toDateHour } from 'src/filters';
const { t } = useI18n();
const route = useRoute();
const $props = defineProps({
id: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.id || route.params.id);
const detailsColumns = ref([
{
name: 'item',
label: t('claim.summary.item'),
field: (row) => row?.item?.id,
sortable: true,
},
{
name: 'description',
label: t('claim.summary.description'),
field: (row) => row?.item?.name,
},
{
name: 'quantity',
label: t('claim.summary.description'),
field: (row) => row?.quantity,
},
{
name: 'price',
label: t('claim.summary.description'),
field: (row) => toCurrency(row?.price),
},
{
name: 'amount',
label: t('claim.summary.description'),
field: (row) => toCurrency(row?.quantity * row?.price),
}
])
</script>
<template>
<CardSummary ref="summary" :url="`Orders/${entityId}/summary`">
<template #header="{ entity }">
{{ t('order.summary.basket') }} #{{ entity?.id }} -
{{ entity?.client?.name }} ({{ entity?.clientFk }})
</template>
<template #body="{ entity }">
<QCard class="vn-one">
<VnLv label="ID" :value="entity.id" />
<VnLv :label="t('order.summary.nickname')">
<template #value>
<span class="link">
{{ entity?.address?.nickname || '-' }}
<CustomerDescriptorProxy :id="entity?.clientFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('order.summary.company')"
:value="entity?.address?.companyFk"
/>
<VnLv
:label="t('order.summary.confirmed')"
:value="entity?.isConfirmed === 1"
/>
</QCard>
<QCard class="vn-one">
<VnLv
:label="t('order.summary.created')"
:value="toDateHour(entity?.created)"
/>
<VnLv
:label="t('order.summary.confirmed')"
:value="toDateHour(entity?.confirmed)"
/>
<VnLv
:label="t('order.summary.landed')"
:value="toDateHour(entity?.landed)"
/>
<VnLv :label="t('order.summary.phone')">
<template #value>
{{ entity?.address?.phone || '-' }}
<a :href="`tel:${entity?.address?.phone}`" class="text-primary">
<QIcon name="phone" />
</a>
</template>
</VnLv>
<VnLv
:label="t('order.summary.createdFrom')"
:value="entity?.sourceApp"
/>
<VnLv :label="t('order.summary.address')">
<template #value>
<span class="address">
{{
`${entity?.address?.street} - ${entity?.address?.city} (${entity?.address?.province?.name})`
}}
</span>
</template>
</VnLv>
</QCard>
<QCard class="vn-one">
<p class="header">
{{ t('order.summary.notes') }}
</p>
<p v-if="entity?.note" class="no-margin">
{{ entity?.note }}
</p>
</QCard>
<QCard class="vn-one">
<VnLv>
<template #label>
<span class="text-h6">{{ t('order.summary.subtotal') }}</span>
</template>
<template #value>
<span class="text-h6">{{ toCurrency(entity?.subTotal) }}</span>
</template>
</VnLv>
<VnLv>
<template #label>
<span class="text-h6">{{ t('order.summary.vat') }}</span>
</template>
<template #value>
<span class="text-h6">{{ toCurrency(entity?.VAT) }}</span>
</template>
</VnLv>
<VnLv>
<template #label>
<span class="text-h6">{{ t('order.summary.total') }}</span>
</template>
<template #value>
<span class="text-h6">{{ toCurrency(entity?.total) }}</span>
</template>
</VnLv>
</QCard>
<QCard>
<p class="header">
{{ t('order.summary.details') }}
</p>
<QTable :columns="detailsColumns" :rows="entity?.rows" flat hide-pagination>
</QTable>
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
.address {
white-space: normal;
}
</style>

View File

@ -0,0 +1,150 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency, toDate } from 'src/filters';
import CardList from 'components/ui/CardList.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnLv from 'components/ui/VnLv.vue';
import OrderSearchbar from 'pages/Order/Card/OrderSearchbar.vue';
const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n();
onMounted(() => (stateStore.rightDrawer = false));
// onMounted(() => (stateStore.rightDrawer = true));
// onUnmounted(() => (stateStore.rightDrawer = false));
function navigate(id) {
router.push({ path: `/order/${id}` });
}
</script>
<template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<OrderSearchbar />
</Teleport>
<!-- <Teleport to="#actions-append">-->
<!-- <div class="row q-gutter-x-sm">-->
<!-- <QBtn-->
<!-- flat-->
<!-- @click="stateStore.toggleRightDrawer()"-->
<!-- round-->
<!-- dense-->
<!-- icon="menu"-->
<!-- >-->
<!-- <QTooltip bottom anchor="bottom right">-->
<!-- {{ t('globals.collapseMenu') }}-->
<!-- </QTooltip>-->
<!-- </QBtn>-->
<!-- </div>-->
<!-- </Teleport>-->
</template>
<!-- <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>-->
<!-- <QScrollArea class="fit text-grey-8">-->
<!-- <ShelvingFilter data-key="OrderList" />-->
<!-- </QScrollArea>-->
<!-- </QDrawer>-->
<QPage class="column items-center q-pa-md">
<div class="card-list">
<VnPaginate
data-key="OrderList"
url="Orders/filter"
:limit="20"
:order="['landed DESC', 'clientFk', 'id DESC']"
:user-params="{ showEmpty: false }"
auto-load
>
<template #body="{ rows }">
<CardList
v-for="row of rows"
:key="row.id"
:id="row.id"
:title="row.code"
@click="navigate(row.id)"
>
<template #list-items>
<VnLv
:label="t('order.field.salesPersonFk')"
:title-label="t('order.field.salesPersonFk')"
>
<template #value>
<span class="link" @click.stop>
{{ row?.name || '-' }}
<WorkerDescriptorProxy :id="row?.salesPersonFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('order.field.clientFk')"
:title-label="t('order.field.clientFk')"
>
<template #value>
<span class="link" @click.stop>
{{ row?.clientName || '-' }}
<CustomerDescriptorProxy :id="row?.clientFk" />
</span>
</template>
</VnLv>
<VnLv
:label="t('order.field.isConfirmed')"
:value="row?.isConfirmed === 1"
/>
<VnLv
:label="t('order.field.created')"
:value="toDate(row?.created)"
/>
<VnLv :label="t('order.field.landed')">
<template #value>
<QBadge color="positive" dense>
{{ toDate(row?.landed) }}
</QBadge>
</template>
</VnLv>
<VnLv
:label="t('order.field.hour')"
:value="row.hourTheoretical || row.hourEffective"
/>
<VnLv
:label="t('order.field.agency')"
:value="row?.agencyName"
/>
<VnLv
:label="t('order.field.total')"
:value="toCurrency(row?.total)"
/>
</template>
<!-- <template #actions>-->
<!-- <QBtn-->
<!-- :label="t('components.smartCard.openSummary')"-->
<!-- @click.stop="viewSummary(row.id)"-->
<!-- color="primary"-->
<!-- style="margin-top: 15px"-->
<!-- />-->
<!-- </template>-->
</CardList>
</template>
</VnPaginate>
</div>
<QPageSticky :offset="[20, 20]">
<RouterLink :to="{ name: 'OrderCreate' }">
<QBtn fab icon="add" color="primary" />
<QTooltip>
{{ t('order.list.newOrder') }}
</QTooltip>
</RouterLink>
</QPageSticky>
</QPage>
</template>
<style lang="scss" scoped>
.card-list {
width: 100%;
max-width: 60em;
}
</style>

View File

@ -0,0 +1,17 @@
<script setup>
import { useStateStore } from 'stores/useStateStore';
import LeftMenu from 'src/components/LeftMenu.vue';
const stateStore = useStateStore();
</script>
<template>
<QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
<QScrollArea class="fit text-grey-8">
<LeftMenu />
</QScrollArea>
</QDrawer>
<QPageContainer>
<RouterView></RouterView>
</QPageContainer>
</template>

View File

@ -8,6 +8,7 @@ import Wagon from './wagon';
import Route from './route'; import Route from './route';
import Supplier from './Supplier'; import Supplier from './Supplier';
import Travel from './travel'; import Travel from './travel';
import Order from './order';
export default [ export default [
Customer, Customer,
@ -20,4 +21,5 @@ export default [
Route, Route,
Supplier, Supplier,
Travel, Travel,
Order,
]; ];

View File

@ -0,0 +1,69 @@
import { RouterView } from 'vue-router';
export default {
path: '/order',
name: 'Order',
meta: {
title: 'order',
icon: 'vn:basket',
},
component: RouterView,
redirect: { name: 'OrderMain' },
menus: {
main: ['OrderList'],
card: ['OrderBasicData'],
},
children: [
{
path: '',
name: 'OrderMain',
component: () => import('src/pages/Order/OrderMain.vue'),
redirect: { name: 'OrderList' },
children: [
{
path: 'list',
name: 'OrderList',
meta: {
title: 'orderList',
icon: 'view_list',
},
component: () => import('src/pages/Order/OrderList.vue'),
},
{
path: 'create',
name: 'OrderCreate',
meta: {
title: 'create',
},
component: () => import('src/pages/Order/Card/OrderForm.vue'),
},
],
},
{
name: 'OrderCard',
path: ':id',
component: () => import('src/pages/Order/Card/OrderCard.vue'),
redirect: { name: 'OrderSummary' },
children: [
{
name: 'OrderSummary',
path: 'summary',
meta: {
title: 'summary',
icon: 'launch',
},
component: () => import('src/pages/Order/Card/OrderSummary.vue'),
},
{
name: 'OrderBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
icon: 'vn:settings',
},
component: () => import('src/pages/Order/Card/OrderForm.vue'),
},
],
},
],
};

View File

@ -8,6 +8,7 @@ import supplier from './modules/Supplier';
import route from './modules/route'; import route from './modules/route';
import travel from './modules/travel'; import travel from './modules/travel';
import shelving from 'src/router/modules/shelving'; import shelving from 'src/router/modules/shelving';
import order from "src/router/modules/order";
const routes = [ const routes = [
{ {
@ -54,6 +55,7 @@ const routes = [
shelving, shelving,
invoiceOut, invoiceOut,
wagon, wagon,
order,
route, route,
supplier, supplier,
travel, travel,

View File

@ -13,6 +13,7 @@ export const useNavigationStore = defineStore('navigationStore', () => {
'invoiceOut', 'invoiceOut',
'worker', 'worker',
'shelving', 'shelving',
'order',
'wagon', 'wagon',
'route', 'route',
'supplier', 'supplier',