add: observer input & tooltips

This commit is contained in:
Alexandro Uc Santos
2025-04-26 14:17:12 -06:00
parent 228609db46
commit 4c8b0470d2
6 changed files with 244 additions and 6 deletions

View File

@@ -231,6 +231,47 @@ td {
border: 1px solid #FBBA33 !important;
}
.box-observers {
display: flex;
flex-direction: column;
}
.box-input {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
}
.input-observer {
flex: 1;
}
.box-emails {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
border-radius: 5px;
padding: 0.5rem;
background-color: #FFF;
border: 1px solid #ccc;
}
.observer-email {
display: flex;
align-items: center;
background-color: #e0f7fa;
padding: 0.5rem;
border-radius: 5px;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.icon-delete {
cursor: pointer;
color: #FF0000;
font-size: 1.5rem;
margin-left: 0.5rem;
transition: color 0.3s;
}
@media (max-width: 1024px) {
th {
font-size: 13px;

View File

@@ -10,6 +10,7 @@
import { useCompanyStore } from '../stores/company';
import Swal from 'sweetalert2';
import { useI18n } from 'vue-i18n';
import { validateEmail } from '../helpers/validations';
const props = defineProps({
location: {
@@ -27,6 +28,8 @@
const { t } = useI18n()
const loading = ref(false);
const emails = ref([]);
const emailInput = ref('');
const title = computed(() => {
return (props.location) ? t('directory.editLocation') : t('directory.createLocation');
@@ -139,6 +142,33 @@
}
}
const addObserver = () => {
const trimmedEmail = emailInput.value.trim()
if (trimmedEmail && validateEmail(trimmedEmail)) {
if(emails.value.includes(trimmedEmail)) {
Swal.fire({
title: t('errors.emailExist'),
icon: 'error'
})
return
}
emails.value.push(trimmedEmail)
emailInput.value = ''
} else if (trimmedEmail) {
Swal.fire({
title: t('errors.email'),
icon: 'error'
})
}
}
const removeObserver = (email) => {
emails.value = emails.value.filter((e) => e !== email)
}
const validations = () => {
errors.value = {
branch_name: locationForm.branch_name.length < 2 ? t('errors.nameRequired') : null,
@@ -251,6 +281,42 @@
v-model="locationForm.description"
></textarea>
</div>
<div class="divider mt-4"></div>
<div class="box-observers mt-4">
<div class="box-input">
<div class="input-observer">
<CustomInput
label="Agregar observador de bodega"
name="email"
type="email"
v-model:field="emailInput"
:filled="false"
:tooltip="t('messages.observerWarehouse')"
/>
</div>
<button
type="button"
@click="addObserver"
class="btn btn-dark"
>
Agregar
</button>
</div>
<h5 v-if="emails.length > 0">Lista de observadores:</h5>
<div class="box-emails" v-if="emails.length > 0">
<div
v-for="(email, index) in emails"
:key="email"
class="observer-email"
>
<span>{{ email }}</span>
<i
@click="removeObserver(email)"
class="fa-solid fa-xmark icon-delete">
</i>
</div>
</div>
</div>
<div class="mt-4 text-center">
<Spiner v-if="loading"/>
<button

View File

@@ -17,6 +17,7 @@
import { useI18n } from 'vue-i18n';
import { computed } from 'vue';
import {getDateTime } from '../helpers/date_formats';
import { validateEmail } from '../helpers/validations';
const loadStore = useLoadsStore();
@@ -43,6 +44,8 @@
const locationDownloadSelected = ref(null)
const originRef = ref('')
const destinationRef = ref('')
const emails = ref([]);
const emailInput = ref('');
const errors = ref({
segment: null,
@@ -362,6 +365,31 @@
? t('loads.create')
: t('loads.edit')
)
const addObserver = () => {
const trimmedEmail = emailInput.value.trim()
if (trimmedEmail && validateEmail(trimmedEmail)) {
if(emails.value.includes(trimmedEmail)) {
Swal.fire({
title: t('errors.emailExist'),
icon: 'error'
})
return
}
emails.value.push(trimmedEmail)
emailInput.value = ''
} else if (trimmedEmail) {
Swal.fire({
title: t('errors.email'),
icon: 'error'
})
}
}
const removeObserver = (email) => {
emails.value = emails.value.filter((e) => e !== email)
}
</script>
<template>
@@ -425,14 +453,14 @@
:error="(submited && !formLoad.weight) ? t('errors.weight') : null"
v-model:field="formLoad.weight"
/>
</div>
<div class="form-section">
<div class="mb-4 mt-3">
<label class="custom-label">{{ t('loads.product') }}</label>
<Products
v-model="formLoad.terms"
/>
</div>
</div>
<div class="form-section">
<Custominput
:label="t('loads.labelPrice')"
type="Number"
@@ -455,6 +483,41 @@
name="owner"
v-model:field="formLoad.owner"
/>
<div class="box-observers mt-4">
<div class="box-input">
<div class="input-observer">
<Custominput
label="Agregar cliente observador"
name="email"
type="email"
v-model:field="emailInput"
:filled="false"
:tooltip="t('messages.observerClient')"
/>
</div>
<button
type="button"
@click="addObserver"
class="btn btn-dark"
>
Agregar
</button>
</div>
<h5 v-if="emails.length > 0">Lista de observadores:</h5>
<div class="box-emails" v-if="emails.length > 0">
<div
v-for="(email, index) in emails"
:key="email"
class="observer-email"
>
<span>{{ email }}</span>
<i
@click="removeObserver(email)"
class="fa-solid fa-xmark icon-delete">
</i>
</div>
</div>
</div>
</div>
</div>
<div class="form-box">

View File

@@ -39,6 +39,9 @@
step: {
type: Number,
default: 0.1
},
tooltip: {
type: String,
}
})
@@ -48,11 +51,24 @@
<template>
<div class="d-flex flex-column gap-2 mb-4">
<label
<!-- <label
class="custom-label"
:class="[label.includes('*') ? 'required' : '']"
:for="name"
>{{ label }}</label>
>{{ label }}</label> -->
<div class="label-box">
<label
class="custom-label label-text"
:class="[label.includes('*') ? 'required' : '']"
:for="name"
>{{ label }}</label>
<div class="help-icon" v-if="tooltip">
<div class="tooltip">
{{ tooltip }}
</div>
</div>
</div>
<input
class="custom-input"
:class="[
@@ -86,4 +102,50 @@
font-size: 12px;
font-weight: 400;
}
.label-box {
display: flex;
flex-direction: row;
width: 100%;
align-items: center;
}
.label-text {
flex: 1;
}
/* Estilo del ícono de ayuda */
.help-icon {
position: relative;
font-size: 20px;
color: #666;
cursor: pointer;
}
/* Tooltip oculto inicialmente */
.tooltip {
position: absolute;
top: 100%;
left: 50%;
width: 180px;
padding: 8px;
background: #333;
color: white;
font-size: 12px;
text-align: center;
border-radius: 5px;
opacity: 0;
pointer-events: none;
transform: translateX(-50%);
margin-top: 8px;
transition: opacity 0.3s ease;
}
/* Mostrar tooltip al pasar el mouse */
.help-icon:hover .tooltip,
.help-icon:active .tooltip {
opacity: 1;
pointer-events: auto;
}
</style>

View File

@@ -113,6 +113,7 @@ const en = {
required: 'Field is required',
requireds: "All fields required",
email: "Email is not valid",
emailExist: 'Email already exists',
weakPassword: "Weak password",
matchPassword: "Passwords do not match",
code: 'Enter valid code',
@@ -156,6 +157,8 @@ const en = {
msgCreatedUser: 'When creating a new user, you inform the account beneficiary that they must use their email to set a password in the <span class="font-bold">Forgot my password</span> section so they can log in.',
loading: 'Please wait!',
savingChanes: 'Saving changes',
observerWarehouse: 'Warehouse observers will receive notifications about the load location, including arrival and departure times, and will be able to view the cargo on the warehouse dashboard.',
observerClient: 'Observers will receive notifications about the load, including the loading and unloading date, as well as any changes in the load status. They can monitor the cargo on their customer dashboard.'
},
global: {
signIn: "Sign In",
@@ -177,7 +180,7 @@ const en = {
footer: 'ETA VIAPORTE ALL RIGHTS RESERVED',
company: 'Company',
users: 'Users',
directory: 'Internal directory',
directory: 'Warehouses',
publications: 'Publications',
calendar: 'Calendar',
carriers: 'Carriers',

View File

@@ -116,6 +116,7 @@ const es = {
required: 'Campo es requerido',
requireds: 'Todos los campos con obligatorios',
email: 'Correo electrónico no es valido',
emailExist: 'Este correo ya fue agregado',
weakPassword: 'Contraseña poco segura',
matchPassword: 'Las contraseñas no coinciden',
code: 'Ingresa código valido',
@@ -159,6 +160,8 @@ const es = {
msgCreatedUser: 'Al crear un nuevo usuario, informa al beneficiario de la cuenta, que debe utilizar su correo electrónico para establecer una contraseña en la sección <span class="font-bold">Olvidé mi contraseña</span> y así poder iniciar sesión.',
loading: 'Por favor espere!',
savingChanes: 'Guardando cambios',
observerWarehouse: 'Los observadores de bodega recibirán notificaciones sobre la ubicación de la carga, incluyendo la hora de llegada y salida, podran visualizar la carga en el panel de bodega.',
observerClient: 'Los observadores recibirán notificaciones sobre la carga, incluyendo la fecha de carga y descarga, así como cualquier cambio en el estado de la carga. podran monitorer la carga en el panel de clientes.'
},
global: {
signIn: 'Ingresar',
@@ -180,7 +183,7 @@ const es = {
footer: 'ETA VIAPORTE TODOS LOS DERECHOS RESERVADOS',
company: 'Empresa',
users: 'Usuarios',
directory: 'Directorio interno',
directory: 'Bodegas',
publications: 'Publicaciones',
calendar: 'Calendario',
carriers: 'Transportistas',