Procházet zdrojové kódy

- Added maintenance interval type clendar days
- Minor improvements to maintenance module

maziggy před 5 měsíci
rodič
revize
1a17b547dc

+ 44 - 5
backend/app/api/routes/maintenance.py

@@ -136,6 +136,7 @@ async def create_maintenance_type(
         name=data.name,
         description=data.description,
         default_interval_hours=data.default_interval_hours,
+        interval_type=data.interval_type,
         icon=data.icon,
         is_system=False,
     )
@@ -225,11 +226,16 @@ async def _get_printer_maintenance_internal(
     due_count = 0
     warning_count = 0
 
+    now = datetime.utcnow()
+
     for maint_type in all_types:
         item = existing_items.get(maint_type.id)
+        default_interval_type = getattr(maint_type, 'interval_type', 'hours') or 'hours'
 
         if item:
             interval = item.custom_interval_hours or maint_type.default_interval_hours
+            # Use custom interval type if set, otherwise use type's default
+            interval_type = getattr(item, 'custom_interval_type', None) or default_interval_type
             enabled = item.enabled
             last_performed_hours = item.last_performed_hours
             last_performed_at = item.last_performed_at
@@ -246,15 +252,41 @@ async def _get_printer_maintenance_internal(
             await db.flush()
 
             interval = maint_type.default_interval_hours
+            interval_type = default_interval_type
             enabled = True
             last_performed_hours = 0.0
             last_performed_at = None
             item_id = item.id
 
-        hours_since = total_hours - last_performed_hours
-        hours_until = interval - hours_since
-        is_due = hours_until <= 0
-        is_warning = hours_until <= (interval * 0.1) and not is_due
+        # Calculate status based on interval type
+        if interval_type == "days":
+            # Time-based: calculate days since last performed
+            if last_performed_at:
+                days_since = (now - last_performed_at).total_seconds() / 86400.0
+            else:
+                # Never performed - consider it due
+                days_since = interval + 1
+
+            days_until = interval - days_since
+            is_due = days_until <= 0
+            is_warning = days_until <= (interval * 0.1) and not is_due
+
+            # For compatibility, also set hours values (but they won't be primary)
+            hours_since = total_hours - last_performed_hours
+            hours_until = 0  # Not applicable for time-based
+        else:
+            # Print-hours based (default)
+            hours_since = total_hours - last_performed_hours
+            hours_until = interval - hours_since
+            is_due = hours_until <= 0
+            is_warning = hours_until <= (interval * 0.1) and not is_due
+
+            # Calculate days for reference
+            if last_performed_at:
+                days_since = (now - last_performed_at).total_seconds() / 86400.0
+            else:
+                days_since = None
+            days_until = None
 
         if enabled:
             if is_due:
@@ -271,9 +303,12 @@ async def _get_printer_maintenance_internal(
             maintenance_type_icon=maint_type.icon,
             enabled=enabled,
             interval_hours=interval,
+            interval_type=interval_type,
             current_hours=total_hours,
             hours_since_maintenance=hours_since,
             hours_until_due=hours_until,
+            days_since_maintenance=days_since if interval_type == "days" else None,
+            days_until_due=days_until if interval_type == "days" else None,
             is_due=is_due,
             is_warning=is_warning,
             last_performed_at=last_performed_at,
@@ -389,6 +424,7 @@ async def perform_maintenance(
 
     # Calculate status
     interval = item.custom_interval_hours or item.maintenance_type.default_interval_hours
+    interval_type = getattr(item.maintenance_type, 'interval_type', 'hours') or 'hours'
     hours_since = current_hours - item.last_performed_hours
     hours_until = interval - hours_since
 
@@ -401,9 +437,12 @@ async def perform_maintenance(
         maintenance_type_icon=item.maintenance_type.icon,
         enabled=item.enabled,
         interval_hours=interval,
+        interval_type=interval_type,
         current_hours=current_hours,
         hours_since_maintenance=hours_since,
-        hours_until_due=hours_until,
+        hours_until_due=hours_until if interval_type == "hours" else 0,
+        days_since_maintenance=0 if interval_type == "days" else None,
+        days_until_due=interval if interval_type == "days" else None,
         is_due=False,
         is_warning=False,
         last_performed_at=item.last_performed_at,

+ 18 - 0
backend/app/core/database.py

@@ -109,3 +109,21 @@ async def run_migrations(conn):
     except Exception:
         # Column already exists
         pass
+
+    # Migration: Add interval_type column to maintenance_types
+    try:
+        await conn.execute(text(
+            "ALTER TABLE maintenance_types ADD COLUMN interval_type VARCHAR(20) DEFAULT 'hours'"
+        ))
+    except Exception:
+        # Column already exists
+        pass
+
+    # Migration: Add custom_interval_type column to printer_maintenance
+    try:
+        await conn.execute(text(
+            "ALTER TABLE printer_maintenance ADD COLUMN custom_interval_type VARCHAR(20)"
+        ))
+    except Exception:
+        # Column already exists
+        pass

+ 4 - 0
backend/app/models/maintenance.py

@@ -15,6 +15,8 @@ class MaintenanceType(Base):
     name: Mapped[str] = mapped_column(String(100))
     description: Mapped[str | None] = mapped_column(Text)
     default_interval_hours: Mapped[float] = mapped_column(Float, default=100.0)
+    # Interval type: "hours" (print hours) or "days" (calendar days)
+    interval_type: Mapped[str] = mapped_column(String(20), default="hours")
     icon: Mapped[str | None] = mapped_column(String(50))  # Icon name for UI
     is_system: Mapped[bool] = mapped_column(Boolean, default=False)  # Pre-defined vs custom
     created_at: Mapped[datetime] = mapped_column(
@@ -37,6 +39,8 @@ class PrinterMaintenance(Base):
 
     # Custom interval for this printer (overrides default if set)
     custom_interval_hours: Mapped[float | None] = mapped_column(Float, nullable=True)
+    # Custom interval type for this printer (overrides default if set)
+    custom_interval_type: Mapped[str | None] = mapped_column(String(20), nullable=True)
 
     # Tracking
     enabled: Mapped[bool] = mapped_column(Boolean, default=True)

+ 15 - 4
backend/app/schemas/maintenance.py

@@ -9,6 +9,8 @@ class MaintenanceTypeBase(BaseModel):
     name: str = Field(..., min_length=1, max_length=100)
     description: str | None = None
     default_interval_hours: float = Field(default=100.0, ge=1.0)
+    # "hours" = print hours, "days" = calendar days
+    interval_type: str = Field(default="hours", pattern="^(hours|days)$")
     icon: str | None = None
 
 
@@ -20,6 +22,7 @@ class MaintenanceTypeUpdate(BaseModel):
     name: str | None = None
     description: str | None = None
     default_interval_hours: float | None = Field(default=None, ge=1.0)
+    interval_type: str | None = Field(default=None, pattern="^(hours|days)$")
     icon: str | None = None
 
 
@@ -46,6 +49,7 @@ class PrinterMaintenanceCreate(PrinterMaintenanceBase):
 
 class PrinterMaintenanceUpdate(BaseModel):
     custom_interval_hours: float | None = None
+    custom_interval_type: str | None = Field(default=None, pattern="^(hours|days)$")
     enabled: bool | None = None
 
 
@@ -94,12 +98,19 @@ class MaintenanceStatus(BaseModel):
     maintenance_type_name: str
     maintenance_type_icon: str | None
     enabled: bool
-    interval_hours: float  # custom or default
+    # Interval configuration
+    interval_hours: float  # custom or default (hours for print-based, days for time-based)
+    interval_type: str  # "hours" or "days"
+    # For print-hour based maintenance
     current_hours: float  # total print hours for printer
     hours_since_maintenance: float  # current - last_performed
-    hours_until_due: float  # interval - hours_since
-    is_due: bool  # hours_until_due <= 0
-    is_warning: bool  # hours_until_due <= 10% of interval
+    hours_until_due: float  # interval - hours_since (for hours type)
+    # For time-based maintenance
+    days_since_maintenance: float | None  # days since last performed
+    days_until_due: float | None  # for days type
+    # Status flags
+    is_due: bool  # hours_until_due <= 0 OR days_until_due <= 0
+    is_warning: bool  # within 10% of interval
     last_performed_at: datetime | None
 
 

+ 7 - 2
frontend/src/api/client.ts

@@ -721,6 +721,7 @@ export interface MaintenanceType {
   name: string;
   description: string | null;
   default_interval_hours: number;
+  interval_type: 'hours' | 'days';  // "hours" = print hours, "days" = calendar days
   icon: string | null;
   is_system: boolean;
   created_at: string;
@@ -730,6 +731,7 @@ export interface MaintenanceTypeCreate {
   name: string;
   description?: string | null;
   default_interval_hours?: number;
+  interval_type?: 'hours' | 'days';
   icon?: string | null;
 }
 
@@ -741,10 +743,13 @@ export interface MaintenanceStatus {
   maintenance_type_name: string;
   maintenance_type_icon: string | null;
   enabled: boolean;
-  interval_hours: number;
+  interval_hours: number;  // For hours type: print hours; for days type: number of days
+  interval_type: 'hours' | 'days';
   current_hours: number;
   hours_since_maintenance: number;
   hours_until_due: number;
+  days_since_maintenance: number | null;  // For days type
+  days_until_due: number | null;  // For days type
   is_due: boolean;
   is_warning: boolean;
   last_performed_at: string | null;
@@ -1274,7 +1279,7 @@ export const api = {
   getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
   getPrinterMaintenance: (printerId: number) =>
     request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),
-  updateMaintenanceItem: (itemId: number, data: { custom_interval_hours?: number | null; enabled?: boolean }) =>
+  updateMaintenanceItem: (itemId: number, data: { custom_interval_hours?: number | null; custom_interval_type?: 'hours' | 'days' | null; enabled?: boolean }) =>
     request<MaintenanceStatus>(`/maintenance/items/${itemId}`, {
       method: 'PATCH',
       body: JSON.stringify(data),

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 452 - 218
frontend/src/pages/MaintenancePage.tsx


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/index-6YKRUSex.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/index-BOLoyzZ9.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/index-BP1T2ML7.css


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
static/assets/index-DO3k-CBG.css


+ 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="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-BOLoyzZ9.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-DO3k-CBG.css">
+    <script type="module" crossorigin src="/assets/index-6YKRUSex.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BP1T2ML7.css">
   </head>
   <body>
     <div id="root"></div>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů