Files
thetool/public/mobile/components/SyncStatus.js
2026-01-27 10:35:15 +01:00

226 lines
8.5 KiB
JavaScript

/**
* Sync Status Component
*
* Shows detailed sync status including pending count,
* last sync time, and manual sync button.
*/
const { ref, computed } = Vue;
export default {
name: 'SyncStatus',
props: {
// Number of pending operations
pendingOperations: {
type: Number,
default: 0
},
// Number of pending photos
pendingPhotos: {
type: Number,
default: 0
},
// Number of failed operations
failedCount: {
type: Number,
default: 0
},
// Last sync timestamp text
lastSyncText: {
type: String,
default: 'Nie synchronisiert'
},
// Whether sync is running
isSyncing: {
type: Boolean,
default: false
},
// Whether device is online
isOnline: {
type: Boolean,
default: true
},
// Sync progress info
syncProgress: {
type: Object,
default: null
}
},
emits: ['sync', 'retry-failed'],
setup(props, { emit }) {
// Total pending count
const totalPending = computed(() => {
return props.pendingOperations + props.pendingPhotos;
});
// Status summary text
const summaryText = computed(() => {
const parts = [];
if (props.pendingOperations > 0) {
parts.push(`${props.pendingOperations} Änderung${props.pendingOperations === 1 ? '' : 'en'}`);
}
if (props.pendingPhotos > 0) {
parts.push(`${props.pendingPhotos} Foto${props.pendingPhotos === 1 ? '' : 's'}`);
}
if (parts.length === 0) {
return 'Keine ausstehenden Änderungen';
}
return parts.join(', ') + ' ausstehend';
});
// Progress text during sync
const progressText = computed(() => {
if (!props.syncProgress) return '';
const { phase, current, total, fileName } = props.syncProgress;
if (phase === 'operations') {
return `Synchronisiere ${current}/${total} Änderungen...`;
}
if (phase === 'photos') {
return fileName
? `Lade ${current}/${total} hoch: ${fileName}`
: `Lade ${current}/${total} Fotos hoch...`;
}
return 'Synchronisiere...';
});
// Handle sync button click
const handleSync = () => {
if (!props.isSyncing && props.isOnline) {
emit('sync');
}
};
// Handle retry failed click
const handleRetryFailed = () => {
if (!props.isSyncing) {
emit('retry-failed');
}
};
return {
totalPending,
summaryText,
progressText,
handleSync,
handleRetryFailed
};
},
template: `
<div class="bg-white dark:bg-slate-800 rounded-xl p-4 shadow-sm border border-slate-200 dark:border-slate-700">
<!-- Header -->
<div class="flex items-center justify-between mb-3">
<h3 class="font-medium text-slate-900 dark:text-white flex items-center gap-2">
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Synchronisation
</h3>
<!-- Online/Offline badge -->
<span
:class="[
'px-2 py-0.5 rounded text-xs font-medium',
isOnline
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300'
]"
>
{{ isOnline ? 'Online' : 'Offline' }}
</span>
</div>
<!-- Sync progress bar (when syncing) -->
<div v-if="isSyncing && syncProgress" class="mb-3">
<div class="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 mb-1">
<span>{{ progressText }}</span>
<span v-if="syncProgress.total">{{ Math.round((syncProgress.current / syncProgress.total) * 100) }}%</span>
</div>
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-1.5">
<div
class="bg-primary h-1.5 rounded-full transition-all duration-300"
:style="{ width: syncProgress.total ? ((syncProgress.current / syncProgress.total) * 100) + '%' : '50%' }"
></div>
</div>
</div>
<!-- Summary -->
<div class="space-y-2 mb-4">
<!-- Pending count -->
<div class="flex items-center justify-between text-sm">
<span class="text-slate-600 dark:text-slate-400">Ausstehend</span>
<span
:class="[
'font-medium',
totalPending > 0 ? 'text-orange-600 dark:text-orange-400' : 'text-slate-900 dark:text-white'
]"
>
{{ summaryText }}
</span>
</div>
<!-- Failed count (if any) -->
<div v-if="failedCount > 0" class="flex items-center justify-between text-sm">
<span class="text-slate-600 dark:text-slate-400">Fehlgeschlagen</span>
<button
@click="handleRetryFailed"
class="text-red-600 dark:text-red-400 font-medium hover:underline"
:disabled="isSyncing"
>
{{ failedCount }} - Erneut versuchen
</button>
</div>
<!-- Last sync -->
<div class="flex items-center justify-between text-sm">
<span class="text-slate-600 dark:text-slate-400">Letzte Synchronisation</span>
<span class="text-slate-900 dark:text-white">{{ lastSyncText }}</span>
</div>
</div>
<!-- Sync button -->
<button
@click="handleSync"
:disabled="isSyncing || !isOnline || totalPending === 0"
:class="[
'w-full py-2.5 px-4 rounded-lg font-medium transition-colors flex items-center justify-center gap-2',
(isSyncing || !isOnline || totalPending === 0)
? 'bg-slate-100 dark:bg-slate-700 text-slate-400 dark:text-slate-500 cursor-not-allowed'
: 'bg-primary text-white hover:bg-primary/90'
]"
>
<!-- Spinner when syncing -->
<svg v-if="isSyncing" 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"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<!-- Sync icon -->
<svg v-else 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span>
{{ isSyncing ? 'Synchronisiere...' : !isOnline ? 'Offline' : totalPending === 0 ? 'Alles synchronisiert' : 'Jetzt synchronisieren' }}
</span>
</button>
<!-- Offline hint -->
<p v-if="!isOnline && totalPending > 0" class="mt-2 text-xs text-slate-500 dark:text-slate-400 text-center">
Änderungen werden synchronisiert, sobald Sie wieder online sind.
</p>
</div>
`
};