Newer
Older
smart-home-server / webclient-vue / src / features / devices / pages / DevicesScanningPage.vue
<template>
  <section class="page">
    <GnPageHeader title="Scanning" kicker="Devices">
      <template #actions>
        <GnButton
          :loading="scanningStore.isLoading"
          icon="ph-magnifying-glass"
          @click="scan"
        >
          Scan
        </GnButton>
      </template>
    </GnPageHeader>

    <div class="scan-filters">
      <span class="filter-label">Mode:</span>
      <GnChip
        :selected="scanningStore.mode === 'setup'"
        clickable
        icon="ph-plug"
        @click="setMode('setup')"
      >
        Setup
      </GnChip>
      <GnChip
        :selected="scanningStore.mode === 'all'"
        clickable
        icon="ph-scan"
        @click="setMode('all')"
      >
        All
      </GnChip>
    </div>

    <AppLoadingState v-if="scanningStore.isLoading" text="Scanning network" />

    <AppErrorState
      v-else-if="scanningStore.error"
      title="Scan failed"
      :error="scanningStore.error"
      :retry="scan"
    />

    <AppEmptyState
      v-else-if="scanningStore.devices.length === 0"
      title="No devices found"
      message="Choose scan mode and click Scan to discover devices."
    />

    <div v-else class="devices-panel">
      <div class="devices-summary">
        <GnBadge variant="primary">{{ scanningStore.total }} found</GnBadge>
      </div>

      <GnTable
        :columns="tableColumns"
        :rows="scanningStore.devices"
        caption="Discovered devices"
      >
        <template #cell-device="{ row }">
          <div class="device-cell">
            <div class="device-icon" v-html="typeIcon(row.device_type)" />
            <div class="device-info">
              <strong>{{ row.device_name || 'Unknown' }}</strong>
              <small>{{ row.device_type || 'unknown' }} — {{ row.ip_address || '—' }}</small>
            </div>
          </div>
        </template>

        <template #cell-status="{ row }">
          <GnBadge :variant="row.status === 'setup' ? 'warning' : 'success'">{{ row.status || 'unknown' }}</GnBadge>
        </template>

        <template #cell-firmware="{ row }">
          <span class="firmware">{{ row.firmware_version || '—' }}</span>
        </template>

        <template #cell-actions="{ row }">
          <GnButton
            v-if="row.status === 'setup'"
            variant="primary"
            icon="ph-plus"
            size="sm"
            @click="openSetup(row)"
          >
            Add
          </GnButton>
          <span v-else class="muted">—</span>
        </template>
      </GnTable>
    </div>

    <GnModal
      :open="showSetupModal"
      title="Setup new device"
      @update:open="showSetupModal = $event"
    >
      <div class="form-group">
        <GnInput v-model="setupForm.alias" label="Alias" placeholder="kitchen_relay" />
      </div>
      <div class="form-group">
        <GnInput v-model="setupForm.name" label="Name" placeholder="Kitchen Relay" />
      </div>
      <div class="form-group">
        <GnInput v-model="setupForm.description" label="Description" />
      </div>
      <div v-if="setupError" class="form-group">
        <GnAlert variant="danger">{{ setupError }}</GnAlert>
      </div>
      <template #footer>
        <GnButton variant="secondary" @click="showSetupModal = false">Cancel</GnButton>
        <GnButton variant="primary" icon="ph-plus" :loading="setupLoading" @click="submitSetup">
          Add device
        </GnButton>
      </template>
    </GnModal>
  </section>
</template>

<script setup>
import { ref, reactive } from "vue";
import { useScanningStore } from "../../../stores/scanning";
import {
  GnPageHeader,
  GnButton,
  GnBadge,
  GnChip,
  GnTable,
  GnModal,
  GnInput,
  GnAlert,
  useToast,
} from "gnexus-ui-kit/vue";
import AppEmptyState from "../../../components/feedback/AppEmptyState.vue";
import AppErrorState from "../../../components/feedback/AppErrorState.vue";
import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";

const scanningStore = useScanningStore();
const toast = useToast();

const showSetupModal = ref(false);
const setupLoading = ref(false);
const setupError = ref("");
const setupForm = reactive({
  device_ip: "",
  alias: "",
  name: "",
  description: "",
});

const TYPE_ICONS = {
  relay: '<i class="ph ph-toggle-left"></i>',
  button: '<i class="ph ph-hand-tap"></i>',
  sensor: '<i class="ph ph-thermometer"></i>',
};

const tableColumns = [
  { key: "device", label: "Device" },
  { key: "status", label: "Status" },
  { key: "firmware", label: "Firmware" },
  { key: "actions", label: "Actions" },
];

function typeIcon(type) {
  return TYPE_ICONS[type] || TYPE_ICONS.relay;
}

function setMode(mode) {
  scanningStore.setMode(mode);
}

function scan() {
  scanningStore.scan();
}

function openSetup(row) {
  setupForm.device_ip = row.ip_address;
  setupForm.alias = "";
  setupForm.name = "";
  setupForm.description = "";
  setupError.value = "";
  showSetupModal.value = true;
}

async function submitSetup() {
  setupLoading.value = true;
  setupError.value = "";

  const result = await scanningStore.setupDevice({ ...setupForm });
  setupLoading.value = false;

  if (!result.ok) {
    setupError.value = result.error?.message || "Failed to setup device";
    return;
  }

  showSetupModal.value = false;
  toast.success({ title: "Device added", text: `Device ${setupForm.alias || setupForm.name || ""} added successfully` });
}
</script>

<style scoped>
.scan-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 24px;
}

.filter-label {
  color: var(--color-muted);
  font-size: 13px;
  text-transform: uppercase;
}

.devices-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 12px;
}

.device-cell {
  display: flex;
  align-items: center;
  gap: 10px;
}

.device-icon {
  font-size: 20px;
  color: var(--color-primary);
}

.device-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.device-info small {
  color: var(--color-muted);
  font-size: 12px;
}

.firmware {
  font-size: 12px;
  color: var(--color-muted);
}

.muted {
  color: var(--color-muted);
}

.form-group {
  margin-bottom: 16px;
}
</style>