Se agregó las gráficas a Tramites #1
32
package-lock.json
generated
32
package-lock.json
generated
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "template-laravel-vuejs",
|
||||
"name": "maquetador-graficas",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@ -8,12 +8,14 @@
|
||||
"@inertiajs/vue3": "^1.0.0-beta.2",
|
||||
"@soketi/soketi": "^1.6.0",
|
||||
"@vueuse/core": "^9.6.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"laravel-echo": "^1.14.2",
|
||||
"pusher-js": "^7.5.0",
|
||||
"sweetalert2": "^11.4.8",
|
||||
"toastr": "^2.1.4",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-multiselect": "^3.0.0-alpha.2"
|
||||
},
|
||||
@ -523,6 +525,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz",
|
||||
@ -1678,6 +1686,18 @@
|
||||
"resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz",
|
||||
"integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ=="
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
|
||||
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@ -5451,6 +5471,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-chartjs": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.2.tgz",
|
||||
"integrity": "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.1",
|
||||
"vue": "^3.0.0-0 || ^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "9.7.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.7.1.tgz",
|
||||
|
||||
@ -22,12 +22,14 @@
|
||||
"@inertiajs/vue3": "^1.0.0-beta.2",
|
||||
"@soketi/soketi": "^1.6.0",
|
||||
"@vueuse/core": "^9.6.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"laravel-echo": "^1.14.2",
|
||||
"pusher-js": "^7.5.0",
|
||||
"sweetalert2": "^11.4.8",
|
||||
"toastr": "^2.1.4",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-multiselect": "^3.0.0-alpha.2"
|
||||
}
|
||||
|
||||
34
resources/js/Components/Dashboard/Charts/Bars.vue
Normal file
34
resources/js/Components/Dashboard/Charts/Bars.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { Bar } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement
|
||||
)
|
||||
|
||||
const props = defineProps({
|
||||
chartData: Object,
|
||||
chartOptions: Object
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Bar :data="chartData" :options="chartOptions" />
|
||||
</template>
|
||||
32
resources/js/Components/Dashboard/Charts/Lines.vue
Normal file
32
resources/js/Components/Dashboard/Charts/Lines.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import { Line } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale
|
||||
)
|
||||
|
||||
const props = defineProps({
|
||||
chartData: Object,
|
||||
chartOptions: Object
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Line :data="chartData" :options="chartOptions" />
|
||||
</template>
|
||||
29
resources/js/Components/Dashboard/Charts/Pie.vue
Normal file
29
resources/js/Components/Dashboard/Charts/Pie.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import { Pie } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, ArcElement)
|
||||
|
||||
const props = defineProps({
|
||||
chartData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
chartOptions: {
|
||||
type: Object,
|
||||
default: () => ({ responsive: true, maintainAspectRatio: false })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pie-chart-wrapper" style="position: relative; width:100%; height:300px;">
|
||||
<Pie :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</template>
|
||||
42
resources/js/Components/Dashboard/Charts/StackedBar.vue
Normal file
42
resources/js/Components/Dashboard/Charts/StackedBar.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import { Bar } from "vue-chartjs";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
} from "chart.js";
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale
|
||||
);
|
||||
|
||||
const props = defineProps({
|
||||
chartData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
chartOptions: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: { stacked: true },
|
||||
y: { stacked: true },
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Bar :data="chartData" :options="chartOptions" />
|
||||
</template>
|
||||
118
resources/js/Components/Dashboard/Form/DateRange.vue
Normal file
118
resources/js/Components/Dashboard/Form/DateRange.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed, onBeforeUnmount } from 'vue';
|
||||
import Input from '@/Components/Dashboard/Form/Input.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({ start: '', end: '' })
|
||||
},
|
||||
required: Boolean,
|
||||
onError: String,
|
||||
idStart: { type: String, default: 'startDate' },
|
||||
idEnd: { type: String, default: 'endDate' },
|
||||
titleStart: { type: String, default: 'Fecha inicio' },
|
||||
titleEnd: { type: String, default: 'Fecha fin' },
|
||||
presets: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
debounceMs: {
|
||||
type: Number,
|
||||
default: 350
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'invalid']);
|
||||
|
||||
const local = ref({ start: props.modelValue.start, end: props.modelValue.end });
|
||||
let debounceId = null;
|
||||
|
||||
// --- utilidades fechas ---
|
||||
const fmt = (d) => new Date(d).toISOString().slice(0,10);
|
||||
const today = fmt(new Date());
|
||||
const firstDayOfMonth = fmt(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||||
const daysAgo = (n) => {
|
||||
const d = new Date(); d.setDate(d.getDate()-n);
|
||||
return fmt(d);
|
||||
};
|
||||
|
||||
// --- validación ---
|
||||
const isValid = computed(() => {
|
||||
const { start, end } = local.value;
|
||||
if (!start || !end) return !props.required; // si no son obligatorias
|
||||
return new Date(start) <= new Date(end);
|
||||
});
|
||||
|
||||
// sincroniza si el padre cambia externamente
|
||||
watch(() => props.modelValue, (nv) => {
|
||||
if (nv?.start !== local.value.start || nv?.end !== local.value.end) {
|
||||
local.value = { start: nv?.start || '', end: nv?.end || '' };
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// emite con debounce solo si es válido
|
||||
const emitChange = () => {
|
||||
clearTimeout(debounceId);
|
||||
debounceId = setTimeout(() => {
|
||||
if (isValid.value) {
|
||||
emit('update:modelValue', { ...local.value });
|
||||
} else {
|
||||
emit('invalid', { ...local.value });
|
||||
}
|
||||
}, props.debounceMs);
|
||||
};
|
||||
|
||||
watch(() => local.value.start, emitChange);
|
||||
watch(() => local.value.end, emitChange);
|
||||
|
||||
// presets
|
||||
const applyPreset = (type) => {
|
||||
if (type === 'today') {
|
||||
local.value = { start: today, end: today };
|
||||
} else if (type === 'last7') {
|
||||
local.value = { start: daysAgo(6), end: today }; // 7 días incluyendo hoy
|
||||
} else if (type === 'month') {
|
||||
local.value = { start: firstDayOfMonth, end: today };
|
||||
}
|
||||
emitChange();
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => clearTimeout(debounceId));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<div class="flex space-x-4">
|
||||
<Input
|
||||
:id="idStart"
|
||||
type="date"
|
||||
:title="titleStart"
|
||||
v-model="local.start"
|
||||
:required="required"
|
||||
:onError="onError"
|
||||
/>
|
||||
<Input
|
||||
:id="idEnd"
|
||||
type="date"
|
||||
:title="titleEnd"
|
||||
v-model="local.end"
|
||||
:required="required"
|
||||
:onError="onError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="presets" class="flex gap-2">
|
||||
<button type="button" class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
|
||||
@click="applyPreset('today')">Hoy</button>
|
||||
<button type="button" class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
|
||||
@click="applyPreset('last7')">Últimos 7 días</button>
|
||||
<button type="button" class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
|
||||
@click="applyPreset('month')">Mes actual</button>
|
||||
</div>
|
||||
|
||||
<p v-if="!isValid" class="text-sm text-red-600 w-full">
|
||||
La fecha fin debe ser mayor o igual a la fecha inicio.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@ -35,7 +35,7 @@ onMounted(()=> {
|
||||
:title="title"
|
||||
/>
|
||||
<div class="flex w-full h-screen">
|
||||
<div
|
||||
<div
|
||||
id="sidebar"
|
||||
class="fixed w-fit h-screen transition-all duration-300 z-10"
|
||||
:class="{'-translate-x-[16.5rem] md:-translate-x-0':sidebarStatus, '-translate-x-0 md:-translate-x-64':!sidebarStatus}"
|
||||
@ -45,9 +45,9 @@ onMounted(()=> {
|
||||
@open="sidebarSwitch()"
|
||||
>
|
||||
<Section name="Principal">
|
||||
<Link
|
||||
<Link
|
||||
icon="home"
|
||||
name="home"
|
||||
name="home"
|
||||
to="dashboard.index"
|
||||
/>
|
||||
<Link
|
||||
@ -55,9 +55,14 @@ onMounted(()=> {
|
||||
name="help.title"
|
||||
to="dashboard.help"
|
||||
/>
|
||||
<Link
|
||||
icon="live_help"
|
||||
name="Graficas"
|
||||
to="dashboard.chart-test.index"
|
||||
/>
|
||||
</Section>
|
||||
<Section name="Configuraciones">
|
||||
<Link
|
||||
<Link
|
||||
icon="manage_accounts"
|
||||
name="profile"
|
||||
to="profile.show"
|
||||
@ -69,7 +74,7 @@ onMounted(()=> {
|
||||
to="dashboard.histories.index"
|
||||
/>
|
||||
</Section>
|
||||
<Link
|
||||
<Link
|
||||
v-if="hasPermission('users.index')"
|
||||
icon="people"
|
||||
name="users.title"
|
||||
@ -93,7 +98,7 @@ onMounted(()=> {
|
||||
</Section>
|
||||
</Sidebar>
|
||||
</div>
|
||||
<div
|
||||
<div
|
||||
class="flex flex-col w-full transition-all duration-300"
|
||||
:class="{'md:w-[calc(100vw-rem)] md:ml-64':sidebarStatus, 'md:w-screen md:ml-0':!sidebarStatus}"
|
||||
>
|
||||
|
||||
494
resources/js/Pages/Dashboard/Tramites.vue
Normal file
494
resources/js/Pages/Dashboard/Tramites.vue
Normal file
@ -0,0 +1,494 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
|
||||
import Bars from "@/Components/Dashboard/Charts/Bars.vue";
|
||||
import Pie from "@/Components/Dashboard/Charts/Pie.vue";
|
||||
import DateRange from "@/Components/Dashboard/Form/DateRange.vue";
|
||||
|
||||
const data = ref(null);
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
|
||||
// Rango inicial: Hoy→Hoy (coincide con preset del DateRange)
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const dateRange = ref({ start: today, end: today });
|
||||
|
||||
// toggles
|
||||
const showBars = ref(false);
|
||||
const showPie = ref(false);
|
||||
|
||||
// datasets
|
||||
const barChartData = ref({ labels: [], datasets: [] });
|
||||
const pieChartData = ref({ labels: [], datasets: [] });
|
||||
|
||||
// opciones reutilizables
|
||||
const barChartOptions = {
|
||||
responsive: true,
|
||||
makeDataset : (label, values) => ({
|
||||
label,
|
||||
data: values,
|
||||
backgroundColor: "rgba(75,192,192,0.2)",
|
||||
borderColor: "rgba(75,192,192,1)",
|
||||
borderWidth: 1,
|
||||
}),
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
callback: (_value, index) => {
|
||||
const lab = barChartData.value.labels[index] || "";
|
||||
return lab.split(" ").map((w) => w.toUpperCase());
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const pieChartOptions = { responsive: true, maintainAspectRatio: false };
|
||||
|
||||
// helpers
|
||||
const diffDays = (a, b) => {
|
||||
if (!a || !b) return 0;
|
||||
const A = new Date(a + "T00:00:00");
|
||||
const B = new Date(b + "T00:00:00");
|
||||
return Math.max(0, Math.round((B - A) / (1000 * 60 * 60 * 24)));
|
||||
};
|
||||
|
||||
const mode = computed(() => {
|
||||
const d = diffDays(dateRange.value.start, dateRange.value.end);
|
||||
if (d === 0) return "día";
|
||||
if (d <= 7) return "semana";
|
||||
return "rango";
|
||||
});
|
||||
|
||||
let abortController = null;
|
||||
let debounceId = null;
|
||||
|
||||
const fetchReport = async ({ start, end }) => {
|
||||
if (abortController) abortController.abort();
|
||||
abortController = new AbortController();
|
||||
const qs = new URLSearchParams();
|
||||
if (start) qs.set("start", start);
|
||||
if (end) qs.set("end", end);
|
||||
|
||||
const res = await fetch(
|
||||
`/dashboard/api/proxy-reporte-especial?${qs.toString()}`,
|
||||
{
|
||||
signal: abortController.signal,
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error("Error de red");
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const payload = await fetchReport({
|
||||
start: dateRange.value.start,
|
||||
end: dateRange.value.end,
|
||||
});
|
||||
data.value = payload;
|
||||
mapToCharts(payload, mode.value);
|
||||
} catch (e) {
|
||||
if (e.name !== "AbortError") {
|
||||
console.error(e);
|
||||
error.value = e.message || "Error desconocido";
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
dateRange,
|
||||
() => {
|
||||
clearTimeout(debounceId);
|
||||
debounceId = setTimeout(load, 350);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(load);
|
||||
|
||||
// --- mapping charts: usa SIEMPRE el rango ---
|
||||
function mapToCharts(api, currentMode) {
|
||||
const list = Array.isArray(api?.movements_by_procedure)
|
||||
? api.movements_by_procedure
|
||||
: [];
|
||||
|
||||
const labels = list.map((it) => (it.name || "").toUpperCase());
|
||||
|
||||
// Pie: total (opened + closed). Si tu API devuelve valores por rango, puedes sustituir aquí.
|
||||
const pieValues = list.map((it) => (it.opened || 0) + (it.closed || 0));
|
||||
pieChartData.value = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Recaudados",
|
||||
data: pieValues,
|
||||
backgroundColor: labels.map(
|
||||
(_, i) => `hsl(${(i * 360) / Math.max(1, labels.length)}, 70%, 50%)`
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Bars: dataset según modo
|
||||
let barLabel = "";
|
||||
let barValues = [];
|
||||
|
||||
if (currentMode === "día") {
|
||||
barLabel = "Solicitados Hoy";
|
||||
barValues = list.map(
|
||||
(it) => it.procedures_opened_today ?? it.opened_today ?? 0
|
||||
);
|
||||
} else if (currentMode === "semana") {
|
||||
barLabel = "Solicitados Semana";
|
||||
barValues = list.map((it) => it.total ?? 0);
|
||||
} else {
|
||||
barLabel = "Solicitados en Rango";
|
||||
barValues = list.map((it) => {
|
||||
if (typeof it.range_total === "number") return it.range_total;
|
||||
return (it.opened || 0) + (it.closed || 0);
|
||||
});
|
||||
}
|
||||
|
||||
barChartData.value = {
|
||||
labels,
|
||||
datasets: [barChartOptions.makeDataset(barLabel, barValues)],
|
||||
};
|
||||
}
|
||||
|
||||
// UI helpers
|
||||
const toggleBars = () => (showBars.value = !showBars.value);
|
||||
const togglePie = () => (showPie.value = !showPie.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loading">Cargando...</div>
|
||||
<div v-else-if="error">Error: {{ error }}</div>
|
||||
<div v-else>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex px-4 sm:px-6 lg:px-8 mb-4 items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">
|
||||
Reporte Especial de Trámites
|
||||
</h1>
|
||||
<p class="text-gray-600">Resumen por unidad administrativa</p>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-3">
|
||||
<button
|
||||
@click="toggleBars"
|
||||
class="bg-primary text-white px-4 py-2 rounded"
|
||||
>
|
||||
Gráfica de Barras
|
||||
</button>
|
||||
<button
|
||||
@click="togglePie"
|
||||
class="bg-primary text-white px-4 py-2 rounded"
|
||||
>
|
||||
Más recaudados
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estadísticas del día -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-sm border border-gray-200 p-6"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center"
|
||||
></div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">
|
||||
Trámites Abiertos Hoy
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
new Intl.NumberFormat().format(
|
||||
data.procedures_opened_today
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-sm border border-gray-200 p-6"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center"
|
||||
></div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">
|
||||
Con Movimientos Hoy
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{
|
||||
new Intl.NumberFormat().format(
|
||||
data.procedures_with_movements_today
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de trámites por administración -->
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden w-full"
|
||||
>
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
Trámites por Unidad Administrativa
|
||||
</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto w-full">
|
||||
<table class="w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Unidad Administrativa
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Abiertos
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Movimientos Hoy
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Cerrados
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Total
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr
|
||||
v-for="administration in data.procedures_by_administration ||
|
||||
[]"
|
||||
:key="administration.id"
|
||||
class="hover:bg-gray-50 transition-colors duration-200"
|
||||
>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ administration.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
||||
>
|
||||
{{
|
||||
new Intl.NumberFormat().format(administration.opened)
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"
|
||||
>
|
||||
{{
|
||||
new Intl.NumberFormat().format(
|
||||
administration.movement_today
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
|
||||
>
|
||||
{{
|
||||
new Intl.NumberFormat().format(administration.closed)
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-semibold text-gray-900">
|
||||
{{
|
||||
new Intl.NumberFormat().format(
|
||||
administration.opened + administration.closed
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="
|
||||
!(
|
||||
data.procedures_by_administration &&
|
||||
data.procedures_by_administration.length
|
||||
)
|
||||
"
|
||||
>
|
||||
<td colspan="5" class="px-6 py-8 text-center">
|
||||
<div class="text-gray-500">No hay datos disponibles</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de trámites por procedimiento -->
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden w-full mt-8"
|
||||
>
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
Trámites por Procedimiento
|
||||
</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto w-full">
|
||||
<table class="w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Procedimiento
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Abiertos
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Movimientos Hoy
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Cerrados
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Total
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr
|
||||
v-for="procedure in data.movements_by_procedure || []"
|
||||
:key="procedure.id"
|
||||
class="hover:bg-gray-50 transition-colors duration-200"
|
||||
>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ procedure.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
||||
>
|
||||
{{ new Intl.NumberFormat().format(procedure.opened) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800"
|
||||
>
|
||||
{{
|
||||
new Intl.NumberFormat().format(
|
||||
procedure.movement_today
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
|
||||
>
|
||||
{{ new Intl.NumberFormat().format(procedure.closed) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-semibold text-gray-900">
|
||||
{{
|
||||
new Intl.NumberFormat().format(
|
||||
procedure.opened + procedure.closed
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="
|
||||
!(
|
||||
data.movements_by_procedure &&
|
||||
data.movements_by_procedure.length
|
||||
)
|
||||
"
|
||||
>
|
||||
<td colspan="5" class="px-6 py-8 text-center">
|
||||
<div class="text-gray-500">No hay datos disponibles</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bars -->
|
||||
<div
|
||||
v-if="showBars"
|
||||
class="max-w-7xl mx-auto mt-12 px-4 sm:px-6 lg:px-8 mb-8 bg-white rounded-lg shadow p-6"
|
||||
>
|
||||
<!-- DateRange mejorado -->
|
||||
<DateRange v-model="dateRange" :presets="true" class="mb-12"/>
|
||||
<Bars :chartData="barChartData" :chartOptions="barChartOptions" />
|
||||
<p class="text-sm text-gray-500 mt-2">Modo actual: {{ mode }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Pie -->
|
||||
<div
|
||||
v-if="showPie"
|
||||
class="max-w-7xl mx-auto mt-8 px-4 sm:px-6 lg:px-8 bg-white rounded-lg shadow p-6"
|
||||
>
|
||||
<Pie :chartData="pieChartData" :chartOptions="pieChartOptions" />
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center">
|
||||
<p class="text-sm text-gray-500">
|
||||
Reporte generado el {{ new Date().toLocaleString("es-MX") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -7,86 +7,91 @@
|
||||
use App\Http\Controllers\Developer\RoleController;
|
||||
use App\Http\Controllers\Example\IndexController as ExampleIndexController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Rutas generales/publicas
|
||||
*
|
||||
*
|
||||
* Rutas accesibles por todos los usuarios y no usuarios
|
||||
*/
|
||||
Route::redirect('/', '/login');
|
||||
|
||||
/**
|
||||
* Rutas del Dashboard
|
||||
*
|
||||
*
|
||||
* El dashboard es el panel de los usuarios de forma general
|
||||
*/
|
||||
Route::prefix('dashboard')->name('dashboard.')->middleware([
|
||||
'auth:sanctum',
|
||||
'verified',
|
||||
config('jetstream.auth_session')
|
||||
'auth:sanctum',
|
||||
'verified',
|
||||
config('jetstream.auth_session')
|
||||
])->group(function () {
|
||||
Route::get('/welcome', [IndexController::class, 'index'])->name('index');
|
||||
Route::inertia('/changelogs', 'Dashboard/Changelogs')->name('changelogs');
|
||||
Route::inertia('/help', 'Dashboard/Help')->name('help');
|
||||
Route::get('/welcome', [IndexController::class, 'index'])->name('index');
|
||||
Route::inertia('/changelogs', 'Dashboard/Changelogs')->name('changelogs');
|
||||
Route::inertia('/help', 'Dashboard/Help')->name('help');
|
||||
|
||||
# Log de Acciones
|
||||
Route::resource('histories', HistoryLogController::class)->only([
|
||||
'index',
|
||||
'store'
|
||||
]);
|
||||
Route::inertia('/api-example', 'Dashboard/Tramites')->name('api-example');
|
||||
Route::get('/api/proxy-reporte-especial', function () {
|
||||
$response = Http::get('https://tramites.comalcalco.gob.mx/reporte-especial?type=api');
|
||||
return $response->json();
|
||||
});
|
||||
|
||||
Route::resource('notifications', NotificationController::class);
|
||||
Route::prefix('/users')->name('users.')->group(function()
|
||||
{
|
||||
Route::get('/notifications', [UserController::class, 'getNotifications'])->name('notifications');
|
||||
});
|
||||
# Log de Acciones
|
||||
Route::resource('histories', HistoryLogController::class)->only([
|
||||
'index',
|
||||
'store'
|
||||
]);
|
||||
|
||||
Route::resource('notifications', NotificationController::class);
|
||||
Route::prefix('/users')->name('users.')->group(function () {
|
||||
Route::get('/notifications', [UserController::class, 'getNotifications'])->name('notifications');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Rutas de administrador
|
||||
*
|
||||
*
|
||||
* Estas ubicaciones son del administrador, sin embargo el desarrollador
|
||||
* puede acceder a ellas.
|
||||
*/
|
||||
Route::prefix('admin')->name('admin.')->middleware([
|
||||
'auth:sanctum',
|
||||
config('jetstream.auth_session')
|
||||
'auth:sanctum',
|
||||
config('jetstream.auth_session')
|
||||
])->group(function () {
|
||||
Route::resource('users', UserController::class);
|
||||
Route::resource('users', UserController::class);
|
||||
|
||||
Route::prefix('/users')->name('users.')->group(function()
|
||||
{
|
||||
Route::get('{user}/settings', [UserController::class, 'settings'])->name('settings');
|
||||
Route::post('/password', [UserController::class, 'updatePassword'])->name('password');
|
||||
Route::post('/syncRoles', [UserController::class, 'syncRoles'])->name('syncRoles');
|
||||
});
|
||||
Route::prefix('/users')->name('users.')->group(function () {
|
||||
Route::get('{user}/settings', [UserController::class, 'settings'])->name('settings');
|
||||
Route::post('/password', [UserController::class, 'updatePassword'])->name('password');
|
||||
Route::post('/syncRoles', [UserController::class, 'syncRoles'])->name('syncRoles');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Rutas solo del desarrollador
|
||||
*
|
||||
*
|
||||
* Son ubicaciones o funciones que pueden llegar a ser muy sensibles en el sistema, por lo que
|
||||
* solo el desarrollador debe de ser capaz de modificarlas o actualizarlas.
|
||||
*/
|
||||
Route::prefix('developer')->name('developer.')->middleware([
|
||||
'auth:sanctum',
|
||||
config('jetstream.auth_session')
|
||||
'auth:sanctum',
|
||||
config('jetstream.auth_session')
|
||||
])->group(function () {
|
||||
Route::resource('roles', RoleController::class);
|
||||
Route::resource('roles', RoleController::class);
|
||||
});
|
||||
|
||||
/**
|
||||
* Elementos de la plantilla
|
||||
*
|
||||
*
|
||||
* Estos son elementos que existen y pueden ser usados en la plantilla, vienen ejemplos de uso.
|
||||
*
|
||||
*
|
||||
* Estas rutas pueden ser comentadas o eliminadas cuando se finalice un proyecto. Por default estan ocultas
|
||||
* en el dashboard.
|
||||
*/
|
||||
Route::prefix('examples')->name('examples.')->middleware([
|
||||
'auth:sanctum',
|
||||
'verified',
|
||||
config('jetstream.auth_session')
|
||||
'auth:sanctum',
|
||||
'verified',
|
||||
config('jetstream.auth_session')
|
||||
])->group(function () {
|
||||
Route::get('/', [ExampleIndexController::class, 'index'])->name('index');
|
||||
});
|
||||
Route::get('/', [ExampleIndexController::class, 'index'])->name('index');
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user