Merge branch 'dev' of https: refs #4074//gitea.verdnatura.es/verdnatura/salix-front into 4074-useAcls
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Jorge Penadés 2024-09-06 13:23:02 +02:00
commit 34a9b4d4d0
266 changed files with 12707 additions and 12898 deletions

33
.husky/addReferenceTag.js Normal file
View File

@ -0,0 +1,33 @@
const fs = require('fs');
const path = require('path');
function getCurrentBranchName(p = process.cwd()) {
if (!fs.existsSync(p)) return false;
const gitHeadPath = path.join(p, '.git', 'HEAD');
if (!fs.existsSync(gitHeadPath))
return getCurrentBranchName(path.resolve(p, '..'));
const headContent = fs.readFileSync(gitHeadPath, 'utf-8');
return headContent.trim().split('/')[2];
}
const branchName = getCurrentBranchName();
if (branchName) {
const msgPath = `.git/COMMIT_EDITMSG`;
const msg = fs.readFileSync(msgPath, 'utf-8');
const reference = branchName.match(/^\d+/);
const referenceTag = `refs #${reference}`;
if (!msg.includes(referenceTag) && reference) {
const splitedMsg = msg.split(':');
if (splitedMsg.length > 1) {
const finalMsg = splitedMsg[0] + ': ' + referenceTag + splitedMsg.slice(1).join(':');
fs.writeFileSync(msgPath, finalMsg);
}
}
}

8
.husky/commit-msg Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "Running husky commit-msg hook"
npx --no-install commitlint --edit
echo "Adding reference tag to commit message"
node .husky/addReferenceTag.js

View File

@ -1,3 +1,229 @@
# Version 24.36 - 2024-08-27
### Added 🆕
- feat(FormModel): trim data by default by:alexm
- feat(orderBasicData): add notes by:alexm
- feat(orderList): correct create order by:alexm
- feat(orderList): use orderFilter and fixed this by:alexm
- feat: #7323 handle workerPhoto (origin/7323_workerPhoto, 7323_workerPhoto) by:Javier Segarra
- feat: add recover password and reset password by:alexm
- feat: refs #7346 add seriaType option by:jgallego
- feat: refs #7346 elimino === by:jgallego
- feat: refs #7346 formdata uses serialType by:jgallego
- feat: refs #7346 refactor by:jgallego
- feat: refs #7346 sonarLint warnings (origin/7346-invoiceOutMultilple, 7346-invoiceOutMultilple) by:jgallego
- feat: refs #7710 uses cloneAll by:jgallego
- fix: refs #7717 fix OrderList table filters' and summary table style by:Jon
### Changed 📦
- feat: refs #7346 refactor by:jgallego
- perf: date fields (mindshore/feature/TicketFutureFilter, feature/TicketFutureFilter) by:Javier Segarra
- perf: refs #7717 right menu filter by:Jon
- perf: use ref at component start by:Javier Segarra
- refactor: refs #7717 delete useless function and import by:Jon
- refactor: refs #7717 deleted useless code by:Jon
### Fixed 🛠️
- feat(orderList): use orderFilter and fixed this by:alexm
- fix(VnTable): orderBy v-model by:alexm
- fix(account_card): redirection by:carlossa
- fix(orderLines): reload when delete and redirect when confirm by:alexm
- fix: #6336 ClaimListStates by:Javier Segarra
- fix: account subsections cards by:carlossa
- fix: duplicate key by:Jon
- fix: order description to vnTable by:alexm
- fix: orderCatalogFilter order by:alexm
- fix: quasar build warnings (6336_claim_fix_states) by:Javier Segarra
- fix: refs #7717 fix OrderList table filters' and summary table style by:Jon
- fix: refs #7717 fix basic data form & minor errors by:Jon
- fix: refs #7717 fix catalog filter, searchbar redirect and search by:Jon
- fix: refs #7717 fix catalog searchbar and worker tests(refs #7323) by:Jon
- fix: refs #7717 fix order sections by:Jon
- fix: refs #7717 fix volume and lines redirect by:Jon
- fix: refs #7717 fixed searchbar filter with rightmenu filters' applied by:Jon
- fix: test by:alexm
- fix: ticketDescriptorMenu by:Javier Segarra
- refs #7355 account fixes by:carlossa
# Version 24.34 - 2024-08-20
### Added 🆕
- chore: #6900 order params by:jorgep
- chore: refs #6900 drop console log by:jorgep
- chore: refs #6900 drop vnCurrency by:jorgep
- chore: refs #6900 fix e2e tests by:jorgep
- chore: refs #6900 mv rectificative logic by:jorgep
- chore: refs #6900 responsive code by:jorgep
- chore: refs #7283 drop array types by:jorgep
- chore: refs #7283 drop import by:jorgep
- chore: refs #7283 fix e2e logout by:jorgep
- chore: refs #7283 update VnAvatar title handling by:jorgep
- chore: refs #7323 fix test by:jorgep
- chore: refs #7323 remove unused import by:jorgep
- chore: refs #7323drop commented code by:jorgep
- feat(VnCard): use props searchbar by:alexm
- feat(customer): improve basicData to balance by:alexm
- feat(customer_balance): refs #6943 add functionality from salix by:alexm
- feat(customer_balance): refs #6943 translations by:alexm
- feat: refs #6130 husky commitLint config by:pablone
- feat: refs #6130 husky hooks by:pablone
- feat: refs #6900 add InvoiceInSerial by:jorgep
- feat: refs #6900 add locale by:jorgep
- feat: refs #6900 use VnTable & sort filter fields by:jorgep
- feat: refs #7323 add flex-wrap by:jorgep
- feat: refs #7323 add my account" btn & fix models log selectable by:jorgep
- feat: refs #7323 improve test by:jorgep
### Changed 📦
- refactor(customer_log: use VnLog by:alexm
- refactor(customer_recovery): to vnTable by:alexm
- refactor(customer_webAccess): FormModel by:alexm
- refactor: refs #7283 update avatar size and color by:jorgep
### Fixed 🛠️
- chore: refs #6900 fix e2e tests by:jorgep
- chore: refs #7283 fix e2e logout by:jorgep
- chore: refs #7323 fix test by:jorgep
- feat: refs #7323 add my account" btn & fix models log selectable by:jorgep
- fix #7355 fix acls list by:carlossa
- fix(VnFilterPanel): emit userParams better by:alexm
- fix(claim_summary): url links (HEAD -> 7864_testToMaster_2434, origin/test, origin/7864_testToMaster_2434, test) by:alexm
- fix(customer_sms: fix reload by:alexm
- fix(twoFactor): unify code login and twoFactor by:alexm
- fix: VnCard VnSearchbar props by:alexm
- fix: accountMailAlias by:alexm
- fix: refs #6130 add commit lint modules by:pablone
- fix: refs #6130 pnpm-lock.yml by:pablone
- fix: refs #6900 improve loading by:jorgep
- fix: refs #6900 improve logic (origin/6900-addSerial) by:jorgep
- fix: refs #6900 improve logic by:jorgep
- fix: refs #6900 rectificative btn reactivity by:jorgep
- fix: refs #6900 use type number by:jorgep
- fix: refs #6900 vat & dueday by:jorgep
- fix: refs #6900 vat, dueday & intrastat by:jorgep
- fix: refs #6989 show entity name & default time from config table by:jorgep
- fix: refs #7283 basicData locale by:jorgep
- fix: refs #7283 itemLastEntries filter by:jorgep
- fix: refs #7283 itemTags & VnImg by:jorgep
- fix: refs #7283 locale by:jorgep
- fix: refs #7283 min-width vnImg by:jorgep
- fix: refs #7283 use vnAvatar & add optional zoom by:jorgep
- fix: refs #7283 userPanel pic by:jorgep
- fix: refs #7323 add department popup by:jorgep
- fix: refs #7323 add locale by:jorgep
- fix: refs #7323 css righ menu by:jorgep
- fix: refs #7323 data-key & add select by:jorgep
- fix: refs #7323 load all opts by:jorgep
- fix: refs #7323 righ menu bug by:jorgep
- fix: refs #7323 use global locale by:jorgep
- fix: refs #7323 use workerFilter (origin/7323-warmfix-fixErrors) by:jorgep
- fix: refs #7323 vnsubtoolbar css by:jorgep
- fix: refs #7323 wrong css by:jorgep
- refs #7355 fix Rol, alias by:carlossa
- refs #7355 fix accountAlias by:carlossa
- refs #7355 fix alias summary by:carlossa
- refs #7355 fix conflicts by:carlossa
- refs #7355 fix create Rol by:carlossa
- refs #7355 fix list by:carlossa
- refs #7355 fix lists redirects summary by:carlossa
- refs #7355 fix roles by:carlossa
- refs #7355 fix search exprBuilder by:carlossa
- refs #7355 fix vnTable by:carlossa
# Version 24.32 - 2024-08-06
### Added 🆕
- chore: refs #7197 drop space by:jorgep
- chore: refs #7197 drop useless attr by:jorgep
- chore: refs #7197 fix test by:jorgep
- chore: refs #7197 fix tests by:jorgep
- chore: refs #7197 fix unit tests by:jorgep
- chore: refs #7197 idrop useless class by:jorgep
- chore: refs #7197 improve form filling in Cypress tests by:jorgep
- chore: refs #7197 remove unused import by:jorgep
- feat: customerPayments card view by:alexm
- feat: refs #6943 lock grid mode by:jorgep
- feat: refs #6943 wip consumption filter by:jorgep
- feat: refs #7197 add correcting filter by:jorgep
- feat: refs #7197 add supplier activities filter option by:jorgep
- feat: refs #7197 summary responsive by:jorgep
- feat: refs #7323 fix descriptors, added VnTable and minor changes by:Jon
- feat: refs #7323 fixed tests, changed calendar styles and fix workerCreate by:Jon
- feat: refs #7356 list & weekly to VnTable and style fixes by:Jon
- feat: refs #7401 add menu options by:pablone
- feat: SalesClientTable by:Javier Segarra
- feat: salesOrderTable by:Javier Segarra
- feat: salesTicketTable by:Javier Segarra
- feat: VnTable SalesTicketTable by:Javier Segarra
- fix: columns style by:alexm
### Changed 📦
- perf: LeftMenu show/hide by:Javier Segarra
- perf: refs #7356 TicketList state column by:Jon
- perf: VnFilterPanel (origin/7323_WorkerMigration_End) by:Javier Segarra
- perf: width SalesTicketsTable by:Javier Segarra
- refactor: #6943 wip use vnTable CustomerCredits by:jorgep
- refactor: CustomerNotifications use VnTable by:alexm
- refactor: CustomerPayments use VnTable by:alexm
- refactor: refs #7014 deleted main files and changed route files by:Jon
- refactor: refs #7014 improved route.js & deleted RouteMain by:Jon
- refactor: refs #7014 refactor <module>Main.vue by:Jon
- refactor: refs #7014 refactor ZoneCard, deleted ZoneMain & created basic tests for functionality by:Jon
- refactor: refs #7197 use invoiceInSearchbar & queryParams by:jorgep
- refactor: refs #7323 hidden column filter proposal by:Jon
- refactor: refs #7356 fixed VnTable filters by:Jon
- refactor: refs #7356 requested changes by:Jon
- refactor: wip use vnTable CustomerCredits by:jorgep
### Fixed 🛠️
- chore: refs #7197 fix test by:jorgep
- chore: refs #7197 fix tests by:jorgep
- chore: refs #7197 fix unit tests by:jorgep
- feat: refs #7323 fix descriptors, added VnTable and minor changes by:Jon
- feat: refs #7323 fixed tests, changed calendar styles and fix workerCreate by:Jon
- feat: refs #7356 list & weekly to VnTable and style fixes by:Jon
- fix(claim): small details (6336-claim-v6) by:alexm
- fix: columns style by:alexm
- fix: customer defaulter add amount order (6943-fixCustomer) by:alexm
- fix: customerDefaulter correct functionality by:alexm
- fix: customerNotifications filter by:alexm
- fix: fix conflicts by:Jon
- fix: refs #6101 fix TicketList by:Jon
- fix: refs #6891 worker tests by:jorgep
- fix: refs #6943 drop padding-left checkbox & create wrap mode vnRow by:jorgep
- fix: refs #6943 prevent undefined by:jorgep
- fix: refs #7014 fix tests by:Jon
- fix: refs #7014 fix wagon module by:Jon
- fix: refs #7197 add url InvoiceInSearchbar by:jorgep
- fix: refs #7197 amount reactivity by:jorgep
- fix: refs #7197 drop character by:jorgep
- fix: refs #7197 reactivity invoiceCorrection by:jorgep
- fix: refs #7197 responsive summary layout by:jorgep
- fix: refs #7197 rollback by:jorgep
- fix: refs #7197 rollback crudModel by:jorgep
- fix: refs #7197 setInvoiceInCorrecition by:jorgep
- fix: refs #7197 vat, intrastat, filter and list sections by:jorgep
- fix: refs #7323 fix department & email table filter by:Jon
- fix: refs #7323 fixed left filter by:Jon
- fix: refs #7323 fix workerTimeControl form by:Jon
- fix: refs #7401 fix routeForm by:pablone
- fix: refs #7401 remove console.log by:pablone
- fix: refs CAU 207504 fix itemDiary and logs by:Jon
- fix: workerCreate form street field to be always upperCase by:Jon
- hotfix: refs CAU #207614 fix sale.concept field by:Jon
- refactor: refs #7356 fixed VnTable filters by:Jon
- refs #6898 fix by:carlossa
- Ticket expedition initial load fix by:wbuezas
# Version 24.28 - 2024-07-09 # Version 24.28 - 2024-07-09
### Added 🆕 ### Added 🆕

1
commitlint.config.js Normal file
View File

@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.34.0", "version": "24.36.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -13,7 +13,10 @@
"test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run", "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest", "test:unit": "vitest",
"test:unit:ci": "vitest run" "test:unit:ci": "vitest run",
"commitlint": "commitlint --edit",
"prepare": "npx husky install",
"addReferenceTag": "node .husky/addReferenceTag.js"
}, },
"dependencies": { "dependencies": {
"@quasar/cli": "^2.3.0", "@quasar/cli": "^2.3.0",
@ -29,6 +32,8 @@
"vue-router": "^4.2.1" "vue-router": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.2.1",
"@commitlint/config-conventional": "^19.1.0",
"@intlify/unplugin-vue-i18n": "^0.8.1", "@intlify/unplugin-vue-i18n": "^0.8.1",
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^1.7.3", "@quasar/app-vite": "^1.7.3",
@ -41,6 +46,7 @@
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.3", "eslint-plugin-cypress": "^2.13.3",
"eslint-plugin-vue": "^9.14.1", "eslint-plugin-vue": "^9.14.1",
"husky": "^8.0.0",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"vitest": "^0.31.1" "vitest": "^0.31.1"

File diff suppressed because it is too large Load Diff

34
src/boot/keyShortcut.js Normal file
View File

@ -0,0 +1,34 @@
export default {
mounted: function (el, binding) {
const shortcut = binding.value ?? '+';
const { key, ctrl, alt, callback } =
typeof shortcut === 'string'
? {
key: shortcut,
ctrl: true,
alt: true,
callback: () =>
document
.querySelector(`button[shortcut="${shortcut}"]`)
?.click(),
}
: binding.value;
const handleKeydown = (event) => {
if (event.key === key && (!ctrl || event.ctrlKey) && (!alt || event.altKey)) {
callback();
}
};
// Attach the event listener to the window
window.addEventListener('keydown', handleKeydown);
el._handleKeydown = handleKeydown;
},
unmounted: function (el) {
if (el._handleKeydown) {
window.removeEventListener('keydown', el._handleKeydown);
}
},
};

View File

@ -0,0 +1,38 @@
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[route.meta.keyBinding.toLowerCase()] = route.path;
return map;
}, {});
const handleKeyDown = (event) => {
const { ctrlKey, altKey, key } = event;
if (ctrlKey && altKey && keyBindingMap[key] && !isNotified) {
event.preventDefault();
router.push(keyBindingMap[key]);
isNotified = true;
}
};
const handleKeyUp = (event) => {
const { ctrlKey, altKey } = event;
// Resetea la bandera cuando se sueltan las teclas ctrl o alt
if (!ctrlKey || !altKey) {
isNotified = false;
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
},
};

View File

@ -1,6 +1,10 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import qFormMixin from './qformMixin'; import qFormMixin from './qformMixin';
import mainShortcutMixin from './mainShortcutMixin';
import keyShortcut from './keyShortcut';
export default boot(({ app }) => { export default boot(({ app }) => {
app.mixin(qFormMixin); app.mixin(qFormMixin);
app.mixin(mainShortcutMixin);
app.directive('shortcut', keyShortcut);
}); });

View File

@ -45,6 +45,7 @@ function onDataSaved(formData) {
} }
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
); );

View File

@ -22,7 +22,7 @@ const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const route = useRoute(); const route = useRoute();
const myForm = ref(null);
const $props = defineProps({ const $props = defineProps({
url: { url: {
type: String, type: String,
@ -87,6 +87,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
defaultTrim: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed( const modelValue = computed(
@ -100,17 +104,19 @@ const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges); const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({}); const originalData = ref({});
const formData = computed(() => state.get(modelValue)); const formData = computed(() => state.get(modelValue));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({ const defaultButtons = computed(() => ({
save: { save: {
color: 'primary', color: 'primary',
icon: 'save', icon: 'save',
label: 'globals.save', label: 'globals.save',
click: () => myForm.value.submit(),
type: 'submit',
}, },
reset: { reset: {
color: 'primary', color: 'primary',
icon: 'restart_alt', icon: 'restart_alt',
label: 'globals.reset', label: 'globals.reset',
click: () => reset(),
}, },
...$props.defaultButtons, ...$props.defaultButtons,
})); }));
@ -148,11 +154,14 @@ if (!$props.url)
(val) => updateAndEmit('onFetch', val) (val) => updateAndEmit('onFetch', val)
); );
watch(formUrl, async () => { watch(
originalData.value = null; () => [$props.url, $props.filter],
reset(); async () => {
await fetch(); originalData.value = null;
}); reset();
await fetch();
}
);
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value && $props.observeFormChanges) if (hasChanges.value && $props.observeFormChanges)
@ -193,6 +202,7 @@ async function save() {
isLoading.value = true; isLoading.value = true;
try { try {
formData.value = trimData(formData.value);
const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const body = $props.mapper ? $props.mapper(formData.value) : formData.value;
const method = $props.urlCreate ? 'post' : 'patch'; const method = $props.urlCreate ? 'post' : 'patch';
const url = const url =
@ -251,6 +261,14 @@ function updateAndEmit(evt, val, res) {
emit(evt, state.get(modelValue), res); emit(evt, state.get(modelValue), res);
} }
function trimData(data) {
if (!$props.defaultTrim) return data;
for (const key in data) {
if (typeof data[key] == 'string') data[key] = data[key].trim();
}
return data;
}
defineExpose({ defineExpose({
save, save,
isLoading, isLoading,
@ -261,7 +279,14 @@ defineExpose({
</script> </script>
<template> <template>
<div class="column items-center full-width"> <div class="column items-center full-width">
<QForm @submit="save" @reset="reset" class="q-pa-md" id="formModel"> <QForm
ref="myForm"
v-if="formData"
@submit="save"
@reset="reset"
class="q-pa-md"
id="formModel"
>
<QCard> <QCard>
<slot <slot
v-if="formData" v-if="formData"
@ -289,7 +314,7 @@ defineExpose({
:color="defaultButtons.reset.color" :color="defaultButtons.reset.color"
:icon="defaultButtons.reset.icon" :icon="defaultButtons.reset.icon"
flat flat
@click="reset" @click="defaultButtons.reset.click"
:disable="!hasChanges" :disable="!hasChanges"
:title="t(defaultButtons.reset.label)" :title="t(defaultButtons.reset.label)"
/> />
@ -329,7 +354,7 @@ defineExpose({
:label="tMobile('globals.save')" :label="tMobile('globals.save')"
color="primary" color="primary"
icon="save" icon="save"
@click="save" @click="defaultButtons.save.click"
:disable="!hasChanges" :disable="!hasChanges"
:title="t(defaultButtons.save.label)" :title="t(defaultButtons.save.label)"
/> />

View File

@ -112,6 +112,7 @@ const getCategoryClass = (category, params) => {
const getSelectedTagValues = async (tag) => { const getSelectedTagValues = async (tag) => {
try { try {
if (!tag?.selectedTag?.id) return;
tag.value = null; tag.value = null;
const filter = { const filter = {
fields: ['value'], fields: ['value'],

View File

@ -7,7 +7,7 @@ import { useQuasar } from 'quasar';
import PinnedModules from './PinnedModules.vue'; import PinnedModules from './PinnedModules.vue';
import UserPanel from 'components/UserPanel.vue'; import UserPanel from 'components/UserPanel.vue';
import VnBreadcrumbs from './common/VnBreadcrumbs.vue'; import VnBreadcrumbs from './common/VnBreadcrumbs.vue';
import VnImg from 'src/components/ui/VnImg.vue'; import VnAvatar from './ui/VnAvatar.vue';
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -24,7 +24,13 @@ const pinnedModulesRef = ref();
<template> <template>
<QHeader color="white" elevated> <QHeader color="white" elevated>
<QToolbar class="q-py-sm q-px-md"> <QToolbar class="q-py-sm q-px-md">
<QBtn @click="stateStore.toggleLeftDrawer()" icon="menu" round dense flat> <QBtn
@click="stateStore.toggleLeftDrawer()"
icon="dock_to_right"
round
dense
flat
>
<QTooltip bottom anchor="bottom right"> <QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }} {{ t('globals.collapseMenu') }}
</QTooltip> </QTooltip>
@ -72,22 +78,13 @@ const pinnedModulesRef = ref();
</QTooltip> </QTooltip>
<PinnedModules ref="pinnedModulesRef" /> <PinnedModules ref="pinnedModulesRef" />
</QBtn> </QBtn>
<QBtn <QBtn class="q-pa-none" rounded dense flat no-wrap id="user">
:class="{ 'q-pa-none': quasar.platform.is.mobile }" <VnAvatar
rounded :worker-id="user.id"
dense :title="user.name"
flat size="lg"
no-wrap color="transparent"
id="user" />
>
<QAvatar size="lg">
<VnImg
:id="user.id"
collection="user"
size="160x160"
:zoom-size="null"
/>
</QAvatar>
<QTooltip bottom> <QTooltip bottom>
{{ t('globals.userPanel') }} {{ t('globals.userPanel') }}
</QTooltip> </QTooltip>

View File

@ -0,0 +1,173 @@
<script setup>
import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useDialogPluginComponent } from 'quasar';
import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
invoiceOutData: {
type: Object,
default: () => {},
},
});
const { dialogRef } = useDialogPluginComponent();
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]);
const inheritWarehouse = ref(true);
const invoiceParams = reactive({
id: $props.invoiceOutData?.id,
});
const invoiceCorrectionTypesOptions = ref([]);
const refund = async () => {
const params = {
id: invoiceParams.id,
cplusRectificationTypeFk: invoiceParams.cplusRectificationTypeFk,
siiTypeInvoiceOutFk: invoiceParams.siiTypeInvoiceOutFk,
invoiceCorrectionTypeFk: invoiceParams.invoiceCorrectionTypeFk,
};
try {
const { data } = await axios.post('InvoiceOuts/refundAndInvoice', params);
notify(t('Refunded invoice'), 'positive');
const [id] = data?.refundId || [];
if (id) router.push({ name: 'InvoiceOutSummary', params: { id } });
} catch (err) {
console.error('Error refunding invoice', err);
}
};
</script>
<template>
<FetchData
url="CplusRectificationTypes"
:filter="{ order: 'description' }"
@on-fetch="
(data) => (
(rectificativeTypeOptions = data),
(invoiceParams.cplusRectificationTypeFk = data.filter(
(type) => type.description == 'I Por diferencias'
)[0].id)
)
"
auto-load
/>
<FetchData
url="SiiTypeInvoiceOuts"
:filter="{ where: { code: { like: 'R%' } } }"
@on-fetch="
(data) => (
(siiTypeInvoiceOutsOptions = data),
(invoiceParams.siiTypeInvoiceOutFk = data.filter(
(type) => type.code == 'R4'
)[0].id)
)
"
auto-load
/>
<FetchData
url="InvoiceCorrectionTypes"
@on-fetch="(data) => (invoiceCorrectionTypesOptions = data)"
auto-load
/>
<QDialog ref="dialogRef">
<FormPopup
@on-submit="refund()"
:custom-submit-button-label="t('Accept')"
:default-cancel-button="false"
>
<template #form-inputs>
<VnRow>
<VnSelect
:label="t('Rectificative type')"
:options="rectificativeTypeOptions"
hide-selected
option-label="description"
option-value="id"
v-model="invoiceParams.cplusRectificationTypeFk"
:required="true"
/>
</VnRow>
<VnRow>
<VnSelect
:label="t('Class')"
:options="siiTypeInvoiceOutsOptions"
hide-selected
option-label="description"
option-value="id"
v-model="invoiceParams.siiTypeInvoiceOutFk"
:required="true"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }} -
{{ scope.opt?.description }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</VnRow>
<VnRow>
<VnSelect
:label="t('Type')"
:options="invoiceCorrectionTypesOptions"
hide-selected
option-label="description"
option-value="id"
v-model="invoiceParams.invoiceCorrectionTypeFk"
:required="true"
/> </VnRow
><VnRow>
<div>
<QCheckbox
:label="t('Inherit warehouse')"
v-model="inheritWarehouse"
/>
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip>
</QIcon>
</div>
</VnRow>
</template>
</FormPopup>
</QDialog>
</template>
<i18n>
en:
Refund invoice: Refund invoice
Rectificative type: Rectificative type
Class: Class
Type: Type
Refunded invoice: Refunded invoice
Inherit warehouse: Inherit the warehouse
Inherit warehouse tooltip: Select this option to inherit the warehouse when refunding the invoice
Accept: Accept
Error refunding invoice: Error refunding invoice
es:
Refund invoice: Abonar factura
Rectificative type: Tipo rectificativa
Class: Clase
Type: Tipo
Refunded invoice: Factura abonada
Inherit warehouse: Heredar el almacén
Inherit warehouse tooltip: Seleccione esta opción para heredar el almacén al abonar la factura.
Accept: Aceptar
Error refunding invoice: Error abonando factura
</i18n>

View File

@ -2,13 +2,12 @@
import { ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar, useDialogPluginComponent } from 'quasar';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue'; import FormPopup from './FormPopup.vue';
import { useDialogPluginComponent } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -18,19 +17,19 @@ const $props = defineProps({
default: () => {}, default: () => {},
}, },
}); });
const { dialogRef } = useDialogPluginComponent(); const { dialogRef } = useDialogPluginComponent();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
const checked = ref(true);
const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id,
refFk: $props.invoiceOutData?.ref,
});
const rectificativeTypeOptions = ref([]); const rectificativeTypeOptions = ref([]);
const siiTypeInvoiceOutsOptions = ref([]); const siiTypeInvoiceOutsOptions = ref([]);
const checked = ref(true);
const transferInvoiceParams = reactive({
id: $props.invoiceOutData?.id,
});
const invoiceCorrectionTypesOptions = ref([]); const invoiceCorrectionTypesOptions = ref([]);
const selectedClient = (client) => { const selectedClient = (client) => {
@ -44,10 +43,9 @@ const makeInvoice = async () => {
const params = { const params = {
id: transferInvoiceParams.id, id: transferInvoiceParams.id,
cplusRectificationTypeFk: transferInvoiceParams.cplusRectificationTypeFk, cplusRectificationTypeFk: transferInvoiceParams.cplusRectificationTypeFk,
siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk,
invoiceCorrectionTypeFk: transferInvoiceParams.invoiceCorrectionTypeFk, invoiceCorrectionTypeFk: transferInvoiceParams.invoiceCorrectionTypeFk,
newClientFk: transferInvoiceParams.newClientFk, newClientFk: transferInvoiceParams.newClientFk,
refFk: transferInvoiceParams.refFk,
siiTypeInvoiceOutFk: transferInvoiceParams.siiTypeInvoiceOutFk,
makeInvoice: checked.value, makeInvoice: checked.value,
}; };
@ -74,7 +72,7 @@ const makeInvoice = async () => {
} }
} }
const { data } = await axios.post('InvoiceOuts/transferInvoice', params); const { data } = await axios.post('InvoiceOuts/transfer', params);
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 } });

View File

@ -11,8 +11,8 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useClipboard } from 'src/composables/useClipboard'; import { useClipboard } from 'src/composables/useClipboard';
import VnImg from 'src/components/ui/VnImg.vue';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import VnAvatar from './ui/VnAvatar.vue';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
@ -136,7 +136,7 @@ const isEmployee = computed(() => useRole().isEmployee());
@update:model-value="saveLanguage" @update:model-value="saveLanguage"
:label="t(`globals.lang['${userLocale}']`)" :label="t(`globals.lang['${userLocale}']`)"
icon="public" icon="public"
color="orange" color="primary"
false-value="es" false-value="es"
true-value="en" true-value="en"
/> />
@ -145,7 +145,7 @@ const isEmployee = computed(() => useRole().isEmployee());
@update:model-value="saveDarkMode" @update:model-value="saveDarkMode"
:label="t(`globals.darkMode`)" :label="t(`globals.darkMode`)"
checked-icon="dark_mode" checked-icon="dark_mode"
color="orange" color="primary"
unchecked-icon="light_mode" unchecked-icon="light_mode"
/> />
</div> </div>
@ -153,10 +153,20 @@ const isEmployee = computed(() => useRole().isEmployee());
<QSeparator vertical inset class="q-mx-lg" /> <QSeparator vertical inset class="q-mx-lg" />
<div class="col column items-center q-mb-sm"> <div class="col column items-center q-mb-sm">
<QAvatar size="80px"> <VnAvatar
<VnImg :id="user.id" collection="user" size="160x160" /> :worker-id="user.id"
</QAvatar> :title="user.name"
size="xxl"
color="transparent"
/>
<QBtn
v-if="isEmployee"
class="q-mt-sm q-px-md"
:to="`/worker/${user.id}`"
color="primary"
:label="t('globals.myAccount')"
dense
/>
<div class="text-subtitle1 q-mt-md"> <div class="text-subtitle1 q-mt-md">
<strong>{{ user.nickname }}</strong> <strong>{{ user.nickname }}</strong>
</div> </div>
@ -168,7 +178,7 @@ const isEmployee = computed(() => useRole().isEmployee());
</div> </div>
<QBtn <QBtn
id="logout" id="logout"
color="orange" color="primary"
flat flat
:label="t('globals.logOut')" :label="t('globals.logOut')"
size="sm" size="sm"

View File

@ -151,7 +151,7 @@ const col = computed(() => {
}; };
} }
if ( if (
(newColumn.name.startsWith('is') || newColumn.name.startsWith('has')) && (/^is[A-Z]/.test(newColumn.name) || /^has[A-Z]/.test(newColumn.name)) &&
newColumn.component == null newColumn.component == null
) )
newColumn.component = 'checkbox'; newColumn.component = 'checkbox';

View File

@ -107,7 +107,9 @@ const orders = ref(parseOrder(routeQuery.filter?.order));
const CrudModelRef = ref({}); const CrudModelRef = ref({});
const showForm = ref(false); const showForm = ref(false);
const splittedColumns = ref({ columns: [] }); const splittedColumns = ref({ columns: [] });
const columnsVisibilitySkiped = ref(); const columnsVisibilitySkipped = ref();
const createForm = ref();
const tableModes = [ const tableModes = [
{ {
icon: 'view_column', icon: 'view_column',
@ -133,12 +135,20 @@ onMounted(() => {
? CARD_MODE ? CARD_MODE
: $props.defaultMode; : $props.defaultMode;
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
columnsVisibilitySkiped.value = [ columnsVisibilitySkipped.value = [
...splittedColumns.value.columns ...splittedColumns.value.columns
.filter((c) => c.visible == false) .filter((c) => c.visible == false)
.map((c) => c.name), .map((c) => c.name),
...['tableActions'], ...['tableActions'],
]; ];
createForm.value = $props.create;
if ($props.create && route?.query?.createForm) {
showForm.value = true;
createForm.value = {
...createForm.value,
...{ formInitialData: JSON.parse(route?.query?.createForm) },
};
}
}); });
watch( watch(
@ -154,24 +164,34 @@ watch(
const isTableMode = computed(() => mode.value == TABLE_MODE); const isTableMode = computed(() => mode.value == TABLE_MODE);
function setUserParams(watchedParams) { function setUserParams(watchedParams, watchedOrder) {
if (!watchedParams) return; if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
const filter = const filter =
typeof watchedParams?.filter == 'string' typeof watchedParams?.filter == 'string'
? JSON.parse(watchedParams?.filter) ? JSON.parse(watchedParams?.filter ?? '{}')
: watchedParams?.filter; : watchedParams?.filter;
const where = filter?.where; const where = filter?.where;
const order = filter?.order; const order = watchedOrder ?? filter?.order;
watchedParams = { ...watchedParams, ...where }; watchedParams = { ...watchedParams, ...where };
delete watchedParams.filter; delete watchedParams.filter;
delete params.value?.filter; delete params.value?.filter;
params.value = { ...params.value, ...watchedParams }; params.value = { ...params.value, ...sanitizer(watchedParams) };
orders.value = parseOrder(order); orders.value = parseOrder(order);
} }
function sanitizer(params) {
for (const [key, value] of Object.entries(params)) {
if (typeof value == 'object') {
const param = Object.values(value)[0];
if (typeof param == 'string') params[key] = param.replaceAll('%', '');
}
}
return params;
}
function splitColumns(columns) { function splitColumns(columns) {
splittedColumns.value = { splittedColumns.value = {
columns: [], columns: [],
@ -305,7 +325,7 @@ defineExpose({
col?.columnFilter !== false && col?.columnFilter !== false &&
col?.name !== 'tableActions' col?.name !== 'tableActions'
" "
v-model="orders[col.name]" v-model="orders[col.orderBy ?? col.name]"
:name="col.orderBy ?? col.name" :name="col.orderBy ?? col.name"
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
:search-url="searchUrl" :search-url="searchUrl"
@ -322,308 +342,291 @@ defineExpose({
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<!-- class in div to fix warn--> <!-- class in div to fix warn-->
<div class="q-px-md">
<CrudModel <CrudModel
v-bind="$attrs" v-bind="$attrs"
:limit="20" :class="$attrs['class'] ?? 'q-px-md'"
ref="CrudModelRef" :limit="20"
@on-fetch="(...args) => emit('onFetch', ...args)" ref="CrudModelRef"
:search-url="searchUrl" @on-fetch="(...args) => emit('onFetch', ...args)"
:disable-infinite-scroll="isTableMode" :search-url="searchUrl"
@save-changes="reload" :disable-infinite-scroll="isTableMode"
:has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable" @save-changes="reload"
:auto-load="hasParams || $attrs['auto-load']" :has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable"
> :auto-load="hasParams || $attrs['auto-load']"
<template >
v-for="(_, slotName) in $slots" <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
#[slotName]="slotData" <slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
:key="slotName" </template>
<template #body="{ rows }">
<QTable
v-bind="table"
class="vnTable"
:columns="splittedColumns.columns"
:rows="rows"
v-model:selected="selected"
:grid="!isTableMode"
table-header-class="bg-header"
card-container-class="grid-three"
flat
:style="isTableMode && `max-height: ${tableHeight}`"
virtual-scroll
@virtual-scroll="
(event) =>
event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate()
"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@update:selected="emit('update:selected', $event)"
> >
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" /> <template #top-left v-if="!$props.withoutHeader">
</template> <slot name="top-left"></slot>
<template #body="{ rows }"> </template>
<QTable <template #top-right v-if="!$props.withoutHeader">
v-bind="table" <VnVisibleColumn
class="vnTable" v-if="isTableMode"
:columns="splittedColumns.columns" v-model="splittedColumns.columns"
:rows="rows" :table-code="tableCode ?? route.name"
v-model:selected="selected" :skip="columnsVisibilitySkipped"
:grid="!isTableMode" />
table-header-class="bg-header" <QBtnToggle
card-container-class="grid-three" v-model="mode"
flat toggle-color="primary"
:style="isTableMode && `max-height: ${tableHeight}`" class="bg-vn-section-color"
virtual-scroll dense
@virtual-scroll=" :options="tableModes.filter((mode) => !mode.disable)"
(event) => />
event.index > rows.length - 2 && <QBtn
CrudModelRef.vnPaginateRef.paginate() v-if="$props.rightSearch"
" icon="filter_alt"
@row-click="(_, row) => rowClickFunction && rowClickFunction(row)" class="bg-vn-section-color q-ml-md"
@update:selected="emit('update:selected', $event)" dense
> @click="stateStore.toggleRightDrawer()"
<template #top-left v-if="!$props.withoutHeader"> />
<slot name="top-left"></slot> </template>
</template> <template #header-cell="{ col }">
<template #top-right v-if="!$props.withoutHeader"> <QTh v-if="col.visible ?? true">
<VnVisibleColumn <div
v-if="isTableMode" class="column self-start q-ml-xs ellipsis"
v-model="splittedColumns.columns" :class="`text-${col?.align ?? 'left'}`"
:table-code="tableCode ?? route.name" :style="$props.columnSearch ? 'height: 75px' : ''"
:skip="columnsVisibilitySkiped" >
/> <div class="row items-center no-wrap" style="height: 30px">
<QBtnToggle <QTooltip v-if="col.toolTip">{{ col.toolTip }}</QTooltip>
v-model="mode" <VnTableOrder
toggle-color="primary" v-model="orders[col.orderBy ?? col.name]"
class="bg-vn-section-color" :name="col.orderBy ?? col.name"
dense :label="col?.label"
:options="tableModes"
/>
<QBtn
v-if="$props.rightSearch"
icon="filter_alt"
title="asd"
class="bg-vn-section-color q-ml-md"
dense
@click="stateStore.toggleRightDrawer()"
/>
</template>
<template #header-cell="{ col }">
<QTh v-if="col.visible ?? true">
<div
class="column self-start q-ml-xs ellipsis"
:class="`text-${col?.align ?? 'left'}`"
:style="$props.columnSearch ? 'height: 75px' : ''"
>
<div
class="row items-center no-wrap"
style="height: 30px"
>
<VnTableOrder
v-model="orders[col.name]"
:name="col.orderBy ?? col.name"
:label="col?.label"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
/>
</div>
<VnTableFilter
v-if="$props.columnSearch"
:column="col"
:show-title="true"
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl" :search-url="searchUrl"
class="full-width"
/> />
</div> </div>
</QTh> <VnTableFilter
</template> v-if="$props.columnSearch"
<template #header-cell-tableActions> :column="col"
<QTh auto-width class="sticky" /> :show-title="true"
</template> :data-key="$attrs['data-key']"
<template #body-cell-tableStatus="{ col, row }"> v-model="params[columnName(col)]"
<QTd auto-width :class="getColAlign(col)"> :search-url="searchUrl"
<VnTableChip class="full-width"
:columns="splittedColumns.columnChips" />
</div>
</QTh>
</template>
<template #header-cell-tableActions>
<QTh auto-width class="sticky" />
</template>
<template #body-cell-tableStatus="{ col, row }">
<QTd auto-width :class="getColAlign(col)">
<VnTableChip :columns="splittedColumns.columnChips" :row="row">
<template #afterChip>
<slot name="afterChip" :row="row"></slot>
</template>
</VnTableChip>
</QTd>
</template>
<template #body-cell="{ col, row, rowIndex }">
<!-- Columns -->
<QTd
auto-width
class="no-margin q-px-xs"
:class="[getColAlign(col), col.columnClass]"
v-if="col.visible ?? true"
@click.ctrl="
($event) =>
rowCtrlClickFunction && rowCtrlClickFunction($event, row)
"
>
<slot
:name="`column-${col.name}`"
:col="col"
:row="row"
:row-index="rowIndex"
>
<VnTableColumn
:column="col"
:row="row" :row="row"
> :is-editable="col.isEditable ?? isEditable"
<template #afterChip> v-model="row[col.name]"
<slot name="afterChip" :row="row"></slot> component-prop="columnField"
</template> />
</VnTableChip> </slot>
</QTd> </QTd>
</template> </template>
<template #body-cell="{ col, row, rowIndex }"> <template #body-cell-tableActions="{ col, row }">
<!-- Columns --> <QTd
<QTd auto-width
auto-width :class="getColAlign(col)"
class="no-margin q-px-xs" class="sticky no-padding"
:class="[getColAlign(col), col.columnClass]" @click="stopEventPropagation($event)"
v-if="col.visible ?? true" >
@click.ctrl=" <QBtn
($event) => v-for="(btn, index) of col.actions"
rowCtrlClickFunction && :key="index"
rowCtrlClickFunction($event, row) :title="btn.title"
:icon="btn.icon"
class="q-px-sm"
flat
:class="
btn.isPrimary ? 'text-primary-light' : 'color-vn-text '
"
:style="`visibility: ${
(btn.show && btn.show(row)) ?? true ? 'visible' : 'hidden'
}`"
@click="btn.action(row)"
/>
</QTd>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
:to="`/${$props.redirect}/` + row.id"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
" "
> >
<slot <QCardSection
:name="`column-${col.name}`" vertical
:col="col" class="no-margin no-padding"
:row="row" :class="colsMap.tableActions ? 'w-80' : 'fit'"
:row-index="rowIndex"
>
<VnTableColumn
:column="col"
:row="row"
:is-editable="col.isEditable ?? isEditable"
v-model="row[col.name]"
component-prop="columnField"
/>
</slot>
</QTd>
</template>
<template #body-cell-tableActions="{ col, row }">
<QTd
auto-width
:class="getColAlign(col)"
class="sticky no-padding"
@click="stopEventPropagation($event)"
>
<QBtn
v-for="(btn, index) of col.actions"
:key="index"
:title="btn.title"
:icon="btn.icon"
class="q-px-sm"
flat
:class="
btn.isPrimary
? 'text-primary-light'
: 'color-vn-text '
"
:style="`visibility: ${
(btn.show && btn.show(row)) ?? true
? 'visible'
: 'hidden'
}`"
@click="btn.action(row)"
/>
</QTd>
</template>
<template #item="{ row, colsMap }">
<component
:is="$props.redirect ? 'router-link' : 'span'"
:to="`/${$props.redirect}/` + row.id"
>
<QCard
bordered
flat
class="row no-wrap justify-between cursor-pointer"
@click="
(_, row) => {
$props.rowClick && $props.rowClick(row);
}
"
> >
<!-- Chips -->
<QCardSection <QCardSection
vertical v-if="splittedColumns.chips.length"
class="no-margin no-padding" class="no-margin q-px-xs q-py-none"
:class="colsMap.tableActions ? 'w-80' : 'fit'"
> >
<!-- Chips --> <VnTableChip
<QCardSection :columns="splittedColumns.chips"
v-if="splittedColumns.chips.length" :row="row"
class="no-margin q-px-xs q-py-none"
> >
<VnTableChip <template #afterChip>
:columns="splittedColumns.chips" <slot name="afterChip" :row="row"></slot>
:row="row" </template>
> </VnTableChip>
<template #afterChip> </QCardSection>
<slot name="afterChip" :row="row"></slot> <!-- Title -->
</template> <QCardSection
</VnTableChip> v-if="splittedColumns.title"
</QCardSection> class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis"
<!-- Title --> >
<QCardSection <span
v-if="splittedColumns.title" :title="row[splittedColumns.title.name]"
class="q-pl-sm q-py-none text-primary-light text-bold text-h6 cardEllipsis" @click="stopEventPropagation($event)"
class="cursor-text"
> >
<span {{ row[splittedColumns.title.name] }}
:title="row[splittedColumns.title.name]" </span>
@click="stopEventPropagation($event)" </QCardSection>
class="cursor-text" <!-- Fields -->
> <QCardSection
{{ row[splittedColumns.title.name] }} class="q-pl-sm q-pr-lg q-py-xs"
</span> :class="$props.cardClass"
</QCardSection> >
<!-- Fields --> <div
<QCardSection v-for="(
class="q-pl-sm q-pr-lg q-py-xs" col, index
:class="$props.cardClass" ) of splittedColumns.cardVisible"
:key="col.name"
class="fields"
> >
<div <VnLv
v-for="( :label="
col, index !col.component && col.label
) of splittedColumns.cardVisible" ? `${col.label}:`
:key="col.name" : ''
class="fields" "
> >
<VnLv <template #value>
:label=" <span
!col.component && col.label @click="stopEventPropagation($event)"
? `${col.label}:` >
: '' <slot
" :name="`column-${col.name}`"
> :col="col"
<template #value> :row="row"
<span :row-index="index"
@click="
stopEventPropagation($event)
"
> >
<slot <VnTableColumn
:name="`column-${col.name}`" :column="col"
:col="col"
:row="row" :row="row"
:row-index="index" :is-editable="false"
> v-model="row[col.name]"
<VnTableColumn component-prop="columnField"
:column="col" :show-label="true"
:row="row" />
:is-editable="false" </slot>
v-model="row[col.name]" </span>
component-prop="columnField" </template>
:show-label="true" </VnLv>
/> </div>
</slot>
</span>
</template>
</VnLv>
</div>
</QCardSection>
</QCardSection> </QCardSection>
<!-- Actions --> </QCardSection>
<QCardSection <!-- Actions -->
v-if="colsMap.tableActions" <QCardSection
class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs" v-if="colsMap.tableActions"
@click="stopEventPropagation($event)" class="column flex-center w-10 no-margin q-pa-xs q-gutter-y-xs"
> @click="stopEventPropagation($event)"
<QBtn >
v-for="(btn, index) of splittedColumns.actions <QBtn
.actions" v-for="(btn, index) of splittedColumns.actions
:key="index" .actions"
:title="btn.title" :key="index"
:icon="btn.icon" :title="btn.title"
class="q-pa-xs" :icon="btn.icon"
flat class="q-pa-xs"
:class=" flat
btn.isPrimary :class="
? 'text-primary-light' btn.isPrimary
: 'color-vn-text ' ? 'text-primary-light'
" : 'color-vn-text '
@click="btn.action(row)" "
/> @click="btn.action(row)"
</QCardSection> />
</QCard> </QCardSection>
</component> </QCard>
</template> </component>
</QTable> </template>
</template> </QTable>
</CrudModel> </template>
</div> </CrudModel>
<QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2"> <QPageSticky v-if="create" :offset="[20, 20]" style="z-index: 2">
<QBtn @click="showForm = !showForm" color="primary" fab icon="add" /> <QBtn @click="showForm = !showForm" color="primary" fab icon="add" shortcut="+" />
<QTooltip> <QTooltip>
{{ create.title }} {{ createForm.title }}
</QTooltip> </QTooltip>
</QPageSticky> </QPageSticky>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> <QDialog v-model="showForm" transition-show="scale" transition-hide="scale">
<FormModelPopup <FormModelPopup
v-bind="create" v-bind="createForm"
:model="$attrs['data-key'] + 'Create'" :model="$attrs['data-key'] + 'Create'"
@on-data-saved="(_, res) => create.onDataSaved(res)" @on-data-saved="(_, res) => createForm.onDataSaved(res)"
> >
<template #form-inputs="{ data }"> <template #form-inputs="{ data }">
<div class="grid-create"> <div class="grid-create">

View File

@ -65,7 +65,7 @@ async function fetchViewConfigData() {
const userConfig = await getConfig('UserConfigViews', { const userConfig = await getConfig('UserConfigViews', {
where: { where: {
...defaultFilter.where, ...defaultFilter.where,
...{ userFk: user.id }, ...{ userFk: user.value.id },
}, },
}); });

View File

@ -37,7 +37,7 @@ const stateStore = useStateStore();
@click="stateStore.toggleRightDrawer()" @click="stateStore.toggleRightDrawer()"
round round
dense dense
icon="menu" icon="dock_to_left"
> >
<QTooltip bottom anchor="bottom right"> <QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }} {{ t('globals.collapseMenu') }}
@ -46,7 +46,7 @@ const stateStore = useStateStore();
</div> </div>
</Teleport> </Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit">
<div id="right-panel"></div> <div id="right-panel"></div>
<slot v-if="!hasContent" name="right-panel" /> <slot v-if="!hasContent" name="right-panel" />
</QScrollArea> </QScrollArea>

View File

@ -8,7 +8,6 @@ import VnSubToolbar from '../ui/VnSubToolbar.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import LeftMenu from 'components/LeftMenu.vue'; import LeftMenu from 'components/LeftMenu.vue';
import RightMenu from 'components/common/RightMenu.vue'; import RightMenu from 'components/common/RightMenu.vue';
const props = defineProps({ const props = defineProps({
dataKey: { type: String, required: true }, dataKey: { type: String, required: true },
baseUrl: { type: String, default: undefined }, baseUrl: { type: String, default: undefined },
@ -17,12 +16,7 @@ const props = defineProps({
descriptor: { type: Object, required: true }, descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined }, filterPanel: { type: Object, default: undefined },
searchDataKey: { type: String, default: undefined }, searchDataKey: { type: String, default: undefined },
searchUrl: { type: String, default: undefined }, searchbarProps: { type: Object, default: undefined },
searchbarLabel: { type: String, default: '' },
searchbarInfo: { type: String, default: '' },
searchCustomRouteRedirect: { type: String, default: undefined },
searchRedirect: { type: Boolean, default: true },
searchMakeFetch: { type: Boolean, default: true },
}); });
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -31,7 +25,10 @@ const url = computed(() => {
if (props.baseUrl) return `${props.baseUrl}/${route.params.id}`; if (props.baseUrl) return `${props.baseUrl}/${route.params.id}`;
return props.customUrl; return props.customUrl;
}); });
const searchRightDataKey = computed(() => {
if (!props.searchDataKey) return route.name;
return props.searchDataKey;
});
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData(props.dataKey, {
url: url.value, url: url.value,
filter: props.filter, filter: props.filter,
@ -65,26 +62,18 @@ if (props.baseUrl) {
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<slot name="searchbar" v-if="props.searchDataKey"> <slot name="searchbar" v-if="props.searchDataKey">
<VnSearchbar <VnSearchbar :data-key="props.searchDataKey" v-bind="props.searchbarProps" />
:data-key="props.searchDataKey"
:url="props.searchUrl"
:label="props.searchbarLabel"
:info="props.searchbarInfo"
:custom-route-redirect-name="searchCustomRouteRedirect"
:redirect="searchRedirect"
/>
</slot> </slot>
<slot v-else name="searchbar" />
<RightMenu> <RightMenu>
<template #right-panel v-if="props.filterPanel"> <template #right-panel v-if="props.filterPanel">
<component :is="props.filterPanel" :data-key="props.searchDataKey" /> <component :is="props.filterPanel" :data-key="searchRightDataKey" />
</template> </template>
</RightMenu> </RightMenu>
<QPageContainer> <QPageContainer>
<QPage> <QPage>
<VnSubToolbar /> <VnSubToolbar />
<div :class="[useCardSize(), $attrs.class]"> <div :class="[useCardSize(), $attrs.class]">
<RouterView /> <RouterView :key="route.fullPath" />
</div> </div>
</QPage> </QPage>
</QPageContainer> </QPageContainer>

View File

@ -1,34 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useCapitalize } from 'src/composables/useCapitalize';
import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
});
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const amount = computed({
get() {
return props.modelValue;
},
set(val) {
emit('update:modelValue', val);
},
});
</script>
<template>
<VnInput
v-model="amount"
type="number"
step="any"
:label="useCapitalize(t('amount'))"
/>
</template>
<i18n>
es:
amount: importe
</i18n>

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useValidator } from 'src/composables/useValidator';
const emit = defineEmits([ const emit = defineEmits([
'update:modelValue', 'update:modelValue',
@ -27,9 +28,11 @@ const $props = defineProps({
default: true, default: true,
}, },
}); });
const { validations } = useValidator();
const { t } = useI18n(); const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); const requiredFieldRule = (val) => validations().required($attrs.required, val);
const vnInputRef = ref(null); const vnInputRef = ref(null);
const value = computed({ const value = computed({
get() { get() {
@ -57,21 +60,22 @@ const focus = () => {
defineExpose({ defineExpose({
focus, focus,
}); });
import { useAttrs } from 'vue';
const $attrs = useAttrs();
const inputRules = [ const mixinRules = [
requiredFieldRule,
...($attrs.rules ?? []),
(val) => { (val) => {
const { min } = vnInputRef.value.$attrs; const { min } = vnInputRef.value.$attrs;
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 });
}, },
]; ];
</script> </script>
<template> <template>
<div <div @mouseover="hover = true" @mouseleave="hover = false">
@mouseover="hover = true"
@mouseleave="hover = false"
:rules="$attrs.required ? [requiredFieldRule] : null"
>
<QInput <QInput
ref="vnInputRef" ref="vnInputRef"
v-model="value" v-model="value"
@ -80,7 +84,7 @@ const inputRules = [
:class="{ required: $attrs.required }" :class="{ required: $attrs.required }"
@keyup.enter="emit('keyup.enter')" @keyup.enter="emit('keyup.enter')"
:clearable="false" :clearable="false"
:rules="inputRules" :rules="mixinRules"
:lazy-rules="true" :lazy-rules="true"
hide-bottom-space hide-bottom-space
> >

View File

@ -3,7 +3,7 @@ import { onMounted, watch, computed, ref } from 'vue';
import { date } from 'quasar'; import { date } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const model = defineModel({ type: String }); const model = defineModel({ type: [String, Date] });
const $props = defineProps({ const $props = defineProps({
isOutlined: { isOutlined: {
type: Boolean, type: Boolean,

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { watch, computed, ref, nextTick } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { date } from 'quasar'; import { date } from 'quasar';
@ -14,13 +14,13 @@ const props = defineProps({
default: false, default: false,
}, },
}); });
const initialDate = ref(model.value ?? Date.vnNew());
const { t } = useI18n(); const { t } = useI18n();
const requiredFieldRule = (val) => !!val || t('globals.fieldRequired'); const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
const dateFormat = 'HH:mm'; const dateFormat = 'HH:mm';
const isPopupOpen = ref(); const isPopupOpen = ref();
const hover = ref(); const hover = ref();
const inputRef = ref();
const styleAttrs = computed(() => { const styleAttrs = computed(() => {
return props.isOutlined return props.isOutlined
@ -50,7 +50,8 @@ const formattedTime = computed({
} }
if (!props.timeOnly) { if (!props.timeOnly) {
const [hh, mm] = time.split(':'); const [hh, mm] = time.split(':');
const date = model.value ?? Date.vnNew();
const date = new Date(model.value ? model.value : initialDate.value);
date.setHours(hh, mm, 0); date.setHours(hh, mm, 0);
time = date?.toISOString(); time = date?.toISOString();
} }
@ -62,37 +63,10 @@ const formattedTime = computed({
function dateToTime(newDate) { function dateToTime(newDate) {
return date.formatDate(new Date(newDate), dateFormat); return date.formatDate(new Date(newDate), dateFormat);
} }
const timeField = ref();
watch(
() => model.value,
(val) => (formattedTime.value = val),
{ immediate: true }
);
watch(
() => formattedTime.value,
async (val) => {
let position = 3;
const input = inputRef.value?.getNativeElement();
if (!val || !input) return;
let [hh, mm] = val.split(':');
hh = parseInt(hh);
if (hh >= 10 || mm != '00') return;
await nextTick();
await nextTick();
if (!hh) position = 0;
input.setSelectionRange(position, position);
},
{ immediate: true }
);
</script> </script>
<template> <template>
<div @mouseover="hover = true" @mouseleave="hover = false"> <div @mouseover="hover = true" @mouseleave="hover = false">
<QInput <QInput
ref="inputRef"
class="vn-input-time" class="vn-input-time"
mask="##:##" mask="##:##"
placeholder="--:--" placeholder="--:--"
@ -102,7 +76,7 @@ watch(
style="min-width: 100px" style="min-width: 100px"
:rules="$attrs.required ? [requiredFieldRule] : null" :rules="$attrs.required ? [requiredFieldRule] : null"
@click="isPopupOpen = false" @click="isPopupOpen = false"
@focus="inputRef.getNativeElement().setSelectionRange(0, 0)" type="time"
> >
<template #append> <template #append>
<QIcon <QIcon
@ -149,8 +123,12 @@ watch(
border-style: solid; border-style: solid;
} }
</style> </style>
<style lang="scss" scoped>
:deep(input[type='time']::-webkit-calendar-picker-indicator) {
display: none;
}
</style>
<i18n> <i18n>
es: es:
Open time: Abrir tiempo Open time: Abrir tiempo
</i18n> </i18n>
, nextTick

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, onUnmounted } from 'vue'; import { ref, onUnmounted, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { date } from 'quasar'; import { date } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
@ -19,6 +19,7 @@ const stateStore = useStateStore();
const validationsStore = useValidator(); const validationsStore = useValidator();
const { models } = validationsStore; const { models } = validationsStore;
const route = useRoute(); const route = useRoute();
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
model: { model: {
@ -213,7 +214,7 @@ function getLogTree(data) {
} }
nLogs++; nLogs++;
modelLog.logs.push(log); modelLog.logs.push(log);
modelLog.summaryId = modelLog.logs[0].summaryId;
// Changes // Changes
const notDelete = log.action != 'delete'; const notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || {}; const olds = (notDelete ? log.oldInstance : null) || {};
@ -381,6 +382,13 @@ setLogTree();
onUnmounted(() => { onUnmounted(() => {
stateStore.rightDrawer = false; stateStore.rightDrawer = false;
}); });
watch(
() => router.currentRoute.value.params.id,
() => {
applyFilter();
}
);
</script> </script>
<template> <template>
<FetchData <FetchData
@ -399,9 +407,12 @@ onUnmounted(() => {
@on-fetch=" @on-fetch="
(data) => (data) =>
(actions = data.map((item) => { (actions = data.map((item) => {
const changedModel = item.changedModel;
return { return {
locale: useCapitalize(validations[item.changedModel].locale.name), locale: useCapitalize(
value: item.changedModel, validations[changedModel]?.locale?.name ?? changedModel
),
value: changedModel,
}; };
})) }))
" "
@ -464,12 +475,17 @@ onUnmounted(() => {
> >
{{ t(modelLog.modelI18n) }} {{ t(modelLog.modelI18n) }}
</QChip> </QChip>
<span class="model-id" v-if="modelLog.summaryId"
>#{{ modelLog.summaryId }}</span <span
> class="model-id q-mr-xs"
<span class="model-value" :title="modelLog.showValue"> v-if="modelLog.summaryId"
{{ modelLog.showValue }} v-text="`#${modelLog.summaryId}`"
</span> />
<span
class="model-value"
:title="modelLog.showValue"
v-text="modelLog.showValue"
/>
<QBtn <QBtn
flat flat
round round

View File

@ -49,6 +49,10 @@ const $props = defineProps({
type: Array, type: Array,
default: null, default: null,
}, },
include: {
type: [Object, Array],
default: null,
},
where: { where: {
type: Object, type: Object,
default: null, default: null,
@ -69,6 +73,14 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
params: {
type: Object,
default: null,
},
noOne: {
type: Boolean,
default: false,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -81,6 +93,11 @@ const myOptionsOriginal = ref([]);
const vnSelectRef = ref(); const vnSelectRef = ref();
const dataRef = ref(); const dataRef = ref();
const lastVal = ref(); const lastVal = ref();
const noOneText = t('globals.noOne');
const noOneOpt = ref({
[optionValue.value]: false,
[optionLabel.value]: noOneText,
});
const value = computed({ const value = computed({
get() { get() {
@ -96,9 +113,11 @@ watch(options, (newValue) => {
setOptions(newValue); setOptions(newValue);
}); });
watch(modelValue, (newValue) => { watch(modelValue, async (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue)) if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue); await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
}); });
onMounted(() => { onMounted(() => {
@ -142,20 +161,26 @@ function filter(val, options) {
async function fetchFilter(val) { async function fetchFilter(val) {
if (!$props.url || !dataRef.value) return; if (!$props.url || !dataRef.value) return;
const { fields, sortBy, limit } = $props; const { fields, include, sortBy, limit } = $props;
const key = const key =
optionFilterValue.value ?? optionFilterValue.value ??
(new RegExp(/\d/g).test(val) (new RegExp(/\d/g).test(val)
? optionValue.value ? optionValue.value
: optionFilter.value ?? optionLabel.value); : optionFilter.value ?? optionLabel.value);
const defaultWhere = $props.useLike let defaultWhere = {};
? { [key]: { like: `%${val}%` } } if ($props.filterOptions.length) {
: { [key]: val }; defaultWhere = $props.filterOptions.reduce((obj, prop) => {
if (!obj.or) obj.or = [];
obj.or.push({ [prop]: getVal(val) });
return obj;
}, {});
} else defaultWhere = { [key]: getVal(val) };
const where = { ...(val ? defaultWhere : {}), ...$props.where }; const where = { ...(val ? defaultWhere : {}), ...$props.where };
const fetchOptions = { where, limit }; const fetchOptions = { where, include, limit };
if (fields) fetchOptions.fields = fields; if (fields) fetchOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy; if (sortBy) fetchOptions.order = sortBy;
return dataRef.value.fetch(fetchOptions); return dataRef.value.fetch(fetchOptions);
} }
@ -176,6 +201,9 @@ async function filterHandler(val, update) {
} else newOptions = filter(val, myOptionsOriginal.value); } else newOptions = filter(val, myOptionsOriginal.value);
update( update(
() => { () => {
if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase()))
newOptions.unshift(noOneOpt.value);
myOptions.value = newOptions; myOptions.value = newOptions;
}, },
(ref) => { (ref) => {
@ -190,6 +218,8 @@ async function filterHandler(val, update) {
function nullishToTrue(value) { function nullishToTrue(value) {
return value ?? true; return value ?? true;
} }
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
</script> </script>
<template> <template>
@ -201,6 +231,7 @@ function nullishToTrue(value) {
:limit="limit" :limit="limit"
:sort-by="sortBy" :sort-by="sortBy"
:fields="fields" :fields="fields"
:params="params"
/> />
<QSelect <QSelect
v-model="value" v-model="value"

View File

@ -31,7 +31,7 @@ const dialog = ref(null);
<div class="container order-catalog-item overflow-hidden"> <div class="container order-catalog-item overflow-hidden">
<QCard class="card shadow-6"> <QCard class="card shadow-6">
<div class="img-wrapper"> <div class="img-wrapper">
<VnImg :id="item.id" zoom-size="lg" class="image" /> <VnImg :id="item.id" class="image" />
<div v-if="item.hex && isCatalog" class="item-color-container"> <div v-if="item.hex && isCatalog" class="item-color-container">
<div <div
class="item-color" class="item-color"

View File

@ -2,10 +2,6 @@
import { computed } from 'vue'; import { computed } from 'vue';
const $props = defineProps({ const $props = defineProps({
maxLength: {
type: Number,
required: true,
},
item: { item: {
type: Object, type: Object,
required: true, required: true,

View File

@ -73,8 +73,7 @@ const containerClasses = computed(() => {
.q-calendar-month__head--workweek, .q-calendar-month__head--workweek,
.q-calendar-month__head--weekday, .q-calendar-month__head--weekday,
// .q-calendar-month__workweek.q-past-day, // .q-calendar-month__workweek.q-past-day,
.q-calendar-month__week :nth-child(6), .q-calendar-month__week :nth-child(n+6):nth-child(-n+7) {
:nth-child(7) {
color: var(--vn-label-color); color: var(--vn-label-color);
} }

View File

@ -1,45 +1,62 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { useColor } from 'src/composables/useColor'; import { useColor } from 'src/composables/useColor';
import { getCssVar } from 'quasar';
const $props = defineProps({ const $props = defineProps({
workerId: { type: Number, required: true }, workerId: { type: Number, required: true },
description: { type: String, default: null }, description: { type: String, default: null },
size: { type: String, default: null },
title: { type: String, default: null }, title: { type: String, default: null },
color: { type: String, default: null },
}); });
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); const token = getTokenMultimedia();
const { t } = useI18n(); const { t } = useI18n();
const title = computed(() => $props.title ?? t('globals.system')); const src = computed(
() => `/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`
);
const title = computed(() => $props.title?.toUpperCase() || t('globals.system'));
const showLetter = ref(false); const showLetter = ref(false);
const backgroundColor = computed(() => {
const color = $props.color || useColor(title.value);
return getCssVar(color) || color;
});
watch(src, () => (showLetter.value = false));
</script> </script>
<template> <template>
<div class="avatar-picture column items-center"> <div class="column items-center">
<QAvatar <QAvatar
:style="{ :style="{ backgroundColor }"
backgroundColor: useColor(title), v-bind="$attrs"
}" :title="title || t('globals.system')"
:size="$props.size"
:title="title"
> >
<template v-if="showLetter">{{ title.charAt(0) }}</template> <template v-if="showLetter">
<QImg {{ title.charAt(0) }}
v-else </template>
:src="`/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`" <QImg v-else :src="src" spinner-color="white" @error="showLetter = true" />
spinner-color="white"
@error="showLetter = true"
/>
</QAvatar> </QAvatar>
<div class="description"> <div class="description">
<slot name="description" v-if="$props.description"> <slot name="description" v-if="description">
<p> <p v-text="description" />
{{ $props.description }}
</p>
</slot> </slot>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
[size='xxl'] {
.q-avatar,
.q-img {
width: 80px;
height: 80px;
}
.q-img {
object-fit: cover;
}
}
</style>

View File

@ -15,7 +15,7 @@ const props = defineProps({
default: null, default: null,
}, },
message: { message: {
type: String, type: [String, Boolean],
default: null, default: null,
}, },
data: { data: {
@ -35,7 +35,10 @@ defineEmits(['confirm', ...useDialogPluginComponent.emits]);
const { dialogRef, onDialogOK } = useDialogPluginComponent(); const { dialogRef, onDialogOK } = useDialogPluginComponent();
const title = props.title || t('Confirm'); const title = props.title || t('Confirm');
const message = props.message || t('Are you sure you want to continue?'); const message =
props.message ||
(props.message !== false ? t('Are you sure you want to continue?') : false);
const isLoading = ref(false); const isLoading = ref(false);
async function confirm() { async function confirm() {
@ -61,12 +64,12 @@ async function confirm() {
size="xl" size="xl"
v-if="icon" v-if="icon"
/> />
<span class="text-h6 text-grey">{{ title }}</span> <span class="text-h6">{{ title }}</span>
<QSpace /> <QSpace />
<QBtn icon="close" :disable="isLoading" flat round dense v-close-popup /> <QBtn icon="close" :disable="isLoading" flat round dense v-close-popup />
</QCardSection> </QCardSection>
<QCardSection class="row items-center"> <QCardSection class="row items-center">
<span v-html="message"></span> <span v-if="message !== false" v-html="message" />
<slot name="customHTML"></slot> <slot name="customHTML"></slot>
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">

View File

@ -24,7 +24,7 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
unRemovableParams: { unremovableParams: {
type: Array, type: Array,
required: false, required: false,
default: () => [], default: () => [],
@ -37,7 +37,7 @@ const $props = defineProps({
}, },
hiddenTags: { hiddenTags: {
type: Array, type: Array,
default: () => ['filter'], default: () => ['filter', 'search', 'or', 'and'],
}, },
customTags: { customTags: {
type: Array, type: Array,
@ -82,23 +82,28 @@ onMounted(() => {
}); });
function setUserParams(watchedParams) { function setUserParams(watchedParams) {
if (!watchedParams) return; if (!watchedParams || Object.keys(watchedParams).length == 0) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
if (typeof watchedParams?.filter == 'string')
watchedParams.filter = JSON.parse(watchedParams.filter);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where }; watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
const order = watchedParams.filter?.order;
delete watchedParams.filter; delete watchedParams.filter;
userParams.value = { ...userParams.value, ...watchedParams }; userParams.value = sanitizer(watchedParams);
emit('setUserParams', userParams.value); emit('setUserParams', userParams.value, order);
} }
watch( watch(
() => route.query[$props.searchUrl], () => route.query[$props.searchUrl],
(val) => setUserParams(val) (val, oldValue) => (val || oldValue) && setUserParams(val)
); );
watch( watch(
() => arrayData.store.userParams, () => arrayData.store.userParams,
(val) => setUserParams(val) (val, oldValue) => (val || oldValue) && setUserParams(val)
); );
watch( watch(
@ -108,58 +113,51 @@ watch(
const isLoading = ref(false); const isLoading = ref(false);
async function search(evt) { async function search(evt) {
if (evt && $props.disableSubmitEvent) return; try {
if (evt && $props.disableSubmitEvent) return;
store.filter.where = {}; store.filter.where = {};
isLoading.value = true; isLoading.value = true;
const filter = { ...userParams.value, ...$props.modelValue }; const filter = { ...userParams.value, ...$props.modelValue };
store.userParamsChanged = true; store.userParamsChanged = true;
const { params: newParams } = await arrayData.addFilter({ const { params: newParams } = await arrayData.addFilter({
params: filter, params: filter,
}); });
userParams.value = newParams; userParams.value = newParams;
if (!$props.showAll && !Object.values(filter).length) store.data = []; if (!$props.showAll && !Object.values(filter).length) store.data = [];
emit('search');
isLoading.value = false; } finally {
emit('search'); isLoading.value = false;
} }
async function reload() {
isLoading.value = true;
const params = Object.values(userParams.value).filter((param) => param);
store.skip = 0;
store.page = 1;
await arrayData.fetch({ append: false });
if (!$props.showAll && !params.length) store.data = [];
isLoading.value = false;
emit('refresh');
} }
async function clearFilters() { async function clearFilters() {
isLoading.value = true; try {
store.userParamsChanged = true; isLoading.value = true;
arrayData.reset(['skip', 'filter.skip', 'page']); store.userParamsChanged = true;
// Filtrar los params no removibles arrayData.reset(['skip', 'filter.skip', 'page']);
const removableFilters = Object.keys(userParams.value).filter((param) => // Filtrar los params no removibles
$props.unRemovableParams.includes(param) const removableFilters = Object.keys(userParams.value).filter((param) =>
); $props.unremovableParams.includes(param)
const newParams = {}; );
// Conservar solo los params que no son removibles const newParams = {};
for (const key of removableFilters) { // Conservar solo los params que no son removibles
newParams[key] = userParams.value[key]; for (const key of removableFilters) {
} newParams[key] = userParams.value[key];
userParams.value = {}; }
userParams.value = { ...newParams }; // Actualizar los params con los removibles userParams.value = {};
await arrayData.applyFilter({ params: userParams.value }); userParams.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: userParams.value });
if (!$props.showAll) { if (!$props.showAll) {
store.data = []; store.data = [];
}
emit('clear');
emit('update:modelValue', userParams.value);
} finally {
isLoading.value = false;
} }
isLoading.value = false;
emit('clear');
emit('update:modelValue', userParams.value);
} }
const tagsList = computed(() => { const tagsList = computed(() => {
@ -173,10 +171,10 @@ const tagsList = computed(() => {
}); });
const tags = computed(() => { const tags = computed(() => {
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key)); return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.label));
}); });
const customTags = computed(() => const customTags = computed(() =>
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key)) tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label))
); );
async function remove(key) { async function remove(key) {
@ -195,8 +193,10 @@ function formatValue(value) {
function sanitizer(params) { function sanitizer(params) {
for (const [key, value] of Object.entries(params)) { for (const [key, value] of Object.entries(params)) {
if (typeof value == 'object') if (value && typeof value === 'object') {
params[key] = Object.values(value)[0].replaceAll('%', ''); const param = Object.values(value)[0];
if (typeof param == 'string') params[key] = param.replaceAll('%', '');
}
} }
return params; return params;
} }
@ -220,32 +220,18 @@ function sanitizer(params) {
</QItemLabel> </QItemLabel>
</QItemSection> </QItemSection>
<QItemSection top side> <QItemSection top side>
<div class="q-gutter-xs"> <QBtn
<QBtn @click="clearFilters"
@click="clearFilters" color="primary"
color="primary" dense
dense flat
flat icon="filter_list_off"
icon="filter_list_off" padding="none"
padding="none" round
round size="sm"
size="sm" >
> <QTooltip>{{ t('Remove filters') }}</QTooltip>
<QTooltip>{{ t('Remove filters') }}</QTooltip> </QBtn>
</QBtn>
<QBtn
@click="reload"
color="primary"
dense
flat
icon="refresh"
padding="none"
round
size="sm"
>
<QTooltip>{{ t('Refresh') }}</QTooltip>
</QBtn>
</div>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
@ -259,7 +245,7 @@ function sanitizer(params) {
<VnFilterPanelChip <VnFilterPanelChip
v-for="chip of tags" v-for="chip of tags"
:key="chip.label" :key="chip.label"
:removable="!unRemovableParams.includes(chip.label)" :removable="!unremovableParams?.includes(chip.label)"
@remove="remove(chip.label)" @remove="remove(chip.label)"
> >
<slot name="tags" :tag="chip" :format-fn="formatValue"> <slot name="tags" :tag="chip" :format-fn="formatValue">

View File

@ -1,6 +1,8 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref } from 'vue';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import noImage from '/no-user.png';
import { useRole } from 'src/composables/useRole';
const $props = defineProps({ const $props = defineProps({
storage: { storage: {
@ -11,14 +13,17 @@ const $props = defineProps({
type: String, type: String,
default: 'catalog', default: 'catalog',
}, },
size: { resolution: {
type: String, type: String,
default: '200x200', default: '200x200',
}, },
zoomSize: { zoomResolution: {
type: String, type: String,
required: false, default: null,
default: 'lg', },
zoom: {
type: Boolean,
default: true,
}, },
id: { id: {
type: Number, type: Number,
@ -28,14 +33,16 @@ const $props = defineProps({
const show = ref(false); const show = ref(false);
const token = useSession().getTokenMultimedia(); const token = useSession().getTokenMultimedia();
const timeStamp = ref(`timestamp=${Date.now()}`); const timeStamp = ref(`timestamp=${Date.now()}`);
import noImage from '/no-user.png'; const isEmployee = useRole().isEmployee();
import { useRole } from 'src/composables/useRole';
const url = computed(() => { const getUrl = (zoom = false) => {
const isEmployee = useRole().isEmployee(); const curResolution = zoom
? $props.zoomResolution || $props.resolution
: $props.resolution;
return isEmployee return isEmployee
? `/api/${$props.storage}/${$props.collection}/${$props.size}/${$props.id}/download?access_token=${token}&${timeStamp.value}` ? `/api/${$props.storage}/${$props.collection}/${curResolution}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
: noImage; : noImage;
}); };
const reload = () => { const reload = () => {
timeStamp.value = `timestamp=${Date.now()}`; timeStamp.value = `timestamp=${Date.now()}`;
}; };
@ -45,23 +52,21 @@ defineExpose({
</script> </script>
<template> <template>
<QImg <QImg
:class="{ zoomIn: $props.zoomSize }" :class="{ zoomIn: zoom }"
:src="url" :src="getUrl()"
v-bind="$attrs" v-bind="$attrs"
@click="show = !show" @click.stop="show = $props.zoom ? true : false"
spinner-color="primary" spinner-color="primary"
/> />
<QDialog v-model="show" v-if="$props.zoomSize"> <QDialog v-if="$props.zoom" v-model="show">
<QImg <QImg
:src="url" :src="getUrl(true)"
size="full"
class="img_zoom"
v-bind="$attrs" v-bind="$attrs"
spinner-color="primary" spinner-color="primary"
class="img_zoom"
/> />
</QDialog> </QDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-img { .q-img {
&.zoomIn { &.zoomIn {

View File

@ -0,0 +1,32 @@
<script setup>
const emit = defineEmits(['submit']);
defineProps({
icon: { type: String, required: false, default: 'phonelink_lock' },
title: { type: String, required: true },
});
</script>
<template>
<QForm @submit="emit('submit')" class="q-gutter-y-md q-pa-lg formCard">
<div class="column items-center">
<QIcon v-if="icon != false" :name="icon" size="xl" color="primary" />
<h5 class="text-center q-my-md">
{{ title }}
</h5>
</div>
<slot></slot>
<div class="q-mt-lg">
<slot name="buttons"></slot>
</div>
</QForm>
</template>
<style lang="scss" scoped>
.formCard {
max-width: 350px;
min-width: 300px;
}
@media (max-width: $breakpoint-xs-max) {
.formCard {
min-width: 100%;
}
}
</style>

View File

@ -10,6 +10,10 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
class: {
type: String,
default: '',
},
autoLoad: { autoLoad: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -215,13 +219,13 @@ defineExpose({ fetch, addFilter, paginate });
v-if="store.data" v-if="store.data"
@load="onLoad" @load="onLoad"
:offset="offset" :offset="offset"
class="full-width" :class="['full-width', props.class]"
:disable="disableInfiniteScroll || !store.hasMoreData" :disable="disableInfiniteScroll || !store.hasMoreData"
v-bind="$attrs" v-bind="$attrs"
> >
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center"> <div v-if="isLoading" class="info-row q-pa-md text-center">
<QSpinner color="orange" size="md" /> <QSpinner color="primary" size="md" />
</div> </div>
</QInfiniteScroll> </QInfiniteScroll>
</template> </template>

View File

@ -1,17 +1,17 @@
<script setup>
defineProps({ wrap: { type: Boolean, default: false } });
</script>
<template> <template>
<div class="vn-row q-gutter-md q-mb-md" :class="{ wrap }"> <div class="vn-row q-gutter-md q-mb-md">
<slot></slot> <slot />
</div> </div>
</template> </template>
<style lang="scss" scopped> <style lang="scss" scoped>
.vn-row { .vn-row {
display: flex; display: flex;
> * { > :deep(*) {
flex: 1; flex: 1;
} }
&[wrap] {
flex-wrap: wrap;
}
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
.vn-row { .vn-row {

View File

@ -63,13 +63,13 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
makeFetch: { whereFilter: {
type: Boolean, type: Function,
default: true, default: undefined,
}, },
}); });
const searchText = ref(''); const searchText = ref();
let arrayDataProps = { ...props }; let arrayDataProps = { ...props };
if (props.redirect) if (props.redirect)
arrayDataProps = { arrayDataProps = {
@ -100,18 +100,23 @@ onMounted(() => {
}); });
async function search() { async function search() {
const staticParams = Object.entries(store.userParams).filter( const staticParams = Object.entries(store.userParams);
([key, value]) => value && (props.staticParams || []).includes(key)
);
arrayData.reset(['skip', 'page']); arrayData.reset(['skip', 'page']);
if (props.makeFetch) const filter = {
await arrayData.applyFilter({ params: {
params: { ...Object.fromEntries(staticParams),
...Object.fromEntries(staticParams), search: searchText.value,
search: searchText.value, },
}, };
});
if (props.whereFilter) {
filter.filter = {
where: props.whereFilter(searchText.value),
};
delete filter.params.search;
}
await arrayData.applyFilter(filter);
} }
</script> </script>
<template> <template>
@ -119,7 +124,7 @@ async function search() {
<QForm @submit="search" id="searchbarForm"> <QForm @submit="search" id="searchbarForm">
<VnInput <VnInput
id="searchbar" id="searchbar"
v-model="searchText" v-model.trim="searchText"
:placeholder="t(props.label)" :placeholder="t(props.label)"
dense dense
standout standout

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onBeforeMount } from 'vue'; import { computed } from 'vue';
import { date } from 'quasar'; import { date } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnAvatar from '../ui/VnAvatar.vue'; import VnAvatar from '../ui/VnAvatar.vue';
@ -10,31 +10,32 @@ const $props = defineProps({
where: { type: Object, default: () => {} }, where: { type: Object, default: () => {} },
}); });
const filter = { const filter = computed(() => {
fields: ['smsFk'], return {
include: { fields: ['smsFk'],
relation: 'sms', include: {
scope: { relation: 'sms',
fields: [ scope: {
'senderFk', fields: [
'sender', 'senderFk',
'destination', 'sender',
'message', 'destination',
'statusCode', 'message',
'status', 'statusCode',
'created', 'status',
], 'created',
include: { ],
relation: 'sender', include: {
scope: { relation: 'sender',
fields: ['name'], scope: {
fields: ['name'],
},
}, },
}, },
}, },
}, ...{ where: $props.where },
}; };
});
onBeforeMount(() => (filter.where = $props.where));
function formatNumber(number) { function formatNumber(number) {
if (number.length <= 10) return number; if (number.length <= 10) return number;

View File

@ -43,20 +43,9 @@ onBeforeUnmount(() => stateStore.toggleSubToolbar());
</slot> </slot>
</QToolbar> </QToolbar>
</template> </template>
<style lang="scss">
.q-toolbar {
background: var(--vn-section-color);
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.sticky { .sticky {
position: sticky; position: sticky;
top: 61px;
z-index: 1; z-index: 1;
} }
@media (max-width: $breakpoint-sm) {
.sticky {
top: 90px;
}
}
</style> </style>

View File

@ -7,5 +7,5 @@ export function getDateQBadgeColor(date) {
let comparation = today - timeTicket; let comparation = today - timeTicket;
if (comparation == 0) return 'warning'; if (comparation == 0) return 'warning';
if (comparation < 0) return 'negative'; if (comparation < 0) return 'success';
} }

View File

@ -28,7 +28,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
delete params.filter; delete params.filter;
store.userParams = { ...params, ...store.userParams }; store.userParams = { ...params, ...store.userParams };
store.userFilter = { ...filter, ...store.userFilter }; store.userFilter = { ...filter, ...store.userFilter };
if (filter.order) store.order = filter.order; if (filter?.order) store.order = filter.order;
} }
}); });

View File

@ -3,12 +3,14 @@ import { useRole } from './useRole';
import { useAcl } from './useAcl'; import { useAcl } from './useAcl';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios'; import axios from 'axios';
import { useRouter } from 'vue-router';
import useNotify from './useNotify'; import useNotify from './useNotify';
import { useTokenConfig } from './useTokenConfig'; import { useTokenConfig } from './useTokenConfig';
const TOKEN_MULTIMEDIA = 'tokenMultimedia'; const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token'; const TOKEN = 'token';
export function useSession() { export function useSession() {
const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
let isCheckingToken = false; let isCheckingToken = false;
let intervalId = null; let intervalId = null;
@ -102,6 +104,31 @@ export function useSession() {
startInterval(); startInterval();
} }
async function setLogin(data) {
const {
data: { multimediaToken },
} = await axios.get('VnUsers/ShareToken', {
headers: { Authorization: data.token },
});
if (!multimediaToken) return;
await login({
...data,
created: Date.now(),
tokenMultimedia: multimediaToken.id,
});
notify('login.loginSuccess', 'positive');
const currentRoute = router.currentRoute.value;
if (currentRoute.query?.redirect) {
router.push(currentRoute.query.redirect);
} else {
router.push({ name: 'Dashboard' });
}
}
function isLoggedIn() { function isLoggedIn() {
const localToken = localStorage.getItem(TOKEN); const localToken = localStorage.getItem(TOKEN);
const sessionToken = sessionStorage.getItem(TOKEN); const sessionToken = sessionStorage.getItem(TOKEN);
@ -163,6 +190,7 @@ export function useSession() {
setToken, setToken,
destroy, destroy,
login, login,
setLogin,
isLoggedIn, isLoggedIn,
checkValidity, checkValidity,
setSession, setSession,

View File

@ -28,7 +28,7 @@ export function useValidator() {
} }
const { t } = useI18n(); const { t } = useI18n();
const validations = function (validation) { const validations = function (validation = {}) {
return { return {
format: (value) => { format: (value) => {
const { allowNull, with: format, allowBlank } = validation; const { allowNull, with: format, allowBlank } = validation;
@ -40,12 +40,15 @@ export function useValidator() {
if (!isValid) return message; if (!isValid) return message;
}, },
presence: (value) => { presence: (value) => {
let message = `Value can't be empty`; let message = t(`globals.valueCantBeEmpty`);
if (validation.message) if (validation.message)
message = t(validation.message) || validation.message; message = t(validation.message) || validation.message;
return !validator.isEmpty(value ? String(value) : '') || message; return !validator.isEmpty(value ? String(value) : '') || message;
}, },
required: (required, value) => {
return required ? !!value || t('globals.fieldRequired') : null;
},
length: (value) => { length: (value) => {
const options = { const options = {
min: validation.min || validation.is, min: validation.min || validation.is,
@ -71,12 +74,17 @@ export function useValidator() {
return validator.isInt(value) || 'Value should be integer'; return validator.isInt(value) || 'Value should be integer';
return validator.isNumeric(value) || 'Value should be a number'; return validator.isNumeric(value) || 'Value should be a number';
}, },
min: (value, min) => {
if (min >= 0)
if (Math.floor(value) < min) return t('inputMin', { value: min });
},
custom: (value) => validation.bindedFunction(value) || 'Invalid value', custom: (value) => validation.bindedFunction(value) || 'Invalid value',
}; };
}; };
return { return {
validate, validate,
validations,
models, models,
}; };
} }

View File

@ -103,10 +103,6 @@ select:-webkit-autofill {
border-radius: 8px; border-radius: 8px;
} }
.card-width {
width: 770px;
}
.vn-card-list { .vn-card-list {
width: 100%; width: 100%;
max-width: 60em; max-width: 60em;
@ -190,6 +186,10 @@ select:-webkit-autofill {
font-size: medium; font-size: medium;
} }
.q-toolbar {
background: var(--vn-section-color);
}
.q-card__actions { .q-card__actions {
justify-content: center; justify-content: center;
} }
@ -264,3 +264,10 @@ input::-webkit-inner-spin-button {
max-width: 400px; max-width: 400px;
} }
} }
.edit-photo-btn {
position: absolute;
right: 12px;
bottom: 12px;
z-index: 1;
cursor: pointer;
}

View File

@ -20,21 +20,21 @@ export function isValidDate(date) {
* Converts a given date to a specific format. * Converts a given date to a specific format.
* *
* @param {number|string|Date} date - The date to be formatted. * @param {number|string|Date} date - The date to be formatted.
* @param {Object} opts - Optional parameters to customize the output format.
* @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned. * @returns {string} The formatted date as a string in 'dd/mm/yyyy' format. If the provided date is not valid, an empty string is returned.
* *
* @example * @example
* // returns "02/12/2022" * // returns "02/12/2022"
* toDateFormat(new Date(2022, 11, 2)); * toDateFormat(new Date(2022, 11, 2));
*/ */
export function toDateFormat(date, locale = 'es-ES') { export function toDateFormat(date, locale = 'es-ES', opts = {}) {
if (!isValidDate(date)) { if (!isValidDate(date)) return '';
return '';
} const format = Object.assign(
return new Date(date).toLocaleDateString(locale, { { year: 'numeric', month: '2-digit', day: '2-digit' },
year: 'numeric', opts
month: '2-digit', );
day: '2-digit', return new Date(date).toLocaleDateString(locale, format);
});
} }
/** /**

View File

@ -1,7 +1,7 @@
export default function dateRange(value) { export default function dateRange(value) {
const minHour = new Date(value); const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0); minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(); const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59); maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour]; return [minHour, maxHour];

View File

@ -0,0 +1,21 @@
// parsing JSON safely
function parseJSON(str, fallback) {
try {
return JSON.parse(str ?? '{}');
} catch (e) {
console.error('Error parsing JSON:', e);
return fallback;
}
}
export default function (route, param) {
// catch route query params
const params = parseJSON(route?.query?.params, {});
// extract and parse filter from params
const { filter: filterStr = '{}' } = params;
const where = parseJSON(filterStr, {})?.where;
if (where && where[param] !== undefined) {
return where[param];
}
return null;
}

View File

@ -11,6 +11,7 @@ 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 getParamWhere from './getParamWhere';
export { export {
toLowerCase, toLowerCase,
@ -26,4 +27,5 @@ export {
toPercentage, toPercentage,
dashIfEmpty, dashIfEmpty,
dateRange, dateRange,
getParamWhere,
}; };

View File

@ -67,6 +67,7 @@ globals:
allRows: 'All { numberRows } row(s)' allRows: 'All { numberRows } row(s)'
markAll: Mark all markAll: Mark all
requiredField: Required field requiredField: Required field
valueCantBeEmpty: Value cannot be empty
class: clase class: clase
type: Type type: Type
reason: reason reason: reason
@ -90,6 +91,11 @@ globals:
salesPerson: SalesPerson salesPerson: SalesPerson
send: Send send: Send
code: Code code: Code
since: Since
from: From
to: To
notes: Notes
refresh: Refresh
pageTitles: pageTitles:
logIn: Login logIn: Login
summary: Summary summary: Summary
@ -248,6 +254,13 @@ globals:
privileges: Privileges privileges: Privileges
ldap: LDAP ldap: LDAP
samba: Samba samba: Samba
twoFactor: Two factor
recoverPassword: Recover password
resetPassword: Reset password
ticketsMonitor: Tickets monitor
clientsActionsMonitor: Clients and actions
serial: Serial
medical: Mutual
created: Created created: Created
worker: Worker worker: Worker
now: Now now: Now
@ -259,6 +272,9 @@ globals:
unsavedPopup: unsavedPopup:
title: Unsaved changes will be lost title: Unsaved changes will be lost
subtitle: Are you sure exit without saving? subtitle: Are you sure exit without saving?
createInvoiceIn: Create invoice in
myAccount: My account
noOne: No one
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -284,14 +300,17 @@ twoFactor:
explanation: >- explanation: >-
Please, enter the verification code that we have sent to your email in the Please, enter the verification code that we have sent to your email in the
next 5 minutes next 5 minutes
pageTitles:
twoFactor: Two-Factor
verifyEmail: verifyEmail:
pageTitles: pageTitles:
verifyEmail: Email verification verifyEmail: Email verification
dashboard: recoverPassword:
pageTitles: userOrEmail: User or recovery email
explanation: >-
We will sent you an email to recover your password
resetPassword:
repeatPassword: Repeat password
passwordNotMatch: Passwords don't match
passwordChanged: Password changed
customer: customer:
list: list:
phone: Phone phone: Phone
@ -442,6 +461,7 @@ entry:
travelFk: Travel travelFk: Travel
isExcludedFromAvailable: Inventory isExcludedFromAvailable: Inventory
isRaid: Raid isRaid: Raid
invoiceAmount: Import
summary: summary:
commission: Commission commission: Commission
currency: Currency currency: Currency
@ -678,6 +698,7 @@ invoiceOut:
chooseValidClient: Choose a valid client chooseValidClient: Choose a valid client
chooseValidCompany: Choose a valid company chooseValidCompany: Choose a valid company
chooseValidPrinter: Choose a valid printer chooseValidPrinter: Choose a valid printer
chooseValidSerialType: Choose a serial type
fillDates: Invoice date and the max date should be filled fillDates: Invoice date and the max date should be filled
invoiceDateLessThanMaxDate: Invoice date can not be less than max date invoiceDateLessThanMaxDate: Invoice date can not be less than max date
invoiceWithFutureDate: Exists an invoice with a future date invoiceWithFutureDate: Exists an invoice with a future date
@ -858,6 +879,7 @@ worker:
timeControl: Time control timeControl: Time control
locker: Locker locker: Locker
balance: Balance balance: Balance
medical: Medical
list: list:
name: Name name: Name
email: Email email: Email
@ -937,6 +959,15 @@ worker:
amount: Importe amount: Importe
remark: Bonficado remark: Bonficado
hasDiploma: Diploma hasDiploma: Diploma
medical:
tableVisibleColumns:
date: Date
time: Hour
center: Formation Center
invoice: Invoice
amount: Amount
isFit: Fit
remark: Observations
imageNotFound: Image not found imageNotFound: Image not found
balance: balance:
tableVisibleColumns: tableVisibleColumns:
@ -1109,9 +1140,12 @@ travel:
agency: Agency agency: Agency
shipped: Shipped shipped: Shipped
landed: Landed landed: Landed
shipHour: Shipment Hour
landHour: Landing Hour
warehouseIn: Warehouse in warehouseIn: Warehouse in
warehouseOut: Warehouse out warehouseOut: Warehouse out
totalEntries: Total entries totalEntries: Total entries
totalEntriesTooltip: Total entries
summary: summary:
confirmed: Confirmed confirmed: Confirmed
entryId: Entry Id entryId: Entry Id

View File

@ -76,6 +76,9 @@ globals:
warehouse: Almacén warehouse: Almacén
company: Empresa company: Empresa
fieldRequired: Campo requerido fieldRequired: Campo requerido
valueCantBeEmpty: El valor no puede estar vacío
Value can't be blank: El valor no puede estar en blanco
Value can't be null: El valor no puede ser nulo
allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }' allowedFilesText: 'Tipos de archivo permitidos: { allowedContentTypes }'
smsSent: SMS enviado smsSent: SMS enviado
confirmDeletion: Confirmar eliminación confirmDeletion: Confirmar eliminación
@ -90,6 +93,11 @@ globals:
salesPerson: Comercial salesPerson: Comercial
send: Enviar send: Enviar
code: Código code: Código
since: Desde
from: Desde
to: Hasta
notes: Notas
refresh: Actualizar
pageTitles: pageTitles:
logIn: Inicio de sesión logIn: Inicio de sesión
summary: Resumen summary: Resumen
@ -232,7 +240,7 @@ globals:
purchaseRequest: Petición de compra purchaseRequest: Petición de compra
weeklyTickets: Tickets programados weeklyTickets: Tickets programados
formation: Formación formation: Formación
locations: Ubicaciones locations: Localizaciones
warehouses: Almacenes warehouses: Almacenes
roles: Roles roles: Roles
connections: Conexiones connections: Conexiones
@ -250,6 +258,13 @@ globals:
packages: Bultos packages: Bultos
ldap: LDAP ldap: LDAP
samba: Samba samba: Samba
twoFactor: Doble factor
recoverPassword: Recuperar contraseña
resetPassword: Restablecer contraseña
ticketsMonitor: Monitor de tickets
clientsActionsMonitor: Clientes y acciones
serial: Facturas por serie
medical: Mutua
created: Fecha creación created: Fecha creación
worker: Trabajador worker: Trabajador
now: Ahora now: Ahora
@ -261,6 +276,9 @@ globals:
unsavedPopup: unsavedPopup:
title: Los cambios que no haya guardado se perderán title: Los cambios que no haya guardado se perderán
subtitle: ¿Seguro que quiere salir sin guardar? subtitle: ¿Seguro que quiere salir sin guardar?
createInvoiceIn: Crear factura recibida
myAccount: Mi cuenta
noOne: Nadie
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -284,14 +302,17 @@ twoFactor:
validate: Validar validate: Validar
insert: Introduce el código de verificación insert: Introduce el código de verificación
explanation: Por favor introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos explanation: Por favor introduce el código de verificación que te hemos enviado a tu email en los próximos 5 minutos
pageTitles:
twoFactor: Doble factor
verifyEmail: verifyEmail:
pageTitles: pageTitles:
verifyEmail: Verificación de correo verifyEmail: Verificación de correo
dashboard: recoverPassword:
pageTitles: userOrEmail: Usuario o correo de recuperación
explanation: >-
Te enviaremos un correo para restablecer tu contraseña
resetPassword:
repeatPassword: Repetir contraseña
passwordNotMatch: Las contraseñas no coinciden
passwordChanged: Contraseña cambiada
customer: customer:
list: list:
phone: Teléfono phone: Teléfono
@ -441,6 +462,7 @@ entry:
travelFk: Envio travelFk: Envio
isExcludedFromAvailable: Inventario isExcludedFromAvailable: Inventario
isRaid: Redada isRaid: Redada
invoiceAmount: Importe
summary: summary:
commission: Comisión commission: Comisión
currency: Moneda currency: Moneda
@ -684,6 +706,7 @@ invoiceOut:
chooseValidClient: Selecciona un cliente válido chooseValidClient: Selecciona un cliente válido
chooseValidCompany: Selecciona una empresa válida chooseValidCompany: Selecciona una empresa válida
chooseValidPrinter: Selecciona una impresora válida chooseValidPrinter: Selecciona una impresora válida
chooseValidSerialType: Selecciona una tipo de serie válida
fillDates: La fecha de la factura y la fecha máxima deben estar completas fillDates: La fecha de la factura y la fecha máxima deben estar completas
invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima invoiceDateLessThanMaxDate: La fecha de la factura no puede ser menor que la fecha máxima
invoiceWithFutureDate: Existe una factura con una fecha futura invoiceWithFutureDate: Existe una factura con una fecha futura
@ -697,8 +720,6 @@ invoiceOut:
percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}' percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}'
pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs' pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs'
negativeBases: negativeBases:
from: Desde
to: Hasta
company: Empresa company: Empresa
country: País country: País
clientId: Id cliente clientId: Id cliente
@ -861,6 +882,8 @@ worker:
timeControl: Control de horario timeControl: Control de horario
locker: Taquilla locker: Taquilla
balance: Balance balance: Balance
formation: Formación
medical: Mutua
list: list:
name: Nombre name: Nombre
email: Email email: Email
@ -873,7 +896,7 @@ worker:
card: card:
workerId: ID Trabajador workerId: ID Trabajador
name: Nombre name: Nombre
email: Email email: Correo personal
phone: Teléfono phone: Teléfono
mobile: Móvil mobile: Móvil
active: Activo active: Activo
@ -931,6 +954,15 @@ worker:
amount: Importe amount: Importe
remark: Bonficado remark: Bonficado
hasDiploma: Diploma hasDiploma: Diploma
medical:
tableVisibleColumns:
date: Fecha
time: Hora
center: Centro de Formación
invoice: Factura
amount: Importe
isFit: Apto
remark: Observaciones
imageNotFound: No se ha encontrado la imagen imageNotFound: No se ha encontrado la imagen
balance: balance:
tableVisibleColumns: tableVisibleColumns:
@ -1088,11 +1120,14 @@ travel:
id: Id id: Id
ref: Referencia ref: Referencia
agency: Agencia agency: Agencia
shipped: Enviado shipped: F.envío
landed: Llegada shipHour: Hora de envío
warehouseIn: Almacén de salida landHour: Hora de llegada
warehouseOut: Almacén de entrada landed: F.entrega
totalEntries: Total de entradas warehouseIn: Alm.salida
warehouseOut: Alm.entrada
totalEntries:
totalEntriesTooltip: Entradas totales
summary: summary:
confirmed: Confirmado confirmed: Confirmado
entryId: Id entrada entryId: Id entrada
@ -1240,8 +1275,6 @@ components:
# LatestBuysFilter # LatestBuysFilter
salesPersonFk: Comprador salesPersonFk: Comprador
supplierFk: Proveedor supplierFk: Proveedor
from: Desde
to: Hasta
active: Activo active: Activo
visible: Visible visible: Visible
floramondo: Floramondo floramondo: Floramondo
@ -1259,6 +1292,7 @@ components:
clone: Clonar clone: Clonar
openCard: Ficha openCard: Ficha
openSummary: Detalles openSummary: Detalles
viewSummary: Vista previa
cardDescriptor: cardDescriptor:
mainList: Listado principal mainList: Listado principal
summary: Resumen summary: Resumen

View File

@ -5,7 +5,7 @@ const quasar = useQuasar();
</script> </script>
<template> <template>
<QLayout view="hHh LpR fFf"> <QLayout view="hHh LpR fFf" v-shortcut>
<Navbar /> <Navbar />
<RouterView></RouterView> <RouterView></RouterView>
<QFooter v-if="quasar.platform.is.mobile"></QFooter> <QFooter v-if="quasar.platform.is.mobile"></QFooter>

View File

@ -6,7 +6,6 @@ import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import FetchData from 'components/FetchData.vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
@ -24,7 +23,6 @@ const stateStore = useStateStore();
const quasar = useQuasar(); const quasar = useQuasar();
const tableRef = ref(); const tableRef = ref();
const rolesOptions = ref([]);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
switch (param) { switch (param) {
@ -41,14 +39,12 @@ const columns = computed(() => [
name: 'id', name: 'id',
label: t('id'), label: t('id'),
isId: true, isId: true,
field: 'id',
cardVisible: true, cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
name: 'model', name: 'model',
label: t('model'), label: t('model'),
field: 'model',
cardVisible: true, cardVisible: true,
create: true, create: true,
}, },
@ -56,15 +52,19 @@ const columns = computed(() => [
align: 'left', align: 'left',
name: 'principalId', name: 'principalId',
label: t('principalId'), label: t('principalId'),
field: 'principalId',
cardVisible: true, cardVisible: true,
component: 'select',
attrs: {
url: 'VnRoles',
optionLabel: 'name',
optionValue: 'name',
},
create: true, create: true,
}, },
{ {
align: 'left', align: 'left',
name: 'property', name: 'property',
label: t('property'), label: t('property'),
field: 'property',
cardVisible: true, cardVisible: true,
create: true, create: true,
}, },
@ -72,7 +72,10 @@ const columns = computed(() => [
align: 'left', align: 'left',
name: 'accessType', name: 'accessType',
label: t('accessType'), label: t('accessType'),
field: 'accessType', component: 'select',
attrs: {
options: ['READ', 'WRITE', '*'],
},
cardVisible: true, cardVisible: true,
create: true, create: true,
}, },
@ -118,13 +121,6 @@ const deleteAcl = async ({ id }) => {
</script> </script>
<template> <template>
<FetchData
url="VnRoles"
:filter="{ fields: ['name'], order: 'name ASC' }"
@on-fetch="(data) => (rolesOptions = data)"
auto-load
/>
<VnSearchbar <VnSearchbar
data-key="AccountAcls" data-key="AccountAcls"
url="ACLs" url="ACLs"
@ -145,9 +141,9 @@ const deleteAcl = async ({ id }) => {
formInitialData: {}, formInitialData: {},
}" }"
order="id DESC" order="id DESC"
:disable-option="{ card: true }"
:columns="columns" :columns="columns"
default-mode="table" default-mode="table"
auto-load
:right-search="true" :right-search="true"
:is-editable="true" :is-editable="true"
:use-model="true" :use-model="true"
@ -162,4 +158,15 @@ es:
Are you sure you want to continue?: ¿Seguro que quieres continuar? Are you sure you want to continue?: ¿Seguro que quieres continuar?
Remove ACL: Eliminar Acl Remove ACL: Eliminar Acl
Do you want to remove this ACL?: ¿Quieres eliminar este ACL? Do you want to remove this ACL?: ¿Quieres eliminar este ACL?
principalId: Rol
model: Modelo
en:
New ACL: New ACL
ACL removed: ACL removed
ACL will be removed: ACL will be removed
Are you sure you want to continue?: Are you sure you want to continue?
Remove ACL: Remove ACL
Do you want to remove this ACL?: Do you want to remove this ACL?
principalId: Rol
model: Models
</i18n> </i18n>

View File

@ -21,24 +21,21 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
name: 'id', name: 'id',
label: t('id'), label: t('Id'),
isId: true, isId: true,
field: 'id',
cardVisible: true, cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
name: 'alias', name: 'alias',
label: t('alias'), label: t('Alias'),
field: 'alias',
cardVisible: true, cardVisible: true,
create: true, create: true,
}, },
{ {
align: 'left', align: 'left',
name: 'description', name: 'description',
label: t('description'), label: t('Description'),
field: 'description',
cardVisible: true, cardVisible: true,
create: true, create: true,
}, },
@ -60,7 +57,7 @@ const columns = computed(() => [
<VnTable <VnTable
ref="tableRef" ref="tableRef"
data-key="AccountAliasList" data-key="AccountAliasList"
:url="`MailAliases`" url="MailAliases"
:create="{ :create="{
urlCreate: 'MailAliases', urlCreate: 'MailAliases',
title: 'Create MailAlias', title: 'Create MailAlias',
@ -69,10 +66,17 @@ const columns = computed(() => [
}" }"
order="id DESC" order="id DESC"
:columns="columns" :columns="columns"
:disable-option="{ card: true }"
default-mode="table" default-mode="table"
auto-load
redirect="account/alias" redirect="account/alias"
:is-editable="true" :is-editable="true"
:use-model="true" :use-model="true"
/> />
</template> </template>
<i18n>
es:
Id: Id
Alias: Alias
Description: Descripción
</i18n>

View File

@ -14,15 +14,23 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
name: 'id', name: 'id',
label: t('id'), label: t('Id'),
isId: true, isId: true,
field: 'id', field: 'id',
cardVisible: true, cardVisible: true,
columnFilter: {
component: 'select',
name: 'search',
attrs: {
url: 'VnUsers/preview',
fields: ['id', 'name'],
},
},
}, },
{ {
align: 'left', align: 'left',
name: 'username', name: 'username',
label: t('nickname'), label: t('Nickname'),
isTitle: true, isTitle: true,
component: 'input', component: 'input',
columnField: { columnField: {
@ -37,7 +45,7 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
name: 'name', name: 'name',
label: t('name'), label: t('Name'),
component: 'input', component: 'input',
columnField: { columnField: {
component: null, component: null,
@ -48,6 +56,14 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
create: true, create: true,
}, },
{
align: 'left',
name: 'email',
label: t('email'),
component: 'input',
create: true,
visible: false,
},
{ {
align: 'right', align: 'right',
label: '', label: '',
@ -57,6 +73,7 @@ const columns = computed(() => [
title: t('View Summary'), title: t('View Summary'),
icon: 'preview', icon: 'preview',
action: (row) => viewSummary(row.id, AccountSummary), action: (row) => viewSummary(row.id, AccountSummary),
isPrimary: true,
}, },
], ],
}, },
@ -83,9 +100,9 @@ const exprBuilder = (param, value) => {
<template> <template>
<VnSearchbar <VnSearchbar
:label="t('account.search')"
data-key="AccountUsers" data-key="AccountUsers"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
:label="t('account.search')"
:info="t('account.searchInfo')" :info="t('account.searchInfo')"
/> />
@ -96,8 +113,14 @@ const exprBuilder = (param, value) => {
order="id DESC" order="id DESC"
:columns="columns" :columns="columns"
default-mode="table" default-mode="table"
auto-load
redirect="account" redirect="account"
:use-model="true" :use-model="true"
/> />
</template> </template>
<i18n>
es:
Id: Id
Nickname: Nickname
Name: Nombre
</i18n>

View File

@ -1,22 +1,8 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import AliasDescriptor from './AliasDescriptor.vue'; import AliasDescriptor from './AliasDescriptor.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const customRouteRedirectName = computed(() => {
return routeName.value;
});
const searchBarDataKeys = {
AliasBasicData: 'AliasBasicData',
AliasUsers: 'AliasUsers',
};
</script> </script>
<template> <template>
@ -24,10 +10,12 @@ const searchBarDataKeys = {
data-key="Alias" data-key="Alias"
base-url="MailAliases" base-url="MailAliases"
:descriptor="AliasDescriptor" :descriptor="AliasDescriptor"
:search-data-key="searchBarDataKeys[routeName]" search-data-key="AccountAliasList"
:search-custom-route-redirect="customRouteRedirectName" :searchbar-props="{
:search-redirect="!!customRouteRedirectName" url: 'MailAliases',
:searchbar-label="t('mailAlias.search')" info: t('mailAlias.searchInfo'),
:searchbar-info="t('mailAlias.searchInfo')" label: t('mailAlias.search'),
searchUrl: 'table',
}"
/> />
</template> </template>

View File

@ -37,9 +37,11 @@ watch(
<VnInput v-model="data.nickname" :label="t('account.card.alias')" /> <VnInput v-model="data.nickname" :label="t('account.card.alias')" />
<VnInput v-model="data.email" :label="t('account.card.email')" /> <VnInput v-model="data.email" :label="t('account.card.email')" />
<VnSelect <VnSelect
url="Languages"
v-model="data.lang" v-model="data.lang"
:options="['es', 'en']"
:label="t('account.card.lang')" :label="t('account.card.lang')"
option-value="code"
option-label="code"
/> />
</div> </div>
</template> </template>

View File

@ -1,34 +1,20 @@
<script setup> <script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import AccountDescriptor from './AccountDescriptor.vue'; import AccountDescriptor from './AccountDescriptor.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const customRouteRedirectName = computed(() => routeName.value);
const searchBarDataKeys = {
AccountSummary: 'AccountSummary',
AccountInheritedRoles: 'AccountInheritedRoles',
AccountMailForwarding: 'AccountMailForwarding',
AccountMailAlias: 'AccountMailAlias',
AccountPrivileges: 'AccountPrivileges',
AccountLog: 'AccountLog',
};
</script> </script>
<template> <template>
<VnCard <VnCard
data-key="Account" data-key="Account"
:descriptor="AccountDescriptor" :descriptor="AccountDescriptor"
:search-data-key="searchBarDataKeys[routeName]" search-data-key="AccountUsers"
:search-custom-route-redirect="customRouteRedirectName" :searchbar-props="{
:search-redirect="!!customRouteRedirectName" url: 'VnUsers/preview',
:searchbar-label="t('account.search')" label: t('account.search'),
:searchbar-info="t('account.searchInfo')" info: t('account.searchInfo'),
}"
/> />
</template> </template>

View File

@ -54,7 +54,7 @@ const hasAccount = ref(false);
</template> </template>
<template #before> <template #before>
<!-- falla id :id="entityId.value" collection="user" size="160x160" --> <!-- falla id :id="entityId.value" collection="user" size="160x160" -->
<VnImg :id="entityId" collection="user" size="160x160" class="photo"> <VnImg :id="entityId" collection="user" resolution="160x160" class="photo">
<template #error> <template #error>
<div <div
class="absolute-full picture text-center q-pa-md flex flex-center" class="absolute-full picture text-center q-pa-md flex flex-center"
@ -72,7 +72,7 @@ const hasAccount = ref(false);
</VnImg> </VnImg>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('account.card.nickname')" :value="entity.nickname" /> <VnLv :label="t('account.card.nickname')" :value="entity.name" />
<VnLv :label="t('account.card.role')" :value="entity.role.name" /> <VnLv :label="t('account.card.role')" :value="entity.role.name" />
</template> </template>
<template #actions="{ entity }"> <template #actions="{ entity }">

View File

@ -1,15 +1,12 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { computed, ref, toRefs } from 'vue'; import { computed, ref, toRefs } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
const quasar = useQuasar();
const $props = defineProps({ const $props = defineProps({
hasAccount: { hasAccount: {
type: Boolean, type: Boolean,
@ -35,7 +32,7 @@ async function updateStatusAccount(active) {
account.value.hasAccount = active; account.value.hasAccount = active;
const status = active ? 'enable' : 'disable'; const status = active ? 'enable' : 'disable';
quasar.notify({ notify({
message: t(`account.card.${status}Account.success`), message: t(`account.card.${status}Account.success`),
type: 'positive', type: 'positive',
}); });
@ -44,19 +41,11 @@ async function updateStatusUser(active) {
await axios.patch(`VnUsers/${entityId.value}`, { active }); await axios.patch(`VnUsers/${entityId.value}`, { active });
account.value.active = active; account.value.active = active;
const status = active ? 'activate' : 'deactivate'; const status = active ? 'activate' : 'deactivate';
quasar.notify({ notify({
message: t(`account.card.actions.${status}User.success`), message: t(`account.card.actions.${status}User.success`),
type: 'positive', type: 'positive',
}); });
} }
function setPassword() {
quasar.dialog({
component: CustomerChangePassword,
componentProps: {
id: entityId.value,
},
});
}
const showSyncDialog = ref(false); const showSyncDialog = ref(false);
const syncPassword = ref(null); const syncPassword = ref(null);
const shouldSyncPassword = ref(false); const shouldSyncPassword = ref(false);
@ -66,22 +55,43 @@ async function sync() {
await axios.patch(`Accounts/${account.value.name}/sync`, { await axios.patch(`Accounts/${account.value.name}/sync`, {
params, params,
}); });
quasar.notify({ notify({
message: t('account.card.actions.sync.success'), message: t('account.card.actions.sync.success'),
type: 'positive', type: 'positive',
}); });
} }
const removeAccount = async () => {
try {
await axios.delete(`VnUsers/${account.value.id}`);
notify(t('Account removed'), 'positive');
} catch (error) {
console.error('Error deleting user', error);
}
};
</script> </script>
<template> <template>
<VnConfirm
v-model="showSyncDialog"
:message="t('account.card.actions.sync.message')"
:title="t('account.card.actions.sync.title')"
:promise="sync"
>
<template #customHTML>
{{ shouldSyncPassword }}
<QCheckbox
:label="t('account.card.actions.sync.checkbox')"
v-model="shouldSyncPassword"
class="full-width"
clearable
clear-icon="close"
>
<QIcon style="padding-left: 10px" color="primary" name="info" size="sm">
<QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip>
</QIcon></QCheckbox
>
<QInput
v-if="shouldSyncPassword"
:label="t('login.password')"
v-model="syncPassword"
class="full-width"
clearable
clear-icon="close"
type="password"
/>
</template>
</VnConfirm>
<QItem <QItem
v-if="account.hasAccount" v-if="account.hasAccount"
v-ripple v-ripple
@ -130,10 +140,4 @@ const removeAccount = async () => {
</QItem> </QItem>
<QSeparator /> <QSeparator />
<!-- <QItem @click="removeAccount(id)" v-ripple clickable>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('account.card.actions.delete.name') }}</QItemSection>
</QItem> -->
</template> </template>

View File

@ -85,7 +85,7 @@ const fetchMailAliases = async () => {
paginateRef.value.fetch(); paginateRef.value.fetch();
}; };
const getAccountData = async () => { const getAccountData = async (reload = true) => {
loading.value = true; loading.value = true;
hasAccount.value = await fetchAccountExistence(); hasAccount.value = await fetchAccountExistence();
if (!hasAccount.value) { if (!hasAccount.value) {
@ -93,7 +93,7 @@ const getAccountData = async () => {
store.data = []; store.data = [];
return; return;
} }
await fetchMailAliases(); reload && (await fetchMailAliases());
loading.value = false; loading.value = false;
}; };
@ -102,13 +102,11 @@ const openCreateMailAliasForm = () => createMailAliasDialogRef.value.show();
watch( watch(
() => route.params.id, () => route.params.id,
() => { () => {
store.url = urlPath;
store.filter = filter.value;
getAccountData(); getAccountData();
} }
); );
onMounted(async () => await getAccountData()); onMounted(async () => await getAccountData(false));
</script> </script>
<template> <template>
@ -171,7 +169,13 @@ onMounted(async () => await getAccountData());
<AccountMailAliasCreateForm @on-submit-create-alias="createMailAlias" /> <AccountMailAliasCreateForm @on-submit-create-alias="createMailAlias" />
</QDialog> </QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]"> <QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="openCreateMailAliasForm()"> <QBtn
fab
icon="add"
color="primary"
@click="openCreateMailAliasForm()"
shortcut="+"
>
<QTooltip>{{ t('warehouses.add') }}</QTooltip> <QTooltip>{{ t('warehouses.add') }}</QTooltip>
</QBtn> </QBtn>
</QPageSticky> </QPageSticky>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@ -12,22 +12,14 @@ const route = useRoute();
const rolesOptions = ref([]); const rolesOptions = ref([]);
const formModelRef = ref(); const formModelRef = ref();
watch(
() => route.params.id,
() => formModelRef.value.reset()
);
</script> </script>
<template> <template>
<FetchData <FetchData url="VnRoles" auto-load @on-fetch="(data) => (rolesOptions = data)" />
url="VnRoles" <FormModel ref="formModelRef" model="AccountPrivileges" url="VnUsers" auto-load>
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (rolesOptions = data)"
/>
<FormModel
ref="formModelRef"
model="AccountPrivileges"
:url="`VnUsers/${route.params.id}`"
:url-create="`VnUsers/${route.params.id}/privileges`"
auto-load
@on-data-saved="formModelRef.fetch()"
>
<template #form="{ data }"> <template #form="{ data }">
<div class="q-gutter-y-sm"> <div class="q-gutter-y-sm">
<QCheckbox <QCheckbox

View File

@ -48,7 +48,7 @@ const filter = {
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</router-link> </router-link>
</QCardSection> </QCardSection>
<VnLv :label="t('account.card.nickname')" :value="account.nickname" /> <VnLv :label="t('account.card.nickname')" :value="account.name" />
<VnLv :label="t('account.card.role')" :value="account.role.name" /> <VnLv :label="t('account.card.role')" :value="account.role.name" />
</QCard> </QCard>
</template> </template>

View File

@ -4,9 +4,9 @@ import { computed, ref } from 'vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RoleSummary from './Card/RoleSummary.vue';
const route = useRoute(); const route = useRoute();
const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -16,32 +16,51 @@ const $props = defineProps({
}); });
const tableRef = ref(); const tableRef = ref();
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const { viewSummary } = useSummaryDialog();
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
name: 'id', name: 'id',
label: t('id'), label: t('Id'),
isId: true, isId: true,
columnFilter: { columnFilter: {
inWhere: true, inWhere: true,
component: 'select',
name: 'search',
attrs: {
url: 'VnRoles',
fields: ['id', 'name'],
},
}, },
cardVisible: true, cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
name: 'name', name: 'name',
label: t('name'), label: t('Name'),
cardVisible: true, cardVisible: true,
create: true, create: true,
}, },
{ {
align: 'left', align: 'left',
name: 'description', name: 'description',
label: t('description'), label: t('Description'),
cardVisible: true, cardVisible: true,
create: true, create: true,
}, },
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('View Summary'),
icon: 'preview',
action: (row) => viewSummary(row.id, RoleSummary),
isPrimary: true,
},
],
},
]); ]);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
switch (param) { switch (param) {
@ -62,16 +81,12 @@ const exprBuilder = (param, value) => {
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <VnSearchbar
<Teleport to="#searchbar"> data-key="Roles"
<VnSearchbar :expr-builder="exprBuilder"
data-key="Roles" :label="t('role.searchRoles')"
:expr-builder="exprBuilder" :info="t('role.searchInfo')"
:label="t('role.searchRoles')" />
:info="t('role.searchInfo')"
/>
</Teleport>
</template>
<VnTable <VnTable
ref="tableRef" ref="tableRef"
data-key="Roles" data-key="Roles"
@ -85,10 +100,16 @@ const exprBuilder = (param, value) => {
}, },
}" }"
order="id ASC" order="id ASC"
:disable-option="{ card: true }"
:columns="columns" :columns="columns"
default-mode="table" default-mode="table"
auto-load
redirect="account/role" redirect="account/role"
:is-editable="true"
/> />
</template> </template>
<i18n>
es:
Id: Id
Description: Descripción
Name: Nombre
</i18n>

View File

@ -23,11 +23,6 @@ const { t } = useI18n();
/> />
</div> </div>
</VnRow> </VnRow>
<VnRow>
<div class="col">
<QCheckbox :label="t('mailAlias.isPublic')" v-model="data.isPublic" />
</div>
</VnRow>
</template> </template>
</FormModel> </FormModel>
</template> </template>

View File

@ -1,31 +1,20 @@
<script setup> <script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import RoleDescriptor from './RoleDescriptor.vue'; import RoleDescriptor from './RoleDescriptor.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const customRouteRedirectName = computed(() => routeName.value);
const searchBarDataKeys = {
RoleSummary: 'RoleSummary',
RoleBasicData: 'RoleBasicData',
SubRoles: 'SubRoles',
InheritedRoles: 'InheritedRoles',
RoleLog: 'RoleLog',
};
</script> </script>
<template> <template>
<VnCard <VnCard
data-key="Role" data-key="Role"
:descriptor="RoleDescriptor" :descriptor="RoleDescriptor"
:search-data-key="searchBarDataKeys[routeName]" search-data-key="AccountRoles"
:search-custom-route-redirect="customRouteRedirectName" :searchbar-props="{
:search-redirect="!!customRouteRedirectName" url: 'VnRoles',
:searchbar-label="t('role.searchRoles')" label: t('role.searchRoles'),
:searchbar-info="t('role.searchInfo')" info: t('role.searchInfo'),
searchUrl: 'table',
}"
/> />
</template> </template>

View File

@ -10,13 +10,10 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import axios from 'axios'; import axios from 'axios';
// import { useSession } from 'src/composables/useSession'; import VnAvatar from 'src/components/ui/VnAvatar.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
// const { getTokenMultimedia } = useSession();
// const token = getTokenMultimedia();
const claimStates = ref([]); const claimStates = ref([]);
const claimStatesCopy = ref([]); const claimStatesCopy = ref([]);
@ -94,15 +91,14 @@ const statesFilter = {
:rules="validate('claim.claimStateFk')" :rules="validate('claim.claimStateFk')"
> >
<template #before> <template #before>
<QAvatar color="orange"> <VnAvatar
<VnImg :worker-id="data.workerFk"
v-if="data.workerFk" size="md"
:size="'160x160'" :title="
:id="data.workerFk" workersOptions.find(({ id }) => id == data.workerFk)?.name
collection="user" "
spinner-color="white" color="primary"
/> />
</QAvatar>
</template> </template>
</VnSelect> </VnSelect>
<QSelect <QSelect

View File

@ -11,9 +11,11 @@ import filter from './ClaimFilter.js';
:descriptor="ClaimDescriptor" :descriptor="ClaimDescriptor"
:filter-panel="ClaimFilter" :filter-panel="ClaimFilter"
search-data-key="ClaimList" search-data-key="ClaimList"
search-url="Claims/filter"
searchbar-label="Search claim"
searchbar-info="You can search by claim id or customer name"
:filter="filter" :filter="filter"
:searchbar-props="{
url: 'Claims/filter',
label: 'Search claim',
info: 'You can search by claim id or customer name',
}"
/> />
</template> </template>

View File

@ -20,20 +20,22 @@ const workers = ref([]);
const selected = ref([]); const selected = ref([]);
const saveButtonRef = ref(); const saveButtonRef = ref();
const developmentsFilter = { const developmentsFilter = computed(() => {
fields: [ return {
'id', fields: [
'claimFk', 'id',
'claimReasonFk', 'claimFk',
'claimResultFk', 'claimReasonFk',
'claimResponsibleFk', 'claimResultFk',
'workerFk', 'claimResponsibleFk',
'claimRedeliveryFk', 'workerFk',
], 'claimRedeliveryFk',
where: { ],
claimFk: route.params.id, where: {
}, claimFk: route.params.id,
}; },
};
});
const columns = computed(() => [ const columns = computed(() => [
{ {
@ -142,9 +144,9 @@ const columns = computed(() => [
ref="claimDevelopmentForm" ref="claimDevelopmentForm"
:data-required="{ claimFk: route.params.id }" :data-required="{ claimFk: route.params.id }"
v-model:selected="selected" v-model:selected="selected"
auto-load
@save-changes="$router.push(`/claim/${route.params.id}/action`)" @save-changes="$router.push(`/claim/${route.params.id}/action`)"
:default-save="false" :default-save="false"
auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable

View File

@ -1,11 +1,10 @@
<script setup> <script setup>
import axios from 'axios'; import axios from 'axios';
import { onMounted, ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import dashIfEmpty from 'src/filters/dashIfEmpty'; import dashIfEmpty from 'src/filters/dashIfEmpty';
import { getUrl } from 'src/composables/getUrl';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
@ -36,8 +35,6 @@ const $props = defineProps({
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const ClaimStates = ref([]); const ClaimStates = ref([]);
const claimUrl = ref();
const salixUrl = ref();
const claimDmsRef = ref(); const claimDmsRef = ref();
const claimDms = ref([]); const claimDms = ref([]);
const multimediaDialog = ref(); const multimediaDialog = ref();
@ -152,11 +149,6 @@ const developmentColumns = ref([
}, },
]); ]);
onMounted(async () => {
salixUrl.value = await getUrl('');
claimUrl.value = salixUrl.value + `claim/${entityId.value}/`;
});
async function getClaimDms() { async function getClaimDms() {
claimDmsFilter.value.where = { claimFk: entityId.value }; claimDmsFilter.value.where = { claimFk: entityId.value };
await claimDmsRef.value.fetch(); await claimDmsRef.value.fetch();
@ -177,10 +169,15 @@ function openDialog(dmsId) {
multimediaSlide.value = dmsId; multimediaSlide.value = dmsId;
multimediaDialog.value = true; multimediaDialog.value = true;
} }
async function changeState(value) { async function changeState(value) {
await axios.patch(`Claims/updateClaim/${entityId.value}`, { claimStateFk: value }); await axios.patch(`Claims/updateClaim/${entityId.value}`, { claimStateFk: value });
router.go(route.fullPath); router.go(route.fullPath);
} }
function claimUrl(section) {
return '#/claim/' + entityId.value + '/' + section;
}
</script> </script>
<template> <template>
@ -234,7 +231,7 @@ async function changeState(value) {
<template #body="{ entity: { claim, salesClaimed, developments } }"> <template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one" v-if="$route.name != 'ClaimSummary'"> <QCard class="vn-one" v-if="$route.name != 'ClaimSummary'">
<VnTitle <VnTitle
:url="`#/claim/${entityId}/basic-data`" :url="claimUrl('basic-data')"
:text="t('globals.pageTitles.basicData')" :text="t('globals.pageTitles.basicData')"
/> />
<VnLv :label="t('claim.created')" :value="toDate(claim.created)" /> <VnLv :label="t('claim.created')" :value="toDate(claim.created)" />
@ -275,7 +272,7 @@ async function changeState(value) {
/> />
</QCard> </QCard>
<QCard class="vn-two"> <QCard class="vn-two">
<VnTitle :url="`#/claim/${entityId}/notes`" :text="t('claim.notes')" /> <VnTitle :url="claimUrl('notes')" :text="t('claim.notes')" />
<ClaimNotes <ClaimNotes
:id="entityId" :id="entityId"
:add-note="false" :add-note="false"
@ -284,7 +281,7 @@ async function changeState(value) {
/> />
</QCard> </QCard>
<QCard class="vn-two" v-if="claimDms?.length"> <QCard class="vn-two" v-if="claimDms?.length">
<VnTitle :url="`#/claim/${entityId}/photos`" :text="t('claim.photos')" /> <VnTitle :url="claimUrl('photos')" :text="t('claim.photos')" />
<div class="container max-container-height" style="overflow: auto"> <div class="container max-container-height" style="overflow: auto">
<div <div
class="multimedia-container" class="multimedia-container"
@ -326,7 +323,7 @@ async function changeState(value) {
</div> </div>
</QCard> </QCard>
<QCard class="vn-max" v-if="salesClaimed.length > 0"> <QCard class="vn-max" v-if="salesClaimed.length > 0">
<VnTitle :url="`#/claim/${entityId}/lines`" :text="t('claim.details')" /> <VnTitle :url="claimUrl('lines')" :text="t('claim.details')" />
<QTable <QTable
:columns="detailsColumns" :columns="detailsColumns"
:rows="salesClaimed" :rows="salesClaimed"
@ -365,7 +362,7 @@ async function changeState(value) {
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-max" v-if="developments.length > 0"> <QCard class="vn-max" v-if="developments.length > 0">
<VnTitle :url="claimUrl + 'development'" :text="t('claim.development')" /> <VnTitle :url="claimUrl('development')" :text="t('claim.development')" />
<QTable <QTable
:columns="developmentColumns" :columns="developmentColumns"
:rows="developments" :rows="developments"
@ -390,7 +387,7 @@ async function changeState(value) {
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-max"> <QCard class="vn-max">
<VnTitle :url="claimUrl + 'action'" :text="t('claim.actions')" /> <VnTitle :url="claimUrl('action')" :text="t('claim.actions')" />
<div id="slider-container" class="q-px-xl q-py-md"> <div id="slider-container" class="q-px-xl q-py-md">
<QSlider <QSlider
v-model="claim.responsibility" v-model="claim.responsibility"

View File

@ -50,7 +50,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
label: t('claim.attendedBy'), label: t('claim.attendedBy'),
name: 'attendedBy', name: 'attendedBy',
cardVisible: true, orderBy: 'workerFk',
columnFilter: { columnFilter: {
component: 'select', component: 'select',
attrs: { attrs: {
@ -63,6 +63,7 @@ const columns = computed(() => [
optionFilter: 'firstName', optionFilter: 'firstName',
}, },
}, },
cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
@ -77,6 +78,9 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
label: t('claim.state'), label: t('claim.state'),
format: ({ stateCode }) =>
claimFilterRef.value?.states.find(({ code }) => code === stateCode)
?.description,
name: 'stateCode', name: 'stateCode',
chip: { chip: {
condition: () => true, condition: () => true,
@ -96,7 +100,7 @@ const columns = computed(() => [
name: 'tableActions', name: 'tableActions',
actions: [ actions: [
{ {
title: t('Client ticket list'), title: t('components.smartCard.viewSummary'),
icon: 'preview', icon: 'preview',
action: (row) => viewSummary(row.id, ClaimSummary), action: (row) => viewSummary(row.id, ClaimSummary),
}, },

View File

@ -42,7 +42,7 @@ claim:
pickup: Recoger pickup: Recoger
null: No null: No
agency: Agencia agency: Agencia
delivery: Entrega delivery: Reparto
fileDescription: 'ID de reclamación {claimId} del cliente {clientName} con ID {clientId}' fileDescription: 'ID de reclamación {claimId} del cliente {clientName} con ID {clientId}'
noData: 'No hay imágenes/videos, haz clic aquí o arrastra y suelta el archivo' noData: 'No hay imágenes/videos, haz clic aquí o arrastra y suelta el archivo'
dragDrop: Arrastra y suelta aquí dragDrop: Arrastra y suelta aquí

View File

@ -218,7 +218,13 @@ const toCustomerAddressEdit = (addressId) => {
</div> </div>
<QPageSticky :offset="[18, 18]"> <QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerAddressCreate()" color="primary" fab icon="add" /> <QBtn
@click.stop="toCustomerAddressCreate()"
color="primary"
fab
icon="add"
shortcut="+"
/>
<QTooltip> <QTooltip>
{{ t('New consignee') }} {{ t('New consignee') }}
</QTooltip> </QTooltip>

View File

@ -17,7 +17,6 @@ import VnTable from 'components/VnTable/VnTable.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import VnSubToolbar from 'components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'components/ui/VnSubToolbar.vue';
import VnFilter from 'components/VnTable/VnFilter.vue'; import VnFilter from 'components/VnTable/VnFilter.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue'; import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue';
import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
@ -73,17 +72,6 @@ const companyFilterColumn = {
}, },
}, },
visible: false, visible: false,
create: true,
};
const referenceColumn = {
align: 'left',
name: 'description',
label: t('Reference'),
};
const onlyCreate = {
visible: false,
columnFilter: false,
create: true,
}; };
const columns = computed(() => [ const columns = computed(() => [
@ -93,10 +81,6 @@ const columns = computed(() => [
label: t('Date'), label: t('Date'),
format: ({ payed }) => toDate(payed), format: ({ payed }) => toDate(payed),
cardVisible: true, cardVisible: true,
create: true,
columnCreate: {
component: 'date',
},
}, },
{ {
align: 'left', align: 'left',
@ -120,21 +104,18 @@ const columns = computed(() => [
}, },
cardVisible: true, cardVisible: true,
}, },
{ ...referenceColumn, isTitle: true, class: 'extend' }, {
companyFilterColumn, align: 'left',
name: 'description',
label: t('Reference'),
isTitle: true,
class: 'extend',
},
{ {
align: 'right', align: 'right',
name: 'bankFk', name: 'bankFk',
label: t('Bank'), label: t('Bank'),
cardVisible: true, cardVisible: true,
create: true,
},
{
align: 'right',
name: 'amountPaid',
label: t('Amount'),
component: 'number',
...onlyCreate,
}, },
{ {
align: 'right', align: 'right',
@ -163,7 +144,6 @@ const columns = computed(() => [
label: t('Conciliated'), label: t('Conciliated'),
cardVisible: true, cardVisible: true,
}, },
{ ...referenceColumn, ...onlyCreate },
{ {
align: 'left', align: 'left',
name: 'tableActions', name: 'tableActions',
@ -194,18 +174,16 @@ onBeforeMount(() => {
companyId.value = user.value.companyFk; companyId.value = user.value.companyFk;
}); });
async function getClientRisk(reload = false) { async function getClientRisk() {
if (reload || !clientRisk.value?.length) { const { data } = await axios.get(`clientRisks`, {
const { data } = await axios.get(`clientRisks`, { params: {
params: { filter: JSON.stringify({
filter: JSON.stringify({ include: { relation: 'company', scope: { fields: ['code'] } },
include: { relation: 'company', scope: { fields: ['code'] } }, where: { clientFk: route.params.id, companyFk: user.value.companyFk },
where: { clientFk: route.params.id, companyFk: user.value.companyFk }, }),
}), },
}, });
}); clientRisk.value = data;
clientRisk.value = data;
}
return clientRisk.value; return clientRisk.value;
} }
@ -230,14 +208,13 @@ async function onFetch(data) {
balances.value = data; balances.value = data;
} }
// BORRAR COMPONENTE Y HACER CON VNTABLE
const showNewPaymentDialog = () => { const showNewPaymentDialog = () => {
quasar.dialog({ quasar.dialog({
component: CustomerNewPayment, component: CustomerNewPayment,
componentProps: { componentProps: {
companyId: companyId.value, companyId: companyId.value,
totalCredit: clientRisk.value[0]?.amount, totalCredit: clientRisk.value[0]?.amount,
promise: getClientRisk(true), promise: () => tableRef.value.reload(),
}, },
}); });
}; };
@ -282,17 +259,6 @@ const showBalancePdf = ({ id }) => {
:is-editable="false" :is-editable="false"
:column-search="false" :column-search="false"
@on-fetch="onFetch" @on-fetch="onFetch"
:create="{
urlCreate: `Clients/${route.params.id}/createReceipt`,
mapper: (data) => {
data.companyFk = data.companyId;
delete data.companyId;
return data;
},
title: t('New payment'),
onDataSaved: () => tableRef.reload(),
formInitialData: { companyId },
}"
auto-load auto-load
> >
<template #column-balance="{ rowIndex }"> <template #column-balance="{ rowIndex }">
@ -314,6 +280,7 @@ const showBalancePdf = ({ id }) => {
value != row.description && value != row.description &&
axios.patch(`Receipts/${row.id}`, { description: value }) axios.patch(`Receipts/${row.id}`, { description: value })
" "
auto-save
> >
<VnInput <VnInput
v-model="scope.value" v-model="scope.value"
@ -323,25 +290,19 @@ const showBalancePdf = ({ id }) => {
/> />
</QPopupEdit> </QPopupEdit>
</template> </template>
<template #column-create-bankFk="{ data, columnName, label }">
<VnSelect
url="Accountings"
:label="label"
:limit="0"
option-label="bank"
sort-by="id"
v-model="data[columnName]"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemLabel>
#{{ scope.opt?.id }}: {{ scope.opt?.bank }}
</QItemLabel>
</QItem>
</template>
</VnSelect>
</template>
</VnTable> </VnTable>
<QPageSticky :offset="[18, 18]" style="z-index: 2">
<QBtn
@click.stop="showNewPaymentDialog()"
color="primary"
fab
icon="add"
shortcut="+"
/>
<QTooltip>
{{ t('New payment') }}
</QTooltip>
</QPageSticky>
</template> </template>
<i18n> <i18n>

View File

@ -7,14 +7,15 @@ import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; 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 VnImg from 'src/components/ui/VnImg.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';
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();
</script> </script>
<template> <template>
<FetchData <FetchData
@ -95,16 +96,20 @@ const contactChannels = ref([]);
:label="t('customer.basicData.salesPerson')" :label="t('customer.basicData.salesPerson')"
:rules="validate('client.salesPersonFk')" :rules="validate('client.salesPersonFk')"
:use-like="false" :use-like="false"
:emit-value="false"
@update:model-value="
(val) => {
title = val?.nickname;
data.salesPersonFk = val?.id;
}
"
> >
<template #prepend> <template #prepend>
<QAvatar color="orange"> <VnAvatar
<VnImg :worker-id="data.salesPersonFk"
v-if="data.salesPersonFk" color="primary"
:id="data.salesPersonFk" :title="title"
collection="user" />
spinner-color="white"
/>
</QAvatar>
</template> </template>
</VnSelect> </VnSelect>
<QSelect <QSelect

View File

@ -10,8 +10,10 @@ import CustomerFilter from '../CustomerFilter.vue';
:descriptor="CustomerDescriptor" :descriptor="CustomerDescriptor"
:filter-panel="CustomerFilter" :filter-panel="CustomerFilter"
search-data-key="CustomerList" search-data-key="CustomerList"
search-url="Clients/extendedListFilter" :searchbar-props="{
searchbar-label="Search customer" url: 'Clients/extendedListFilter',
searchbar-info="You can search by customer id or name" label: 'Search customer',
info: 'You can search by customer id or name',
}"
/> />
</template> </template>

View File

@ -1,8 +1,6 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n';
import CustomerConsumptionFilter from './CustomerConsumptionFilter.vue'; import CustomerConsumptionFilter from './CustomerConsumptionFilter.vue';
import { useStateStore } from 'src/stores/useStateStore'; import { useStateStore } from 'src/stores/useStateStore';
const { t } = useI18n();
</script> </script>
<template> <template>

View File

@ -193,6 +193,7 @@ const updateData = () => {
color="primary" color="primary"
fab fab
icon="add" icon="add"
shortcut="+"
/> />
<QTooltip> <QTooltip>
{{ t('New contract') }} {{ t('New contract') }}

View File

@ -139,7 +139,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
<QBtn <QBtn
:to="{ :to="{
name: 'TicketList', name: 'TicketList',
query: { params: JSON.stringify({ clientFk: entity.id }) }, query: { table: JSON.stringify({ clientFk: entity.id }) },
}" }"
size="md" size="md"
icon="vn:ticket" icon="vn:ticket"
@ -150,7 +150,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
<QBtn <QBtn
:to="{ :to="{
name: 'InvoiceOutList', name: 'InvoiceOutList',
query: { params: JSON.stringify({ clientFk: entity.id }) }, query: { table: JSON.stringify({ clientFk: entity.id }) },
}" }"
size="md" size="md"
icon="vn:invoice-out" icon="vn:invoice-out"
@ -161,7 +161,7 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
<QBtn <QBtn
:to="{ :to="{
name: 'OrderCreate', name: 'OrderCreate',
query: { clientFk: entity.id }, query: { clientId: entity.id },
}" }"
size="md" size="md"
icon="vn:basketadd" icon="vn:basketadd"
@ -169,8 +169,19 @@ const setData = (entity) => (data.value = useCardDescription(entity?.name, entit
> >
<QTooltip>{{ t('New order') }}</QTooltip> <QTooltip>{{ t('New order') }}</QTooltip>
</QBtn> </QBtn>
<QBtn size="md" icon="face" color="primary"> <QBtn
<!-- TODO:: Redirigir a la vista de usuario cuando exista --> :to="{
name: 'AccountList',
query: {
table: JSON.stringify({
filter: { where: { id: entity.id } },
}),
},
}"
size="md"
icon="face"
color="primary"
>
<QTooltip>{{ t('Go to user') }}</QTooltip> <QTooltip>{{ t('Go to user') }}</QTooltip>
</QBtn> </QBtn>
</QCardActions> </QCardActions>

View File

@ -1,262 +1,6 @@
<script setup> <script setup>
import { onBeforeMount, ref } from 'vue'; import VnLog from 'src/components/common/VnLog.vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const route = useRoute();
const stateStore = useStateStore();
const clientLogs = ref(null);
const urlClientLogsEditors = ref(null);
const urlClientLogsModels = ref(null);
const clientLogsModelsOptions = ref([]);
const clientLogsOptions = ref([]);
const clientLogsEditorsOptions = ref([]);
const radioButtonValue = ref('all');
const insert = ref(false);
const update = ref(false);
const deletes = ref(false);
const select = ref(false);
const neq = ref(null);
const inq = ref([]);
const filterClientLogs = {
fields: [
'id',
'originFk',
'userFk',
'action',
'changedModel',
'oldInstance',
'newInstance',
'creationDate',
'changedModel',
'changedModelId',
'changedModelValue',
'description',
],
include: [
{
relation: 'user',
scope: {
fields: ['nickname', 'name', 'image'],
include: { relation: 'worker', scope: { fields: ['id'] } },
},
},
],
order: ['creationDate DESC', 'id DESC'],
limit: 20,
};
const filterClientLogsEditors = {
fields: ['id', 'nickname', 'name', 'image'],
order: 'nickname',
limit: 30,
};
const filterClientLogsModels = { order: ['changedModel'] };
const urlBase = `ClientLogs/${route.params.id}`;
onBeforeMount(() => {
stateStore.rightDrawer = true;
filterClientLogs.where = {
and: [
{ originFk: `${route.params.id}` },
{ userFk: { neq: radioButtonValue.value } },
{ action: { inq: inq.value } },
],
};
urlClientLogsEditors.value = `${urlBase}/editors`;
urlClientLogsModels.value = `${urlBase}/models`;
});
const getClientLogs = async (value, status) => {
if (status === 'neq') {
neq.value = value;
} else {
setInq(value, status);
}
filterClientLogs.where = {
and: [
{ originFk: `${route.params.id}` },
{ userFk: { neq: neq.value } },
{ action: { inq: inq.value } },
],
};
clientLogs.value?.fetch();
};
const setInq = (value, status) => {
if (status) {
if (!inq.value.includes(value)) {
inq.value.push(value);
}
} else {
inq.value = inq.value.filter((item) => item !== value);
}
};
</script> </script>
<template> <template>
<FetchData <VnLog model="Client" />
:filter="filterClientLogs"
@on-fetch="(data) => (clientLogsOptions = data)"
auto-load
url="ClientLogs"
ref="clientLogs"
/>
<FetchData
:filter="filterClientLogsEditors"
@on-fetch="(data) => (clientLogsEditorsOptions = data)"
auto-load
:url="urlClientLogsEditors"
/>
<FetchData
:filter="filterClientLogsModels"
@on-fetch="(data) => (clientLogsModelsOptions = data)"
auto-load
:url="urlClientLogsModels"
/>
<h5 class="flex justify-center color-vn-label">
{{ t('globals.noResults') }}
</h5>
<QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer">
<div class="q-mt-sm q-px-md">
<VnInput :label="t('Search')" clearable>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>
{{ t('Search by id or concept') }}
</QTooltip>
</QIcon>
</template>
</VnInput>
<VnSelect
:label="t('Entity')"
:options="[]"
class="q-mt-md"
hide-selected
option-label="name"
option-value="id"
/>
<div class="q-mt-lg">
<QRadio
:dark="true"
:label="t('All')"
@update:model-value="getClientLogs($event, 'neq')"
dense
v-model="radioButtonValue"
val="all"
/>
</div>
<div class="q-mt-md">
<QRadio
:dark="true"
:label="t('User')"
@update:model-value="getClientLogs($event, 'neq')"
dense
v-model="radioButtonValue"
val="user"
/>
</div>
<div class="q-mt-md">
<QRadio
:dark="true"
:label="t('System')"
@update:model-value="getClientLogs($event, 'neq')"
dense
v-model="radioButtonValue"
val="system"
/>
</div>
<VnSelect
:label="t('User')"
:options="[]"
class="q-mt-sm"
hide-selected
option-label="name"
option-value="id"
/>
<VnInput :label="t('Changes')" clearable class="q-mt-sm">
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>
{{ t('Search by changes') }}
</QTooltip>
</QIcon>
</template>
</VnInput>
<div class="q-mt-md">
<QCheckbox
:label="t('Creates')"
@update:model-value="getClientLogs('insert', $event)"
v-model="insert"
/>
</div>
<div>
<QCheckbox
:label="t('Edits')"
@update:model-value="getClientLogs('update', $event)"
v-model="update"
/>
</div>
<div>
<QCheckbox
:label="t('Deletes')"
@update:model-value="getClientLogs('delete', $event)"
v-model="deletes"
/>
</div>
<div>
<QCheckbox
:label="t('Accesses')"
@update:model-value="getClientLogs('select', $event)"
v-model="select"
/>
</div>
<VnInputDate :label="t('Date')" class="q-mt-sm" />
<VnInput :label="t('To')" clearable class="q-mt-md" />
</div>
</QDrawer>
<QPageSticky
:offset="[18, 18]"
v-if="radioButtonValue !== 'all' || insert || update || deletes || select"
>
<QBtn color="primary" fab icon="filter_alt_off" />
<QTooltip>
{{ t('Quit filter') }}
</QTooltip>
</QPageSticky>
</template> </template>
<i18n>
es:
Search: Buscar
Search by id or concept: xxx
Entity: Entidad
All: Todo
User: Usuario
System: Sistema
Changes: Cambios
Search by changes: xxx
Creates: Crea
Edits: Modifica
Deletes: Elimina
Accesses: Accede
Date: Fecha
To: Hasta
Quit filter: Quitar filtro
</i18n>

View File

@ -1,15 +1,18 @@
<script setup> <script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnNotes from 'src/components/ui/VnNotes.vue'; import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute(); const route = useRoute();
const noteFilter = { const noteFilter = computed(() => {
order: 'created DESC', return {
where: { order: 'created DESC',
clientFk: `${route.params.id}`, where: {
}, clientFk: `${route.params.id}`,
}; },
};
});
</script> </script>
<template> <template>

View File

@ -1,146 +1,107 @@
<script setup> <script setup>
import axios from 'axios';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute } from 'vue-router';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate } from 'src/filters';
import FetchData from 'components/FetchData.vue'; import VnTable from 'components/VnTable/VnTable.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const tableRef = ref();
const rows = ref([]); const filter = computed(() => {
return {
const filter = { where: { clientFk: route.params.id },
where: { clientFk: route.params.id }, };
order: ['started DESC'], });
limit: 20, const componentColumn = (type) => {
return {
columnFilter: {
component: type,
},
columnCreate: {
component: type,
},
};
}; };
const tableColumnComponents = {
since: {
component: 'span',
props: () => {},
event: () => {},
},
to: {
component: 'span',
props: () => {},
event: () => {},
},
amount: {
component: 'span',
props: () => {},
event: () => {},
},
period: {
component: 'span',
props: () => {},
event: () => {},
},
};
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
field: 'started', label: t('globals.since'),
label: t('Since'), name: 'started',
name: 'since', format: ({ started }) => toDate(started),
format: (value) => toDate(value), create: true,
...componentColumn('date'),
}, },
{ {
align: 'left', align: 'left',
field: 'finished', name: 'finished',
label: t('To'), label: t('globals.to'),
name: 'to', format: ({ finished }) => toDate(finished),
format: (value) => toDate(value), create: true,
...componentColumn('date'),
}, },
{ {
align: 'left', align: 'left',
field: 'amount',
label: t('Amount'),
name: 'amount', name: 'amount',
format: (value) => toCurrency(value), label: t('globals.amount'),
format: ({ amount }) => toCurrency(amount),
create: true,
...componentColumn('number'),
}, },
{ {
align: 'left', align: 'left',
field: 'period',
label: t('Period'),
name: 'period', name: 'period',
label: t('Period'),
create: true,
...componentColumn('number'),
},
{
align: 'left',
name: 'tableActions',
actions: [
{
title: t('Finish that recovery period'),
icon: 'lock',
show: (row) => !row.finished,
action: ({ id }) => setFinished(id),
isPrimary: true,
},
],
}, },
]); ]);
const toCustomerRecoverieCreate = () => { function setFinished(id) {
router.push({ name: 'CustomerRecoverieCreate' }); axios.patch(`Recoveries/${id}`, { finished: Date.vnNow() });
}; tableRef.value.reload();
}
</script> </script>
<template> <template>
<FetchData <VnTable
:filter="filter" ref="tableRef"
@on-fetch="(data) => (rows = data)" data-key="Recoveries"
auto-load
url="Recoveries" url="Recoveries"
search-url="recoveries"
:filter="filter"
order="started DESC"
:columns="columns"
:use-model="true"
:right-search="false"
:create="{
urlCreate: 'Recoveries',
title: 'New recovery',
onDataSaved: () => tableRef.reload(),
formInitialData: { clientFk: route.params.id, started: Date.vnNew() },
}"
auto-load
/> />
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns"
:no-data-label="t('globals.noResults')"
:pagination="{ rowsPerPage: 12 }"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
>
<template #body-cell="props">
<QTd :props="props">
<QTr :props="props" class="cursor-pointer">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template>
</QTable>
</QPage>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerRecoverieCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New recoverie') }}
</QTooltip>
</QPageSticky>
</template> </template>
<style lang="scss">
.consignees-card {
border: 2px solid var(--vn-accent-color);
border-radius: 10px;
padding: 10px;
}
.label-color {
color: var(--vn-label-color);
}
</style>
<i18n> <i18n>
es: es:
Since: Desde
To: Hasta
Amount: Importe
Period: Periodo Period: Periodo
New recoverie: Nuevo recobro New recovery: Nuevo recobro
Finish that recovery period: Terminar recobro
</i18n> </i18n>

View File

@ -1,17 +1,16 @@
<script setup> <script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnSms from 'src/components/ui/VnSms.vue'; import VnSms from 'src/components/ui/VnSms.vue';
const route = useRoute(); const route = useRoute();
const id = route.params.id; const where = computed(() => {
return {
const where = { clientFk: route.params.id,
clientFk: id, ticketFk: null,
ticketFk: null, };
}; });
</script> </script>
<template> <template>
<div class="column items-center"> <VnSms url="clientSms" :where="where" />
<VnSms url="clientSms" :where="where" />
</div>
</template> </template>

View File

@ -217,7 +217,11 @@ const creditWarning = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one" v-if="entity.account"> <QCard class="vn-one" v-if="entity.account">
<VnTitle :text="t('customer.summary.businessData')" /> <VnTitle
:url="`https://grafana.verdnatura.es/d/adjlxzv5yjt34d/analisis-de-clientes-7c-crm?orgId=1&var-clientFk=${entityId}`"
:text="t('customer.summary.businessData')"
icon="vn:grafana"
/>
<VnLv <VnLv
:label="t('customer.summary.totalGreuge')" :label="t('customer.summary.totalGreuge')"
:value="toCurrency(entity.totalGreuge)" :value="toCurrency(entity.totalGreuge)"

View File

@ -2,164 +2,81 @@
import { computed, ref } 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 axios from 'axios';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue'; import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';
import FormModel from 'components/FormModel.vue';
const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator();
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const stateStore = useStateStore();
const active = ref(false);
const canChangePassword = ref(0); const canChangePassword = ref(0);
const email = ref(null);
const isLoading = ref(false);
const name = ref(null);
const usersPreviewRef = ref(null);
const user = ref([]);
const dataChanges = computed(() => { const filter = computed(() => {
return ( return {
user.value.active !== active.value || fields: ['active', 'email', 'name'],
user.value.email !== email.value || where: { id: route.params.id },
user.value.name !== name.value };
);
}); });
const filter = { where: { id: `${route.params.id}` } };
const showChangePasswordDialog = () => { const showChangePasswordDialog = () => {
quasar.dialog({ quasar.dialog({
component: CustomerChangePassword, component: CustomerChangePassword,
componentProps: { componentProps: {
id: route.params.id, id: route.params.id,
promise: usersPreviewRef.value.fetch(),
}, },
}); });
}; };
const setInitialData = () => { async function hasCustomerRole() {
if (user.value.length) { const { data } = await axios(`Clients/${route.params.id}/hasCustomerRole`);
active.value = user.value[0].active; canChangePassword.value = data;
email.value = user.value[0].email; }
name.value = user.value[0].name;
}
};
const onSubmit = async () => {
isLoading.value = true;
const payload = {
active: active.value,
email: email.value,
name: name.value,
};
try {
await axios.patch(`Clients/${route.params.id}/updateUser`, payload);
notify('globals.dataSaved', 'positive');
if (usersPreviewRef.value) usersPreviewRef.value.fetch();
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
}
};
</script> </script>
<template> <template>
<FetchData <FormModel
url="VnUsers/preview"
:url-update="`Clients/${route.params.id}/updateUser`"
:filter="filter" :filter="filter"
@on-fetch=" model="webAccess"
(data) => { :mapper="
user = data; ({ active, name, email }) => {
setInitialData(); return {
active,
name,
email,
};
} }
" "
@on-fetch="hasCustomerRole()"
auto-load auto-load
ref="usersPreviewRef" >
url="VnUsers/preview" <template #form="{ data, validate }">
/> <QCheckbox :label="t('Enable web access')" v-model="data.active" />
<FetchData <VnInput :label="t('User')" clearable v-model="data.name" />
:url="`Clients/${route.params.id}/hasCustomerRole`" <VnInput
@on-fetch="(data) => (canChangePassword = data)" :label="t('Recovery email')"
auto-load :rules="validate('client.email')"
/> clearable
type="email"
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> v-model="data.email"
<QBtnGroup push class="q-gutter-x-sm"> class="q-mt-sm"
<QBtn :info="t('This email is used for user to regain access their account')"
:disabled="isLoading"
:label="t('globals.reset')"
:loading="isLoading"
@click="setInitialData"
color="primary"
flat
icon="restart_alt"
type="reset"
/> />
</template>
<template #moreActions>
<QBtn <QBtn
:disabled="isLoading"
:label="t('Change password')" :label="t('Change password')"
:loading="isLoading"
@click.stop="showChangePasswordDialog()"
color="primary" color="primary"
flat
icon="edit" icon="edit"
v-if="canChangePassword" :disable="!canChangePassword"
@click="showChangePasswordDialog()"
/> />
<QBtn </template>
:disabled="isLoading || !dataChanges" </FormModel>
:label="t('globals.save')"
:loading="isLoading"
@click="onSubmit"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QCardSection>
<QForm>
<QCheckbox :label="t('Enable web access')" v-model="active" />
<div class="q-px-sm">
<VnInput :label="t('User')" clearable v-model="name" />
<VnInput
:label="t('Recovery email')"
:rules="validate('client.email')"
clearable
type="email"
v-model="email"
class="q-mt-sm"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t(
'This email is used for user to regain access their account'
)
}}</QTooltip>
</QIcon>
</template>
</VnInput>
</div>
</QForm>
</QCardSection>
</QCard>
</div>
</template> </template>
<i18n> <i18n>

View File

@ -357,7 +357,7 @@ const columns = computed(() => [
isPrimary: true, isPrimary: true,
}, },
{ {
title: t('Client ticket list'), title: t('components.smartCard.viewSummary'),
icon: 'preview', icon: 'preview',
action: (row) => viewSummary(row.id, CustomerSummary), action: (row) => viewSummary(row.id, CustomerSummary),
}, },

View File

@ -3,7 +3,7 @@ import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -47,7 +47,11 @@ const props = defineProps({
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnCurrency v-model="params.amount" is-outlined /> <VnInputNumber
:label="t('Amount')"
v-model="params.amount"
is-outlined
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>

View File

@ -2,18 +2,24 @@
import { onBeforeMount, reactive, ref } from 'vue'; import { onBeforeMount, reactive, 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 { useDialogPluginComponent } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
import { usePrintService } from 'composables/usePrintService';
import useNotify from 'src/composables/useNotify.js';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputNumber from 'components/common/VnInputNumber.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';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const { notify } = useNotify();
const { sendEmail, openReport } = usePrintService();
const { dialogRef } = useDialogPluginComponent(); const { dialogRef } = useDialogPluginComponent();
const $props = defineProps({ const $props = defineProps({
@ -27,7 +33,7 @@ const $props = defineProps({
}, },
promise: { promise: {
type: Function, type: Function,
required: true, default: null,
}, },
}); });
@ -36,11 +42,12 @@ const urlCreate = ref([]);
const companyOptions = ref([]); const companyOptions = ref([]);
const bankOptions = ref([]); const bankOptions = ref([]);
const clientFindOne = ref([]); const clientFindOne = ref([]);
const deliveredAmount = ref(null);
const amountToReturn = ref(null);
const viewReceipt = ref(); const viewReceipt = ref();
const sendEmail = ref(false); const shouldSendEmail = ref(false);
const isLoading = ref(false); const maxAmount = ref();
const accountingType = ref({});
const isCash = ref(false);
const formModelRef = ref(false);
const filterBanks = { const filterBanks = {
fields: ['id', 'bank', 'accountingTypeFk'], fields: ['id', 'bank', 'accountingTypeFk'],
@ -67,87 +74,123 @@ onBeforeMount(() => {
urlCreate.value = `Clients/${route.params.id}/createReceipt`; urlCreate.value = `Clients/${route.params.id}/createReceipt`;
}); });
const setPaymentType = (value) => { function setPaymentType(accounting) {
// if (id === 1) initialData.description = 'Credit card'; if (!accounting) return;
// if (id === 2) initialData.description = 'Cash'; accountingType.value = accounting.accountingType;
// if (id === 3 || id === 3117) initialData.description = '';
// if (id === 4) initialData.description = 'Transfer'; initialData.description = [];
// const CASH_CODE = 2; initialData.payed = Date.vnNew();
// // const CASH_CODE = 2 isCash.value = accountingType.value.code == 'cash';
// if (!value) return; viewReceipt.value = isCash.value;
// const accountingType = CASH_CODE; if (accountingType.value.daysInFuture)
// initialData.description = ''; initialData.payed.setDate(
// viewReceipt.value = value == CASH_CODE; initialData.payed.getDate() + accountingType.value.daysInFuture
// if (accountingType.code == 'compensation') this.receipt.description = ''; );
// else { maxAmount.value = accountingType.value && accountingType.value.maxAmount;
// if (
// accountingType.receiptDescription != null && if (accountingType.value.code == 'compensation')
// accountingType.receiptDescription != '' return (initialData.description = '');
// ) if (accountingType.value.receiptDescription)
// this.receipt.description.push(accountingType.receiptDescription); initialData.description.push(accountingType.value.receiptDescription);
// if (this.originalDescription) if (initialData.description) initialData.description.push(initialData.description);
// this.receipt.description.push(this.originalDescription);
// this.receipt.description = this.receipt.description.join(', '); initialData.description = initialData.description.join(', ');
// } }
// this.maxAmount = accountingType && accountingType.maxAmount;
// this.receipt.payed = Date.vnNew();
// if (accountingType.daysInFuture)
// this.receipt.payed.setDate(
// this.receipt.payed.getDate() + accountingType.daysInFuture
// );
};
const calculateFromAmount = (event) => { const calculateFromAmount = (event) => {
amountToReturn.value = parseFloat(event) * -1 + parseFloat(deliveredAmount.value); initialData.amountToReturn =
parseFloat(initialData.deliveredAmount) + parseFloat(event) * -1;
}; };
const calculateFromDeliveredAmount = (event) => { const calculateFromDeliveredAmount = (event) => {
amountToReturn.value = parseFloat($props.totalCredit) * -1 + parseFloat(event); initialData.amountToReturn = parseFloat(event) - initialData.amountPaid;
}; };
const setClientEmail = (data) => { function onBeforeSave(data) {
initialData.email = data.email; const exceededAmount = data.amountPaid > maxAmount.value;
}; if (isCash.value && exceededAmount)
return notify(t('Amount exceeded', { maxAmount: maxAmount.value }), 'negative');
const onDataSaved = async () => { if (isCash.value && shouldSendEmail.value && !data.email)
isLoading.value = true; return notify(t('There is no assigned email for this client'), 'negative');
if ($props.promise) {
try { data.bankFk = data.bankFk.id;
await $props.promise(); return data;
} finally { }
isLoading.value = false;
if (closeButton.value) closeButton.value.click(); async function onDataSaved(formData, { id }) {
} try {
if (shouldSendEmail.value && isCash.value)
await sendEmail(`Receipts/${id}/receipt-email`, {
recipient: formData.email,
});
if (viewReceipt.value) openReport(`Receipts/${id}/receipt-pdf`);
} finally {
if ($props.promise) $props.promise();
if (closeButton.value) closeButton.value.click();
} }
}; }
async function accountShortToStandard({ target: { value } }) {
if (!value) return (initialData.description = '');
initialData.compensationAccount = value.replace('.', '0'.repeat(11 - value.length));
const params = { bankAccount: initialData.compensationAccount };
const { data } = await axios(`Clients/getClientOrSupplierReference`, { params });
if (!data.clientId) {
initialData.description = t('Supplier Compensation Reference', {
supplierId: data.supplierId,
supplierName: data.supplierName,
});
return;
}
initialData.description = t('Client Compensation Reference', {
clientId: data.clientId,
clientName: data.clientName,
});
}
async function getAmountPaid() {
const filter = {
where: {
clientFk: route.params.id,
companyFk: initialData.companyFk,
},
};
const { data } = await axios(`ClientRisks`, {
params: { filter: JSON.stringify(filter) },
});
initialData.amountPaid = (data?.length && data[0].amount) || undefined;
}
</script> </script>
<template> <template>
<QDialog ref="dialogRef"> <QDialog ref="dialogRef" persistent>
<fetch-data <FetchData
@on-fetch="(data) => (companyOptions = data)" @on-fetch="(data) => (companyOptions = data)"
auto-load auto-load
url="Companies" url="Companies"
/> />
<fetch-data <FetchData
:filter="filterBanks" :filter="filterBanks"
@on-fetch="(data) => (bankOptions = data)" @on-fetch="(data) => (bankOptions = data)"
auto-load auto-load
url="Accountings" url="Accountings"
/> />
<fetch-data <FetchData
:filter="filterClientFindOne" :filter="filterClientFindOne"
@on-fetch="setClientEmail" @on-fetch="({ email }) => (initialData.email = email)"
auto-load auto-load
url="Clients/findOne" url="Clients/findOne"
/> />
<FormModel <FormModel
:default-actions="false" ref="formModelRef"
:form-initial-data="initialData" :form-initial-data="initialData"
:observe-form-changes="false" :observe-form-changes="false"
:url-create="urlCreate" :url-create="urlCreate"
@on-data-saved="onDataSaved()" :mapper="onBeforeSave"
@on-data-saved="onDataSaved"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<span ref="closeButton" class="row justify-end close-icon" v-close-popup> <span ref="closeButton" class="row justify-end close-icon" v-close-popup>
@ -171,19 +214,22 @@ const onDataSaved = async () => {
option-label="code" option-label="code"
option-value="id" option-value="id"
v-model="data.companyFk" v-model="data.companyFk"
@update:model-value="getAmountPaid()"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelect <VnSelect
:label="t('Bank')" :label="t('Bank')"
:options="bankOptions"
:required="true"
@update:model-value="setPaymentType($event)"
hide-selected
option-label="bank"
option-value="id"
v-model="data.bankFk" v-model="data.bankFk"
url="Accountings"
option-label="bank"
:include="{ relation: 'accountingType' }"
sort-by="id"
:limit="0"
@update:model-value="
(value, options) => setPaymentType(value, options)
"
:emit-value="false"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -200,59 +246,54 @@ const onDataSaved = async () => {
:required="true" :required="true"
@update:model-value="calculateFromAmount($event)" @update:model-value="calculateFromAmount($event)"
clearable clearable
type="number"
v-model.number="data.amountPaid" v-model.number="data.amountPaid"
/> />
</VnRow> </VnRow>
<div v-if="data.bankFk?.accountingType?.code == 'compensation'">
<div class="text-h6" v-if="data.bankFk === 3 || data.bankFk === 3117"> <div class="text-h6">
{{ t('Compensation') }} {{ t('Compensation') }}
</div> </div>
<VnRow>
<VnRow>
<div class="col" v-if="data.bankFk === 3 || data.bankFk === 3117">
<VnInput <VnInput
:label="t('Compensation account')" :label="t('Compensation account')"
clearable clearable
v-model="data.compensationAccount" v-model="data.compensationAccount"
@blur="accountShortToStandard"
/> />
</div> </VnRow>
<VnInput </div>
:label="t('Reference')" <VnInput
:required="true" :label="t('Reference')"
clearable :required="true"
v-model="data.description" clearable
/> v-model="data.description"
</VnRow> />
<div class="q-mt-lg" v-if="data.bankFk === 2"> <div v-if="data.bankFk?.accountingType?.code == 'cash'">
<div class="text-h6">{{ t('Cash') }}</div> <div class="text-h6">{{ t('Cash') }}</div>
<VnRow> <VnRow>
<VnInput <VnInputNumber
:label="t('Delivered amount')" :label="t('Delivered amount')"
@update:model-value="calculateFromDeliveredAmount($event)" @update:model-value="calculateFromDeliveredAmount($event)"
clearable clearable
type="number" v-model="data.deliveredAmount"
v-model="deliveredAmount"
/> />
<VnInput <VnInputNumber
:label="t('Amount to return')" :label="t('Amount to return')"
clearable
disable disable
type="number" v-model="data.amountToReturn"
v-model="amountToReturn"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox v-model="viewReceipt" /> <QCheckbox v-model="viewReceipt" :label="t('View recipt')" />
<QCheckbox v-model="sendEmail" /> <QCheckbox v-model="shouldSendEmail" :label="t('Send email')" />
</VnRow> </VnRow>
</div> </div>
<div class="q-mt-lg row justify-end"> <div class="q-mt-lg row justify-end">
<QBtn <QBtn
:disabled="isLoading" :disabled="formModelRef.isLoading"
:label="t('globals.cancel')" :label="t('globals.cancel')"
:loading="isLoading" :loading="formModelRef.isLoading"
class="q-ml-sm" class="q-ml-sm"
color="primary" color="primary"
flat flat
@ -260,9 +301,9 @@ const onDataSaved = async () => {
v-close-popup v-close-popup
/> />
<QBtn <QBtn
:disabled="isLoading" :disabled="formModelRef.isLoading"
:label="t('globals.save')" :label="t('globals.save')"
:loading="isLoading" :loading="formModelRef.isLoading"
color="primary" color="primary"
type="submit" type="submit"
/> />
@ -287,4 +328,8 @@ es:
Send email: Enviar correo Send email: Enviar correo
Compensation: Compensación Compensation: Compensación
Compensation account: Cuenta para compensar Compensation account: Cuenta para compensar
Supplier Compensation Reference: ({supplierId}) Ntro Proveedor {supplierName}
Client Compensation Reference: ({clientId}) Ntro Cliente {clientName}
There is no assigned email for this client: No hay correo asignado para este cliente
Amount exceeded: Según ley contra el fraude no se puede recibir cobros por importe igual o superior a {maxAmount}
</i18n> </i18n>

View File

@ -19,8 +19,6 @@ const { t } = useI18n();
const { hasAny } = useRole(); const { hasAny } = useRole();
const isAdministrative = () => hasAny(['administrative']); const isAdministrative = () => hasAny(['administrative']);
const suppliersOptions = ref([]);
const travelsOptions = ref([]);
const companiesOptions = ref([]); const companiesOptions = ref([]);
const currenciesOptions = ref([]); const currenciesOptions = ref([]);
@ -29,20 +27,6 @@ const onFilterTravelSelected = (formData, id) => {
}; };
</script> </script>
<template> <template>
<FetchData
url="Suppliers"
:filter="{ fields: ['id', 'nickname'] }"
order="nickname"
@on-fetch="(data) => (suppliersOptions = data)"
auto-load
/>
<FetchData
url="Travels/filter"
:filter="{ fields: ['id', 'warehouseInName'] }"
order="id"
@on-fetch="(data) => (travelsOptions = data)"
auto-load
/>
<FetchData <FetchData
ref="companiesRef" ref="companiesRef"
url="Companies" url="Companies"
@ -71,9 +55,10 @@ const onFilterTravelSelected = (formData, id) => {
<VnSelect <VnSelect
:label="t('entry.basicData.supplier')" :label="t('entry.basicData.supplier')"
v-model="data.supplierFk" v-model="data.supplierFk"
:options="suppliersOptions" url="Suppliers"
option-value="id" option-value="id"
option-label="nickname" option-label="nickname"
:fields="['id', 'nickname']"
hide-selected hide-selected
:required="true" :required="true"
map-options map-options
@ -92,7 +77,8 @@ const onFilterTravelSelected = (formData, id) => {
<VnSelectDialog <VnSelectDialog
:label="t('entry.basicData.travel')" :label="t('entry.basicData.travel')"
v-model="data.travelFk" v-model="data.travelFk"
:options="travelsOptions" url="Travels/filter"
:fields="['id', 'warehouseInName']"
option-value="id" option-value="id"
option-label="warehouseInName" option-label="warehouseInName"
map-options map-options

View File

@ -423,7 +423,7 @@ const lockIconType = (groupingMode, mode) => {
<span v-if="props.row.item.subName" class="subName"> <span v-if="props.row.item.subName" class="subName">
{{ props.row.item.subName }} {{ props.row.item.subName }}
</span> </span>
<FetchedTags :item="props.row.item" :max-length="5" /> <FetchedTags :item="props.row.item" />
</QTd> </QTd>
</QTr> </QTr>
</template> </template>

View File

@ -10,8 +10,10 @@ import EntryFilter from '../EntryFilter.vue';
:descriptor="EntryDescriptor" :descriptor="EntryDescriptor"
:filter-panel="EntryFilter" :filter-panel="EntryFilter"
search-data-key="EntryList" search-data-key="EntryList"
search-url="Entries/filter" :searchbar-props="{
searchbar-label="Search entries" url: 'Entries/filter',
searchbar-info="You can search by entry reference" label: 'Search entries',
info: 'You can search by entry reference',
}"
/> />
</template> </template>

View File

@ -319,7 +319,7 @@ const fetchEntryBuys = async () => {
<span v-if="row.item.subName" class="subName"> <span v-if="row.item.subName" class="subName">
{{ row.item.subName }} {{ row.item.subName }}
</span> </span>
<FetchedTags :item="row.item" :max-length="5" /> <FetchedTags :item="row.item" />
</QTd> </QTd>
</QTr> </QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys --> <!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->

View File

@ -20,11 +20,17 @@ const $props = defineProps({
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const entriesTableColumns = computed(() => [ const entriesTableColumns = computed(() => [
{
align: 'left',
name: 'id',
field: 'id',
label: t('globals.id'),
},
{ {
align: 'left', align: 'left',
name: 'itemFk', name: 'itemFk',
field: 'itemFk', field: 'itemFk',
label: t('globals.id'), label: t('entry.latestBuys.tableVisibleColumns.itemFk'),
}, },
{ {
align: 'left', align: 'left',
@ -76,11 +82,11 @@ const entriesTableColumns = computed(() => [
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">
<QBtn <QBtn
:label="t('Print buys')" :label="t('printLabels')"
color="primary" color="primary"
icon="print" icon="print"
:loading="isLoading" :loading="isLoading"
@click="openReport(`Entries/${entityId}/buy-label`)" @click="openReport(`Entries/${entityId}/print`)"
unelevated unelevated
autofocus autofocus
/> />
@ -109,6 +115,19 @@ const entriesTableColumns = computed(() => [
<QTd v-for="col in props.cols" :key="col.name"> <QTd v-for="col in props.cols" :key="col.name">
{{ col.value }} {{ col.value }}
</QTd> </QTd>
<QBtn
icon="print"
v-if="props.row.stickers > 0"
:loading="isLoading"
@click="
openReport(
`Entries/${props.row.id}/buy-label`
)
"
unelevated
>
<QTooltip>{{ t('printLabel') }}</QTooltip>
</QBtn>
</QTr> </QTr>
</template> </template>
</QTable> </QTable>

View File

@ -20,7 +20,6 @@ const props = defineProps({
const currenciesOptions = ref([]); const currenciesOptions = ref([]);
const companiesOptions = ref([]); const companiesOptions = ref([]);
const suppliersOptions = ref([]);
const stateStore = useStateStore(); const stateStore = useStateStore();
onMounted(async () => { onMounted(async () => {
@ -45,14 +44,6 @@ onMounted(async () => {
@on-fetch="(data) => (currenciesOptions = data)" @on-fetch="(data) => (currenciesOptions = data)"
auto-load auto-load
/> />
<FetchData
url="Suppliers"
:filter="{ fields: ['id', 'nickname', 'name'] }"
order="nickname"
@on-fetch="(data) => (suppliersOptions = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
@ -135,9 +126,11 @@ onMounted(async () => {
:label="t('params.supplierFk')" :label="t('params.supplierFk')"
v-model="params.supplierFk" v-model="params.supplierFk"
@update:model-value="searchFn()" @update:model-value="searchFn()"
:options="suppliersOptions" url="Suppliers"
option-value="id" option-value="id"
option-label="name" option-label="name"
:fields="['id', 'name', 'nickname']"
sort-by="nickname"
hide-selected hide-selected
dense dense
outlined outlined

View File

@ -7,11 +7,16 @@ import { useStateStore } from 'stores/useStateStore';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import EntrySummary from './Card/EntrySummary.vue';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import TravelDescriptorProxy from 'src/pages/Travel/Card/TravelDescriptorProxy.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
const { viewSummary } = useSummaryDialog();
const entryFilter = { const entryFilter = {
include: [ include: [
{ {
@ -142,6 +147,12 @@ const columns = computed(() => [
create: true, create: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef), format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef),
}, },
{
align: 'left',
label: t('entry.list.tableVisibleColumns.invoiceAmount'),
name: 'invoiceAmount',
cardVisible: true,
},
{ {
align: 'left', align: 'left',
label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'), label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'),
@ -168,6 +179,18 @@ const columns = computed(() => [
inWhere: true, inWhere: true,
}, },
}, },
{
align: 'right',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.viewSummary'),
icon: 'preview',
action: (row) => viewSummary(row.id, EntrySummary),
isPrimary: true,
},
],
},
]); ]);
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
@ -201,7 +224,20 @@ onMounted(async () => {
redirect="entry" redirect="entry"
auto-load auto-load
:right-search="false" :right-search="false"
/> >
<template #column-supplierFk="{ row }">
<span class="link" @click.stop>
{{ row.supplierName }}
<SupplierDescriptorProxy :id="row.supplierFk" />
</span>
</template>
<template #column-travelFk="{ row }">
<span class="link" @click.stop>
{{ row.travelRef }}
<TravelDescriptorProxy :id="row.travelFk" />
</span>
</template>
</VnTable>
</template> </template>
<i18n> <i18n>

View File

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

View File

@ -10,4 +10,5 @@ landed: Landed
shipped: Shipped shipped: Shipped
fromShipped: Shipped(from) fromShipped: Shipped(from)
toShipped: Shipped(to) toShipped: Shipped(to)
printBuys: Print buys printLabels: Print stickers
printLabel: Print sticker

View File

@ -14,4 +14,5 @@ landed: F. llegada
shipped: F. salida shipped: F. salida
fromShipped: F. salida(desde) fromShipped: F. salida(desde)
toShipped: F. salida(hasta) toShipped: F. salida(hasta)
Print buys: Imprimir etiquetas printLabels: Imprimir etiquetas
printLabel: Imprimir etiqueta

View File

@ -130,8 +130,6 @@ onBeforeMount(async () => {
}); });
onBeforeRouteUpdate(async (to, from) => { onBeforeRouteUpdate(async (to, from) => {
invoiceInCorrection.correcting.length = 0;
invoiceInCorrection.corrected = null;
if (to.params.id !== from.params.id) { if (to.params.id !== from.params.id) {
await setInvoiceCorrection(to.params.id); await setInvoiceCorrection(to.params.id);
const { data } = await axios.get(`InvoiceIns/${to.params.id}/getTotals`); const { data } = await axios.get(`InvoiceIns/${to.params.id}/getTotals`);
@ -140,6 +138,8 @@ onBeforeRouteUpdate(async (to, from) => {
}); });
async function setInvoiceCorrection(id) { async function setInvoiceCorrection(id) {
invoiceInCorrection.correcting.length = 0;
invoiceInCorrection.corrected = null;
const { data: correctingData } = await axios.get('InvoiceInCorrections', { const { data: correctingData } = await axios.get('InvoiceInCorrections', {
params: { filter: { where: { correctingFk: id } } }, params: { filter: { where: { correctingFk: id } } },
}); });

View File

@ -8,23 +8,22 @@ import { useArrayData } from 'src/composables/useArrayData';
import CrudModel from 'src/components/CrudModel.vue'; import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const route = useRoute(); const route = useRoute();
const { notify } = useNotify(); const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const arrayData = useArrayData(); const arrayData = useArrayData();
const invoiceIn = computed(() => arrayData.store.data); const invoiceIn = computed(() => arrayData.store.data);
const currency = computed(() => invoiceIn.value?.currency?.code);
const rowsSelected = ref([]); const rowsSelected = ref([]);
const banks = ref([]); const banks = ref([]);
const invoiceInFormRef = ref(); const invoiceInFormRef = ref();
const invoiceId = +route.params.id; const invoiceId = +route.params.id;
const placeholder = 'yyyy/mm/dd';
const filter = { where: { invoiceInFk: invoiceId } }; const filter = { where: { invoiceInFk: invoiceId } };
const columns = computed(() => [ const columns = computed(() => [
@ -104,42 +103,7 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
> >
<template #body-cell-duedate="{ row }"> <template #body-cell-duedate="{ row }">
<QTd> <QTd>
<QInput <VnInputDate v-model="row.dueDated" />
v-model="row.dueDated"
mask="date"
:placeholder="placeholder"
clearable
clear-icon="close"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="row.dueDated" landscape>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.confirm')"
color="primary"
flat
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QTd> </QTd>
</template> </template>
<template #body-cell-bank="{ row, col }"> <template #body-cell-bank="{ row, col }">
@ -164,7 +128,7 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
</template> </template>
<template #body-cell-amount="{ row }"> <template #body-cell-amount="{ row }">
<QTd> <QTd>
<VnCurrency <VnInputNumber
v-model="row.amount" v-model="row.amount"
:is-outlined="false" :is-outlined="false"
clearable clearable
@ -174,11 +138,11 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
</template> </template>
<template #body-cell-foreignvalue="{ row }"> <template #body-cell-foreignvalue="{ row }">
<QTd> <QTd>
<QInput <VnInputNumber
:class="{ :class="{
'no-pointer-events': !isNotEuro(invoiceIn.currency.code), 'no-pointer-events': !isNotEuro(currency),
}" }"
:disable="!isNotEuro(invoiceIn.currency.code)" :disable="!isNotEuro(currency)"
v-model="row.foreignValue" v-model="row.foreignValue"
clearable clearable
clear-icon="close" clear-icon="close"
@ -191,9 +155,7 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
<QTd /> <QTd />
<QTd /> <QTd />
<QTd> <QTd>
{{ {{ toCurrency(getTotalAmount(rows), currency) }}
toCurrency(getTotalAmount(rows), invoiceIn.currency.code)
}}
</QTd> </QTd>
<QTd /> <QTd />
</QTr> </QTr>
@ -207,51 +169,11 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
<QSeparator /> <QSeparator />
<QList> <QList>
<QItem> <QItem>
<QInput <VnInputDate
class="full-width" class="full-width"
:label="t('Date')" :label="t('Date')"
v-model="props.row.dueDated" v-model="props.row.dueDated"
mask="date" />
:placeholder="placeholder"
clearable
clear-icon="close"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate
v-model="props.row.dueDated"
landscape
>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="
t('globals.cancel')
"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="
t('globals.confirm')
"
color="primary"
flat
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItem> </QItem>
<QItem> <QItem>
<VnSelect <VnSelect
@ -274,24 +196,20 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
</VnSelect> </VnSelect>
</QItem> </QItem>
<QItem> <QItem>
<QInput <VnInputNumber
:label="t('Amount')" :label="t('Amount')"
class="full-width" class="full-width"
v-model="props.row.amount" v-model="props.row.amount"
clearable
clear-icon="close"
/> />
</QItem> </QItem>
<QItem> <QItem>
<QInput <VnInputNumber
:label="t('Foreign value')" :label="t('Foreign value')"
class="full-width" class="full-width"
:class="{ :class="{
'no-pointer-events': !isNotEuro( 'no-pointer-events': !isNotEuro(currency),
invoiceIn.currency.code
),
}" }"
:disable="!isNotEuro(invoiceIn.currency.code)" :disable="!isNotEuro(currency)"
v-model="props.row.foreignValue" v-model="props.row.foreignValue"
clearable clearable
clear-icon="close" clear-icon="close"

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