226 lines
8.5 KiB
JavaScript
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>
|
|
`
|
|
};
|