Files
space-booking/frontend/src/views/Users.vue
Claude Agent 6edf87c899 refactor(frontend): simplify booking forms and convert to modals
- Simplify BookingForm: remove recurring bookings, templates, and attachments
- Replace datetime-local with separate date/time inputs for better UX
- Convert inline forms to modals (Admin spaces, Users, SpaceDetail)
- Unify Create and Edit booking forms with identical styling and structure
- Add Space field to Edit modal (read-only)
- Fix calendar initial load and set week start to Monday
- Translate all form labels and messages to Romanian

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 15:36:22 +00:00

623 lines
14 KiB
Vue

<template>
<div class="users">
<div class="page-header">
<h2>Admin Dashboard - User Management</h2>
<button class="btn btn-primary" @click="openCreateModal">
Create New User
</button>
</div>
<!-- Filters -->
<div class="card">
<h3>Filters</h3>
<div class="filters">
<div class="form-group">
<label for="filter-role">Filter by Role</label>
<select id="filter-role" v-model="filterRole" @change="loadUsers">
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<div class="form-group">
<label for="filter-org">Filter by Organization</label>
<input
id="filter-org"
v-model="filterOrganization"
type="text"
placeholder="Enter organization name"
@input="loadUsers"
/>
</div>
</div>
</div>
<!-- Users List -->
<div class="card">
<h3>All Users</h3>
<div v-if="loadingUsers" class="loading">Loading users...</div>
<div v-else-if="users.length === 0" class="empty">
No users found. {{ filterRole || filterOrganization ? 'Try different filters.' : 'Create one above!' }}
</div>
<table v-else class="users-table">
<thead>
<tr>
<th>Email</th>
<th>Full Name</th>
<th>Role</th>
<th>Organization</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.email }}</td>
<td>{{ user.full_name }}</td>
<td>
<span :class="['badge', user.role === 'admin' ? 'badge-admin' : 'badge-user']">
{{ user.role }}
</span>
</td>
<td>{{ user.organization || '-' }}</td>
<td>
<span :class="['badge', user.is_active ? 'badge-active' : 'badge-inactive']">
{{ user.is_active ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="actions">
<button
class="btn btn-sm btn-secondary"
@click="startEdit(user)"
:disabled="loading"
>
Edit
</button>
<button
:class="['btn', 'btn-sm', user.is_active ? 'btn-warning' : 'btn-success']"
@click="toggleStatus(user)"
:disabled="loading"
>
{{ user.is_active ? 'Deactivate' : 'Activate' }}
</button>
<button
class="btn btn-sm btn-danger"
@click="showResetPassword(user)"
:disabled="loading"
>
Reset Password
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Create/Edit User Modal -->
<div v-if="showFormModal" class="modal" @click.self="closeFormModal">
<div class="modal-content">
<h3>{{ editingUser ? 'Edit User' : 'Create New User' }}</h3>
<form @submit.prevent="handleSubmit" class="user-form">
<div class="form-group">
<label for="email">Email *</label>
<input
id="email"
v-model="formData.email"
type="email"
required
placeholder="user@example.com"
:disabled="!!editingUser"
/>
</div>
<div class="form-group">
<label for="full_name">Full Name *</label>
<input
id="full_name"
v-model="formData.full_name"
type="text"
required
placeholder="John Doe"
/>
</div>
<div class="form-group" v-if="!editingUser">
<label for="password">Password *</label>
<input
id="password"
v-model="formData.password"
type="password"
:required="!editingUser"
placeholder="Minimum 8 characters"
minlength="8"
/>
</div>
<div class="form-group">
<label for="role">Role *</label>
<select id="role" v-model="formData.role" required>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group">
<label for="organization">Organization</label>
<input
id="organization"
v-model="formData.organization"
type="text"
placeholder="Optional organization"
/>
</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="success" class="success">{{ success }}</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="loading">
{{ editingUser ? 'Update' : 'Create' }}
</button>
<button type="button" class="btn btn-secondary" @click="closeFormModal">
Cancel
</button>
</div>
</form>
</div>
</div>
<!-- Reset Password Modal -->
<div v-if="resetPasswordUser" class="modal" @click.self="closeResetPassword">
<div class="modal-content">
<h3>Reset Password for {{ resetPasswordUser.full_name }}</h3>
<form @submit.prevent="handleResetPassword">
<div class="form-group">
<label for="new_password">New Password *</label>
<input
id="new_password"
v-model="newPassword"
type="password"
required
placeholder="Minimum 8 characters"
minlength="8"
/>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="loading">
Reset Password
</button>
<button type="button" class="btn btn-secondary" @click="closeResetPassword">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { usersApi, handleApiError } from '@/services/api'
import type { User } from '@/types'
const users = ref<User[]>([])
const loadingUsers = ref(false)
const loading = ref(false)
const error = ref('')
const success = ref('')
const editingUser = ref<User | null>(null)
const showFormModal = ref(false)
const resetPasswordUser = ref<User | null>(null)
const newPassword = ref('')
const filterRole = ref('')
const filterOrganization = ref('')
const formData = ref({
email: '',
full_name: '',
password: '',
role: 'user',
organization: ''
})
const loadUsers = async () => {
loadingUsers.value = true
error.value = ''
try {
const params: { role?: string; organization?: string } = {}
if (filterRole.value) params.role = filterRole.value
if (filterOrganization.value) params.organization = filterOrganization.value
users.value = await usersApi.list(params)
} catch (err) {
error.value = handleApiError(err)
} finally {
loadingUsers.value = false
}
}
const handleSubmit = async () => {
loading.value = true
error.value = ''
success.value = ''
try {
if (editingUser.value) {
await usersApi.update(editingUser.value.id, {
full_name: formData.value.full_name,
role: formData.value.role,
organization: formData.value.organization || undefined
})
success.value = 'User updated successfully!'
} else {
await usersApi.create({
email: formData.value.email,
full_name: formData.value.full_name,
password: formData.value.password,
role: formData.value.role,
organization: formData.value.organization || undefined
})
success.value = 'User created successfully!'
}
await loadUsers()
closeFormModal()
// Clear success message after 3 seconds
setTimeout(() => {
success.value = ''
}, 3000)
} catch (err) {
error.value = handleApiError(err)
} finally {
loading.value = false
}
}
const openCreateModal = () => {
resetForm()
showFormModal.value = true
}
const startEdit = (user: User) => {
editingUser.value = user
formData.value = {
email: user.email,
full_name: user.full_name,
password: '',
role: user.role,
organization: user.organization || ''
}
showFormModal.value = true
}
const closeFormModal = () => {
showFormModal.value = false
resetForm()
}
const resetForm = () => {
editingUser.value = null
formData.value = {
email: '',
full_name: '',
password: '',
role: 'user',
organization: ''
}
}
const toggleStatus = async (user: User) => {
loading.value = true
error.value = ''
success.value = ''
try {
await usersApi.updateStatus(user.id, !user.is_active)
success.value = `User ${user.is_active ? 'deactivated' : 'activated'} successfully!`
await loadUsers()
setTimeout(() => {
success.value = ''
}, 3000)
} catch (err) {
error.value = handleApiError(err)
} finally {
loading.value = false
}
}
const showResetPassword = (user: User) => {
resetPasswordUser.value = user
newPassword.value = ''
}
const closeResetPassword = () => {
resetPasswordUser.value = null
newPassword.value = ''
}
const handleResetPassword = async () => {
if (!resetPasswordUser.value) return
loading.value = true
error.value = ''
success.value = ''
try {
await usersApi.resetPassword(resetPasswordUser.value.id, newPassword.value)
success.value = 'Password reset successfully!'
closeResetPassword()
setTimeout(() => {
success.value = ''
}, 3000)
} catch (err) {
error.value = handleApiError(err)
} finally {
loading.value = false
}
}
onMounted(() => {
loadUsers()
})
</script>
<style scoped>
.users {
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.page-header h2 {
margin: 0;
}
.card {
background: white;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.form-group label {
font-weight: 500;
color: #374151;
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 14px;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-group input:disabled {
background: #f3f4f6;
cursor: not-allowed;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 8px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #2563eb;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: #4b5563;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover:not(:disabled) {
background: #059669;
}
.btn-warning {
background: #f59e0b;
color: white;
}
.btn-warning:hover:not(:disabled) {
background: #d97706;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #dc2626;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.error {
padding: 12px;
background: #fee2e2;
color: #991b1b;
border-radius: 4px;
margin-top: 12px;
}
.success {
padding: 12px;
background: #d1fae5;
color: #065f46;
border-radius: 4px;
margin-top: 12px;
}
.loading {
text-align: center;
color: #6b7280;
padding: 24px;
}
.empty {
text-align: center;
color: #9ca3af;
padding: 24px;
}
.users-table {
width: 100%;
border-collapse: collapse;
}
.users-table th {
text-align: left;
padding: 12px;
background: #f9fafb;
font-weight: 600;
color: #374151;
border-bottom: 2px solid #e5e7eb;
}
.users-table td {
padding: 12px;
border-bottom: 1px solid #e5e7eb;
}
.users-table tr:hover {
background: #f9fafb;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.badge-active {
background: #d1fae5;
color: #065f46;
}
.badge-inactive {
background: #fee2e2;
color: #991b1b;
}
.badge-admin {
background: #dbeafe;
color: #1e40af;
}
.badge-user {
background: #f3f4f6;
color: #374151;
}
.actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 24px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.modal-content h3 {
margin-bottom: 16px;
}
</style>