Cambios #2
@ -1,10 +1,10 @@
|
||||
VITE_APP_NAME=GOLSCONTROL
|
||||
VITE_APP_NAME=Ayuntamiento de Comalcalco
|
||||
VITE_APP_URL=http://frontend.golscontrol.test
|
||||
VITE_APP_LANG=es-MX
|
||||
VITE_APP_PORT=3000
|
||||
VITE_PAGINATION=25
|
||||
|
||||
VITE_APP_API=http://localhost:8080 #Colocar http://localhost:8080 con el puerto del backend / http://backend.golscontrol.test
|
||||
VITE_API_URL=http://localhost:8080
|
||||
VITE_APP_API_SECURE=false
|
||||
|
||||
VITE_MICROSERVICE_STOCK=http://localhost:3000/api
|
||||
|
||||
@ -3,12 +3,12 @@
|
||||
--color-page-t: #000;
|
||||
--color-page-d: #292524;
|
||||
--color-page-dt: #fff;
|
||||
--color-primary: #374151;
|
||||
--color-primary: #621132;
|
||||
--color-primary-t: #fff;
|
||||
--color-primary-d: #1c1917;
|
||||
--color-primary-dt: #fff;
|
||||
--color-secondary: #3b82f6;
|
||||
--color-secondary-t: #fff;
|
||||
--color-secondary: #fff;
|
||||
--color-secondary-t: #621132;
|
||||
--color-secondary-d: #312e81;
|
||||
--color-secondary-dt: #fff;
|
||||
--color-primary-info: #06b6d4;
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
services:
|
||||
holos-frontend:
|
||||
comal-pagos-frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: dockerfile
|
||||
ports:
|
||||
- "${APP_PORT}:5173"
|
||||
volumes:
|
||||
- .:/var/www/holos.frontend
|
||||
- /var/www/holos.frontend/node_modules
|
||||
- .:/var/www/comal-pagos.frontend
|
||||
- /var/www/comal-pagos.frontend/node_modules
|
||||
networks:
|
||||
- holos-network
|
||||
- comal-pagos-network
|
||||
|
||||
networks:
|
||||
holos-network:
|
||||
comal-pagos-network:
|
||||
driver: bridge
|
||||
@ -1,6 +1,6 @@
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
WORKDIR /var/www/holos.frontend
|
||||
WORKDIR /var/www/comal-pagos.frontend
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
@ -11,7 +11,5 @@ COPY . .
|
||||
COPY install.sh /usr/local/bin/install.sh
|
||||
RUN chmod +x /usr/local/bin/install.sh
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/install.sh"]
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
@ -2,7 +2,7 @@
|
||||
<html id="main-page" lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Holos</title>
|
||||
</head>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
#! /bin/bash
|
||||
#! /bin/sh
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
fi
|
||||
|
||||
if [ ! -f colors.json ]; then
|
||||
cp colors.json.example colors.json
|
||||
if [ ! -f colors.css ]; then
|
||||
cp colors.css.example colors.css
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
64
package-lock.json
generated
64
package-lock.json
generated
@ -23,6 +23,7 @@
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-multiselect": "^3.2.0",
|
||||
"vue-qrcode-reader": "^5.7.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"ziggy-js": "^2.5.2"
|
||||
},
|
||||
@ -1224,6 +1225,18 @@
|
||||
"vite": "^5.2.0 || ^6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dom-webcodecs": {
|
||||
"version": "0.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.17.tgz",
|
||||
"integrity": "sha512-IwKW5uKL0Zrv5ccUJpjIlqf7ppk2v29l/ZLQxLlwHxljBfnDD9Gxm+hzMkGM0AOAL/21H0pp7cTUYLiiVUGchA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/emscripten": {
|
||||
"version": "1.41.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
|
||||
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
@ -1492,6 +1505,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/barcode-detector": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.2.2.tgz",
|
||||
"integrity": "sha512-JcSekql+EV93evfzF9zBr+Y6aRfkR+QFvgyzbwQ0dbymZXoAI9+WgT7H1E429f+3RKNncHz2CW98VQtaaKpmfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/dom-webcodecs": "^0.1.11",
|
||||
"zxing-wasm": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz",
|
||||
@ -3230,6 +3253,12 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sdp": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz",
|
||||
"integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
@ -3641,6 +3670,19 @@
|
||||
"npm": ">= 6.14.15"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-qrcode-reader": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.7.3.tgz",
|
||||
"integrity": "sha512-iSGko42FsEvdHyizBMBs/X+HMO9Z5ONDxjW+mQdoraOR5emRNedmjC5SEJdYzGz8ZP5ME3lwB4iHy3S7MOt5Qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"barcode-detector": "2.2.2",
|
||||
"webrtc-adapter": "8.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
@ -3662,6 +3704,19 @@
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
|
||||
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"sdp": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0",
|
||||
"npm": ">=3.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
@ -3711,6 +3766,15 @@
|
||||
"@types/qs": "^6.9.17",
|
||||
"qs": "~6.9.7"
|
||||
}
|
||||
},
|
||||
"node_modules/zxing-wasm": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.1.3.tgz",
|
||||
"integrity": "sha512-MYm9k/5YVs4ZOTIFwlRjfFKD0crhefgbnt1+6TEpmKUDFp3E2uwqGSKwQOd2hOIsta/7Usq4hnpNRYTLoljnfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/emscripten": "^1.39.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "notsoweb.frontend",
|
||||
"copyright": "Notsoweb Software Inc.",
|
||||
"copyright": "Golsystems",
|
||||
"private": true,
|
||||
"version": "0.9.10",
|
||||
"type": "module",
|
||||
@ -25,6 +25,7 @@
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-multiselect": "^3.2.0",
|
||||
"vue-qrcode-reader": "^5.7.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"ziggy-js": "^2.5.2"
|
||||
},
|
||||
|
||||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
59
src/components/App/AddressSection.vue
Normal file
59
src/components/App/AddressSection.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(['address-created']);
|
||||
|
||||
/** Refs */
|
||||
const addressName = ref("");
|
||||
|
||||
/** Métodos */
|
||||
const handleSubmit = () => {
|
||||
if (!addressName.value.trim()) {
|
||||
window.Notify.warning(window.Lang('address.validation.required'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Emitir evento al padre con la nueva dirección
|
||||
emit('address-created', {
|
||||
name: addressName.value
|
||||
});
|
||||
|
||||
// Limpiar el formulario
|
||||
addressName.value = "";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
{{ $t('address.create.title') }}
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="mb-5">
|
||||
<label
|
||||
for="addressName"
|
||||
class="block text-sm text-gray-600 font-medium mb-2"
|
||||
>
|
||||
{{ $t('address.name') }}:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="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"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-1/4 bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors"
|
||||
>
|
||||
{{ $t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
114
src/components/App/CheckoutDelivery.vue
Normal file
114
src/components/App/CheckoutDelivery.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
import Input from "@Holos/Form/Input.vue";
|
||||
import QRscan from "./QRscan.vue";
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(["fine-searched"]);
|
||||
|
||||
/** Refs */
|
||||
const numero_entrega = ref("");
|
||||
const fineData = ref({
|
||||
fecha: "",
|
||||
numero_entrega: "",
|
||||
nombre: "",
|
||||
monto_entrega: "",
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
const handleSearch = () => {
|
||||
if (!numero_entrega.value.trim()) {
|
||||
Notify.warning("Por favor ingresa un número de entrega");
|
||||
return;
|
||||
}
|
||||
|
||||
fineData.value = {
|
||||
fecha: "2025-11-11",
|
||||
numero_entrega: numero_entrega.value,
|
||||
nombre: "Juan Pérez García",
|
||||
monto_entrega: "$1,500.00",
|
||||
};
|
||||
|
||||
emit("fine-searched", fineData.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6 p-6">
|
||||
<div class="bg-white rounded-xl p-6 shadow-lg">
|
||||
<h3 class="text-xl font-semibold mb-4 text-gray-800">Buscar Multa</h3>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-600 font-medium mb-2">
|
||||
Escanear Código QR
|
||||
</label>
|
||||
<div
|
||||
class="w-full h-64 bg-gray-800 rounded-lg overflow-hidden"
|
||||
>
|
||||
<!-- QR -->
|
||||
<QRscan
|
||||
v-model:numero_entrega="numero_entrega"
|
||||
@fine-searched="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="mb-5">
|
||||
<Input
|
||||
v-model="numero_entrega"
|
||||
id="Número de entrega"
|
||||
type="text"
|
||||
placeholder="Ingrese el número de entrega"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleSearch"
|
||||
type="button"
|
||||
class="w-full bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors"
|
||||
>
|
||||
Buscar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 shadow-lg">
|
||||
<form @submit.prevent="handlePayment">
|
||||
<!-- Fecha -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input
|
||||
v-model="fineData.fecha"
|
||||
id="Fecha de entrega"
|
||||
type="date"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nombre -->
|
||||
<div class="mb-5">
|
||||
<Input v-model="fineData.nombre" id="Nombre del cajero" type="text"/>
|
||||
</div>
|
||||
|
||||
<!-- Monto a Pagar -->
|
||||
<div class="mb-5">
|
||||
<Input
|
||||
v-model="fineData.monto_entrega"
|
||||
id="Monto a entregar"
|
||||
type="text"
|
||||
class="text-lg font-semibold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Botón Cobrar -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-green-700 hover:bg-green-600 text-white font-medium py-3.5 rounded-lg transition-colors"
|
||||
>
|
||||
Entregar caja
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
196
src/components/App/ConceptSection.vue
Normal file
196
src/components/App/ConceptSection.vue
Normal file
@ -0,0 +1,196 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useForm, api, apiURL } from "@Services/Api";
|
||||
|
||||
import Input from "@Holos/Form/Input.vue";
|
||||
import Textarea from "@Holos/Form/Textarea.vue";
|
||||
import Selectable from "@Holos/Form/Selectable.vue";
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(['concept-created']);
|
||||
|
||||
const form = useForm({
|
||||
direction_id: null,
|
||||
unit_id: null,
|
||||
short_name: "",
|
||||
name: "",
|
||||
legal_instrument: "",
|
||||
article: "",
|
||||
content: "",
|
||||
min_amount_uma: "",
|
||||
max_amount_uma: "",
|
||||
min_amount_peso: "",
|
||||
max_amount_peso: "",
|
||||
unit_cost_uma: "",
|
||||
unit_cost_peso: "",
|
||||
});
|
||||
|
||||
const addresses = 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 form.direction === 'object' ? form.direction.id : Number(form.direction_id),
|
||||
unit_id: form.unit ? (typeof form.unit === 'object' ? form.unit.id : Number(form.unit)) : null,
|
||||
short_name: form.short_name,
|
||||
name: form.name,
|
||||
legal_instrument: form.legal_instrument,
|
||||
article: form.article,
|
||||
content: form.content,
|
||||
min_amount_uma: form.min_amount_uma ? Number(form.min_amount_uma) : null,
|
||||
max_amount_uma: form.max_amount_uma ? Number(form.max_amount_uma) : null,
|
||||
min_amount_peso: form.min_amount_peso ? Number(form.min_amount_peso) : null,
|
||||
max_amount_peso: form.max_amount_peso ? Number(form.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>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
{{ $t('concept.create.title') }}
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="mb-5 grid grid-cols-2 gap-4">
|
||||
<Selectable
|
||||
v-model="form.direction"
|
||||
label="name"
|
||||
value="id"
|
||||
:title="$t('concept.direction')"
|
||||
:options="addresses"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
v-model="form.short_name"
|
||||
class="col-span-2"
|
||||
:id="$t('concept.shortName')"
|
||||
type="text"
|
||||
:onError="form.errors.short_name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
v-model="form.name"
|
||||
class="col-span-2"
|
||||
:id="$t('concept.name')"
|
||||
type="text"
|
||||
:onError="form.errors.name"
|
||||
required
|
||||
/>
|
||||
<div class="mb-5 grid grid-cols-2 gap-4 py-2">
|
||||
<Input
|
||||
v-model="form.legal_instrument"
|
||||
class="col-span-2"
|
||||
:id="$t('concept.legal')"
|
||||
type="text"
|
||||
:onError="form.errors.legal_instrument"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
v-model="form.article"
|
||||
class="col-span-2"
|
||||
:id="$t('concept.article')"
|
||||
type="text"
|
||||
:onError="form.errors.article"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
v-model="form.content"
|
||||
class="col-span-2"
|
||||
:id="$t('concept.description')"
|
||||
:onError="form.errors.content"
|
||||
required
|
||||
/>
|
||||
<div class="mb-5 grid grid-cols-2 gap-4 py-2">
|
||||
<Input
|
||||
v-model="form.min_amount_uma"
|
||||
:id="$t('concept.minimumAmountUma')"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:onError="form.errors.min_amount_uma"
|
||||
/>
|
||||
<Input
|
||||
v-model="form.max_amount_uma"
|
||||
:id="$t('concept.maximumAmountUma')"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:onError="form.errors.max_amount_uma"
|
||||
/>
|
||||
</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>
|
||||
<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">
|
||||
<Selectable
|
||||
v-model="form.unit"
|
||||
label="name"
|
||||
value="id"
|
||||
:title="$t('concept.sizeUnit')"
|
||||
:options="units"
|
||||
/>
|
||||
<Input
|
||||
v-model="form.unit_cost_uma"
|
||||
:id="$t('concept.costUnitUma')"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:onError="form.errors.unit_cost_uma"
|
||||
/>
|
||||
<Input
|
||||
v-model="form.unit_cost_peso"
|
||||
:id="$t('concept.costUnitPeso')"
|
||||
type="number"
|
||||
step="0.01"
|
||||
:onError="form.errors.unit_cost_peso"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 p-7 flex justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
: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"
|
||||
>
|
||||
{{ form.processing ? 'Guardando...' : $t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
212
src/components/App/DiscountSection.vue
Normal file
212
src/components/App/DiscountSection.vue
Normal file
@ -0,0 +1,212 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import Input from "@Holos/Form/Input.vue";
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(['discount-authorized']);
|
||||
|
||||
/** Refs */
|
||||
const searchForm = ref({
|
||||
fineNumber: "",
|
||||
placa: "",
|
||||
curp: ""
|
||||
});
|
||||
|
||||
const discountData = ref({
|
||||
fecha: "",
|
||||
placa: "",
|
||||
vin: "",
|
||||
licencia: "",
|
||||
tarjeta: "",
|
||||
rfc: "",
|
||||
nombre: "",
|
||||
montoOriginal: "",
|
||||
pagoMinimo: "",
|
||||
nuevoMonto: ""
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
const handleSearch = () => {
|
||||
if (!searchForm.value.fineNumber && !searchForm.value.placa && !searchForm.value.curp) {
|
||||
alert("Por favor ingresa al menos un criterio de búsqueda");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Buscando multa:", searchForm.value);
|
||||
|
||||
// Simular datos encontrados (temporal)
|
||||
discountData.value = {
|
||||
fecha: "2025-11-11",
|
||||
placa: "ABC-123-DEF",
|
||||
vin: "1HGBH41JXMN109186",
|
||||
licencia: "LIC123456",
|
||||
tarjeta: "TC987654",
|
||||
rfc: "XAXX010101000",
|
||||
nombre: "Juan Pérez García",
|
||||
montoOriginal: "$1,500.00",
|
||||
pagoMinimo: "$750.00",
|
||||
nuevoMonto: ""
|
||||
};
|
||||
};
|
||||
|
||||
const handleAuthorize = () => {
|
||||
if (!discountData.value.nuevoMonto) {
|
||||
alert("Por favor ingresa el nuevo monto autorizado");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Autorizando descuento:", discountData.value);
|
||||
emit('discount-authorized', discountData.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<!-- Formulario de búsqueda -->
|
||||
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-6">
|
||||
<h3 class="text-xl font-semibold mb-6 text-gray-800">
|
||||
Buscar Multa para Autorización
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="handleSearch">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||||
<!-- Número de Multa -->
|
||||
<Input
|
||||
v-model="searchForm.fineNumber"
|
||||
id="Número de Multa"
|
||||
type="text"
|
||||
placeholder=""
|
||||
/>
|
||||
|
||||
<!-- Placa -->
|
||||
<Input
|
||||
v-model="searchForm.placa"
|
||||
id="Placa"
|
||||
type="text"
|
||||
placeholder=""
|
||||
/>
|
||||
|
||||
<!-- CURP -->
|
||||
<Input
|
||||
v-model="searchForm.curp"
|
||||
id="CURP"
|
||||
type="text"
|
||||
placeholder=""
|
||||
/>
|
||||
|
||||
<!-- Botón Buscar -->
|
||||
<button
|
||||
type="submit"
|
||||
class="py-3 px-8 rounded-lg transition-colors h-[42px] bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium"
|
||||
>
|
||||
Buscar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Detalles para Autorización -->
|
||||
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg">
|
||||
<h3 class="text-xl font-semibold mb-6 text-gray-800">
|
||||
Detalles para Autorización
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="handleAuthorize">
|
||||
<!-- Fecha y Placa -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input
|
||||
v-model="discountData.fecha"
|
||||
id="Fecha de Multa"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
v-model="discountData.placa"
|
||||
id="Placa"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- VIN y Licencia -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input
|
||||
v-model="discountData.vin"
|
||||
id="VIN"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
v-model="discountData.licencia"
|
||||
id="Licencia"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tarjeta de Circulación y RFC -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input
|
||||
v-model="discountData.tarjeta"
|
||||
id="Tarjeta de Circulación"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
v-model="discountData.rfc"
|
||||
id="RFC"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nombre -->
|
||||
<div class="mb-5">
|
||||
<Input
|
||||
v-model="discountData.nombre"
|
||||
id="Nombre"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Línea separadora -->
|
||||
<hr class="my-6 border-gray-300" />
|
||||
|
||||
<!-- Monto Original y Pago Mínimo -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input
|
||||
v-model="discountData.montoOriginal"
|
||||
id="Monto a Pagar (Original)"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
v-model="discountData.pagoMinimo"
|
||||
id="Pago Mínimo"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nuevo Monto a Cobrar (editable) -->
|
||||
<div class="mb-6">
|
||||
<Input
|
||||
v-model="discountData.nuevoMonto"
|
||||
id="Nuevo Monto a Cobrar (Autorizado)"
|
||||
type="text"
|
||||
placeholder="Ingrese el nuevo monto autorizado"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Botón Autorizar -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-green-700 hover:bg-green-600 text-white font-medium py-3.5 rounded-lg transition-colors"
|
||||
>
|
||||
Autorizar descuento
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
277
src/components/App/FineSection.vue
Normal file
277
src/components/App/FineSection.vue
Normal file
@ -0,0 +1,277 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useApi, apiURL } from "@/services/Api.js";
|
||||
|
||||
import Input from "@Holos/Form/Input.vue";
|
||||
import QRscan from "./QRscan.vue";
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(["fine-searched"]);
|
||||
|
||||
const api = useApi();
|
||||
|
||||
/** Refs */
|
||||
const fineNumber = ref("");
|
||||
const fineData = ref({
|
||||
fecha: "",
|
||||
placa: "",
|
||||
vin: "",
|
||||
licencia: "",
|
||||
tarjeta: "",
|
||||
rfc: "",
|
||||
nombre: "",
|
||||
monto: "",
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
const handleSearch = async () => {
|
||||
if (!fineNumber.value.trim()) {
|
||||
window.Notify.warning("Por favor ingresa o escanea una multa");
|
||||
return;
|
||||
}
|
||||
|
||||
await api.get(apiURL(`fines/search`), {
|
||||
params: {
|
||||
id: fineNumber.value.trim(),
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const model = data.model;
|
||||
|
||||
if (model.payments && model.payments.length > 0) {
|
||||
const payment = model.payments[0];
|
||||
|
||||
if (payment.status === "paid") {
|
||||
window.Notify.info("La multa ya ha sido pagada previamente");
|
||||
|
||||
fineData.value = {
|
||||
id: model.id,
|
||||
fecha: new Date(model.created_at).toLocaleDateString("es-MX"),
|
||||
placa: model.plate || "",
|
||||
vin: model.vin || "",
|
||||
licencia: model.license || "",
|
||||
tarjeta: model.circulation || "",
|
||||
rfc: model.rfc || "",
|
||||
nombre: model.name || "",
|
||||
monto: `$${data.total_to_pay.toFixed(2)}`,
|
||||
isPaid: true,
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fineData.value = {
|
||||
id: model.id,
|
||||
fecha: new Date(model.created_at).toLocaleDateString("es-MX"),
|
||||
placa: model.plate || "",
|
||||
vin: model.vin || "",
|
||||
licencia: model.license || "",
|
||||
tarjeta: model.circulation || "",
|
||||
rfc: model.rfc || "",
|
||||
nombre: model.name || "",
|
||||
monto: `$${data.total_to_pay.toFixed(2)}`,
|
||||
isPaid: false,
|
||||
};
|
||||
|
||||
emit("fine-searched", {
|
||||
...fineData.value,
|
||||
rawData: data,
|
||||
});
|
||||
},
|
||||
onFail: (error) => {
|
||||
window.Notify.error(error.message || "Error al buscar la multa");
|
||||
fineData.value = {
|
||||
fecha: "",
|
||||
placa: "",
|
||||
vin: "",
|
||||
licencia: "",
|
||||
tarjeta: "",
|
||||
rfc: "",
|
||||
nombre: "",
|
||||
monto: "",
|
||||
};
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error al buscar multa:", error);
|
||||
window.Notify.error("Ocurrió un error al buscar la multa");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handlePayment = async () => {
|
||||
if (!fineData.value.monto) {
|
||||
window.Notify.warning("No hay multa cargada para cobrar");
|
||||
return;
|
||||
}
|
||||
|
||||
await api.post(apiURL(`fines/${fineData.value.id}/mark-as-paid`), {
|
||||
onSuccess: (data) => {
|
||||
window.Notify.success("Multa cobrada exitosamente");
|
||||
|
||||
emit("payment-processed", { ...fineData.value, paymentData: data });
|
||||
|
||||
fineData.value = {
|
||||
id: null,
|
||||
fecha: "",
|
||||
placa: "",
|
||||
vin: "",
|
||||
licencia: "",
|
||||
tarjeta: "",
|
||||
rfc: "",
|
||||
nombre: "",
|
||||
monto: "",
|
||||
};
|
||||
fineNumber.value = "";
|
||||
},
|
||||
onFail: (error) => {
|
||||
window.Notify.error(
|
||||
error.message || "Error al procesar el pago de la multa"
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
window.Notify.error("Ocurrió un error al procesar el pago de la multa");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clearForm = () => {
|
||||
fineData.value = {
|
||||
id: null,
|
||||
fecha: "",
|
||||
placa: "",
|
||||
vin: "",
|
||||
licencia: "",
|
||||
tarjeta: "",
|
||||
rfc: "",
|
||||
nombre: "",
|
||||
monto: "",
|
||||
};
|
||||
fineNumber.value = "";
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
|
||||
<div class="bg-white rounded-xl p-6 shadow-lg">
|
||||
<h3 class="text-xl font-semibold mb-4 text-gray-800">Buscar Multa</h3>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-600 font-medium mb-2">
|
||||
Escanear Código QR
|
||||
</label>
|
||||
<div
|
||||
class="w-full h-80 bg-gray-800 rounded-lg flex flex-col items-center justify-center"
|
||||
>
|
||||
<!-- QR -->
|
||||
<QRscan
|
||||
v-model:fineNumber="fineNumber"
|
||||
@fine-searched="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<Input
|
||||
v-model="fineNumber"
|
||||
id="Número de Multa"
|
||||
type="text"
|
||||
placeholder="Ingrese el número de multa"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleSearch"
|
||||
type="button"
|
||||
class="w-full bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors"
|
||||
>
|
||||
Buscar
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-6 shadow-lg">
|
||||
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||
Detalles de la Multa
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="handlePayment">
|
||||
<!-- Fecha y Placa -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input
|
||||
v-model="fineData.fecha"
|
||||
id="Fecha de Multa"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
<Input v-model="fineData.placa" id="Placa" type="text" disabled />
|
||||
</div>
|
||||
|
||||
<!-- VIN y Licencia -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input v-model="fineData.vin" id="VIN" type="text" disabled />
|
||||
<Input
|
||||
v-model="fineData.licencia"
|
||||
id="Licencia"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tarjeta de Circulación y RFC -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input
|
||||
v-model="fineData.tarjeta"
|
||||
id="Tarjeta de Circulación"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
<Input v-model="fineData.rfc" id="RFC" type="text" disabled />
|
||||
</div>
|
||||
|
||||
<!-- Nombre -->
|
||||
<div class="mb-5">
|
||||
<Input v-model="fineData.nombre" id="Nombre" type="text" disabled />
|
||||
</div>
|
||||
|
||||
<!-- Línea separadora -->
|
||||
<hr class="my-6 border-gray-300" />
|
||||
|
||||
<!-- Monto a Pagar -->
|
||||
<div class="mb-5">
|
||||
<Input
|
||||
v-model="fineData.monto"
|
||||
id="Monto a Pagar"
|
||||
type="text"
|
||||
disabled
|
||||
class="text-lg font-semibold"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div v-if="fineData.isPaid">
|
||||
<button
|
||||
type="button"
|
||||
@click="clearForm"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3.5 rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
Limpiar y buscar otra multa
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Botón Cobrar -->
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="fineData.isPaid"
|
||||
:class="[
|
||||
'w-full font-medium py-3.5 rounded-lg transition-colors',
|
||||
fineData.isPaid
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-700 hover:bg-green-600 text-white',
|
||||
]"
|
||||
>
|
||||
Cobrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
202
src/components/App/MembershipSection.vue
Normal file
202
src/components/App/MembershipSection.vue
Normal file
@ -0,0 +1,202 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
import Input from "@Holos/Form/Input.vue";
|
||||
import Selectable from "@Holos/Form/Selectable.vue";
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(["membership-paid"]);
|
||||
|
||||
/** Refs */
|
||||
const searchForm = ref({
|
||||
membershipNumber: "",
|
||||
curp: "",
|
||||
});
|
||||
|
||||
const memberData = ref({
|
||||
image: null,
|
||||
name: "",
|
||||
curp: "",
|
||||
tutorName: "",
|
||||
});
|
||||
|
||||
const paymentData = ref({
|
||||
service: [],
|
||||
quantity: 1,
|
||||
totalAmount: "",
|
||||
});
|
||||
|
||||
const serviceOptions = ref([
|
||||
{ id: 1, description: "Membresía Mensual" },
|
||||
{ id: 2, description: "Membresía Trimestral" },
|
||||
{ id: 3, description: "Membresía Semestral" },
|
||||
{ id: 4, description: "Membresía Anual" },
|
||||
]);
|
||||
|
||||
/** Métodos */
|
||||
const handleSearch = () => {
|
||||
if (!searchForm.value.membershipNumber && !searchForm.value.curp) {
|
||||
alert("Por favor ingresa al menos un criterio de búsqueda");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Buscando miembro:", searchForm.value);
|
||||
|
||||
// Simular datos encontrados
|
||||
memberData.value = {
|
||||
name: "Juan Pérez García",
|
||||
curp: "PEGJ900101HDFRNN09",
|
||||
tutorName: "María López Hernández",
|
||||
image: null,
|
||||
};
|
||||
};
|
||||
|
||||
const handlePayment = () => {
|
||||
if (!paymentData.value.service) {
|
||||
alert("Por favor selecciona un servicio");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Procesando pago de membresía:", {
|
||||
member: memberData.value,
|
||||
payment: paymentData.value,
|
||||
});
|
||||
|
||||
emit("membership-paid", {
|
||||
member: memberData.value,
|
||||
payment: paymentData.value,
|
||||
});
|
||||
};
|
||||
|
||||
const calculateTotal = () => {
|
||||
paymentData.value.totalAmount = "0.00";
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<!-- Formulario de búsqueda -->
|
||||
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-6">
|
||||
<h3 class="text-xl font-semibold mb-6 text-gray-800">Buscar miembro</h3>
|
||||
<form @submit.prevent="handleSearch">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
|
||||
<!-- Número de Membresía -->
|
||||
<Input
|
||||
v-model="searchForm.membershipNumber"
|
||||
id="Número de Membresía"
|
||||
type="text"
|
||||
placeholder=""
|
||||
/>
|
||||
|
||||
<!-- CURP -->
|
||||
<Input
|
||||
v-model="searchForm.curp"
|
||||
id="CURP"
|
||||
type="text"
|
||||
placeholder=""
|
||||
/>
|
||||
|
||||
<!-- Botón Buscar -->
|
||||
<button
|
||||
type="submit"
|
||||
class="py-3 px-8 rounded-lg transition-colors h-[42px] bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium"
|
||||
>
|
||||
Buscar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Información del Miembro -->
|
||||
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-6">
|
||||
<h3 class="text-xl font-semibold mb-6 text-gray-800">
|
||||
Información del Miembro
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Fotografía -->
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 font-medium mb-2">
|
||||
Fotografía
|
||||
</label>
|
||||
<div
|
||||
class="w-full h-64 bg-gray-200 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-24 w-24 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datos del miembro -->
|
||||
<div class="space-y-5">
|
||||
<Input v-model="memberData.name" id="Nombre" type="text" disabled />
|
||||
|
||||
<Input v-model="memberData.curp" id="CURP" type="text" disabled />
|
||||
|
||||
<Input
|
||||
v-model="memberData.tutorName"
|
||||
id="Nombre Tutor"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Realizar Cobro de Membresía -->
|
||||
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg">
|
||||
<h3 class="text-xl font-semibold mb-6 text-gray-800">
|
||||
Realizar Cobro de Membresía
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="handlePayment">
|
||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||
<!-- Servicio a Pagar -->
|
||||
<Selectable
|
||||
v-model="paymentData.service"
|
||||
label="description"
|
||||
title="Servicio a pagar"
|
||||
:options="serviceOptions"
|
||||
/>
|
||||
|
||||
<!-- Cantidad -->
|
||||
<Input
|
||||
v-model="paymentData.quantity"
|
||||
id="Cantidad (días)"
|
||||
type="number"
|
||||
min="1"
|
||||
@input="calculateTotal"
|
||||
/>
|
||||
|
||||
<!-- Monto Total -->
|
||||
<Input
|
||||
v-model="paymentData.totalAmount"
|
||||
id="Monto Total"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Botón Cobrar -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-green-700 hover:bg-green-600 text-white font-medium py-3.5 rounded-lg transition-colors"
|
||||
>
|
||||
Pagar membresía
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
130
src/components/App/QRscan.vue
Normal file
130
src/components/App/QRscan.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { QrcodeStream } from 'vue-qrcode-reader';
|
||||
|
||||
/** Props y Emits */
|
||||
defineProps({
|
||||
fineNumber: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:fineNumber', 'fine-searched']);
|
||||
|
||||
/** Refs */
|
||||
const error = ref('');
|
||||
const isScanning = ref(true);
|
||||
|
||||
/** Métodos */
|
||||
function paintBoundingBox(detectedCodes, ctx) {
|
||||
for (const detectedCode of detectedCodes) {
|
||||
const {
|
||||
boundingBox: { x, y, width, height }
|
||||
} = detectedCode;
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = '#7a0b3a'; // Color del tema
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
function onDetect(detectedCodes) {
|
||||
if (detectedCodes.length > 0) {
|
||||
const code = detectedCodes[0].rawValue;
|
||||
|
||||
// Emitir el código escaneado al componente padre
|
||||
emit('update:fineNumber', code);
|
||||
|
||||
// Pausar el escaneo después de detectar
|
||||
isScanning.value = false;
|
||||
|
||||
// Emitir evento para buscar la multa
|
||||
emit('fine-searched');
|
||||
|
||||
// Reactivar el escaneo después de 2 segundos
|
||||
setTimeout(() => {
|
||||
isScanning.value = true;
|
||||
error.value = '';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function onError(err) {
|
||||
error.value = `[${err.name}]: `;
|
||||
|
||||
if (err.name === 'NotAllowedError') {
|
||||
error.value += 'Necesitas otorgar permiso de acceso a la cámara';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
error.value += 'No se encontró cámara en este dispositivo';
|
||||
} else if (err.name === 'NotSupportedError') {
|
||||
error.value += 'Se requiere contexto seguro';
|
||||
} else if (err.name === 'NotReadableError') {
|
||||
error.value += '¿La cámara ya está en uso?';
|
||||
} else if (err.name === 'OverconstrainedError') {
|
||||
error.value += 'Las cámaras instaladas no son adecuadas';
|
||||
} else if (err.name === 'StreamApiNotSupportedError') {
|
||||
error.value += 'No es compatible con este navegador';
|
||||
} else if (err.name === 'InsecureContextError') {
|
||||
error.value += 'El acceso a la cámara solo se permite en contexto seguro.';
|
||||
} else {
|
||||
error.value += err.message;
|
||||
}
|
||||
|
||||
console.error('Error de cámara:', err);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full">
|
||||
<!-- Componente de escaneo QR -->
|
||||
<QrcodeStream
|
||||
v-if="isScanning"
|
||||
@detect="onDetect"
|
||||
@error="onError"
|
||||
:track="paintBoundingBox"
|
||||
class="w-full h-full rounded-lg overflow-hidden"
|
||||
>
|
||||
</QrcodeStream>
|
||||
|
||||
<!-- Mensaje de éxito -->
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full rounded-lg bg-green-600 flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-16 w-16 mx-auto mb-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-lg font-semibold">¡Código detectado!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mensaje de error -->
|
||||
<div
|
||||
v-if="error"
|
||||
class="absolute bottom-0 left-0 right-0 bg-red-500 text-white p-3 text-sm"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(video) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
@ -19,6 +19,6 @@ const home = () => {
|
||||
class="flex w-full justify-center items-center space-x-2 cursor-pointer"
|
||||
@click="home"
|
||||
>
|
||||
<img :src="$page.app.logo" class="h-20" />
|
||||
<img :src="'/logo.png'" class="h-20" />
|
||||
</div>
|
||||
</template>
|
||||
@ -80,6 +80,83 @@ export default {
|
||||
description: 'Historial de acciones realizadas por los usuarios en orden cronológico.'
|
||||
}
|
||||
},
|
||||
address: {
|
||||
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: {
|
||||
title: 'Crear nueva dirección',
|
||||
description: 'Permite crear nuevas direcciones.',
|
||||
onSuccess: 'Dirección creada exitosamente',
|
||||
onError: 'Error al crear la dirección'
|
||||
},
|
||||
edit: {
|
||||
title: 'Editar dirección',
|
||||
description: 'Permite editar direcciones existentes.',
|
||||
onSuccess: 'Dirección actualizada exitosamente',
|
||||
onError: 'Error al actualizar la dirección'
|
||||
},
|
||||
list: {
|
||||
title: 'Listado de Direcciones',
|
||||
empty: 'No hay direcciones registradas'
|
||||
},
|
||||
deleted: 'Dirección eliminada',
|
||||
permissions: {
|
||||
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: {
|
||||
title: 'Membresías',
|
||||
create: {
|
||||
title: 'Crear membresía',
|
||||
description: 'Permite crear nuevas membresías.',
|
||||
onSuccess: 'Membresía creada exitosamente',
|
||||
onError: 'Error al crear la membresía'
|
||||
}
|
||||
},
|
||||
app: {
|
||||
theme: {
|
||||
dark: 'Tema oscuro',
|
||||
|
||||
@ -35,9 +35,34 @@ onMounted(() => {
|
||||
to="dashboard.index"
|
||||
/>
|
||||
<Link
|
||||
icon="person"
|
||||
name="profile"
|
||||
to="profile.show"
|
||||
icon="add_home"
|
||||
name="Gestionar Direcciones"
|
||||
to="address.index"
|
||||
/>
|
||||
<Link
|
||||
icon="how_to_vote"
|
||||
name="Gestionar Conceptos"
|
||||
to="concept.index"
|
||||
/>
|
||||
<Link
|
||||
icon="receipt_long"
|
||||
name="Gestionar Multas"
|
||||
to="fine.index"
|
||||
/>
|
||||
<Link
|
||||
icon="car_tag"
|
||||
name="Autorizar Descuentos"
|
||||
to="discount.index"
|
||||
/>
|
||||
<Link
|
||||
icon="card_membership"
|
||||
name="Cobro de Membresía"
|
||||
to="membership.index"
|
||||
/>
|
||||
<Link
|
||||
icon="point_of_sale"
|
||||
name="Entrega de caja"
|
||||
to="checkout.index"
|
||||
/>
|
||||
</Section>
|
||||
<Section
|
||||
|
||||
@ -21,11 +21,12 @@ const form = useForm({
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
roles: []
|
||||
roles: [],
|
||||
address: ''
|
||||
});
|
||||
|
||||
const roles = ref([]);
|
||||
|
||||
const addresses = ref([]);
|
||||
/** Métodos */
|
||||
function submit() {
|
||||
form.transform(data => ({
|
||||
@ -80,5 +81,12 @@ onMounted(() => {
|
||||
:options="roles"
|
||||
multiple
|
||||
/>
|
||||
<Selectable
|
||||
v-model="form.address"
|
||||
label="description"
|
||||
title="Direcciones"
|
||||
:options="addresses"
|
||||
multiple
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
157
src/pages/App/Address/Index.vue
Normal file
157
src/pages/App/Address/Index.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useSearcher, useForm } from "@Services/Api";
|
||||
import { apiTo, transl } from "./Module";
|
||||
|
||||
import Table from "@Holos/Table.vue";
|
||||
import IconButton from "@Holos/Button/Icon.vue";
|
||||
import PageHeader from "@Holos/PageHeader.vue";
|
||||
import AddressForm from "@App/AddressSection.vue";
|
||||
import Editview from "@Holos/Modal/Edit.vue";
|
||||
import DestroyView from "@Holos/Modal/Template/Destroy.vue";
|
||||
import ModalController from "@Controllers/ModalController.js";
|
||||
|
||||
/** Controladores */
|
||||
const Modal = new ModalController();
|
||||
|
||||
/** Propiedades */
|
||||
const destroyModal = ref(Modal.destroyModal);
|
||||
const editModal = ref(Modal.editModal);
|
||||
const modelModal = ref(Modal.modelModal);
|
||||
|
||||
const models = ref({
|
||||
data: [],
|
||||
total: 0,
|
||||
});
|
||||
|
||||
/** Inicializar searcher */
|
||||
const searcher = useSearcher({
|
||||
url: apiTo("index"),
|
||||
filters: {},
|
||||
onSuccess: (data) => {
|
||||
models.value = data.models;
|
||||
},
|
||||
});
|
||||
|
||||
/** Cargar datos iniciales */
|
||||
onMounted(() => {
|
||||
searcher.search();
|
||||
});
|
||||
|
||||
const handleAddressCreated = async (address) => {
|
||||
const form = useForm({
|
||||
name: address.name,
|
||||
});
|
||||
|
||||
form.post(apiTo("store"), {
|
||||
onSuccess: () => {
|
||||
Notify.success(transl('create.onSuccess'));
|
||||
searcher.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
Notify.error(transl('create.onError'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddressUpdated = async () => {
|
||||
const form = useForm({
|
||||
name: modelModal.value.name,
|
||||
});
|
||||
|
||||
form.put(apiTo("update", { direction: modelModal.value.id }), {
|
||||
onSuccess: () => {
|
||||
Notify.success(transl('edit.onSuccess'));
|
||||
searcher.refresh();
|
||||
Modal.switchEditModal();
|
||||
},
|
||||
onError: () => {
|
||||
Notify.error(transl('edit.onError'));
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader :title="transl('title')" />
|
||||
<AddressForm @address-created="handleAddressCreated" />
|
||||
<div class="m-3">
|
||||
<Table
|
||||
:items="models"
|
||||
:processing="searcher.processing"
|
||||
@send-pagination="(page) => searcher.pagination(page)"
|
||||
>
|
||||
<template #head>
|
||||
<th v-text="$t('name')" />
|
||||
<th v-text="$t('actions')" class="w-32 text-center" />
|
||||
</template>
|
||||
<template #body="{ items }">
|
||||
<tr v-for="model in items" :key="model.id" class="table-row">
|
||||
<td class="table-cell border">
|
||||
{{ model.name }}
|
||||
</td>
|
||||
<td class="table-cell">
|
||||
<div class="table-actions">
|
||||
<IconButton
|
||||
icon="edit"
|
||||
:title="$t('crud.edit')"
|
||||
@click="Modal.switchEditModal(model)"
|
||||
outline
|
||||
/>
|
||||
<IconButton
|
||||
icon="delete"
|
||||
:title="$t('crud.destroy')"
|
||||
@click="Modal.switchDestroyModal(model)"
|
||||
outline
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template #empty>
|
||||
<td colspan="2" class="table-cell text-center py-4 text-gray-500">
|
||||
{{ transl('list.empty') }}
|
||||
</td>
|
||||
</template>
|
||||
</Table>
|
||||
<DestroyView
|
||||
title="name"
|
||||
subtitle=""
|
||||
:model="modelModal"
|
||||
:show="destroyModal"
|
||||
:to="(id) => apiTo('destroy', { direction: id })"
|
||||
@close="Modal.switchDestroyModal"
|
||||
@update="
|
||||
() => {
|
||||
const index = models.data.findIndex((m) => m.id === modelModal.id);
|
||||
if (index > -1) {
|
||||
models.data.splice(index, 1);
|
||||
models.total--;
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Editview
|
||||
:show="editModal"
|
||||
:title="transl('edit.title')"
|
||||
@close="Modal.switchEditModal"
|
||||
@update="handleAddressUpdated"
|
||||
>
|
||||
<div class="p-4">
|
||||
<label
|
||||
for="editAddressName"
|
||||
class="block text-sm text-gray-600 font-medium mb-2"
|
||||
>
|
||||
{{ transl('name') }}:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="editAddressName"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</Editview>
|
||||
</div>
|
||||
</template>
|
||||
21
src/pages/App/Address/Module.js
Normal file
21
src/pages/App/Address/Module.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { lang } from '@Lang/i18n';
|
||||
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||
|
||||
// Ruta API
|
||||
const apiTo = (name, params = {}) => route(`directions.${name}`, params)
|
||||
|
||||
// Ruta visual
|
||||
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `address.${name}`, params, query })
|
||||
|
||||
// Obtener traducción del componente
|
||||
const transl = (str) => lang(`address.${str}`)
|
||||
|
||||
// Determina si un usuario puede hacer algo no en base a los permisos
|
||||
const can = (permission) => hasPermission(`address.${permission}`)
|
||||
|
||||
export {
|
||||
can,
|
||||
viewTo,
|
||||
apiTo,
|
||||
transl
|
||||
}
|
||||
9
src/pages/App/Checkout/Index.vue
Normal file
9
src/pages/App/Checkout/Index.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import PageHeader from "@Holos/PageHeader.vue";
|
||||
import Checkout from '@App/CheckoutDelivery.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader title="Entrega de Caja" />
|
||||
<Checkout />
|
||||
</template>
|
||||
284
src/pages/App/Concept/Index.vue
Normal file
284
src/pages/App/Concept/Index.vue
Normal file
@ -0,0 +1,284 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useSearcher, useForm, api, apiURL } from "@Services/Api";
|
||||
import { apiTo, transl } from "./Module";
|
||||
|
||||
import Table from "@Holos/Table.vue";
|
||||
import IconButton from "@Holos/Button/Icon.vue";
|
||||
import PageHeader from "@Holos/PageHeader.vue";
|
||||
import ConceptForm from "@App/ConceptSection.vue";
|
||||
import DestroyView from "@Holos/Modal/Template/Destroy.vue";
|
||||
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 */
|
||||
const Modal = new ModalController();
|
||||
|
||||
/** Propiedades */
|
||||
const destroyModal = ref(Modal.destroyModal);
|
||||
const editModal = ref(Modal.editModal);
|
||||
const modelModal = ref(Modal.modelModal);
|
||||
|
||||
const addresses = ref([]);
|
||||
const units = ref([]);
|
||||
|
||||
const models = ref({
|
||||
data: [],
|
||||
total: 0,
|
||||
});
|
||||
|
||||
/** Inicializar searcher */
|
||||
const searcher = useSearcher({
|
||||
url: apiTo("index"),
|
||||
filters: {},
|
||||
onSuccess: (data) => {
|
||||
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 */
|
||||
onMounted(() => {
|
||||
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>
|
||||
<template>
|
||||
<PageHeader :title="transl('title')" />
|
||||
<ConceptForm @concept-created="handleConceptCreated" />
|
||||
<div class="m-3">
|
||||
<Table
|
||||
:items="models"
|
||||
:processing="searcher.processing"
|
||||
@send-pagination="(page) => searcher.pagination(page)"
|
||||
>
|
||||
<template #head>
|
||||
<th v-text="transl('direction')" />
|
||||
<th v-text="transl('shortName')" />
|
||||
<th v-text="transl('name')" />
|
||||
<th v-text="transl('legal')" />
|
||||
<th v-text="transl('article')" />
|
||||
<th v-text="transl('description')" />
|
||||
<th v-text="$t('actions')" class="w-32 text-center" />
|
||||
</template>
|
||||
<template #body="{ items }">
|
||||
<tr v-for="model in items" :key="model.id" class="table-row">
|
||||
<td class="table-cell border">
|
||||
{{ 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 class="table-cell">
|
||||
<div class="table-actions">
|
||||
<IconButton
|
||||
icon="edit"
|
||||
:title="$t('crud.edit')"
|
||||
@click="Modal.switchEditModal(model)"
|
||||
outline
|
||||
/>
|
||||
<IconButton
|
||||
icon="delete"
|
||||
:title="$t('crud.destroy')"
|
||||
@click="Modal.switchDestroyModal(model)"
|
||||
outline
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template #empty>
|
||||
<td colspan="7" class="table-cell text-center py-4 text-gray-500">
|
||||
{{ transl("list.empty") }}
|
||||
</td>
|
||||
</template>
|
||||
</Table>
|
||||
<DestroyView
|
||||
title="name"
|
||||
subtitle="content"
|
||||
:model="modelModal"
|
||||
:show="destroyModal"
|
||||
:to="(id) => apiTo('destroy', { charge_concept: id })"
|
||||
@close="Modal.switchDestroyModal"
|
||||
@update="
|
||||
() => {
|
||||
const index = models.data.findIndex((m) => m.id === modelModal.id);
|
||||
if (index > -1) {
|
||||
models.data.splice(index, 1);
|
||||
models.total--;
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<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>
|
||||
</template>
|
||||
21
src/pages/App/Concept/Module.js
Normal file
21
src/pages/App/Concept/Module.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { lang } from '@Lang/i18n';
|
||||
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||
|
||||
// Ruta API
|
||||
const apiTo = (name, params = {}) => route(`charge-concepts.${name}`, params)
|
||||
|
||||
// Ruta visual
|
||||
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `concept.${name}`, params, query })
|
||||
|
||||
// Obtener traducción del componente
|
||||
const transl = (str) => lang(`concept.${str}`)
|
||||
|
||||
// Determina si un usuario puede hacer algo no en base a los permisos
|
||||
const can = (permission) => hasPermission(`concept.${permission}`)
|
||||
|
||||
export {
|
||||
can,
|
||||
viewTo,
|
||||
apiTo,
|
||||
transl
|
||||
}
|
||||
9
src/pages/App/Discount/Index.vue
Normal file
9
src/pages/App/Discount/Index.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import PageHeader from "@Holos/PageHeader.vue";
|
||||
import Discount from '@App/DiscountSection.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader title="Autorizar Descuentos" />
|
||||
<Discount />
|
||||
</template>
|
||||
9
src/pages/App/Fine/Index.vue
Normal file
9
src/pages/App/Fine/Index.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import PageHeader from "@Holos/PageHeader.vue";
|
||||
import FineSection from '@App/FineSection.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader title="Cobro de Multa" />
|
||||
<FineSection />
|
||||
</template>
|
||||
21
src/pages/App/Fine/Module.js
Normal file
21
src/pages/App/Fine/Module.js
Normal 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
|
||||
}
|
||||
64
src/pages/App/Membership/Create.vue
Normal file
64
src/pages/App/Membership/Create.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { api, useForm } from '@Services/Api';
|
||||
import { apiTo, transl, viewTo } from './Module';
|
||||
|
||||
import IconButton from '@Holos/Button/Icon.vue'
|
||||
import Input from '@Holos/Form/Input.vue';
|
||||
import Selectable from '@Holos/Form/Selectable.vue';
|
||||
import PageHeader from '@Holos/PageHeader.vue';
|
||||
import Form from './Form.vue'
|
||||
|
||||
/** Definidores */
|
||||
const router = useRouter();
|
||||
|
||||
/** Propiedades */
|
||||
const form = useForm({
|
||||
name: '',
|
||||
paternal: '',
|
||||
maternal: '',
|
||||
curp: '',
|
||||
membership_number: '',
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
function submit() {
|
||||
form.transform(data => ({
|
||||
...data,
|
||||
})).post(apiTo('store'), {
|
||||
onSuccess: () => {
|
||||
Notify.success(Lang('register.create.onSuccess'))
|
||||
router.push(viewTo({ name: 'index' }));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Ciclos */
|
||||
onMounted(() => {
|
||||
api.get(route('system.roles'), {
|
||||
onSuccess: (r) => {
|
||||
roles.value = r.roles;
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader :title="transl('create.title')">
|
||||
<RouterLink :to="viewTo({ name: 'index' })">
|
||||
<IconButton
|
||||
class="text-white"
|
||||
icon="arrow_back"
|
||||
:title="$t('return')"
|
||||
filled
|
||||
/>
|
||||
</RouterLink>
|
||||
</PageHeader>
|
||||
<Form
|
||||
action="create"
|
||||
:form="form"
|
||||
@submit="submit"
|
||||
>
|
||||
</Form>
|
||||
</template>
|
||||
67
src/pages/App/Membership/Form.vue
Normal file
67
src/pages/App/Membership/Form.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { transl } from './Module';
|
||||
|
||||
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||
import Input from '@Holos/Form/Input.vue';
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits([
|
||||
'submit'
|
||||
])
|
||||
|
||||
/** Propiedades */
|
||||
defineProps({
|
||||
action: {
|
||||
default: 'create',
|
||||
type: String
|
||||
},
|
||||
form: Object
|
||||
})
|
||||
|
||||
/** Métodos */
|
||||
function submit() {
|
||||
emit('submit')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full pb-2">
|
||||
<p class="text-justify text-sm" v-text="transl(`${action}.description`)" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<form @submit.prevent="submit" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
<Input
|
||||
v-model="form.name"
|
||||
id="name"
|
||||
:onError="form.errors.name"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
v-model="form.paternal"
|
||||
id="paternal"
|
||||
:onError="form.errors.paternal"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
v-model="form.maternal"
|
||||
id="maternal"
|
||||
:onError="form.errors.maternal"
|
||||
/>
|
||||
<Input
|
||||
v-model="form.curp"
|
||||
id="Curp"
|
||||
:onError="form.errors.curp"
|
||||
type="text"
|
||||
/>
|
||||
<slot />
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4">
|
||||
<PrimaryButton
|
||||
v-text="$t(action)"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
21
src/pages/App/Membership/Index.vue
Normal file
21
src/pages/App/Membership/Index.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { can, viewTo, } from "./Module";
|
||||
|
||||
import SearcherHead from "@Holos/Searcher.vue";
|
||||
import IconButton from '@Holos/Button/Icon.vue'
|
||||
import MembershipSection from "@App/MembershipSection.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SearcherHead title="Cobro de Membresía" @search="(x) => searcher.search(x)">
|
||||
<RouterLink v-if="can('create')" :to="viewTo({ name: 'create' })">
|
||||
<IconButton
|
||||
class="text-white"
|
||||
icon="add"
|
||||
:title="$t('crud.create')"
|
||||
filled
|
||||
/>
|
||||
</RouterLink>
|
||||
</SearcherHead>
|
||||
<MembershipSection />
|
||||
</template>
|
||||
21
src/pages/App/Membership/Module.js
Normal file
21
src/pages/App/Membership/Module.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { lang } from '@Lang/i18n';
|
||||
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||
|
||||
// Ruta API
|
||||
const apiTo = (name, params = {}) => route(`membership.${name}`, params)
|
||||
|
||||
// Ruta visual
|
||||
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `membership.${name}`, params, query })
|
||||
|
||||
// Obtener traducción del componente
|
||||
const transl = (str) => lang(`membership.${str}`)
|
||||
|
||||
// Determina si un usuario puede hacer algo no en base a los permisos
|
||||
const can = (permission) => true//hasPermission(`membership.${permission}`)
|
||||
|
||||
export {
|
||||
can,
|
||||
viewTo,
|
||||
apiTo,
|
||||
transl
|
||||
}
|
||||
@ -48,6 +48,71 @@ const router = createRouter({
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'address',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'address.index',
|
||||
component: () => import('@Pages/App/Address/Index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'concept',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'concept.index',
|
||||
component: () => import('@Pages/App/Concept/Index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'fine',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'fine.index',
|
||||
component: () => import('@Pages/App/Fine/Index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'discount',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'discount.index',
|
||||
component: () => import('@Pages/App/Discount/Index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'membership',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'membership.index',
|
||||
component: () => import('@Pages/App/Membership/Index.vue')
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
name: 'membership.create',
|
||||
component: () => import('@Pages/App/Membership/Create.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'checkout',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'checkout.index',
|
||||
component: () => import('@Pages/App/Checkout/Index.vue')
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -16,6 +16,7 @@ export default defineConfig({
|
||||
'@Components': fileURLToPath(new URL('./src/components', import.meta.url)),
|
||||
'@Controllers': fileURLToPath(new URL('./src/controllers', import.meta.url)),
|
||||
'@Holos': fileURLToPath(new URL('./src/components/Holos', import.meta.url)),
|
||||
'@App': fileURLToPath(new URL('./src/components/App', import.meta.url)),
|
||||
'@Layouts': fileURLToPath(new URL('./src/layouts', import.meta.url)),
|
||||
'@Lang': fileURLToPath(new URL('./src/lang', import.meta.url)),
|
||||
'@Router': fileURLToPath(new URL('./src/router', import.meta.url)),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user