All files / src/components SmartPlugCard.tsx

78.51% Statements 212/270
67.44% Branches 29/43
19.23% Functions 5/26
78.51% Lines 212/270

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 3551x                           1x 27x 27x 27x 27x 27x     27x 27x 27x 27x 27x     27x 27x 27x 27x   27x     27x 27x 27x     27x     27x 27x 27x             27x     27x 27x 27x     27x   27x 27x 27x   27x 27x 27x 27x   27x 27x 27x 27x 27x 27x 27x 27x 27x 27x     27x 27x 13x 14x 14x 14x 14x 14x             27x 27x     27x 14x 14x 14x 14x       27x 1x 1x           1x 1x 1x 1x 1x       1x   1x       27x 27x 27x 27x 27x 27x 27x   27x   27x 27x 27x 27x 27x 27x 27x   27x   27x 27x     27x 27x 27x   27x 27x   27x 27x 27x     27x 6x   6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x     6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x     6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x     6x 4x 4x 4x 4x 4x 4x 4x 4x 4x   4x   4x   4x 4x 4x 4x 4x   4x 4x   4x   4x 4x 4x   4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x                             4x       6x 6x 6x 6x 6x 6x   6x   6x 6x 6x 6x 6x 6x   6x 6x 6x 6x   27x 27x     27x                             27x                             27x 1x 1x 1x 1x 1x 1x       1x 1x   27x   27x  
import { useState } from 'react';
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar } from 'lucide-react';
import { api } from '../api/client';
import type { SmartPlug, SmartPlugUpdate } from '../api/client';
import { Card, CardContent } from './Card';
import { Button } from './Button';
import { ConfirmModal } from './ConfirmModal';
 
interface SmartPlugCardProps {
  plug: SmartPlug;
  onEdit: (plug: SmartPlug) => void;
}
 
export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
  const queryClient = useQueryClient();
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
  const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
  const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false);
  const [isExpanded, setIsExpanded] = useState(false);
 
  // Fetch current status
  const { data: status, isLoading: statusLoading, refetch: refetchStatus } = useQuery({
    queryKey: ['smart-plug-status', plug.id],
    queryFn: () => api.getSmartPlugStatus(plug.id),
    refetchInterval: 30000, // Refresh every 30 seconds
  });
 
  // Fetch printers for linking
  const { data: printers } = useQuery({
    queryKey: ['printers'],
    queryFn: api.getPrinters,
  });
 
  const linkedPrinter = printers?.find(p => p.id === plug.printer_id);
 
  // Control mutation
  const controlMutation = useMutation({
    mutationFn: (action: 'on' | 'off' | 'toggle') => api.controlSmartPlug(plug.id, action),
    onSuccess: () => {
      refetchStatus();
    },
  });
 
  // Update mutation
  const updateMutation = useMutation({
    mutationFn: (data: SmartPlugUpdate) => api.updateSmartPlug(plug.id, data),
    onSuccess: () => {
      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] });
      }
    },
  });
 
  // Delete mutation
  const deleteMutation = useMutation({
    mutationFn: () => api.deleteSmartPlug(plug.id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
    },
  });
 
  const isOn = status?.state === 'ON';
  const isReachable = status?.reachable ?? false;
  const isPending = controlMutation.isPending;
 
  return (
    <>
      <Card className="relative">
        <CardContent className="p-4">
          {/* Header Row */}
          <div className="flex items-start justify-between mb-3">
            <div className="flex items-center gap-3">
              <div className={`p-2 rounded-lg ${isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20'}`}>
                <Plug className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
              </div>
              <div>
                <h3 className="font-medium text-white">{plug.name}</h3>
                <p className="text-sm text-bambu-gray">{plug.ip_address}</p>
              </div>
            </div>
 
            {/* Status indicator */}
            <div className="flex items-center gap-2">
              {statusLoading ? (
                <Loader2 className="w-4 h-4 text-bambu-gray animate-spin" />
              ) : isReachable ? (
                <div className="flex items-center gap-1 text-sm">
                  <Wifi className="w-4 h-4 text-bambu-green" />
                  <span className={isOn ? 'text-bambu-green' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
                </div>
              ) : (
                <div className="flex items-center gap-1 text-sm text-red-400">
                  <WifiOff className="w-4 h-4" />
                  <span>Offline</span>
                </div>
              )}
            </div>
          </div>
 
          {/* Linked Printer */}
          {linkedPrinter && (
            <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
              <span className="text-xs text-bambu-gray">Linked to: </span>
              <span className="text-sm text-white">{linkedPrinter.name}</span>
            </div>
          )}
 
          {/* Feature Badges */}
          {(plug.power_alert_enabled || plug.schedule_enabled) && (
            <div className="flex flex-wrap gap-1.5 mb-3">
              {plug.power_alert_enabled && (
                <span className="flex items-center gap-1 px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded-full">
                  <Bell className="w-3 h-3" />
                  Alerts
                </span>
              )}
              {plug.schedule_enabled && (
                <span className="flex items-center gap-1 px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full">
                  <Calendar className="w-3 h-3" />
                  {plug.schedule_on_time && plug.schedule_off_time
                    ? `${plug.schedule_on_time} - ${plug.schedule_off_time}`
                    : plug.schedule_on_time
                      ? `On ${plug.schedule_on_time}`
                      : `Off ${plug.schedule_off_time}`}
                </span>
              )}
            </div>
          )}
 
          {/* Quick Controls */}
          <div className="flex gap-2 mb-3">
            <Button
              size="sm"
              variant={isOn ? 'primary' : 'secondary'}
              disabled={!isReachable || isPending}
              onClick={() => setShowPowerOnConfirm(true)}
              className="flex-1"
            >
              {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
              On
            </Button>
            <Button
              size="sm"
              variant={!isOn ? 'primary' : 'secondary'}
              disabled={!isReachable || isPending}
              onClick={() => setShowPowerOffConfirm(true)}
              className="flex-1"
            >
              {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
              Off
            </Button>
          </div>
 
          {/* Toggle Settings Panel */}
          <button
            onClick={() => setIsExpanded(!isExpanded)}
            className="w-full flex items-center justify-between py-2 text-sm text-bambu-gray hover:text-white transition-colors"
          >
            <span className="flex items-center gap-2">
              <Settings2 className="w-4 h-4" />
              Automation Settings
            </span>
            <span>{isExpanded ? '-' : '+'}</span>
          </button>
 
          {/* Expanded Settings */}
          {isExpanded && (
            <div className="pt-3 border-t border-bambu-dark-tertiary space-y-4">
              {/* Enabled Toggle */}
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-sm text-white">Enabled</p>
                  <p className="text-xs text-bambu-gray">Enable automation for this plug</p>
                </div>
                <label className="relative inline-flex items-center cursor-pointer">
                  <input
                    type="checkbox"
                    checked={plug.enabled}
                    onChange={(e) => updateMutation.mutate({ enabled: e.target.checked })}
                    className="sr-only peer"
                  />
                  <div className="w-9 h-5 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-bambu-green"></div>
                </label>
              </div>
 
              {/* Auto On */}
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-sm text-white">Auto On</p>
                  <p className="text-xs text-bambu-gray">Turn on when print starts</p>
                </div>
                <label className="relative inline-flex items-center cursor-pointer">
                  <input
                    type="checkbox"
                    checked={plug.auto_on}
                    onChange={(e) => updateMutation.mutate({ auto_on: e.target.checked })}
                    className="sr-only peer"
                  />
                  <div className="w-9 h-5 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-bambu-green"></div>
                </label>
              </div>
 
              {/* Auto Off */}
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-sm text-white">Auto Off</p>
                  <p className="text-xs text-bambu-gray">Turn off when print completes (one-shot)</p>
                </div>
                <label className="relative inline-flex items-center cursor-pointer">
                  <input
                    type="checkbox"
                    checked={plug.auto_off}
                    onChange={(e) => updateMutation.mutate({ auto_off: e.target.checked })}
                    className="sr-only peer"
                  />
                  <div className="w-9 h-5 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-bambu-green"></div>
                </label>
              </div>
 
              {/* Delay Mode */}
              {plug.auto_off && (
                <div className="space-y-3 pl-4 border-l-2 border-bambu-dark-tertiary">
                  <div>
                    <p className="text-sm text-white mb-2">Turn Off Delay Mode</p>
                    <div className="flex gap-2">
                      <button
                        onClick={() => updateMutation.mutate({ off_delay_mode: 'time' })}
                        className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
                          plug.off_delay_mode === 'time'
                            ? 'bg-bambu-green text-white'
                            : 'bg-bambu-dark text-bambu-gray hover:text-white'
                        }`}
                      >
                        <Clock className="w-4 h-4" />
                        Time
                      </button>
                      <button
                        onClick={() => updateMutation.mutate({ off_delay_mode: 'temperature' })}
                        className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
                          plug.off_delay_mode === 'temperature'
                            ? 'bg-bambu-green text-white'
                            : 'bg-bambu-dark text-bambu-gray hover:text-white'
                        }`}
                      >
                        <Thermometer className="w-4 h-4" />
                        Temp
                      </button>
                    </div>
                  </div>
 
                  {plug.off_delay_mode === 'time' ? (
                    <div>
                      <label className="block text-xs text-bambu-gray mb-1">Delay (minutes)</label>
                      <input
                        type="number"
                        min="1"
                        max="60"
                        value={plug.off_delay_minutes}
                        onChange={(e) => updateMutation.mutate({ off_delay_minutes: parseInt(e.target.value) || 5 })}
                        className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none"
                      />
                    </div>
                  ) : (
                    <div>
                      <label className="block text-xs text-bambu-gray mb-1">Temperature threshold (C)</label>
                      <input
                        type="number"
                        min="30"
                        max="100"
                        value={plug.off_temp_threshold}
                        onChange={(e) => updateMutation.mutate({ off_temp_threshold: parseInt(e.target.value) || 70 })}
                        className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:border-bambu-green focus:outline-none"
                      />
                      <p className="text-xs text-bambu-gray mt-1">Turns off when nozzle cools below this temperature</p>
                    </div>
                  )}
                </div>
              )}
 
              {/* Action Buttons */}
              <div className="flex gap-2 pt-2">
                <Button
                  size="sm"
                  variant="secondary"
                  onClick={() => onEdit(plug)}
                  className="flex-1"
                >
                  <Edit2 className="w-4 h-4" />
                  Edit
                </Button>
                <Button
                  size="sm"
                  variant="secondary"
                  onClick={() => setShowDeleteConfirm(true)}
                  className="text-red-400 hover:text-red-300"
                >
                  <Trash2 className="w-4 h-4" />
                </Button>
              </div>
            </div>
          )}
        </CardContent>
      </Card>
 
      {/* Delete Confirmation */}
      {showDeleteConfirm && (
        <ConfirmModal
          title="Delete Smart Plug"
          message={`Are you sure you want to delete "${plug.name}"? This cannot be undone.`}
          confirmText="Delete"
          variant="danger"
          onConfirm={() => {
            deleteMutation.mutate();
            setShowDeleteConfirm(false);
          }}
          onCancel={() => setShowDeleteConfirm(false)}
        />
      )}
 
      {/* Power On Confirmation */}
      {showPowerOnConfirm && (
        <ConfirmModal
          title="Turn On Smart Plug"
          message={`Are you sure you want to turn on "${plug.name}"?`}
          confirmText="Turn On"
          variant="default"
          onConfirm={() => {
            controlMutation.mutate('on');
            setShowPowerOnConfirm(false);
          }}
          onCancel={() => setShowPowerOnConfirm(false)}
        />
      )}
 
      {/* Power Off Confirmation */}
      {showPowerOffConfirm && (
        <ConfirmModal
          title="Turn Off Smart Plug"
          message={`Are you sure you want to turn off "${plug.name}"? This will cut power to the connected device.`}
          confirmText="Turn Off"
          variant="danger"
          onConfirm={() => {
            controlMutation.mutate('off');
            setShowPowerOffConfirm(false);
          }}
          onCancel={() => setShowPowerOffConfirm(false)}
        />
      )}
    </>
  );
}