mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
feat(printers): sort by ETA (#1609)
Adds an "ETA" option to the Printers page sort dropdown.
Sorts the fleet by remaining print time so the printer that's
finishing next sits at the top — useful for staging the next
job's filament ahead of time.
Tier ordering:
- Tier 0: currently printing with remaining_time > 0
(sorted ascending by remaining minutes)
- Tier 1: currently printing without an ETA yet
(post-start_print window before total time is known)
- Tier 2: idle / finished
- Tier 3: offline
Name tiebreaker within every tier. The asc / desc arrow
still applies after tiers resolve.
Data source is the cached remaining_time (minutes) on the
per-printer status query — the same field the per-card ETA
label and the fleet "next finish" badge already read from.
No new backend round-trip; the sort consumes data that's
already in React Query cache and updated on every WebSocket
push.
groupedPrinters returns null for ETA too — every printer's
ETA is unique so section headers would just produce a header
per row. Flat list, like the existing name sort.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -182,6 +182,7 @@ export default {
|
||||
status: 'Status',
|
||||
model: 'Modell',
|
||||
location: 'Standort',
|
||||
eta: 'Restzeit',
|
||||
ascending: 'Aufsteigend sortieren',
|
||||
descending: 'Absteigend sortieren',
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
status: 'Status',
|
||||
model: 'Model',
|
||||
location: 'Location',
|
||||
eta: 'ETA',
|
||||
ascending: 'Sort ascending',
|
||||
descending: 'Sort descending',
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
status: 'Estado',
|
||||
model: 'Modelo',
|
||||
location: 'Ubicación',
|
||||
eta: 'Tiempo restante',
|
||||
ascending: 'Orden ascendente',
|
||||
descending: 'Orden descendente',
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
status: 'Statut',
|
||||
model: 'Modèle',
|
||||
location: 'Emplacement',
|
||||
eta: 'Temps restant',
|
||||
ascending: 'Tri croissant',
|
||||
descending: 'Tri décroissant',
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
status: 'Stato',
|
||||
model: 'Modello',
|
||||
location: 'Posizione',
|
||||
eta: 'Tempo rimanente',
|
||||
ascending: 'Ordina crescente',
|
||||
descending: 'Ordina decrescente',
|
||||
},
|
||||
|
||||
@@ -181,6 +181,7 @@ export default {
|
||||
status: 'ステータス',
|
||||
model: 'モデル',
|
||||
location: 'ロケーション',
|
||||
eta: '残り時間',
|
||||
ascending: '昇順で並べ替え',
|
||||
descending: '降順で並べ替え',
|
||||
},
|
||||
|
||||
@@ -170,6 +170,7 @@ export default {
|
||||
status: '상태',
|
||||
model: '모델',
|
||||
location: '위치',
|
||||
eta: '남은 시간',
|
||||
ascending: '오름차순 정렬',
|
||||
descending: '내림차순 정렬'
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
status: 'Status',
|
||||
model: 'Modelo',
|
||||
location: 'Localização',
|
||||
eta: 'Tempo restante',
|
||||
ascending: 'Ordem crescente',
|
||||
descending: 'Ordem decrescente',
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
status: 'Durum',
|
||||
model: 'Model',
|
||||
location: 'Konum',
|
||||
eta: 'Kalan süre',
|
||||
ascending: 'Artan sırala',
|
||||
descending: 'Azalan sırala',
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
status: '状态',
|
||||
model: '型号',
|
||||
location: '位置',
|
||||
eta: '剩余时间',
|
||||
ascending: '升序排列',
|
||||
descending: '降序排列',
|
||||
},
|
||||
|
||||
@@ -182,6 +182,7 @@ export default {
|
||||
status: '狀態',
|
||||
model: '型號',
|
||||
location: '位置',
|
||||
eta: '剩餘時間',
|
||||
ascending: '升序排列',
|
||||
descending: '降序排列',
|
||||
},
|
||||
|
||||
@@ -1095,7 +1095,7 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
);
|
||||
}
|
||||
|
||||
type SortOption = 'name' | 'status' | 'model' | 'location';
|
||||
type SortOption = 'name' | 'status' | 'model' | 'location' | 'eta';
|
||||
type ViewMode = 'expanded' | 'compact';
|
||||
|
||||
type ToolbarDropdownOption<T extends string> = {
|
||||
@@ -8014,6 +8014,28 @@ export function PrintersPage() {
|
||||
return getPriority(statusA) - getPriority(statusB);
|
||||
});
|
||||
break;
|
||||
case 'eta':
|
||||
sorted.sort((a, b) => {
|
||||
const statusA = queryClient.getQueryData<{ connected: boolean; state: string | null; remaining_time: number | null }>(['printerStatus', a.id]);
|
||||
const statusB = queryClient.getQueryData<{ connected: boolean; state: string | null; remaining_time: number | null }>(['printerStatus', b.id]);
|
||||
|
||||
const tier = (s: typeof statusA) => {
|
||||
if (!s?.connected) return 3; // offline last
|
||||
if (s.state === 'RUNNING' && s.remaining_time != null && s.remaining_time > 0) return 0; // printing with ETA
|
||||
if (s.state === 'RUNNING') return 1; // printing without ETA
|
||||
return 2; // idle
|
||||
};
|
||||
|
||||
const ta = tier(statusA);
|
||||
const tb = tier(statusB);
|
||||
if (ta !== tb) return ta - tb;
|
||||
if (ta === 0) {
|
||||
const diff = (statusA!.remaining_time ?? 0) - (statusB!.remaining_time ?? 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply ascending/descending
|
||||
@@ -8069,7 +8091,7 @@ export function PrintersPage() {
|
||||
|
||||
// Group printers when sorted by location, status, or model
|
||||
const groupedPrinters = useMemo(() => {
|
||||
if (sortBy === 'name') return null;
|
||||
if (sortBy === 'name' || sortBy === 'eta') return null;
|
||||
|
||||
const groups: Record<string, typeof sortedPrinters> = {};
|
||||
|
||||
@@ -8207,6 +8229,7 @@ export function PrintersPage() {
|
||||
{ value: 'status', label: t('printers.sort.status') },
|
||||
{ value: 'model', label: t('printers.sort.model') },
|
||||
{ value: 'location', label: t('printers.sort.location') },
|
||||
{ value: 'eta', label: t('printers.sort.eta') },
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-BPZeidkD.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DSkiM9pi.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D9kvaB_m.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user