from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QGridLayout,
QProgressBar,
QScrollArea
)
from view.shared.components.buttons import (
Button
)
from view.shared.components.labels import (
PageSubtitle
)
from view.shared.components.cards import (
Card,
StatCard
)
from view.shared.components.modal import (
Modal
)
from view.shared.components.toast import (
ToastManager
)
from view.admin.dialogs.add_device_dialog import (
AddDeviceDialog
)
from view.admin.dialogs.manage_device_dialog import (
ManageDeviceDialog
)
DEVICE_TYPES = [
"VR Headset",
"AR Glasses",
"XR Controller",
]
STATUSES = [
"Online",
"Offline",
"In Use",
"Maintenance",
"Faulty",
]
# Chip-uri de status (culori soft, in ton cu
# badge-urile din restul aplicatiei).
CHIP_BASE = (
"border-radius: 10px;"
"padding: 4px 12px;"
"font-size: 11px;"
"font-weight: 700;"
)
CHIP_STYLES = {
"Online": (
"background-color: #dcfce7;"
"color: #166534;"
),
"Offline": (
"background-color: #e5e7eb;"
"color: #6b7280;"
),
"In Use": (
"background-color: #dbeafe;"
"color: #1e40af;"
),
"Maintenance": (
"background-color: #fef3c7;"
"color: #92400e;"
),
"Faulty": (
"background-color: #fee2e2;"
"color: #991b1b;"
),
}
def chip_style(status):
"""
Stilul complet pentru chip-ul de status.
"""
return CHIP_BASE + CHIP_STYLES.get(
status,
CHIP_STYLES["Offline"]
)
# Date mock — la integrare se inlocuiesc cu
# apeluri prin viewmodel (GET /api/devices).
# Endpointul nu exista inca in backend.
DEVICES = [
{
"name": "XR Station Alpha",
"type": "VR Headset",
"dev_id": "DEV-001",
"version": "v3.2.1",
"status": "Online",
"paired": True,
"battery": 87,
"network": 94,
"note": None,
},
{
"name": "XR Station Beta",
"type": "AR Glasses",
"dev_id": "DEV-002",
"version": "v3.1.9",
"status": "Faulty",
"paired": True,
"battery": 32,
"network": 45,
"note": "Sensor calibration failed",
},
{
"name": "XR Station Gamma",
"type": "VR Headset",
"dev_id": "DEV-003",
"version": "v3.0.8",
"status": "Offline",
"paired": False,
"battery": 12,
"network": 0,
"note": None,
},
{
"name": "Controller Set A",
"type": "XR Controller",
"dev_id": "DEV-004",
"version": "v2.4.0",
"status": "In Use",
"paired": True,
"battery": 75,
"network": 98,
"note": None,
},
{
"name": "XR Station Delta",
"type": "AR Glasses",
"dev_id": "DEV-005",
"version": "v3.2.1",
"status": "Online",
"paired": True,
"battery": 61,
"network": 88,
"note": None,
},
{
"name": "Controller Set B",
"type": "XR Controller",
"dev_id": "DEV-006",
"version": "v2.3.5",
"status": "Maintenance",
"paired": False,
"battery": 55,
"network": 72,
"note": None,
},
]
class DeviceCard(Card):
"""
Card de device: status, nume, detalii,
banner de problema (la Faulty), bare
Battery/Network si un buton Manage pentru
setari.
"""
def __init__(
self,
device,
on_change=None,
on_delete=None,
parent=None
):
super().__init__(parent)
self.device = device
self.on_change = on_change
self.on_delete = on_delete
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 16, 20, 16)
layout.setSpacing(10)
#
# Header
#
header = QHBoxLayout()
header.setSpacing(10)
self.status_chip = QLabel()
self.name_label = QLabel()
self.name_label.setObjectName(
"SettingTitle"
)
self.paired_label = QLabel()
self.paired_label.setObjectName(
"SettingSubtitle"
)
manage_btn = Button(
"Manage",
variant="primary",
icon="fa5s.cog"
)
manage_btn.clicked.connect(
self.open_manage_dialog
)
header.addWidget(self.status_chip)
header.addWidget(self.name_label)
header.addStretch()
header.addWidget(self.paired_label)
header.addWidget(manage_btn)
layout.addLayout(header)
#
# Detalii
#
self.details_label = QLabel()
self.details_label.setObjectName(
"SettingSubtitle"
)
layout.addWidget(self.details_label)
#
# Banner de problema (Faulty)
#
self.note_banner = QLabel()
self.note_banner.setStyleSheet(
"background-color: #fee2e2;"
"color: #991b1b;"
"border-radius: 8px;"
"padding: 8px 12px;"
"font-size: 12px;"
"font-weight: 600;"
)
layout.addWidget(self.note_banner)
#
# Baterie + Retea
#
meters = QHBoxLayout()
meters.setSpacing(16)
for key, caption in [
("battery", "Battery"),
("network", "Network"),
]:
meter_layout = QHBoxLayout()
meter_layout.setSpacing(8)
caption_label = QLabel(caption)
caption_label.setObjectName(
"SettingSubtitle"
)
bar = QProgressBar()
bar.setRange(0, 100)
bar.setValue(device[key])
bar.setTextVisible(False)
bar.setFixedHeight(8)
low = device[key] < 20
bar.setStyleSheet(
"QProgressBar {"
" background-color: #e5e7eb;"
" border: none;"
" border-radius: 4px;"
"}"
"QProgressBar::chunk {"
f" background-color: "
f"{'#dc2626' if low else '#181717'};"
" border-radius: 4px;"
"}"
)
value_label = QLabel(
f"{device[key]}%"
)
value_label.setObjectName(
"SettingState"
)
meter_layout.addWidget(
caption_label
)
meter_layout.addWidget(bar, 1)
meter_layout.addWidget(
value_label
)
meters.addLayout(meter_layout, 1)
layout.addLayout(meters)
self.refresh()
def refresh(self):
status = self.device["status"]
self.status_chip.setText(status)
self.status_chip.setStyleSheet(
chip_style(status)
)
self.name_label.setText(
self.device["name"]
)
self.details_label.setText(
f"{self.device['type']} • "
f"{self.device['dev_id']} • "
f"{self.device['version']}"
)
self.paired_label.setText(
"Paired"
if self.device["paired"]
else "Unpaired"
)
show_note = (
status == "Faulty"
and bool(self.device["note"])
)
self.note_banner.setVisible(
show_note
)
if show_note:
self.note_banner.setText(
self.device["note"]
)
def open_manage_dialog(self):
dialog = ManageDeviceDialog(
self.device,
STATUSES,
DEVICE_TYPES,
parent=self
)
saved = dialog.exec()
if dialog.deleted:
if self.on_delete:
self.on_delete(self)
return
if saved:
self.device["status"] = (
dialog.result_data["status"]
)
self.device["paired"] = (
dialog.result_data["paired"]
)
# Edit Details modifica direct device-ul,
# deci cardul trebuie reimprospatat chiar
# daca la final ai dat Cancel.
if saved or dialog.details_changed:
self.refresh()
if self.on_change:
self.on_change()
ToastManager.show(
self.window(),
"Device updated",
variant="success",
sub_message=self.device["name"]
)
class DeviceStationManagementPage(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
layout.setContentsMargins(
20, 20, 20, 20
)
layout.setSpacing(16)
#
# Subtitlu + actiuni globale
#
top_layout = QHBoxLayout()
top_layout.setSpacing(12)
top_layout.addWidget(
PageSubtitle(
"Monitor and control all XR "
"devices and stations."
)
)
top_layout.addStretch()
scan_btn = Button(
"Scan Network",
variant="secondary",
icon="fa5s.sync"
)
scan_btn.clicked.connect(
self.scan_network
)
push_btn = Button(
"Push Update",
variant="secondary",
icon="fa5s.upload"
)
push_btn.clicked.connect(
self.push_update
)
add_btn = Button(
"Add Device",
variant="primary",
icon="fa5s.plus"
)
add_btn.clicked.connect(
self.add_device
)
top_layout.addWidget(scan_btn)
top_layout.addWidget(push_btn)
top_layout.addWidget(add_btn)
layout.addLayout(top_layout)
#
# Statistici
#
stats_layout = QHBoxLayout()
stats_layout.setSpacing(12)
self.stat_labels = {}
stats = [
("total", "Total Devices",
"fa5s.desktop"),
("online", "Online",
"fa5s.wifi"),
("offline", "Offline",
"fa5s.power-off"),
("in_use", "In Use",
"fa5s.play-circle"),
("maintenance", "Maintenance",
"fa5s.tools"),
("faulty", "Faulty",
"fa5s.exclamation-triangle"),
]
for key, text, icon in stats:
card = StatCard(
"0",
text,
icon
)
card.setFixedHeight(100)
self.stat_labels[key] = (
card.findChild(
QLabel,
"StatNumber"
)
)
stats_layout.addWidget(card)
layout.addLayout(stats_layout)
#
# Grid de device-uri (scrollabil)
#
scroll_content = QWidget()
content_layout = QVBoxLayout(
scroll_content
)
content_layout.setContentsMargins(
0, 0, 0, 0
)
self.grid = QGridLayout()
self.grid.setSpacing(16)
content_layout.addLayout(self.grid)
content_layout.addStretch()
self.device_cards = []
for device in DEVICES:
self._add_card(device)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(
QScrollArea.NoFrame
)
scroll_area.setHorizontalScrollBarPolicy(
Qt.ScrollBarAlwaysOff
)
scroll_area.setStyleSheet(
"QScrollArea { background: transparent; }"
"QScrollArea > QWidget > QWidget "
"{ background: transparent; }"
)
scroll_area.setWidget(scroll_content)
layout.addWidget(scroll_area)
self.update_stats()
def _add_card(self, device):
card = DeviceCard(
device,
on_change=self.update_stats,
on_delete=self.remove_device
)
index = len(self.device_cards)
self.grid.addWidget(
card,
index // 2,
index % 2
)
self.device_cards.append(card)
def remove_device(self, card):
device = card.device
if device in DEVICES:
DEVICES.remove(device)
self.device_cards.remove(card)
self.grid.removeWidget(card)
card.setParent(None)
card.deleteLater()
# Pozitiile din grila sunt pe index,
# deci dupa stergere trebuie rearanjate.
for index, remaining in enumerate(
self.device_cards
):
self.grid.addWidget(
remaining,
index // 2,
index % 2
)
remaining.setVisible(True)
self.update_stats()
ToastManager.show(
self.window(),
"Device deleted",
variant="info",
sub_message=(
f"{device['name']} "
f"({device['dev_id']})"
)
)
#
# Actiuni globale
#
def add_device(self):
dialog = AddDeviceDialog(
DEVICE_TYPES,
parent=self
)
if dialog.exec():
device = dialog.device_data
DEVICES.append(device)
self._add_card(device)
self.update_stats()
ToastManager.show(
self.window(),
"Device added",
variant="success",
sub_message=(
f"{device['name']} "
f"({device['dev_id']})"
)
)
def scan_network(self):
ToastManager.show(
self.window(),
"Network scan complete",
variant="info",
sub_message=(
f"{len(self.device_cards)} "
f"devices found (mock)"
)
)
def push_update(self):
online = sum(
1 for card in self.device_cards
if card.device["status"]
in ("Online", "In Use")
)
modal = Modal(
title="Push Update",
message=(
f"Push firmware update to "
f"{online} connected devices?"
),
confirm_text="Push",
cancel_text="Cancel",
parent=self
)
if modal.exec():
ToastManager.show(
self.window(),
"Update queued",
variant="success",
sub_message=(
f"Firmware update sent to "
f"{online} devices (mock)"
)
)
#
# Statistici
#
def update_stats(self):
statuses = [
card.device["status"]
for card in self.device_cards
]
values = {
"total": len(self.device_cards),
"online": statuses.count("Online"),
"offline": statuses.count("Offline"),
"in_use": statuses.count("In Use"),
"maintenance": statuses.count(
"Maintenance"
),
"faulty": statuses.count("Faulty"),
}
for key, value in values.items():
self.stat_labels[key].setText(
str(value)
)
add device
from view.admin.dialogs.form_modal import (
FormModal
)
from view.shared.components.inputs import (
TextInput,
Dropdown
)
class AddDeviceDialog(FormModal):
"""
Inregistrarea unui device nou.
Tipurile disponibile vin ca parametru de la
pagina (asa constantele stau intr-un singur
loc, fara import circular).
Construieste device_data la Save.
"""
def __init__(
self,
device_types,
parent=None
):
super().__init__(
title="Add Device",
parent=parent
)
self.name_input = TextInput(
"Device Name"
)
self.type_combo = Dropdown(
device_types
)
self.dev_id_input = TextInput(
"Device ID (ex: DEV-007)"
)
self.version_input = TextInput(
"Firmware Version (ex: v3.2.1)"
)
self.add_field(self.name_input)
self.add_field(self.type_combo)
self.add_field(self.dev_id_input)
self.add_field(self.version_input)
def validate(self):
return (
self.require(
self.name_input.text(),
"Device name"
)
or self.require(
self.dev_id_input.text(),
"Device ID"
)
or self.require(
self.version_input.text(),
"Firmware version"
)
)
def accept(self):
self.device_data = {
"name":
self.name_input.text(),
"type":
self.type_combo.currentText(),
"dev_id":
self.dev_id_input.text(),
"version":
self.version_input.text(),
# Un device nou intra nepairuit si
# offline pana e detectat in retea.
"status": "Offline",
"paired": False,
"battery": 100,
"network": 0,
"note": None,
}
super().accept()
edit device
from view.admin.dialogs.form_modal import (
FormModal
)
from view.shared.components.inputs import (
TextInput,
Dropdown
)
class EditDeviceDialog(FormModal):
"""
Datele de identitate ale device-ului:
nume, tip, ID si versiune de firmware.
(Starea operationala — status, pairing,
reboot etc. — se schimba din
ManageDeviceDialog.)
Construieste device_data la Save.
"""
def __init__(
self,
device,
device_types,
parent=None
):
super().__init__(
title="Edit Device",
confirm_text="Save Changes",
parent=parent
)
self.name_input = TextInput(
"Device Name"
)
self.name_input.setText(
device["name"]
)
self.type_combo = Dropdown(
device_types
)
self.type_combo.setCurrentText(
device["type"]
)
self.dev_id_input = TextInput(
"Device ID"
)
self.dev_id_input.setText(
device["dev_id"]
)
self.version_input = TextInput(
"Firmware Version"
)
self.version_input.setText(
device["version"]
)
self.add_field(self.name_input)
self.add_field(self.type_combo)
self.add_field(self.dev_id_input)
self.add_field(self.version_input)
def validate(self):
return (
self.require(
self.name_input.text(),
"Device name"
)
or self.require(
self.dev_id_input.text(),
"Device ID"
)
or self.require(
self.version_input.text(),
"Firmware version"
)
)
def accept(self):
self.device_data = {
"name":
self.name_input.text(),
"type":
self.type_combo.currentText(),
"dev_id":
self.dev_id_input.text(),
"version":
self.version_input.text(),
}
super().accept()