feat: File Locking System (Ein-/Auschecken) + Konflikt-Email
Backend - FileLock Model + API: - POST /files/<id>/lock - Datei auschecken (sperren) - POST /files/<id>/unlock - Datei einchecken (entsperren) - POST /files/<id>/heartbeat - "Datei noch offen" (alle 60s) - GET /files/<id>/lock-status - Sperrstatus abfragen - GET /files/locks - Alle aktiven Sperren auflisten - Auto-Unlock: Kein Heartbeat seit 5 Min -> Sperre wird freigegeben - 423 Locked wenn bereits von anderem User gesperrt - Admin kann fremde Sperren aufheben Dateiliste + Sync-API: - Lock-Info (locked, locked_by, locked_at) pro Datei mitgeliefert - Sync-Tree enthaelt Lock-Status fuer Desktop/Mobile-Clients Web-UI: - Schloss-Icon mit Benutzername bei gesperrten Dateien - Tooltip: "Ausgecheckt von Adam seit 14:30" - Gesperrte Dateien: "Oeffnen nicht moeglich" Toast-Meldung (eigene Sperren sind erlaubt) Konflikt-Email an Admin: - Wer hat die Konflikt-Kopie erstellt (Name + Email) - Welche Datei (Name + Ordnerpfad) - Name der Konflikt-Kopie - Von wem gesperrt (Name + Email + seit wann) - Erklaerungstext was passiert ist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,9 @@
|
||||
<i :class="fileIcon(data)" class="file-icon"></i>
|
||||
<span>{{ data.name }}</span>
|
||||
<Tag v-if="data.shared" value="Geteilt" severity="info" class="shared-tag" />
|
||||
<span v-if="data.locked" class="lock-badge" :title="'Ausgecheckt von ' + data.locked_by + ' seit ' + formatDate(data.locked_at)">
|
||||
<i class="pi pi-lock"></i> {{ data.locked_by }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
@@ -223,6 +226,7 @@
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useFilesStore } from '../stores/files'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import apiClient from '../api/client'
|
||||
@@ -238,6 +242,7 @@ import ProgressBar from 'primevue/progressbar'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const filesStore = useFilesStore()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -298,6 +303,15 @@ function handleDoubleClick(event) {
|
||||
}
|
||||
|
||||
function openPreview(data) {
|
||||
if (data.locked && data.locked_by !== auth.user?.username) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Datei gesperrt',
|
||||
detail: `${data.name} wird von ${data.locked_by} bearbeitet. Oeffnen nicht moeglich.`,
|
||||
life: 5000,
|
||||
})
|
||||
return
|
||||
}
|
||||
const previewable = /\.(pdf|docx?|xlsx?|pptx?|txt|md|json|xml|csv|py|js|html|css|yml|yaml|png|jpe?g|gif|svg|webp|bmp|odt|ods|odp|rtf)$/i
|
||||
if (previewable.test(data.name)) {
|
||||
router.push(`/preview/${data.id}`)
|
||||
@@ -706,6 +720,12 @@ onMounted(() => {
|
||||
}
|
||||
.file-icon { font-size: 1.125rem; width: 1.25rem; text-align: center; }
|
||||
.shared-tag { font-size: 0.7rem; }
|
||||
.lock-badge {
|
||||
display: inline-flex; align-items: center; gap: 0.25rem;
|
||||
font-size: 0.7rem; color: var(--p-orange-600); background: var(--p-orange-50);
|
||||
padding: 0.125rem 0.375rem; border-radius: 4px; margin-left: 0.25rem;
|
||||
}
|
||||
.lock-badge i { font-size: 0.65rem; }
|
||||
.row-actions { display: flex; gap: 0; }
|
||||
.empty-state {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user