Просмотр исходного кода

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.
maziggy 2 месяцев назад
Родитель
Сommit
c930c0e80d

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -182,6 +182,7 @@ export default {
       status: 'Status',
       model: 'Modell',
       location: 'Standort',
+      eta: 'Restzeit',
       ascending: 'Aufsteigend sortieren',
       descending: 'Absteigend sortieren',
     },

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -182,6 +182,7 @@ export default {
       status: 'Status',
       model: 'Model',
       location: 'Location',
+      eta: 'ETA',
       ascending: 'Sort ascending',
       descending: 'Sort descending',
     },

+ 1 - 0
frontend/src/i18n/locales/es.ts

@@ -182,6 +182,7 @@ export default {
       status: 'Estado',
       model: 'Modelo',
       location: 'Ubicación',
+      eta: 'Tiempo restante',
       ascending: 'Orden ascendente',
       descending: 'Orden descendente',
     },

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -182,6 +182,7 @@ export default {
       status: 'Statut',
       model: 'Modèle',
       location: 'Emplacement',
+      eta: 'Temps restant',
       ascending: 'Tri croissant',
       descending: 'Tri décroissant',
     },

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -182,6 +182,7 @@ export default {
       status: 'Stato',
       model: 'Modello',
       location: 'Posizione',
+      eta: 'Tempo rimanente',
       ascending: 'Ordina crescente',
       descending: 'Ordina decrescente',
     },

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -181,6 +181,7 @@ export default {
       status: 'ステータス',
       model: 'モデル',
       location: 'ロケーション',
+      eta: '残り時間',
       ascending: '昇順で並べ替え',
       descending: '降順で並べ替え',
     },

+ 1 - 0
frontend/src/i18n/locales/ko.ts

@@ -170,6 +170,7 @@ export default {
       status: '상태',
       model: '모델',
       location: '위치',
+      eta: '남은 시간',
       ascending: '오름차순 정렬',
       descending: '내림차순 정렬'
     },

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -182,6 +182,7 @@ export default {
       status: 'Status',
       model: 'Modelo',
       location: 'Localização',
+      eta: 'Tempo restante',
       ascending: 'Ordem crescente',
       descending: 'Ordem decrescente',
     },

+ 1 - 0
frontend/src/i18n/locales/tr.ts

@@ -182,6 +182,7 @@ export default {
       status: 'Durum',
       model: 'Model',
       location: 'Konum',
+      eta: 'Kalan süre',
       ascending: 'Artan sırala',
       descending: 'Azalan sırala',
     },

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -182,6 +182,7 @@ export default {
       status: '状态',
       model: '型号',
       location: '位置',
+      eta: '剩余时间',
       ascending: '升序排列',
       descending: '降序排列',
     },

+ 1 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -182,6 +182,7 @@ export default {
       status: '狀態',
       model: '型號',
       location: '位置',
+      eta: '剩餘時間',
       ascending: '升序排列',
       descending: '降序排列',
     },

+ 25 - 2
frontend/src/pages/PrintersPage.tsx

@@ -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

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-DSkiM9pi.js


+ 1 - 1
static/index.html

@@ -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>

Некоторые файлы не были показаны из-за большого количества измененных файлов