Initial commit
This commit is contained in:
231
frontend-internal/src/routes/users/+page.svelte
Normal file
231
frontend-internal/src/routes/users/+page.svelte
Normal file
@@ -0,0 +1,231 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/utils/api';
|
||||
import { toast } from '$lib/stores/toast';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
|
||||
let users = [];
|
||||
let tenants = [];
|
||||
let isLoading = false;
|
||||
let showModal = false;
|
||||
let editingUser = null;
|
||||
|
||||
let formData = {
|
||||
email: '',
|
||||
password: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role: 'AGENT',
|
||||
tenant_id: '',
|
||||
is_active: true
|
||||
};
|
||||
|
||||
const ROLES = [
|
||||
{ value: 'ADMIN', label: 'Administrador (Global)' },
|
||||
{ value: 'SUPPORT_MANAGER', label: 'Gerente de Soporte' },
|
||||
{ value: 'AGENT', label: 'Agente de Soporte' },
|
||||
{ value: 'AUDITOR', label: 'Auditor' },
|
||||
{ value: 'CLIENT_ADMIN', label: 'Admin Cliente' },
|
||||
{ value: 'CLIENT_USER', label: 'Usuario Cliente' }
|
||||
];
|
||||
|
||||
async function loadData() {
|
||||
isLoading = true;
|
||||
try {
|
||||
const [usersData, tenantsData] = await Promise.all([
|
||||
api.get('/users/'),
|
||||
api.get('/tenants/')
|
||||
]);
|
||||
users = usersData;
|
||||
tenants = tenantsData;
|
||||
} catch (e) {
|
||||
toast.error('Error cargando datos');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
editingUser = null;
|
||||
formData = {
|
||||
email: '',
|
||||
password: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
role: 'AGENT',
|
||||
tenant_id: '',
|
||||
is_active: true
|
||||
};
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
function openEditModal(user) {
|
||||
editingUser = user;
|
||||
formData = {
|
||||
email: user.email,
|
||||
password: '', // Don't show password
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
role: user.role,
|
||||
tenant_id: user.tenant_id || '',
|
||||
is_active: user.is_active
|
||||
};
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const payload = { ...formData };
|
||||
if (!payload.password) delete payload.password; // Don't send empty password on edit
|
||||
if (!payload.tenant_id) payload.tenant_id = null; // Send null if empty string
|
||||
|
||||
if (editingUser) {
|
||||
await api.put(`/users/${editingUser.id}`, payload);
|
||||
toast.success('Usuario actualizado');
|
||||
} else {
|
||||
if (!payload.password) {
|
||||
toast.error('La contraseña es requerida para nuevos usuarios');
|
||||
return;
|
||||
}
|
||||
await api.post('/users/', payload);
|
||||
toast.success('Usuario creado');
|
||||
}
|
||||
showModal = false;
|
||||
loadData();
|
||||
} catch (e) {
|
||||
toast.error(e.message || 'Error guardando usuario');
|
||||
}
|
||||
}
|
||||
|
||||
function getTenantName(id) {
|
||||
if (!id) return '-';
|
||||
const t = tenants.find(t => t.id === id);
|
||||
return t ? t.name : id;
|
||||
}
|
||||
|
||||
onMount(loadData);
|
||||
</script>
|
||||
|
||||
<div class="px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-xl font-semibold text-gray-900">Usuarios</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">Gestión de usuarios internos y de clientes.</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<button
|
||||
type="button"
|
||||
on:click={openCreateModal}
|
||||
class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 sm:w-auto"
|
||||
>
|
||||
Nuevo Usuario
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-col">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Usuario</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Rol</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Cliente (Tenant)</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Estado</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Acciones</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#if isLoading}
|
||||
<tr><td colspan="5" class="text-center py-4">Cargando...</td></tr>
|
||||
{:else if users.length === 0}
|
||||
<tr><td colspan="5" class="text-center py-4">No hay usuarios registrados</td></tr>
|
||||
{:else}
|
||||
{#each users as user}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
|
||||
<div class="font-medium text-gray-900">{user.first_name} {user.last_name}</div>
|
||||
<div class="text-gray-500">{user.email}</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.role}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{getTenantName(user.tenant_id)}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<span class:bg-green-100={user.is_active} class:text-green-800={user.is_active} class:bg-red-100={!user.is_active} class:text-red-800={!user.is_active} class="inline-flex rounded-full px-2 text-xs font-semibold leading-5">
|
||||
{user.is_active ? 'Activo' : 'Inactivo'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<button on:click={() => openEditModal(user)} class="text-indigo-600 hover:text-indigo-900">Editar</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal open={showModal} title={editingUser ? 'Editar Usuario' : 'Nuevo Usuario'} on:close={() => showModal = false}>
|
||||
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="first_name" class="block text-sm font-medium text-gray-700">Nombre</label>
|
||||
<input type="text" id="first_name" bind:value={formData.first_name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
|
||||
</div>
|
||||
<div>
|
||||
<label for="last_name" class="block text-sm font-medium text-gray-700">Apellido</label>
|
||||
<input type="text" id="last_name" bind:value={formData.last_name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input type="email" id="email" bind:value={formData.email} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Contraseña {editingUser ? '(dejar en blanco para mantener)' : ''}</label>
|
||||
<input type="password" id="password" bind:value={formData.password} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="role" class="block text-sm font-medium text-gray-700">Rol</label>
|
||||
<select id="role" bind:value={formData.role} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
|
||||
{#each ROLES as role}
|
||||
<option value={role.value}>{role.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="tenant" class="block text-sm font-medium text-gray-700">Cliente (Opcional - solo para usuarios externos)</label>
|
||||
<select id="tenant" bind:value={formData.tenant_id} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
|
||||
<option value="">-- Ninguno (Usuario Interno) --</option>
|
||||
{#each tenants as tenant}
|
||||
<option value={tenant.id}>{tenant.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="is_active" bind:checked={formData.is_active} class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
<label for="is_active" class="ml-2 block text-sm text-gray-900">Activo</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
|
||||
<button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:col-start-2 sm:text-sm">
|
||||
Guardar
|
||||
</button>
|
||||
<button type="button" on:click={() => showModal = false} class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:col-start-1 sm:text-sm">
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
Reference in New Issue
Block a user