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
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
<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
|
||||
@@ -12,12 +15,34 @@ interface Module {
|
||||
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()])
|
||||
await Promise.all([loadModules(), loadSystemStatus(), loadVisibility()])
|
||||
})
|
||||
|
||||
async function loadModules() {
|
||||
@@ -37,11 +62,31 @@ async function loadSystemStatus() {
|
||||
const res = await api.get<any>('/modules/developer/status')
|
||||
systemStatus.value = res.data
|
||||
} catch (e) {
|
||||
// Developer module might not be enabled
|
||||
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
|
||||
|
||||
@@ -49,43 +94,95 @@ async function toggleModule(mod: Module) {
|
||||
await api.post(`/modules/${mod.id}/toggle`, { enabled: !mod.enabled })
|
||||
mod.enabled = !mod.enabled
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Fehler')
|
||||
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">⚙️ Module</h1>
|
||||
<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">Benutzer</p>
|
||||
<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">Aufträge</p>
|
||||
<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">Stundenzettel</p>
|
||||
<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">Aktive Module</p>
|
||||
<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>
|
||||
|
||||
<!-- Modules List -->
|
||||
<div class="card">
|
||||
<h2 class="text-lg font-semibold mb-4">Verfügbare Module</h2>
|
||||
<!-- 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>
|
||||
|
||||
<div v-if="loading" class="text-center py-8 text-gray-500">Lädt...</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
|
||||
@@ -121,12 +218,124 @@ async function toggleModule(mod: Module) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="text-lg font-semibold mb-2">Hinweis</h2>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Core-Module (Basis-System, Auftragsverwaltung) können nicht deaktiviert werden.
|
||||
Änderungen an Modulen werden sofort wirksam.
|
||||
</p>
|
||||
<!-- 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>
|
||||
|
||||
Reference in New Issue
Block a user