SmartPlugCard.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import { useState } from 'react';
  2. import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
  3. import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar, LayoutGrid, ExternalLink, Home, Radio, Eye, Globe } from 'lucide-react';
  4. import { useTranslation } from 'react-i18next';
  5. import { api } from '../api/client';
  6. import type { SmartPlug, SmartPlugUpdate } from '../api/client';
  7. import { Card, CardContent } from './Card';
  8. import { Button } from './Button';
  9. import { ConfirmModal } from './ConfirmModal';
  10. import { useToast } from '../contexts/ToastContext';
  11. interface SmartPlugCardProps {
  12. plug: SmartPlug;
  13. onEdit: (plug: SmartPlug) => void;
  14. }
  15. export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
  16. const { t } = useTranslation();
  17. const queryClient = useQueryClient();
  18. const { showToast } = useToast();
  19. const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
  20. const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
  21. const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false);
  22. const [isExpanded, setIsExpanded] = useState(false);
  23. // Fetch current status
  24. const { data: status, isLoading: statusLoading } = useQuery({
  25. queryKey: ['smart-plug-status', plug.id],
  26. queryFn: () => api.getSmartPlugStatus(plug.id),
  27. refetchInterval: 30000, // Refresh every 30 seconds
  28. });
  29. // Fetch printers for linking
  30. const { data: printers } = useQuery({
  31. queryKey: ['printers'],
  32. queryFn: api.getPrinters,
  33. });
  34. const linkedPrinter = printers?.find(p => p.id === plug.printer_id);
  35. // Control mutation with optimistic updates
  36. const controlMutation = useMutation({
  37. mutationFn: (action: 'on' | 'off' | 'toggle') => api.controlSmartPlug(plug.id, action),
  38. onMutate: async (action) => {
  39. // Cancel any outgoing refetches
  40. await queryClient.cancelQueries({ queryKey: ['smart-plug-status', plug.id] });
  41. // Snapshot the previous value
  42. const previousStatus = queryClient.getQueryData(['smart-plug-status', plug.id]);
  43. // Optimistically update to the new value
  44. const newState = action === 'on' ? 'ON' : action === 'off' ? 'OFF' : (status?.state === 'ON' ? 'OFF' : 'ON');
  45. queryClient.setQueryData(['smart-plug-status', plug.id], (old: typeof status) => ({
  46. ...old,
  47. state: newState,
  48. }));
  49. return { previousStatus };
  50. },
  51. onError: (_err, action, context) => {
  52. // Rollback on error
  53. if (context?.previousStatus) {
  54. queryClient.setQueryData(['smart-plug-status', plug.id], context.previousStatus);
  55. }
  56. showToast(t('smartPlugs.failedToTurn', { action, name: plug.name }), 'error');
  57. },
  58. onSettled: () => {
  59. // Refetch after a short delay to get actual state
  60. setTimeout(() => {
  61. queryClient.invalidateQueries({ queryKey: ['smart-plug-status', plug.id] });
  62. queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
  63. }, 1000);
  64. },
  65. });
  66. // Update mutation
  67. const updateMutation = useMutation({
  68. mutationFn: (data: SmartPlugUpdate) => api.updateSmartPlug(plug.id, data),
  69. onSuccess: () => {
  70. queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
  71. // Also invalidate printer-specific smart plug queries to keep PrintersPage in sync
  72. if (plug.printer_id) {
  73. queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', plug.printer_id] });
  74. queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] });
  75. }
  76. },
  77. });
  78. // Delete mutation
  79. const deleteMutation = useMutation({
  80. mutationFn: () => api.deleteSmartPlug(plug.id),
  81. onSuccess: () => {
  82. queryClient.invalidateQueries({ queryKey: ['smart-plugs'] });
  83. // Also invalidate printer card HA entity queries
  84. if (plug.printer_id) {
  85. queryClient.invalidateQueries({ queryKey: ['scriptPlugsByPrinter', plug.printer_id] });
  86. }
  87. },
  88. });
  89. const isOn = status?.state === 'ON';
  90. // For MQTT plugs, consider reachable if we have power data (even if backend says not reachable)
  91. const hasMqttData = plug.plug_type === 'mqtt' && (status?.energy?.power !== null && status?.energy?.power !== undefined);
  92. const isReachable = (status?.reachable ?? false) || hasMqttData;
  93. const isPending = controlMutation.isPending;
  94. // Generate admin URL with auto-login credentials (Tasmota only)
  95. const getAdminUrl = () => {
  96. if (plug.plug_type !== 'tasmota' || !plug.ip_address) return null;
  97. const ip = plug.ip_address;
  98. if (plug.username && plug.password) {
  99. // Use HTTP Basic Auth in URL for auto-login
  100. return `http://${encodeURIComponent(plug.username)}:${encodeURIComponent(plug.password)}@${ip}/`;
  101. }
  102. return `http://${ip}/`;
  103. };
  104. const adminUrl = getAdminUrl();
  105. return (
  106. <>
  107. <Card className="relative">
  108. <CardContent className="p-4">
  109. {/* Header Row */}
  110. <div className="flex items-start justify-between gap-2 mb-3">
  111. <div className="flex items-center gap-3 min-w-0 flex-1">
  112. <div className={`p-2 rounded-lg flex-shrink-0 ${
  113. plug.plug_type === 'mqtt'
  114. ? (isReachable ? 'bg-teal-500/20' : 'bg-red-500/20')
  115. : (isReachable ? (isOn ? 'bg-bambu-green/20' : 'bg-bambu-dark') : 'bg-red-500/20')
  116. }`}>
  117. {plug.plug_type === 'mqtt' ? (
  118. <Radio className={`w-5 h-5 ${isReachable ? 'text-teal-400' : 'text-red-400'}`} />
  119. ) : plug.plug_type === 'homeassistant' ? (
  120. <Home className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
  121. ) : plug.plug_type === 'rest' ? (
  122. <Globe className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
  123. ) : (
  124. <Plug className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
  125. )}
  126. </div>
  127. <div className="min-w-0">
  128. <h3 className="font-medium text-white truncate">{plug.name}</h3>
  129. <p
  130. className="text-sm text-bambu-gray truncate"
  131. title={plug.plug_type === 'mqtt' ? plug.mqtt_topic ?? undefined : plug.plug_type === 'homeassistant' ? plug.ha_entity_id ?? undefined : plug.plug_type === 'rest' ? plug.rest_on_url ?? plug.rest_off_url ?? undefined : plug.ip_address ?? undefined}
  132. >
  133. {plug.plug_type === 'mqtt' ? plug.mqtt_topic : plug.plug_type === 'homeassistant' ? plug.ha_entity_id : plug.plug_type === 'rest' ? (plug.rest_on_url || plug.rest_off_url) : plug.ip_address}
  134. </p>
  135. </div>
  136. </div>
  137. {/* Status indicator */}
  138. <div className="flex flex-col items-end gap-1 flex-shrink-0">
  139. {statusLoading ? (
  140. <Loader2 className="w-4 h-4 text-bambu-gray animate-spin" />
  141. ) : plug.plug_type === 'mqtt' ? (
  142. /* MQTT plugs - show badge and checkmark when receiving data */
  143. <div className="flex items-center gap-1.5 text-sm whitespace-nowrap">
  144. <span className="px-1.5 py-0.5 bg-teal-500/20 text-teal-400 text-[10px] font-medium rounded flex-shrink-0">MQTT</span>
  145. {isReachable && <span className="text-status-ok">✓</span>}
  146. </div>
  147. ) : plug.plug_type === 'homeassistant' ? (
  148. <div className="flex items-center gap-1 text-sm">
  149. <span className="px-1 py-0.5 bg-blue-500/20 text-blue-400 text-[10px] font-medium rounded">HA</span>
  150. <span className={isReachable ? (isOn ? 'text-status-ok' : 'text-bambu-gray') : 'text-status-error'}>
  151. {isReachable ? (status?.state || '?') : t('smartPlugs.offline')}
  152. </span>
  153. </div>
  154. ) : plug.plug_type === 'rest' ? (
  155. <div className="flex items-center gap-1 text-sm">
  156. <span className="px-1 py-0.5 bg-purple-500/20 text-purple-400 text-[10px] font-medium rounded">REST</span>
  157. <span className={isReachable ? (isOn ? 'text-status-ok' : 'text-bambu-gray') : 'text-status-error'}>
  158. {isReachable ? (status?.state || '?') : t('smartPlugs.offline')}
  159. </span>
  160. </div>
  161. ) : isReachable ? (
  162. <div className="flex items-center gap-1 text-sm">
  163. <Wifi className="w-4 h-4 text-status-ok" />
  164. <span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>{status?.state || t('smartPlugs.unknown')}</span>
  165. </div>
  166. ) : (
  167. <div className="flex items-center gap-1 text-sm text-status-error">
  168. <WifiOff className="w-4 h-4" />
  169. <span>{t('smartPlugs.offline')}</span>
  170. </div>
  171. )}
  172. {/* Admin page link - only for Tasmota */}
  173. {adminUrl && (
  174. <a
  175. href={adminUrl}
  176. target="_blank"
  177. rel="noopener noreferrer"
  178. className="flex items-center gap-1 px-2 py-0.5 bg-bambu-dark hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white text-xs rounded-full transition-colors"
  179. title={t('smartPlugs.openPlugAdminPage')}
  180. >
  181. <ExternalLink className="w-3 h-3" />
  182. {t('smartPlugs.admin')}
  183. </a>
  184. )}
  185. </div>
  186. </div>
  187. {/* Linked Printer */}
  188. {linkedPrinter && (
  189. <div className="mb-3 px-2 py-1.5 bg-bambu-dark rounded-lg">
  190. <span className="text-xs text-bambu-gray">{t('smartPlugs.linkedTo')} </span>
  191. <span className="text-sm text-white">{linkedPrinter.name}</span>
  192. </div>
  193. )}
  194. {/* Feature Badges */}
  195. {(plug.power_alert_enabled || plug.schedule_enabled || plug.plug_type === 'mqtt') && (
  196. <div className="flex flex-wrap gap-1.5 mb-3">
  197. {plug.plug_type === 'mqtt' && (
  198. <span className="flex items-center gap-1 px-2 py-0.5 bg-teal-500/20 text-teal-400 text-xs rounded-full">
  199. <Eye className="w-3 h-3" />
  200. {t('smartPlugs.monitorOnly')}
  201. </span>
  202. )}
  203. {plug.power_alert_enabled && (
  204. <span className="flex items-center gap-1 px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded-full">
  205. <Bell className="w-3 h-3" />
  206. {t('smartPlugs.alerts')}
  207. </span>
  208. )}
  209. {plug.schedule_enabled && (
  210. <span className="flex items-center gap-1 px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded-full">
  211. <Calendar className="w-3 h-3" />
  212. {plug.schedule_on_time && plug.schedule_off_time
  213. ? `${plug.schedule_on_time} - ${plug.schedule_off_time}`
  214. : plug.schedule_on_time
  215. ? t('smartPlugs.scheduleOn', { time: plug.schedule_on_time })
  216. : t('smartPlugs.scheduleOff', { time: plug.schedule_off_time })}
  217. </span>
  218. )}
  219. </div>
  220. )}
  221. {/* Quick Controls - hidden for MQTT plugs (monitor-only) */}
  222. {plug.plug_type !== 'mqtt' && (
  223. <div className="flex gap-2 mb-3">
  224. <Button
  225. size="sm"
  226. variant={isOn ? 'primary' : 'secondary'}
  227. disabled={!isReachable || isPending}
  228. onClick={() => setShowPowerOnConfirm(true)}
  229. className="flex-1"
  230. >
  231. {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Power className="w-4 h-4" />}
  232. {t('smartPlugs.on')}
  233. </Button>
  234. <Button
  235. size="sm"
  236. variant={!isOn ? 'primary' : 'secondary'}
  237. disabled={!isReachable || isPending}
  238. onClick={() => setShowPowerOffConfirm(true)}
  239. className="flex-1"
  240. >
  241. {isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <PowerOff className="w-4 h-4" />}
  242. {t('smartPlugs.off')}
  243. </Button>
  244. </div>
  245. )}
  246. {/* Energy display for MQTT plugs */}
  247. {plug.plug_type === 'mqtt' && status?.energy && (
  248. <div className="flex gap-2 mb-3 px-3 py-2 bg-bambu-dark rounded-lg">
  249. {status.energy.power !== null && status.energy.power !== undefined && (
  250. <div className="flex-1 text-center">
  251. <p className="text-lg font-semibold text-white">{Math.round(status.energy.power)}W</p>
  252. <p className="text-xs text-bambu-gray">{t('smartPlugs.power')}</p>
  253. </div>
  254. )}
  255. {status.energy.today !== null && status.energy.today !== undefined && (
  256. <div className="flex-1 text-center border-l border-bambu-dark-tertiary">
  257. <p className="text-lg font-semibold text-white">{status.energy.today.toFixed(3)}</p>
  258. <p className="text-xs text-bambu-gray">{t('smartPlugs.kwhToday')}</p>
  259. </div>
  260. )}
  261. </div>
  262. )}
  263. {/* Toggle Settings Panel */}
  264. <button
  265. onClick={() => setIsExpanded(!isExpanded)}
  266. className="w-full flex items-center justify-between py-2 text-sm text-bambu-gray hover:text-white transition-colors"
  267. >
  268. <span className="flex items-center gap-2">
  269. <Settings2 className="w-4 h-4" />
  270. {plug.plug_type === 'mqtt' ? t('smartPlugs.settings') : t('smartPlugs.automationSettings')}
  271. </span>
  272. <span>{isExpanded ? '-' : '+'}</span>
  273. </button>
  274. {/* Expanded Settings */}
  275. {isExpanded && (
  276. <div className="pt-3 border-t border-bambu-dark-tertiary space-y-4">
  277. {/* Show in Switchbar Toggle */}
  278. <div className="flex items-center justify-between">
  279. <div className="flex items-center gap-2">
  280. <LayoutGrid className="w-4 h-4 text-bambu-green" />
  281. <div>
  282. <p className="text-sm text-white">{t('smartPlugs.showInSwitchbar')}</p>
  283. <p className="text-xs text-bambu-gray">{t('smartPlugs.quickAccessSidebar')}</p>
  284. </div>
  285. </div>
  286. <label className="relative inline-flex items-center cursor-pointer">
  287. <input
  288. type="checkbox"
  289. checked={plug.show_in_switchbar}
  290. onChange={(e) => updateMutation.mutate({ show_in_switchbar: e.target.checked })}
  291. className="sr-only peer"
  292. />
  293. <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>
  294. </label>
  295. </div>
  296. {/* Automation controls - only for controllable plugs (not MQTT) */}
  297. {plug.plug_type !== 'mqtt' && (
  298. <>
  299. {/* Enabled Toggle */}
  300. <div className="flex items-center justify-between">
  301. <div>
  302. <p className="text-sm text-white">{t('smartPlugs.enabled')}</p>
  303. <p className="text-xs text-bambu-gray">{t('smartPlugs.enableAutomation')}</p>
  304. </div>
  305. <label className="relative inline-flex items-center cursor-pointer">
  306. <input
  307. type="checkbox"
  308. checked={plug.enabled}
  309. onChange={(e) => updateMutation.mutate({ enabled: e.target.checked })}
  310. className="sr-only peer"
  311. />
  312. <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>
  313. </label>
  314. </div>
  315. {/* Auto On */}
  316. <div className="flex items-center justify-between">
  317. <div>
  318. <p className="text-sm text-white">{t('smartPlugs.autoOn')}</p>
  319. <p className="text-xs text-bambu-gray">{t('smartPlugs.autoOnDescription')}</p>
  320. </div>
  321. <label className="relative inline-flex items-center cursor-pointer">
  322. <input
  323. type="checkbox"
  324. checked={plug.auto_on}
  325. onChange={(e) => updateMutation.mutate({ auto_on: e.target.checked })}
  326. className="sr-only peer"
  327. />
  328. <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>
  329. </label>
  330. </div>
  331. {/* Auto Off */}
  332. <div className="flex items-center justify-between">
  333. <div>
  334. <p className="text-sm text-white">{t('smartPlugs.autoOff')}</p>
  335. <p className="text-xs text-bambu-gray">{t('smartPlugs.autoOffDescription')}</p>
  336. </div>
  337. <label className="relative inline-flex items-center cursor-pointer">
  338. <input
  339. type="checkbox"
  340. checked={plug.auto_off}
  341. onChange={(e) => updateMutation.mutate({ auto_off: e.target.checked })}
  342. className="sr-only peer"
  343. />
  344. <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>
  345. </label>
  346. </div>
  347. {/* Auto Off Persistent */}
  348. {plug.auto_off && (
  349. <div className="flex items-center justify-between pl-4 border-l-2 border-bambu-dark-tertiary">
  350. <div>
  351. <p className="text-sm text-white">{t('smartPlugs.autoOffPersistent')}</p>
  352. <p className="text-xs text-bambu-gray">{t('smartPlugs.autoOffPersistentDescription')}</p>
  353. </div>
  354. <label className="relative inline-flex items-center cursor-pointer">
  355. <input
  356. type="checkbox"
  357. checked={plug.auto_off_persistent}
  358. onChange={(e) => updateMutation.mutate({ auto_off_persistent: e.target.checked })}
  359. className="sr-only peer"
  360. />
  361. <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>
  362. </label>
  363. </div>
  364. )}
  365. {/* Delay Mode */}
  366. {plug.auto_off && (
  367. <div className="space-y-3 pl-4 border-l-2 border-bambu-dark-tertiary">
  368. <div>
  369. <p className="text-sm text-white mb-2">{t('smartPlugs.turnOffDelayMode')}</p>
  370. <div className="flex gap-2">
  371. <button
  372. onClick={() => updateMutation.mutate({ off_delay_mode: 'time' })}
  373. className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
  374. plug.off_delay_mode === 'time'
  375. ? 'bg-bambu-green text-white'
  376. : 'bg-bambu-dark text-bambu-gray hover:text-white'
  377. }`}
  378. >
  379. <Clock className="w-4 h-4" />
  380. {t('smartPlugs.time')}
  381. </button>
  382. <button
  383. onClick={() => updateMutation.mutate({ off_delay_mode: 'temperature' })}
  384. className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors ${
  385. plug.off_delay_mode === 'temperature'
  386. ? 'bg-bambu-green text-white'
  387. : 'bg-bambu-dark text-bambu-gray hover:text-white'
  388. }`}
  389. >
  390. <Thermometer className="w-4 h-4" />
  391. {t('smartPlugs.temp')}
  392. </button>
  393. </div>
  394. </div>
  395. {plug.off_delay_mode === 'time' ? (
  396. <div>
  397. <label className="block text-xs text-bambu-gray mb-1">{t('smartPlugs.delayMinutes')}</label>
  398. <input
  399. type="number"
  400. min="1"
  401. max="60"
  402. value={plug.off_delay_minutes}
  403. onChange={(e) => updateMutation.mutate({ off_delay_minutes: parseInt(e.target.value) || 5 })}
  404. 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"
  405. />
  406. </div>
  407. ) : (
  408. <div>
  409. <label className="block text-xs text-bambu-gray mb-1">{t('smartPlugs.tempThreshold')}</label>
  410. <input
  411. type="number"
  412. min="30"
  413. max="100"
  414. value={plug.off_temp_threshold}
  415. onChange={(e) => updateMutation.mutate({ off_temp_threshold: parseInt(e.target.value) || 70 })}
  416. 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"
  417. />
  418. <p className="text-xs text-bambu-gray mt-1">{t('smartPlugs.tempThresholdDescription')}</p>
  419. </div>
  420. )}
  421. </div>
  422. )}
  423. </>
  424. )}
  425. {/* Action Buttons */}
  426. <div className="flex gap-2 pt-2">
  427. <Button
  428. size="sm"
  429. variant="secondary"
  430. onClick={() => onEdit(plug)}
  431. className="flex-1"
  432. >
  433. <Edit2 className="w-4 h-4" />
  434. {t('smartPlugs.edit')}
  435. </Button>
  436. <Button
  437. size="sm"
  438. variant="secondary"
  439. onClick={() => setShowDeleteConfirm(true)}
  440. className="text-red-400 hover:text-red-300"
  441. >
  442. <Trash2 className="w-4 h-4" />
  443. </Button>
  444. </div>
  445. </div>
  446. )}
  447. </CardContent>
  448. </Card>
  449. {/* Delete Confirmation */}
  450. {showDeleteConfirm && (
  451. <ConfirmModal
  452. title={t('smartPlugs.deleteSmartPlug')}
  453. message={t('smartPlugs.deleteConfirm', { name: plug.name })}
  454. confirmText={t('smartPlugs.delete')}
  455. variant="danger"
  456. onConfirm={() => {
  457. deleteMutation.mutate();
  458. setShowDeleteConfirm(false);
  459. }}
  460. onCancel={() => setShowDeleteConfirm(false)}
  461. />
  462. )}
  463. {/* Power On Confirmation */}
  464. {showPowerOnConfirm && (
  465. <ConfirmModal
  466. title={t('smartPlugs.turnOnSmartPlug')}
  467. message={t('smartPlugs.turnOnConfirm', { name: plug.name })}
  468. confirmText={t('smartPlugs.turnOn')}
  469. variant="default"
  470. onConfirm={() => {
  471. controlMutation.mutate('on');
  472. setShowPowerOnConfirm(false);
  473. }}
  474. onCancel={() => setShowPowerOnConfirm(false)}
  475. />
  476. )}
  477. {/* Power Off Confirmation */}
  478. {showPowerOffConfirm && (
  479. <ConfirmModal
  480. title={t('smartPlugs.turnOffSmartPlug')}
  481. message={t('smartPlugs.turnOffConfirm', { name: plug.name })}
  482. confirmText={t('smartPlugs.turnOff')}
  483. variant="danger"
  484. onConfirm={() => {
  485. controlMutation.mutate('off');
  486. setShowPowerOffConfirm(false);
  487. }}
  488. onCancel={() => setShowPowerOffConfirm(false)}
  489. />
  490. )}
  491. </>
  492. );
  493. }