Newer
Older
smart-home-server / webclient-vue / src / features / areas / pages / AreaDetailPage.vue
@Eugene Sukhodolskiy Eugene Sukhodolskiy 23 hours ago 7 KB Add script detail pages with scope grouping
<template>
  <section class="page">
    <AppLoadingState v-if="areasStore.isLoading || areasStore.isLoadingAreaDetail" text="Loading area" />

    <AppErrorState
      v-else-if="areasStore.error && !areasStore.areas.length"
      title="Areas loading failed"
      :message="areasStore.error.message"
      :retry="init"
    />

    <AppErrorState
      v-else-if="areasStore.errorAreaDetail"
      title="Area loading failed"
      :message="areasStore.errorAreaDetail.message"
      :retry="init"
    />

    <div v-else-if="area">
      <GnPageHeader :title="area.display_name" kicker="Area">
        <template #actions>
          <GnButton variant="secondary" icon="ph-pencil" @click="openRename">Rename</GnButton>
          <GnButton
            variant="secondary"
            icon="ph-arrow-up"
            :disabled="area.parent_id == null || area.parent_id === 0"
            @click="unassign"
          >
            Unassign
          </GnButton>
          <GnButton
            :variant="isFavorite ? 'warning' : 'secondary'"
            :icon="isFavorite ? 'ph-star' : 'ph-star'"
            @click="favoritesStore.toggle(area.id)"
          >
            {{ isFavorite ? 'Unstar' : 'Star' }}
          </GnButton>
          <GnButton variant="danger" icon="ph-trash" @click="openRemove">Remove</GnButton>
        </template>
      </GnPageHeader>

      <div class="area-meta">
        <GnBadge variant="secondary">{{ area.type }}</GnBadge>
        <code>{{ area.alias }}</code>
        <span v-if="area.parent_id" class="area-parent">Parent: {{ parentArea?.display_name || area.parent_id }}</span>
      </div>

      <div class="devices-panel">
        <div class="block-title">Devices ({{ areasStore.currentAreaDevices.length }})</div>
        <AppEmptyState
          v-if="areasStore.currentAreaDevices.length === 0"
          title="No devices"
          message="No devices assigned to this area."
        />
        <GnTable v-else :rows="areasStore.currentAreaDevices" :columns="deviceColumns">
          <template #cell-actions="{ row }">
            <router-link :to="{ name: 'devices' }" class="area-link">View device</router-link>
          </template>
        </GnTable>
      </div>

      <div class="devices-panel">
        <div class="block-title">Scripts ({{ areasStore.currentAreaScripts.length }})</div>
        <AppEmptyState
          v-if="areasStore.currentAreaScripts.length === 0"
          title="No scripts"
          message="No scripts assigned to this area."
        />
        <GnTable v-else :rows="areasStore.currentAreaScripts" :columns="scriptColumns">
          <template #cell-actions="{ row }">
            <router-link :to="{ name: 'scripts-actions' }" class="area-link">View script</router-link>
          </template>
        </GnTable>
      </div>
    </div>

    <AppEmptyState
      v-else
      title="Area not found"
      message="The requested area does not exist."
    />

    <GnModal :open="showRenameModal" title="Rename area" @update:open="showRenameModal = $event">
      <div class="form-group">
        <GnInput v-model="renameForm.display_name" label="Display name" />
      </div>
      <div v-if="renameError" class="form-group">
        <GnAlert variant="danger">{{ renameError }}</GnAlert>
      </div>
      <template #footer>
        <GnButton variant="secondary" @click="showRenameModal = false">Cancel</GnButton>
        <GnButton variant="primary" icon="ph-check" :loading="renameLoading" @click="submitRename">Rename</GnButton>
      </template>
    </GnModal>

    <GnConfirmDialog
      :open="showRemoveDialog"
      title="Remove area"
      :message="removeDialogMessage"
      confirm-text="Remove"
      cancel-text="Cancel"
      confirm-variant="danger"
      @update:open="showRemoveDialog = $event"
      @confirm="submitRemove"
    />
  </section>
</template>

<script setup>
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useAreasStore } from "../../../stores/areas";
import { useFavoritesStore } from "../../../stores/favorites";
import {
  GnPageHeader,
  GnButton,
  GnBadge,
  GnModal,
  GnInput,
  GnAlert,
  GnTable,
  GnConfirmDialog,
} from "gnexus-ui-kit/vue";
import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";
import AppErrorState from "../../../components/feedback/AppErrorState.vue";
import AppEmptyState from "../../../components/feedback/AppEmptyState.vue";

const route = useRoute();
const router = useRouter();
const areasStore = useAreasStore();
const favoritesStore = useFavoritesStore();

const area = computed(() => areasStore.areasById[String(route.params.id)] || null);
const isFavorite = computed(() => (area.value ? favoritesStore.has(area.value.id) : false));
const parentArea = computed(() => {
  if (!area.value?.parent_id) return null;
  return areasStore.areasById[String(area.value.parent_id)] || null;
});

const showRenameModal = ref(false);
const renameLoading = ref(false);
const renameError = ref("");
const renameForm = reactive({ areaId: null, display_name: "" });

const showRemoveDialog = ref(false);
const removeDialogMessage = ref("");

const deviceColumns = [
  { key: "alias", label: "Alias" },
  { key: "name", label: "Name" },
  { key: "device_type", label: "Type" },
];

const scriptColumns = [
  { key: "alias", label: "Alias" },
  { key: "name", label: "Name" },
  { key: "state", label: "State" },
];

function openRename() {
  if (!area.value) return;
  renameForm.areaId = area.value.id;
  renameForm.display_name = area.value.display_name;
  renameError.value = "";
  showRenameModal.value = true;
}

async function submitRename() {
  renameLoading.value = true;
  renameError.value = "";

  const result = await areasStore.renameArea(renameForm.areaId, renameForm.display_name);
  renameLoading.value = false;

  if (!result.ok) {
    renameError.value = result.error?.message || "Failed to rename area";
    return;
  }

  showRenameModal.value = false;
}

function openRemove() {
  if (!area.value) return;
  removeDialogMessage.value = `Are you sure you want to remove area "${area.value.display_name}"?`;
  showRemoveDialog.value = true;
}

async function submitRemove() {
  if (!area.value) return;
  await areasStore.removeArea(area.value.id);
  router.push({ name: "areas-tree" });
}

async function unassign() {
  if (!area.value) return;
  await areasStore.unassignArea(area.value.id);
}

async function init() {
  const id = route.params.id;
  if (!id) return;

  if (areasStore.areas.length === 0) {
    await areasStore.loadAreas();
  }

  if (areasStore.areasById[String(id)]) {
    await areasStore.loadAreaDetail(id);
  }
}

onMounted(() => {
  init();
});

onUnmounted(() => {
  areasStore.clearAreaDetail();
});
</script>

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

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

.devices-panel {
  margin-bottom: 24px;
}

.block-title {
  font-weight: 700;
  text-transform: uppercase;
  margin-bottom: 12px;
  color: var(--color-primary);
}

.form-group {
  margin-bottom: 16px;
}

.area-link {
  color: var(--color-primary);
}
</style>