IKHSAN
ENID Baca dalam Bahasa Indonesia

opendesk

Public
Branch
main
Latest commit
08d9f3328 October 2026

Agents.vue

Open raw
<template>
  <SettingsLayoutBase
    :title="__('Agents')"
    :description="__('Add, manage agents and assign roles to them.')"
  >
    <template #header-actions>
      <Button
        @click="() => setActiveSettingsTab('Invite Agents')"
        label="New"
        variant="solid"
        class="rtl:flex-row-reverse"
      >
        <template #prefix>
          <LucidePlus class="h-4 w-4 stroke-1.5" />
        </template>
      </Button>
    </template>
    <template #header-bottom>
      <div class="flex items-center gap-2 justify-between">
        <div class="relative grow">
          <TextInput
            :model-value="search"
            @update:model-value="search = $event"
            :placeholder="__('Search')"
            type="text"
            class="focus:ring-0 border-outline-gray-2"
            :debounce="300"
          >
            <template #prefix>
              <LucideSearch class="size-4" />
            </template>
          </TextInput>
          <Button
            v-if="search"
            icon="lucide-x"
            variant="ghost"
            @click="search = ''"
            class="absolute end-1 top-1/2 -translate-y-1/2"
          />
        </div>
        <Dropdown :options="dropdownOptions" align="end">
          <template #default="{ open }">
            <Button :label="activeFilter">
              <template #suffix>
                <component
                  :is="open ? LucideChevronUp : LucideChevronDown"
                  class="h-4"
                />
              </template>
            </Button>
          </template>
          <template #item-label="{ item }">
            <button
              class="group flex text-ink-gray-6 gap-4 w-full justify-between items-center rounded-4 text-base"
              @click="item.onClick"
            >
              <div class="flex items-center justify-between flex-1">
                <span class="whitespace-nowrap">
                  {{ item.label }}
                </span>
                <LucideCheck
                  v-if="activeFilter === item.label"
                  class="size-4 text-ink-gray-7"
                />
              </div>
            </button>
          </template>
        </Dropdown>
      </div>
    </template>
    <template #content>
      <div class="grow">
        <!-- loading state -->
        <div
          v-if="agents.loading"
          class="flex mt-28 justify-between w-full h-full"
        >
          <Button
            :loading="agents.loading"
            variant="ghost"
            class="w-full"
            size="lg"
          />
        </div>
        <!-- Empty State -->
        <EmptyState
          v-if="!agents.loading && !agents.data?.length"
          variant="badge"
          :icon="AgentIcon"
          title="No agent found"
          :description="
            activeFilter.length
              ? 'Try a different search or filter.'
              : 'Add one to get started.'
          "
        />
        <!-- Agent List -->
        <div
          class="w-full"
          v-if="!agents.loading && Boolean(agents.data?.length)"
        >
          <div
            class="grid grid-cols-8 items-center gap-3 text-sm text-ink-gray-5"
          >
            <div class="col-span-6 text-p-sm">{{ __("Agent name") }}</div>
          </div>
          <hr class="mt-2" />
          <div v-for="(agent, index) in agents.data" :key="agent.agent_name">
            <div class="flex items-center justify-between h-14 group rounded-4">
              <div class="flex items-center gap-x-3 grow">
                <Avatar
                  :image="agent.user_image"
                  :label="agent.agent_name"
                  size="xl"
                />
                <div>
                  <div class="flex items-center gap-2">
                    <p class="text-base">
                      {{ agent.agent_name }}
                    </p>
                    <Badge
                      :label="__('Inactive')"
                      :theme="'gray'"
                      :class="!agent.is_active ? 'opacity-100' : 'opacity-0'"
                      variant="subtle"
                    />
                  </div>
                  <div class="text-base text-ink-gray-6 mt-1">
                    {{ agent.name }}
                  </div>
                </div>
              </div>
              <div class="flex items-center gap-2">
                <Dropdown
                  v-if="isManager"
                  class="flex justify-end items-center"
                  :options="getRoles(agent.name)"
                  :label="getUserRole(agent.name)"
                  :button="{
                    label: getUserRole(agent.name),
                    iconRight: 'lucide-chevron-down',
                    iconLeft:
                      getUserRole(agent.name) === 'Agent'
                        ? 'lucide-user'
                        : getUserRole(agent.name) === 'Manager'
                        ? 'lucide-briefcase'
                        : null,
                  }"
                  align="end"
                />
                <Dropdown :options="getOptions(agent)" :key="agent" align="end">
                  <Button icon="lucide-more-horizontal" variant="ghost" />
                </Dropdown>
              </div>
            </div>
            <hr v-if="index !== agents.data.length - 1" />
          </div>
          <!-- Load More Button -->
          <div class="flex justify-center">
            <Button
              v-if="!agents.loading && agents.hasNextPage"
              class="mt-3.5 p-2"
              @click="() => agents.next()"
              :loading="agents.loading"
              :label="__('Load More')"
              icon-left="lucide-refresh-cw"
            />
          </div>
        </div>
      </div>
    </template>
  </SettingsLayoutBase>
</template>

<script setup lang="ts">
import LucideChevronUp from "~icons/lucide/chevron-up";
import LucideChevronDown from "~icons/lucide/chevron-down";
import LucideCheck from "~icons/lucide/check";
import EmptyState from "@/components/EmptyState.vue";
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
import { useAuthStore } from "@/stores/auth";
import { useUserStore } from "@/stores/user";
import { __ } from "@/translation";
import { Avatar, Button, call, Dropdown, toast } from "frappe-ui";
import { h, onUnmounted } from "vue";
import AgentIcon from "../icons/AgentIcon.vue";
import { activeFilter, useAgents } from "./agents";
import { setActiveSettingsTab } from "./settingsModal";

const { getUserRole, updateUserRoleCache } = useUserStore();
const { isManager } = useAuthStore();

const agentStore = useAgents();
const search = agentStore.search;
const agents = agentStore.agents;

function getRoles(agent: string) {
  const agentRole = getUserRole(agent);
  const role = (label: string, icon: string) => ({
    label,
    icon,
    selected: agentRole === label,
    onClick: () => updateRole(agent, label),
  });
  const roles = [role("Agent", "lucide-user")];
  if (isManager) {
    roles.unshift(role("Manager", "lucide-briefcase"));
  }

  return roles;
}

function updateRole(agent: string, newRole: string) {
  const currentRole = getUserRole(agent);
  if (currentRole === newRole) {
    return;
  }

  call("opendesk.opendesk.doctype.od_agent.od_agent.update_agent_role", {
    user: agent,
    new_role: newRole,
  }).then(() => {
    updateUserRoleCache(agent, newRole);
    toast.success(__(`Role updated to ${newRole} successfully.`));
  });
}

function getOptions(agent) {
  let filters = agentStore.filters;
  return [
    {
      label: "Disable Agent",
      icon: "lucide-x-circle",
      onClick: async () => {
        await agentStore.updateAgent(agent.name, 0);
        agents.reload({ ...filters, search: search.value });
      },
      condition: () => agent.is_active,
    },
    {
      label: "Enable Agent",
      icon: "lucide-check-circle",
      onClick: async () => {
        await agentStore.updateAgent(agent.name, 1);
        agents.reload({ ...filters, search: search.value });
      },
      condition: () => !agent.is_active,
    },
  ];
}

const dropdownOptions = [
  {
    label: "All",
    onClick: () => {
      agentStore.filters["is_active"] = ["in", [0, 1]];
      activeFilter.value = "All";
    },
  },
  {
    label: "Active",
    onClick: () => {
      agentStore.filters["is_active"] = ["=", 1];
      activeFilter.value = "Active";
    },
  },
  {
    label: "Inactive",
    onClick: () => {
      agentStore.filters["is_active"] = ["=", 0];
      activeFilter.value = "Inactive";
    },
  },
];

onUnmounted(() => {
  agents.filters = {};
});
</script>
bytes
8,699
lines
280
Type
vue
Latest commit
28 October 2026