Files
lic/front-end/app/pages/sistema/usuarios.vue
2026-04-21 18:05:15 -03:00

246 lines
8.7 KiB
Vue

<!-- front-end/app/pages/sistema/usuarios.vue -->
<script setup lang="ts">
const { apiFetch } = useApi()
interface ApiUser {
ID: string
Email: string
Name: string
Role: string
IsActive: boolean
}
const { data: usuarios, refresh } = await useAsyncData('usuarios', () =>
apiFetch<ApiUser[]>('/users')
)
const roleLabel: Record<string, string> = {
admin: 'Administrador',
member: 'Membro',
}
// --- Modal Criar ---
const showCreate = ref(false)
const createForm = reactive({ name: '', email: '', password: '', role: 'member' })
const createError = ref('')
const createLoading = ref(false)
async function criarUsuario() {
createError.value = ''
createLoading.value = true
try {
await apiFetch('/users', {
method: 'POST',
body: { name: createForm.name, email: createForm.email, password: createForm.password, role: createForm.role },
})
showCreate.value = false
createForm.name = ''
createForm.email = ''
createForm.password = ''
createForm.role = 'member'
await refresh()
} catch (err: any) {
createError.value = err?.data?.error || 'Erro ao criar usuário.'
} finally {
createLoading.value = false
}
}
// --- Modal Editar ---
const showEdit = ref(false)
const editUser = ref<ApiUser | null>(null)
const editForm = reactive({ name: '', role: 'member' })
const editError = ref('')
const editLoading = ref(false)
function abrirEditar(u: ApiUser) {
editUser.value = u
editForm.name = u.Name
editForm.role = u.Role
editError.value = ''
showEdit.value = true
}
async function salvarEdicao() {
if (!editUser.value) return
editError.value = ''
editLoading.value = true
try {
await apiFetch(`/users/${editUser.value.ID}`, {
method: 'PUT',
body: { name: editForm.name, role: editForm.role },
})
showEdit.value = false
await refresh()
} catch (err: any) {
editError.value = err?.data?.error || 'Erro ao salvar.'
} finally {
editLoading.value = false
}
}
// --- Toggle status ---
const togglingId = ref<string | null>(null)
async function toggleStatus(u: ApiUser) {
togglingId.value = u.ID
try {
if (u.IsActive) {
await apiFetch(`/users/${u.ID}`, { method: 'DELETE' })
} else {
const isActive = true
await apiFetch(`/users/${u.ID}`, { method: 'PUT', body: { is_active: isActive } })
}
await refresh()
} catch {}
togglingId.value = null
}
</script>
<template>
<div class="page">
<AppTopbar title="Usuários" breadcrumb="Sistema · Usuários">
<template #actions>
<UButton size="sm" class="btn-primary" @click="showCreate = true">+ Novo Usuário</UButton>
</template>
</AppTopbar>
<div class="content">
<div class="card">
<table class="tbl">
<thead>
<tr>
<th>Nome</th>
<th>E-mail</th>
<th>Perfil</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="u in (usuarios ?? [])" :key="u.ID">
<td>{{ u.Name }}</td>
<td class="email">{{ u.Email }}</td>
<td>{{ roleLabel[u.Role] ?? u.Role }}</td>
<td>
<span :class="['badge', u.IsActive ? 'badge-active' : 'badge-inactive']">
{{ u.IsActive ? 'Ativo' : 'Inativo' }}
</span>
</td>
<td class="actions-cell">
<button class="act-btn" @click="abrirEditar(u)">Editar</button>
<button
class="act-btn"
:class="u.IsActive ? 'act-danger' : 'act-success'"
:disabled="togglingId === u.ID"
@click="toggleStatus(u)"
>
{{ u.IsActive ? 'Desativar' : 'Ativar' }}
</button>
</td>
</tr>
<tr v-if="!usuarios?.length">
<td colspan="5" class="empty">Nenhum usuário encontrado.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Modal Criar -->
<div v-if="showCreate" class="overlay">
<div class="modal">
<h2>Novo Usuário</h2>
<div class="field">
<label>Nome</label>
<UInput v-model="createForm.name" placeholder="Nome completo" class="w-full" />
</div>
<div class="field">
<label>E-mail</label>
<UInput v-model="createForm.email" type="email" placeholder="email@empresa.com" class="w-full" />
</div>
<div class="field">
<label>Senha</label>
<UInput v-model="createForm.password" type="password" placeholder="Mínimo 8 caracteres" class="w-full" />
</div>
<div class="field">
<label>Perfil</label>
<select v-model="createForm.role" class="sel">
<option value="member">Membro</option>
<option value="admin">Administrador</option>
</select>
</div>
<p v-if="createError" class="err">{{ createError }}</p>
<div class="modal-actions">
<button class="btn-cancel" @click="showCreate = false">Cancelar</button>
<UButton class="btn-primary" size="sm" :loading="createLoading" @click="criarUsuario">Criar</UButton>
</div>
</div>
</div>
<!-- Modal Editar -->
<div v-if="showEdit" class="overlay">
<div class="modal">
<h2>Editar Usuário</h2>
<div class="field">
<label>Nome</label>
<UInput v-model="editForm.name" class="w-full" />
</div>
<div class="field">
<label>Perfil</label>
<select v-model="editForm.role" class="sel">
<option value="member">Membro</option>
<option value="admin">Administrador</option>
</select>
</div>
<p v-if="editError" class="err">{{ editError }}</p>
<div class="modal-actions">
<button class="btn-cancel" @click="showEdit = false">Cancelar</button>
<UButton class="btn-primary" size="sm" :loading="editLoading" @click="salvarEdicao">Salvar</UButton>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.page { display: flex; flex-direction: column; height: 100vh; }
.content { padding: 20px 22px; flex: 1; overflow-y: auto; }
.card { background: white; border-radius: 11px; border: 1px solid #e2e8f0; overflow: hidden; }
.btn-primary { background: linear-gradient(135deg, #667eea, #764ba2) !important; }
.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
.tbl thead tr { background: #f8fafc; border-bottom: 1px solid #e2e8f0; }
.tbl th { padding: 10px 14px; text-align: left; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
.tbl td { padding: 12px 14px; border-bottom: 1px solid #f1f5f9; color: #1e293b; }
.tbl tbody tr:last-child td { border-bottom: none; }
.tbl tbody tr:hover { background: #fafafa; }
.email { color: #64748b; }
.empty { text-align: center; color: #94a3b8; padding: 32px !important; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; }
.badge-active { background: #dcfce7; color: #16a34a; }
.badge-inactive { background: #f1f5f9; color: #94a3b8; }
.actions-cell { display: flex; gap: 6px; }
.act-btn { font-size: 12px; font-weight: 500; padding: 4px 10px; border-radius: 6px; border: 1px solid #e2e8f0; background: white; cursor: pointer; color: #667eea; transition: all 0.15s; }
.act-btn:hover { background: #f0f3ff; border-color: #667eea; }
.act-danger { color: #dc2626; }
.act-danger:hover { background: #fff1f1; border-color: #dc2626; }
.act-success { color: #16a34a; }
.act-success:hover { background: #dcfce7; border-color: #16a34a; }
.act-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Modal */
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal { background: white; border-radius: 14px; padding: 28px; width: 100%; max-width: 400px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); }
.modal h2 { font-size: 16px; font-weight: 700; color: #0f172a; margin-bottom: 18px; }
.field { margin-bottom: 14px; }
.field label { display: block; font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 5px; }
.sel { width: 100%; padding: 8px 10px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 13px; color: #0f172a; background: white; }
.err { color: #dc2626; font-size: 12px; margin-bottom: 10px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.btn-cancel { font-size: 13px; padding: 6px 14px; border-radius: 8px; border: 1px solid #e2e8f0; background: white; cursor: pointer; color: #64748b; }
.btn-cancel:hover { background: #f8fafc; }
</style>