<script setup> import axios from 'axios'; import { computed, ref, onBeforeMount } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useStateStore } from 'stores/useStateStore'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; import { toDate, toCurrency, dashIfEmpty } from 'src/filters/index'; import useNotify from 'src/composables/useNotify'; import TicketSummary from './Card/TicketSummary.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import VnTable from 'src/components/VnTable/VnTable.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnRow from 'src/components/ui/VnRow.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import TicketFilter from './TicketFilter.vue'; import VnInput from 'src/components/common/VnInput.vue'; import FetchData from 'src/components/FetchData.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import { toTimeFormat } from 'src/filters/date'; import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import TicketProblems from 'src/components/TicketProblems.vue'; const route = useRoute(); const router = useRouter(); const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); const quasar = useQuasar(); const { notify } = useNotify(); const clientsOptions = ref([]); const addressesOptions = ref([]); const agenciesOptions = ref([]); const selectedClient = ref(); const stateStore = useStateStore(); const from = Date.vnNew(); from.setHours(0, 0, 0, 0); from.setDate(from.getDate() - 7); const to = Date.vnNew(); to.setHours(23, 59, 0, 0); to.setDate(to.getDate() + 1); const userParams = { from: null, to: null, }; onBeforeMount(() => { initializeFromQuery(); stateStore.rightDrawer = true; if (!route.query.createForm) return; onClientSelected(JSON.parse(route.query.createForm)); }); const initializeFromQuery = () => { const query = route.query.table ? JSON.parse(route.query.table) : {}; from.value = query.from || from.toISOString(); to.value = query.to || to.toISOString(); Object.assign(userParams, { from, to }); }; const selectedRows = ref([]); const hasSelectedRows = computed(() => selectedRows.value.length > 0); const showForm = ref(false); const dialogData = ref(); const companiesOptions = ref([]); const accountingOptions = ref([]); const amountToReturn = ref(); const columns = computed(() => [ { align: 'left', name: 'statusIcons', hidden: true, format: () => '', columnClass: 'expand', }, { align: 'left', name: 'id', label: t('ticketList.id'), chip: { condition: () => true, }, isId: true, }, { align: 'left', label: t('ticketList.salesPerson'), name: 'salesPersonFk', component: 'select', attrs: { url: 'Workers/activeWithInheritedRole', fields: ['id', 'name'], where: { role: 'salesPerson' }, optionFilter: 'firstName', useLike: false, }, columnField: { component: null, }, columnClass: 'expand', format: (row, dashIfEmpty) => dashIfEmpty(row.salesPerson), }, { align: 'left', name: 'shippedDate', cardVisible: true, label: t('ticketList.shipped'), columnFilter: { component: 'date', alias: 't', inWhere: true, }, format: ({ shippedDate }) => toDate(shippedDate), }, { align: 'left', name: 'shipped', label: t('ticketList.hour'), format: (row) => toTimeFormat(row.shipped), }, { align: 'left', name: 'zoneLanding', label: t('ticketList.closure'), format: (row, dashIfEmpty) => dashIfEmpty(toTimeFormat(row.zoneLanding)), }, { align: 'left', name: 'nickname', label: t('ticketList.nickname'), columnClass: 'expand', }, { align: 'left', name: 'addressNickname', label: t('ticketList.addressNickname'), columnClass: 'expand', }, { align: 'left', name: 'province', label: t('ticketList.province'), columnClass: 'expand', }, { align: 'left', name: 'stateFk', label: t('ticketList.state'), columnFilter: { name: 'stateFk', component: 'select', attrs: { url: 'States', fields: ['id', 'name'], }, }, columnClass: 'expand', }, { align: 'left', name: 'zoneFk', label: t('ticketList.zone'), columnFilter: { component: 'select', attrs: { url: 'Zones', fields: ['id', 'name'], }, alias: 't', inWhere: true, }, columnClass: 'expand', format: (row, dashIfEmpty) => dashIfEmpty(row.zoneName), }, { align: 'left', name: 'warehouse', label: t('ticketList.warehouse'), columnClass: 'expand', }, { align: 'left', name: 'totalWithVat', label: t('ticketList.totalWithVat'), cardVisible: true, columnFilter: { component: 'number', inWhere: true, }, format: (row) => toCurrency(row.totalWithVat), }, { align: 'left', name: 'packing', label: t('ticketSale.packaging'), format: (row, dashIfEmpty) => dashIfEmpty(row.packing), }, { align: 'right', name: 'tableActions', actions: [ { title: t('ticketList.toLines'), icon: 'list', isPrimary: true, action: (row) => redirectToLines(row.id), }, { title: t('components.smartCard.viewSummary'), icon: 'preview', action: (row, evt) => { if (evt && evt.ctrlKey) { const url = router.resolve({ params: { id: row.id }, name: 'TicketCard', }).href; window.open(url, '_blank'); } else viewSummary(row.id, TicketSummary); }, }, ], }, ]); function redirectToLines(id) { const url = `#/ticket/${id}/sale`; window.open(url, '_blank'); } const onClientSelected = async (formData) => { await fetchClient(formData); await fetchAddresses(formData); }; const fetchAvailableAgencies = async (formData) => { if (!formData.warehouseId || !formData.addressId || !formData.landed) return; let params = { warehouseFk: formData.warehouseId, addressFk: formData.addressId, landed: formData.landed, }; const { data } = await axios.get('Agencies/getAgenciesWithWarehouse', { params }); agenciesOptions.value = data; const defaultAgency = agenciesOptions.value.find( (agency) => agency.agencyModeFk === selectedClient.value.defaultAddress.agencyModeFk ); if (defaultAgency) formData.agencyModeId = defaultAgency.agencyModeFk; }; const fetchClient = async (formData) => { const filter = { include: { relation: 'defaultAddress', scope: { fields: ['id', 'agencyModeFk'], }, }, where: { id: formData.clientId }, }; const params = { filter: JSON.stringify(filter) }; const { data } = await axios.get('Clients', { params }); const [client] = data; selectedClient.value = client; }; const fetchAddresses = async (formData) => { if (!formData.clientId) return; const filter = { fields: ['nickname', 'street', 'city', 'id', 'isActive'], order: ['isDefaultAddress DESC', 'isActive DESC', 'nickname ASC'], }; const params = { filter: JSON.stringify(filter) }; const { data } = await axios.get(`Clients/${formData.clientId}/addresses`, { params, }); addressesOptions.value = data; addressesOptions.value = data; const { defaultAddress } = selectedClient.value; formData.addressId = defaultAddress.id; }; const getColor = (row) => { if (row.alertLevelCode === 'OK') return 'bg-success'; else if (row.alertLevelCode === 'FREE') return 'bg-notice'; else if (row.alertLevel === 1) return 'bg-warning'; else if (row.alertLevel === 0) return 'bg-alert'; }; const getDateColor = (date) => { const today = Date.vnNew(); today.setHours(0, 0, 0, 0); const timeTicket = new Date(date); timeTicket.setHours(0, 0, 0, 0); const comparation = today - timeTicket; if (comparation == 0) return 'bg-warning'; if (comparation < 0) return 'bg-success'; }; async function makeInvoice(ticket) { const ticketsIds = ticket.map((item) => item.id); const { data } = await axios.post(`Tickets/invoiceTicketsAndPdf`, { ticketsIds }); const response = data; if (response) quasar.notify({ message: t('globals.dataSaved'), type: 'positive', }); } async function sendDocuware(ticket) { try { let ticketIds = ticket.map((item) => item.id); const { data } = await axios.post(`Docuwares/upload`, { fileCabinet: 'deliveryNote', ticketIds, }); for (let ticket of ticketIds) { ticket.stateFk = data.id; ticket.state = data.name; ticket.alertLevel = data.alertLevel; ticket.alertLevelCode = data.code; } notify('globals.dataSaved', 'positive'); } catch (err) { console.err('err: ', err); } } function openBalanceDialog(ticket) { const checkedTickets = ticket; const amountPaid = ref(0); const clientFk = ref(null); const description = ref([]); const firstTicketClientId = checkedTickets[0].clientFk; const isSameClient = checkedTickets.every( (ticket) => ticket.clientFk === firstTicketClientId ); if (!isSameClient) { throw new Error('You cannot make a payment on account from multiple clients'); } for (let ticketData of checkedTickets) { amountPaid.value += ticketData.totalWithVat; clientFk.value = ticketData.clientFk; description.value.push(ticketData.id); } const balanceCreateDialog = ref({ amountPaid: amountPaid.value, clientFk: clientFk.value, description: `Albaran: ${description.value.join(', ')}`, }); dialogData.value = balanceCreateDialog; showForm.value = true; } async function onSubmit() { const { data: email } = await axios.get('Clients', { params: { filter: JSON.stringify({ where: { id: dialogData.value.value.clientFk } }), }, }); const { data } = await axios.post( `Clients/${dialogData.value.value.clientFk}/createReceipt`, { payed: dialogData.value.payed, companyFk: dialogData.value.companyFk, bankFk: dialogData.value.bankFk, amountPaid: dialogData.value.value.amountPaid, description: dialogData.value.value.description, clientFk: dialogData.value.value.clientFk, email: email[0].email, } ); if (data) notify('globals.dataSaved', 'positive'); showForm.value = false; } const setAmountToReturn = (newAmountGiven) => { const amountPaid = dialogData.value.value.amountPaid; amountToReturn.value = newAmountGiven - amountPaid; }; function setReference(data) { let newDescription = ''; switch (data) { case 1: newDescription = `${t( 'ticketList.creditCard' )}, ${dialogData.value.value.description.replace( /^(Credit Card, |Cash, |Transfers, )/, '' )}`; break; case 2: newDescription = `${t( 'ticketList.cash' )}, ${dialogData.value.value.description.replace( /^(Credit Card, |Cash, |Transfers, )/, '' )}`; break; case 3: newDescription = `${newDescription.replace( /^(Credit Card, |Cash, |Transfers, )/, '' )}`; break; case 4: newDescription = `${t( 'ticketList.transfers' )}, ${dialogData.value.value.description.replace( /^(Credit Card, |Cash, |Transfers, )/, '' )}`; break; case 3317: newDescription = ''; break; default: break; } dialogData.value.value.description = newDescription; } </script> <template> <FetchData url="Companies" @on-fetch="(data) => (companiesOptions = data)" auto-load /> <FetchData url="Accountings" @on-fetch="(data) => (accountingOptions = data)" auto-load /> <VnSearchbar data-key="TicketList" :label="t('Search ticket')" :info="t('You can search by ticket id or alias')" data-cy="ticketListSearchBar" /> <RightMenu> <template #right-panel> <TicketFilter data-key="TicketList" /> </template> </RightMenu> <VnTable ref="tableRef" data-key="TicketList" url="Tickets/filter" :create="{ urlCreate: 'Tickets/new', title: t('ticketList.createTicket'), onDataSaved: ({ id }) => tableRef.redirect(id), formInitialData: { clientId: null }, }" default-mode="table" :order="['shippedDate DESC', 'shippedHour ASC', 'zoneLanding ASC', 'id']" :columns="columns" :user-params="userParams" :right-search="false" redirect="ticket" v-model:selected="selectedRows" :table="{ 'row-key': 'id', selection: 'multiple', }" data-cy="ticketListTable" > <template #column-statusIcons="{ row }"> <TicketProblems :row="row" /> </template> <template #column-salesPersonFk="{ row }"> <span class="link" @click.stop> {{ dashIfEmpty(row.userName) }} <CustomerDescriptorProxy :id="row.salesPersonFk" /> </span> </template> <template #column-shippedDate="{ row }"> <span v-if="getDateColor(row.shipped)"> <QChip :class="getDateColor(row.shipped)" dense square> {{ toDate(row.shippedDate) }} </QChip> </span> </template> <template #column-nickname="{ row }"> <span class="link" @click.stop> {{ row.nickname }} <CustomerDescriptorProxy :id="row.clientFk" /> </span> </template> <template #column-addressNickname="{ row }"> <span class="link" @click.stop> {{ row.addressNickname }} <CustomerDescriptorProxy :id="row.clientFk" /> </span> </template> <template #column-stateFk="{ row }"> <span v-if="row.refFk"> <span class="link" @click.stop> {{ row.refFk }} <InvoiceOutDescriptorProxy :id="row.invoiceOutId" /> </span> </span> <span v-else-if="getColor(row)"> <QChip :class="getColor(row)" dense square> {{ row.state }} </QChip> </span> <span v-else> {{ row.state }} </span> </template> <template #column-zoneFk="{ row }"> <span class="link" @click.stop> {{ dashIfEmpty(row.zoneName) }} <ZoneDescriptorProxy :id="row.zoneFk" /> </span> </template> <template #column-totalWithVat="{ row }"> <QChip v-if="row.totalWithVat > 0 && row.totalWithVat < 50" class="bg-warning" dense square > {{ row.totalWithVat }} </QChip> </template> <template #more-create-dialog="{ data }"> <VnRow> <VnSelect url="Clients" :fields="['id', 'name']" :label="t('ticketList.client')" v-model="data.clientId" :options="clientsOptions" option-value="id" option-label="name" hide-selected required @update:model-value="(client) => onClientSelected(data)" :sort-by="'id ASC'" > <template #option="scope"> <QItem v-bind="scope.itemProps"> <QItemSection> <QItemLabel> {{ scope.opt.name }} </QItemLabel> <QItemLabel caption> {{ `#${scope.opt.id}` }} </QItemLabel> </QItemSection> </QItem> </template> </VnSelect> </VnRow> <VnRow> <VnSelect :label="t('basicData.address')" v-model="data.addressId" :options="addressesOptions" option-value="id" option-label="nickname" hide-selected map-options required :disable="!data.clientId" :sort-by="'isActive DESC'" @update:model-value="() => fetchAvailableAgencies(data)" > <template #option="scope"> <QItem v-bind="scope.itemProps" :class="{ disabled: !scope.opt.isActive }" > <QItemSection style="min-width: min-content" avatar> <QIcon v-if=" scope.opt.isActive && selectedClient?.defaultAddressFk === scope.opt.id " size="sm" color="grey" name="star" class="fill-icon" /> </QItemSection> <QItemSection> <QItemLabel :class="{ 'color-vn-label': !scope.opt?.isActive, }" > {{ `${ !scope.opt?.isActive ? t('basicData.inactive') : '' } ` }} <span> {{ scope.opt?.nickname }}: {{ scope.opt?.street }}, {{ scope.opt?.city }} </span> </QItemLabel> </QItemSection> </QItem> </template> </VnSelect> </VnRow> <VnRow> <div class="col"> <VnInputDate placeholder="dd-mm-aaa" :label="t('globals.landed')" v-model="data.landed" @update:model-value="() => fetchAvailableAgencies(data)" /> </div> </VnRow> <VnRow> <div class="col"> <VnSelect url="Warehouses" :sort-by="['name']" :label="t('globals.warehouse')" v-model="data.warehouseId" :options="warehousesOptions" option-value="id" option-label="name" hide-selected required @update:model-value="() => fetchAvailableAgencies(data)" /> </div> </VnRow> <VnRow> <div class="col"> <VnSelect :label="t('globals.agency')" v-model="data.agencyModeId" :options="agenciesOptions" option-value="agencyModeFk" option-label="agencyMode" hide-selected /> </div> </VnRow> </template> </VnTable> <QPageSticky :offset="[20, 80]" style="z-index: 2"> <QBtn v-if="hasSelectedRows" @click="makeInvoice(selectedRows)" color="primary" fab icon="vn:invoice-in" /> <QTooltip> {{ t('ticketList.createInvoice') }} </QTooltip> </QPageSticky> <QPageSticky v-if="hasSelectedRows" :offset="[20, 140]" style="z-index: 2"> <QBtn @click.stop="openBalanceDialog(selectedRows)" color="primary" fab icon="vn:recovery" /> <QTooltip> {{ t('ticketList.accountPayment') }} </QTooltip> </QPageSticky> <QDialog ref="dialogRef" v-model="showForm"> <QCard class="q-pa-md q-mb-md"> <QForm @submit="onSubmit()" class="q-pa-sm"> {{ t('ticketList.addPayment') }} <VnRow> <VnInputDate :label="t('ticketList.date')" v-model="dialogData.payed" /> <VnSelect :label="t('ticketList.company')" v-model="dialogData.companyFk" :options="companiesOptions" option-value="id" option-label="code" hide-selected > </VnSelect> </VnRow> <VnRow> <VnSelect :label="t('ticketList.bank')" v-model="dialogData.bankFk" :options="accountingOptions" option-value="id" option-label="bank" hide-selected @update:model-value="setReference" /> <VnInput :label="t('ticketList.amount')" v-model="dialogData.value.amountPaid" /> </VnRow> <VnRow v-if="dialogData.bankFk === 2"> <span> {{ t('ticketList.cash') }} </span> </VnRow> <VnRow v-if="dialogData.bankFk === 2"> <VnInput :label="t('ticketList.deliveredAmount')" v-model="dialogData.value.amountGiven" @update:model-value="setAmountToReturn" type="number" /> <VnInput :label="t('ticketList.amountToReturn')" :model-value="amountToReturn" type="number" readonly /> </VnRow> <VnRow v-if="dialogData.bankFk === 3 || dialogData.bankFk === 3117"> <VnInput :label="t('ticketList.compensation')" v-model="dialogData.value.compensation" type="text" /> </VnRow> <VnRow> <VnInput :label="t('ticketList.reference')" v-model="dialogData.value.description" type="text" /> </VnRow> <VnRow v-if="dialogData.bankFk === 2"> <QCheckbox :label="t('ticketList.viewReceipt')" v-model="dialogData.value.viewReceipt" :toggle-indeterminate="false" /> <QCheckbox :label="t('ticketList.sendEmail')" v-model="dialogData.value.senEmail" :toggle-indeterminate="false" /> </VnRow> <div class="q-mt-lg row justify-end"> <QBtn :label="t('globals.save')" color="primary" @click="onSubmit()" /> <QBtn flat :label="t('globals.close')" color="primary" v-close-popup /> </div> </QForm> </QCard> </QDialog> <QPageSticky v-if="hasSelectedRows" :offset="[20, 200]" style="z-index: 2"> <QBtn @click="sendDocuware(selectedRows)" color="primary" fab icon="install_mobile" /> <QTooltip> {{ t('ticketList.sendDocuware') }} </QTooltip> </QPageSticky> </template> <style scoped> .disabled, .disabled *, [disabled], [disabled] * { cursor: pointer !important; } </style> <i18n> es: Search ticket: Buscar ticket You can search by ticket id or alias: Puedes buscar por id o alias del ticket Zone: Zona New ticket: Nuevo ticket Component lack: Faltan componentes </i18n>