174 lines
4.5 KiB
Vue
174 lines
4.5 KiB
Vue
<template>
|
|
<v-card
|
|
:color="tileColor"
|
|
variant="tonal"
|
|
class="h-100 d-flex flex-column tile-wrapper"
|
|
:class="{ 'draggable-tile': draggable }"
|
|
>
|
|
<!-- Drag Handle -->
|
|
<div v-if="draggable" class="drag-handle d-flex align-center justify-center pa-2" @mousedown.prevent>
|
|
<v-icon icon="mdi-drag-vertical" size="small" class="text-disabled"></v-icon>
|
|
</div>
|
|
|
|
<!-- Tile Header -->
|
|
<v-card-title class="d-flex align-center justify-space-between pa-3 pb-0">
|
|
<div class="d-flex align-center">
|
|
<v-icon :icon="tileIcon" class="mr-2"></v-icon>
|
|
<span class="text-subtitle-1 font-weight-bold">{{ tile.title }}</span>
|
|
</div>
|
|
<div class="d-flex align-center">
|
|
<!-- RBAC Badge -->
|
|
<v-chip size="small" :color="accessLevelColor" class="text-caption mr-1">
|
|
{{ accessLevelText }}
|
|
</v-chip>
|
|
<!-- Visibility Toggle -->
|
|
<v-btn
|
|
v-if="showVisibilityToggle"
|
|
icon
|
|
size="x-small"
|
|
variant="text"
|
|
@click="toggleVisibility"
|
|
:title="tile.preference?.visible ? 'Hide tile' : 'Show tile'"
|
|
>
|
|
<v-icon :icon="tile.preference?.visible ? 'mdi-eye' : 'mdi-eye-off'"></v-icon>
|
|
</v-btn>
|
|
</div>
|
|
</v-card-title>
|
|
|
|
<!-- Tile Content Slot -->
|
|
<v-card-text class="flex-grow-1 pa-3">
|
|
<slot>
|
|
<!-- Default content if no slot provided -->
|
|
<p class="text-body-2">{{ tile.description }}</p>
|
|
</slot>
|
|
</v-card-text>
|
|
|
|
<!-- Tile Footer Actions -->
|
|
<v-card-actions class="mt-auto pa-3 pt-0">
|
|
<v-spacer></v-spacer>
|
|
<v-btn
|
|
v-if="showActionButton"
|
|
variant="text"
|
|
size="small"
|
|
:prepend-icon="actionIcon"
|
|
@click="handleTileClick"
|
|
>
|
|
{{ actionText }}
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useTileStore } from '~/stores/tiles'
|
|
import type { TilePermission } from '~/composables/useRBAC'
|
|
|
|
interface Props {
|
|
tile: TilePermission
|
|
draggable?: boolean
|
|
showVisibilityToggle?: boolean
|
|
showActionButton?: boolean
|
|
actionIcon?: string
|
|
actionText?: string
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
draggable: true,
|
|
showVisibilityToggle: true,
|
|
showActionButton: true,
|
|
actionIcon: 'mdi-open-in-new',
|
|
actionText: 'Open'
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
click: [tile: TilePermission]
|
|
toggleVisibility: [tileId: string, visible: boolean]
|
|
}>()
|
|
|
|
const tileStore = useTileStore()
|
|
|
|
// Tile color based on ID
|
|
const tileColor = computed(() => {
|
|
const colors: Record<string, string> = {
|
|
'ai-logs': 'indigo',
|
|
'financial-dashboard': 'green',
|
|
'salesperson-hub': 'orange',
|
|
'user-management': 'blue',
|
|
'service-moderation-map': 'teal',
|
|
'gamification-control': 'purple',
|
|
'system-health': 'red'
|
|
}
|
|
return colors[props.tile.id] || 'surface'
|
|
})
|
|
|
|
// Tile icon based on ID
|
|
const tileIcon = computed(() => {
|
|
const icons: Record<string, string> = {
|
|
'ai-logs': 'mdi-robot',
|
|
'financial-dashboard': 'mdi-chart-line',
|
|
'salesperson-hub': 'mdi-account-tie',
|
|
'user-management': 'mdi-account-group',
|
|
'service-moderation-map': 'mdi-map',
|
|
'gamification-control': 'mdi-trophy',
|
|
'system-health': 'mdi-heart-pulse'
|
|
}
|
|
return icons[props.tile.id] || 'mdi-view-dashboard'
|
|
})
|
|
|
|
// Access level indicator
|
|
const accessLevelColor = computed(() => {
|
|
if (props.tile.minRank && props.tile.minRank > 5) return 'warning'
|
|
if (props.tile.requiredRole?.includes('admin')) return 'error'
|
|
return 'success'
|
|
})
|
|
|
|
const accessLevelText = computed(() => {
|
|
if (props.tile.minRank) return `Rank ${props.tile.minRank}+`
|
|
if (props.tile.requiredRole?.length) return props.tile.requiredRole[0]
|
|
return 'All'
|
|
})
|
|
|
|
// Methods
|
|
function handleTileClick() {
|
|
emit('click', props.tile)
|
|
}
|
|
|
|
function toggleVisibility() {
|
|
const newVisible = !props.tile.preference?.visible
|
|
tileStore.toggleTileVisibility(props.tile.id)
|
|
emit('toggleVisibility', props.tile.id, newVisible)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.tile-wrapper {
|
|
position: relative;
|
|
transition: box-shadow 0.2s ease-in-out, transform 0.2s ease-in-out;
|
|
}
|
|
|
|
.tile-wrapper:hover {
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.drag-handle {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 24px;
|
|
background-color: rgba(0, 0, 0, 0.02);
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
cursor: grab;
|
|
z-index: 1;
|
|
}
|
|
|
|
.drag-handle:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.draggable-tile {
|
|
user-select: none;
|
|
}
|
|
</style> |