0
0
Fork 0

Merge branch 'dev' into 6739-invoiceOut

This commit is contained in:
Jon Elias 2024-06-14 09:45:03 +00:00
commit 21506da779
39 changed files with 1261 additions and 87 deletions

View File

@ -1,3 +1,100 @@
# Version 24.24 - 2024-06-11
### Added 🆕
- feat: 6942 hashtag in key : value summary by:jgallego
- feat: #6957: Rename FetchedTags instance tag by:Javier Segarra
- feat: refactor template by:Javier Segarra
- feat: refs #6600 Add option to add comment for photo motivation by:jorgep
- feat: refs #6942 test e2e tobook & toUnbook by:jorgep
- feat: refs #6942 to book summary button & reactive value by:jorgep
- feat: refs #6942 to unbook by:jorgep
- feat: refs #6942 url update by:jorgep
- feat: refs #6942 use correct currency in InvoiceIn components by:jorgep
- feat: refs #6942 vat rate total by:jorgep
- feat: refs #7494 new icons (7494-icons) by:alexm
- feat: refs #7494 new icons by:alexm
- feat: refs #7542 drop space by:jorgep
- feat: refs #7542 empty by:jorgep
- fix: refs #6942 changes and new features by:jorgep
- fix: style by:Javier Segarra
- style: color transparent when is fetive by:Javier Segarra
- style: fix color when is empty by:Javier Segarra
- style: reset poc style (6957_refactorFetechedTags) by:Javier Segarra
- style: reset poc style by:Javier Segarra
- style updates by:Javier Segarra
### Changed 📦
- feat: refactor template by:Javier Segarra
- perf: 6957 add color as new shared variable by:Javier Segarra
- perf: 6957 change fetchedTags color by:Javier Segarra
- perf: remove local tree variable by:Javier Segarra
- refactor: add flat by:alexm
- refactor: refs #6600 replace QInput to VnInput by:jorgep
- refactor: refs #6652 improved defaulter section by:Jon
- refactor: refs #6942 Fix getTotalAmount function to correctly calculate the total amount in InvoiceInDueDay.vue by:jorgep
- refactor: refs #6942 new summary layout by:jorgep
- refactor: refs #6942 store key & actions by:jorgep
- refactor: refs #6942 summary by:jorgep
- refactor: refs #6942 use router hook by:jorgep
- refactor: refs #6942 WIP summary layout by:jorgep
### Fixed 🛠️
- fix: 9-12 by:Javier Segarra
- fix: defaulter icon by:alexm
- fix: refs #5186 validation by:jorgep
- fix: refs #6095 add reFfk null on search by:pablone
- fix: refs #6942 cardDescriptor use store if its popup or different source data by:jorgep
- fix: refs #6942 changes and new features by:jorgep
- fix: refs #6942 drop comments by:jorgep
- fix: refs #6942 drop console by:jorgep
- fix: refs #6942 drop console.log by:jorgep
- fix: refs #6942 e2e test (origin/6942-warmfix-fixFormModel) by:jorgep
- fix: refs #6942 e2e tests by:jorgep
- fix: refs #6942 e2e tests by:jorgep
- fix: refs #6942 fix emit on data saved by:jorgep
- fix: refs #6942 fix emit on reset by:jorgep
- fix: refs #6942 fix vncard by:jorgep
- fix: refs #6942 formModel & CardDescriptor by:jorgep
- fix: refs #6942 formModel watch changes & invoiceInCreate by:jorgep
- fix: refs #6942 import by:jorgep
- fix: refs #6942 reloading by:jorgep
- fix: refs #6942 rollback by:jorgep
- fix: refs #6942 selectable expense by:jorgep
- fix: refs #6942 skip e2e tests by:jorgep
- fix: refs #6942 table bottom highlight & drop isBooked field by:jorgep
- fix: refs #6942 tests e2e by:jorgep
- fix: refs #6942 tests & summary table spacing by:jorgep
- fix: refs #6942 unit tests by:jorgep
- fix: refs #6942 vnLocation by:jorgep
- fix: refs #6942 wip: formModel by:jorgep
- fix: refs #7542 use right panel by:jorgep
- fix: searchbar redirect by:alexm
- fix: style by:Javier Segarra
- fix: WorkerCalendarItem by:Javier Segarra
- mini fix by:wbuezas
- refs #6111 clean code fix changes by:carlossa
- refs #6111 fix merge, fix column by:carlossa
- refs #6111 fix qtable, actions, scroll by:carlossa
- refs #6111 fix routeList by:carlossa
- refs #6111 fix sticky by:carlossa
- refs #6111 fix trad remove logs by:carlossa
- refs #6111 fix visibleColumns by:carlossa
- refs #6111 routeList fix by:carlossa
- refs #6332 fix calendar by:carlossa
- refs #6332 fix colors by:carlossa
- refs #6332 fix festive by:carlossa
- refs #6820 fix BasicData Tickets by:carlossa
- refs #6820 fix error front by:carlossa
- refs #6820 fix traduction by:carlossa
- refs #7391 fix textarea by:carlossa
- refs #7396 fix summary by:carlossa
- Search childs fix by:wbuezas
- small fix by:wbuezas
- style: fix color when is empty by:Javier Segarra
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
@ -11,6 +108,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- (Item) => Se añade la opción de añadir un comentario del motivo de hacer una foto - (Item) => Se añade la opción de añadir un comentario del motivo de hacer una foto
- (Worker) => Se añade la opción de crear un trabajador ajeno a la empresa - (Worker) => Se añade la opción de crear un trabajador ajeno a la empresa
- (Route) => Ahora se muestran todos los cmrs
## [2418.01] ## [2418.01]

4
Jenkinsfile vendored
View File

@ -94,7 +94,7 @@ pipeline {
sh 'quasar build' sh 'quasar build'
script { script {
def packageJson = readJSON file: 'package.json' def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
} }
dockerBuild() dockerBuild()
} }
@ -106,7 +106,7 @@ pipeline {
steps { steps {
script { script {
def packageJson = readJSON file: 'package.json' def packageJson = readJSON file: 'package.json'
env.VERSION = packageJson.version env.VERSION = "${packageJson.version}-build${env.BUILD_ID}"
} }
withKubeConfig([ withKubeConfig([
serverUrl: "$KUBERNETES_API", serverUrl: "$KUBERNETES_API",

34
changelog.sh Normal file
View File

@ -0,0 +1,34 @@
features_types=(chore feat style)
changes_types=(refactor perf)
fix_types=(fix revert)
file="CHANGELOG.md"
file_tmp="temp_log.txt"
file_current_tmp="temp_current_log.txt"
setType(){
echo "### $1" >> $file_tmp
arr=("$@")
echo "" > $file_current_tmp
for i in "${arr[@]}"
do
git log --grep="$i" --oneline --no-merges --format="- %s %d by:%an" master..test >> $file_current_tmp
done
# remove duplicates
sort -o $file_current_tmp -u $file_current_tmp
cat $file_current_tmp >> $file_tmp
echo "" >> $file_tmp
# remove tmp current file
[ -e $file_current_tmp ] && rm $file_current_tmp
}
echo "# Version XX.XX - XXXX-XX-XX" >> $file_tmp
echo "" >> $file_tmp
setType "Added 🆕" "${features_types[@]}"
setType "Changed 📦" "${changes_types[@]}"
setType "Fixed 🛠️" "${fix_types[@]}"
cat $file >> $file_tmp
mv $file_tmp $file

View File

@ -12,6 +12,7 @@ import SkeletonForm from 'components/ui/SkeletonForm.vue';
import VnConfirm from './ui/VnConfirm.vue'; import VnConfirm from './ui/VnConfirm.vue';
import { tMobile } from 'src/composables/tMobile'; import { tMobile } from 'src/composables/tMobile';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useRoute } from 'vue-router';
const { push } = useRouter(); const { push } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
@ -20,6 +21,7 @@ const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator(); const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const route = useRoute();
const $props = defineProps({ const $props = defineProps({
url: { url: {
@ -28,7 +30,7 @@ const $props = defineProps({
}, },
model: { model: {
type: String, type: String,
default: '', default: null,
}, },
filter: { filter: {
type: Object, type: Object,
@ -82,17 +84,18 @@ const $props = defineProps({
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`
);
const componentIsRendered = ref(false); const componentIsRendered = ref(false);
const arrayData = useArrayData($props.model); const arrayData = useArrayData(modelValue);
const isLoading = ref(false); const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false); const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges); const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({}); const originalData = ref({});
const formData = computed(() => state.get($props.model)); const formData = computed(() => state.get(modelValue));
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({ const defaultButtons = computed(() => ({
save: { save: {
@ -114,7 +117,7 @@ onMounted(async () => {
nextTick(() => (componentIsRendered.value = true)); nextTick(() => (componentIsRendered.value = true));
// Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
state.set($props.model, $props.formInitialData); state.set(modelValue, $props.formInitialData);
if ($props.autoLoad && !$props.formInitialData && $props.url) await fetch(); if ($props.autoLoad && !$props.formInitialData && $props.url) await fetch();
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data); else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
@ -161,8 +164,8 @@ onBeforeRouteLeave((to, from, next) => {
onUnmounted(() => { onUnmounted(() => {
// Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas. // Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
if (hasChanges.value) return state.set($props.model, originalData.value); if (hasChanges.value) return state.set(modelValue, originalData.value);
if ($props.clearStoreOnUnmount) state.unset($props.model); if ($props.clearStoreOnUnmount) state.unset(modelValue);
}); });
async function fetch() { async function fetch() {
@ -174,7 +177,7 @@ async function fetch() {
updateAndEmit('onFetch', data); updateAndEmit('onFetch', data);
} catch (e) { } catch (e) {
state.set($props.model, {}); state.set(modelValue, {});
originalData.value = {}; originalData.value = {};
} }
} }
@ -235,11 +238,11 @@ function filter(value, update, filterOptions) {
} }
function updateAndEmit(evt, val, res) { function updateAndEmit(evt, val, res) {
state.set($props.model, val); state.set(modelValue, val);
originalData.value = val && JSON.parse(JSON.stringify(val)); originalData.value = val && JSON.parse(JSON.stringify(val));
if (!$props.url) arrayData.store.data = val; if (!$props.url) arrayData.store.data = val;
emit(evt, state.get($props.model), res); emit(evt, state.get(modelValue), res);
} }
defineExpose({ save, isLoading, hasChanges }); defineExpose({ save, isLoading, hasChanges });

View File

@ -13,6 +13,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
info: {
type: String,
default: '',
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -83,6 +87,11 @@ const inputRules = [
v-if="hover && value && !$attrs.disabled" v-if="hover && value && !$attrs.disabled"
@click="value = null" @click="value = null"
></QIcon> ></QIcon>
<QIcon v-if="info" name="info">
<QTooltip max-width="350px">
{{ info }}
</QTooltip>
</QIcon>
</template> </template>
</QInput> </QInput>
</div> </div>

View File

@ -28,7 +28,7 @@ const toggleCardCheck = (item) => {
<div class="title text-primary text-weight-bold text-h5"> <div class="title text-primary text-weight-bold text-h5">
{{ $props.title }} {{ $props.title }}
</div> </div>
<QChip class="q-chip-color" outline size="sm"> <QChip v-if="$props.id" class="q-chip-color" outline size="sm">
{{ t('ID') }}: {{ $props.id }} {{ t('ID') }}: {{ $props.id }}
</QChip> </QChip>
</div> </div>

33
src/composables/useAcl.js Normal file
View File

@ -0,0 +1,33 @@
import axios from 'axios';
import { useState } from './useState';
export function useAcl() {
const state = useState();
async function fetch() {
const { data } = await axios.get('VnUsers/acls');
const acls = {};
data.forEach((acl) => {
acls[acl.model] = acls[acl.model] || {};
acls[acl.model][acl.property] = acls[acl.model][acl.property] || {};
acls[acl.model][acl.property][acl.accessType] = true;
});
state.setAcls(acls);
}
function hasAny(model, prop, accessType) {
const acls = state.getAcls().value[model];
if (acls)
return ['*', prop].some((key) => {
const acl = acls[key];
return acl && (acl['*'] || acl[accessType]);
});
}
return {
fetch,
hasAny,
state,
};
}

View File

@ -1,5 +1,6 @@
import { useState } from './useState'; import { useState } from './useState';
import { useRole } from './useRole'; import { useRole } from './useRole';
import { useAcl } from './useAcl';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios'; import axios from 'axios';
import useNotify from './useNotify'; import useNotify from './useNotify';
@ -88,6 +89,7 @@ export function useSession() {
setSession(data); setSession(data);
await useRole().fetch(); await useRole().fetch();
await useAcl().fetch();
await useUserConfig().fetch(); await useUserConfig().fetch();
await useTokenConfig().fetch(); await useTokenConfig().fetch();

View File

@ -15,6 +15,7 @@ if (sessionStorage.getItem('user'))
user.value = JSON.parse(sessionStorage.getItem('user')); user.value = JSON.parse(sessionStorage.getItem('user'));
const roles = ref([]); const roles = ref([]);
const acls = ref([]);
const tokenConfig = ref({}); const tokenConfig = ref({});
const drawer = ref(true); const drawer = ref(true);
const headerMounted = ref(false); const headerMounted = ref(false);
@ -42,6 +43,14 @@ export function useState() {
function setRoles(data) { function setRoles(data) {
roles.value = data; roles.value = data;
} }
function getAcls() {
return computed(() => acls.value);
}
function setAcls(data) {
acls.value = data;
}
function getTokenConfig() { function getTokenConfig() {
return computed(() => { return computed(() => {
return tokenConfig.value; return tokenConfig.value;
@ -69,6 +78,8 @@ export function useState() {
setUser, setUser,
getRoles, getRoles,
setRoles, setRoles,
getAcls,
setAcls,
getTokenConfig, getTokenConfig,
setTokenConfig, setTokenConfig,
set, set,

View File

@ -396,6 +396,7 @@ entry:
type: Type type: Type
color: Color color: Color
id: ID id: ID
printedStickers: Printed stickers
notes: notes:
observationType: Observation type observationType: Observation type
descriptor: descriptor:
@ -472,6 +473,7 @@ ticket:
agency: Agency agency: Agency
zone: Zone zone: Zone
warehouse: Warehouse warehouse: Warehouse
collection: Collection
route: Route route: Route
invoice: Invoice invoice: Invoice
shipped: Shipped shipped: Shipped
@ -581,6 +583,9 @@ claim:
created: Created created: Created
state: State state: State
pickup: Pick up pickup: Pick up
null: No
agency: Agency
delivery: Delivery
photo: photo:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}' fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file' noData: 'There are no images/videos, click here or drag and drop the file'
@ -962,7 +967,7 @@ roadmap:
route: route:
pageTitles: pageTitles:
routes: Routes routes: Routes
cmrsList: External CMRs list cmrsList: CMRs list
RouteList: List RouteList: List
routeCreate: New route routeCreate: New route
basicData: Basic Data basicData: Basic Data
@ -1177,6 +1182,7 @@ item:
available: Available available: Available
warehouseText: 'Calculated on the warehouse of { warehouseName }' warehouseText: 'Calculated on the warehouse of { warehouseName }'
itemDiary: Item diary itemDiary: Item diary
producer: Producer
list: list:
id: Identifier id: Identifier
grouping: Grouping grouping: Grouping

View File

@ -394,6 +394,7 @@ entry:
type: Tipo type: Tipo
color: Color color: Color
id: ID id: ID
printedStickers: Etiquetas impresas
notes: notes:
observationType: Tipo de observación observationType: Tipo de observación
descriptor: descriptor:
@ -470,6 +471,7 @@ ticket:
agency: Agencia agency: Agencia
zone: Zona zone: Zona
warehouse: Almacén warehouse: Almacén
collection: Colección
route: Ruta route: Ruta
invoice: Factura invoice: Factura
shipped: Enviado shipped: Enviado
@ -950,7 +952,7 @@ roadmap:
route: route:
pageTitles: pageTitles:
routes: Rutas routes: Rutas
cmrsList: Listado de CMRs externos cmrsList: Listado de CMRs
RouteList: Listado RouteList: Listado
routeCreate: Nueva ruta routeCreate: Nueva ruta
basicData: Datos básicos basicData: Datos básicos
@ -1166,6 +1168,7 @@ item:
available: Disponible available: Disponible
warehouseText: 'Calculado sobre el almacén de { warehouseName }' warehouseText: 'Calculado sobre el almacén de { warehouseName }'
itemDiary: Registro de compra-venta itemDiary: Registro de compra-venta
producer: Productor
list: list:
id: Identificador id: Identificador
grouping: Grouping grouping: Grouping

View File

@ -0,0 +1,104 @@
<script setup>
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const { t } = useI18n();
const { notify } = useNotify();
const onSynchronizeAll = async () => {
try {
notify(t('Synchronizing in the background'), 'positive');
await axios.patch(`Accounts/syncAll`);
} catch (error) {
console.error('Error synchronizing all accounts', error);
}
};
const onSynchronizeRoles = async () => {
try {
await axios.patch(`RoleInherits/sync`);
notify(t('Roles synchronized!'), 'positive');
} catch (error) {
console.error('Error synchronizing roles', error);
}
};
</script>
<template>
<QPage>
<VnSubToolbar />
<FormModel
:url="`AccountConfigs/${1}`"
:url-update="`AccountConfigs/${1}`"
model="AccountAccounts"
auto-load
>
<template #moreActions>
<QBtn
class="q-ml-none"
color="primary"
:label="t('accounts.syncAll')"
@click="onSynchronizeAll()"
/>
<QBtn
color="primary"
:label="t('accounts.syncRoles')"
@click="onSynchronizeRoles()"
/>
</template>
<template #form="{ data }">
<div class="q-gutter-y-sm">
<VnInput :label="t('accounts.homedir')" v-model="data.homedir" />
<VnInput :label="t('accounts.shell')" v-model="data.shell" />
<VnInput
:label="t('accounts.idBase')"
v-model="data.idBase"
type="number"
min="0"
/>
<VnRow>
<VnInput
:label="t('accounts.min')"
v-model="data.min"
type="number"
min="0"
/>
<VnInput
:label="t('accounts.max')"
v-model="data.max"
type="number"
min="0"
/>
</VnRow>
<VnRow>
<VnInput
:label="t('accounts.warn')"
v-model="data.warn"
type="number"
min="0"
/>
<VnInput
:label="t('accounts.inact')"
v-model="data.inact"
type="number"
min="0"
/>
</VnRow>
</div>
</template>
</FormModel>
</QPage>
</template>
<i18n>
es:
Roles synchronized!: ¡Roles sincronizados!
Synchronizing in the background: Sincronizando en segundo plano
</i18n>

View File

@ -8,12 +8,12 @@ import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CardList from 'src/components/ui/CardList.vue'; import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import AclFilter from './Acls/AclFilter.vue'; import AclFilter from './Acls/AclFilter.vue';
import AclFormView from './Acls/AclFormView.vue';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import AclFormView from './Acls/AclFormView.vue';
defineProps({ defineProps({
id: { id: {

View File

@ -0,0 +1,112 @@
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import { toDateTimeFormat } from 'src/filters/date.js';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { useVnConfirm } from 'composables/useVnConfirm';
const { t } = useI18n();
const router = useRouter();
const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm();
const paginateRef = ref(null);
const filter = {
fields: ['id', 'created', 'userId'],
include: {
relation: 'user',
scope: {
fields: ['username'],
},
},
order: 'created DESC',
};
const urlPath = 'AccessTokens';
const refresh = () => paginateRef.value.fetch();
const navigate = (id) => router.push({ name: 'AccountSummary', params: { id } });
const killSession = async (id) => {
try {
await axios.delete(`${urlPath}/${id}`);
paginateRef.value.fetch();
notify(t('Session killed'), 'positive');
} catch (error) {
console.error('Error killing session', error);
}
};
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
:data-key="urlPath"
ref="paginateRef"
:filter="filter"
:url="urlPath"
order="created DESC"
auto-load
>
<template #body="{ rows }">
<CardList
:key="row.id"
:title="row.user?.username"
@click="navigate(row.userId)"
v-for="row of rows"
>
<template #list-items>
<div style="flex-direction: column; width: 100%">
<VnLv
:label="t('connections.username')"
:value="row.user?.username"
>
</VnLv>
<VnLv
:label="t('connections.created')"
:value="toDateTimeFormat(row.created)"
>
</VnLv>
</div>
</template>
<template #actions>
<QBtn
class="q-mt-xs"
:label="t('connections.killSession')"
@click.stop="
openConfirmationModal(
t('Session will be killed'),
t('Are you sure you want to continue?'),
() => killSession(row.id)
)
"
outline
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="refresh" color="primary" @click="refresh()">
<QTooltip>{{ t('connections.refresh') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
Session killed: Sesión matada
Session will be killed: Se va a matar la sesión
Are you sure you want to continue?: ¿Seguro que quieres continuar?
</i18n>

View File

@ -0,0 +1,171 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const { t } = useI18n();
const { notify } = useNotify();
const arrayData = useArrayData('AccountLdap');
const URL_UPDATE = `LdapConfigs/${1}`;
const URL_CREATE = `LdapConfigs`;
const DEFAULT_DATA = {
id: 1,
hasData: false,
groupDn: null,
password: null,
rdn: null,
server: null,
userDn: null,
};
const initialData = ref({
...DEFAULT_DATA,
});
const hasData = computed({
get: () => initialData.value.hasData,
set: (val) => {
initialData.value.hasData = val;
if (!val) formCustomFn.value = deleteMailForward;
else formCustomFn.value = null;
},
});
const initialDataLoaded = ref(false);
const formUrlCreate = ref(null);
const formUrlUpdate = ref(null);
const formCustomFn = ref(null);
const onTestConection = async () => {
try {
await axios.get(`LdapConfigs/test`);
notify(t('LDAP connection established!'), 'positive');
} catch (error) {
console.error('Error testing connection', error);
}
};
const getInitialLdapConfig = async () => {
try {
initialDataLoaded.value = false;
const { data } = await axios.get(URL_UPDATE);
initialData.value = data;
hasData.value = true;
return data;
} catch (error) {
hasData.value = false;
arrayData.destroy();
console.error('Error fetching initial LDAP config', error);
return null;
} finally {
// Si asignamos un valor a urlUpdate, debemos asignar null a urlCreate y viceversa, ya puede causar problemas en formModel
if (hasData.value) {
formUrlUpdate.value = URL_UPDATE;
formUrlCreate.value = null;
} else {
formUrlUpdate.value = null;
formUrlCreate.value = URL_CREATE;
}
initialDataLoaded.value = true;
}
};
const deleteMailForward = async () => {
try {
await axios.delete(URL_UPDATE);
initialData.value = { ...DEFAULT_DATA };
hasData.value = false;
notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
};
onMounted(async () => await getInitialLdapConfig());
</script>
<template>
<QPage>
<VnSubToolbar />
<FormModel
:key="initialDataLoaded"
model="AccountLdap"
:form-initial-data="initialData"
:url-create="formUrlCreate"
:url-update="formUrlUpdate"
:save-fn="formCustomFn"
auto-load
@on-data-saved="getInitialLdapConfig()"
>
<template #moreActions>
<QBtn
class="q-ml-none"
color="primary"
:label="t('ldap.testConnection')"
@click="onTestConection()"
>
<QTooltip>
{{ t('ldap.testConnection') }}
</QTooltip>
</QBtn>
</template>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md">
<div class="col">
<QCheckbox
:label="t('ldap.enableSync')"
v-model="data.hasData"
@update:model-value="($event) => (hasData = $event)"
:toggle-indeterminate="false"
/>
</div>
</VnRow>
<template v-if="hasData">
<VnInput
:label="t('ldap.server')"
clearable
v-model="data.server"
:required="true"
:rules="validate('LdapConfig.server')"
/>
<VnInput
:label="t('ldap.rdn')"
clearable
v-model="data.rdn"
:required="true"
:rules="validate('LdapConfig.rdn')"
/>
<VnInput
:label="t('ldap.password')"
clearable
type="password"
v-model="data.password"
:required="true"
:rules="validate('LdapConfig.password')"
/>
<VnInput :label="t('ldap.userDN')" clearable v-model="data.userDn" />
<VnInput
:label="t('ldap.groupDN')"
clearable
v-model="data.groupDn"
/>
</template>
</template>
</FormModel>
</QPage>
</template>
<i18n>
es:
LDAP connection established!: ¡Conexión con LDAP establecida!
</i18n>

View File

@ -0,0 +1,187 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
const { t } = useI18n();
const { notify } = useNotify();
const arrayData = useArrayData('AccountSamba');
const formModel = ref(null);
const URL_UPDATE = `SambaConfigs/${1}`;
const URL_CREATE = `SambaConfigs`;
const DEFAULT_DATA = {
id: 1,
hasData: false,
adDomain: null,
adController: null,
adUser: null,
adPassword: null,
userDn: null,
verifyCert: false,
};
const initialData = ref({
...DEFAULT_DATA,
});
const hasData = computed({
get: () => initialData.value.hasData,
set: (val) => {
initialData.value.hasData = val;
if (!val) formCustomFn.value = deleteMailForward;
else formCustomFn.value = null;
},
});
const initialDataLoaded = ref(false);
const formUrlCreate = ref(null);
const formUrlUpdate = ref(null);
const formCustomFn = ref(null);
const onTestConection = async () => {
try {
await axios.get(`SambaConfigs/test`);
notify(t('Samba connection established!'), 'positive');
} catch (error) {
console.error('Error testing connection', error);
}
};
const getInitialSambaConfig = async () => {
try {
initialDataLoaded.value = false;
const { data } = await axios.get(URL_UPDATE);
initialData.value = data;
hasData.value = true;
return data;
} catch (error) {
hasData.value = false;
arrayData.destroy();
console.error('Error fetching initial Samba config', error);
return null;
} finally {
if (hasData.value) {
formUrlUpdate.value = URL_UPDATE;
formUrlCreate.value = null;
} else {
formUrlUpdate.value = null;
formUrlCreate.value = URL_CREATE;
}
initialDataLoaded.value = true;
}
};
const deleteMailForward = async () => {
try {
await axios.delete(URL_UPDATE);
initialData.value = { ...DEFAULT_DATA };
hasData.value = false;
notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
};
onMounted(async () => await getInitialSambaConfig());
</script>
<template>
<QPage>
<VnSubToolbar />
<FormModel
ref="formModel"
:key="initialDataLoaded"
model="AccountSamba"
:form-initial-data="initialData"
:url-create="formUrlCreate"
:url-update="formUrlUpdate"
:save-fn="formCustomFn"
auto-load
@on-data-saved="getInitialSambaConfig()"
>
<template #moreActions>
<QBtn
class="q-ml-none"
color="primary"
:label="t('samba.testConnection')"
:disable="formModel.hasChanges"
@click="onTestConection()"
>
<QTooltip>
{{ t('samba.testConnection') }}
</QTooltip>
</QBtn>
</template>
<template #form="{ data, validate }">
<VnRow class="row q-gutter-md">
<div class="col">
<QCheckbox
:label="t('samba.enableSync')"
v-model="data.hasData"
@update:model-value="($event) => (hasData = $event)"
:toggle-indeterminate="false"
/>
</div>
</VnRow>
<template v-if="hasData">
<VnInput
:label="t('samba.domainAD')"
clearable
v-model="data.adDomain"
:required="true"
:rules="validate('SambaConfigs.server')"
/>
<VnInput
:label="t('samba.domainController')"
clearable
v-model="data.adController"
:required="true"
:rules="validate('SambaConfigs.adController')"
/>
<VnInput
:label="t('samba.userAD')"
clearable
v-model="data.adUser"
:rules="validate('SambaConfigs.adUser')"
/>
<VnInput
:label="t('samba.passwordAD')"
clearable
type="password"
v-model="data.adPassword"
/>
<VnInput
:label="t('samba.domainPart')"
clearable
v-model="data.userDn"
:required="true"
:rules="validate('SambaConfigs.userDn')"
/>
<QCheckbox
:label="t('samba.verifyCertificate')"
v-model="data.verifyCert"
:rules="validate('SambaConfigs.groupDn')"
:toggle-indeterminate="false"
/>
</template>
</template>
</FormModel>
</QPage>
</template>
<i18n>
es:
Samba connection established!: ¡Conexión con LDAP establecida!
</i18n>

View File

@ -1,11 +1,9 @@
<script setup> <script setup>
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
</script> </script>

View File

@ -67,6 +67,7 @@ ldap:
groupDN: Group DN groupDN: Group DN
testConnection: Test connection testConnection: Test connection
success: LDAP connection established! success: LDAP connection established!
password: Password
samba: samba:
enableSync: Enable synchronization enableSync: Enable synchronization
domainController: Domain controller domainController: Domain controller
@ -78,6 +79,21 @@ samba:
verifyCertificate: Verify certificate verifyCertificate: Verify certificate
testConnection: Test connection testConnection: Test connection
success: Samba connection established! success: Samba connection established!
accounts:
homedir: Homedir base
shell: Shell
idBase: User and role base id
min: Min
max: Max
warn: Warn
inact: Inact
syncAll: Synchronize all
syncRoles: Synchronize roles
connections:
refresh: Refresh
username: Username
created: Created
killSession: Kill session
acls: acls:
role: Role role: Role
accessType: Access type accessType: Access type

View File

@ -70,6 +70,7 @@ mailAlias:
name: Nombre name: Nombre
isPublic: Público isPublic: Público
ldap: ldap:
password: Contraseña
enableSync: Habilitar sincronización enableSync: Habilitar sincronización
server: Servidor server: Servidor
rdn: RDN rdn: RDN
@ -86,9 +87,24 @@ samba:
userAD: Usuario AD userAD: Usuario AD
passwordAD: Contraseña AD passwordAD: Contraseña AD
domainPart: DN usuarios (sin la parte del dominio) domainPart: DN usuarios (sin la parte del dominio)
Verify certificate: Verificar certificado verifyCertificate: Verificar certificado
testConnection: Probar conexión testConnection: Probar conexión
success: ¡Conexión con Samba establecida! success: ¡Conexión con Samba establecida!
accounts:
homedir: Directorio base para carpetas de usuario
shell: Intérprete de línea de comandos
idBase: Id base usuarios y roles
min: Min
max: Max
warn: Warn
inact: Inact
syncAll: Sincronizar todo
syncRoles: Sincronizar roles
connections:
refresh: Actualizar
username: Nombre de usuario
created: Creado
killSession: Matar sesión
acls: acls:
role: Rol role: Rol
accessType: Tipo de acceso accessType: Tipo de acceso

View File

@ -234,7 +234,7 @@ const showBalancePdf = (balance) => {
<template #body-cell-employee="{ row }"> <template #body-cell-employee="{ row }">
<QTd auto-width @click.stop> <QTd auto-width @click.stop>
<QBtn color="blue" flat no-caps>{{ row.userName }}</QBtn> <QBtn color="blue" flat no-caps>{{ row.userName }}</QBtn>
<WorkerDescriptorProxy :id="row.clientFk" /> <WorkerDescriptorProxy :id="row.workerFk" />
</QTd> </QTd>
</template> </template>

View File

@ -82,6 +82,16 @@ const tableColumnComponents = computed(() => ({
}, },
event: getInputEvents, event: getInputEvents,
}, },
printedStickers: {
component: VnInput,
props: {
type: 'number',
min: 0,
class: 'input-number',
dense: true,
},
event: getInputEvents,
},
weight: { weight: {
component: VnInput, component: VnInput,
props: { props: {
@ -147,7 +157,7 @@ const entriesTableColumns = computed(() => {
return [ return [
{ {
label: t('entry.summary.item'), label: t('entry.summary.item'),
field: 'id', field: 'itemFk',
name: 'item', name: 'item',
align: 'left', align: 'left',
}, },
@ -169,6 +179,12 @@ const entriesTableColumns = computed(() => {
name: 'stickers', name: 'stickers',
align: 'left', align: 'left',
}, },
{
label: t('entry.buys.printedStickers'),
field: 'printedStickers',
name: 'printedStickers',
align: 'left',
},
{ {
label: t('entry.summary.weight'), label: t('entry.summary.weight'),
field: 'weight', field: 'weight',
@ -216,7 +232,6 @@ const entriesTableColumns = computed(() => {
}); });
const copyOriginalRowsData = (rows) => { const copyOriginalRowsData = (rows) => {
// el objetivo de esto es guardar los valores iniciales de todas las rows para evitar guardar cambios si la data no cambió al disparar los eventos
originalRowDataCopy.value = JSON.parse(JSON.stringify(rows)); originalRowDataCopy.value = JSON.parse(JSON.stringify(rows));
}; };
@ -386,19 +401,16 @@ const lockIconType = (groupingMode, mode) => {
</template> </template>
<ItemDescriptorProxy <ItemDescriptorProxy
v-if="col.name === 'item'" v-if="col.name === 'item'"
:id="props.row.id" :id="props.row.item.id"
/> />
</component> </component>
</QTd> </QTd>
</QTr> </QTr>
<QTr no-hover> <QTr no-hover class="full-width infoRow" style="column-span: all">
<QTd /> <QTd />
<QTd> <QTd cols>
<span>{{ props.row.item.itemType.code }}</span> <span>{{ props.row.item.itemType.code }}</span>
</QTd> </QTd>
<QTd>
<span>{{ props.row.item.id }}</span>
</QTd>
<QTd> <QTd>
<span>{{ props.row.item.size }}</span> <span>{{ props.row.item.size }}</span>
</QTd> </QTd>
@ -413,10 +425,6 @@ const lockIconType = (groupingMode, mode) => {
<FetchedTags :item="props.row.item" :max-length="5" /> <FetchedTags :item="props.row.item" :max-length="5" />
</QTd> </QTd>
</QTr> </QTr>
<!-- Esta última row es utilizada para agregar un espaciado y así marcar una diferencia visual entre los diferentes buys -->
<QTr v-if="props.rowIndex !== rows.length - 1" class="separation-row">
<QTd colspan="12" class="vn-table-separation-row" />
</QTr>
</template> </template>
<template #item="props"> <template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
@ -466,11 +474,13 @@ const lockIconType = (groupingMode, mode) => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.separation-row { .q-table--horizontal-separator tbody tr:nth-child(odd) > td {
background-color: var(--vn-section-color) !important; border-bottom-width: 0px;
border-top-width: 2px;
border-color: var(--vn-text-color);
} }
.grid-style-transition { .infoRow > td {
transition: transform 0.28s, background-color 0.28s; color: var(--vn-label-color);
} }
</style> </style>

View File

@ -16,6 +16,7 @@ import useCardDescription from 'src/composables/useCardDescription';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
import { dashIfEmpty } from 'src/filters';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -182,6 +183,10 @@ const openCloneDialog = async () => {
</span> </span>
</template> </template>
</VnLv> </VnLv>
<VnLv
:label="t('item.descriptor.producer')"
:value="dashIfEmpty(entity.subName)"
/>
<VnLv <VnLv
v-if="entity.value5" v-if="entity.value5"
:label="t('item.descriptor.color')" :label="t('item.descriptor.color')"

View File

@ -1,11 +1,11 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -16,10 +16,11 @@ const props = defineProps({
}); });
const countries = ref(); const countries = ref();
const warehouses = ref();
</script> </script>
<template> <template>
<FetchData url="Countries" @on-fetch="(data) => (countries = data)" auto-load /> <FetchData url="Countries" @on-fetch="(data) => (countries = data)" auto-load />
<FetchData url="Warehouses" @on-fetch="(data) => (warehouses = 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">
@ -93,13 +94,13 @@ const countries = ref();
<QItemSection v-if="!countries"> <QItemSection v-if="!countries">
<QSkeleton type="QInput" class="full-width" /> <QSkeleton type="QInput" class="full-width" />
</QItemSection> </QItemSection>
<QItemSection v-if="countries" class="q-mb-sm"> <QItemSection v-if="countries">
<QSelect <VnSelect
:label="t('route.cmr.list.country')" :label="t('route.cmr.list.country')"
v-model="params.country" v-model="params.country"
:options="countries" :options="countries"
option-value="country" option-label="name"
option-label="country" option-value="id"
transition-show="jump-down" transition-show="jump-down"
transition-hide="jump-up" transition-hide="jump-up"
emit-value emit-value
@ -111,9 +112,23 @@ const countries = ref();
<template #prepend> <template #prepend>
<QIcon name="flag" size="sm"></QIcon> <QIcon name="flag" size="sm"></QIcon>
</template> </template>
</QSelect> </VnSelect>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<VnSelect
:label="t('globals.warehouse')"
:options="warehouses"
hide-selected
option-label="name"
option-value="id"
v-model="params.warehouseFk"
rounded
dense
outlined
>
</VnSelect>
</QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
@ -126,7 +141,6 @@ const countries = ref();
</template> </template>
</VnFilterPanel> </VnFilterPanel>
</template> </template>
<i18n> <i18n>
en: en:
params: params:

View File

@ -14,6 +14,7 @@ const { t } = useI18n();
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); const token = getTokenMultimedia();
const selected = ref([]); const selected = ref([]);
const warehouses = ref([]);
const columns = computed(() => [ const columns = computed(() => [
{ {
@ -63,6 +64,13 @@ const columns = computed(() => [
sortable: true, sortable: true,
headerStyle: 'padding-left: 33px', headerStyle: 'padding-left: 33px',
}, },
{
name: 'warehouseFk',
label: t('globals.warehouse'),
field: ({ warehouseFk }) => warehouseFk,
align: 'center',
sortable: true,
},
{ {
name: 'icons', name: 'icons',
align: 'center', align: 'center',
@ -99,7 +107,7 @@ function downloadPdfs() {
<div class="list"> <div class="list">
<VnPaginate <VnPaginate
data-key="CmrList" data-key="CmrList"
:url="`Routes/getExternalCmrs`" :url="`Routes/cmrs`"
order="cmrFk DESC" order="cmrFk DESC"
limit="null" limit="null"
auto-load auto-load
@ -147,6 +155,11 @@ function downloadPdfs() {
<CustomerDescriptorProxy :id="value" /> <CustomerDescriptorProxy :id="value" />
</QTd> </QTd>
</template> </template>
<template #body-cell-warehouseFk="{ value }">
<QTd align="center">
{{ warehouses.find(({ id }) => id === value)?.name }}
</QTd>
</template>
<template #body-cell-icons="{ value }"> <template #body-cell-icons="{ value }">
<QTd align="center"> <QTd align="center">
<a :href="getCmrUrl(value)" target="_blank"> <a :href="getCmrUrl(value)" target="_blank">

View File

@ -35,6 +35,7 @@ const ticket = ref();
const salesLines = ref(null); const salesLines = ref(null);
const editableStates = ref([]); const editableStates = ref([]);
const ticketUrl = ref(); const ticketUrl = ref();
const grafanaUrl = 'https://grafana.verdnatura.es';
onMounted(async () => { onMounted(async () => {
ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/'; ticketUrl.value = (await getUrl('ticket/')) + entityId.value + '/';
@ -159,6 +160,20 @@ async function changeState(value) {
:label="t('ticket.summary.warehouse')" :label="t('ticket.summary.warehouse')"
:value="ticket.warehouse?.name" :value="ticket.warehouse?.name"
/> />
<VnLv
:label="t('ticket.summary.collection')"
:value="ticket.ticketCollections[0]?.collectionFk"
>
<template #value>
<a
:href="`${grafanaUrl}/d/d552ab74-85b4-4e7f-a279-fab7cd9c6124/control-de-expediciones?orgId=1&var-collectionFk=${ticket.ticketCollections[0]?.collectionFk}`"
target="_blank"
class="grafana"
>
{{ ticket.ticketCollections[0]?.collectionFk }}
</a>
</template>
</VnLv>
<VnLv :label="t('ticket.summary.route')" :value="ticket.routeFk" /> <VnLv :label="t('ticket.summary.route')" :value="ticket.routeFk" />
<VnLv :label="t('ticket.summary.invoice')"> <VnLv :label="t('ticket.summary.invoice')">
<template #value> <template #value>
@ -487,4 +502,7 @@ async function changeState(value) {
.fetched-tags { .fetched-tags {
max-width: 70%; max-width: 70%;
} }
.grafana {
color: $primary-light;
}
</style> </style>

View File

@ -0,0 +1,99 @@
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import VnRow from 'components/ui/VnRow.vue';
import FormPopup from 'src/components/FormPopup.vue';
import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({
id: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['onSubmit']);
const { t } = useI18n();
const { notify } = useNotify();
const formData = reactive({
newPassword: null,
repeatPassword: null,
});
const passRequirements = ref([]);
const setPassword = async () => {
try {
if (!formData.newPassword) {
notify(t('You must enter a new password'), 'negative');
return;
}
if (formData.newPassword != formData.repeatPassword) {
notify(t(`Passwords don't match`), 'negative');
return;
}
await axios.patch(`Workers/${$props.id}/setPassword`, {
newPass: formData.newPassword,
});
notify(t('Password changed!'), 'positive');
emit('onSubmit');
} catch (err) {
console.error('Error setting password', err);
}
};
const getPassRequirements = async () => {
const { data } = await axios.get('UserPasswords/findOne');
passRequirements.value = data;
};
onMounted(async () => await getPassRequirements());
</script>
<template>
<FormPopup :title="t('Reset password')" @on-submit="setPassword()">
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
:label="t('New password')"
v-model="formData.newPassword"
type="password"
:required="true"
:info="
t('passwordRequirements', {
length: passRequirements.length,
nAlpha: passRequirements.nAlpha,
nUpper: passRequirements.nUpper,
nDigits: passRequirements.nDigits,
nPunct: passRequirements.nPunct,
})
"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnInput
:label="t('Repeat password')"
v-model="formData.repeatPassword"
type="password"
/>
</VnRow>
</template>
</FormPopup>
</template>
<i18n>
es:
Reset password: Restablecer contraseña
New password: Nueva contraseña
Repeat password: Repetir contraseña
You must enter a new password: Debes introducir la nueva contraseña
Passwords don't match: Las contraseñas no coinciden
</i18n>

View File

@ -6,8 +6,10 @@ import { useSession } from 'src/composables/useSession';
import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import WorkerChangePasswordForm from 'src/pages/Worker/Card/WorkerChangePasswordForm.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import axios from 'axios';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -25,12 +27,16 @@ const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
const state = useState(); const state = useState();
const user = state.getUser();
const changePasswordFormDialog = ref(null);
const cardDescriptorRef = ref(null);
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const worker = ref(); const worker = ref();
const workerExcluded = ref(false);
const filter = { const filter = {
include: [ include: [
{ {
@ -71,14 +77,44 @@ function getWorkerAvatar() {
const token = getTokenMultimedia(); const token = getTokenMultimedia();
return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`; return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`;
} }
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
if (!entity) return; if (!entity) return;
data.value = useCardDescription(entity.user.nickname, entity.id); data.value = useCardDescription(entity.user.nickname, entity.id);
}; };
const openChangePasswordForm = () => changePasswordFormDialog.value.show();
const getIsExcluded = async () => {
try {
const { data } = await axios.get(
`WorkerDisableExcludeds/${entityId.value}/exists`
);
if (!data) return;
workerExcluded.value = data.exists;
} catch (err) {
console.error('Error getting worker excluded: ', err);
}
};
const handleExcluded = async () => {
if (workerExcluded.value)
await axios.delete(`WorkerDisableExcludeds/${entityId.value}`);
else
await axios.post(`WorkerDisableExcludeds`, {
workerFk: entityId.value,
dated: new Date(),
});
workerExcluded.value = !workerExcluded.value;
};
const refetch = async () => await cardDescriptorRef.value.getData();
</script> </script>
<template> <template>
<CardDescriptor <CardDescriptor
ref="cardDescriptorRef"
module="Worker" module="Worker"
data-key="workerData" data-key="workerData"
:url="`Workers/${entityId}`" :url="`Workers/${entityId}`"
@ -90,9 +126,34 @@ const setData = (entity) => {
(data) => { (data) => {
worker = data; worker = data;
setData(data); setData(data);
getIsExcluded();
} }
" "
> >
<template #menu="{}">
<QItem v-ripple clickable @click="handleExcluded()">
<QItemSection>
{{
workerExcluded
? t('Click to allow the user to be disabled')
: t('Click to exclude the user from getting disabled')
}}
</QItemSection>
</QItem>
<QItem
v-if="!worker.user.emailVerified && user.id != worker.id"
v-ripple
clickable
@click="openChangePasswordForm()"
>
<QItemSection>
{{ t('Change password') }}
<QDialog ref="changePasswordFormDialog">
<WorkerChangePasswordForm @on-submit="refetch()" :id="entityId" />
</QDialog>
</QItemSection>
</QItem>
</template>
<template #before> <template #before>
<QImg :src="getWorkerAvatar()" class="photo"> <QImg :src="getWorkerAvatar()" class="photo">
<template #error> <template #error>
@ -139,3 +200,10 @@ const setData = (entity) => {
height: 256px; height: 256px;
} }
</style> </style>
<i18n>
es:
Click to allow the user to be disabled: Marcar para deshabilitar
Click to exclude the user from getting disabled: Marcar para no deshabilitar
Change password: Cambiar contraseña
</i18n>

View File

@ -460,7 +460,7 @@ onMounted(async () => {
style="margin-left: 1px" style="margin-left: 1px"
/> />
</QBtnGroup> </QBtnGroup>
<QBtnGroup push class="q-gutter-x-sm" flat style="margin-left: 0px"> <QBtnGroup push class="q-gutter-x-sm q-ml-none" flat>
<QBtn <QBtn
v-if="reason && state && (isHimSelf || isHr)" v-if="reason && state && (isHimSelf || isHr)"
:label="t('Reason')" :label="t('Reason')"

View File

@ -40,6 +40,7 @@ onMounted(() => {
<template> <template>
<FormModel <FormModel
model="createDepartmentChild"
:form-initial-data="departmentChildData" :form-initial-data="departmentChildData"
:observe-form-changes="false" :observe-form-changes="false"
:default-actions="false" :default-actions="false"

View File

@ -86,7 +86,7 @@ onBeforeMount(async () => {
url-create="Workers/new" url-create="Workers/new"
model="worker" model="worker"
:form-initial-data="formData" :form-initial-data="formData"
@on-data-saved="({ id }) => $router.push({ path: `/worker/${id}` })" @on-data-saved="(__, { id }) => $router.push({ path: `/worker/${id}` })"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow> <VnRow>

View File

@ -6,12 +6,11 @@ import { useQuasar } from 'quasar';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
import CreateDepartmentChild from './CreateDepartmentChild.vue'; import CreateDepartmentChild from './CreateDepartmentChild.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify();
const state = useState(); const state = useState();
const router = useRouter(); const router = useRouter();
@ -62,25 +61,28 @@ const removeNode = (node) => {
const { id, parentFk } = node; const { id, parentFk } = node;
quasar quasar
.dialog({ .dialog({
title: t('Are you sure you want to delete it?'), component: VnConfirm,
message: t('Delete department'), componentProps: {
ok: { title: t('Are you sure you want to delete it?'),
push: true, message: t('Delete department'),
color: 'primary', promise: () => remove(id),
}, },
cancel: true,
}) })
.onOk(async () => { .onOk(async () => await fetchNodeLeaves(parentFk));
try {
await axios.post(`/Departments/${id}/removeChild`, id);
notify(t('department.departmentRemoved'), 'positive');
await fetchNodeLeaves(parentFk);
} catch (err) {
console.error('Error removing department');
}
});
}; };
async function remove(id) {
try {
await axios.post(`/Departments/${id}/removeChild`, { id });
quasar.notify({
message: t('department.departmentRemoved'),
type: 'positive',
});
} catch (err) {
console.error('Error removing department');
}
}
const showCreateNodeForm = (nodeId) => { const showCreateNodeForm = (nodeId) => {
showCreateNodeFormVal.value = true; showCreateNodeFormVal.value = true;
creationNodeSelectedId.value = nodeId; creationNodeSelectedId.value = nodeId;

View File

@ -0,0 +1 @@
passwordRequirements: 'The password must have at least { length } length characters, {nAlpha} alphabetic characters, {nUpper} capital letters, {nDigits} digits and {nPunct} symbols (Ex: $%&.)\n'

View File

@ -3,3 +3,4 @@ You can search by worker id or name: Puedes buscar por id o nombre del trabajado
Locker: Taquilla Locker: Taquilla
Internal: Interno Internal: Interno
External: Externo External: Externo
passwordRequirements: 'La contraseña debe tener al menos { length } caracteres de longitud, {nAlpha} caracteres alfabéticos, {nUpper} letras mayúsculas, {nDigits} dígitos y {nPunct} símbolos (Ej: $%&.)'

View File

@ -13,6 +13,7 @@ import { useRole } from 'src/composables/useRole';
import { useUserConfig } from 'src/composables/useUserConfig'; import { useUserConfig } from 'src/composables/useUserConfig';
import { toLowerCamel } from 'src/filters'; import { toLowerCamel } from 'src/filters';
import { useTokenConfig } from 'src/composables/useTokenConfig'; import { useTokenConfig } from 'src/composables/useTokenConfig';
import { useAcl } from 'src/composables/useAcl';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
@ -55,6 +56,7 @@ export default route(function (/* { store, ssrContext } */) {
const stateRoles = state.getRoles().value; const stateRoles = state.getRoles().value;
if (stateRoles.length === 0) { if (stateRoles.length === 0) {
await useRole().fetch(); await useRole().fetch();
await useAcl().fetch();
await useUserConfig().fetch(); await useUserConfig().fetch();
await useTokenConfig().fetch(); await useTokenConfig().fetch();
} }

View File

@ -11,7 +11,16 @@ export default {
component: RouterView, component: RouterView,
redirect: { name: 'AccountMain' }, redirect: { name: 'AccountMain' },
menus: { menus: {
main: ['AccountList', 'AccountAliasList', 'AccountRoles', 'AccountAcls'], main: [
'AccountList',
'AccountAliasList',
'AccountRoles',
'AccountAccounts',
'AccountLdap',
'AccountSamba',
'AccountAcls',
'AccountConnections',
],
card: [], card: [],
}, },
children: [ children: [
@ -30,6 +39,15 @@ export default {
}, },
component: () => import('src/pages/Account/AccountList.vue'), component: () => import('src/pages/Account/AccountList.vue'),
}, },
{
path: 'role-list',
name: 'AccountRoles',
meta: {
title: 'roles',
icon: 'group',
},
component: () => import('src/pages/Account/Role/AccountRoles.vue'),
},
{ {
path: 'alias-list', path: 'alias-list',
name: 'AccountAliasList', name: 'AccountAliasList',
@ -39,6 +57,45 @@ export default {
}, },
component: () => import('src/pages/Account/AccountAliasList.vue'), component: () => import('src/pages/Account/AccountAliasList.vue'),
}, },
{
path: 'connections',
name: 'AccountConnections',
meta: {
title: 'connections',
icon: 'check',
},
component: () => import('src/pages/Account/AccountConnections.vue'),
},
{
path: 'accounts',
name: 'AccountAccounts',
meta: {
title: 'accounts',
icon: 'accessibility',
roles: ['itManagement'],
},
component: () => import('src/pages/Account/AccountAccounts.vue'),
},
{
path: 'ldap',
name: 'AccountLdap',
meta: {
title: 'ldap',
icon: 'account_tree',
roles: ['itManagement'],
},
component: () => import('src/pages/Account/AccountLdap.vue'),
},
{
path: 'samba',
name: 'AccountSamba',
meta: {
title: 'samba',
icon: 'preview',
roles: ['itManagement'],
},
component: () => import('src/pages/Account/AccountSamba.vue'),
},
{ {
path: 'acls', path: 'acls',
name: 'AccountAcls', name: 'AccountAcls',
@ -53,15 +110,6 @@ export default {
name: 'AccountAclForm', name: 'AccountAclForm',
component: () => import('src/pages/Account/Acls/AclFormView.vue'), component: () => import('src/pages/Account/Acls/AclFormView.vue'),
}, },
{
path: 'role-list',
name: 'AccountRoles',
meta: {
title: 'roles',
icon: 'group',
},
component: () => import('src/pages/Account/Role/AccountRoles.vue'),
},
], ],
}, },
], ],

View File

@ -10,6 +10,7 @@ import wagon from './modules/wagon';
import supplier from './modules/Supplier'; import supplier from './modules/Supplier';
import travel from './modules/travel'; import travel from './modules/travel';
import department from './modules/department'; import department from './modules/department';
import role from './modules/role';
import ItemType from './modules/itemType'; import ItemType from './modules/itemType';
import shelving from 'src/router/modules/shelving'; import shelving from 'src/router/modules/shelving';
import order from 'src/router/modules/order'; import order from 'src/router/modules/order';
@ -21,7 +22,6 @@ import zone from 'src/router/modules/zone';
import account from './modules/account'; import account from './modules/account';
import monitor from 'src/router/modules/monitor'; import monitor from 'src/router/modules/monitor';
import mailAlias from './modules/mailAlias'; import mailAlias from './modules/mailAlias';
import role from './modules/role';
const routes = [ const routes = [
{ {

View File

@ -0,0 +1,88 @@
import { vi, describe, expect, it, beforeAll, afterAll } from 'vitest';
import { axios, flushPromises } from 'app/test/vitest/helper';
import { useAcl } from 'src/composables/useAcl';
describe('useAcl', () => {
const acl = useAcl();
const mockAcls = [
{
model: 'Address',
property: '*',
accessType: '*',
permission: 'ALLOW',
principalType: 'ROLE',
principalId: 'employee',
},
{
model: 'Worker',
property: 'holidays',
accessType: 'READ',
permission: 'ALLOW',
principalType: 'ROLE',
principalId: 'employee',
},
{
model: 'Url',
property: 'getByUser',
accessType: 'READ',
permission: 'ALLOW',
principalType: 'ROLE',
principalId: '$everyone',
},
{
model: 'TpvTransaction',
property: 'start',
accessType: 'WRITE',
permission: 'ALLOW',
principalType: 'ROLE',
principalId: '$authenticated',
},
];
beforeAll(async () => {
vi.spyOn(axios, 'get').mockResolvedValue({ data: mockAcls });
await acl.fetch();
});
afterAll(async () => await flushPromises());
describe('hasAny', () => {
it('should return false if no roles matched', async () => {
expect(acl.hasAny('Worker', 'updateAttributes', 'WRITE')).toBeFalsy();
});
it('should return false if no roles matched', async () => {
expect(acl.hasAny('Worker', 'holidays', 'READ')).toBeTruthy();
});
describe('*', () => {
it('should return true if an acl matched', async () => {
expect(acl.hasAny('Address', '*', 'WRITE')).toBeTruthy();
});
it('should return false if no acls matched', async () => {
expect(acl.hasAny('Worker', '*', 'READ')).toBeFalsy();
});
});
describe('$authenticated', () => {
it('should return false if no acls matched', async () => {
expect(acl.hasAny('Url', 'getByUser', '*')).toBeFalsy();
});
it('should return true if an acl matched', async () => {
expect(acl.hasAny('Url', 'getByUser', 'READ')).toBeTruthy();
});
});
describe('$everyone', () => {
it('should return false if no acls matched', async () => {
expect(acl.hasAny('TpvTransaction', 'start', 'READ')).toBeFalsy();
});
it('should return false if an acl matched', async () => {
expect(acl.hasAny('TpvTransaction', 'start', 'WRITE')).toBeTruthy();
});
});
});
});

View File

@ -1,5 +1,5 @@
import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, beforeEach } from 'vitest';
import { axios, flushPromises } from 'app/test/vitest/helper'; import { axios } from 'app/test/vitest/helper';
import { useSession } from 'composables/useSession'; import { useSession } from 'composables/useSession';
import { useState } from 'composables/useState'; import { useState } from 'composables/useState';
@ -87,13 +87,17 @@ describe('session', () => {
}, },
}, },
]; ];
beforeEach(() => {
vi.spyOn(axios, 'get').mockImplementation((url) => {
if (url === 'VnUsers/acls') return Promise.resolve({ data: [] });
return Promise.resolve({
data: { roles: rolesData, user: expectedUser },
});
});
});
it('should fetch the user roles and then set token in the sessionStorage', async () => { it('should fetch the user roles and then set token in the sessionStorage', async () => {
const expectedRoles = ['salesPerson', 'admin']; const expectedRoles = ['salesPerson', 'admin'];
vi.spyOn(axios, 'get').mockResolvedValue({
data: { roles: rolesData, user: expectedUser },
});
const expectedToken = 'mySessionToken'; const expectedToken = 'mySessionToken';
const expectedTokenMultimedia = 'mySessionTokenMultimedia'; const expectedTokenMultimedia = 'mySessionTokenMultimedia';
const keepLogin = false; const keepLogin = false;
@ -117,10 +121,6 @@ describe('session', () => {
it('should fetch the user roles and then set token in the localStorage', async () => { it('should fetch the user roles and then set token in the localStorage', async () => {
const expectedRoles = ['salesPerson', 'admin']; const expectedRoles = ['salesPerson', 'admin'];
vi.spyOn(axios, 'get').mockResolvedValue({
data: { roles: rolesData, user: expectedUser },
});
const expectedToken = 'myLocalToken'; const expectedToken = 'myLocalToken';
const expectedTokenMultimedia = 'myLocalTokenMultimedia'; const expectedTokenMultimedia = 'myLocalTokenMultimedia';
const keepLogin = true; const keepLogin = true;

View File

@ -23,8 +23,9 @@ describe('Login', () => {
}, },
}; };
vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { token: 'token' } }); vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { token: 'token' } });
vi.spyOn(axios, 'get').mockResolvedValue({ vi.spyOn(axios, 'get').mockImplementation((url) => {
data: { roles: [], user: expectedUser , multimediaToken: {id:'multimediaToken' }}, if (url === 'VnUsers/acls') return Promise.resolve({ data: [] });
return Promise.resolve({data: { roles: [], user: expectedUser , multimediaToken: {id:'multimediaToken' }}});
}); });
vi.spyOn(vm.quasar, 'notify'); vi.spyOn(vm.quasar, 'notify');