modificado uso de CORS en la api, ahora acepta localhost y planilla.interno.com
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { PrismaClient } from './prisma/generated/client/index.js';
|
import { PrismaClient } from './prisma/generated/client/index.js';
|
||||||
import { Decimal } from '@prisma/client/runtime/library.js';
|
import { Decimal } from '@prisma/client/runtime/library.js';
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
// Import new routers
|
// Import new routers
|
||||||
import empleadosRouter from './routes/empleados/empleados.js';
|
import empleadosRouter from './routes/empleados/empleados.js';
|
||||||
@@ -8,6 +9,12 @@ import asistenciasRouter from './routes/asistencias/asistencias.js';
|
|||||||
import tareasRouter from './routes/tareas/tareas.js';
|
import tareasRouter from './routes/tareas/tareas.js';
|
||||||
import planillasRouter from './routes/planillas/planillas.js';
|
import planillasRouter from './routes/planillas/planillas.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Resto del código
|
||||||
|
|
||||||
BigInt.prototype.toJSON = function () { return this.toString(); };
|
BigInt.prototype.toJSON = function () { return this.toString(); };
|
||||||
Decimal.prototype.toJSON = function () { return this.toString(); };
|
Decimal.prototype.toJSON = function () { return this.toString(); };
|
||||||
|
|
||||||
@@ -15,6 +22,11 @@ const prisma = new PrismaClient();
|
|||||||
export const app = express();
|
export const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: ['http://localhost:5173', 'https://planilla.interno.com'],
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
// Mount new routers
|
// Mount new routers
|
||||||
app.use('/api/empleados', empleadosRouter);
|
app.use('/api/empleados', empleadosRouter);
|
||||||
app.use('/api/asistencias', asistenciasRouter);
|
app.use('/api/asistencias', asistenciasRouter);
|
||||||
|
|||||||
@@ -5,9 +5,18 @@ import router from './router'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './style.css' // Tailwind o tus estilos globales
|
import './style.css' // Tailwind o tus estilos globales
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const app =
|
const app =
|
||||||
createApp(App)
|
createApp(App)
|
||||||
|
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: 'https://planilla.interno.com',
|
||||||
|
credentials: true // si usás cookies/sesiones
|
||||||
|
}));
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,84 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia';
|
||||||
|
import apiClient from '../apiClient'; // ← asegurate que tenga baseURL y headers
|
||||||
|
|
||||||
|
// Valor por defecto para el empleado activo
|
||||||
|
const getDefaultEmpleado = () => ({
|
||||||
|
id: null,
|
||||||
|
created_at: null,
|
||||||
|
updated_at: null,
|
||||||
|
name: '',
|
||||||
|
cedula: null,
|
||||||
|
ubicacion: '.', // default del schema
|
||||||
|
grupo_estudio: null,
|
||||||
|
empleado: true, // marcamos que SÍ es empleado
|
||||||
|
avatar_url: null,
|
||||||
|
telefono: null,
|
||||||
|
idciat: null,
|
||||||
|
// Ojo: asistencias, tareasRealizadas y planillas se manejan en otras stores
|
||||||
|
// No sé la forma exacta de esos arrays; se cargan aparte.
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useEmpleadosStore = defineStore('empleados', {
|
||||||
|
state: () => ({
|
||||||
|
empleados: [],
|
||||||
|
currentEmpleado: getDefaultEmpleado(),
|
||||||
|
}),
|
||||||
|
|
||||||
export const useEmpleados = defineStore('empleados', {
|
|
||||||
state: () => ({ empleados: [] }),
|
|
||||||
actions: {
|
actions: {
|
||||||
// placeholder para cargar/crear empleados
|
// ────────── CRUD básico ──────────
|
||||||
async fetchAll () {
|
async fetchEmpleados () {
|
||||||
// simulamos fetch
|
try {
|
||||||
this.empleados = []
|
const { data } = await apiClient.get('/api/empleados');
|
||||||
|
this.empleados = data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error al traer empleados:', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchEmpleadoById (id) {
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get(`/api/empleados/${id}`);
|
||||||
|
this.currentEmpleado = data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error al traer empleado ${id}:`, err);
|
||||||
|
this.currentEmpleado = getDefaultEmpleado();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createEmpleado (empleadoData) {
|
||||||
|
try {
|
||||||
|
await apiClient.post('/api/empleados', empleadoData);
|
||||||
|
await this.fetchEmpleados();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creando empleado:', err);
|
||||||
|
throw err; // para que el form muestre feedback
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateEmpleado (id, empleadoData) {
|
||||||
|
try {
|
||||||
|
await apiClient.put(`/api/empleados/${id}`, empleadoData);
|
||||||
|
await this.fetchEmpleados();
|
||||||
|
this.clearCurrentEmpleado();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error actualizando empleado ${id}:`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteEmpleado (id) {
|
||||||
|
try {
|
||||||
|
await apiClient.delete(`/api/empleados/${id}`);
|
||||||
|
await this.fetchEmpleados();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error eliminando empleado ${id}:`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ────────── helpers ──────────
|
||||||
|
clearCurrentEmpleado () {
|
||||||
|
this.currentEmpleado = getDefaultEmpleado();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-6 bg-gray-50 min-h-screen">
|
<div class="p-6 bg-gray-50 min-h-screen">
|
||||||
|
<!-- … encabezado … -->
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h1 class="text-4xl font-bold text-gray-800">Gestión de Empleados</h1>
|
<h1 class="text-4xl font-bold text-gray-800">Gestión de Empleados</h1>
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
@click="goToCreateEmployee"
|
@click="goToCreateEmployee"
|
||||||
class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-150 ease-in-out"
|
class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-150 ease-in-out"
|
||||||
>
|
>
|
||||||
|
<!-- ícono ➕ -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-2" viewBox="0 0 20 20" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -16,53 +18,56 @@
|
|||||||
<p class="mt-2 text-gray-600">Visualiza, crea y gestiona los empleados de la organización.</p>
|
<p class="mt-2 text-gray-600">Visualiza, crea y gestiona los empleados de la organización.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- selector de vista -->
|
||||||
<div class="mb-6 flex justify-end items-center space-x-3">
|
<div class="mb-6 flex justify-end items-center space-x-3">
|
||||||
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
|
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
|
||||||
<button
|
<button
|
||||||
@click="currentView = 'card'"
|
@click="currentView = 'card'"
|
||||||
:class="[
|
:class="btnClass('card')"
|
||||||
'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2',
|
|
||||||
currentView === 'card'
|
|
||||||
? 'bg-blue-500 text-white shadow-sm focus:ring-blue-400'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
Tarjetas
|
Tarjetas
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="currentView = 'table'"
|
@click="currentView = 'table'"
|
||||||
:class="[
|
:class="btnClass('table')"
|
||||||
'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2',
|
|
||||||
currentView === 'table'
|
|
||||||
? 'bg-blue-500 text-white shadow-sm focus:ring-blue-400'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
Tabla
|
Tabla
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- contenido -->
|
||||||
<div>
|
<div>
|
||||||
<div v-if="loading" class="text-center py-10">
|
<div v-if="loading" class="text-center py-10">
|
||||||
<p class="text-gray-500 text-xl">Cargando empleados...</p>
|
<p class="text-gray-500 text-xl">Cargando empleados...</p>
|
||||||
<!-- You can add a spinner here -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="error" class="text-center py-10">
|
<div v-else-if="error" class="text-center py-10">
|
||||||
<p class="text-red-500 text-xl">Error al cargar los empleados: {{ error }}</p>
|
<p class="text-red-500 text-xl">Error al cargar los empleados: {{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Card View -->
|
<!-- vista de tarjetas -->
|
||||||
<div v-if="currentView === 'card'" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
<div
|
||||||
<CardEmpleado v-for="employee in employees" :key="employee.id" :employee="employee" />
|
v-if="currentView === 'card'"
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||||
|
>
|
||||||
|
<CardEmpleado
|
||||||
|
v-for="employee in employees"
|
||||||
|
:key="employee.id"
|
||||||
|
:employee="employee"
|
||||||
|
/>
|
||||||
<div v-if="employees.length === 0" class="col-span-full text-center py-10">
|
<div v-if="employees.length === 0" class="col-span-full text-center py-10">
|
||||||
<p class="text-gray-500 text-xl">No hay empleados para mostrar en la vista de tarjetas.</p>
|
<p class="text-gray-500 text-xl">No hay empleados para mostrar en la vista de tarjetas.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table View -->
|
<!-- vista de tabla -->
|
||||||
<div v-if="currentView === 'table'">
|
<div v-if="currentView === 'table'">
|
||||||
<TablaEmpleados :employees="employees" />
|
<TablaEmpleados :employees="employees" />
|
||||||
<div v-if="employees.length === 0" class="text-center py-10 bg-white shadow-md rounded-lg mt-4">
|
<div
|
||||||
|
v-if="employees.length === 0"
|
||||||
|
class="text-center py-10 bg-white shadow-md rounded-lg mt-4"
|
||||||
|
>
|
||||||
<p class="text-gray-500 text-xl">No hay empleados para mostrar en la vista de tabla.</p>
|
<p class="text-gray-500 text-xl">No hay empleados para mostrar en la vista de tabla.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,137 +77,59 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRouter } from 'vue-router'
|
import { storeToRefs } from 'pinia';
|
||||||
import CardEmpleado from '@/components/empleados/cardEmpleado.vue' // Adjusted path
|
import { useRouter } from 'vue-router';
|
||||||
import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue' // Adjusted path
|
|
||||||
|
|
||||||
// Define the structure of the employee object
|
import CardEmpleado from '@/components/empleados/cardEmpleado.vue';
|
||||||
interface Employee {
|
import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue';
|
||||||
id: string | number
|
import { useEmpleadosStore } from '@/stores/useEmpleados.js'; // ruta según tu proyecto
|
||||||
name: string
|
|
||||||
cedula: number
|
|
||||||
avatar_url?: string
|
|
||||||
telefono?: string
|
|
||||||
ubicacion: string
|
|
||||||
idciat?: string
|
|
||||||
grupo_estudio?: string
|
|
||||||
empleado: boolean // To ensure we are dealing with employees
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
// --- refs locales ---
|
||||||
const currentView = ref<'card' | 'table'>('card') // Default view
|
const router = useRouter();
|
||||||
const employees = ref<Employee[]>([])
|
const currentView = ref<'card' | 'table'>('card');
|
||||||
const loading = ref(true)
|
const loading = ref(true);
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
// Mock data for employees - replace with actual API call
|
// --- store ---
|
||||||
const mockEmployees: Employee[] = [
|
const empleadosStore = useEmpleadosStore();
|
||||||
{
|
const { empleados } = storeToRefs(empleadosStore);
|
||||||
id: '1',
|
|
||||||
name: 'Ana García',
|
// alias para plantilla
|
||||||
cedula: 123456789,
|
const employees = empleados;
|
||||||
avatar_url: 'https://randomuser.me/api/portraits/women/60.jpg',
|
|
||||||
telefono: '0991234567',
|
// --- helpers ---
|
||||||
ubicacion: 'Oficina Central',
|
const btnClass = (view: 'card' | 'table') => [
|
||||||
idciat: 'AG001',
|
'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||||
grupo_estudio: 'Desarrollo Frontend',
|
currentView.value === view
|
||||||
empleado: true,
|
? 'bg-blue-500 text-white shadow-sm focus:ring-blue-400'
|
||||||
},
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400',
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Carlos Rodriguez',
|
|
||||||
cedula: 987654321,
|
|
||||||
avatar_url: 'https://randomuser.me/api/portraits/men/45.jpg',
|
|
||||||
telefono: '0987654321',
|
|
||||||
ubicacion: 'Sucursal Norte',
|
|
||||||
idciat: 'CR002',
|
|
||||||
grupo_estudio: 'Backend Services',
|
|
||||||
empleado: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Luisa Martinez',
|
|
||||||
cedula: 112233445,
|
|
||||||
// avatar_url: '', // Test fallback avatar
|
|
||||||
telefono: '0976543210',
|
|
||||||
ubicacion: 'Remoto',
|
|
||||||
idciat: 'LM003',
|
|
||||||
// grupo_estudio: '', // Test missing optional field
|
|
||||||
empleado: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'Jorge Herrera',
|
|
||||||
cedula: 223344556,
|
|
||||||
avatar_url: 'https://randomuser.me/api/portraits/men/50.jpg',
|
|
||||||
telefono: '0965432109',
|
|
||||||
ubicacion: 'Oficina Central',
|
|
||||||
idciat: 'JH004',
|
|
||||||
grupo_estudio: 'QA Team',
|
|
||||||
empleado: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// --- fetch inicial ---
|
||||||
const fetchEmployees = async () => {
|
const fetchEmployees = async () => {
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
error.value = null
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
// Simulate API call delay
|
await empleadosStore.fetchEmpleados();
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// In a real application, you would fetch from your API:
|
|
||||||
// const response = await fetch('/api/clientes?empleado=true');
|
|
||||||
// if (!response.ok) {
|
|
||||||
// throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
// }
|
|
||||||
// const data = await response.json();
|
|
||||||
// employees.value = data.filter((cliente: Employee) => cliente.empleado);
|
|
||||||
|
|
||||||
// Using mock data for now
|
|
||||||
employees.value = mockEmployees.filter(e => e.empleado);
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch employees:", e);
|
console.error('Error al traer empleados:', e);
|
||||||
if (e instanceof Error) {
|
error.value = e instanceof Error ? e.message : 'Ocurrió un error desconocido.';
|
||||||
error.value = e.message;
|
|
||||||
} else {
|
|
||||||
error.value = "Ocurrió un error desconocido."
|
|
||||||
}
|
|
||||||
employees.value = []; // Clear employees on error or set to empty if preferred
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(fetchEmployees);
|
||||||
fetchEmployees()
|
|
||||||
})
|
|
||||||
|
|
||||||
const goToCreateEmployee = () => {
|
// --- navegación ---
|
||||||
router.push({ name: 'empleados-new' });
|
const goToCreateEmployee = () => router.push({ name: 'empleados-new' });
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Additional styling for the index page */
|
.min-h-screen { min-height: calc(100vh - var(--navbar-height, 0px)); }
|
||||||
.min-h-screen {
|
button.focus\:ring-blue-400:focus { box-shadow: 0 0 0 3px rgba(96, 165, 250, .5); }
|
||||||
min-height: calc(100vh - var(--navbar-height, 0px)); /* Adjust if you have a fixed navbar */
|
button.focus\:ring-gray-400:focus { box-shadow: 0 0 0 3px rgba(156, 163, 175, .5); }
|
||||||
}
|
.view-enter-active, .view-leave-active { transition: opacity .3s ease; }
|
||||||
|
.view-enter-from, .view-leave-to { opacity: 0; }
|
||||||
/* Styling for active/inactive toggle buttons */
|
|
||||||
button.focus\:ring-blue-400:focus { /* More specific focus for blue buttons */
|
|
||||||
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.5); /* Tailwind's blue-400 with 50% opacity */
|
|
||||||
}
|
|
||||||
button.focus\:ring-gray-400:focus { /* More specific focus for gray buttons */
|
|
||||||
box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.5); /* Tailwind's gray-400 with 50% opacity */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transition for views, if desired (can be more complex) */
|
|
||||||
.view-enter-active, .view-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
.view-enter-from, .view-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user