garázs fejlesztés

This commit is contained in:
Roo
2026-07-20 11:17:52 +00:00
parent 2e0abc62a7
commit 4594de6c11
80 changed files with 9135 additions and 8702 deletions

View File

@@ -265,6 +265,32 @@
</svg>
<p class="text-sm text-slate-400">{{ $t('users.details.no_person_record') }}</p>
</div>
<!-- ============================================================ -->
<!-- Danger Zone (Superadmin only) -->
<!-- ============================================================ -->
<div v-if="isSuperadmin" class="border border-red-500/30 bg-red-500/5 rounded-xl p-6">
<div class="flex items-center gap-3 mb-4">
<div class="p-2 rounded-lg bg-red-500/10 text-red-400">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div>
<h3 class="text-base font-semibold text-red-400">{{ $t('users.details.danger_zone') }}</h3>
<p class="text-xs text-slate-400 mt-0.5">{{ $t('users.details.danger_zone_warning') }}</p>
</div>
</div>
<button
@click="openDangerModal"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-red-600/20 text-red-300 hover:bg-red-600/30 border border-red-500/30 rounded-lg transition"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
{{ $t('users.details.danger_zone_button') }}
</button>
</div>
</div>
<!-- ============================================================ -->
@@ -570,6 +596,73 @@
</div>
</div>
<!-- ============================================================ -->
<!-- Danger Zone Modal (Double Opt-in) -->
<!-- ============================================================ -->
<div
v-if="showDangerModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
>
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="closeDangerModal"></div>
<!-- Modal -->
<div class="relative w-full max-w-md bg-slate-800 border border-red-500/30 rounded-2xl shadow-2xl">
<!-- Header -->
<div class="px-6 py-4 border-b border-red-500/20 flex items-center gap-3">
<div class="p-2 rounded-lg bg-red-500/10 text-red-400">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 class="text-lg font-semibold text-red-400">{{ $t('users.details.danger_zone') }}</h3>
</div>
<!-- Body -->
<div class="p-6 space-y-4">
<p class="text-sm text-slate-300">
{{ $t('users.details.danger_zone_warning') }}
</p>
<div class="bg-slate-700/50 rounded-lg p-3">
<p class="text-xs text-slate-400 mb-1">{{ $t('users.details.danger_zone_confirm_label') }}</p>
<p class="text-sm text-white font-mono">{{ user?.email }}</p>
</div>
<div>
<input
v-model="dangerConfirmEmail"
type="email"
:placeholder="$t('users.details.danger_zone_confirm_placeholder')"
class="w-full px-3 py-2 text-sm bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500"
/>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-slate-700 flex items-center justify-end gap-3">
<button
@click="closeDangerModal"
:disabled="dangerDeleting"
class="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white bg-slate-700/50 hover:bg-slate-700 rounded-lg transition disabled:opacity-50"
>
{{ $t('users.details.danger_zone_cancel') }}
</button>
<button
@click="executeHardDelete"
:disabled="dangerConfirmEmail !== user?.email || dangerDeleting"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium bg-red-600 text-white hover:bg-red-500 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg v-if="dangerDeleting" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
{{ $t('users.details.danger_zone_confirm_button') }}
</button>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- Notification Toast -->
<!-- ============================================================ -->
@@ -634,6 +727,11 @@ const editForm = ref({
person_birth_date: '',
})
// Danger Zone Modal
const showDangerModal = ref(false)
const dangerConfirmEmail = ref('')
const dangerDeleting = ref(false)
// Notification
const notification = ref({ show: false, type: 'success' as 'success' | 'error', message: '' })
@@ -691,6 +789,69 @@ const hasEditPermission = computed(() => {
return !!auth.user?.system_capabilities?.['users:edit']
})
const isSuperadmin = computed(() => {
return auth.user?.role === 'SUPERADMIN'
})
// Danger Zone functions
function openDangerModal() {
dangerConfirmEmail.value = ''
showDangerModal.value = true
}
function closeDangerModal() {
showDangerModal.value = false
dangerConfirmEmail.value = ''
}
async function executeHardDelete() {
if (dangerConfirmEmail.value !== user.value?.email) return
dangerDeleting.value = true
try {
const tokenCookie = useCookie('access_token')
const token = (tokenCookie.value || auth.token || '').trim()
const headers: Record<string, string> = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
}
const res = await fetch(`/api/v1/admin/users/${userId}/hard`, {
method: 'DELETE',
headers,
})
if (!res.ok) {
const errData = await res.json().catch(() => ({}))
const detail = errData.detail || ''
if (res.status === 403) {
throw new Error('danger_zone_error_not_superadmin')
}
if (res.status === 400 && detail.includes('financial')) {
throw new Error('danger_zone_error_financial')
}
throw new Error(detail || `Delete failed: ${res.status}`)
}
closeDangerModal()
showNotification('success', 'danger_zone_success')
// Redirect to users list after a short delay
setTimeout(() => {
router.push('/users')
}, 2000)
} catch (e: any) {
const msg = e.message || 'danger_zone_error_generic'
// Check if it's a translation key
if (msg.startsWith('danger_zone_error')) {
showNotification('error', msg)
} else {
showNotification('error', 'danger_zone_error_generic')
}
} finally {
dangerDeleting.value = false
}
}
// API calls
async function fetchUser() {
loading.value = true
@@ -858,7 +1019,9 @@ async function saveEdit() {
// Notification helper
function showNotification(type: 'success' | 'error', message: string) {
notification.value = { show: true, type, message }
// If message is a translation key, translate it
const translated = $t(message.startsWith('danger_zone') ? `users.details.${message}` : message)
notification.value = { show: true, type, message: translated }
setTimeout(() => {
notification.value.show = false
}, 4000)
@@ -973,4 +1136,4 @@ function memberStatusDotClass(status: string): string {
onMounted(() => {
fetchUser()
})
</script>
</script>