Sfoglia il codice sorgente

Refactored maintenance settings

Martin Ziegler 9 mesi fa
parent
commit
8df273b821

+ 294 - 137
frontend/src/pages/MaintenancePage.tsx

@@ -8,10 +8,8 @@ import {
   Clock,
   Clock,
   Plus,
   Plus,
   Trash2,
   Trash2,
-  Settings2,
   ChevronDown,
   ChevronDown,
   ChevronUp,
   ChevronUp,
-  RotateCcw,
   Droplet,
   Droplet,
   Flame,
   Flame,
   Ruler,
   Ruler,
@@ -19,6 +17,7 @@ import {
   Square,
   Square,
   Cable,
   Cable,
   Edit3,
   Edit3,
+  RotateCcw,
 } from 'lucide-react';
 } from 'lucide-react';
 import { api } from '../api/client';
 import { api } from '../api/client';
 import type { MaintenanceStatus, PrinterMaintenanceOverview } from '../api/client';
 import type { MaintenanceStatus, PrinterMaintenanceOverview } from '../api/client';
@@ -334,134 +333,266 @@ function PrinterSection({
   );
   );
 }
 }
 
 
-// Settings modal for managing custom types
-function SettingsModal({
-  onClose,
+// Settings section component - maintenance types and per-printer interval overrides
+function SettingsSection({
+  overview,
   types,
   types,
+  onUpdateInterval,
   onAddType,
   onAddType,
   onDeleteType,
   onDeleteType,
 }: {
 }: {
-  onClose: () => void;
+  overview: PrinterMaintenanceOverview[] | undefined;
   types: Array<{ id: number; name: string; default_interval_hours: number; icon: string | null; is_system: boolean }>;
   types: Array<{ id: number; name: string; default_interval_hours: number; icon: string | null; is_system: boolean }>;
+  onUpdateInterval: (id: number, customInterval: number | null) => void;
   onAddType: (data: { name: string; description?: string; default_interval_hours: number; icon?: string }) => void;
   onAddType: (data: { name: string; description?: string; default_interval_hours: number; icon?: string }) => void;
   onDeleteType: (id: number) => void;
   onDeleteType: (id: number) => void;
 }) {
 }) {
-  const [name, setName] = useState('');
-  const [interval, setInterval] = useState('100');
-  const [icon, setIcon] = useState('Wrench');
+  const [editingInterval, setEditingInterval] = useState<number | null>(null);
+  const [intervalInput, setIntervalInput] = useState('');
+  const [showAddType, setShowAddType] = useState(false);
+  const [newTypeName, setNewTypeName] = useState('');
+  const [newTypeInterval, setNewTypeInterval] = useState('100');
+  const [newTypeIcon, setNewTypeIcon] = useState('Wrench');
+
+  const handleSaveInterval = (itemId: number, defaultInterval: number) => {
+    const newInterval = parseFloat(intervalInput);
+    if (!isNaN(newInterval) && newInterval > 0) {
+      const customInterval = Math.abs(newInterval - defaultInterval) < 0.01 ? null : newInterval;
+      onUpdateInterval(itemId, customInterval);
+    }
+    setEditingInterval(null);
+  };
 
 
-  const handleSubmit = (e: React.FormEvent) => {
+  const handleAddType = (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
-    if (name.trim() && parseFloat(interval) > 0) {
+    if (newTypeName.trim() && parseFloat(newTypeInterval) > 0) {
       onAddType({
       onAddType({
-        name: name.trim(),
-        default_interval_hours: parseFloat(interval),
-        icon,
+        name: newTypeName.trim(),
+        default_interval_hours: parseFloat(newTypeInterval),
+        icon: newTypeIcon,
       });
       });
-      setName('');
-      setInterval('100');
+      setNewTypeName('');
+      setNewTypeInterval('100');
+      setShowAddType(false);
     }
     }
   };
   };
 
 
+  const printerItems = overview?.map(p => ({
+    printerId: p.printer_id,
+    printerName: p.printer_name,
+    items: p.maintenance_items.sort((a, b) => a.maintenance_type_id - b.maintenance_type_id),
+  })).sort((a, b) => a.printerName.localeCompare(b.printerName)) || [];
+
+  const systemTypes = types.filter(t => t.is_system);
   const customTypes = types.filter(t => !t.is_system);
   const customTypes = types.filter(t => !t.is_system);
 
 
   return (
   return (
-    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
-      <div className="bg-bambu-dark-secondary rounded-lg p-6 w-full max-w-lg" onClick={e => e.stopPropagation()}>
-        <h3 className="text-lg font-semibold text-white mb-4">Maintenance Settings</h3>
-
-        {/* Existing custom types */}
-        {customTypes.length > 0 && (
-          <div className="mb-6">
-            <h4 className="text-sm text-bambu-gray mb-2">Custom Maintenance Types</h4>
-            <div className="space-y-2">
-              {customTypes.map((type) => {
-                const Icon = getIcon(type.icon);
-                return (
-                  <div key={type.id} className="flex items-center justify-between p-2 bg-bambu-dark rounded">
-                    <div className="flex items-center gap-2">
-                      <Icon className="w-4 h-4 text-bambu-gray" />
-                      <span className="text-white text-sm">{type.name}</span>
-                      <span className="text-bambu-gray text-xs">({type.default_interval_hours}h)</span>
+    <div className="space-y-8">
+      {/* Maintenance Types */}
+      <div>
+        <div className="flex items-center justify-between mb-4">
+          <h2 className="text-lg font-semibold text-white">Maintenance Types</h2>
+          <Button size="sm" variant="secondary" onClick={() => setShowAddType(!showAddType)}>
+            <Plus className="w-4 h-4" />
+            Add Custom Type
+          </Button>
+        </div>
+
+        {/* Add custom type form */}
+        {showAddType && (
+          <Card className="mb-4">
+            <div className="p-4">
+              <form onSubmit={handleAddType}>
+                <div className="flex gap-3 items-end">
+                  <div className="flex-1">
+                    <label className="block text-xs text-bambu-gray mb-1">Name</label>
+                    <input
+                      type="text"
+                      value={newTypeName}
+                      onChange={(e) => setNewTypeName(e.target.value)}
+                      className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm"
+                      placeholder="e.g., Replace HEPA Filter"
+                      autoFocus
+                    />
+                  </div>
+                  <div className="w-28">
+                    <label className="block text-xs text-bambu-gray mb-1">Interval (hours)</label>
+                    <input
+                      type="number"
+                      value={newTypeInterval}
+                      onChange={(e) => setNewTypeInterval(e.target.value)}
+                      className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm"
+                      min="1"
+                    />
+                  </div>
+                  <div>
+                    <label className="block text-xs text-bambu-gray mb-1">Icon</label>
+                    <div className="flex gap-1">
+                      {Object.keys(iconMap).map((iconName) => {
+                        const IconComp = iconMap[iconName];
+                        return (
+                          <button
+                            key={iconName}
+                            type="button"
+                            onClick={() => setNewTypeIcon(iconName)}
+                            className={`p-2 rounded ${
+                              newTypeIcon === iconName
+                                ? 'bg-bambu-green text-white'
+                                : 'bg-bambu-dark-tertiary text-bambu-gray hover:text-white'
+                            }`}
+                          >
+                            <IconComp className="w-4 h-4" />
+                          </button>
+                        );
+                      })}
                     </div>
                     </div>
-                    <button
-                      onClick={() => {
-                        if (confirm(`Delete "${type.name}"?`)) {
-                          onDeleteType(type.id);
-                        }
-                      }}
-                      className="p-1 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-red-400"
-                    >
-                      <Trash2 className="w-4 h-4" />
-                    </button>
                   </div>
                   </div>
-                );
-              })}
+                  <div className="flex gap-2">
+                    <Button type="submit" size="sm" disabled={!newTypeName.trim()}>Add</Button>
+                    <Button type="button" size="sm" variant="secondary" onClick={() => setShowAddType(false)}>Cancel</Button>
+                  </div>
+                </div>
+              </form>
             </div>
             </div>
-          </div>
+          </Card>
         )}
         )}
 
 
-        {/* Add new type */}
-        <form onSubmit={handleSubmit}>
-          <h4 className="text-sm text-bambu-gray mb-2">Add Custom Type</h4>
-          <div className="flex gap-2 mb-3">
-            <input
-              type="text"
-              value={name}
-              onChange={(e) => setName(e.target.value)}
-              className="flex-1 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm"
-              placeholder="Name (e.g., Replace HEPA Filter)"
-            />
-            <input
-              type="number"
-              value={interval}
-              onChange={(e) => setInterval(e.target.value)}
-              className="w-20 px-2 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white text-sm"
-              placeholder="Hours"
-              min="1"
-            />
-          </div>
-          <div className="flex items-center justify-between">
-            <div className="flex gap-1">
-              {Object.keys(iconMap).map((iconName) => {
-                const IconComp = iconMap[iconName];
-                return (
+        {/* Types grid */}
+        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
+          {/* System types */}
+          {systemTypes.map((type) => {
+            const Icon = getIcon(type.icon);
+            return (
+              <div key={type.id} className="bg-bambu-dark-secondary rounded-lg p-4 border border-bambu-dark-tertiary">
+                <div className="flex items-center gap-3">
+                  <div className="p-2 bg-bambu-dark rounded-lg">
+                    <Icon className="w-5 h-5 text-bambu-gray" />
+                  </div>
+                  <div className="flex-1 min-w-0">
+                    <div className="text-sm font-medium text-white truncate">{type.name}</div>
+                    <div className="text-xs text-bambu-gray mt-0.5">{type.default_interval_hours}h interval</div>
+                  </div>
+                </div>
+              </div>
+            );
+          })}
+          {/* Custom types */}
+          {customTypes.map((type) => {
+            const Icon = getIcon(type.icon);
+            return (
+              <div key={type.id} className="bg-bambu-dark-secondary rounded-lg p-4 border border-bambu-green/30">
+                <div className="flex items-center gap-3">
+                  <div className="p-2 bg-bambu-green/20 rounded-lg">
+                    <Icon className="w-5 h-5 text-bambu-green" />
+                  </div>
+                  <div className="flex-1 min-w-0">
+                    <div className="flex items-center gap-2">
+                      <span className="text-sm font-medium text-white truncate">{type.name}</span>
+                      <span className="px-1.5 py-0.5 bg-bambu-green/20 text-bambu-green text-[10px] font-medium rounded">Custom</span>
+                    </div>
+                    <div className="text-xs text-bambu-gray mt-0.5">{type.default_interval_hours}h interval</div>
+                  </div>
                   <button
                   <button
-                    key={iconName}
-                    type="button"
-                    onClick={() => setIcon(iconName)}
-                    className={`p-1.5 rounded ${
-                      icon === iconName
-                        ? 'bg-bambu-green text-white'
-                        : 'bg-bambu-dark text-bambu-gray hover:text-white'
-                    }`}
+                    onClick={() => {
+                      if (confirm(`Delete "${type.name}"?`)) {
+                        onDeleteType(type.id);
+                      }
+                    }}
+                    className="p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors"
                   >
                   >
-                    <IconComp className="w-4 h-4" />
+                    <Trash2 className="w-4 h-4" />
                   </button>
                   </button>
-                );
-              })}
-            </div>
-            <Button type="submit" size="sm" disabled={!name.trim()}>
-              <Plus className="w-4 h-4" />
-              Add
-            </Button>
-          </div>
-        </form>
-
-        <div className="mt-6 pt-4 border-t border-bambu-dark-tertiary flex justify-end">
-          <Button variant="secondary" onClick={onClose}>
-            Close
-          </Button>
+                </div>
+              </div>
+            );
+          })}
         </div>
         </div>
       </div>
       </div>
+
+      {/* Per-printer interval overrides */}
+      {printerItems.length > 0 && (
+        <div>
+          <h2 className="text-lg font-semibold text-white mb-2">Interval Overrides</h2>
+          <p className="text-sm text-bambu-gray mb-4">
+            Set custom intervals per printer.
+          </p>
+          <div className="space-y-3">
+            {printerItems.map((printer) => (
+              <Card key={printer.printerId}>
+                <div className="p-4">
+                  <h3 className="text-sm font-medium text-white mb-3">{printer.printerName}</h3>
+                  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
+                    {printer.items.map((item) => {
+                      const Icon = getIcon(item.maintenance_type_icon);
+                      const typeInfo = types.find(t => t.id === item.maintenance_type_id);
+                      const defaultInterval = typeInfo?.default_interval_hours || item.interval_hours;
+                      const isEditing = editingInterval === item.id;
+
+                      return (
+                        <div key={item.id} className="flex items-center gap-2 p-2 bg-bambu-dark rounded-lg">
+                          <Icon className="w-4 h-4 text-bambu-gray shrink-0" />
+                          <span className="text-xs text-bambu-gray flex-1 truncate">{item.maintenance_type_name}</span>
+
+                          {isEditing ? (
+                            <div className="flex items-center gap-1">
+                              <input
+                                type="number"
+                                value={intervalInput}
+                                onChange={(e) => setIntervalInput(e.target.value)}
+                                onKeyDown={(e) => {
+                                  if (e.key === 'Enter') handleSaveInterval(item.id, defaultInterval);
+                                  if (e.key === 'Escape') setEditingInterval(null);
+                                }}
+                                className="w-16 px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-white text-xs"
+                                min="1"
+                                autoFocus
+                              />
+                              <Button size="sm" onClick={() => handleSaveInterval(item.id, defaultInterval)}>OK</Button>
+                            </div>
+                          ) : (
+                            <button
+                              onClick={() => {
+                                setEditingInterval(item.id);
+                                setIntervalInput(item.interval_hours.toString());
+                              }}
+                              className="px-2 py-1 bg-bambu-dark-tertiary hover:bg-bambu-dark-secondary border border-bambu-dark-tertiary hover:border-bambu-green rounded text-xs font-medium text-white transition-colors"
+                            >
+                              {item.interval_hours}h
+                              <Edit3 className="w-3 h-3 inline ml-1.5 text-bambu-gray" />
+                            </button>
+                          )}
+                        </div>
+                      );
+                    })}
+                  </div>
+                </div>
+              </Card>
+            ))}
+          </div>
+        </div>
+      )}
+
+      {printerItems.length === 0 && (
+        <Card>
+          <CardContent className="text-center py-12">
+            <Clock className="w-12 h-12 mx-auto mb-4 text-bambu-gray/30" />
+            <p className="text-bambu-gray">No printers configured</p>
+            <p className="text-sm text-bambu-gray/70 mt-1">
+              Add printers to configure maintenance intervals
+            </p>
+          </CardContent>
+        </Card>
+      )}
     </div>
     </div>
   );
   );
 }
 }
 
 
+type TabType = 'status' | 'settings';
+
 export function MaintenancePage() {
 export function MaintenancePage() {
   const queryClient = useQueryClient();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const { showToast } = useToast();
-  const [showSettings, setShowSettings] = useState(false);
+  const [activeTab, setActiveTab] = useState<TabType>('status');
 
 
   const { data: overview, isLoading } = useQuery({
   const { data: overview, isLoading } = useQuery({
     queryKey: ['maintenanceOverview'],
     queryKey: ['maintenanceOverview'],
@@ -561,52 +692,78 @@ export function MaintenancePage() {
   return (
   return (
     <div className="p-8">
     <div className="p-8">
       {/* Header */}
       {/* Header */}
-      <div className="mb-6 flex items-center justify-between">
-        <div>
-          <h1 className="text-2xl font-bold text-white">Maintenance</h1>
-          <p className="text-bambu-gray text-sm">
-            {totalDue > 0 && <span className="text-red-400">{totalDue} tasks overdue</span>}
-            {totalDue > 0 && totalWarning > 0 && ' · '}
-            {totalWarning > 0 && <span className="text-yellow-400">{totalWarning} due soon</span>}
-            {totalDue === 0 && totalWarning === 0 && 'All maintenance up to date'}
-          </p>
-        </div>
-        <Button variant="secondary" onClick={() => setShowSettings(true)}>
-          <Settings2 className="w-4 h-4" />
-          Settings
-        </Button>
+      <div className="mb-6">
+        <h1 className="text-2xl font-bold text-white">Maintenance</h1>
+        <p className="text-bambu-gray text-sm">
+          {activeTab === 'status' ? (
+            <>
+              {totalDue > 0 && <span className="text-red-400">{totalDue} tasks overdue</span>}
+              {totalDue > 0 && totalWarning > 0 && ' · '}
+              {totalWarning > 0 && <span className="text-yellow-400">{totalWarning} due soon</span>}
+              {totalDue === 0 && totalWarning === 0 && 'All maintenance up to date'}
+            </>
+          ) : (
+            'Configure maintenance types and intervals'
+          )}
+        </p>
       </div>
       </div>
 
 
-      {/* Printers - sorted alphabetically */}
-      <div className="space-y-4">
-        {overview && overview.length > 0 ? (
-          [...overview].sort((a, b) => a.printer_name.localeCompare(b.printer_name)).map((printerOverview) => (
-            <PrinterSection
-              key={printerOverview.printer_id}
-              overview={printerOverview}
-              onPerform={handlePerform}
-              onToggle={handleToggle}
-              onSetHours={handleSetHours}
-            />
-          ))
-        ) : (
-          <Card>
-            <CardContent className="text-center py-12">
-              <Wrench className="w-12 h-12 mx-auto mb-4 text-bambu-gray/30" />
-              <p className="text-bambu-gray">No printers configured</p>
-              <p className="text-sm text-bambu-gray/70 mt-1">
-                Add printers to start tracking maintenance
-              </p>
-            </CardContent>
-          </Card>
-        )}
+      {/* Tabs */}
+      <div className="flex gap-1 mb-6 border-b border-bambu-dark-tertiary">
+        <button
+          onClick={() => setActiveTab('status')}
+          className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
+            activeTab === 'status'
+              ? 'text-bambu-green border-bambu-green'
+              : 'text-bambu-gray border-transparent hover:text-white'
+          }`}
+        >
+          Status
+        </button>
+        <button
+          onClick={() => setActiveTab('settings')}
+          className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
+            activeTab === 'settings'
+              ? 'text-bambu-green border-bambu-green'
+              : 'text-bambu-gray border-transparent hover:text-white'
+          }`}
+        >
+          Settings
+        </button>
       </div>
       </div>
 
 
-      {/* Settings modal */}
-      {showSettings && types && (
-        <SettingsModal
-          onClose={() => setShowSettings(false)}
-          types={types}
+      {/* Tab content */}
+      {activeTab === 'status' ? (
+        <div className="space-y-4">
+          {overview && overview.length > 0 ? (
+            [...overview].sort((a, b) => a.printer_name.localeCompare(b.printer_name)).map((printerOverview) => (
+              <PrinterSection
+                key={printerOverview.printer_id}
+                overview={printerOverview}
+                onPerform={handlePerform}
+                onToggle={handleToggle}
+                onSetHours={handleSetHours}
+              />
+            ))
+          ) : (
+            <Card>
+              <CardContent className="text-center py-12">
+                <Wrench className="w-12 h-12 mx-auto mb-4 text-bambu-gray/30" />
+                <p className="text-bambu-gray">No printers configured</p>
+                <p className="text-sm text-bambu-gray/70 mt-1">
+                  Add printers to start tracking maintenance
+                </p>
+              </CardContent>
+            </Card>
+          )}
+        </div>
+      ) : (
+        <SettingsSection
+          overview={overview}
+          types={types || []}
+          onUpdateInterval={(id, customInterval) =>
+            updateMutation.mutate({ id, data: { custom_interval_hours: customInterval } })
+          }
           onAddType={(data) => addTypeMutation.mutate(data)}
           onAddType={(data) => addTypeMutation.mutate(data)}
           onDeleteType={(id) => deleteTypeMutation.mutate(id)}
           onDeleteType={(id) => deleteTypeMutation.mutate(id)}
         />
         />

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BF_tKqob.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CJM6Rz8E.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-eHK2TMOH.js


+ 2 - 2
static/index.html

@@ -7,8 +7,8 @@
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-Bi9VwPck.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CJM6Rz8E.css">
+    <script type="module" crossorigin src="/assets/index-eHK2TMOH.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BF_tKqob.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff