ADD: Mejora en la gestión de direcciones y conceptos, incluyendo validaciones y traducciones

This commit is contained in:
Juan Felipe Zapata Moreno 2025-11-18 21:14:29 -06:00
parent 740f7f5b78
commit 7f82f57588
8 changed files with 421 additions and 118 deletions

View File

@ -11,7 +11,5 @@ COPY . .
COPY install.sh /usr/local/bin/install.sh COPY install.sh /usr/local/bin/install.sh
RUN chmod +x /usr/local/bin/install.sh RUN chmod +x /usr/local/bin/install.sh
EXPOSE 3000
ENTRYPOINT ["/usr/local/bin/install.sh"] ENTRYPOINT ["/usr/local/bin/install.sh"]
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

View File

@ -10,15 +10,15 @@ const addressName = ref("");
/** Métodos */ /** Métodos */
const handleSubmit = () => { const handleSubmit = () => {
if (!addressName.value.trim()) { if (!addressName.value.trim()) {
alert("Por favor ingresa un nombre de dirección"); window.Notify.warning(window.Lang('address.validation.required'));
return; return;
} }
// Emitir evento al padre con la nueva dirección // Emitir evento al padre con la nueva dirección
emit('address-created', { emit('address-created', {
name: addressName.value name: addressName.value
}); });
// Limpiar el formulario // Limpiar el formulario
addressName.value = ""; addressName.value = "";
}; };
@ -27,7 +27,7 @@ const handleSubmit = () => {
<template> <template>
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-10"> <div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-10">
<h3 class="text-xl font-semibold mb-4 text-gray-800"> <h3 class="text-xl font-semibold mb-4 text-gray-800">
Crear nueva dirección {{ $t('address.create.title') }}
</h3> </h3>
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
@ -36,12 +36,13 @@ const handleSubmit = () => {
for="addressName" for="addressName"
class="block text-sm text-gray-600 font-medium mb-2" class="block text-sm text-gray-600 font-medium mb-2"
> >
Nombre de la Dirección: {{ $t('address.name') }}:
</label> </label>
<input <input
type="text" type="text"
id="adddressName" id="addressName"
v-model="addressName" v-model="addressName"
:placeholder="$t('address.placeholder')"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
@ -50,7 +51,7 @@ const handleSubmit = () => {
type="submit" type="submit"
class="w-1/4 bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors" class="w-1/4 bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors"
> >
Guardar {{ $t('save') }}
</button> </button>
</div> </div>
</form> </form>

View File

@ -1,136 +1,194 @@
<script setup> <script setup>
import { ref } from "vue"; import { onMounted, ref } from "vue";
import { useForm } from "@Services/Api"; import { useForm, api, apiURL } from "@Services/Api";
import Input from "@Holos/Form/Input.vue"; import Input from "@Holos/Form/Input.vue";
import Textarea from "@Holos/Form/Textarea.vue"; import Textarea from "@Holos/Form/Textarea.vue";
import Selectable from "@Holos/Form/Selectable.vue"; import Selectable from "@Holos/Form/Selectable.vue";
/** Eventos */
const emit = defineEmits(['concept-created']);
const form = useForm({ const form = useForm({
address: [], direction_id: null,
shortName: "", unit_id: null,
short_name: "",
name: "", name: "",
legal: "", legal_instrument: "",
article: "", article: "",
description: "", content: "",
minimumAmount: "", min_amount_uma: "",
maximumAmount: "", max_amount_uma: "",
sizeUnit: [], min_amount_peso: "",
max_amount_peso: "",
unit_cost_uma: "",
unit_cost_peso: "",
}); });
const addresses = ref([]); const addresses = ref([]);
const sizeUnit = ref([]); const units = ref([]);
/** Cargar cosas */
onMounted(async () => {
api.get(apiURL('directions'), {
onSuccess: (data) => {
addresses.value = data.models?.data || data.data || [];
}
});
api.get(apiURL('units'), {
onSuccess: (data) => {
units.value = data.models?.data || data.data || [];
}
});
});
/** Métodos */
const handleSubmit = () => {
const transformedData = {
direction_id: typeof modelModal.value.direction === 'object' ? modelModal.value.direction.id : Number(modelModal.value.direction_id),
unit_id: modelModal.value.unit ? (typeof modelModal.value.unit === 'object' ? modelModal.value.unit.id : Number(modelModal.value.unit)) : null,
short_name: modelModal.value.short_name,
name: modelModal.value.name,
legal_instrument: modelModal.value.legal_instrument,
article: modelModal.value.article,
content: modelModal.value.content,
min_amount_uma: modelModal.value.min_amount_uma ? Number(modelModal.value.min_amount_uma) : null,
max_amount_uma: modelModal.value.max_amount_uma ? Number(modelModal.value.max_amount_uma) : null,
min_amount_peso: modelModal.value.min_amount_peso ? Number(modelModal.value.min_amount_peso) : null,
max_amount_peso: modelModal.value.max_amount_peso ? Number(modelModal.value.max_amount_peso) : null,
unit_cost_uma: form.unit_cost_uma ? Number(form.unit_cost_uma) : null,
unit_cost_peso: form.unit_cost_peso ? Number(form.unit_cost_peso) : null,
};
emit('concept-created', transformedData);
};
</script> </script>
<template> <template>
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-10"> <div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-10">
<h3 class="text-xl font-semibold mb-4 text-gray-800"> <h3 class="text-xl font-semibold mb-4 text-gray-800">
Crear nuevo concepto de cobro {{ $t('concept.create.title') }}
</h3> </h3>
<form> <form @submit.prevent="handleSubmit">
<div class="mb-5 grid grid-cols-2 gap-4"> <div class="mb-5 grid grid-cols-2 gap-4">
<Selectable <Selectable
v-model="form.address" v-model="form.direction"
label="description" label="name"
title="Direcciones" value="id"
:title="$t('concept.direction')"
:options="addresses" :options="addresses"
multiple
required required
/> />
<Input <Input
v-model="form.shortName" v-model="form.short_name"
class="col-span-2" class="col-span-2"
id="Nombre corto" :id="$t('concept.shortName')"
type="text" type="text"
:onError="form.errors.shortName" :onError="form.errors.short_name"
required required
/> />
</div> </div>
<Input <Input
v-model="form.name" v-model="form.name"
class="col-span-2" class="col-span-2"
id="Nombre" :id="$t('concept.name')"
type="text" type="text"
:onError="form.errors.name" :onError="form.errors.name"
required required
/> />
<div class="mb-5 grid grid-cols-2 gap-4 py-2"> <div class="mb-5 grid grid-cols-2 gap-4 py-2">
<Input <Input
v-model="form.legal" v-model="form.legal_instrument"
class="col-span-2" class="col-span-2"
id="Instrumento Legal" :id="$t('concept.legal')"
type="text" type="text"
:onError="form.errors.legal" :onError="form.errors.legal_instrument"
required required
/> />
<Input <Input
v-model="form.article" v-model="form.article"
class="col-span-2" class="col-span-2"
id="Articulado" :id="$t('concept.article')"
type="text" type="text"
:onError="form.errors.article" :onError="form.errors.article"
required required
/> />
</div> </div>
<Textarea <Textarea
v-model="form.description" v-model="form.content"
class="col-span-2" class="col-span-2"
id="Descripción" :id="$t('concept.description')"
:onError="form.errors.description" :onError="form.errors.content"
required required
/> />
<div class="mb-5 grid grid-cols-2 gap-4 py-2"> <div class="mb-5 grid grid-cols-2 gap-4 py-2">
<Input <Input
v-model="form.minimumAmount" v-model="form.min_amount_uma"
class="col-span-2" :id="$t('concept.minimumAmountUma')"
id="Monto mínimo (UMAs)" type="number"
type="text" step="0.01"
:onError="form.errors.minimumAmount" :onError="form.errors.min_amount_uma"
required
/> />
<Input <Input
v-model="form.maximumAmount" v-model="form.max_amount_uma"
class="col-span-2" :id="$t('concept.maximumAmountUma')"
id="Monto máximo (UMAs)" type="number"
type="text" step="0.01"
:onError="form.errors.maximumAmount" :onError="form.errors.max_amount_uma"
required />
</div>
<div class="mb-5 grid grid-cols-2 gap-4 py-2">
<Input
v-model="form.min_amount_peso"
:id="$t('concept.minimumAmountPeso')"
type="number"
step="0.01"
:onError="form.errors.min_amount_peso"
/>
<Input
v-model="form.max_amount_peso"
:id="$t('concept.maximumAmountPeso')"
type="number"
step="0.01"
:onError="form.errors.max_amount_peso"
/> />
</div> </div>
<hr class="my-4 border-gray-300" /> <hr class="my-4 border-gray-300" />
<h4 class="text-lg font-semibold mb-4 text-gray-700">
{{ $t('concept.tabulator') }}
</h4>
<div class="mb-5 grid grid-cols-3 gap-4 py-2"> <div class="mb-5 grid grid-cols-3 gap-4 py-2">
<Selectable <Selectable
v-model="form.sizeUnit" v-model="form.unit"
label="description" label="name"
title="Unidad de medida" value="id"
:options="sizeUnit" :title="$t('concept.sizeUnit')"
multiple :options="units"
required
/> />
<Input <Input
v-model="form.costMin" v-model="form.unit_cost_uma"
class="col-span-2" :id="$t('concept.costUnitUma')"
id="Costo mínimo (UMAs)" type="number"
type="text" step="0.01"
:onError="form.errors.costMin" :onError="form.errors.unit_cost_uma"
required
/> />
<Input <Input
v-model="form.costMax" v-model="form.unit_cost_peso"
class="col-span-2" :id="$t('concept.costUnitPeso')"
id="Costo máximo (UMAs)" type="number"
type="text" step="0.01"
:onError="form.errors.costMax" :onError="form.errors.unit_cost_peso"
required />
/>
</div> </div>
<div class="mb-3 p-7 flex justify-center"> <div class="mb-3 p-7 flex justify-center">
<button <button
type="submit" type="submit"
class="w-1/4 bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors" :disabled="form.processing"
class="w-1/4 bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors disabled:opacity-50"
> >
Guardar {{ form.processing ? 'Guardando...' : $t('save') }}
</button> </button>
</div> </div>
</form> </form>

View File

@ -82,8 +82,13 @@ export default {
}, },
address: { address: {
title: 'Direcciones', title: 'Direcciones',
name: 'Nombre de la Dirección',
placeholder: 'Ingrese el nombre de la dirección',
validation: {
required: 'Por favor ingresa un nombre de dirección'
},
create: { create: {
title: 'Crear dirección', title: 'Crear nueva dirección',
description: 'Permite crear nuevas direcciones.', description: 'Permite crear nuevas direcciones.',
onSuccess: 'Dirección creada exitosamente', onSuccess: 'Dirección creada exitosamente',
onError: 'Error al crear la dirección' onError: 'Error al crear la dirección'
@ -94,11 +99,55 @@ export default {
onSuccess: 'Dirección actualizada exitosamente', onSuccess: 'Dirección actualizada exitosamente',
onError: 'Error al actualizar la dirección' onError: 'Error al actualizar la dirección'
}, },
list: {
title: 'Listado de Direcciones',
empty: 'No hay direcciones registradas'
},
deleted: 'Dirección eliminada', deleted: 'Dirección eliminada',
permissions: { permissions: {
title: 'Permisos de dirección' title: 'Permisos de dirección'
} }
}, },
concept: {
title: 'Conceptos de Cobro',
direction: 'Dirección',
shortName: 'Nombre Corto',
name: 'Nombre',
legal: 'Instrumento Legal',
article: 'Articulado',
description: 'Contenido',
minimumAmountUma: 'Monto Mínimo (UMAs)',
maximumAmountUma: 'Monto Máximo (UMAs)',
minimumAmountPeso: 'Monto Mínimo (Pesos)',
maximumAmountPeso: 'Monto Máximo (Pesos)',
tabulator: 'Tabulador',
sizeUnit: 'Unidad de Medida',
costUnitUma: 'Costo Unitario (UMAs)',
costUnitPeso: 'Costo Unitario (Pesos)',
validation: {
required: 'Por favor complete todos los campos requeridos'
},
create: {
title: 'Nuevo Concepto de Cobro',
description: 'Permite crear nuevos conceptos de cobro.',
onSuccess: 'Concepto creado exitosamente',
onError: 'Error al crear el concepto'
},
edit: {
title: 'Editar concepto',
description: 'Permite editar conceptos existentes.',
onSuccess: 'Concepto actualizado exitosamente',
onError: 'Error al actualizar el concepto'
},
list: {
title: 'Listado de Conceptos',
empty: 'No hay conceptos registrados'
},
deleted: 'Concepto eliminado',
permissions: {
title: 'Permisos de concepto'
}
},
membership: { membership: {
title: 'Membresías', title: 'Membresías',
create: { create: {

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { useSearcher, useForm } from "@Services/Api"; import { useSearcher, useForm } from "@Services/Api";
import { can, apiTo, viewTo, transl } from "./Module"; import { apiTo, transl } from "./Module";
import Table from "@Holos/Table.vue"; import Table from "@Holos/Table.vue";
import IconButton from "@Holos/Button/Icon.vue"; import IconButton from "@Holos/Button/Icon.vue";
@ -45,8 +45,12 @@ const handleAddressCreated = async (address) => {
form.post(apiTo("store"), { form.post(apiTo("store"), {
onSuccess: () => { onSuccess: () => {
Notify.success(transl('create.onSuccess'));
searcher.refresh(); searcher.refresh();
}, },
onError: () => {
Notify.error(transl('create.onError'));
}
}); });
}; };
@ -55,17 +59,21 @@ const handleAddressUpdated = async () => {
name: modelModal.value.name, name: modelModal.value.name,
}); });
form.put(apiTo("update", { id: modelModal.value.id }), { form.put(apiTo("update", { direction: modelModal.value.id }), {
onSuccess: () => { onSuccess: () => {
Notify.success(transl('edit.onSuccess'));
searcher.refresh(); searcher.refresh();
Modal.switchEditModal(); Modal.switchEditModal();
}, },
onError: () => {
Notify.error(transl('edit.onError'));
}
}); });
}; };
</script> </script>
<template> <template>
<PageHeader title="Creación de Dirección" /> <PageHeader :title="transl('title')" />
<AddressForm @address-created="handleAddressCreated" /> <AddressForm @address-created="handleAddressCreated" />
<div class="m-3"> <div class="m-3">
<Table <Table
@ -102,7 +110,7 @@ const handleAddressUpdated = async () => {
</template> </template>
<template #empty> <template #empty>
<td colspan="2" class="table-cell text-center py-4 text-gray-500"> <td colspan="2" class="table-cell text-center py-4 text-gray-500">
No hay direcciones registradas {{ transl('list.empty') }}
</td> </td>
</template> </template>
</Table> </Table>
@ -111,7 +119,7 @@ const handleAddressUpdated = async () => {
subtitle="" subtitle=""
:model="modelModal" :model="modelModal"
:show="destroyModal" :show="destroyModal"
:to="(address) => apiTo('destroy', { id: address.id })" :to="(id) => apiTo('destroy', { direction: id })"
@close="Modal.switchDestroyModal" @close="Modal.switchDestroyModal"
@update=" @update="
() => { () => {
@ -125,7 +133,7 @@ const handleAddressUpdated = async () => {
/> />
<Editview <Editview
:show="editModal" :show="editModal"
:title="$t('crud.edit')" :title="transl('edit.title')"
@close="Modal.switchEditModal" @close="Modal.switchEditModal"
@update="handleAddressUpdated" @update="handleAddressUpdated"
> >
@ -134,12 +142,13 @@ const handleAddressUpdated = async () => {
for="editAddressName" for="editAddressName"
class="block text-sm text-gray-600 font-medium mb-2" class="block text-sm text-gray-600 font-medium mb-2"
> >
Nombre de la Dirección: {{ transl('name') }}:
</label> </label>
<input <input
type="text" type="text"
id="editAddressName" id="editAddressName"
v-model="modelModal.name" v-model="modelModal.name"
:placeholder="transl('placeholder')"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>

View File

@ -1,8 +1,7 @@
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { RouterLink } from "vue-router"; import { useSearcher, useForm, api, apiURL } from "@Services/Api";
import { useSearcher } from "@Services/Api"; import { apiTo, transl } from "./Module";
import { can, apiTo, viewTo, transl } from "./Module";
import Table from "@Holos/Table.vue"; import Table from "@Holos/Table.vue";
import IconButton from "@Holos/Button/Icon.vue"; import IconButton from "@Holos/Button/Icon.vue";
@ -10,6 +9,10 @@ import PageHeader from "@Holos/PageHeader.vue";
import ConceptForm from "@App/ConceptSection.vue"; import ConceptForm from "@App/ConceptSection.vue";
import DestroyView from "@Holos/Modal/Template/Destroy.vue"; import DestroyView from "@Holos/Modal/Template/Destroy.vue";
import ModalController from "@Controllers/ModalController.js"; import ModalController from "@Controllers/ModalController.js";
import Editview from "@Holos/Modal/Edit.vue";
import Input from "@Holos/Form/Input.vue";
import Textarea from "@Holos/Form/Textarea.vue";
import Selectable from "@Holos/Form/Selectable.vue";
/** Controladores */ /** Controladores */
const Modal = new ModalController(); const Modal = new ModalController();
@ -19,67 +22,133 @@ const destroyModal = ref(Modal.destroyModal);
const editModal = ref(Modal.editModal); const editModal = ref(Modal.editModal);
const modelModal = ref(Modal.modelModal); const modelModal = ref(Modal.modelModal);
/** Inicializar searcher */ const addresses = ref([]);
//const models = ref(searcher.models); const units = ref([]);
const models = ref({ const models = ref({
data: [], data: [],
total: 0, total: 0,
}); });
const processing = ref(false);
//const searcher = useSearcher(apiTo("index")); /** Inicializar searcher */
const searcher = { const searcher = useSearcher({
models, url: apiTo("index"),
processing, filters: {},
pagination: () => {}, onSuccess: (data) => {
search: () => {}, models.value = data.models;
}; },
});
api.get(apiURL("directions"), {
onSuccess: (data) => {
addresses.value = data.models?.data || data.data || [];
},
});
api.get(apiURL("units"), {
onSuccess: (data) => {
units.value = data.models?.data || data.data || [];
},
});
/** Cargar datos iniciales */ /** Cargar datos iniciales */
onMounted(() => { onMounted(() => {
searcher.search(); searcher.search();
}); });
/** crear concepto */
const handleConceptCreated = async (conceptData) => {
const form = useForm(conceptData);
form.post(apiTo("store"), {
onSuccess: () => {
Notify.success(transl("create.onSuccess"));
searcher.refresh();
},
onError: () => {
Notify.error(transl("create.onError"));
},
});
};
const handleConceptUpdated = async () => {
const transformedData = {
direction_id: typeof modelModal.value.direction === 'object' ? modelModal.value.direction.id : Number(modelModal.value.direction_id),
unit_id: modelModal.value.unit ? (typeof modelModal.value.unit === 'object' ? modelModal.value.unit.id : Number(modelModal.value.unit)) : null,
short_name: modelModal.value.short_name,
name: modelModal.value.name,
legal_instrument: modelModal.value.legal_instrument,
article: modelModal.value.article,
content: modelModal.value.content,
min_amount_uma: modelModal.value.min_amount_uma ? Number(modelModal.value.min_amount_uma) : null,
max_amount_uma: modelModal.value.max_amount_uma ? Number(modelModal.value.max_amount_uma) : null,
min_amount_peso: modelModal.value.min_amount_peso ? Number(modelModal.value.min_amount_peso) : null,
max_amount_peso: modelModal.value.max_amount_peso ? Number(modelModal.value.max_amount_peso) : null,
unit_cost_uma: modelModal.value.unit_cost_uma ? Number(modelModal.value.unit_cost_uma) : null,
unit_cost_peso: modelModal.value.unit_cost_peso ? Number(modelModal.value.unit_cost_peso) : null,
};
const form = useForm(transformedData);
form.put(apiTo("update", { charge_concept: modelModal.value.id }), {
onSuccess: () => {
Notify.success(transl("edit.onSuccess"));
searcher.refresh();
Modal.switchEditModal();
},
onError: () => {
Notify.error(transl("edit.onError"));
},
});
};
</script> </script>
<template> <template>
<PageHeader title="Creación de Concepto" /> <PageHeader :title="transl('title')" />
<ConceptForm /> <ConceptForm @concept-created="handleConceptCreated" />
<div class="m-3"> <div class="m-3">
<Table <Table
:items="models" :items="models"
:processing="searcher.processing.value" :processing="searcher.processing"
@send-pagination="(page) => searcher.pagination(page)" @send-pagination="(page) => searcher.pagination(page)"
> >
<template #head> <template #head>
<th v-text="'Dirección'" /> <th v-text="transl('direction')" />
<th v-text="'Nombre corto'" /> <th v-text="transl('shortName')" />
<th v-text="'Nombre'" /> <th v-text="transl('name')" />
<th v-text="'Instrumento Legal'" /> <th v-text="transl('legal')" />
<th v-text="'Articulado'" /> <th v-text="transl('article')" />
<th v-text="'Descripción'" /> <th v-text="transl('description')" />
<th v-text="$t('actions')" class="w-32 text-center" /> <th v-text="$t('actions')" class="w-32 text-center" />
</template> </template>
<template #body="{ items }"> <template #body="{ items }">
<tr v-for="model in items" :key="model.id" class="table-row"> <tr v-for="model in items" :key="model.id" class="table-row">
<td class="table-cell border"> <td class="table-cell border">
{{ model.description }} {{ model.direction?.name || "-" }}
</td>
<td class="table-cell border">
{{ model.short_name }}
</td>
<td class="table-cell border">
{{ model.name }}
</td>
<td class="table-cell border">
{{ model.legal_instrument }}
</td>
<td class="table-cell border">
{{ model.article }}
</td>
<td class="table-cell border">
{{ model.content }}
</td> </td>
<td class="table-cell"> <td class="table-cell">
<div class="table-actions"> <div class="table-actions">
<IconButton <IconButton
v-if="can('edit') && ![1, 2].includes(model.id)" icon="edit"
icon="license" :title="$t('crud.edit')"
:title="transl('permissions.title')"
@click="Modal.switchEditModal(model)" @click="Modal.switchEditModal(model)"
outline outline
/> />
<RouterLink
v-if="can('edit')"
class="h-fit"
:to="viewTo({ name: 'edit', params: { id: model.id } })"
>
<IconButton icon="edit" :title="$t('crud.edit')" outline />
</RouterLink>
<IconButton <IconButton
v-if="can('destroy') && ![1, 2].includes(model.id)"
icon="delete" icon="delete"
:title="$t('crud.destroy')" :title="$t('crud.destroy')"
@click="Modal.switchDestroyModal(model)" @click="Modal.switchDestroyModal(model)"
@ -90,18 +159,17 @@ onMounted(() => {
</tr> </tr>
</template> </template>
<template #empty> <template #empty>
<td colspan="2" class="table-cell text-center py-4 text-gray-500"> <td colspan="7" class="table-cell text-center py-4 text-gray-500">
No hay direcciones registradas {{ transl("list.empty") }}
</td> </td>
</template> </template>
</Table> </Table>
<DestroyView <DestroyView
v-if="can('destroy')" title="name"
title="description" subtitle="content"
subtitle=""
:model="modelModal" :model="modelModal"
:show="destroyModal" :show="destroyModal"
:to="(address) => apiTo('destroy', { address: address.id })" :to="(id) => apiTo('destroy', { charge_concept: id })"
@close="Modal.switchDestroyModal" @close="Modal.switchDestroyModal"
@update=" @update="
() => { () => {
@ -113,5 +181,104 @@ onMounted(() => {
} }
" "
/> />
<Editview
:show="editModal"
:title="transl('edit.title')"
@close="Modal.switchEditModal"
@update="handleConceptUpdated"
>
<div class="p-4 space-y-4">
<Selectable
v-model="modelModal.direction"
label="name"
value="id"
:title="$t('concept.direction')"
:options="addresses"
required
/>
<Input
v-model="modelModal.short_name"
:id="$t('concept.shortName')"
type="text"
required
/>
<Input
v-model="modelModal.name"
:id="$t('concept.name')"
type="text"
required
/>
<Input
v-model="modelModal.legal_instrument"
:id="$t('concept.legal')"
type="text"
required
/>
<Input
v-model="modelModal.article"
:id="$t('concept.article')"
type="text"
required
/>
<Textarea
v-model="modelModal.content"
:id="$t('concept.description')"
required
/>
<div class="grid grid-cols-2 gap-4">
<Input
v-model="modelModal.min_amount_uma"
:id="$t('concept.minimumAmountUma')"
type="number"
step="0.01"
/>
<Input
v-model="modelModal.max_amount_uma"
:id="$t('concept.maximumAmountUma')"
type="number"
step="0.01"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<Input
v-model="modelModal.min_amount_peso"
:id="$t('concept.minimumAmountPeso')"
type="number"
step="0.01"
/>
<Input
v-model="modelModal.max_amount_peso"
:id="$t('concept.maximumAmountPeso')"
type="number"
step="0.01"
/>
</div>
<hr class="my-4 border-gray-300" />
<h4 class="text-lg font-semibold mb-2 text-gray-700">
{{ $t('concept.tabulator') }}
</h4>
<Selectable
v-model="modelModal.unit"
label="name"
value="id"
:title="$t('concept.sizeUnit')"
:options="units"
/>
<div class="grid grid-cols-2 gap-4">
<Input
v-model="modelModal.unit_cost_uma"
:id="$t('concept.costUnitUma')"
type="number"
step="0.01"
/>
<Input
v-model="modelModal.unit_cost_peso"
:id="$t('concept.costUnitPeso')"
type="number"
step="0.01"
/>
</div>
</div>
</Editview>
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@ import { lang } from '@Lang/i18n';
import { hasPermission } from '@Plugins/RolePermission.js'; import { hasPermission } from '@Plugins/RolePermission.js';
// Ruta API // Ruta API
const apiTo = (name, params = {}) => route(`concept.${name}`, params) const apiTo = (name, params = {}) => route(`charge-concepts.${name}`, params)
// Ruta visual // Ruta visual
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `concept.${name}`, params, query }) const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `concept.${name}`, params, query })

View File

@ -0,0 +1,21 @@
import { lang } from '@Lang/i18n';
import { hasPermission } from '@Plugins/RolePermission.js';
// Ruta API
const apiTo = (name, params = {}) => route(`fines.${name}`, params)
// Ruta visual
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `fine.${name}`, params, query })
// Obtener traducción del componente
const transl = (str) => lang(`fine.${str}`)
// Determina si un usuario puede hacer algo no en base a los permisos
const can = (permission) => hasPermission(`fine.${permission}`)
export {
can,
viewTo,
apiTo,
transl
}