Files
secu-frontend/src/views/ModulesView.vue
OpenClaw e38467e146 feat: Add module visibility management
- ModulesView: new 'Sichtbarkeit' tab with role matrix
- Chef can toggle visibility per module per role
- Sidebar filters modules based on user role visibility
- Quick actions for bulk enable/disable
2026-03-13 10:44:22 +00:00

342 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { api } from '@/api'
const { t } = useI18n()
interface Module {
id: string
name: string
display_name: string
description?: string
is_core: boolean
enabled: boolean
config: Record<string, unknown>
}
interface ModuleVisibility {
module_key: string
role_disponent: boolean
role_mitarbeiter: boolean
role_subunternehmer: boolean
}
const modules = ref<Module[]>([])
const loading = ref(true)
const systemStatus = ref<any>(null)
const visibility = ref<ModuleVisibility[]>([])
const savingVisibility = ref(false)
const activeTab = ref<'modules' | 'visibility'>('modules')
const moduleList = [
{ key: 'qualifications', name: '🎓 Qualifikationen', icon: '🎓' },
{ key: 'shifts', name: '📅 Schichtplanung', icon: '📅' },
{ key: 'patrols', name: '📍 Rundgänge', icon: '📍' },
{ key: 'incidents', name: '🚨 Vorfälle', icon: '🚨' },
{ key: 'documents', name: '📁 Dokumente', icon: '📁' },
{ key: 'vehicles', name: '🚗 Fahrzeuge', icon: '🚗' },
{ key: 'customers', name: '🤝 Kunden', icon: '🤝' },
{ key: 'billing', name: '💰 Abrechnung', icon: '💰' },
{ key: 'partnerships', name: '🔗 Partnerschaften', icon: '🔗' },
]
onMounted(async () => {
await Promise.all([loadModules(), loadSystemStatus(), loadVisibility()])
})
async function loadModules() {
loading.value = true
try {
const res = await api.get<{ modules: Module[] }>('/modules/org')
modules.value = res.data.modules
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
async function loadSystemStatus() {
try {
const res = await api.get<any>('/modules/developer/status')
systemStatus.value = res.data
} catch (e) {
console.log('Dev status not available')
}
}
async function loadVisibility() {
try {
const res = await api.get<{ visibility: ModuleVisibility[] }>('/modules/visibility')
visibility.value = res.data.visibility
// Ensure all modules have visibility entries
for (const mod of moduleList) {
if (!visibility.value.find(v => v.module_key === mod.key)) {
visibility.value.push({
module_key: mod.key,
role_disponent: true,
role_mitarbeiter: true,
role_subunternehmer: false
})
}
}
} catch (e) {
console.error(e)
}
}
async function toggleModule(mod: Module) {
if (mod.is_core) return
try {
await api.post(`/modules/${mod.id}/toggle`, { enabled: !mod.enabled })
mod.enabled = !mod.enabled
} catch (e) {
alert(e instanceof Error ? e.message : t('messages.error'))
}
}
function getVisibility(moduleKey: string): ModuleVisibility {
return visibility.value.find(v => v.module_key === moduleKey) || {
module_key: moduleKey,
role_disponent: true,
role_mitarbeiter: true,
role_subunternehmer: false
}
}
function toggleVisibility(moduleKey: string, role: 'role_disponent' | 'role_mitarbeiter' | 'role_subunternehmer') {
const vis = visibility.value.find(v => v.module_key === moduleKey)
if (vis) {
vis[role] = !vis[role]
}
}
async function saveVisibility() {
savingVisibility.value = true
try {
await api.put('/modules/visibility', { settings: visibility.value })
alert(t('messages.saved'))
} catch (e) {
alert(e instanceof Error ? e.message : t('messages.error'))
} finally {
savingVisibility.value = false
}
}
function setAllForRole(role: 'role_disponent' | 'role_mitarbeiter' | 'role_subunternehmer', value: boolean) {
for (const vis of visibility.value) {
vis[role] = value
}
}
</script>
<template>
<div class="space-y-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"> {{ t('modules.title') }}</h1>
<!-- System Status -->
<div v-if="systemStatus" class="card bg-gradient-to-r from-primary-500 to-primary-700 text-white">
<h2 class="text-lg font-semibold mb-4">System Status</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p class="text-primary-100 text-sm">{{ t('users.employees') }}</p>
<p class="text-2xl font-bold">{{ systemStatus.stats?.user_count || 0 }}</p>
</div>
<div>
<p class="text-primary-100 text-sm">{{ t('orders.title') }}</p>
<p class="text-2xl font-bold">{{ systemStatus.stats?.order_count || 0 }}</p>
</div>
<div>
<p class="text-primary-100 text-sm">{{ t('timesheets.title') }}</p>
<p class="text-2xl font-bold">{{ systemStatus.stats?.timesheet_count || 0 }}</p>
</div>
<div>
<p class="text-primary-100 text-sm">{{ t('modules.enabled') }}</p>
<p class="text-2xl font-bold">{{ systemStatus.stats?.enabled_modules || 0 }}</p>
</div>
</div>
</div>
<!-- Tabs -->
<div class="flex gap-2 border-b dark:border-gray-700">
<button
@click="activeTab = 'modules'"
:class="['px-4 py-2 font-medium border-b-2 transition-colors',
activeTab === 'modules' ? 'border-primary-500 text-primary-600' : 'border-transparent text-gray-500 hover:text-gray-700']"
>
📦 {{ t('modules.title') }}
</button>
<button
@click="activeTab = 'visibility'"
:class="['px-4 py-2 font-medium border-b-2 transition-colors',
activeTab === 'visibility' ? 'border-primary-500 text-primary-600' : 'border-transparent text-gray-500 hover:text-gray-700']"
>
👁 Sichtbarkeit
</button>
</div>
<!-- Modules Tab -->
<div v-if="activeTab === 'modules'" class="card">
<h2 class="text-lg font-semibold mb-4">{{ t('modules.title') }}</h2>
<div v-if="loading" class="text-center py-8 text-gray-500">{{ t('app.loading') }}</div>
<div v-else class="space-y-4">
<div
v-for="mod in modules"
:key="mod.id"
class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"
>
<div class="flex-1">
<div class="flex items-center gap-2">
<h3 class="font-medium text-gray-900 dark:text-white">{{ mod.display_name }}</h3>
<span v-if="mod.is_core" class="badge badge-primary">Core</span>
</div>
<p v-if="mod.description" class="text-sm text-gray-500 mt-1">{{ mod.description }}</p>
</div>
<button
:disabled="mod.is_core"
:class="[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
mod.enabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-gray-600',
mod.is_core ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
]"
@click="toggleModule(mod)"
>
<span
:class="[
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
mod.enabled ? 'translate-x-6' : 'translate-x-1'
]"
/>
</button>
</div>
</div>
</div>
<!-- Visibility Tab -->
<div v-if="activeTab === 'visibility'" class="card">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-lg font-semibold">👁 Modul-Sichtbarkeit pro Rolle</h2>
<p class="text-sm text-gray-500 mt-1">Legen Sie fest, welche Module für welche Rollen sichtbar sind.</p>
</div>
<button
@click="saveVisibility"
:disabled="savingVisibility"
class="btn btn-primary"
>
{{ savingVisibility ? t('app.loading') : t('app.save') }}
</button>
</div>
<!-- Quick Actions -->
<div class="flex flex-wrap gap-2 mb-6 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span class="text-sm text-gray-500 mr-2">Schnellauswahl:</span>
<button @click="setAllForRole('role_disponent', true)" class="text-xs btn btn-secondary">
Disponent: Alle an
</button>
<button @click="setAllForRole('role_disponent', false)" class="text-xs btn btn-secondary">
Disponent: Alle aus
</button>
<button @click="setAllForRole('role_mitarbeiter', true)" class="text-xs btn btn-secondary">
Mitarbeiter: Alle an
</button>
<button @click="setAllForRole('role_mitarbeiter', false)" class="text-xs btn btn-secondary">
Mitarbeiter: Alle aus
</button>
</div>
<!-- Visibility Matrix -->
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="text-left text-sm text-gray-500 border-b border-gray-200 dark:border-gray-700">
<th class="pb-3 pr-4">Modul</th>
<th class="pb-3 px-4 text-center">
<div class="flex flex-col items-center">
<span>👔</span>
<span class="text-xs">Disponent</span>
</div>
</th>
<th class="pb-3 px-4 text-center">
<div class="flex flex-col items-center">
<span>👷</span>
<span class="text-xs">Mitarbeiter</span>
</div>
</th>
<th class="pb-3 px-4 text-center">
<div class="flex flex-col items-center">
<span>🏢</span>
<span class="text-xs">Subuntern.</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="mod in moduleList"
:key="mod.key"
class="border-b border-gray-100 dark:border-gray-800"
>
<td class="py-3 pr-4">
<span class="font-medium">{{ mod.name }}</span>
</td>
<td class="py-3 px-4 text-center">
<button
:class="[
'w-8 h-8 rounded-lg transition-colors flex items-center justify-center',
getVisibility(mod.key).role_disponent
? 'bg-green-100 text-green-600 hover:bg-green-200'
: 'bg-gray-100 text-gray-400 hover:bg-gray-200'
]"
@click="toggleVisibility(mod.key, 'role_disponent')"
>
{{ getVisibility(mod.key).role_disponent ? '✓' : '✗' }}
</button>
</td>
<td class="py-3 px-4 text-center">
<button
:class="[
'w-8 h-8 rounded-lg transition-colors flex items-center justify-center',
getVisibility(mod.key).role_mitarbeiter
? 'bg-green-100 text-green-600 hover:bg-green-200'
: 'bg-gray-100 text-gray-400 hover:bg-gray-200'
]"
@click="toggleVisibility(mod.key, 'role_mitarbeiter')"
>
{{ getVisibility(mod.key).role_mitarbeiter ? '✓' : '✗' }}
</button>
</td>
<td class="py-3 px-4 text-center">
<button
:class="[
'w-8 h-8 rounded-lg transition-colors flex items-center justify-center',
getVisibility(mod.key).role_subunternehmer
? 'bg-green-100 text-green-600 hover:bg-green-200'
: 'bg-gray-100 text-gray-400 hover:bg-gray-200'
]"
@click="toggleVisibility(mod.key, 'role_subunternehmer')"
>
{{ getVisibility(mod.key).role_subunternehmer ? '✓' : '✗' }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p class="text-sm text-blue-700 dark:text-blue-300">
<strong>💡 Hinweis:</strong> Chef sieht immer alle Module.
Diese Einstellungen betreffen nur Disponenten, Mitarbeiter und Subunternehmer.
</p>
</div>
</div>
</div>
</template>