Feature/multiple switches per prtinter (#786)

Feature/multiple switches per prtinter (#786)
This commit is contained in:
ManuelW
2026-03-23 10:09:14 +01:00
committed by GitHub
parent 27e96c9827
commit 9e81f246d8
12 changed files with 185 additions and 188 deletions
+17
View File
@@ -201,6 +201,23 @@ async def get_script_plugs_by_printer(
return ha_entities
@router.get("/by-printer/{printer_id}/all", response_model=list[SmartPlugResponse])
async def get_all_smart_plugs_by_printer(
printer_id: int,
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.SMART_PLUGS_READ),
):
"""Get all smart plugs assigned to a printer that should appear on the printer card.
Returns power plugs (tasmota, mqtt) plus HA entities with show_on_printer_card enabled.
"""
result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id == printer_id))
plugs = result.scalars().all()
return [
plug for plug in plugs if plug.plug_type != "homeassistant" or (plug.ha_entity_id and plug.show_on_printer_card)
]
# Tasmota Discovery Endpoints
# NOTE: These must be defined BEFORE /{plug_id} routes to avoid path conflicts
+1
View File
@@ -3277,6 +3277,7 @@ export const api = {
getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
getScriptPlugsByPrinter: (printerId: number) => request<SmartPlug[]>(`/smart-plugs/by-printer/${printerId}/scripts`),
getAllSmartPlugsByPrinter: (printerId: number) => request<SmartPlug[]>(`/smart-plugs/by-printer/${printerId}/all`),
createSmartPlug: (data: SmartPlugCreate) =>
request<SmartPlug>('/smart-plugs/', {
method: 'POST',
@@ -255,7 +255,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
// Also invalidate printer card HA entity queries
queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] });
queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter'] });
onClose();
},
onError: (err: Error) => {
@@ -269,7 +269,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
// Also invalidate printer card HA entity queries
queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter'] });
queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter'] });
onClose();
},
onError: (err: Error) => {
+2 -3
View File
@@ -80,8 +80,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
// Also invalidate printer-specific smart plug queries to keep PrintersPage in sync
if (plug.printer_id) {
queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', plug.printer_id] });
queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] });
queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', plug.printer_id] });
}
},
});
@@ -93,7 +92,7 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
// Also invalidate printer card HA entity queries
if (plug.printer_id) {
queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] });
queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', plug.printer_id] });
}
},
});
+5 -5
View File
@@ -374,12 +374,12 @@ export default {
resumeTitle: 'Druck fortsetzen',
resumeMessage: 'Möchten Sie den Druck auf "{{name}}" fortsetzen?',
resumeButton: 'Druck fortsetzen',
powerOnTitle: 'Drucker einschalten',
powerOnMessage: 'Möchten Sie die Stromversorgung für "{{name}}" wirklich EINSCHALTEN?',
powerOnTitle: 'Schalter einschalten',
powerOnMessage: 'Möchten Sie "{{name}}" wirklich EINSCHALTEN?',
powerOnButton: 'Einschalten',
powerOffTitle: 'Drucker ausschalten',
powerOffMessage: 'Möchten Sie die Stromversorgung für "{{name}}" wirklich AUSSCHALTEN?',
powerOffWarning: 'WARNUNG: "{{name}}" druckt gerade! Möchten Sie die Stromversorgung wirklich AUSSCHALTEN? Dies unterbricht den Druck und kann den Drucker beschädigen.',
powerOffTitle: 'Schalter ausschalten',
powerOffMessage: 'Möchten Sie "{{name}}" wirklich AUSSCHALTEN?',
powerOffWarning: 'WARNUNG: "{{name}}" druckt gerade! Möchten Sie wirklich AUSSCHALTEN? Dies unterbricht den Druck und kann den Drucker beschädigen.',
powerOffButton: 'Ausschalten',
},
// Discovery
+5 -5
View File
@@ -374,12 +374,12 @@ export default {
resumeTitle: 'Resume Print',
resumeMessage: 'Are you sure you want to resume the print on "{{name}}"?',
resumeButton: 'Resume Print',
powerOnTitle: 'Power On Printer',
powerOnMessage: 'Are you sure you want to turn ON the power for "{{name}}"?',
powerOnTitle: 'Power On Switch',
powerOnMessage: 'Are you sure you want to turn ON "{{name}}"?',
powerOnButton: 'Power On',
powerOffTitle: 'Power Off Printer',
powerOffMessage: 'Are you sure you want to turn OFF the power for "{{name}}"?',
powerOffWarning: 'WARNING: "{{name}}" is currently printing! Are you sure you want to turn OFF the power? This will interrupt the print and may damage the printer.',
powerOffTitle: 'Power Off Switch',
powerOffMessage: 'Are you sure you want to turn OFF "{{name}}"?',
powerOffWarning: 'WARNING: "{{name}}" is currently printing! Are you sure you want to turn it OFF? This will interrupt the print and may damage the printer.',
powerOffButton: 'Power Off',
},
// Discovery
+3 -3
View File
@@ -374,12 +374,12 @@ export default {
resumeTitle: 'Reprendre l\'impression',
resumeMessage: 'Reprendre l\'impression sur "{{name}}" ?',
resumeButton: 'Reprendre',
powerOnTitle: 'Allumer l\'imprimante',
powerOnTitle: 'Allumer Interrupteur',
powerOnMessage: 'Allumer "{{name}}" ?',
powerOnButton: 'Allumer',
powerOffTitle: 'Éteindre l\'imprimante',
powerOffTitle: 'Éteindre Interrupteur',
powerOffMessage: 'Éteindre "{{name}}" ?',
powerOffWarning: 'ATTENTION : "{{name}}" imprime ! L\'éteindre maintenant peut endommager l\'imprimante.',
powerOffWarning: 'ATTENTION : "{{name}}" imprime actuellement ! Êtes-vous sûr de vouloir l\'éteindre ? Cela interrompra l\'impression et pourrait endommager l\'imprimante.',
powerOffButton: 'Éteindre',
},
// Discovery
+3 -3
View File
@@ -374,12 +374,12 @@ export default {
resumeTitle: 'Riprendi Stampa',
resumeMessage: 'Sei sicuro di riprendere la stampa su "{{name}}"?',
resumeButton: 'Riprendi Stampa',
powerOnTitle: 'Accendi Stampante',
powerOnTitle: 'Accendi Interruttore',
powerOnMessage: 'Sei sicuro di accendere "{{name}}"?',
powerOnButton: 'Accendi',
powerOffTitle: 'Spegni Stampante',
powerOffTitle: 'Spegni Interruttore',
powerOffMessage: 'Sei sicuro di spegnere "{{name}}"?',
powerOffWarning: 'AVVISO: "{{name}}" sta stampando! Sei sicuro di spegnere? Questo interromperà la stampa e potrebbe danneggiare la stampante.',
powerOffWarning: 'AVVISO: "{{name}}" sta stampando! Sei sicuro di spegnerlo? Questo interromperà la stampa e potrebbe danneggiare la stampante.',
powerOffButton: 'Spegni',
},
// Discovery
+5 -5
View File
@@ -373,12 +373,12 @@ export default {
resumeTitle: '印刷を再開',
resumeMessage: '「{{name}}」の印刷を再開しますか?',
resumeButton: '印刷を再開',
powerOnTitle: 'プリンターの電源をオン',
powerOnMessage: '「{{name}}」の電源をオンにしますか?',
powerOnTitle: 'スイッチをオン',
powerOnMessage: '「{{name}}」をオンにしますか?',
powerOnButton: '電源オン',
powerOffTitle: 'プリンターの電源をオフ',
powerOffMessage: '「{{name}}」の電源をオフにしますか?',
powerOffWarning: '警告: 「{{name}}」は現在印刷中です!電源をオフにしますか?印刷が中断され、プリンターが損傷する可能性があります。',
powerOffTitle: 'スイッチをオフ',
powerOffMessage: '「{{name}}」をオフにしますか?',
powerOffWarning: '警告: 「{{name}}」は現在印刷中です!オフにしますか?印刷が中断され、プリンターが損傷する可能性があります。',
powerOffButton: '電源オフ',
},
// Discovery
+5 -5
View File
@@ -374,12 +374,12 @@ export default {
resumeTitle: 'Retomar Impressão',
resumeMessage: 'Tem certeza de que deseja retomar a impressão em "{{name}}"?',
resumeButton: 'Retomar Impressão',
powerOnTitle: 'Ligar Impressora',
powerOnMessage: 'Tem certeza de que deseja ligar a impressora "{{name}}"?',
powerOnTitle: 'Ligar Interruptor',
powerOnMessage: 'Tem certeza de que deseja ligar "{{name}}"?',
powerOnButton: 'Ligar',
powerOffTitle: 'Desligar Impressora',
powerOffMessage: 'Tem certeza de que deseja desligar a impressora "{{name}}"?',
powerOffWarning: 'AVISO: "{{name}}" está imprimindo no momento! Tem certeza de que deseja desligar a impressora? Isso interromperá a impressão e pode danificar a impressora.',
powerOffTitle: 'Desligar Interruptor',
powerOffMessage: 'Tem certeza de que deseja desligar "{{name}}"?',
powerOffWarning: 'AVISO: "{{name}}" está imprimindo! Tem certeza de que deseja desligá-lo? Isso interromperá a impressão e pode danificar a impressora.',
powerOffButton: 'Desligar',
},
// Discovery
+6 -6
View File
@@ -374,12 +374,12 @@ export default {
resumeTitle: '继续打印',
resumeMessage: '确定要继续"{{name}}"上的打印吗?',
resumeButton: '继续打印',
powerOnTitle: '开启打印机',
powerOnMessage: '确定要打开"{{name}}"的电源吗?',
powerOnButton: '开机',
powerOffTitle: '关闭打印机',
powerOffMessage: '确定要关闭"{{name}}"的电源吗?',
powerOffWarning: '警告:"{{name}}"正在打印中!确定要关闭电源吗?这将中断打印并可能损坏打印机。',
powerOnTitle: '开启开关',
powerOnMessage: '确定要开启"{{name}}"吗?',
powerOnButton: '开启',
powerOffTitle: '关闭开关',
powerOffMessage: '确定要关闭"{{name}}"吗?',
powerOffWarning: '警告:"{{name}}"正在打印中!确定要关闭吗?这将中断打印并可能损坏打印机。',
powerOffButton: '关机',
},
// Discovery
+131 -151
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useQuery, useQueries, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useTheme } from '../contexts/ThemeContext';
import { useAuth } from '../contexts/AuthContext';
@@ -41,7 +41,6 @@ import {
CheckCircle,
XCircle,
User,
Home,
Printer as PrinterIcon,
Info,
Cable,
@@ -1554,8 +1553,8 @@ function PrinterCard({
const [showEditModal, setShowEditModal] = useState(false);
const [showFileManager, setShowFileManager] = useState(false);
const [showMQTTDebug, setShowMQTTDebug] = useState(false);
const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false);
const [showPowerOnConfirm, setShowPowerOnConfirm] = useState<number | null>(null);
const [showPowerOffConfirm, setShowPowerOffConfirm] = useState<number | null>(null);
const [showHMSModal, setShowHMSModal] = useState(false);
const [showStopConfirm, setShowStopConfirm] = useState(false);
const [showPauseConfirm, setShowPauseConfirm] = useState(false);
@@ -1780,24 +1779,19 @@ function PrinterCard({
? currentTrayNow
: cachedTrayNow.current;
// Fetch smart plug for this printer
const { data: smartPlug } = useQuery({
queryKey: ['smartPlugByPrinter', printer.id],
queryFn: () => api.getSmartPlugByPrinter(printer.id),
// Fetch smart plugs for this printer
const { data: smartPlugs } = useQuery({
queryKey: ['smartPlugsByPrinter', printer.id],
queryFn: () => api.getAllSmartPlugsByPrinter(printer.id),
});
// Fetch script plugs for this printer (for multi-device control)
const { data: scriptPlugs } = useQuery({
queryKey: ['scriptPlugsByPrinter', printer.id],
queryFn: () => api.getScriptPlugsByPrinter(printer.id),
});
// Fetch smart plug status if plug exists (faster refresh for energy monitoring)
const { data: plugStatus } = useQuery({
queryKey: ['smartPlugStatus', smartPlug?.id],
queryFn: () => smartPlug ? api.getSmartPlugStatus(smartPlug.id) : null,
enabled: !!smartPlug,
refetchInterval: 10000, // 10 seconds for real-time power display
// Fetch smart plug status for all plugs (faster refresh for energy monitoring)
const plugStatusResults = useQueries({
queries: (smartPlugs || []).map(plug => ({
queryKey: ['smartPlugStatus', plug.id],
queryFn: () => api.getSmartPlugStatus(plug.id),
refetchInterval: 10000, // 10 seconds for real-time power display
})),
});
// Fetch queue count for this printer
@@ -1892,26 +1886,26 @@ function PrinterCard({
// Smart plug control mutations
const powerControlMutation = useMutation({
mutationFn: (action: 'on' | 'off') =>
smartPlug ? api.controlSmartPlug(smartPlug.id, action) : Promise.reject('No plug'),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smartPlugStatus', smartPlug?.id] });
mutationFn: ({ plugId, action }: { plugId: number; action: 'on' | 'off' }) =>
api.controlSmartPlug(plugId, action),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', printer.id] });
queryClient.invalidateQueries({ queryKey: ['smartPlugStatus', variables.plugId] });
},
});
const toggleAutoOffMutation = useMutation({
mutationFn: (enabled: boolean) =>
smartPlug ? api.updateSmartPlug(smartPlug.id, { auto_off: enabled }) : Promise.reject('No plug'),
mutationFn: ({ plugId, enabled }: { plugId: number; enabled: boolean }) =>
api.updateSmartPlug(plugId, { auto_off: enabled }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', printer.id] });
// Also invalidate the smart-plugs list to keep Settings page in sync
queryClient.invalidateQueries({ queryKey: ['smartPlugsByPrinter', printer.id] });
queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
},
});
// Run HA entity mutation — scripts use 'on' (trigger), switches use 'toggle'
// Run HA script mutation
const runScriptMutation = useMutation({
mutationFn: ({ id, action }: { id: number; action: 'on' | 'toggle' }) => api.controlSmartPlug(id, action),
mutationFn: (id: number) => api.controlSmartPlug(id, 'on'),
onSuccess: () => {
showToast(t('printers.toast.scriptTriggered'));
},
@@ -3969,118 +3963,104 @@ function PrinterCard({
)}
{/* Smart Plug Controls - hidden in compact mode */}
{smartPlug && viewMode === 'expanded' && (
<div className="mt-4 pt-4 border-t border-bambu-dark-tertiary">
<div className="flex items-center gap-3">
{/* Plug name and status */}
<div className="flex items-center gap-2 min-w-0">
<Zap className="w-4 h-4 text-bambu-gray flex-shrink-0" />
<span className="text-sm text-white truncate">{smartPlug.name}</span>
{plugStatus && (
<span
className={`text-xs px-1.5 py-0.5 rounded flex-shrink-0 ${
plugStatus.state === 'ON'
? 'bg-bambu-green/20 text-bambu-green'
: plugStatus.state === 'OFF'
? 'bg-red-500/20 text-red-400'
: 'bg-bambu-gray/20 text-bambu-gray'
}`}
>
{plugStatus.state || '?'}
{plugStatus.state === 'ON' && plugStatus.energy?.power != null && (
<span className="text-yellow-400 ml-1.5">· {plugStatus.energy.power}W</span>
{smartPlugs && smartPlugs.length > 0 && viewMode === 'expanded' && (
<div className="mt-4 pt-4 border-t border-bambu-dark-tertiary space-y-2">
{smartPlugs.map((plug, index) => {
const plugStatus = plugStatusResults[index]?.data;
const isScript = plug.plug_type === 'homeassistant' && plug.ha_entity_id?.startsWith('script.');
return (
<div key={plug.id} className="flex items-center gap-3">
<div className="flex items-center gap-2 min-w-0">
<Zap className="w-4 h-4 text-bambu-gray flex-shrink-0" />
<span className="text-sm text-white truncate">{plug.name}</span>
{plugStatus && (
<span className={`text-xs px-1.5 py-0.5 rounded flex-shrink-0 ${
plugStatus.state === 'ON'
? 'bg-bambu-green/20 text-bambu-green'
: plugStatus.state === 'OFF'
? 'bg-red-500/20 text-red-400'
: 'bg-bambu-gray/20 text-bambu-gray'
}`}>
{plugStatus.state || '?'}
{plugStatus.state === 'ON' && plugStatus.energy?.power != null && (
<span className="text-yellow-400 ml-1.5">· {plugStatus.energy.power}W</span>
)}
</span>
)}
</span>
)}
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Power buttons */}
<div className="flex items-center gap-1">
<button
onClick={() => setShowPowerOnConfirm(true)}
disabled={powerControlMutation.isPending || plugStatus?.state === 'ON' || !hasPermission('smart_plugs:control')}
className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
!hasPermission('smart_plugs:control')
? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
: plugStatus?.state === 'ON'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
}`}
title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : undefined}
>
<Power className="w-3 h-3" />
On
</button>
<button
onClick={() => setShowPowerOffConfirm(true)}
disabled={powerControlMutation.isPending || plugStatus?.state === 'OFF' || !hasPermission('smart_plugs:control')}
className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
!hasPermission('smart_plugs:control')
? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
: plugStatus?.state === 'OFF'
? 'bg-red-500/30 text-red-400'
: 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
}`}
title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : undefined}
>
<PowerOff className="w-3 h-3" />
Off
</button>
</div>
{/* Auto-off toggle */}
<div className="flex items-center gap-2 flex-shrink-0">
<span className={`text-xs hidden sm:inline ${smartPlug.auto_off_executed ? 'text-bambu-green' : 'text-bambu-gray'}`}>
{smartPlug.auto_off_executed ? 'Auto-off done' : 'Auto-off'}
</span>
<button
onClick={() => toggleAutoOffMutation.mutate(!smartPlug.auto_off)}
disabled={toggleAutoOffMutation.isPending || smartPlug.auto_off_executed || !hasPermission('smart_plugs:control')}
title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : (smartPlug.auto_off_executed ? t('printers.autoOffExecuted') : t('printers.autoOffAfterPrint'))}
className={`relative w-9 h-5 rounded-full transition-colors flex-shrink-0 ${
!hasPermission('smart_plugs:control')
? 'bg-bambu-dark-tertiary/50 cursor-not-allowed'
: smartPlug.auto_off_executed
? 'bg-bambu-green/50 cursor-not-allowed'
: smartPlug.auto_off ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
}`}
>
<span
className={`absolute top-[2px] left-[2px] w-4 h-4 bg-white rounded-full transition-transform ${
smartPlug.auto_off || smartPlug.auto_off_executed ? 'translate-x-4' : 'translate-x-0'
}`}
/>
</button>
</div>
</div>
{/* HA entity buttons row */}
{scriptPlugs && scriptPlugs.length > 0 && (
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-bambu-dark-tertiary/50">
<Home className="w-3.5 h-3.5 text-blue-400 flex-shrink-0" />
<span className="text-xs text-bambu-gray">HA:</span>
<div className="flex flex-wrap gap-1">
{scriptPlugs.map(script => {
const isScript = script.ha_entity_id?.startsWith('script.');
return (
<button
key={script.id}
onClick={() => runScriptMutation.mutate({ id: script.id, action: isScript ? 'on' : 'toggle' })}
disabled={runScriptMutation.isPending}
title={`${isScript ? 'Run' : 'Toggle'} ${script.ha_entity_id}`}
className="px-2 py-0.5 text-xs bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 rounded transition-colors flex items-center gap-1"
>
<Play className="w-2.5 h-2.5" />
{script.name}
</button>
);
})}
</div>
<div className="flex-1" />
{isScript ? (
<button
onClick={() => runScriptMutation.mutate(plug.id)}
disabled={runScriptMutation.isPending || !hasPermission('smart_plugs:control')}
className="px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 bg-blue-500/20 text-blue-400 hover:bg-blue-500/30"
title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : 'Run script'}
>
<Play className="w-3 h-3" />
Run
</button>
) : (
<div className="flex items-center gap-1">
<button
onClick={() => setShowPowerOnConfirm(plug.id)}
disabled={powerControlMutation.isPending || plugStatus?.state === 'ON' || !hasPermission('smart_plugs:control')}
className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
!hasPermission('smart_plugs:control')
? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
: plugStatus?.state === 'ON'
? 'bg-bambu-green text-white'
: 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
}`}
title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : undefined}
>
<Power className="w-3 h-3" />
On
</button>
<button
onClick={() => setShowPowerOffConfirm(plug.id)}
disabled={powerControlMutation.isPending || plugStatus?.state === 'OFF' || !hasPermission('smart_plugs:control')}
className={`px-2 py-1 text-xs rounded transition-colors flex items-center gap-1 ${
!hasPermission('smart_plugs:control')
? 'bg-bambu-dark text-bambu-gray/50 cursor-not-allowed'
: plugStatus?.state === 'OFF'
? 'bg-red-500/30 text-red-400'
: 'bg-bambu-dark text-bambu-gray hover:text-white hover:bg-bambu-dark-tertiary'
}`}
title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : undefined}
>
<PowerOff className="w-3 h-3" />
Off
</button>
</div>
)}
{!isScript && (
<div className="flex items-center gap-2 flex-shrink-0">
<span className={`text-xs hidden sm:inline ${plug.auto_off_executed ? 'text-bambu-green' : 'text-bambu-gray'}`}>
{plug.auto_off_executed ? 'Auto-off done' : 'Auto-off'}
</span>
<button
onClick={() => toggleAutoOffMutation.mutate({ plugId: plug.id, enabled: !plug.auto_off })}
disabled={toggleAutoOffMutation.isPending || plug.auto_off_executed || !hasPermission('smart_plugs:control')}
title={!hasPermission('smart_plugs:control') ? t('printers.permission.noSmartPlugControl') : (plug.auto_off_executed ? t('printers.autoOffExecuted') : t('printers.autoOffAfterPrint'))}
className={`relative w-9 h-5 rounded-full transition-colors flex-shrink-0 ${
!hasPermission('smart_plugs:control')
? 'bg-bambu-dark-tertiary/50 cursor-not-allowed'
: plug.auto_off_executed
? 'bg-bambu-green/50 cursor-not-allowed'
: plug.auto_off ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
}`}
>
<span
className={`absolute top-[2px] left-[2px] w-4 h-4 bg-white rounded-full transition-transform ${
plug.auto_off || plug.auto_off_executed ? 'translate-x-4' : 'translate-x-0'
}`}
/>
</button>
</div>
)}
</div>
</div>
)}
);
})}
</div>
)}
@@ -4514,36 +4494,36 @@ function PrinterCard({
)}
{/* Power On Confirmation */}
{showPowerOnConfirm && smartPlug && (
{showPowerOnConfirm !== null && (
<ConfirmModal
title={t('printers.confirm.powerOnTitle')}
message={t('printers.confirm.powerOnMessage', { name: printer.name })}
message={t('printers.confirm.powerOnMessage', { name: smartPlugs?.find(p => p.id === showPowerOnConfirm)?.name || '' })}
confirmText={t('printers.confirm.powerOnButton')}
variant="default"
onConfirm={() => {
powerControlMutation.mutate('on');
setShowPowerOnConfirm(false);
powerControlMutation.mutate({ plugId: showPowerOnConfirm, action: 'on' });
setShowPowerOnConfirm(null);
}}
onCancel={() => setShowPowerOnConfirm(false)}
onCancel={() => setShowPowerOnConfirm(null)}
/>
)}
{/* Power Off Confirmation */}
{showPowerOffConfirm && smartPlug && (
{showPowerOffConfirm !== null && (
<ConfirmModal
title={t('printers.confirm.powerOffTitle')}
message={
status?.state === 'RUNNING'
? t('printers.confirm.powerOffWarning', { name: printer.name })
: t('printers.confirm.powerOffMessage', { name: printer.name })
? t('printers.confirm.powerOffWarning', { name: smartPlugs?.find(p => p.id === showPowerOffConfirm)?.name || '' })
: t('printers.confirm.powerOffMessage', { name: smartPlugs?.find(p => p.id === showPowerOffConfirm)?.name || '' })
}
confirmText={t('printers.confirm.powerOffButton')}
variant="danger"
onConfirm={() => {
powerControlMutation.mutate('off');
setShowPowerOffConfirm(false);
powerControlMutation.mutate({ plugId: showPowerOffConfirm, action: 'off' });
setShowPowerOffConfirm(null);
}}
onCancel={() => setShowPowerOffConfirm(false)}
onCancel={() => setShowPowerOffConfirm(null)}
/>
)}