246 lines
8.7 KiB
Vue
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>
|