Merge branch 'dev' of https: refs #7301//gitea.verdnatura.es/verdnatura/salix-front into 7301-itemLastEntries

This commit is contained in:
Pablo Natek 2024-12-12 09:03:09 +01:00
commit 52cacd8c16
217 changed files with 3746 additions and 3062 deletions

View File

@ -1,3 +1,273 @@
# Version 24.50 - 2024-12-10
### Added 🆕
- feat: add reportFileName option by:Javier Segarra
- feat: all clients just with global series by:jgallego
- feat: improve Merge branch 'test' into dev by:Javier Segarra
- feat: manual invoice in two lines by:jgallego
- feat: manualInvoice with address by:jgallego
- feat: randomize functions and example by:Javier Segarra
- feat: refs #6999 added search when user tabs on a filter with value by:Jon
- feat: refs #6999 added tab to search in VnTable filter by:Jon
- feat: refs #7346 #7346 improve form by:Javier Segarra
- feat: refs #7346 address ordered by:jgallego
- feat: refs #7346 radioButton by:jgallego
- feat: refs #7346 style radioButton by:jgallego
- feat: refs #7346 traducciones en cammelCase (7346-manualInvoice) by:jgallego
- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon
- feat: refs #8061 #8061 updates by:Javier Segarra
- feat: refs #8087 reactive data by:jorgep
- feat: refs #8087 refs#8087 Redadas en travel by:Carlos Andrés
- feat: refs #8138 add component ticket problems by:pablone
- feat: refs #8163 add max length and more tests by:wbuezas
- feat: refs #8163 add prop by:wbuezas
- feat: refs #8163 add VnInput insert functionality and e2e test by:wbuezas
- feat: refs #8163 limit with maxLength by:Javier Segarra
- feat: refs #8163 maxLength SupplierFD account by:Javier Segarra
- feat: refs #8163 maxLengthVnInput by:Javier Segarra
- feat: refs #8163 use VnAccountNumber in VnAccountNumber by:Javier Segarra
- feat: refs #8166 show notification by:jorgep
### Changed 📦
- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon
- perf: add dataCy by:Javier Segarra
- perf: refs #7346 #7346 Imrpove interface dialog by:Javier Segarra
- perf: refs #7346 #7346 use v-show instead v-if by:Javier Segarra
- perf: refs #8036 currentFilter by:alexm
- perf: refs #8061 filter autonomy by:Javier Segarra
- perf: refs #8061 solve conflicts and random posCode it by:Javier Segarra
- perf: refs #8061 use opts from VnSelect by:Javier Segarra
- perf: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra
- perf: remove console by:Javier Segarra
- perf: remove timeout by:Javier Segarra
- perf: test command fillInForm by:Javier Segarra
- refactor: refs #8162 remove comment by:wbuezas
- refactor: remove unnecesary things by:wbuezas
### Fixed 🛠️
- fix: #8016 fetching data by:Javier Segarra
- fix: icons by:jgallego
- fix: refs #7229 download file by:jorgep
- fix: refs #7229 remove catch by:jorgep
- fix: refs #7229 set url by:jorgep
- fix: refs #7229 test by:jorgep
- fix: refs #7229 url by:jorgep
- fix: refs #7229 url + test by:jorgep
- fix: refs #7304 7304 clean warning by:carlossa
- fix: refs #7304 fix list by:carlossa
- fix: refs #7304 fix warning by:carlossa
- fix: refs #7346 traslations by:jgallego
- fix: refs #7529 add save by:carlossa
- fix: refs #7529 fix e2e by:carlossa
- fix: refs #7529 fix front by:carlossa
- fix: refs #7529 fix scss by:carlossa
- fix: refs #7529 fix te2e by:carlossa
- fix: refs #7529 fix workerPit e2e by:carlossa
- fix: refs #7529 front by:carlossa
- fix: refs #8036 apply exprBuilder after save filters by:alexm
- fix: refs #8036 only add where when required by:alexm
- fix: refs #8038 solve conflicts by:Jon
- fix: refs #8061 improve code dependencies (origin/8061_improve_newCP) by:Javier Segarra
- fix: refs #8138 move component from ui folder by:pablone
- fix: refs #8138 sme minor issues by:pablone
- fix: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra
- fix: refs #8163 minor problem when keypress by:Javier Segarra
- fix: refs #8166 show zone error by:jorgep
- fix: removed selectedClient by:jgallego
- refs #7529 fix workerPit by:carlossa
- revert: refs #8061 test #8061 updates by:Javier Segarra
- test: fix own test by:Javier Segarra
- test: refs #8162 #8162 fix TicketList spec by:Javier Segarra
# Version 24.48 - 2024-11-25
### Added 🆕
- chore: correct checkNotification (fix_customer_issues) by:alexm
- chore: perf (warmFix_order_equalSalix) by:alexm
- chore: refs #6818 add spaces by:jorgep
- chore: refs #6818 drop useless code & comment by:jorgep
- chore: refs #7273 sticky add btn & refactor by:jorgep
- chore: refs #7524 fix test by:jorgep
- chore: refs #8039 not required by:alexm
- chore: refs #8078 fiz tests by:jorgep
- chore: refs #8078 rollback ref by:jorgep
- chore: remove console.log (warmFix_invoiceOut_Global) by:alexm
- chore: typo (fix_itemType-redirection) by:alexm
- feat: #6943 use openURL quasar by:Javier Segarra
- feat: #7782 add cypress report by:Javier Segarra
- feat: #7782 cypress.config watchForFileChanges by:Javier Segarra
- feat: #7782 npm run resetDatabase by:Javier Segarra
- feat: #7782 waitUntil domContentLoad by:Javier Segarra
- feat: added composable to confirm orders by:Jon
- feat: add /reports in gitignore (warmFix_reports_in_gitignore) by:alexm
- feat: apply changes for customerModule by:Javier Segarra
- feat: disabled buttons by:Javier Segarra
- feat: move buttons to DescriptorMenu by:Javier Segarra
- feat: refs #6818 add icon by:jorgep
- feat: refs #6818 fetch url & default channel by:jorgep
- feat: refs #6818 saysimple integration by:jorgep
- feat: refs #6839 module searching (6839-addSearchMenu) by:jorgep
- feat: refs #6839 normalize search by:jorgep
- feat: refs #6919 sync entry data by:jorgep
- feat: refs #7006 itemType basic data new inputs by:guillermo
- feat: refs #7006 itemTypeLog added by:guillermo
- feat: refs #7193 modified parking to use the scope and corrected small errors by:Jon
- feat: refs #7206 added inactive label and corrected minor errors by:Jon
- feat: refs #7308 #7308 remove warnings related to useSession by:Javier Segarra
- feat: refs #7349 usa back con permisos by:jgallego
- feat: refs #7524 add front test by:jorgep
- feat: refs #7874 improve vn-notes ui by:jorgep
- feat: refs #7970 notify changes by:Jon
- feat(): refs #8039 canceledError not notify by:alexm
- feat: refs #8039 notify error unify by:alexm
- feat: refs #8039 show duplicate request in local by:alexm
- feat: refs #8078 add shortcut multi selection by:jorgep
- feat: refs #8078 add tests by:jorgep
- feat: refs#8087 Redadas en travel by:Carlos Andrés
- feat: refs #8087 Traspasar redadas a travels by:Carlos Andrés
- feat: remove comments by:Javier Segarra
- feat(Supplier): add companySize by:alexm
- feat: use composable to unify logic by:Javier Segarra
- feat(VnInput): empty to null by:alexm
- feat(VnSelect): order data equal salix by:alexm
- feat(VnSelect): refs #7136 add scroll (7136-vnSelect_paginate_simplify_2) by:alexm
### Changed 📦
- chore: perf (warmFix_order_equalSalix) by:alexm
- chore: refs #7273 sticky add btn & refactor by:jorgep
- fix: better performance (warmFix_accountAcls) by:alexm
- perf: minor bugs detected by:Javier Segarra
- perf: refs #6943 #6943 merge command by:Javier Segarra
- perf: refs #7283 #7283 declare composable inst4ead code duplicated by:Javier Segarra
- perf: refs #7283 #7283 handle composable i18n by:Javier Segarra
- perf: refs #7283 #7283 handle i18n by:Javier Segarra
- perf: refs #7283 #7283 i18n params by:Javier Segarra
- perf: refs #7308 #7308 remove comments by:Javier Segarra
- perf: remove appendParams by:Javier Segarra
- perf: use const in VnLocation by:Javier Segarra
- perf: use required instead :required="true" by:Javier Segarra
- refactor: apply QPopupProxy by:wbuezas
- refactor: changed confirmOrder directory by:Jon
- refactor: change keyup.enter for update:model-value by:wbuezas
- refactor(InvoiceInBasicData): use VnDms by:alexm
- refactor: modified composable by:Jon
- refactor: refs #6818 change channel source by:jorgep
- refactor: refs #6818 channel logic by:jorgep
- refactor: refs #6919 export filter by:jorgep
- refactor: refs #7132 1st wave of changes in global translations files by:Jon
- refactor: refs #7132 account's module translations by:Jon
- refactor: refs #7132 customer's module translations by:Jon
- refactor: refs #7132 deleted pageTitles repeated by:Jon
- refactor: refs #7132 delete duplicate translations' keys by:Jon
- refactor: refs #7132 deleted useless code by:Jon
- refactor: refs #7132 global translations files changed by:Jon
- refactor: refs #7266 Changed method name by:guillermo
- refactor: refs #7950 Created cmr model by:guillermo
- refactor: refs #7970 added emit by:Jon
- refactor: refs #7970 refactored VnConfirm to emit events by:Jon
- refactor: refs #8185 modified LeftMenu to avoid duplicates by:Jon
- refactor: remove unused variable by:wbuezas
- refactor: revert catalog changes by:Jon
- refactor: small change by:wbuezas
- test: refactor e2e by:alexm
- test: refs #8039 add hasNotify and, refactor: agencyWorkCenter test by:alexm
### Fixed 🛠️
- chore: refs #7524 fix test by:jorgep
- fix: better performance (warmFix_accountAcls) by:alexm
- fix: catalog view category and type filter by:wbuezas
- fix: category and tags filters by:Jon
- fix: changed route.query by:Jon
- fix: change type vnput by:Javier Segarra
- fix(ClaimList): stateCode orderBy priority by:alexm
- fix: entryFilters by:carlossa
- fix: filter panel by:Jon
- fix(InvoiceOutGlobal): parallelism by:alexm
- fix: itemBotanical by:Javier Segarra
- fix: itemType redirection and fix filters by:alexm
- fix: logout spec (warmFix_logout.spec) by:alexm
- fix: merge errors by:alexm
- fix: order catalog by:wbuezas
- fix: order catalog fixes by:wbuezas
- fix: refs #6818 use right icon by:jorgep
- fix: refs #6896 fixed module problems by:Jon
- fix: refs #7193 fixed e2e test by:Jon
- fix: refs #7206 deleted duplicate code by:Jon
- fix: refs #7273 use same filter by:jorgep
- fix: refs #7283 #7283 bugs by:Javier Segarra
- fix: refs #7283 #7283 ItemDiary subToolbar by:Javier Segarra
- fix: refs #7283 #7283 ItemSummary bugs by:Javier Segarra
- fix: refs #7283 Account image resolution by:guillermo
- fix: refs #7283 css by:jorgep
- fix: refs #7283 filter by:carlossa
- fix: refs #7283 fix image by:carlossa
- fix: refs #7283 fix pr by:carlossa
- fix: refs #7283 fix preview by:carlossa
- fix: refs #7283 fix required by:carlossa
- fix: refs #7283 item filters by:carlossa
- fix: refs #7283 itemtype fix by:carlossa
- fix: refs #7283 order translation by:carlossa
- fix: refs #7283 preview by:carlossa
- fix: refs #7283 tooltips !Item by:Javier Segarra
- fix: refs #7306 clean warning by:carlossa
- fix: refs #7310 clean warning by:carlossa
- fix: refs #7323 locale #7396 by:jorgep
- fix: refs #7323 show advanced fields by:jorgep
- fix: refs #7349 dependencia no usada by:jgallego
- fix: refs #7524 e2e & worker module by:jorgep
- fix: refs #7874 add title by:jorgep
- fix: refs #7874 show name by:jorgep
- fix: refs #7943 use correct data-key by:jorgep
- fix: refs #7943 use summary by:jorgep
- fix: refs #8039 bad tests by:alexm
- fix: refs #8039 o not handle unnecessary errors by:alexm
- fix: refs #8078 e2e #7970 by:jorgep
- fix: refs #8078 handleSelection by:jorgep
- fix: refs #8078 improve cy command (8078-enableMultiSelection) by:jorgep
- fix: refs #8078 improve handleSelection by:jorgep
- fix: reset category by:wbuezas
- fix: tag chips by:Jon
- fix: vnSearchbar spec (warmFix_vnSearchBar.spec) by:alexm
- fix(VnSelect): setOptions when applyFilter by:alexm
- fix: worker test e2e by:Jon
- Merge branch 'dev' into fix_customer_issues by:Javier Segarra
- refactor: revert catalog changes by:Jon
- refs #7283 fix conflicts by:carlossa
- refs #7283 fix descriptorproxy by:carlossa
- refs #7283 fixedPrice by:carlossa
- refs #7283 fixedPrices by:carlossa
- refs #7283 fix itemFixed by:carlossa
- refs #7283 fix itemFixedPrice by:carlossa
- refs #7283 fix itemMigration by:carlossa
- refs #7283 fix itemMigration list filters by:carlossa
- refs #7283 fix items by:carlossa
- refs #7283 fix items error get images by:carlossa
- refs #7283 fix items images by:carlossa
- refs #7283 fix request by:carlossa
- refs #7283 fix searchbar by:carlossa
- refs #7283 fix viewSummary by:carlossa
- refs #7283 fix yml list basicData by:carlossa
- refs #7283 itemRequest fix by:carlossa
- refs #7283 itemRequest fix deny by:carlossa
- refs #7283 itemRequest fix reload by:carlossa
- refs #72983 fix filters by:carlossa
- revert: commit by:Javier Segarra
- revert e57a253c6f649382da187d1129449d265fb26d3b by:Javier Segarra
- test: #8162 fix clientList spec by:Javier Segarra
- test: #8162 fix vnLocation spec by:Javier Segarra
- test: fix arrayData by:Javier Segarra
- test: fix e2e by:alexm
- test: fix e2e by:Javier Segarra
- test: refs #8039 fix WorkerNotification e2e by:alexm
- test: refs #8039 fix ZoneWarehouse e2e by:alexm
- warmfix: ItemLastEntries to date (origin/warmfix_itemLastEntriesFilter) by:Javier Segarra
# Version 24.40 - 2024-10-02 # Version 24.40 - 2024-10-02
### Added 🆕 ### Added 🆕

6
Jenkinsfile vendored
View File

@ -4,7 +4,8 @@ def PROTECTED_BRANCH
def BRANCH_ENV = [ def BRANCH_ENV = [
test: 'test', test: 'test',
master: 'production' master: 'production',
beta: 'production'
] ]
node { node {
@ -15,7 +16,8 @@ node {
PROTECTED_BRANCH = [ PROTECTED_BRANCH = [
'dev', 'dev',
'test', 'test',
'master' 'master',
'beta'
].contains(env.BRANCH_NAME) ].contains(env.BRANCH_NAME)
// https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables // https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables

View File

@ -1,4 +1,7 @@
const { defineConfig } = require('cypress'); const { defineConfig } = require('cypress');
// https://docs.cypress.io/app/tooling/reporters
// https://docs.cypress.io/app/references/configuration
// https://www.npmjs.com/package/cypress-mochawesome-reporter
module.exports = defineConfig({ module.exports = defineConfig({
e2e: { e2e: {
@ -16,6 +19,7 @@ module.exports = defineConfig({
reporterOptions: { reporterOptions: {
charts: true, charts: true,
reportPageTitle: 'Cypress Inline Reporter', reportPageTitle: 'Cypress Inline Reporter',
reportFilename: '[status]_[datetime]-report',
embeddedScreenshots: true, embeddedScreenshots: true,
reportDir: 'test/cypress/reports', reportDir: 'test/cypress/reports',
inlineAssets: true, inlineAssets: true,

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.50.0", "version": "24.52.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",

View File

@ -0,0 +1,36 @@
import routes from 'src/router/modules';
import { useRouter } from 'vue-router';
let isNotified = false;
export default {
created: function () {
const router = useRouter();
const keyBindingMap = routes
.filter((route) => route.meta.keyBinding)
.reduce((map, route) => {
map['Key' + route.meta.keyBinding.toUpperCase()] = route.path;
return map;
}, {});
const handleKeyDown = (event) => {
const { ctrlKey, altKey, code } = event;
if (ctrlKey && altKey && keyBindingMap[code] && !isNotified) {
event.preventDefault();
router.push(keyBindingMap[code]);
isNotified = true;
}
};
const handleKeyUp = (event) => {
const { ctrlKey, altKey } = event;
if (!ctrlKey || !altKey) {
isNotified = false;
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
},
};

View File

@ -1,13 +1,37 @@
import { getCurrentInstance } from 'vue'; function focusFirstInput(input) {
input.focus();
return;
}
export default { export default {
mounted: function () { mounted: function () {
const vm = getCurrentInstance();
if (vm.type.name === 'QForm') {
if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) {
// TODO: AUTOFOCUS IS NOT FOCUSING
const that = this; const that = this;
this.$el.addEventListener('keyup', function (evt) {
const form = document.querySelector('.q-form#formModel');
if (!form) return;
try {
const inputsFormCard = form.querySelectorAll(
`input:not([disabled]):not([type="checkbox"])`
);
if (inputsFormCard.length) {
focusFirstInput(inputsFormCard[0]);
}
const textareas = document.querySelectorAll(
'textarea:not([disabled]), [contenteditable]:not([disabled])'
);
if (textareas.length) {
focusFirstInput(textareas[textareas.length - 1]);
}
const inputs = document.querySelectorAll(
'form#formModel input:not([disabled]):not([type="checkbox"])'
);
const input = inputs[0];
if (!input) return;
focusFirstInput(input);
} catch (error) {
console.error(error);
}
form.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter') { if (evt.key === 'Enter') {
const input = evt.target; const input = evt.target;
if (input.type == 'textarea' && evt.shiftKey) { if (input.type == 'textarea' && evt.shiftKey) {
@ -24,7 +48,5 @@ export default {
that.onSubmit(); that.onSubmit();
} }
}); });
}
}
}, },
}; };

View File

@ -1,15 +1,18 @@
import axios from 'axios';
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import qFormMixin from './qformMixin'; import qFormMixin from './qformMixin';
import keyShortcut from './keyShortcut'; import keyShortcut from './keyShortcut';
import useNotify from 'src/composables/useNotify.js'; import { QForm } from 'quasar';
import { CanceledError } from 'axios'; import { QLayout } from 'quasar';
import mainShortcutMixin from './mainShortcutMixin';
const { notify } = useNotify(); import { useCau } from 'src/composables/useCau';
export default boot(({ app }) => { export default boot(({ app }) => {
app.mixin(qFormMixin); QForm.mixins = [qFormMixin];
QLayout.mixins = [mainShortcutMixin];
app.directive('shortcut', keyShortcut); app.directive('shortcut', keyShortcut);
app.config.errorHandler = (error) => { app.config.errorHandler = async (error) => {
let message; let message;
const response = error.response; const response = error.response;
const responseData = response?.data; const responseData = response?.data;
@ -40,12 +43,12 @@ export default boot(({ app }) => {
} }
console.error(error); console.error(error);
if (error instanceof CanceledError) { if (error instanceof axios.CanceledError) {
const env = process.env.NODE_ENV; const env = process.env.NODE_ENV;
if (env && env !== 'development') return; if (env && env !== 'development') return;
message = 'Duplicate request'; message = 'Duplicate request';
} }
notify(message ?? 'globals.error', 'negative', 'error'); await useCau(response, message);
}; };
}); });

View File

@ -17,10 +17,6 @@ const $props = defineProps({
type: Number, type: Number,
default: null, default: null,
}, },
provinces: {
type: Array,
default: () => [],
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -44,19 +40,23 @@ const onDataSaved = (...args) => {
url-create="towns" url-create="towns"
model="city" model="city"
@on-data-saved="onDataSaved" @on-data-saved="onDataSaved"
data-cy="newCityForm"
> >
<template #form-inputs="{ data, validate }"> <template #form-inputs="{ data, validate }">
<VnRow> <VnRow>
<VnInput <VnInput
:label="t('Names')" :label="t('Name')"
v-model="data.name" v-model="data.name"
:rules="validate('city.name')" :rules="validate('city.name')"
required
data-cy="cityName"
/> />
<VnSelectProvince <VnSelectProvince
:province-selected="$props.provinceSelected" :province-selected="$props.provinceSelected"
:country-fk="$props.countryFk" :country-fk="$props.countryFk"
v-model="data.provinceFk" v-model="data.provinceFk"
:provinces="$props.provinces" required
data-cy="provinceCity"
/> />
</VnRow> </VnRow>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { reactive, ref, watch } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
@ -21,13 +21,14 @@ const postcodeFormData = reactive({
provinceFk: null, provinceFk: null,
townFk: null, townFk: null,
}); });
const townsFetchDataRef = ref(false);
const townFilter = ref({});
const townsFetchDataRef = ref(null); const countriesRef = ref(false);
const provincesFetchDataRef = ref(null);
const countriesOptions = ref([]);
const provincesOptions = ref([]); const provincesOptions = ref([]);
const townsOptions = ref([]); const townsOptions = ref([]);
const town = ref({}); const town = ref({});
const countryFilter = ref({});
function onDataSaved(formData) { function onDataSaved(formData) {
const newPostcode = { const newPostcode = {
@ -39,110 +40,91 @@ function onDataSaved(formData) {
({ id }) => id === formData.provinceFk ({ id }) => id === formData.provinceFk
); );
newPostcode.province = provinceObject?.name; newPostcode.province = provinceObject?.name;
const countryObject = countriesOptions.value.find( const countryObject = countriesRef.value.opts.find(
({ id }) => id === formData.countryFk ({ id }) => id === formData.countryFk
); );
newPostcode.country = countryObject?.name; newPostcode.country = countryObject?.name;
emit('onDataSaved', newPostcode); emit('onDataSaved', newPostcode);
} }
async function setCountry(countryFk, data) {
data.townFk = null;
data.provinceFk = null;
data.countryFk = countryFk;
await fetchTowns();
}
// Province
async function handleProvinces(data) {
provincesOptions.value = data;
if (postcodeFormData.countryFk) {
await fetchTowns();
}
}
async function setProvince(id, data) {
if (data.provinceFk === id) return;
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (newProvince) data.countryFk = newProvince.countryFk;
postcodeFormData.provinceFk = id;
await fetchTowns();
}
async function onProvinceCreated(data) {
postcodeFormData.provinceFk = data.id;
}
function provinceByCountry(countryFk = postcodeFormData.countryFk) {
return provincesOptions.value
.filter((province) => province.countryFk === countryFk)
.map(({ id }) => id);
}
// Town
async function handleTowns(data) {
townsOptions.value = data;
}
function setTown(newTown, data) {
town.value = newTown;
data.provinceFk = newTown?.provinceFk ?? newTown;
data.countryFk = newTown?.province?.countryFk ?? newTown;
}
async function onCityCreated(newTown, formData) { async function onCityCreated(newTown, formData) {
await provincesFetchDataRef.value.fetch();
newTown.province = provincesOptions.value.find( newTown.province = provincesOptions.value.find(
(province) => province.id === newTown.provinceFk (province) => province.id === newTown.provinceFk
); );
formData.townFk = newTown; formData.townFk = newTown;
setTown(newTown, formData); setTown(newTown, formData);
} }
async function fetchTowns(countryFk = postcodeFormData.countryFk) {
function setTown(newTown, data) { if (!countryFk) return;
if (!newTown) return; const provinces = postcodeFormData.provinceFk
town.value = newTown; ? [postcodeFormData.provinceFk]
data.provinceFk = newTown.provinceFk; : provinceByCountry();
data.countryFk = newTown.province.countryFk; townFilter.value.where = {
}
async function setProvince(id, data) {
const newProvince = provincesOptions.value.find((province) => province.id == id);
if (!newProvince) return;
data.countryFk = newProvince.countryFk;
}
async function onProvinceCreated(data) {
await provincesFetchDataRef.value.fetch({
where: { countryFk: postcodeFormData.countryFk },
});
postcodeFormData.provinceFk.value = data.id;
}
watch(
() => [postcodeFormData.countryFk],
async (newCountryFk, oldValueFk) => {
if (Array.isArray(newCountryFk)) {
newCountryFk = newCountryFk[0];
}
if (Array.isArray(oldValueFk)) {
oldValueFk = oldValueFk[0];
}
if (!!oldValueFk && newCountryFk !== oldValueFk) {
postcodeFormData.provinceFk = null;
postcodeFormData.townFk = null;
}
if (oldValueFk !== newCountryFk) {
await provincesFetchDataRef.value.fetch({
where: {
countryFk: newCountryFk,
},
});
await townsFetchDataRef.value.fetch({
where: {
provinceFk: { provinceFk: {
inq: provincesOptions.value.map(({ id }) => id), inq: provinces,
}, },
}, };
}); await townsFetchDataRef.value?.fetch();
} }
}
);
watch( async function filterTowns(name) {
() => postcodeFormData.provinceFk, if (name !== '') {
async (newProvinceFk, oldValueFk) => { townFilter.value.where = {
if (Array.isArray(newProvinceFk)) { name: {
newProvinceFk = newProvinceFk[0]; like: `%${name}%`,
},
};
await townsFetchDataRef.value?.fetch();
} }
if (newProvinceFk !== oldValueFk) {
await townsFetchDataRef.value.fetch({
where: { provinceFk: newProvinceFk },
});
}
}
);
async function handleProvinces(data) {
provincesOptions.value = data;
}
async function handleTowns(data) {
townsOptions.value = data;
}
async function handleCountries(data) {
countriesOptions.value = data;
} }
</script> </script>
<template> <template>
<FetchData
ref="provincesFetchDataRef"
@on-fetch="handleProvinces"
:sort-by="['name ASC']"
:limit="30"
auto-load
url="Provinces/location"
/>
<FetchData <FetchData
ref="townsFetchDataRef" ref="townsFetchDataRef"
:sort-by="['name ASC']" :sort-by="['name ASC']"
:limit="30" :limit="30"
:filter="townFilter"
@on-fetch="handleTowns" @on-fetch="handleTowns"
auto-load auto-load
url="Towns/location" url="Towns/location"
@ -164,10 +146,13 @@ async function handleCountries(data) {
v-model="data.code" v-model="data.code"
:rules="validate('postcode.code')" :rules="validate('postcode.code')"
clearable clearable
required
data-cy="locationPostcode"
/> />
<VnSelectDialog <VnSelectDialog
:label="t('City')" :label="t('City')"
@update:model-value="(value) => setTown(value, data)" @update:model-value="(value) => setTown(value, data)"
@filter="filterTowns"
:tooltip="t('Create city')" :tooltip="t('Create city')"
v-model="data.townFk" v-model="data.townFk"
:options="townsOptions" :options="townsOptions"
@ -176,7 +161,8 @@ async function handleCountries(data) {
:rules="validate('postcode.city')" :rules="validate('postcode.city')"
:acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:emit-value="false" :emit-value="false"
:clearable="true" required
data-cy="locationTown"
> >
<template #option="{ itemProps, opt }"> <template #option="{ itemProps, opt }">
<QItem v-bind="itemProps"> <QItem v-bind="itemProps">
@ -193,7 +179,6 @@ async function handleCountries(data) {
<CreateNewCityForm <CreateNewCityForm
:country-fk="data.countryFk" :country-fk="data.countryFk"
:province-selected="data.provinceFk" :province-selected="data.provinceFk"
:provinces="provincesOptions"
@on-data-saved=" @on-data-saved="
(_, requestResponse) => (_, requestResponse) =>
onCityCreated(requestResponse, data) onCityCreated(requestResponse, data)
@ -207,21 +192,31 @@ async function handleCountries(data) {
:country-fk="data.countryFk" :country-fk="data.countryFk"
:province-selected="data.provinceFk" :province-selected="data.provinceFk"
@update:model-value="(value) => setProvince(value, data)" @update:model-value="(value) => setProvince(value, data)"
@update:options="
(data) => {
provincesOptions = data;
}
"
v-model="data.provinceFk" v-model="data.provinceFk"
:clearable="true"
:provinces="provincesOptions"
@on-province-created="onProvinceCreated" @on-province-created="onProvinceCreated"
required
/> />
<VnSelect <VnSelect
url="Countries" ref="countriesRef"
:limit="30"
:filter="countryFilter"
:sort-by="['name ASC']" :sort-by="['name ASC']"
auto-load
url="Countries"
required
:label="t('Country')" :label="t('Country')"
@update:options="handleCountries"
hide-selected hide-selected
option-label="name" option-label="name"
option-value="id" option-value="id"
v-model="data.countryFk" v-model="data.countryFk"
:rules="validate('postcode.countryFk')" :rules="validate('postcode.countryFk')"
@update:model-value="(value) => setCountry(value, data)"
data-cy="locationCountry"
/> />
</VnRow> </VnRow>
</template> </template>

View File

@ -1,8 +1,7 @@
<script setup> <script setup>
import { reactive, ref } from 'vue'; import { computed, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
@ -21,34 +20,24 @@ const $props = defineProps({
type: Number, type: Number,
default: null, default: null,
}, },
provinces: {
type: Array,
default: () => [],
},
}); });
const autonomiesOptions = ref([]); const autonomiesRef = ref([]);
const onDataSaved = (dataSaved, requestResponse) => { const onDataSaved = (dataSaved, requestResponse) => {
requestResponse.autonomy = autonomiesOptions.value.find( requestResponse.autonomy = autonomiesRef.value.opts.find(
(autonomy) => autonomy.id == requestResponse.autonomyFk (autonomy) => autonomy.id == requestResponse.autonomyFk
); );
emit('onDataSaved', dataSaved, requestResponse); emit('onDataSaved', dataSaved, requestResponse);
}; };
const where = computed(() => {
if (!$props.countryFk) {
return {};
}
return { countryFk: $props.countryFk };
});
</script> </script>
<template> <template>
<FetchData
@on-fetch="(data) => (autonomiesOptions = data)"
auto-load
:filter="{
where: {
countryFk: $props.countryFk,
},
}"
url="Autonomies/location"
:sort-by="['name ASC']"
:limit="30"
/>
<FormModelPopup <FormModelPopup
:title="t('New province')" :title="t('New province')"
:subtitle="t('Please, ensure you put the correct data!')" :subtitle="t('Please, ensure you put the correct data!')"
@ -63,10 +52,19 @@ const onDataSaved = (dataSaved, requestResponse) => {
:label="t('Name')" :label="t('Name')"
v-model="data.name" v-model="data.name"
:rules="validate('province.name')" :rules="validate('province.name')"
required
data-cy="provinceName"
/> />
<VnSelect <VnSelect
data-cy="autonomyProvince"
required
ref="autonomiesRef"
auto-load
:where="where"
url="Autonomies/location"
:sort-by="['name ASC']"
:limit="30"
:label="t('Autonomy')" :label="t('Autonomy')"
:options="autonomiesOptions"
hide-selected hide-selected
option-label="name" option-label="name"
option-value="id" option-value="id"

View File

@ -10,6 +10,7 @@ import VnPaginate from 'components/ui/VnPaginate.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue'; import SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile'; import { tMobile } from 'src/composables/tMobile';
import getDifferences from 'src/filters/getDifferences';
const { push } = useRouter(); const { push } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
@ -94,6 +95,7 @@ defineExpose({
saveChanges, saveChanges,
getChanges, getChanges,
formData, formData,
originalData,
vnPaginateRef, vnPaginateRef,
}); });
@ -174,14 +176,13 @@ async function saveChanges(data) {
const changes = data || getChanges(); const changes = data || getChanges();
try { try {
await axios.post($props.saveUrl || $props.url + '/crud', changes); await axios.post($props.saveUrl || $props.url + '/crud', changes);
} catch (e) { } finally {
return (isLoading.value = false); isLoading.value = false;
} }
originalData.value = JSON.parse(JSON.stringify(formData.value)); originalData.value = JSON.parse(JSON.stringify(formData.value));
if (changes.creates?.length) await vnPaginateRef.value.fetch(); if (changes.creates?.length) await vnPaginateRef.value.fetch();
hasChanges.value = false; hasChanges.value = false;
isLoading.value = false;
emit('saveChanges', data); emit('saveChanges', data);
quasar.notify({ quasar.notify({
type: 'positive', type: 'positive',
@ -248,7 +249,7 @@ function getChanges() {
for (const [i, row] of formData.value.entries()) { for (const [i, row] of formData.value.entries()) {
if (!row[pk]) { if (!row[pk]) {
creates.push(row); creates.push(row);
} else if (originalData.value) { } else if (originalData.value[i]) {
const data = getDifferences(originalData.value[i], row); const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) { if (!isEmpty(data)) {
updates.push({ updates.push({
@ -267,28 +268,6 @@ function getChanges() {
return changes; return changes;
} }
function getDifferences(obj1, obj2) {
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (let key in obj1) {
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key];
}
}
for (let key in obj2) {
if (
obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])
) {
diff[key] = obj2[key];
}
}
return diff;
}
function isEmpty(obj) { function isEmpty(obj) {
if (obj == null) return true; if (obj == null) return true;
if (obj === undefined) return true; if (obj === undefined) return true;

View File

@ -156,7 +156,6 @@ const rotateRight = () => {
}; };
const onSubmit = () => { const onSubmit = () => {
try {
if (!newPhoto.files && !newPhoto.url) { if (!newPhoto.files && !newPhoto.url) {
notify(t('Select an image'), 'negative'); notify(t('Select an image'), 'negative');
return; return;
@ -173,9 +172,6 @@ const onSubmit = () => {
newPhoto.blob = file; newPhoto.blob = file;
}) })
.then(() => makeRequest()); .then(() => makeRequest());
} catch (err) {
console.error('Error uploading image');
}
}; };
const makeRequest = async () => { const makeRequest = async () => {

View File

@ -51,7 +51,6 @@ const onDataSaved = () => {
}; };
const onSubmit = async () => { const onSubmit = async () => {
try {
isLoading.value = true; isLoading.value = true;
const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk })); const rowsToEdit = $props.rows.map((row) => ({ id: row.id, itemFk: row.itemFk }));
const payload = { const payload = {
@ -63,9 +62,6 @@ const onSubmit = async () => {
await axios.post($props.editUrl, payload); await axios.post($props.editUrl, payload);
onDataSaved(); onDataSaved();
isLoading.value = false; isLoading.value = false;
} catch (err) {
console.error('Error submitting table cell edit');
}
}; };
const closeForm = () => { const closeForm = () => {
@ -89,12 +85,14 @@ const closeForm = () => {
hide-selected hide-selected
option-label="label" option-label="label"
v-model="selectedField" v-model="selectedField"
data-cy="field-to-edit"
/> />
<component <component
:is="inputs[selectedField?.component || 'input']" :is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}" v-bind="selectedField?.attrs || {}"
v-model="newValue" v-model="newValue"
:label="t('Value')" :label="t('Value')"
data-cy="value-to-edit"
style="width: 200px" style="width: 200px"
/> />
</VnRow> </VnRow>

View File

@ -84,7 +84,6 @@ const tableColumns = computed(() => [
]); ]);
const onSubmit = async () => { const onSubmit = async () => {
try {
let filter = itemFilter; let filter = itemFilter;
const params = itemFilterParams; const params = itemFilterParams;
const where = {}; const where = {};
@ -109,9 +108,6 @@ const onSubmit = async () => {
params: { filter: JSON.stringify(filter) }, params: { filter: JSON.stringify(filter) },
}); });
tableRows.value = data; tableRows.value = data;
} catch (err) {
console.error('Error fetching entries items');
}
}; };
const closeForm = () => { const closeForm = () => {

View File

@ -86,7 +86,6 @@ const tableColumns = computed(() => [
]); ]);
const onSubmit = async () => { const onSubmit = async () => {
try {
let filter = travelFilter; let filter = travelFilter;
const params = travelFilterParams; const params = travelFilterParams;
const where = {}; const where = {};
@ -109,9 +108,6 @@ const onSubmit = async () => {
params: { filter: JSON.stringify(filter) }, params: { filter: JSON.stringify(filter) },
}); });
tableRows.value = data; tableRows.value = data;
} catch (err) {
console.error('Error fetching travels');
}
}; };
const closeForm = () => { const closeForm = () => {

View File

@ -91,6 +91,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
maxWidth: {
type: [String, Boolean],
default: '800px',
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed( const modelValue = computed(
@ -106,6 +110,7 @@ const originalData = ref({});
const formData = computed(() => state.get(modelValue)); const formData = computed(() => state.get(modelValue));
const defaultButtons = computed(() => ({ const defaultButtons = computed(() => ({
save: { save: {
dataCy: 'saveDefaultBtn',
color: 'primary', color: 'primary',
icon: 'save', icon: 'save',
label: 'globals.save', label: 'globals.save',
@ -113,6 +118,7 @@ const defaultButtons = computed(() => ({
type: 'submit', type: 'submit',
}, },
reset: { reset: {
dataCy: 'resetDefaultBtn',
color: 'primary', color: 'primary',
icon: 'restart_alt', icon: 'restart_alt',
label: 'globals.reset', label: 'globals.reset',
@ -203,7 +209,9 @@ async function save() {
isLoading.value = true; isLoading.value = true;
try { try {
formData.value = trimData(formData.value); formData.value = trimData(formData.value);
const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const body = $props.mapper
? $props.mapper(formData.value, originalData.value)
: formData.value;
const method = $props.urlCreate ? 'post' : 'patch'; const method = $props.urlCreate ? 'post' : 'patch';
const url = const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url; $props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
@ -283,6 +291,7 @@ defineExpose({
@submit="save" @submit="save"
@reset="reset" @reset="reset"
class="q-pa-md" class="q-pa-md"
:style="maxWidth ? 'max-width: ' + maxWidth : ''"
id="formModel" id="formModel"
> >
<QCard> <QCard>
@ -317,6 +326,7 @@ defineExpose({
:title="t(defaultButtons.reset.label)" :title="t(defaultButtons.reset.label)"
/> />
<QBtnDropdown <QBtnDropdown
data-cy="saveAndContinueDefaultBtn"
v-if="$props.goTo" v-if="$props.goTo"
@click="saveAndGo" @click="saveAndGo"
:label="tMobile('globals.saveAndContinue')" :label="tMobile('globals.saveAndContinue')"
@ -371,7 +381,6 @@ defineExpose({
color: black; color: black;
} }
#formModel { #formModel {
max-width: 800px;
width: 100%; width: 100%;
} }

View File

@ -62,6 +62,7 @@ defineExpose({
@click="emit('onDataCanceled')" @click="emit('onDataCanceled')"
v-close-popup v-close-popup
data-cy="FormModelPopup_cancel" data-cy="FormModelPopup_cancel"
z-max
/> />
<QBtn <QBtn
:label="t('globals.save')" :label="t('globals.save')"
@ -72,6 +73,7 @@ defineExpose({
:disabled="isLoading" :disabled="isLoading"
:loading="isLoading" :loading="isLoading"
data-cy="FormModelPopup_save" data-cy="FormModelPopup_save"
z-max
/> />
</div> </div>
</template> </template>

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
const emit = defineEmits(['onSubmit']); const emit = defineEmits(['onSubmit']);
defineProps({ const $props = defineProps({
title: { title: {
type: String, type: String,
default: '', default: '',
@ -25,16 +25,21 @@ defineProps({
type: String, type: String,
default: '', default: '',
}, },
submitOnEnter: {
type: Boolean,
default: true,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
const onSubmit = () => { const onSubmit = () => {
if ($props.submitOnEnter) {
emit('onSubmit'); emit('onSubmit');
closeForm(); closeForm();
}
}; };
const closeForm = () => { const closeForm = () => {

View File

@ -9,6 +9,8 @@ import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import axios from 'axios'; import axios from 'axios';
import { getParamWhere } from 'src/filters';
import { useRoute } from 'vue-router';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -26,28 +28,21 @@ const props = defineProps({
}, },
}); });
const itemCategories = ref([]); const route = useRoute();
const selectedCategoryFk = ref(null);
const selectedTypeFk = ref(null);
const itemTypesOptions = ref([]); const itemTypesOptions = ref([]);
const suppliersOptions = ref([]); const suppliersOptions = ref([]);
const tagOptions = ref([]); const tagOptions = ref([]);
const tagValues = ref([]); const tagValues = ref([]);
const categoryList = ref(null);
const selectedCategoryFk = ref(getParamWhere(route.query.table, 'categoryFk', false));
const selectedTypeFk = ref(getParamWhere(route.query.table, 'typeFk', false));
const categoryList = computed(() => { const selectedCategory = computed(() => {
return (itemCategories.value || []) return (categoryList.value || []).find(
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
});
const selectedCategory = computed(() =>
(itemCategories.value || []).find(
(category) => category?.id === selectedCategoryFk.value (category) => category?.id === selectedCategoryFk.value
)
); );
});
const selectedType = computed(() => { const selectedType = computed(() => {
return (itemTypesOptions.value || []).find( return (itemTypesOptions.value || []).find(
@ -87,8 +82,7 @@ const applyTags = (params, search) => {
search(); search();
}; };
const fetchItemTypes = async (id) => { const fetchItemTypes = async (id = selectedCategoryFk.value) => {
try {
const filter = { const filter = {
fields: ['id', 'name', 'categoryFk'], fields: ['id', 'name', 'categoryFk'],
where: { categoryFk: id }, where: { categoryFk: id },
@ -99,9 +93,6 @@ const fetchItemTypes = async (id) => {
params: { filter: JSON.stringify(filter) }, params: { filter: JSON.stringify(filter) },
}); });
itemTypesOptions.value = data; itemTypesOptions.value = data;
} catch (err) {
console.error('Error fetching item types', err);
}
}; };
const getCategoryClass = (category, params) => { const getCategoryClass = (category, params) => {
@ -111,7 +102,6 @@ const getCategoryClass = (category, params) => {
}; };
const getSelectedTagValues = async (tag) => { const getSelectedTagValues = async (tag) => {
try {
if (!tag?.selectedTag?.id) return; if (!tag?.selectedTag?.id) return;
tag.value = null; tag.value = null;
const filter = { const filter = {
@ -125,24 +115,25 @@ const getSelectedTagValues = async (tag) => {
params, params,
}); });
tag.valueOptions = data; tag.valueOptions = data;
} catch (err) {
console.error('Error getting selected tag values');
}
}; };
const removeTag = (index, params, search) => { const removeTag = (index, params, search) => {
(tagValues.value || []).splice(index, 1); (tagValues.value || []).splice(index, 1);
applyTags(params, search); applyTags(params, search);
}; };
const setCategoryList = (data) => {
categoryList.value = (data || [])
.filter((category) => category.display)
.map((category) => ({
...category,
icon: `vn:${(category.icon || '').split('-')[1]}`,
}));
fetchItemTypes();
};
</script> </script>
<template> <template>
<FetchData <FetchData url="ItemCategories" limit="30" auto-load @on-fetch="setCategoryList" />
url="ItemCategories"
limit="30"
auto-load
@on-fetch="(data) => (itemCategories = data)"
/>
<FetchData <FetchData
url="Suppliers" url="Suppliers"
limit="30" limit="30"

View File

@ -177,6 +177,7 @@ function normalize(text) {
class="full-width" class="full-width"
filled filled
dense dense
autofocus
/> />
</QItem> </QItem>
<QSeparator /> <QSeparator />

View File

@ -39,14 +39,10 @@ const refund = async () => {
invoiceCorrectionTypeFk: invoiceParams.invoiceCorrectionTypeFk, invoiceCorrectionTypeFk: invoiceParams.invoiceCorrectionTypeFk,
}; };
try {
const { data } = await axios.post('InvoiceOuts/refundAndInvoice', params); const { data } = await axios.post('InvoiceOuts/refundAndInvoice', params);
notify(t('Refunded invoice'), 'positive'); notify(t('Refunded invoice'), 'positive');
const [id] = data?.refundId || []; const [id] = data?.refundId || [];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } }); if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) {
console.error('Error refunding invoice', err);
}
}; };
</script> </script>

View File

@ -49,7 +49,6 @@ const makeInvoice = async () => {
makeInvoice: checked.value, makeInvoice: checked.value,
}; };
try {
if (checked.value && hasToInvoiceByAddress) { if (checked.value && hasToInvoiceByAddress) {
const response = await new Promise((resolve) => { const response = await new Promise((resolve) => {
quasar quasar
@ -76,9 +75,6 @@ const makeInvoice = async () => {
notify(t('Transferred invoice'), 'positive'); notify(t('Transferred invoice'), 'positive');
const id = data?.[0]; const id = data?.[0];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } }); if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) {
console.error('Error transfering invoice', err);
}
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, watch } from 'vue';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -7,7 +7,7 @@ import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import CreateNewProvinceForm from './CreateNewProvinceForm.vue'; import CreateNewProvinceForm from './CreateNewProvinceForm.vue';
const emit = defineEmits(['onProvinceCreated']); const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched', 'update:options']);
const $props = defineProps({ const $props = defineProps({
countryFk: { countryFk: {
type: Number, type: Number,
@ -17,20 +17,23 @@ const $props = defineProps({
type: Number, type: Number,
default: null, default: null,
}, },
provinces: {
type: Array,
default: () => [],
},
}); });
const provinceFk = defineModel({ type: Number, default: null }); const provinceFk = defineModel({ type: Number, default: null });
const { validate } = useValidator(); const { validate } = useValidator();
const { t } = useI18n(); const { t } = useI18n();
const filter = ref({
include: { relation: 'country' },
where: {
countryFk: $props.countryFk,
},
});
const provincesOptions = ref($props.provinces); const provincesOptions = ref($props.provinces);
provinceFk.value = $props.provinceSelected;
const provincesFetchDataRef = ref(); const provincesFetchDataRef = ref();
provinceFk.value = $props.provinceSelected;
if (!$props.countryFk) {
filter.value.where = {};
}
async function onProvinceCreated(_, data) { async function onProvinceCreated(_, data) {
await provincesFetchDataRef.value.fetch({ where: { countryFk: $props.countryFk } }); await provincesFetchDataRef.value.fetch({ where: { countryFk: $props.countryFk } });
provinceFk.value = data.id; provinceFk.value = data.id;
@ -38,24 +41,33 @@ async function onProvinceCreated(_, data) {
} }
async function handleProvinces(data) { async function handleProvinces(data) {
provincesOptions.value = data; provincesOptions.value = data;
emit('update:options', data);
} }
watch(
() => $props.countryFk,
async () => {
if ($props.countryFk) {
filter.value.where.countryFk = $props.countryFk;
} else filter.value.where = {};
await provincesFetchDataRef.value.fetch({});
emit('onProvinceFetched', provincesOptions.value);
}
);
</script> </script>
<template> <template>
<FetchData <FetchData
ref="provincesFetchDataRef" ref="provincesFetchDataRef"
:filter="{ :filter="filter"
include: { relation: 'country' },
where: {
countryFk: $props.countryFk,
},
}"
@on-fetch="handleProvinces" @on-fetch="handleProvinces"
url="Provinces" url="Provinces"
auto-load
/> />
<VnSelectDialog <VnSelectDialog
data-cy="locationProvince"
:label="t('Province')" :label="t('Province')"
:options="$props.provinces" :options="provincesOptions"
:tooltip="t('Create province')" :tooltip="t('Create province')"
hide-selected hide-selected
v-model="provinceFk" v-model="provinceFk"

View File

@ -610,6 +610,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
$props.rowClick && $props.rowClick(row); $props.rowClick && $props.rowClick(row);
} }
" "
style="height: 100%"
> >
<QCardSection <QCardSection
vertical vertical

View File

@ -58,7 +58,6 @@ const getConfig = async (url, filter) => {
}; };
const fetchViewConfigData = async () => { const fetchViewConfigData = async () => {
try {
const userConfigFilter = { const userConfigFilter = {
where: { tableCode: $props.tableCode, userFk: user.value.id }, where: { tableCode: $props.tableCode, userFk: user.value.id },
}; };
@ -83,13 +82,9 @@ const fetchViewConfigData = async () => {
$props.allColumns.forEach((col) => (defaultColumns[col] = true)); $props.allColumns.forEach((col) => (defaultColumns[col] = true));
setUserConfigViewData(defaultColumns); setUserConfigViewData(defaultColumns);
} }
} catch (err) {
console.error('Error fetching config view data', err);
}
}; };
const saveConfig = async () => { const saveConfig = async () => {
try {
const params = {}; const params = {};
const configuration = {}; const configuration = {};
@ -128,9 +123,6 @@ const saveConfig = async () => {
emitSavedConfig(); emitSavedConfig();
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
popupProxyRef.value.hide(); popupProxyRef.value.hide();
} catch (err) {
console.error('Error saving user view config', err);
}
}; };
const emitSavedConfig = () => { const emitSavedConfig = () => {

View File

@ -1,20 +1,24 @@
<script setup> <script setup>
import { ref, watch } from 'vue'; import { nextTick, ref, watch } from 'vue';
import { QInput } from 'quasar'; import { QInput } from 'quasar';
const props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
type: String, type: String,
default: '', default: '',
}, },
insertable: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['update:modelValue', 'accountShortToStandard']); const emit = defineEmits(['update:modelValue', 'accountShortToStandard']);
let internalValue = ref(props.modelValue); let internalValue = ref($props.modelValue);
watch( watch(
() => props.modelValue, () => $props.modelValue,
(newVal) => { (newVal) => {
internalValue.value = newVal; internalValue.value = newVal;
} }
@ -28,8 +32,46 @@ watch(
} }
); );
const handleKeydown = (e) => {
if (e.key === 'Backspace') return;
if (e.key === '.') {
accountShortToStandard();
// TODO: Fix this setTimeout, with nextTick doesn't work
setTimeout(() => {
setCursorPosition(0, e.target);
}, 1);
return;
}
if ($props.insertable && e.key.match(/[0-9]/)) {
handleInsertMode(e);
}
};
function setCursorPosition(pos, el = vnInputRef.value) {
el.focus();
el.setSelectionRange(pos, pos);
}
const vnInputRef = ref(false);
const handleInsertMode = (e) => {
e.preventDefault();
const input = e.target;
const cursorPos = input.selectionStart;
const { maxlength } = vnInputRef.value;
let currentValue = internalValue.value;
if (!currentValue) currentValue = e.key;
const newValue = e.key;
if (newValue && !isNaN(newValue) && cursorPos < maxlength) {
internalValue.value =
currentValue.substring(0, cursorPos) +
newValue +
currentValue.substring(cursorPos + 1);
}
nextTick(() => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1);
});
};
function accountShortToStandard() { function accountShortToStandard() {
internalValue.value = internalValue.value.replace( internalValue.value = internalValue.value?.replace(
'.', '.',
'0'.repeat(11 - internalValue.value.length) '0'.repeat(11 - internalValue.value.length)
); );
@ -37,5 +79,5 @@ function accountShortToStandard() {
</script> </script>
<template> <template>
<q-input v-model="internalValue" /> <QInput @keydown="handleKeydown" ref="vnInputRef" v-model="internalValue" />
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, ref, useAttrs } from 'vue'; import { computed, ref, useAttrs, nextTick } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRequired } from 'src/composables/useRequired'; import { useRequired } from 'src/composables/useRequired';
@ -34,6 +34,14 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
insertable: {
type: Boolean,
default: false,
},
maxlength: {
type: Number,
default: null,
},
}); });
const vnInputRef = ref(null); const vnInputRef = ref(null);
@ -69,6 +77,9 @@ const mixinRules = [
requiredFieldRule, requiredFieldRule,
...($attrs.rules ?? []), ...($attrs.rules ?? []),
(val) => { (val) => {
const { maxlength } = vnInputRef.value;
if (maxlength && +val.length > maxlength)
return t(`maxLength`, { value: maxlength });
const { min, max } = vnInputRef.value.$attrs; const { min, max } = vnInputRef.value.$attrs;
if (!min) return null; if (!min) return null;
if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min }); if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min });
@ -78,6 +89,33 @@ const mixinRules = [
} }
}, },
]; ];
const handleKeydown = (e) => {
if (e.key === 'Backspace') return;
if ($props.insertable && e.key.match(/[0-9]/)) {
handleInsertMode(e);
}
};
const handleInsertMode = (e) => {
e.preventDefault();
const input = e.target;
const cursorPos = input.selectionStart;
const { maxlength } = vnInputRef.value;
let currentValue = value.value;
if (!currentValue) currentValue = e.key;
const newValue = e.key;
if (newValue && !isNaN(newValue) && cursorPos < maxlength) {
value.value =
currentValue.substring(0, cursorPos) +
newValue +
currentValue.substring(cursorPos + 1);
}
nextTick(() => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1);
});
};
</script> </script>
<template> <template>
@ -89,10 +127,12 @@ const mixinRules = [
:type="$attrs.type" :type="$attrs.type"
:class="{ required: isRequired }" :class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')" @keyup.enter="emit('keyup.enter')"
@keydown="handleKeydown"
:clearable="false" :clearable="false"
:rules="mixinRules" :rules="mixinRules"
:lazy-rules="true" :lazy-rules="true"
hide-bottom-space hide-bottom-space
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
> >
<template v-if="$slots.prepend" #prepend> <template v-if="$slots.prepend" #prepend>
<slot name="prepend" /> <slot name="prepend" />
@ -129,9 +169,11 @@ const mixinRules = [
<i18n> <i18n>
en: en:
inputMin: Must be more than {value} inputMin: Must be more than {value}
maxLength: The value exceeds {value} characters
inputMax: Must be less than {value} inputMax: Must be less than {value}
es: es:
inputMin: Debe ser mayor a {value} inputMin: Debe ser mayor a {value}
maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value} inputMax: Debe ser menor a {value}
</i18n> </i18n>
<style lang="scss"> <style lang="scss">

View File

@ -2,7 +2,7 @@
import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue'; import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue';
import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import VnSelectDialog from 'components/common/VnSelectDialog.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ref } from 'vue'; import { computed } from 'vue';
import { useAttrs } from 'vue'; import { useAttrs } from 'vue';
import { useRequired } from 'src/composables/useRequired'; import { useRequired } from 'src/composables/useRequired';
const { t } = useI18n(); const { t } = useI18n();
@ -43,7 +43,7 @@ const formatLocation = (obj, properties) => {
return filteredParts.join(', '); return filteredParts.join(', ');
}; };
const modelValue = ref( const modelValue = computed(() =>
props.location ? formatLocation(props.location, locationProperties) : null props.location ? formatLocation(props.location, locationProperties) : null
); );
@ -75,7 +75,6 @@ const handleModelValue = (data) => {
:input-debounce="300" :input-debounce="300"
:class="{ required: isRequired }" :class="{ required: isRequired }"
v-bind="$attrs" v-bind="$attrs"
clearable
:emit-value="false" :emit-value="false"
:tooltip="t('Create new location')" :tooltip="t('Create new location')"
:rules="mixinRules" :rules="mixinRules"

View File

@ -238,6 +238,7 @@ async function openPointRecord(id, modelLog) {
pointRecord.value = parseProps(propNames, locale, data); pointRecord.value = parseProps(propNames, locale, data);
} }
async function setLogTree(data) { async function setLogTree(data) {
if (!data) return;
logTree.value = getLogTree(data); logTree.value = getLogTree(data);
} }

View File

@ -8,7 +8,14 @@ import dataByOrder from 'src/utils/dataByOrder';
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']); const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
const $attrs = useAttrs(); const $attrs = useAttrs();
const { t } = useI18n(); const { t } = useI18n();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const isRequired = computed(() => {
return useRequired($attrs).isRequired;
});
const requiredFieldRule = computed(() => {
return useRequired($attrs).requiredFieldRule;
});
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
type: [String, Number, Object], type: [String, Number, Object],
@ -261,7 +268,7 @@ async function onScroll({ to, direction, from, index }) {
defineExpose({ opts: myOptions }); defineExpose({ opts: myOptions });
function handleKeyDown(event) { function handleKeyDown(event) {
if (event.key === 'Tab') { if (event.key === 'Tab' && !event.shiftKey) {
event.preventDefault(); event.preventDefault();
const inputValue = vnSelectRef.value?.inputValue; const inputValue = vnSelectRef.value?.inputValue;
@ -279,6 +286,17 @@ function handleKeyDown(event) {
} }
vnSelectRef.value?.hidePopup(); vnSelectRef.value?.hidePopup();
} }
const focusableElements = document.querySelectorAll(
'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
);
const currentIndex = Array.prototype.indexOf.call(
focusableElements,
event.target
);
if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) {
focusableElements[currentIndex + 1].focus();
}
} }
} }
</script> </script>
@ -306,10 +324,11 @@ function handleKeyDown(event) {
:input-debounce="useURL ? '300' : '0'" :input-debounce="useURL ? '300' : '0'"
:loading="isLoading" :loading="isLoading"
@virtual-scroll="onScroll" @virtual-scroll="onScroll"
:data-cy="$attrs.dataCy ?? $attrs.label + '_select'"
> >
<template v-if="isClearable" #append> <template #append>
<QIcon <QIcon
v-show="value" v-show="isClearable && value"
name="close" name="close"
@click.stop=" @click.stop="
() => { () => {
@ -322,7 +341,22 @@ function handleKeyDown(event) {
/> />
</template> </template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName"> <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> <div v-if="slotName == 'append'">
<QIcon
v-show="isClearable && value"
name="close"
@click.stop="
() => {
value = null;
emit('remove');
}
"
class="cursor-pointer"
size="xs"
/>
<slot name="append" v-if="$slots.append" v-bind="slotData ?? {}" />
</div>
<slot v-else :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template> </template>
</QSelect> </QSelect>
</template> </template>

View File

@ -43,6 +43,7 @@ const isAllowedToCreate = computed(() => {
> >
<template v-if="isAllowedToCreate" #append> <template v-if="isAllowedToCreate" #append>
<QIcon <QIcon
:data-cy="$attrs.dataCy ?? $attrs.label + '_icon'"
@click.stop.prevent="$refs.dialog.show()" @click.stop.prevent="$refs.dialog.show()"
:name="actionIcon" :name="actionIcon"
:size="actionIcon === 'add' ? 'xs' : 'sm'" :size="actionIcon === 'add' ? 'xs' : 'sm'"

View File

@ -0,0 +1,85 @@
<script setup>
import { computed, useAttrs } from 'vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue';
const emit = defineEmits(['update:modelValue']);
const $props = defineProps({
hasAvatar: {
type: Boolean,
default: false,
},
hasInfo: {
type: Boolean,
default: false,
},
modelValue: {
type: [String, Number, Object],
default: null,
},
});
const $attrs = useAttrs();
const value = computed({
get() {
return $props.modelValue;
},
set(val) {
emit('update:modelValue', val);
},
});
const url = computed(() => {
let url = 'Workers/search';
const { departmentCodes } = $attrs.params ?? {};
if (!departmentCodes) return url;
const params = new URLSearchParams({
departmentCodes: JSON.stringify(departmentCodes),
});
return url.concat(`?${params.toString()}`);
});
</script>
<template>
<VnSelect
:label="$t('globals.worker')"
v-bind="$attrs"
v-model="value"
:url="url"
option-value="id"
option-label="nickname"
:fields="['id', 'name', 'nickname', 'code']"
sort-by="nickname ASC"
>
<template #prepend v-if="$props.hasAvatar">
<VnAvatar :worker-id="value" color="primary" :title="title" />
</template>
<template #append v-if="$props.hasInfo">
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ $t($props.hasInfo) }}</QTooltip>
</QIcon>
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt.name }}
</QItemLabel>
<QItemLabel v-if="!scope.opt.id">
{{ scope.opt.nickname }}
</QItemLabel>
<QItemLabel caption v-else>
{{ scope.opt.nickname }}, {{ scope.opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</template>
<i18n>
es:
Responsible for approving invoices: Responsable de aprobar las facturas
</i18n>

View File

@ -67,7 +67,7 @@ const dialog = ref(null);
<QTooltip>{{ t('globals.add') }}</QTooltip> <QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog"> <QPopupProxy ref="dialog">
<OrderCatalogItemDialog <OrderCatalogItemDialog
:prices="item.prices" :item="item"
@added="() => dialog.hide()" @added="() => dialog.hide()"
/> />
</QPopupProxy> </QPopupProxy>

View File

@ -6,7 +6,7 @@ import { useColor } from 'src/composables/useColor';
import { getCssVar } from 'quasar'; import { getCssVar } from 'quasar';
const $props = defineProps({ const $props = defineProps({
workerId: { type: Number, required: true }, workerId: { type: [Number, undefined], default: null },
description: { type: String, default: null }, description: { type: String, default: null },
title: { type: String, default: null }, title: { type: String, default: null },
color: { type: String, default: null }, color: { type: String, default: null },
@ -38,7 +38,13 @@ watch(src, () => (showLetter.value = false));
<template v-if="showLetter"> <template v-if="showLetter">
{{ title.charAt(0) }} {{ title.charAt(0) }}
</template> </template>
<QImg v-else :src="src" spinner-color="white" @error="showLetter = true" /> <QImg
v-else-if="workerId"
:src="src"
spinner-color="white"
@error="showLetter = true"
/>
<QIcon v-else name="mood" size="xs" />
</QAvatar> </QAvatar>
<div class="description"> <div class="description">
<slot name="description" v-if="description"> <slot name="description" v-if="description">

View File

@ -61,6 +61,7 @@ const emit = defineEmits([
'update:modelValue', 'update:modelValue',
'refresh', 'refresh',
'clear', 'clear',
'search',
'init', 'init',
'remove', 'remove',
'setUserParams', 'setUserParams',
@ -74,8 +75,11 @@ const arrayData = useArrayData($props.dataKey, {
const route = useRoute(); const route = useRoute();
const store = arrayData.store; const store = arrayData.store;
const userParams = ref({}); const userParams = ref({});
defineExpose({ search, sanitizer, params: userParams });
onMounted(() => { onMounted(() => {
userParams.value = $props.modelValue ?? {}; if (!userParams.value) userParams.value = $props.modelValue ?? {};
emit('init', { params: userParams.value }); emit('init', { params: userParams.value });
}); });
@ -101,7 +105,8 @@ watch(
watch( watch(
() => arrayData.store.userParams, () => arrayData.store.userParams,
(val, oldValue) => (val || oldValue) && setUserParams(val) (val, oldValue) => (val || oldValue) && setUserParams(val),
{ immediate: true }
); );
watch( watch(
@ -197,7 +202,7 @@ const customTags = computed(() =>
async function remove(key) { async function remove(key) {
userParams.value[key] = undefined; userParams.value[key] = undefined;
search(); await search();
emit('remove', key); emit('remove', key);
emit('update:modelValue', userParams.value); emit('update:modelValue', userParams.value);
} }
@ -223,8 +228,6 @@ function sanitizer(params) {
} }
return params; return params;
} }
defineExpose({ search, sanitizer, userParams });
</script> </script>
<template> <template>
@ -272,6 +275,7 @@ defineExpose({ search, sanitizer, userParams });
:key="chip.label" :key="chip.label"
:removable="!unremovableParams?.includes(chip.label)" :removable="!unremovableParams?.includes(chip.label)"
@remove="remove(chip.label)" @remove="remove(chip.label)"
data-cy="vnFilterPanelChip"
> >
<slot name="tags" :tag="chip" :format-fn="formatValue"> <slot name="tags" :tag="chip" :format-fn="formatValue">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">

View File

@ -1,22 +1,28 @@
<script setup> <script setup>
import { reactive, useAttrs, onBeforeMount, capitalize } from 'vue'; import { ref, reactive, useAttrs, onBeforeMount, capitalize } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { parsePhone } from 'src/filters';
import useOpenURL from 'src/composables/useOpenURL';
const props = defineProps({ const props = defineProps({
phoneNumber: { type: [String, Number], default: null }, phoneNumber: { type: [String, Number], default: null },
channel: { type: Number, default: null }, channel: { type: Number, default: null },
country: { type: String, default: null },
}); });
const phone = ref(props.phoneNumber);
const config = reactive({ const config = reactive({
sip: { icon: 'phone', href: `sip:${props.phoneNumber}` }, sip: { icon: 'phone', href: `sip:${props.phoneNumber}` },
'say-simple': { 'say-simple': {
icon: 'vn:saysimple', icon: 'vn:saysimple',
href: null, url: null,
channel: props.channel, channel: props.channel,
}, },
}); });
const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip'; const type = Object.keys(config).find((key) => key in useAttrs()) || 'sip';
onBeforeMount(async () => { onBeforeMount(async () => {
if (!phone.value) return;
let { channel } = config[type]; let { channel } = config[type];
if (type === 'say-simple') { if (type === 'say-simple') {
@ -24,23 +30,28 @@ onBeforeMount(async () => {
.data; .data;
if (!channel) channel = defaultChannel; if (!channel) channel = defaultChannel;
phone.value = await parsePhone(props.phoneNumber, props.country.toLowerCase());
config[ config[
type type
].href = `${url}?customerIdentity=%2B${props.phoneNumber}&channelId=${channel}`; ].url = `${url}?customerIdentity=%2B${phone.value}&channelId=${channel}`;
} }
}); });
function handleClick() {
if (config[type].url) useOpenURL(config[type].url);
else if (config[type].href) window.location.href = config[type].href;
}
</script> </script>
<template> <template>
<QBtn <QBtn
v-if="phoneNumber" v-if="phone"
flat flat
round round
:icon="config[type].icon" :icon="config[type].icon"
size="sm" size="sm"
color="primary" color="primary"
padding="none" padding="none"
:href="config[type].href" @click.stop="handleClick"
@click.stop
> >
<QTooltip> <QTooltip>
{{ capitalize(type).replace('-', '') }} {{ capitalize(type).replace('-', '') }}

View File

@ -6,7 +6,6 @@ import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { toDateHourMin } from 'src/filters'; import { toDateHourMin } from 'src/filters';
import { useState } from 'src/composables/useState';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import VnUserLink from 'components/ui/VnUserLink.vue'; import VnUserLink from 'components/ui/VnUserLink.vue';
@ -26,9 +25,7 @@ const $props = defineProps({
}); });
const { t } = useI18n(); const { t } = useI18n();
const state = useState();
const quasar = useQuasar(); const quasar = useQuasar();
const currentUser = ref(state.getUser());
const newNote = reactive({ text: null, observationTypeFk: null }); const newNote = reactive({ text: null, observationTypeFk: null });
const observationTypes = ref([]); const observationTypes = ref([]);
const vnPaginateRef = ref(); const vnPaginateRef = ref();
@ -101,6 +98,7 @@ onBeforeRouteLeave((to, from, next) => {
@click="insert" @click="insert"
class="q-mb-xs" class="q-mb-xs"
dense dense
data-cy="saveNote"
/> />
</template> </template>
</VnInput> </VnInput>

View File

@ -132,10 +132,24 @@ const addFilter = async (filter, params) => {
async function fetch(params) { async function fetch(params) {
useArrayData(props.dataKey, params); useArrayData(props.dataKey, params);
arrayData.reset(['filter.skip', 'skip']); arrayData.reset(['filter.skip', 'skip', 'page']);
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false, updateRouter: mounted.value });
if (!store.hasMoreData) isLoading.value = false; return emitStoreData();
}
async function update(params) {
useArrayData(props.dataKey, params);
const { limit, skip } = store;
store.limit = limit + skip;
store.skip = 0;
await arrayData.fetch({ append: false });
store.limit = limit;
store.skip = skip;
return emitStoreData();
}
function emitStoreData() {
if (!store.hasMoreData) isLoading.value = false;
emit('onFetch', store.data); emit('onFetch', store.data);
return store.data; return store.data;
} }
@ -181,7 +195,7 @@ async function onLoad(index, done) {
done(isDone); done(isDone);
} }
defineExpose({ fetch, addFilter, paginate }); defineExpose({ fetch, update, addFilter, paginate });
</script> </script>
<template> <template>

View File

@ -1,11 +1,24 @@
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { getUrl } from './getUrl'; import { getUrl } from './getUrl';
import axios from 'axios';
import { exportFile } from 'quasar';
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); const token = getTokenMultimedia();
export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile', url) { export async function downloadFile(id, model = 'dms', urlPath = '/downloadFile', url) {
let appUrl = await getUrl('', 'lilium'); const appUrl = (await getUrl('', 'lilium')).replace('/#/', '');
appUrl = appUrl.replace('/#/', ''); const response = await axios.get(
window.open(url ?? `${appUrl}/api/${model}/${id}${urlPath}?access_token=${token}`); url ?? `${appUrl}/api/${model}/${id}${urlPath}?access_token=${token}`,
{ responseType: 'blob' }
);
const contentDisposition = response.headers['content-disposition'];
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
const filename =
matches != null && matches[1]
? matches[1].replace(/['"]/g, '')
: 'downloaded-file';
exportFile(filename, response.data);
} }

View File

@ -87,7 +87,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const params = { filter }; const params = { filter };
Object.assign(params, userParams); Object.assign(params, userParams);
params.filter.skip = store.skip; if (params.filter) params.filter.skip = store.skip;
if (store?.order && typeof store?.order == 'string') store.order = [store.order]; if (store?.order && typeof store?.order == 'string') store.order = [store.order];
if (store.order?.length) params.filter.order = [...store.order]; if (store.order?.length) params.filter.order = [...store.order];
else delete params.filter.order; else delete params.filter.order;

73
src/composables/useCau.js Normal file
View File

@ -0,0 +1,73 @@
import VnInput from 'src/components/common/VnInput.vue';
import { useVnConfirm } from 'src/composables/useVnConfirm';
import axios from 'axios';
import { ref } from 'vue';
import { i18n } from 'src/boot/i18n';
import useNotify from 'src/composables/useNotify.js';
export async function useCau(res, message) {
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const { config, headers, request, status, statusText, data } = res || {};
const { params, url, method, signal, headers: confHeaders } = config || {};
const { message: resMessage, code, name } = data?.error || {};
const additionalData = {
path: location.hash,
message: resMessage,
code,
request: request?.responseURL,
status,
name,
statusText: statusText,
config: {
url,
method,
params,
headers: confHeaders,
aborted: signal?.aborted,
version: headers?.['salix-version'],
},
};
const opts = {
actions: [
{
icon: 'support_agent',
color: 'primary',
dense: true,
flat: false,
round: true,
handler: async () => {
const locale = i18n.global.t;
const reason = ref(
code == 'ACCESS_DENIED' ? locale('cau.askPrivileges') : ''
);
openConfirmationModal(
locale('cau.title'),
locale('cau.subtitle'),
async () => {
await axios.post('OsTickets/send-to-support', {
reason: reason.value,
additionalData,
});
},
null,
{
component: VnInput,
props: {
modelValue: reason,
'onUpdate:modelValue': (val) => (reason.value = val),
label: locale('cau.inputLabel'),
class: 'full-width',
required: true,
autofocus: true,
},
}
);
},
},
],
};
notify(message ?? 'globals.error', 'negative', 'error', opts);
}

View File

@ -2,7 +2,7 @@ import { Notify } from 'quasar';
import { i18n } from 'src/boot/i18n'; import { i18n } from 'src/boot/i18n';
export default function useNotify() { export default function useNotify() {
const notify = (message, type, icon) => { const notify = (message, type, icon, opts = {}) => {
const defaultIcons = { const defaultIcons = {
warning: 'warning', warning: 'warning',
negative: 'error', negative: 'error',
@ -13,6 +13,7 @@ export default function useNotify() {
message: i18n.global.t(message), message: i18n.global.t(message),
type: type, type: type,
icon: icon ? icon : defaultIcons[type], icon: icon ? icon : defaultIcons[type],
...opts,
}); });
}; };

View File

@ -2,8 +2,14 @@ import { useValidator } from 'src/composables/useValidator';
export function useRequired($attrs) { export function useRequired($attrs) {
const { validations } = useValidator(); const { validations } = useValidator();
const hasRequired = Object.keys($attrs).includes('required');
const isRequired = Object.keys($attrs).includes('required'); let isRequired = false;
if (hasRequired) {
const required = $attrs['required'];
if (typeof required === 'boolean') {
isRequired = required;
}
}
const requiredFieldRule = (val) => validations().required(isRequired, val); const requiredFieldRule = (val) => validations().required(isRequired, val);
return { return {

View File

@ -20,7 +20,7 @@ export function useRole() {
function hasAny(roles) { function hasAny(roles) {
const roleStore = state.getRoles(); const roleStore = state.getRoles();
if (typeof roles === 'string') roles = [roles];
for (const role of roles) { for (const role of roles) {
if (roleStore.value.indexOf(role) !== -1) return true; if (roleStore.value.indexOf(role) !== -1) return true;
} }

View File

@ -1,20 +1,27 @@
import { h } from 'vue';
import { Dialog } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import { useQuasar } from 'quasar';
export function useVnConfirm() { export function useVnConfirm() {
const quasar = useQuasar(); const openConfirmationModal = (
title,
const openConfirmationModal = (title, message, promise, successFn) => { message,
quasar promise,
.dialog({ successFn,
component: VnConfirm, customHTML = {}
componentProps: { ) => {
const { component, props } = customHTML;
Dialog.create({
component: h(
VnConfirm,
{
title: title, title: title,
message: message, message: message,
promise: promise, promise: promise,
}, },
}) { customHTML: () => h(component, props) }
.onOk(async () => { ),
}).onOk(async () => {
if (successFn) successFn(); if (successFn) successFn();
}); });
}; };

View File

@ -0,0 +1,21 @@
export default function getDifferences(obj1, obj2) {
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (let key in obj1) {
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key];
}
}
for (let key in obj2) {
if (
obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])
) {
diff[key] = obj2[key];
}
}
return diff;
}

View File

@ -1,4 +1,3 @@
// parsing JSON safely
function parseJSON(str, fallback) { function parseJSON(str, fallback) {
try { try {
return JSON.parse(str ?? '{}'); return JSON.parse(str ?? '{}');

View File

@ -0,0 +1,6 @@
export default function getUpdatedValues(keys, formData) {
return keys.reduce((acc, key) => {
acc[key] = formData[key];
return acc;
}, {});
}

View File

@ -11,11 +11,17 @@ import dashIfEmpty from './dashIfEmpty';
import dateRange from './dateRange'; import dateRange from './dateRange';
import toHour from './toHour'; import toHour from './toHour';
import dashOrCurrency from './dashOrCurrency'; import dashOrCurrency from './dashOrCurrency';
import getDifferences from './getDifferences';
import getUpdatedValues from './getUpdatedValues';
import getParamWhere from './getParamWhere'; import getParamWhere from './getParamWhere';
import parsePhone from './parsePhone';
import isDialogOpened from './isDialogOpened'; import isDialogOpened from './isDialogOpened';
export { export {
getUpdatedValues,
getDifferences,
isDialogOpened, isDialogOpened,
parsePhone,
toLowerCase, toLowerCase,
toLowerCamel, toLowerCamel,
toDate, toDate,

18
src/filters/parsePhone.js Normal file
View File

@ -0,0 +1,18 @@
import axios from 'axios';
export default async function parsePhone(phone, country) {
if (!phone) return;
if (phone.startsWith('+')) return `${phone.slice(1)}`;
if (phone.startsWith('00')) return `${phone.slice(2)}`;
let prefix;
try {
prefix = (await axios.get(`Prefixes/${country.toLowerCase()}`)).data?.prefix;
} catch (e) {
prefix = (await axios.get('PbxConfigs/findOne')).data?.defaultPrefix;
}
prefix = prefix.replace(/^0+/, '');
if (phone.startsWith(prefix)) return phone;
return `${prefix}${phone}`;
}

View File

@ -60,7 +60,7 @@ globals:
reference: Reference reference: Reference
agency: Agency agency: Agency
warehouseOut: Warehouse Out warehouseOut: Warehouse Out
wareHouseIn: Warehouse In warehouseIn: Warehouse In
landed: Landed landed: Landed
shipped: Shipped shipped: Shipped
totalEntries: Total entries totalEntries: Total entries
@ -298,6 +298,7 @@ globals:
clientsActionsMonitor: Clients and actions clientsActionsMonitor: Clients and actions
serial: Serial serial: Serial
medical: Mutual medical: Mutual
pit: IRPF
RouteExtendedList: Router RouteExtendedList: Router
wasteRecalc: Waste recaclulate wasteRecalc: Waste recaclulate
operator: Operator operator: Operator
@ -330,6 +331,7 @@ globals:
fi: FI fi: FI
myTeam: My team myTeam: My team
departmentFk: Department departmentFk: Department
countryFk: Country
changePass: Change password changePass: Change password
deleteConfirmTitle: Delete selected elements deleteConfirmTitle: Delete selected elements
changeState: Change state changeState: Change state
@ -367,6 +369,11 @@ resetPassword:
repeatPassword: Repeat password repeatPassword: Repeat password
passwordNotMatch: Passwords don't match passwordNotMatch: Passwords don't match
passwordChanged: Password changed passwordChanged: Password changed
cau:
title: Send cau
subtitle: By sending this ticket, all the data related to the error, the section, the user, etc., are already sent.
inputLabel: Explain why this error should not appear
askPrivileges: Ask for privileges
entry: entry:
list: list:
newEntry: New entry newEntry: New entry
@ -396,8 +403,8 @@ entry:
buys: Buys buys: Buys
stickers: Stickers stickers: Stickers
package: Package package: Package
packing: Packing packing: Pack.
grouping: Grouping grouping: Group.
buyingValue: Buying value buyingValue: Buying value
import: Import import: Import
pvp: PVP pvp: PVP
@ -767,7 +774,8 @@ travel:
thermographs: Thermographs thermographs: Thermographs
hb: HB hb: HB
basicData: basicData:
daysInForward: Days in forward daysInForward: Automatic movement (Raid)
isRaid: Raid
thermographs: thermographs:
temperature: Temperature temperature: Temperature
destination: Destination destination: Destination

View File

@ -303,6 +303,7 @@ globals:
clientsActionsMonitor: Clientes y acciones clientsActionsMonitor: Clientes y acciones
serial: Facturas por serie serial: Facturas por serie
medical: Mutua medical: Mutua
pit: IRPF
wasteRecalc: Recalcular mermas wasteRecalc: Recalcular mermas
operator: Operario operator: Operario
parking: Parking parking: Parking
@ -334,6 +335,7 @@ globals:
SSN: NSS SSN: NSS
fi: NIF fi: NIF
myTeam: Mi equipo myTeam: Mi equipo
countryFk: País
changePass: Cambiar contraseña changePass: Cambiar contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado changeState: Cambiar estado
@ -369,6 +371,11 @@ resetPassword:
repeatPassword: Repetir contraseña repeatPassword: Repetir contraseña
passwordNotMatch: Las contraseñas no coinciden passwordNotMatch: Las contraseñas no coinciden
passwordChanged: Contraseña cambiada passwordChanged: Contraseña cambiada
cau:
title: Enviar cau
subtitle: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc
inputLabel: Explique el motivo por el que no deberia aparecer este fallo
askPrivileges: Solicitar permisos
entry: entry:
list: list:
newEntry: Nueva entrada newEntry: Nueva entrada
@ -399,8 +406,8 @@ entry:
buys: Compras buys: Compras
stickers: Etiquetas stickers: Etiquetas
package: Embalaje package: Embalaje
packing: Packing packing: Pack.
grouping: Grouping grouping: Group.
buyingValue: Coste buyingValue: Coste
import: Importe import: Importe
pvp: PVP pvp: PVP
@ -761,7 +768,8 @@ travel:
thermographs: Termógrafos thermographs: Termógrafos
hb: HB hb: HB
basicData: basicData:
daysInForward: Días redada daysInForward: Desplazamiento automatico (redada)
isRaid: Redada
thermographs: thermographs:
temperature: Temperatura temperature: Temperatura
destination: Destino destination: Destino

View File

@ -1,50 +1,10 @@
<script setup> <script setup>
import { useQuasar } from 'quasar';
import Navbar from 'src/components/NavBar.vue'; import Navbar from 'src/components/NavBar.vue';
import { useRouter } from 'vue-router';
import routes from 'src/router/modules';
import { onMounted } from 'vue';
const quasar = useQuasar();
onMounted(() => {
let isNotified = false;
const router = useRouter();
const keyBindingMap = routes
.filter((route) => route.meta.keyBinding)
.reduce((map, route) => {
map['Key' + route.meta.keyBinding.toUpperCase()] = route.path;
return map;
}, {});
const handleKeyDown = (event) => {
const { ctrlKey, altKey, code } = event;
if (ctrlKey && altKey && keyBindingMap[code] && !isNotified) {
event.preventDefault();
router.push(keyBindingMap[code]);
isNotified = true;
}
};
const handleKeyUp = (event) => {
const { ctrlKey, altKey } = event;
if (!ctrlKey || !altKey) {
isNotified = false;
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
});
</script> </script>
<template> <template>
<QLayout view="hHh LpR fFf" v-shortcut> <QLayout view="hHh LpR fFf" v-shortcut>
<Navbar /> <Navbar />
<RouterView></RouterView> <RouterView></RouterView>
<QFooter v-if="quasar.platform.is.mobile"></QFooter> <QFooter v-if="$q.platform.is.mobile"></QFooter>
</QLayout> </QLayout>
</template> </template>

View File

@ -11,21 +11,13 @@ const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
const onSynchronizeAll = async () => { const onSynchronizeAll = async () => {
try {
notify(t('Synchronizing in the background'), 'positive'); notify(t('Synchronizing in the background'), 'positive');
await axios.patch(`Accounts/syncAll`); await axios.patch(`Accounts/syncAll`);
} catch (error) {
console.error('Error synchronizing all accounts', error);
}
}; };
const onSynchronizeRoles = async () => { const onSynchronizeRoles = async () => {
try {
await axios.patch(`RoleInherits/sync`); await axios.patch(`RoleInherits/sync`);
notify(t('Roles synchronized!'), 'positive'); notify(t('Roles synchronized!'), 'positive');
} catch (error) {
console.error('Error synchronizing roles', error);
}
}; };
</script> </script>

View File

@ -111,7 +111,6 @@ const columns = computed(() => [
}, },
]); ]);
const deleteAcl = async ({ id }) => { const deleteAcl = async ({ id }) => {
try {
await new Promise((resolve) => { await new Promise((resolve) => {
quasar quasar
.dialog({ .dialog({
@ -131,9 +130,6 @@ const deleteAcl = async ({ id }) => {
await axios.delete(`ACLs/${id}`); await axios.delete(`ACLs/${id}`);
tableRef.value.reload(); tableRef.value.reload();
notify('ACL removed', 'positive'); notify('ACL removed', 'positive');
} catch (error) {
console.error('Error deleting Acl: ', error);
}
}; };
</script> </script>

View File

@ -34,13 +34,9 @@ const refresh = () => paginateRef.value.fetch();
const navigate = (id) => router.push({ name: 'AccountSummary', params: { id } }); const navigate = (id) => router.push({ name: 'AccountSummary', params: { id } });
const killSession = async ({ userId, created }) => { const killSession = async ({ userId, created }) => {
try {
await axios.post(`${urlPath}/killSession`, { userId, created }); await axios.post(`${urlPath}/killSession`, { userId, created });
paginateRef.value.fetch(); paginateRef.value.fetch();
notify(t('Session killed'), 'positive'); notify(t('Session killed'), 'positive');
} catch (error) {
console.error('Error killing session', error);
}
}; };
</script> </script>

View File

@ -31,7 +31,6 @@ const rolesOptions = ref([]);
<VnFilterPanel <VnFilterPanel
:data-key="props.dataKey" :data-key="props.dataKey"
:search-button="true" :search-button="true"
:hidden-tags="['search']"
:redirect="false" :redirect="false"
search-url="table" search-url="table"
> >

View File

@ -40,12 +40,8 @@ const formUrlCreate = ref(null);
const formUrlUpdate = ref(null); const formUrlUpdate = ref(null);
const formCustomFn = ref(null); const formCustomFn = ref(null);
const onTestConection = async () => { const onTestConection = async () => {
try {
await axios.get(`LdapConfigs/test`); await axios.get(`LdapConfigs/test`);
notify(t('LDAP connection established!'), 'positive'); notify(t('LDAP connection established!'), 'positive');
} catch (error) {
console.error('Error testing connection', error);
}
}; };
const getInitialLdapConfig = async () => { const getInitialLdapConfig = async () => {
try { try {
@ -72,14 +68,10 @@ const getInitialLdapConfig = async () => {
} }
}; };
const deleteMailForward = async () => { const deleteMailForward = async () => {
try {
await axios.delete(URL_UPDATE); await axios.delete(URL_UPDATE);
initialData.value = { ...DEFAULT_DATA }; initialData.value = { ...DEFAULT_DATA };
hasData.value = false; hasData.value = false;
notify(t('globals.dataSaved'), 'positive'); notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
}; };
onMounted(async () => await getInitialLdapConfig()); onMounted(async () => await getInitialLdapConfig());

View File

@ -7,6 +7,7 @@ import AccountSummary from './Card/AccountSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import AccountFilter from './AccountFilter.vue'; import AccountFilter from './AccountFilter.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const tableRef = ref(); const tableRef = ref();
@ -22,10 +23,27 @@ const columns = computed(() => [
field: 'id', field: 'id',
cardVisible: true, cardVisible: true,
}, },
{
align: 'left',
name: 'name',
label: t('Name'),
component: 'input',
columnField: {
component: null,
},
cardVisible: true,
create: true,
},
{ {
align: 'left', align: 'left',
name: 'roleFk', name: 'roleFk',
label: t('role'), label: t('Role'),
component: 'select',
attrs: {
url: 'VnRoles',
optionValue: 'id',
optionLabel: 'name',
},
columnFilter: { columnFilter: {
component: 'select', component: 'select',
name: 'roleFk', name: 'roleFk',
@ -35,7 +53,11 @@ const columns = computed(() => [
optionLabel: 'name', optionLabel: 'name',
}, },
}, },
columnField: {
component: null,
},
format: ({ role }, dashIfEmpty) => dashIfEmpty(role?.name), format: ({ role }, dashIfEmpty) => dashIfEmpty(role?.name),
create: true,
}, },
{ {
align: 'left', align: 'left',
@ -51,20 +73,32 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
name: 'name', name: 'email',
label: t('Name'), label: t('Email'),
component: 'input', component: 'input',
columnField: { columnField: {
component: null, component: null,
}, },
cardVisible: true,
create: true, create: true,
visible: false,
}, },
{ {
align: 'left', align: 'left',
name: 'email', name: 'password',
label: t('email'), label: t('Password'),
component: 'input', columnField: {
component: null,
},
attrs: {},
required: true,
visible: false,
},
{
align: 'left',
name: 'active',
label: t('Active'),
component: 'checkbox',
create: true, create: true,
visible: false, visible: false,
}, },
@ -101,7 +135,6 @@ const exprBuilder = (param, value) => {
} }
}; };
</script> </script>
<template> <template>
<VnSearchbar <VnSearchbar
data-key="AccountList" data-key="AccountList"
@ -119,6 +152,12 @@ const exprBuilder = (param, value) => {
ref="tableRef" ref="tableRef"
data-key="AccountList" data-key="AccountList"
url="VnUsers/preview" url="VnUsers/preview"
:create="{
urlCreate: 'VnUsers',
title: t('Create user'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {},
}"
:filter="filter" :filter="filter"
order="id DESC" order="id DESC"
:columns="columns" :columns="columns"
@ -127,7 +166,19 @@ const exprBuilder = (param, value) => {
:use-model="true" :use-model="true"
:right-search="false" :right-search="false"
auto-load auto-load
>
<template #more-create-dialog="{ data }">
<QCardSection>
<VnInput
:label="t('Password')"
v-model="data.password"
type="password"
:required="true"
autocomplete="new-password"
/> />
</QCardSection>
</template>
</VnTable>
</template> </template>
<i18n> <i18n>
@ -135,4 +186,7 @@ const exprBuilder = (param, value) => {
Id: Id Id: Id
Nickname: Nickname Nickname: Nickname
Name: Nombre Name: Nombre
Password: Contraseña
Active: Activo
Role: Rol
</i18n> </i18n>

View File

@ -46,12 +46,8 @@ const formUrlUpdate = ref(null);
const formCustomFn = ref(null); const formCustomFn = ref(null);
const onTestConection = async () => { const onTestConection = async () => {
try {
await axios.get(`SambaConfigs/test`); await axios.get(`SambaConfigs/test`);
notify(t('Samba connection established!'), 'positive'); notify(t('Samba connection established!'), 'positive');
} catch (error) {
console.error('Error testing connection', error);
}
}; };
const getInitialSambaConfig = async () => { const getInitialSambaConfig = async () => {
@ -79,14 +75,10 @@ const getInitialSambaConfig = async () => {
}; };
const deleteMailForward = async () => { const deleteMailForward = async () => {
try {
await axios.delete(URL_UPDATE); await axios.delete(URL_UPDATE);
initialData.value = { ...DEFAULT_DATA }; initialData.value = { ...DEFAULT_DATA };
hasData.value = false; hasData.value = false;
notify(t('globals.dataSaved'), 'positive'); notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
}; };
onMounted(async () => await getInitialSambaConfig()); onMounted(async () => await getInitialSambaConfig());

View File

@ -37,11 +37,7 @@ onBeforeMount(() => {
@on-fetch="(data) => (rolesOptions = data)" @on-fetch="(data) => (rolesOptions = data)"
auto-load auto-load
/> />
<VnFilterPanel <VnFilterPanel :data-key="props.dataKey" :search-button="true">
:data-key="props.dataKey"
:search-button="true"
:hidden-tags="['search']"
>
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`acls.aclFilter.${tag.label}`) }}: </strong> <strong>{{ t(`acls.aclFilter.${tag.label}`) }}: </strong>

View File

@ -44,13 +44,9 @@ const removeAlias = () => {
cancel: true, cancel: true,
}) })
.onOk(async () => { .onOk(async () => {
try {
await axios.delete(`MailAliases/${entityId.value}`); await axios.delete(`MailAliases/${entityId.value}`);
notify(t('Alias removed'), 'positive'); notify(t('Alias removed'), 'positive');
router.push({ name: 'AccountAlias' }); router.push({ name: 'AccountAlias' });
} catch (err) {
console.error('Error removing alias');
}
}); });
}; };
</script> </script>

View File

@ -8,7 +8,7 @@ import { useAcl } from 'src/composables/useAcl';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import VnConfirm from 'src/components/ui/VnConfirm.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue';
import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import VnChangePassword from 'src/components/common/VnChangePassword.vue';
import useNotify from 'src/composables/useNotify.js'; import { useQuasar } from 'quasar';
const $props = defineProps({ const $props = defineProps({
hasAccount: { hasAccount: {
@ -21,7 +21,7 @@ const { t } = useI18n();
const { hasAccount } = toRefs($props); const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const route = useRoute(); const route = useRoute();
const { notify } = useNotify(); const { notify } = useQuasar();
const account = computed(() => useArrayData('AccountId').store.data[0]); const account = computed(() => useArrayData('AccountId').store.data[0]);
account.value.hasAccount = hasAccount.value; account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id); const entityId = computed(() => +route.params.id);

View File

@ -44,32 +44,23 @@ const fetchMailForwards = async () => {
try { try {
const response = await axios.get(`MailForwards/${route.params.id}`); const response = await axios.get(`MailForwards/${route.params.id}`);
return response.data; return response.data;
} catch (err) { } catch {
console.error('Error fetching mail forwards', err);
return null; return null;
} }
}; };
const deleteMailForward = async () => { const deleteMailForward = async () => {
try {
await axios.delete(`MailForwards/${route.params.id}`); await axios.delete(`MailForwards/${route.params.id}`);
formData.value.forwardTo = null; formData.value.forwardTo = null;
initialData.value.forwardTo = null; initialData.value.forwardTo = null;
initialData.value.hasData = hasData.value; initialData.value.hasData = hasData.value;
notify(t('globals.dataSaved'), 'positive'); notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
}; };
const updateMailForward = async () => { const updateMailForward = async () => {
try {
await axios.patch('MailForwards', formData.value); await axios.patch('MailForwards', formData.value);
initialData.value = { ...formData.value }; initialData.value = { ...formData.value };
initialData.value.hasData = hasData.value; initialData.value.hasData = hasData.value;
} catch (err) {
console.error('Error creating mail forward', err);
}
}; };
const onSubmit = async () => { const onSubmit = async () => {

View File

@ -13,12 +13,7 @@ const props = defineProps({
</script> </script>
<template> <template>
<VnFilterPanel <VnFilterPanel :data-key="props.dataKey" :search-button="true" :redirect="false">
:data-key="props.dataKey"
:search-button="true"
:hidden-tags="['search']"
:redirect="false"
>
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`role.${tag.label}`) }}: </strong> <strong>{{ t(`role.${tag.label}`) }}: </strong>

View File

@ -32,12 +32,8 @@ const filter = {
where: { id: entityId }, where: { id: entityId },
}; };
const removeRole = async () => { const removeRole = async () => {
try {
await axios.delete(`VnRoles/${entityId.value}`); await axios.delete(`VnRoles/${entityId.value}`);
notify(t('Role removed'), 'positive'); notify(t('Role removed'), 'positive');
} catch (error) {
console.error('Error deleting role', error);
}
}; };
</script> </script>

View File

@ -95,6 +95,7 @@ const columns = computed(() => [
optionLabel: 'description', optionLabel: 'description',
}, },
}, },
orderBy: 'priority',
}, },
{ {
align: 'right', align: 'right',

View File

@ -2,6 +2,7 @@
import { onBeforeMount, ref, watch } from 'vue'; import { onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import axios from 'axios'; import axios from 'axios';
@ -52,7 +53,6 @@ const addressFilter = {
onBeforeMount(() => { onBeforeMount(() => {
const { id } = route.params; const { id } = route.params;
getAddressesData(id);
getClientData(id); getClientData(id);
}); });
@ -60,23 +60,10 @@ watch(
() => route.params.id, () => route.params.id,
(newValue) => { (newValue) => {
if (!newValue) return; if (!newValue) return;
getAddressesData(newValue);
getClientData(newValue); getClientData(newValue);
} }
); );
const getAddressesData = async (id) => {
try {
const { data } = await axios.get(`Clients/${id}/addresses`, {
params: { filter: JSON.stringify(addressFilter) },
});
addresses.value = data;
sortAddresses();
} catch (error) {
return error;
}
};
const getClientData = async (id) => { const getClientData = async (id) => {
try { try {
const { data } = await axios.get(`Clients/${id}`); const { data } = await axios.get(`Clients/${id}`);
@ -101,9 +88,9 @@ const setDefault = (address) => {
}); });
}; };
const sortAddresses = () => { const sortAddresses = (data) => {
if (!client.value || !addresses.value) return; if (!client.value || !data) return;
addresses.value = addresses.value.sort((a, b) => { addresses.value = data.sort((a, b) => {
return isDefaultAddress(b) - isDefaultAddress(a); return isDefaultAddress(b) - isDefaultAddress(a);
}); });
}; };
@ -124,8 +111,17 @@ const toCustomerAddressEdit = (addressId) => {
</script> </script>
<template> <template>
<FetchData
@on-fetch="sortAddresses"
auto-load
data-key="CustomerAddresses"
order="id DESC"
ref="vnPaginateRef"
:filter="addressFilter"
:url="`Clients/${route.params.id}/addresses`"
/>
<div class="full-width flex justify-center"> <div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg" v-if="addresses.length"> <QCard class="card-width q-pa-lg">
<QCardSection> <QCardSection>
<div <div
v-for="(item, index) in addresses" v-for="(item, index) in addresses"
@ -167,7 +163,7 @@ const toCustomerAddressEdit = (addressId) => {
<div>{{ item.street }}</div> <div>{{ item.street }}</div>
<div> <div>
{{ item.postalCode }} - {{ item.city }}, {{ item.postalCode }} - {{ item.city }},
{{ item.province.name }} {{ item.province?.name }}
</div> </div>
<div> <div>
{{ item.phone }} {{ item.phone }}

View File

@ -8,14 +8,14 @@ import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue'; import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
import { getDifferences, getUpdatedValues } from 'src/filters';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const businessTypes = ref([]); const businessTypes = ref([]);
const contactChannels = ref([]); const contactChannels = ref([]);
const title = ref();
const handleSalesModelValue = (val) => ({ const handleSalesModelValue = (val) => ({
or: [ or: [
{ id: val }, { id: val },
@ -30,6 +30,13 @@ const exprBuilder = (param, value) => {
and: [{ active: { neq: false } }, handleSalesModelValue(value)], and: [{ active: { neq: false } }, handleSalesModelValue(value)],
}; };
}; };
function onBeforeSave(formData, originalData) {
return getUpdatedValues(
Object.keys(getDifferences(formData, originalData)),
formData
);
}
</script> </script>
<template> <template>
<FetchData <FetchData
@ -43,7 +50,12 @@ const exprBuilder = (param, value) => {
@on-fetch="(data) => (businessTypes = data)" @on-fetch="(data) => (businessTypes = data)"
auto-load auto-load
/> />
<FormModel :url="`Clients/${route.params.id}`" auto-load model="customer"> <FormModel
:url="`Clients/${route.params.id}`"
auto-load
model="customer"
:mapper="onBeforeSave"
>
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow> <VnRow>
<VnInput <VnInput
@ -94,6 +106,7 @@ const exprBuilder = (param, value) => {
:rules="validate('client.phone')" :rules="validate('client.phone')"
clearable clearable
v-model="data.phone" v-model="data.phone"
data-cy="customerPhone"
/> />
<VnInput <VnInput
:label="t('customer.summary.mobile')" :label="t('customer.summary.mobile')"
@ -103,41 +116,17 @@ const exprBuilder = (param, value) => {
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelect <VnSelectWorker
url="Workers/search"
v-model="data.salesPersonFk"
:label="t('customer.summary.salesPerson')" :label="t('customer.summary.salesPerson')"
v-model="data.salesPersonFk"
:params="{ :params="{
departmentCodes: ['VT', 'shopping'], departmentCodes: ['VT', 'shopping'],
}" }"
:fields="['id', 'nickname']" :has-avatar="true"
sort-by="nickname ASC"
option-label="nickname"
option-value="id"
:rules="validate('client.salesPersonFk')" :rules="validate('client.salesPersonFk')"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
emit-value emit-value
auto-load
>
<template #prepend>
<VnAvatar
:worker-id="data.salesPersonFk"
color="primary"
:title="title"
/> />
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt?.nickname }},
{{ scope.opt?.code }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect <VnSelect
v-model="data.contactChannelFk" v-model="data.contactChannelFk"
:options="contactChannels" :options="contactChannels"
@ -155,7 +144,6 @@ const exprBuilder = (param, value) => {
url="Clients" url="Clients"
:input-debounce="0" :input-debounce="0"
:label="t('customer.basicData.previousClient')" :label="t('customer.basicData.previousClient')"
:options="clients"
:rules="validate('client.transferorFk')" :rules="validate('client.transferorFk')"
emit-value emit-value
map-options map-options

View File

@ -28,12 +28,7 @@ const getBankEntities = (data, formData) => {
</script> </script>
<template> <template>
<FormModel <FormModel :url-update="`Clients/${route.params.id}`" auto-load model="customer">
:url-update="`Clients/${route.params.id}`"
:url="`Clients/${route.params.id}/getCard`"
auto-load
model="customer"
>
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow> <VnRow>
<VnSelect <VnSelect

View File

@ -93,22 +93,6 @@ const columns = computed(() => [
<WorkerDescriptorProxy :id="row.worker.id" /> <WorkerDescriptorProxy :id="row.worker.id" />
</template> </template>
</VnTable> </VnTable>
<!-- <QTable
:columns="columns"
:pagination="{ rowsPerPage: 0 }"
:rows="rows"
hide-bottom
row-key="id"
v-model:selected="selected"
class="card-width q-px-lg"
>
<template #body-cell-employee="{ row }">
<QTd @click.stop>
<span class="link">{{ row.worker.user.nickname }}</span>
<WorkerDescriptorProxy :id="row.clientFk" />
</QTd>
</template>
</QTable> -->
</template> </template>
<i18n> <i18n>

View File

@ -36,7 +36,10 @@ const entityId = computed(() => {
}); });
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity?.name, entity?.id)); const setData = (entity) => {
data.value = useCardDescription(entity?.name, entity?.id);
if (customer.value) customer.value.webAccess = data.value?.account?.isActive;
};
const debtWarning = computed(() => { const debtWarning = computed(() => {
return customer.value?.debt > customer.value?.credit ? 'negative' : 'primary'; return customer.value?.debt > customer.value?.credit ? 'negative' : 'primary';
}); });

View File

@ -34,7 +34,6 @@ function handleLocation(data, location) {
/> />
<FormModel <FormModel
:url-update="`Clients/${route.params.id}/updateFiscalData`" :url-update="`Clients/${route.params.id}/updateFiscalData`"
:url="`Clients/${route.params.id}/getCard`"
auto-load auto-load
model="customer" model="customer"
> >
@ -68,6 +67,7 @@ function handleLocation(data, location) {
option-label="vat" option-label="vat"
option-value="id" option-value="id"
v-model="data.sageTaxTypeFk" v-model="data.sageTaxTypeFk"
data-cy="sageTaxTypeFk"
:required="data.isTaxDataChecked" :required="data.isTaxDataChecked"
/> />
<VnSelect <VnSelect
@ -76,6 +76,7 @@ function handleLocation(data, location) {
hide-selected hide-selected
option-label="transaction" option-label="transaction"
option-value="id" option-value="id"
data-cy="sageTransactionTypeFk"
v-model="data.sageTransactionTypeFk" v-model="data.sageTransactionTypeFk"
:required="data.isTaxDataChecked" :required="data.isTaxDataChecked"
> >

View File

@ -1,12 +1,11 @@
<script setup> <script setup>
import { computed, ref, onMounted } from 'vue'; import { computed, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { toCurrency, toPercentage, toDate, dashOrCurrency } from 'src/filters'; import { toCurrency, toPercentage, toDate, dashOrCurrency } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import { getUrl } from 'src/composables/getUrl';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import VnLinkMail from 'src/components/ui/VnLinkMail.vue'; import VnLinkMail from 'src/components/ui/VnLinkMail.vue';
@ -95,13 +94,14 @@ const sumRisk = ({ clientRisks }) => {
:phone-number="entity.mobile" :phone-number="entity.mobile"
:channel="entity.country?.saySimpleCountry?.channel" :channel="entity.country?.saySimpleCountry?.channel"
class="q-ml-xs" class="q-ml-xs"
:country="entity.country?.code"
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :value="entity.email" copy <VnLv :value="entity.email" copy
><template #label> ><template #label>
{{ t('globals.params.email') }} {{ t('globals.params.email') }}
<VnLinkMail email="entity.email"></VnLinkMail> </template <VnLinkMail :email="entity.email"></VnLinkMail> </template
></VnLv> ></VnLv>
<VnLv <VnLv
:label="t('customer.summary.salesPerson')" :label="t('customer.summary.salesPerson')"

View File

@ -1,164 +1,82 @@
<script setup> <script setup>
import { computed, onBeforeMount, ref, watch, nextTick } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'src/components/common/VnInput.vue'; import FormModel from 'components/FormModel.vue';
import FetchData from 'components/FetchData.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios'; const formModelRef = ref(false);
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const { notify } = useNotify();
const stateStore = useStateStore();
const amountInputRef = ref(null); const amountInputRef = ref(null);
const initialDated = Date.vnNew();
const unpaidClient = ref(false);
const isLoading = ref(false);
const amount = ref(null);
const dated = ref(initialDated);
const initialData = ref({ const initialData = ref({
dated: initialDated, dated: Date.vnNew(),
amount: null,
}); });
const hasChanged = computed(() => { const filterClientFindOne = {
return ( fields: ['unpaid', 'dated', 'amount'],
initialData.value.dated !== dated.value || where: {
initialData.value.amount !== amount.value
);
});
onBeforeMount(() => {
getData(route.params.id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getData(newValue);
}
);
const getData = async (id) => {
const filter = { where: { clientFk: id } };
try {
const { data } = await axios.get('ClientUnpaids', {
params: { filter: JSON.stringify(filter) },
});
if (data.length) {
setValues(data[0]);
} else {
defaultValues();
}
} catch (error) {
defaultValues();
}
};
const setValues = (data) => {
unpaidClient.value = true;
amount.value = data.amount;
dated.value = data.dated;
initialData.value = data;
};
const defaultValues = () => {
unpaidClient.value = false;
initialData.value.amount = null;
setInitialData();
};
const setInitialData = () => {
amount.value = initialData.value.amount;
dated.value = initialData.value.dated;
};
const onSubmit = async () => {
isLoading.value = true;
const payload = {
amount: amount.value,
clientFk: route.params.id, clientFk: route.params.id,
dated: dated.value, },
}; };
try {
await axios.patch('ClientUnpaids', payload);
notify('globals.dataSaved', 'positive');
unpaidClient.value = true;
} catch (error) {
notify('errors.writeRequest', 'negative');
} finally {
isLoading.value = false;
}
};
watch(
() => unpaidClient.value,
async (val) => {
await nextTick();
if (val) amountInputRef.value.focus();
}
);
</script> </script>
<template> <template>
<Teleport v-if="stateStore?.isSubToolbarShown()" to="#st-actions"> <FetchData
<QBtnGroup push class="q-gutter-x-sm"> :filter="filterClientFindOne"
<QBtn auto-load
:disabled="!hasChanged" url="ClientUnpaids"
:label="t('globals.reset')" @on-fetch="
:loading="isLoading" (data) => {
@click="setInitialData" const unpaid = data.length == 1;
color="primary" initialData = { ...data[0], unpaid };
flat }
icon="restart_alt" "
type="reset"
/> />
<QBtn <QCard>
:disabled="!hasChanged" <FormModel
:label="t('globals.save')" v-if="'unpaid' in initialData"
:loading="isLoading" :observe-form-changes="false"
@click="onSubmit" ref="formModelRef"
color="primary" model="unpaid"
icon="save" url-update="ClientUnpaids"
/> :mapper="(formData) => ({ ...formData, clientFk: route.params.id })"
</QBtnGroup> :form-initial-data="initialData"
</Teleport> >
<template #form="{ data }">
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QForm>
<VnRow> <VnRow>
<div class="col"> <QCheckbox :label="t('Unpaid client')" v-model="data.unpaid" />
<QCheckbox :label="t('Unpaid client')" v-model="unpaidClient" />
</div>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md" v-show="unpaidClient"> <VnRow class="row q-gutter-md q-mb-md" v-show="data.unpaid">
<div class="col"> <div class="col">
<VnInputDate :label="t('Date')" v-model="dated" /> <VnInputDate
data-cy="customerUnpaidDate"
:label="t('Date')"
v-model="data.dated"
/>
</div> </div>
<div class="col"> <div class="col">
<VnInput <VnInputNumber
data-cy="customerUnpaidAmount"
ref="amountInputRef" ref="amountInputRef"
:label="t('Amount')" :label="t('Amount')"
clearable clearable
type="number" v-model="data.amount"
v-model="amount"
autofocus autofocus
> >
<template #append></template></VnInput <template #append></template></VnInputNumber
> >
</div> </div>
</VnRow> </VnRow>
</QForm> </template>
</FormModel>
</QCard> </QCard>
</div>
</template> </template>
<i18n> <i18n>

View File

@ -25,12 +25,12 @@ async function hasCustomerRole() {
</script> </script>
<template> <template>
<FormModel <FormModel
url="VnUsers/preview"
:url-update="`Clients/${route.params.id}/updateUser`" :url-update="`Clients/${route.params.id}/updateUser`"
:filter="filter" :filter="filter"
model="webAccess" model="customer"
:mapper=" :mapper="
({ active, name, email }) => { ({ account }) => {
const { name, email, active } = account;
return { return {
active, active,
name, name,
@ -42,14 +42,14 @@ async function hasCustomerRole() {
auto-load auto-load
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<QCheckbox :label="t('Enable web access')" v-model="data.active" /> <QCheckbox :label="t('Enable web access')" v-model="data.account.active" />
<VnInput :label="t('User')" clearable v-model="data.name" /> <VnInput :label="t('User')" clearable v-model="data.account.name" />
<VnInput <VnInput
:label="t('Recovery email')" :label="t('Recovery email')"
:rules="validate('client.email')" :rules="validate('client.email')"
clearable clearable
type="email" type="email"
v-model="data.email" v-model="data.account.email"
class="q-mt-sm" class="q-mt-sm"
:info="t('This email is used for user to regain access their account')" :info="t('This email is used for user to regain access their account')"
/> />

View File

@ -1,9 +1,9 @@
<script setup> <script setup>
import { computed, onBeforeMount, ref, watch } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import FetchData from 'src/components/FetchData.vue';
import { toCurrency, toDateHourMin } from 'src/filters'; import { toCurrency, toDateHourMin } from 'src/filters';
@ -20,10 +20,11 @@ const filter = {
{ relation: 'mandateType', scope: { fields: ['id', 'name'] } }, { relation: 'mandateType', scope: { fields: ['id', 'name'] } },
{ relation: 'company', scope: { fields: ['id', 'code'] } }, { relation: 'company', scope: { fields: ['id', 'code'] } },
], ],
where: { clientFk: null }, where: { clientFk: route.params.id },
order: ['created DESC'], order: ['created DESC'],
limit: 20, limit: 20,
}; };
const ClientDmsRef = ref(false);
const tableColumnComponents = { const tableColumnComponents = {
state: { state: {
@ -50,7 +51,7 @@ const tableColumnComponents = {
component: CustomerCheckIconTooltip, component: CustomerCheckIconTooltip,
props: ({ row }) => ({ props: ({ row }) => ({
transaction: row, transaction: row,
promise: refreshData, promise: () => ClientDmsRef.value.fetch(),
}), }),
event: () => {}, event: () => {},
}, },
@ -89,38 +90,16 @@ const columns = computed(() => [
name: 'validate', name: 'validate',
}, },
]); ]);
onBeforeMount(() => {
getData(route.params.id);
});
watch(
() => route.params.id,
(newValue) => {
if (!newValue) return;
getData(newValue);
}
);
const getData = async (id) => {
filter.where.clientFk = id;
try {
const { data } = await axios.get('clients/transactions', {
params: { filter: JSON.stringify(filter) },
});
rows.value = data;
} catch (error) {
return error;
}
};
const refreshData = () => {
getData(route.params.id);
};
</script> </script>
<template> <template>
<div class="full-width flex justify-center"> <FetchData
ref="ClientDmsRef"
:filter="filter"
@on-fetch="(data) => (rows = data)"
auto-load
url="Clients/transactions"
/>
<QPage class="card-width q-pa-lg"> <QPage class="card-width q-pa-lg">
<QTable <QTable
:columns="columns" :columns="columns"
@ -135,13 +114,9 @@ const refreshData = () => {
<QTr :props="props"> <QTr :props="props">
<component <component
:is="tableColumnComponents[props.col.name].component" :is="tableColumnComponents[props.col.name].component"
@click=" @click="tableColumnComponents[props.col.name].event(props)"
tableColumnComponents[props.col.name].event(props)
"
class="rounded-borders q-pa-sm" class="rounded-borders q-pa-sm"
v-bind=" v-bind="tableColumnComponents[props.col.name].props(props)"
tableColumnComponents[props.col.name].props(props)
"
> >
{{ props.value }} {{ props.value }}
</component> </component>
@ -154,7 +129,6 @@ const refreshData = () => {
{{ t('globals.noResults') }} {{ t('globals.noResults') }}
</h5> </h5>
</QPage> </QPage>
</div>
</template> </template>
<i18n> <i18n>

View File

@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
const { t } = useI18n(); const { t } = useI18n();
defineProps({ defineProps({
@ -65,19 +66,14 @@ const exprBuilder = (param, value) => {
</QItem> </QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnSelect <VnSelectWorker
url="Workers/search" :label="t('Salesperson')"
v-model="params.salesPersonFk"
:params="{ :params="{
departmentCodes: ['VT'], departmentCodes: ['VT'],
}" }"
auto-load
:label="t('Salesperson')"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
v-model="params.salesPersonFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
option-value="id"
option-label="name"
sort-by="nickname ASC"
emit-value emit-value
map-options map-options
use-input use-input
@ -86,23 +82,12 @@ const exprBuilder = (param, value) => {
outlined outlined
rounded rounded
:input-debounce="0" :input-debounce="0"
> />
<template #option="{ itemProps, opt }">
<QItem v-bind="itemProps">
<QItemSection>
<QItemLabel>{{ opt.name }}</QItemLabel>
<QItemLabel caption>
{{ opt.nickname }},{{ opt.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template></VnSelect
>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection <QItemSection>
><VnSelect <VnSelect
url="Provinces" url="Provinces"
:label="t('Province')" :label="t('Province')"
v-model="params.provinceFk" v-model="params.provinceFk"
@ -120,14 +105,12 @@ const exprBuilder = (param, value) => {
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-md"> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnInput :label="t('City')" v-model="params.city" is-outlined /> <VnInput :label="t('City')" v-model="params.city" is-outlined />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QSeparator /> <QItem class="q-mb-sm">
<QExpansionItem :label="t('More options')" expand-separator>
<QItem>
<QItemSection> <QItemSection>
<VnInput :label="t('Phone')" v-model="params.phone" is-outlined> <VnInput :label="t('Phone')" v-model="params.phone" is-outlined>
<template #prepend> <template #prepend>
@ -136,7 +119,7 @@ const exprBuilder = (param, value) => {
</VnInput> </VnInput>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnInput :label="t('Email')" v-model="params.email" is-outlined> <VnInput :label="t('Email')" v-model="params.email" is-outlined>
<template #prepend> <template #prepend>
@ -145,7 +128,8 @@ const exprBuilder = (param, value) => {
</VnInput> </VnInput>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem class="q-mb-sm">
<QItemSection>
<VnSelect <VnSelect
url="Zones" url="Zones"
:label="t('Zone')" :label="t('Zone')"
@ -160,9 +144,9 @@ const exprBuilder = (param, value) => {
outlined outlined
rounded rounded
auto-load auto-load
/> /></QItemSection>
</QItem> </QItem>
<QItem> <QItem class="q-mb-sm">
<QItemSection> <QItemSection>
<VnInput <VnInput
:label="t('Postcode')" :label="t('Postcode')"
@ -171,7 +155,6 @@ const exprBuilder = (param, value) => {
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
</QExpansionItem>
</template> </template>
</VnFilterPanel> </VnFilterPanel>
</template> </template>
@ -203,7 +186,6 @@ es:
Salesperson: Comercial Salesperson: Comercial
Province: Provincia Province: Provincia
City: Ciudad City: Ciudad
More options: Más opciones
Phone: Teléfono Phone: Teléfono
Email: Email Email: Email
Zone: Zona Zone: Zona

View File

@ -2,7 +2,6 @@
import { ref, computed, markRaw } from 'vue'; import { ref, computed, markRaw } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnLocation from 'src/components/common/VnLocation.vue'; import VnLocation from 'src/components/common/VnLocation.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
@ -12,6 +11,7 @@ import RightMenu from 'src/components/common/RightMenu.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import CustomerFilter from './CustomerFilter.vue'; import CustomerFilter from './CustomerFilter.vue';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@ -421,40 +421,17 @@ function handleLocation(data, location) {
auto-load auto-load
> >
<template #more-create-dialog="{ data }"> <template #more-create-dialog="{ data }">
<VnSelect <VnSelectWorker
url="Workers/search"
v-model="data.salesPersonFk"
:label="t('customer.summary.salesPerson')" :label="t('customer.summary.salesPerson')"
v-model="data.salesPersonFk"
:params="{ :params="{
departmentCodes: ['VT', 'shopping'], departmentCodes: ['VT', 'shopping'],
}" }"
:fields="['id', 'nickname', 'code']" :has-avatar="true"
sort-by="nickname ASC" :id-value="data.salesPersonFk"
option-label="nickname"
option-value="id"
emit-value emit-value
auto-load auto-load
>
<template #prepend>
<VnAvatar
:worker-id="data.salesPersonFk"
color="primary"
:title="title"
/> />
</template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt?.nickname }},
{{ scope.opt?.code }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnLocation <VnLocation
:acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]" :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]"
v-model="data.location" v-model="data.location"

View File

@ -1,9 +1,8 @@
<script setup> <script setup>
import { onBeforeMount, reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import VnLocation from 'src/components/common/VnLocation.vue'; import VnLocation from 'src/components/common/VnLocation.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
@ -19,26 +18,10 @@ const router = useRouter();
const formInitialData = reactive({ isDefaultAddress: false }); const formInitialData = reactive({ isDefaultAddress: false });
const urlCreate = ref('');
const agencyModes = ref([]); const agencyModes = ref([]);
const incoterms = ref([]); const incoterms = ref([]);
const customsAgents = ref([]); const customsAgents = ref([]);
onBeforeMount(() => {
urlCreate.value = `Clients/${route.params.id}/createAddress`;
getCustomsAgents();
});
const getCustomsAgents = async () => {
const { data } = await axios.get('CustomsAgents');
customsAgents.value = data;
};
const refreshData = () => {
getCustomsAgents();
};
const toCustomerAddress = () => { const toCustomerAddress = () => {
router.push({ router.push({
name: 'CustomerAddress', name: 'CustomerAddress',
@ -54,9 +37,19 @@ function handleLocation(data, location) {
data.provinceFk = provinceFk; data.provinceFk = provinceFk;
data.countryFk = countryFk; data.countryFk = countryFk;
} }
function onAgentCreated({ id, fiscalName }, data) {
customsAgents.value.push({ id, fiscalName });
data.customsAgentFk = id;
}
</script> </script>
<template> <template>
<FetchData
@on-fetch="(data) => (customsAgents = data)"
auto-load
url="CustomsAgents"
/>
<FetchData <FetchData
@on-fetch="(data) => (agencyModes = data)" @on-fetch="(data) => (agencyModes = data)"
auto-load auto-load
@ -67,7 +60,7 @@ function handleLocation(data, location) {
<FormModel <FormModel
:form-initial-data="formInitialData" :form-initial-data="formInitialData"
:observe-form-changes="false" :observe-form-changes="false"
:url-create="urlCreate" :url-create="`Clients/${route.params.id}/createAddress`"
@on-data-saved="toCustomerAddress()" @on-data-saved="toCustomerAddress()"
model="client" model="client"
> >
@ -139,6 +132,7 @@ function handleLocation(data, location) {
/> />
<VnSelectDialog <VnSelectDialog
url="CustomsAgents"
:label="t('Customs agent')" :label="t('Customs agent')"
:options="customsAgents" :options="customsAgents"
hide-selected hide-selected
@ -148,7 +142,11 @@ function handleLocation(data, location) {
:tooltip="t('Create a new expense')" :tooltip="t('Create a new expense')"
> >
<template #form> <template #form>
<CustomerNewCustomsAgent @on-data-saved="refreshData()" /> <CustomerNewCustomsAgent
@on-data-saved="
(requestResponse) => onAgentCreated(requestResponse, data)
"
/>
</template> </template>
</VnSelectDialog> </VnSelectDialog>
</VnRow> </VnRow>

View File

@ -144,7 +144,7 @@ function handleLocation(data, location) {
:url="`Addresses/${route.params.addressId}`" :url="`Addresses/${route.params.addressId}`"
@on-data-saved="onDataSaved()" @on-data-saved="onDataSaved()"
auto-load auto-load
model="client" model="customer"
> >
<template #moreActions> <template #moreActions>
<QBtn <QBtn

View File

@ -106,7 +106,6 @@ const setParams = (params) => {
}; };
const getPreview = async () => { const getPreview = async () => {
try {
const params = { const params = {
recipientId: entityId, recipientId: entityId,
}; };
@ -125,9 +124,6 @@ const getPreview = async () => {
htmlContent: data, htmlContent: data,
}, },
}); });
} catch (err) {
notify('Errors getting preview', 'negative');
}
}; };
const onSubmit = async () => { const onSubmit = async () => {

View File

@ -6,6 +6,7 @@ import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -48,14 +49,9 @@ const { t } = useI18n();
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelect <VnSelectWorker
:label="t('department.bossDepartment')" :label="t('department.bossDepartment')"
v-model="data.workerFk" v-model="data.workerFk"
url="Workers/search"
option-value="id"
option-label="name"
hide-selected
map-options
:rules="validate('department.workerFk')" :rules="validate('department.workerFk')"
/> />
<VnSelect <VnSelect

View File

@ -42,13 +42,9 @@ const setData = (entity) => {
}; };
const removeDepartment = async () => { const removeDepartment = async () => {
try {
await axios.post(`/Departments/${entityId.value}/removeChild`, entityId.value); await axios.post(`/Departments/${entityId.value}/removeChild`, entityId.value);
router.push({ name: 'WorkerDepartment' }); router.push({ name: 'WorkerDepartment' });
notify('department.departmentRemoved', 'positive'); notify('department.departmentRemoved', 'positive');
} catch (err) {
console.error('Error removing department');
}
}; };
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();

View File

@ -236,13 +236,9 @@ const copyOriginalRowsData = (rows) => {
}; };
const saveChange = async (field, { rowIndex, row }) => { const saveChange = async (field, { rowIndex, row }) => {
try {
if (originalRowDataCopy.value[rowIndex][field] == row[field]) return; if (originalRowDataCopy.value[rowIndex][field] == row[field]) return;
await axios.patch(`Buys/${row.id}`, row); await axios.patch(`Buys/${row.id}`, row);
originalRowDataCopy.value[rowIndex][field] = row[field]; originalRowDataCopy.value[rowIndex][field] = row[field];
} catch (err) {
console.error('Error saving changes', err);
}
}; };
const openRemoveDialog = async () => { const openRemoveDialog = async () => {
@ -260,15 +256,11 @@ const openRemoveDialog = async () => {
}, },
}) })
.onOk(async () => { .onOk(async () => {
try {
await deleteBuys(); await deleteBuys();
const notifyMessage = t( const notifyMessage = t(
`Buy${rowsSelected.value.length > 1 ? 's' : ''} deleted` `Buy${rowsSelected.value.length > 1 ? 's' : ''} deleted`
); );
notify(notifyMessage, 'positive'); notify(notifyMessage, 'positive');
} catch (err) {
console.error('Error deleting buys');
}
}); });
}; };
@ -282,7 +274,6 @@ const importBuys = () => {
}; };
const toggleGroupingMode = async (buy, mode) => { const toggleGroupingMode = async (buy, mode) => {
try {
const groupingMode = mode === 'grouping' ? mode : 'packing'; const groupingMode = mode === 'grouping' ? mode : 'packing';
const newGroupingMode = buy.groupingMode === groupingMode ? null : groupingMode; const newGroupingMode = buy.groupingMode === groupingMode ? null : groupingMode;
const params = { const params = {
@ -290,9 +281,6 @@ const toggleGroupingMode = async (buy, mode) => {
}; };
await axios.patch(`Buys/${buy.id}`, params); await axios.patch(`Buys/${buy.id}`, params);
buy.groupingMode = newGroupingMode; buy.groupingMode = newGroupingMode;
} catch (err) {
console.error('Error toggling grouping mode');
}
}; };
const lockIconType = (groupingMode, mode) => { const lockIconType = (groupingMode, mode) => {

View File

@ -123,20 +123,15 @@ const fillData = async (rawData) => {
}; };
const fetchBuys = async (buys) => { const fetchBuys = async (buys) => {
try {
const params = { buys }; const params = { buys };
const { data } = await axios.post( const { data } = await axios.post(
`Entries/${route.params.id}/importBuysPreview`, `Entries/${route.params.id}/importBuysPreview`,
params params
); );
importData.value.buys = data; importData.value.buys = data;
} catch (err) {
console.error('Error fetching buys');
}
}; };
const onSubmit = async () => { const onSubmit = async () => {
try {
const params = importData.value; const params = importData.value;
const hasAnyEmptyRow = params.buys.some((buy) => { const hasAnyEmptyRow = params.buys.some((buy) => {
return buy.itemFk === null; return buy.itemFk === null;
@ -150,9 +145,6 @@ const onSubmit = async () => {
await axios.post(`Entries/${route.params.id}/importBuys`, params); await axios.post(`Entries/${route.params.id}/importBuys`, params);
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
redirectToBuysView(); redirectToBuysView();
} catch (err) {
console.error('Error importing buys', err);
}
}; };
const redirectToBuysView = () => { const redirectToBuysView = () => {

View File

@ -147,12 +147,9 @@ async function setEntryData(data) {
} }
const fetchEntryBuys = async () => { const fetchEntryBuys = async () => {
try {
const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`); const { data } = await axios.get(`Entries/${entry.value.id}/getBuys`);
if (data) entryBuys.value = data; if (data) entryBuys.value = data;
} catch (err) {
console.error('Error fetching entry buys');
}
}; };
</script> </script>

View File

@ -82,11 +82,11 @@ const entriesTableColumns = computed(() => [
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">
<QBtn <QBtn
:label="t('printLabels')" :label="t('myEntries.printLabels')"
color="primary" color="primary"
icon="print" icon="print"
:loading="isLoading" :loading="isLoading"
@click="openReport(`Entries/${entityId}/print`)" @click="openReport(`Entries/${entityId}/labelSupplier`)"
unelevated unelevated
autofocus autofocus
/> />
@ -126,7 +126,9 @@ const entriesTableColumns = computed(() => [
" "
unelevated unelevated
> >
<QTooltip>{{ t('viewLabel') }}</QTooltip> <QTooltip>{{
t('myEntries.viewLabel')
}}</QTooltip>
</QBtn> </QBtn>
</QTr> </QTr>
</template> </template>

View File

@ -221,7 +221,7 @@ onMounted(async () => {
t('entry.list.tableVisibleColumns.isExcludedFromAvailable') t('entry.list.tableVisibleColumns.isExcludedFromAvailable')
}}</QTooltip> }}</QTooltip>
</QIcon> </QIcon>
<QIcon v-if="!!row.daysInForward" name="vn:net" color="primary"> <QIcon v-if="!!row.isRaid" name="vn:net" color="primary">
<QTooltip> <QTooltip>
{{ {{
t('globals.raid', { daysInForward: row.daysInForward }) t('globals.raid', { daysInForward: row.daysInForward })

View File

@ -30,8 +30,6 @@ const recalc = async () => {
isLoading.value = true; isLoading.value = true;
await axios.post('Applications/waste_addSales/execute-proc', params); await axios.post('Applications/waste_addSales/execute-proc', params);
notify('wasteRecalc.recalcOk', 'positive'); notify('wasteRecalc.recalcOk', 'positive');
} catch (err) {
console.error(err);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -101,7 +101,7 @@ const columns = computed(() => [
name: 'tableActions', name: 'tableActions',
actions: [ actions: [
{ {
title: t('printLabels'), title: t('myEntries.printLabels'),
icon: 'print', icon: 'print',
isPrimary: true, isPrimary: true,
action: (row) => printBuys(row.id), action: (row) => printBuys(row.id),

View File

@ -184,5 +184,4 @@ es:
Amount: Importe Amount: Importe
Issued: Fecha factura Issued: Fecha factura
Id or supplier: Id o proveedor Id or supplier: Id o proveedor
More options: Más opciones
</i18n> </i18n>

View File

@ -61,7 +61,6 @@ const showSendInvoiceDialog = (type) => {
}; };
const sendEmailInvoice = async ({ address }) => { const sendEmailInvoice = async ({ address }) => {
try {
if (!address) notify(`The email can't be empty`, 'negative'); if (!address) notify(`The email can't be empty`, 'negative');
if (invoiceFormType.value === 'pdf') { if (invoiceFormType.value === 'pdf') {
@ -70,16 +69,10 @@ const sendEmailInvoice = async ({ address }) => {
recipient: address, recipient: address,
}); });
} else { } else {
return sendEmail( return sendEmail(`InvoiceOuts/${$props.invoiceOutData.ref}/invoice-csv-email`, {
`InvoiceOuts/${$props.invoiceOutData.ref}/invoice-csv-email`,
{
recipientId: $props.invoiceOutData.client.id, recipientId: $props.invoiceOutData.client.id,
recipient: address, recipient: address,
} });
);
}
} catch (err) {
console.error('Error sending email', err);
} }
}; };
@ -88,34 +81,21 @@ const redirectToInvoiceOutList = () => {
}; };
const deleteInvoice = async () => { const deleteInvoice = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.id}/delete`); await axios.post(`InvoiceOuts/${$props.invoiceOutData.id}/delete`);
notify(t('InvoiceOut deleted'), 'positive'); notify(t('InvoiceOut deleted'), 'positive');
} catch (err) {
console.error('Error deleting invoice out', err);
}
}; };
const bookInvoice = async () => { const bookInvoice = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.ref}/book`); await axios.post(`InvoiceOuts/${$props.invoiceOutData.ref}/book`);
notify(t('InvoiceOut booked'), 'positive'); notify(t('InvoiceOut booked'), 'positive');
} catch (err) {
console.error('Error booking invoice out', err);
}
}; };
const generateInvoicePdf = async () => { const generateInvoicePdf = async () => {
try {
await axios.post(`InvoiceOuts/${$props.invoiceOutData.id}/createPdf`); await axios.post(`InvoiceOuts/${$props.invoiceOutData.id}/createPdf`);
notify(t('The invoice PDF document has been regenerated'), 'positive'); notify(t('The invoice PDF document has been regenerated'), 'positive');
} catch (err) {
console.error('Error generating invoice out pdf', err);
}
}; };
const refundInvoice = async (withWarehouse) => { const refundInvoice = async (withWarehouse) => {
try {
const params = { ref: $props.invoiceOutData.ref, withWarehouse: withWarehouse }; const params = { ref: $props.invoiceOutData.ref, withWarehouse: withWarehouse };
const { data } = await axios.post('InvoiceOuts/refund', params); const { data } = await axios.post('InvoiceOuts/refund', params);
location.href = window.origin + `/#/ticket/${data[0].id}/sale`; location.href = window.origin + `/#/ticket/${data[0].id}/sale`;
@ -125,9 +105,6 @@ const refundInvoice = async (withWarehouse) => {
}), }),
'positive' 'positive'
); );
} catch (err) {
console.error('Error generating invoice out pdf', err);
}
}; };
const showTransferInvoiceForm = async () => { const showTransferInvoiceForm = async () => {

View File

@ -83,8 +83,6 @@ const states = ref();
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QSeparator />
<QExpansionItem :label="t('More options')" expand-separator>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
@ -105,14 +103,9 @@ const states = ref();
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate v-model="params.dued" :label="t('Dued')" is-outlined />
v-model="params.dued"
:label="t('Dued')"
is-outlined
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
</QExpansionItem>
</template> </template>
</VnFilterPanel> </VnFilterPanel>
</template> </template>
@ -149,5 +142,4 @@ es:
Issued: Fecha emisión Issued: Fecha emisión
Created: Fecha creación Created: Fecha creación
Dued: Fecha vencimiento Dued: Fecha vencimiento
More options: Más opciones
</i18n> </i18n>

View File

@ -52,7 +52,9 @@ const columns = computed(() => [
label: t('invoiceOutList.tableVisibleColumns.id'), label: t('invoiceOutList.tableVisibleColumns.id'),
chip: { condition: () => true }, chip: { condition: () => true },
isId: true, isId: true,
columnFilter: { name: 'search' }, columnFilter: {
name: 'id',
},
}, },
{ {
align: 'left', align: 'left',
@ -60,8 +62,15 @@ const columns = computed(() => [
label: t('globals.reference'), label: t('globals.reference'),
isTitle: true, isTitle: true,
component: 'select', component: 'select',
attrs: { url: MODEL, optionLabel: 'ref', optionValue: 'id' }, attrs: {
url: MODEL,
optionLabel: 'ref',
optionValue: 'ref',
},
columnField: { component: null }, columnField: { component: null },
columnFilter: {
inWhere: true,
},
}, },
{ {
align: 'left', align: 'left',
@ -77,8 +86,15 @@ const columns = computed(() => [
label: t('globals.client'), label: t('globals.client'),
cardVisible: true, cardVisible: true,
component: 'select', component: 'select',
attrs: { url: 'Clients', fields: ['id', 'name'] }, attrs: {
columnField: { component: null }, url: 'Clients',
fields: ['id', 'socialName'],
optionLabel: 'socialName',
optionValue: 'id',
},
columnField: {
component: null,
},
}, },
{ {
align: 'left', align: 'left',
@ -135,15 +151,10 @@ onMounted(() => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
function openPdf(id) { function openPdf(id) {
try {
openReport(`${MODEL}/${id}/download`); openReport(`${MODEL}/${id}/download`);
} catch (err) {
console.error('Error opening PDF', err);
}
} }
function downloadPdf() { function downloadPdf() {
try {
if (selectedRows.value.size === 0) return; if (selectedRows.value.size === 0) return;
const selectedCardsArray = Array.from(selectedRows.value.values()); const selectedCardsArray = Array.from(selectedRows.value.values());
@ -151,9 +162,7 @@ function downloadPdf() {
const [invoiceOut] = selectedCardsArray; const [invoiceOut] = selectedCardsArray;
openPdf(invoiceOut.id); openPdf(invoiceOut.id);
} else { } else {
const invoiceOutIdsArray = selectedCardsArray.map( const invoiceOutIdsArray = selectedCardsArray.map((invoiceOut) => invoiceOut.id);
(invoiceOut) => invoiceOut.id
);
const invoiceOutIds = invoiceOutIdsArray.join(','); const invoiceOutIds = invoiceOutIdsArray.join(',');
const params = { const params = {
@ -162,9 +171,6 @@ function downloadPdf() {
openReport(`${MODEL}/downloadZip`, params); openReport(`${MODEL}/downloadZip`, params);
} }
} catch (err) {
console.error('Error opening PDF');
}
} }
watchEffect(selectedRows); watchEffect(selectedRows);

View File

@ -11,7 +11,7 @@ invoiceOutList:
ref: Referencia ref: Referencia
issued: Fecha emisión issued: Fecha emisión
created: F. creación created: F. creación
dueDate: F. máxima dueDate: Fecha vencimiento
invoiceOutSerial: Serial invoiceOutSerial: Serial
ticket: Ticket ticket: Ticket
taxArea: Area taxArea: Area

View File

@ -2,12 +2,15 @@
import { ref, nextTick } from 'vue'; import { ref, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios';
import CrudModel from 'src/components/CrudModel.vue'; import CrudModel from 'src/components/CrudModel.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import useNotify from 'src/composables/useNotify.js';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify();
const itemBarcodeRef = ref(null); const itemBarcodeRef = ref(null);
@ -23,6 +26,24 @@ const focusLastInput = () => {
if (lastInput) lastInput.focus(); if (lastInput) lastInput.focus();
}); });
}; };
const removeRow = (row) => {
itemBarcodeRef.value.remove([row]);
};
const submit = async (rows) => {
const params = rows[rows.length - 1];
let { data } = await axios.get('ItemBarcodes');
const code = params.code;
if (data.some((codes) => codes.code === code)) {
notify(t('Codes can not be repeated'), 'negative');
itemBarcodeRef.value.reset();
return;
}
await axios.patch(`ItemBarcodes`, params);
notify(t('globals.dataSaved'), 'positive');
};
</script> </script>
<template> <template>
<div class="full-width flex justify-center"> <div class="full-width flex justify-center">
@ -39,6 +60,7 @@ const focusLastInput = () => {
ref="itemBarcodeRef" ref="itemBarcodeRef"
url="ItemBarcodes" url="ItemBarcodes"
auto-load auto-load
:save-fn="submit"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="q-px-lg q-py-md"> <QCard class="q-px-lg q-py-md">
@ -54,7 +76,7 @@ const focusLastInput = () => {
focusable-input focusable-input
/> />
<QIcon <QIcon
@click="itemBarcodeRef.remove([row])" @click="removeRow(row)"
class="cursor-pointer q-ml-md" class="cursor-pointer q-ml-md"
color="primary" color="primary"
name="delete" name="delete"
@ -89,4 +111,5 @@ es:
Code: Código Code: Código
Remove barcode: Quitar código de barras Remove barcode: Quitar código de barras
Add barcode: Añadir código de barras Add barcode: Añadir código de barras
Codes can not be repeated: Los códigos no puden ser repetidos
</i18n> </i18n>

View File

@ -70,6 +70,7 @@ const onIntrastatCreated = (response, formData) => {
option-label="name" option-label="name"
hide-selected hide-selected
map-options map-options
required
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted, reactive, computed } from 'vue'; import { ref, reactive, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -67,7 +67,7 @@ const handlePhotoUpdated = (evt = false) => {
<template> <template>
<div class="relative-position"> <div class="relative-position">
<VnImg ref="image" :id="Number($props.entityId)" zoom-resolution="1600x900"> <VnImg ref="image" :id="parseInt($props.entityId)" zoom-resolution="1600x900">
<template #error> <template #error>
<div class="absolute-full picture text-center q-pa-md flex flex-center"> <div class="absolute-full picture text-center q-pa-md flex flex-center">
<div> <div>

Some files were not shown because too many files have changed in this diff Show More