Compare commits

7 Commits

Author SHA1 Message Date
Alexandro Uc
e243e8397d fix: translate contactCard 2026-04-03 14:47:35 -06:00
Alexandro Uc
4570526ceb fix: sidebar label privacy list 2026-04-03 14:45:01 -06:00
Alexandro Uc
a9f7349039 add: translate texts of privacy list module 2026-04-03 14:41:44 -06:00
Alexandro Uc
0fbe83d737 local search & fix texts 2026-04-01 20:41:09 -06:00
Alexandro Uc
974c34ad1c add: private load actions & delete contact 2026-04-01 20:41:09 -06:00
Alexandro Uc
9e6e0948d4 add: services 2026-04-01 20:41:09 -06:00
Alexandro Uc
291dbd2f35 add: contacts module & actions to private list 2026-04-01 20:41:09 -06:00
28 changed files with 806 additions and 44 deletions

View File

@@ -3,6 +3,11 @@ body {
padding: 0px; padding: 0px;
} }
/*Colors*/
.primary-color-eta {
color: #FBBA33;
}
.flex-d-column { .flex-d-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -35,12 +40,6 @@ body {
border-radius: 3rem !important; border-radius: 3rem !important;
} }
/* Fuentes */
.font-bold {
font-weight: bold;
}
/* *********** */
.divider { .divider {
display: block; display: block;
height: 2px; height: 2px;
@@ -130,6 +129,12 @@ body {
color: #FBBA33; color: #FBBA33;
} }
.text-tertiary {
font-size: 1rem !important;
font-weight: 500 !important;
color: rgb(181, 168, 168) !important;
}
.card-info, .card-info,
.card-fixed { .card-fixed {
width: 100%; width: 100%;

View File

@@ -0,0 +1,15 @@
.font-bold {
font-weight: bold;
}
.fsize-1 {
font-size: 1rem;
}
.fsize-1-5 {
font-size: 1.5rem;
}
.fsize-2 {
font-size: 2rem;
}

View File

@@ -121,6 +121,9 @@
<template> <template>
<div class="card-fixed card-load mt-4"> <div class="card-fixed card-load mt-4">
<div class="row"> <div class="row">
<p v-if="load?.privacy === true && load?.company?._id === authStore?.user?.company._id && !share">
<i class="fa-solid fa-lock primary-color-eta fsize-1-5"></i> <span class="text-tertiary fsize-1">Carga privada</span>
</p>
<div class="col-lg-6 col-sm-12"> <div class="col-lg-6 col-sm-12">
<p> <p>
<span>{{t('loads.origin')}}: </span> <span>{{t('loads.origin')}}: </span>

View File

@@ -0,0 +1,43 @@
<script setup>
const props = defineProps({
saerch: {
type: [String]
},
placeholder: {
type: String,
}
})
defineEmits(['update:saerch'])
</script>
<template>
<div class="mb-4">
<input
class="custom-search"
type="search"
name="search"
:placeholder="placeholder"
:value="saerch"
@input="$event => $emit('update:saerch', $event.target.value)"
/>
</div>
</template>
<style scoped>
.custom-search {
width: 100%;
padding: 12px 16px;
border: 1px solid rgb(222, 214, 214);
border-radius: 5px;
}
.custom-search:focus {
outline: none;
border-color: rgb(217, 202, 202);
}
</style>

View File

@@ -0,0 +1,96 @@
<script setup>
defineProps({
modelValue: {
type: Boolean,
default: false
},
label: {
type: String,
required: false
},
name: {
type: String,
required: true
},
value: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:modelValue'])
const toggle = (event) => {
emit('update:modelValue', event.target.checked)
}
</script>
<template>
<label class="switch-container">
<span v-if="label" class="custom-label label-text flex1">{{ label }}</span>
<input
type="checkbox"
:name="name"
:checked="modelValue"
@change="toggle"
>
<span class="slider"></span>
</label>
</template>
<style scoped>
.flex1 {
flex: 1;
}
.switch-container {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 16px;
}
/* ocultar input */
.switch-container input {
display: none;
}
/* fondo del switch */
.slider {
position: relative;
width: 45px;
height: 25px;
background-color: #ccc;
border-radius: 25px;
transition: 0.3s;
}
/* circulo */
.slider::before {
content: "";
position: absolute;
width: 18px;
height: 18px;
left: 4px;
top: 3.5px;
background-color: white;
border-radius: 50%;
transition: 0.3s;
}
/* estado activo */
input:checked + .slider {
background-color: #5A67DD; /* tu color */
}
input:checked + .slider::before {
transform: translateX(20px);
}
/* hover */
.switch-container:hover .slider {
opacity: 0.8;
}
</style>

View File

@@ -198,7 +198,7 @@
<option v-for="vehicle in vehiclesAvailable" :value="vehicle._id"> <option v-for="vehicle in vehiclesAvailable" :value="vehicle._id">
{{vehicle.vehicle_code?.toUpperCase()}} - {{ vehicle.truck_type }} {{vehicle.vehicle_code?.toUpperCase()}} - {{ vehicle.truck_type }}
<span v-if="vehicle?.driver">- {{ vehicle?.driver?.first_name + ' ' + vehicle?.driver?.last_name }}</span> <span v-if="vehicle?.driver">- {{ vehicle?.driver?.first_name + ' ' + vehicle?.driver?.last_name }}</span>
<span v-else>- Sin conductor</span> <span v-else>- Sin conductor -</span>
</option> </option>
</select> </select>
</div> </div>

View File

@@ -159,7 +159,10 @@ const en = {
loading: 'Please wait!', loading: 'Please wait!',
savingChanes: 'Saving changes', 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.', 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.' 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.',
notDriverAssign: 'This offer does not include an assigned driver. Please request that the carrier assign an operator to continue the loading process.',
changeSuccess: 'Changes successfully implemented!',
actionSuccess: 'Action successfully completed!',
}, },
global: { global: {
signIn: "Sign In", signIn: "Sign In",
@@ -208,6 +211,9 @@ const en = {
delivered: 'Delivered', delivered: 'Delivered',
downloading: 'Downloading', downloading: 'Downloading',
add: 'Add', add: 'Add',
dashboard: 'Dashboard',
notDriver: 'Without driver',
mistake: 'Mistake'
}, },
login: { login: {
title: 'Sign in', title: 'Sign in',
@@ -495,6 +501,19 @@ const en = {
}, },
store: { store: {
title: "Warehouse" title: "Warehouse"
},
contacts: {
privacyList: 'Private list',
truckTypesUsed: 'Types of Used Trucks',
deleteCarrier: 'Delete carrier',
QuestionDeleteCarrier: 'Are you sure you want to remove this carrier from the private list?',
QuestionAddCarrier: 'Are you sure you want to add this carrier to the private list?',
deletedCarrier: 'Carrier removed',
deletedCarrierSuccess: 'The carrier has been removed from the private list',
deletedCarrierError: 'The removal of the carrier from the private list could not be completed.',
settingsCompany: 'Company settings',
privacyCompanyLabel: 'Activate company privacy settings',
privacyLoadLabel: 'Post as private',
} }
}; };

View File

@@ -162,7 +162,10 @@ const es = {
loading: 'Por favor espere!', loading: 'Por favor espere!',
savingChanes: 'Guardando cambios', 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.', 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.' 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.',
notDriverAssign: 'Oferta sin conductor asignado. Solicite al transportista que asigne un operador para continuar con el proceso de carga.',
changeSuccess: 'Cambios aplicados con exito!',
actionSuccess: 'Acción realizada con exito!',
}, },
global: { global: {
signIn: 'Ingresar', signIn: 'Ingresar',
@@ -211,6 +214,9 @@ const es = {
delivered: 'Entregado', delivered: 'Entregado',
downloading: 'Descargando', downloading: 'Descargando',
add: 'Agregar', add: 'Agregar',
dashboard: 'Panel',
notDriver: 'Sin conductor',
mistake: 'Error'
}, },
login: { login: {
title: 'Iniciar sesión', title: 'Iniciar sesión',
@@ -504,6 +510,19 @@ const es = {
}, },
store: { store: {
title: "Bodega" title: "Bodega"
},
contacts: {
privacyList: 'Lista privada',
truckTypesUsed: 'Tipos de camiones usados',
deleteCarrier: 'Eliminar transportista',
QuestionDeleteCarrier: '¿Estas seguro de eliminar este transportista de la lista privada?',
QuestionAddCarrier: '¿Estas seguro de añadir a este transportista de la lista privada?',
deletedCarrier: 'Transportista eliminado',
deletedCarrierSuccess: 'Se ha eliminado el transportista de la lista privada',
deletedCarrierError: 'No se pudo completar la eliminación del transportista de la lista privada',
settingsCompany: 'Configuraciones de empresa',
privacyCompanyLabel: 'Activar configuración de privacidad de la empresa',
privacyLoadLabel: 'Publicar como privada',
} }
}; };

View File

@@ -1,10 +1,9 @@
<script setup> <script setup>
import LoadingModal from '../components/ui/LoadingModal.vue'; import LoadingModal from '../components/ui/LoadingModal.vue';
import NavBar from '../components/NavBar.vue'; import NavBar from './components/NavBar.vue';
import Sidebar from '../components/Sidebar.vue'; import Sidebar from './components/Sidebar.vue';
import ProfilePopup from '../views/profile/modals/ProfilePopup.vue'; import ProfilePopup from '../views/profile/modals/ProfilePopup.vue';
import NotificationsPopup from '../components/NotificationsPopup.vue'; import NotificationsPopup from './components/NotificationsPopup.vue';
</script> </script>
<template> <template>
@@ -19,6 +18,7 @@
</div> </div>
<LoadingModal/> <LoadingModal/>
<ProfilePopup/> <ProfilePopup/>
<!-- <ConfigPopup/> -->
<NotificationsPopup/> <NotificationsPopup/>
</template> </template>

View File

@@ -0,0 +1,99 @@
<script setup>
import { useNotificationsStore } from '../../stores/notifications';
import CustomSwitch from '../../components/CustomSwitch.vue'
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { onMounted } from 'vue';
import { watch } from 'vue';
import { usePrivacyStore } from '../../stores/privacy';
const lang = ref(null);
const privacy = ref(false);
const noty = useNotificationsStore();
const privacyStore = usePrivacyStore();
const { t, locale } = useI18n();
onMounted(() => {
lang.value = localStorage.getItem('lang') ?? 'es';
locale.value = lang.value;
privacy.value = privacyStore.privacy;
});
watch(lang, () => {
locale.value = lang.value
localStorage.setItem('lang', lang.value)
})
watch(privacy, () => {
privacyStore.updatePrivacy(privacy.value);
})
const closePopup = () => {
noty.toggleConfig();
}
</script>
<template>
<div
v-if="noty.openConfig"
>
<div
class="content-popup"
@click="closePopup()"
>
</div>
<div
class="profile-card">
<i class="fa-solid fa-xmark close-icon" @click="closePopup()"></i>
<br>
<CustomSwitch
label="Activar configuracion de privacidad"
v-model="privacy"
name="privacity"
/>
</div>
</div>
</template>
<style scoped>
.content-popup {
position: fixed;
z-index: 1000;
width: 100wv;
right: 0px;
top: 0px;
left: 0px;
cursor: pointer;
bottom: 0px;
background-color: #000;
opacity: 0.2;
}
.profile-card {
position: fixed;
flex: 1;
right: 20px;
top: 70px;
z-index: 2000;
width: 340px;
background-color: #FFF;
opacity: 1;
border-radius: 13px;
padding: 20px 20px;
display: flex;
flex-direction: column;
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.10));
}
.close-icon {
display: flex;
position: absolute;
right: 20px;
top: 16px;
cursor: pointer;
font-size: 24px;
}
</style>

View File

@@ -1,10 +1,10 @@
<script setup> <script setup>
import { RouterLink } from 'vue-router'; import { RouterLink } from 'vue-router';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../../stores/auth';
import { useNotificationsStore } from '../stores/notifications'; import { useNotificationsStore } from '../../stores/notifications';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { getNotificationsCompany } from '../services/company'; import { getNotificationsCompany } from '../../services/company';
const auth = useAuthStore(); const auth = useAuthStore();
const noty = useNotificationsStore(); const noty = useNotificationsStore();
@@ -75,7 +75,16 @@
<a <a
active-class="router-link-active" active-class="router-link-active"
@click="noty.toggleProfile" @click="noty.toggleProfile"
class="nav-link"><i class="fa-regular fa-user"></i></a> class="nav-link">
<i class="fa-regular fa-user"></i>
</a>
<!-- <a
v-if="permission === 'role_shipper'"
active-class="router-link-active"
@click="noty.toggleConfig"
class="nav-link">
<i class="fa-solid fa-gear"></i>
</a> -->
</div> </div>
</div> </div>
</nav> </nav>

View File

@@ -2,10 +2,10 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import {getDateTime} from '../helpers/date_formats'; import {getDateTime} from '../../helpers/date_formats';
import { deleteNotification } from '../services/company'; import { deleteNotification } from '../../services/company';
import { useNotificationsStore } from '../stores/notifications'; import { useNotificationsStore } from '../../stores/notifications';
import Spiner from './ui/Spiner.vue'; import Spiner from '../../components/ui/Spiner.vue';
const props = defineProps({ const props = defineProps({
noty: { noty: {

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { useNotificationsStore } from '../stores/notifications'; import { useNotificationsStore } from '../../stores/notifications';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import NotificationCard from './NotificationCard.vue'; import NotificationCard from './NotificationCard.vue';

View File

@@ -1,12 +1,12 @@
<script setup> <script setup>
import { RouterLink, useRoute, useRouter } from 'vue-router'; import { RouterLink, useRoute, useRouter } from 'vue-router';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../../stores/auth';
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useCompanyStore } from '../stores/company'; import { useCompanyStore } from '../../stores/company';
import { useVehiclesStore } from '../stores/vehicles'; import { useVehiclesStore } from '../../stores/vehicles';
import { useLoadsStore } from '../stores/loads'; import { useLoadsStore } from '../../stores/loads';
import { useNotificationsStore } from '../stores/notifications'; import { useNotificationsStore } from '../../stores/notifications';
const route = useRoute(); const route = useRoute();
const auth = useAuthStore(); const auth = useAuthStore();
@@ -65,7 +65,7 @@
<i class="fa-solid fa-gauge-high" :class="[route.name === 'home' ? 'router-link-active' : '']"></i> <i class="fa-solid fa-gauge-high" :class="[route.name === 'home' ? 'router-link-active' : '']"></i>
<RouterLink <RouterLink
active-class="router-link-active" active-class="router-link-active"
class="nav-link" :to="{name: 'home'}">Dashboard</RouterLink> class="nav-link" :to="{name: 'home'}">{{ t('global.dashboard') }}</RouterLink>
</div> </div>
</li> </li>
<li <li
@@ -159,6 +159,16 @@
class="nav-link" :to="{name: 'calculator'}">{{ t('global.calculator') }}</RouterLink> class="nav-link" :to="{name: 'calculator'}">{{ t('global.calculator') }}</RouterLink>
</div> </div>
</li> </li>
<li
v-if="permission === 'role_shipper' && jobRole !== roleCheck"
:class="[route.name === 'groups' ? 'bg-nav-active' : '']">
<div>
<i class="fa-regular fa-address-book" :class="[route.name === 'groups' ? 'router-link-active' : '']"></i>
<RouterLink
active-class=""
class="nav-link" :to="{name: 'groups'}">{{ t('contacts.privacyList') }}</RouterLink>
</div>
</li>
</ul> </ul>
<div class="eta-info"> <div class="eta-info">
<div class="divider"></div> <div class="divider"></div>

View File

@@ -1,4 +1,5 @@
import './assets/main.css' import './assets/main.css'
import './assets/styles/fonts.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'

View File

@@ -200,7 +200,16 @@ const router = createRouter({
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'not-found', name: 'not-found',
component: () => import('../views/dashboard/HomeView.vue'), component: () => import('../views/dashboard/HomeView.vue'),
} },
{
path: 'lista-privada',
name: 'groups',
meta: {
permissions: ['role_shipper'],
roles: ['staff', 'manager', 'owner']
},
component: () => import('../views/contacts/ContactsView.vue'),
},
] ]
} }
] ]

View File

@@ -202,3 +202,67 @@ export const deleteNotification = async(id) => {
}; };
} }
} }
export const enablePrivacyCompany = async(formData) => {
try {
const endpoint = '/v1/companies/own';
const {data} = await api.patch(endpoint, formData);
return {
msg: 'success',
data: data
};
} catch (error) {
return {
msg: error.response.data.error ?? "Algo salió mal, intente nás tarde",
data: null
};
}
}
export const getPrivateListService = async() => {
try {
const endpoint = `/v1/groups/private`;
const {data} = await api.get(endpoint);
return {
msg: 'success',
data: data
};
} catch (error) {
return {
msg: error.response.data.error ?? "Algo salió mal, intente nás tarde",
data: null
};
}
}
export const addCompanyPrivicyListService = async(id) => {
try {
const endpoint = `/v1/groups/private/${id}`;
const {data} = await api.patch(endpoint);
return {
msg: 'success',
data: data
};
} catch (error) {
return {
msg: error.response.data.error ?? "Algo salió mal, intente nás tarde",
data: null
};
}
}
export const deleteCompanyPrivicyListService = async(id) => {
try {
const endpoint = `/v1/groups/private/${id}`;
const {data} = await api.delete(endpoint);
return {
msg: 'success',
data: data
};
} catch (error) {
return {
msg: error.response.data.error ?? "Algo salió mal, intente nás tarde",
data: null
};
}
}

View File

@@ -5,12 +5,14 @@ import { renewToken } from '../services/auth';
import {useNotificationsStore} from './notifications'; import {useNotificationsStore} from './notifications';
import { useLoadsStore } from "./loads"; import { useLoadsStore } from "./loads";
import { updateMyUserProfile } from "../services/company"; import { updateMyUserProfile } from "../services/company";
import { usePrivacyStore } from "./privacy";
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const noty = useNotificationsStore(); const noty = useNotificationsStore();
const privicyStore = usePrivacyStore();
const loadStore = useLoadsStore(); const loadStore = useLoadsStore();
const sesion = ref('') const sesion = ref('')
const checking = ref(false); const checking = ref(false);
@@ -39,6 +41,7 @@ export const useAuthStore = defineStore('auth', () => {
if(resp.msg === 'success') { if(resp.msg === 'success') {
user.value = resp.data.user; user.value = resp.data.user;
sesion.value = resp.data.session_token; sesion.value = resp.data.session_token;
privicyStore.privacy = resp.data.user?.company?.privacy || false;
token.value = resp.data.accessToken; token.value = resp.data.accessToken;
localStorage.setItem('session', resp.data.session_token); localStorage.setItem('session', resp.data.session_token);
localStorage.setItem('access', resp.data.accessToken); localStorage.setItem('access', resp.data.accessToken);

View File

@@ -8,6 +8,7 @@ export const useNotificationsStore = defineStore('notifications', () => {
const error = ref(false) const error = ref(false)
const show = ref(false); const show = ref(false);
const openProfile = ref(false); const openProfile = ref(false);
const openConfig = ref(false);
const openNotifications = ref(false); const openNotifications = ref(false);
const notifications = ref([]); const notifications = ref([]);
const newNoty = ref(false); const newNoty = ref(false);
@@ -26,6 +27,10 @@ export const useNotificationsStore = defineStore('notifications', () => {
openProfile.value = !openProfile.value; openProfile.value = !openProfile.value;
} }
const toggleConfig = () => {
openConfig.value = !openConfig.value;
}
const toggleNotifications = () => { const toggleNotifications = () => {
openNotifications.value = !openNotifications.value; openNotifications.value = !openNotifications.value;
} }
@@ -39,6 +44,7 @@ export const useNotificationsStore = defineStore('notifications', () => {
newNoty.value = false; newNoty.value = false;
show.value = false; show.value = false;
openNotifications.value = false; openNotifications.value = false;
openConfig.value = false;
toggleProfile.value = false; toggleProfile.value = false;
} }
@@ -51,8 +57,10 @@ export const useNotificationsStore = defineStore('notifications', () => {
notifications, notifications,
newNoty, newNoty,
openProfile, openProfile,
openConfig,
removeNoty, removeNoty,
toggleProfile, toggleProfile,
toggleConfig,
openNotifications, openNotifications,
toggleNotifications, toggleNotifications,
clear clear

76
src/stores/privacy.js Normal file
View File

@@ -0,0 +1,76 @@
import { defineStore } from "pinia"
import { ref } from "vue";
import { addCompanyPrivicyListService, deleteCompanyPrivicyListService, enablePrivacyCompany, getPrivateListService } from "../services/company";
export const usePrivacyStore = defineStore('privacy', () => {
const privacy = ref(false);
const privateListRef = ref([]);
const loading = ref(false);
const privateList = ref([]);
const updatePrivacy = async (value) => {
const data = {
"privacy": value
};
const response = await enablePrivacyCompany(data);
if(response.msg == 'success') {
privacy.value = response.data.privacy || false;
}
}
const getPrivateList = async (id) => {
loading.value = true;
const response = await getPrivateListService();
loading.value = false;
if(response.msg == 'success') {
privateList.value = response.data.allowedCompanies || [];
privateListRef.value = privateList.value.map((e) => e._id);
return 'success';
} else {
return response.msg;
}
}
const addCompanyToPrivateList = async (id) => {
const response = await addCompanyPrivicyListService(id);
if(response.msg == 'success') {
privateListRef.value = [
...privateListRef.value,
id
];
return 'success';
} else {
return response.msg;
}
}
const deleteCompanyToPrivateList = async (id) => {
const response = await deleteCompanyPrivicyListService(id);
if(response.msg == 'success') {
removeContact(id);
return 'success';
} else {
return response.msg;
}
}
/// State
const removeContact = (id) => {
privateList.value = privateList.value.filter((e) => e._id !== id);
privateListRef.value = privateListRef.value.filter((e) => e !== id);
}
return {
loading,
privacy,
privateListRef,
privateList,
removeContact,
updatePrivacy,
getPrivateList,
deleteCompanyToPrivateList,
addCompanyToPrivateList
}
});

View File

@@ -98,7 +98,7 @@
localStorage.setItem('id', result.data._id); localStorage.setItem('id', result.data._id);
localStorage.setItem('session', auth.sesion); localStorage.setItem('session', auth.sesion);
localStorage.setItem('access', auth.accessToken); localStorage.setItem('access', auth.token);
const userData = { const userData = {
"first_name" : user.name, "first_name" : user.name,

View File

@@ -8,16 +8,21 @@
import EditCompanyModal from './modals/EditCompanyModal.vue'; import EditCompanyModal from './modals/EditCompanyModal.vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import CustomSwitch from '../../components/CustomSwitch.vue';
import { usePrivacyStore } from '../../stores/privacy';
const auth = useAuthStore() const auth = useAuthStore()
const privacyStore = usePrivacyStore();
const company = useCompanyStore(); const company = useCompanyStore();
const { user } = storeToRefs(auth); const { user } = storeToRefs(auth);
const { t, locale } = useI18n() const { t, locale } = useI18n()
const privacy = ref(false);
onMounted(() => { onMounted(() => {
if(user.value) { if(user.value) {
getInitialData() getInitialData()
} }
privacy.value = privacyStore.privacy;
}); });
watch(user, () => { watch(user, () => {
@@ -26,6 +31,10 @@
} }
}) })
watch(privacy, () => {
privacyStore.updatePrivacy(privacy.value);
})
const getInitialData = async() => { const getInitialData = async() => {
await company.getCompanyData(); await company.getCompanyData();
} }
@@ -36,7 +45,7 @@
<template> <template>
<EditCompanyModal v-if="company.loading === false"/> <EditCompanyModal v-if="company.loading === false"/>
<div> <div>
<h2 class="title my-5">{{ t('company.title') }}</h2> <h2 class="title my-2">{{ t('company.title') }}</h2>
<div class="card-info"> <div class="card-info">
<Spiner v-if="company.loading"/> <Spiner v-if="company.loading"/>
<div v-else class="view"> <div v-else class="view">
@@ -93,6 +102,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-fixed flex-d-column"
v-if="auth.user?.job_role === 'owner' || auth.user?.job_role === 'manager'">
<h3> {{ t('contacts.settingsCompany') }} </h3>
<hr>
<CustomSwitch
:label="t('contacts.privacyCompanyLabel')"
v-model="privacy"
name="privacity"
/>
</div>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,69 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { usePrivacyStore } from '../../stores/privacy';
import ContactCard from './components/ContactCard.vue';
import { useI18n } from 'vue-i18n';
import CustomSearchInput from '../../components/CustomSearchInput.vue';
import Spiner from '../../components/ui/Spiner.vue';
const privateStore = usePrivacyStore();
const contacts = ref([]);
const { t } = useI18n();
const query = ref('');
let timeout = null;
onMounted( async () => {
await privateStore.getPrivateList();
contacts.value = privateStore.privateList;
});
watch(query, () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
search();
}, 400);
});
watch(
() => privateStore.privateList,
(newValue) => {
contacts.value = newValue;
}
);
const search = () => {
if(query.value.length > 0) {
contacts.value = privateStore.privateList.filter(
(e) => e.company_name
.toLowerCase()
.includes(query.value.toLocaleLowerCase()));
} else {
contacts.value = privateStore.privateList;
}
};
</script>
<template>
<CustomSearchInput
:placeholder="t('carriers.searchByCarrier')"
v-model:saerch="query"
/>
<div class="row">
<div v-if="privateStore.loading" class="d-flex justify-content-center">
<Spiner />
</div>
<ContactCard
v-else
v-for="contact in contacts"
:key="contact.id"
:contact="contact"
/>
</div>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,108 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '../../../stores/auth';
import Swal from 'sweetalert2';
import { usePrivacyStore } from '../../../stores/privacy';
import { computed } from 'vue';
const props = defineProps({
contact: {
type: Object,
required: true
}
})
const { t } = useI18n();
const authStore = useAuthStore();
const contactsStore = usePrivacyStore();
const handleDeleteContact = async() => {
Swal.fire({
title: t('contacts.deleteCarrier'),
text: t('contacts.QuestionDeleteCarrier'),
icon: 'warning',
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonText: t('buttons.delete'),
cancelButtonText: t('buttons.cancel'),
}).then(async(result) => {
const id = props.contact._id;
if(result.isConfirmed) {
Swal.fire({
title: t('messages.loading'),
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
const resp = await contactsStore.deleteCompanyToPrivateList(id);
Swal.close();
if(resp === 'success') {
Swal.fire({
text: t('messages.actionSuccess'),
icon: "success"
});
} else {
Swal.fire({
title: t('global.mistake'),
text: resp,
icon: "error"
});
}
}
});
}
const categories = computed(() => (props.contact.categories)
? props.contact.categories.map((e) => e.name).join(', ')
: ''
)
const truckTypes = computed(() => (props.contact.truck_type)
? props.contact.truck_type.join(', ')
: ''
)
const states = computed(() => (props.contact.company_state)
? props.contact.company_state.join(', ')
: ''
)
</script>
<template>
<div class="col-lg-6 col-12">
<div class="card-fixed flex-d-column">
<div class="d-flex">
<h2 class="flex1">{{ contact.company_name }}</h2>
<button
v-if="(authStore.user?.job_role === 'owner' || authStore.user?.job_role === 'manager')"
class="btn-primary-sm bg-danger sizeBtn"
@click="handleDeleteContact"
>
<i class="fa-solid fa-trash"></i>
</button>
</div>
<p><span class="font-bold">RFC: </span> {{ contact.rfc}}</p>
<p><span class="font-bold">{{ t('global.segments') }}:</span> {{ categories }}</p>
<p><span class="font-bold">{{ t('contacts.truckTypesUsed') }}: </span> {{ truckTypes }}</p>
<p><span class="font-bold">{{ t('global.states') }}: </span> {{ states }}</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.flex1 {
flex: 1;
}
.sizeBtn {
width: 46px;
height: 40px;
margin-left: 16px;
}
</style>

View File

@@ -14,15 +14,17 @@
import { useCompanyStore } from '../../../stores/company'; import { useCompanyStore } from '../../../stores/company';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed } from 'vue'; import { computed } from 'vue';
import {getDateToLocal } from '../../../helpers/date_formats';
import { validateEmail } from '../../../helpers/validations'; import { validateEmail } from '../../../helpers/validations';
import AddressPreview from '../../../components/AddressPreview.vue'; import AddressPreview from '../../../components/AddressPreview.vue';
import CustomSwitch from '../../../components/CustomSwitch.vue';
import { usePrivacyStore } from '../../../stores/privacy';
const loadStore = useLoadsStore(); const loadStore = useLoadsStore();
const notyStore = useNotificationsStore(); const notyStore = useNotificationsStore();
const auth = useAuthStore(); const auth = useAuthStore();
const companyStore = useCompanyStore() const companyStore = useCompanyStore()
const privacyStore = usePrivacyStore()
const windowWidth = ref(window.innerWidth); const windowWidth = ref(window.innerWidth);
const zoom = ref(6); const zoom = ref(6);
const heightMap = ref(768); const heightMap = ref(768);
@@ -42,7 +44,8 @@
const destinationRef = ref('') const destinationRef = ref('')
const emails = ref([]); const emails = ref([]);
const emailInput = ref(''); const emailInput = ref('');
const isPrivate = ref(false);
const errors = ref({ const errors = ref({
segment: null, segment: null,
truckType: null, truckType: null,
@@ -76,10 +79,12 @@
zoom.value = 4; zoom.value = 4;
heightMap.value = 420; heightMap.value = 420;
} }
isPrivate.value = privacyStore.privacy;
getLocations('unloading'); getLocations('unloading');
getLocations('loading'); getLocations('loading');
formLoad.owner = auth.user?.first_name + ' ' + auth.user?.last_name; formLoad.owner = auth.user?.first_name + ' ' + auth.user?.last_name;
if(loadStore.currentLoad){ if(loadStore.currentLoad){
isPrivate.value = loadStore.currentLoad.privacy || false
const dateStart = loadStore.currentLoad.est_loading_date; const dateStart = loadStore.currentLoad.est_loading_date;
const dateEnd = loadStore.currentLoad.est_unloading_date; const dateEnd = loadStore.currentLoad.est_unloading_date;
formLoad.price = loadStore.currentLoad.actual_cost; formLoad.price = loadStore.currentLoad.actual_cost;
@@ -257,7 +262,8 @@
posted_by_name: formLoad.owner, posted_by_name: formLoad.owner,
origin_warehouse: locationLoadSelected.value?._id || null, origin_warehouse: locationLoadSelected.value?._id || null,
destination_warehouse: locationDownloadSelected.value?._id || null, destination_warehouse: locationDownloadSelected.value?._id || null,
alert_list: emails.value.length > 0 ? emails.value : null alert_list: emails.value.length > 0 ? emails.value : null,
privacy: isPrivate.value
}; };
return loadData; return loadData;
} }
@@ -605,6 +611,13 @@
<div class="modal-footer custom-footer"> <div class="modal-footer custom-footer">
<Spiner v-if="isLoading"/> <Spiner v-if="isLoading"/>
<div v-else class="btns-footer"> <div v-else class="btns-footer">
<div class="switch-container">
<CustomSwitch
:label="t('contacts.privacyLoadLabel')"
v-model="isPrivate"
name="contacts"
/>
</div>
<button <button
type="button" type="button"
class="btn btn-danger" class="btn btn-danger"
@@ -651,20 +664,22 @@
width: 100%; width: 100%;
} }
.custom-footer {
display: flex;
justify-content: center;
}
.error-msg { .error-msg {
color: red; color: red;
font-size: 12px; font-size: 12px;
font-weight: 300; font-weight: 300;
} }
.custom-footer {
display: flex;
justify-content: flex-end;
}
.btns-footer { .btns-footer {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
justify-content: flex-end;
align-items: center;
} }
.chekmark { .chekmark {
@@ -690,5 +705,15 @@
flex-direction: column; flex-direction: column;
gap: 2rem; gap: 2rem;
} }
.btns-footer {
flex-wrap: wrap;
}
.switch-container {
flex: 1 1 100%;
justify-content: flex-end;
margin-bottom: 0.3rem;
}
} }
</style> </style>

View File

@@ -215,7 +215,7 @@
<div v-if="!proposal.vehicle?.driver" <div v-if="!proposal.vehicle?.driver"
class="box-note bg-warning mb-3" class="box-note bg-warning mb-3"
> >
<i class="fa-solid fa-triangle-exclamation"></i> Oferta sin conductor asignado. Solicite al transportista que asigne un operador para continuar con el proceso de carga. <i class="fa-solid fa-triangle-exclamation"></i> {{ t('messages.notDriverAssign') }}
</div> </div>
<Spiner v-if="isLoadingActions"/> <Spiner v-if="isLoadingActions"/>
<div class="d-flex justify-content-end gap-3" v-else> <div class="d-flex justify-content-end gap-3" v-else>

View File

@@ -9,7 +9,9 @@
import Cities from '../../components/ui/Cities.vue'; import Cities from '../../components/ui/Cities.vue';
import Pagination from '../../components/Pagination.vue'; import Pagination from '../../components/Pagination.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { usePrivacyStore } from '../../stores/privacy';
const privacyStore = usePrivacyStore();
const {loading, companies, getCompaniesData, companiesTotal, currentCompaniesPage} = useDirectory(); const {loading, companies, getCompaniesData, companiesTotal, currentCompaniesPage} = useDirectory();
const query = ref(''); const query = ref('');
const selectedTruckType = ref([]); const selectedTruckType = ref([]);
@@ -22,10 +24,11 @@
const limit = 10; const limit = 10;
onMounted(() => { onMounted( async () => {
filterQuery.value.company_type = 'carrier'; filterQuery.value.company_type = 'carrier';
filterQuery.value.limit = 'elements=' + limit; filterQuery.value.limit = 'elements=' + limit;
filterQuery.value.page = "page=0"; filterQuery.value.page = "page=0";
await privacyStore.getPrivateList();
getCompaniesData(filterQuery.value); getCompaniesData(filterQuery.value);
}); });

View File

@@ -2,13 +2,65 @@
import { RouterLink } from 'vue-router'; import { RouterLink } from 'vue-router';
import { getDateMonthDay } from '../../../helpers/date_formats'; import { getDateMonthDay } from '../../../helpers/date_formats';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Swal from 'sweetalert2';
import { onMounted, ref } from 'vue';
import { useAuthStore } from '../../../stores/auth';
import { usePrivacyStore } from '../../../stores/privacy';
defineProps({ const props = defineProps({
company: { company: {
type: Object, type: Object,
required: true, required: true,
} }
}); });
const authStore = useAuthStore();
const privacyStore = usePrivacyStore();
const existInPrivateList = ref(false);
onMounted(() => {
const id = props.company._id;
existInPrivateList.value = privacyStore.privateListRef.includes(id);
})
const handleAddPrivateList = async() => {
Swal.fire({
title: t('contacts.privacyList'),
text: t('contacts.QuestionAddCarrier'),
icon: 'info',
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonText: t('buttons.yes'),
cancelButtonText: t('buttons.no'),
}).then(async(result) => {
if(result.isConfirmed) {
Swal.fire({
title: t('messages.loading'),
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading()
},
});
const response = await privacyStore.addCompanyToPrivateList(props.company._id);
Swal.close();
if(response === 'success') {
existInPrivateList.value = true;
Swal.fire({
text: t('messages.actionSuccess'),
icon: "success"
});
} else {
Swal.fire({
title: 'Error',
text: response,
icon: "error"
});
}
}
});
}
const { t } = useI18n() const { t } = useI18n()
</script> </script>
@@ -30,9 +82,16 @@
<p><span>{{ t('labels.infoCompany') }}: </span>{{company.company_description}}</p> <p><span>{{ t('labels.infoCompany') }}: </span>{{company.company_description}}</p>
</div> </div>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end gap-2">
<button
v-if="authStore.user?.permissions === 'role_shipper' && !existInPrivateList"
class="btn-primary-sm bg-dark radius-sm"
@click="handleAddPrivateList"
>
Añadir a lista privada
</button>
<RouterLink <RouterLink
class="btn-primary-sm" class="btn-primary-sm radius-sm"
:to="{name: 'public-users', params: {id: company._id}}" :to="{name: 'public-users', params: {id: company._id}}"
>{{ t('buttons.profile') }}</RouterLink> >{{ t('buttons.profile') }}</RouterLink>
</div> </div>