add: contacts module & actions to private list

This commit is contained in:
Alexandro Uc
2026-03-28 14:44:54 -06:00
parent 4cc8fd7082
commit 291dbd2f35
9 changed files with 455 additions and 10 deletions

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,93 @@
<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">{{ label }}</span>
<input
type="checkbox"
:name="name"
:checked="modelValue"
@change="toggle"
>
<span class="slider"></span>
</label>
</template>
<style scoped>
.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

@@ -159,6 +159,16 @@
class="nav-link" :to="{name: 'calculator'}">{{ t('global.calculator') }}</RouterLink>
</div>
</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'}">Lista privada</RouterLink>
</div>
</li>
</ul>
<div class="eta-info">
<div class="divider"></div>

View File

@@ -200,7 +200,16 @@ const router = createRouter({
path: '/:pathMatch(.*)*',
name: 'not-found',
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'),
},
]
}
]

66
src/stores/contacts.js Normal file
View File

@@ -0,0 +1,66 @@
import { defineStore } from "pinia"
import { ref } from "vue";
export const useContactsStore = defineStore('contacts', () => {
const contacts = ref([
{
"id": 13,
"company": "Altos",
"companyId": 929,
"rfc": "USKSK00101",
"category": "Agricola"
},
{
"id": 15,
"company": "Altos logos",
"companyId": 2018,
"rfc": "USKSK0010a",
"category": "Agricola"
},
{
"id": 18,
"company": "Bravos SA",
"companyId": 199,
"rfc": "UJSK8991",
"category": "Materiales"
},
{
"id": 10,
"company": "Kolo",
"companyId": 1993,
"rfc": "JKDKD91001",
"category": "Servicios"
},
{
"id": 39,
"company": "Altos",
"companyId": 929,
"rfc": "USKSK00101",
"category": "Agricola"
},
{
"id": 19,
"company": "Altos",
"companyId": 929,
"rfc": "USKSK00101",
"category": "Agricola"
},
{
"id": 934,
"company": "Altos",
"companyId": 929,
"rfc": "USKSK00101",
"category": "Agricola"
},
]);
const removeContact = (id) => {
contacts.value = contacts.value.filter((e) => e.id !== id);
}
return {
contacts,
removeContact
}
});

View File

@@ -0,0 +1,54 @@
<script setup>
import { ref, watch } from 'vue';
import { useContactsStore } from '../../stores/contacts';
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 contactsStore = useContactsStore();
const { t } = useI18n();
const loading = ref(false);
const query = ref('');
let timeout = null;
const search = () => {
console.log('Searching:', query.value);
};
watch(query, (newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
if (newValue.length >= 2) {
search();
}
}, 400);
});
</script>
<template>
<CustomSearchInput
:placeholder="t('carriers.searchByCarrier')"
v-model:saerch="query"
/>
<div class="row">
<div v-if="loading" class="d-flex justify-content-center">
<Spiner />
</div>
<ContactCard
v-else
v-for="contact in contactsStore.contacts"
:key="contact.id"
:contact="contact"
/>
</div>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,90 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '../../../stores/auth';
import Swal from 'sweetalert2';
import { useContactsStore } from '../../../stores/contacts';
const props = defineProps({
contact: {
type: Object,
required: true
}
})
const { t } = useI18n();
const authStore = useAuthStore();
const contactsStore = useContactsStore();
const handleDeleteContact = async() => {
Swal.fire({
title: 'Eliminar transportista',
text: '¿Estas seguro de eliminar este transportista de la lista privada?',
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()
},
});
let resp = null;
setTimeout(() => {
resp = true
Swal.close();
if(resp != null) {
contactsStore.removeContact(id);
Swal.fire({
title: 'Transportista eliminado',
text: 'Se ha eliminado el transportista de la lista privada',
icon: "success"
});
} else {
Swal.fire({
title: t('errors.msgTitleNotDel'),
text: 'No se pudo completar la eliminación del transportista de la lista privada',
icon: "error"
});
}
}, 500);
}
});
}
</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 }}</h2>
<button
v-if="(authStore.user?.job_role === 'owner' || authStore.user?.job_role === 'manager')"
class="btn-primary-sm bg-danger"
@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> {{contact.category}}</p>
<p><span class="font-bold">{{ t('directory.typeTruckNeed') }}: </span> Torton</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.flex1 {
flex: 1;
}
</style>

View File

@@ -14,9 +14,9 @@
import { useCompanyStore } from '../../../stores/company';
import { useI18n } from 'vue-i18n';
import { computed } from 'vue';
import {getDateToLocal } from '../../../helpers/date_formats';
import { validateEmail } from '../../../helpers/validations';
import AddressPreview from '../../../components/AddressPreview.vue';
import CustomSwitch from '../../../components/CustomSwitch.vue';
const loadStore = useLoadsStore();
@@ -42,6 +42,7 @@
const destinationRef = ref('')
const emails = ref([]);
const emailInput = ref('');
const isPrivate = ref(false);
const errors = ref({
segment: null,
@@ -605,6 +606,14 @@
<div class="modal-footer custom-footer">
<Spiner v-if="isLoading"/>
<div v-else class="btns-footer">
<div class="switch-container">
<CustomSwitch
v-if="loadStore?.currentLoad == null || loadStore.currentLoad?.status === 'Draft'"
label='Publicar como privada'
v-model="isPrivate"
name="contacts"
/>
</div>
<button
type="button"
class="btn btn-danger"
@@ -651,20 +660,22 @@
width: 100%;
}
.custom-footer {
display: flex;
justify-content: center;
}
.error-msg {
color: red;
font-size: 12px;
font-weight: 300;
}
.custom-footer {
display: flex;
justify-content: flex-end;
}
.btns-footer {
display: flex;
gap: 1rem;
justify-content: flex-end;
align-items: center;
}
.chekmark {
@@ -690,5 +701,15 @@
flex-direction: column;
gap: 2rem;
}
.btns-footer {
flex-wrap: wrap;
}
.switch-container {
flex: 1 1 100%;
justify-content: flex-end;
margin-bottom: 0.3rem;
}
}
</style>

View File

@@ -2,6 +2,9 @@
import { RouterLink } from 'vue-router';
import { getDateMonthDay } from '../../../helpers/date_formats';
import { useI18n } from 'vue-i18n';
import Swal from 'sweetalert2';
import { ref } from 'vue';
import { useAuthStore } from '../../../stores/auth';
defineProps({
company: {
@@ -10,6 +13,53 @@
}
});
const authStore = useAuthStore();
let existInPrivateList = ref(false);
const handleAddPrivateList = async() => {
Swal.fire({
title: 'Lista privadad',
text: '¿Estas seguro de añadir a este transportista de la lista privada?',
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()
},
});
let resp = null;
setTimeout(() => {
resp = true
Swal.close();
if(resp != null) {
// contactsStore.removeContact(id);
existInPrivateList.value = true;
Swal.fire({
title: 'Transportista añadido',
text: 'Se ha añadido este transportista a la lista privada',
icon: "success"
});
} else {
Swal.fire({
title: t('errors.msgTitleNotDel'),
text: 'No se pudo completar la añadir este transportista a la lista privada',
icon: "error"
});
}
}, 500);
}
});
}
const { t } = useI18n()
</script>
@@ -30,9 +80,18 @@
<p><span>{{ t('labels.infoCompany') }}: </span>{{company.company_description}}</p>
</div>
</div>
<div class="d-flex justify-content-end">
<div
class="d-flex justify-content-end gap-2"
v-if="authStore.user?.permissions === 'role_shipper'">
<button
v-if="!existInPrivateList"
class="btn-primary-sm bg-dark radius-sm"
@click="handleAddPrivateList"
>
Añadir a lista privada
</button>
<RouterLink
class="btn-primary-sm"
class="btn-primary-sm radius-sm"
:to="{name: 'public-users', params: {id: company._id}}"
>{{ t('buttons.profile') }}</RouterLink>
</div>