refactor: seed agent availability from session

This commit is contained in:
Sydney
2026-06-26 11:43:00 +05:30
parent 9b15a0289d
commit 6e53b2fe30
5 changed files with 90 additions and 76 deletions
-32
View File
@@ -1,32 +0,0 @@
import { createResource, toast } from "frappe-ui";
import { computed } from "vue";
import { __ } from "@/translation";
import { useAuthStore } from "@/stores/auth";
const availability = createResource({
url: "helpdesk.api.agent.get_my_availability",
});
const setAvailability = createResource({
url: "helpdesk.api.agent.set_my_availability",
onSuccess: () => { availability.reload();
toast.success(__("Status updated successfully."));
},
});
export function useAvailability() {
const authStore = useAuthStore();
if (authStore.isAgent && !availability.fetched) {
availability.fetch();
}
const currentStatus = computed(() => availability.data?.availability || "");
const options = computed(() => availability.data?.options || []);
function setStatus(status: string) {
if (status === currentStatus.value) return;
setAvailability.submit({ availability: status });
}
return { currentStatus, options, setStatus };
}
+68 -11
View File
@@ -1,8 +1,10 @@
import { useAuthStore } from "@/stores/auth";
import { globalStore } from "@/stores/globalStore";
import { __ } from "@/translation";
import { HDAgentStatus } from "@/types/doctypes";
import { createListResource } from "frappe-ui";
import { createListResource, createResource, toast } from "frappe-ui";
import { defineStore } from "pinia";
import { reactive } from "vue";
import { computed, reactive, watch } from "vue";
interface LiveAvailability {
availability: string;
@@ -35,6 +37,9 @@ const dotColorMap: Record<string, string> = {
const defaultColor = "bg-surface-gray-5";
export const useAgentStatusStore = defineStore("agentStatus", () => {
// Set only for sessions that have an HD Agent record; null otherwise.
const myAgentName = window.agent;
const statuses = createListResource({
doctype: "HD Agent Status",
cache: ["HD Agent Status", "list"],
@@ -44,19 +49,63 @@ export const useAgentStatusStore = defineStore("agentStatus", () => {
auto: true,
});
// Live availability pushed over the socket when an agent changes their
// status (helpdesk.api.agent.set_my_availability). Keyed by HD Agent name,
// it overrides the value fetched when a list first loaded.
// Live availability keyed by HD Agent name. Seeded by fetches and kept current
// over the socket — set_my_availability broadcasts to every client, so this is
// the single source of truth for "who is what right now", including ourselves.
const liveStatuses = reactive<Record<string, LiveAvailability>>({});
function applyLive(agent: string, availability?: string, changedOn?: string) {
if (!agent || !availability) return;
liveStatuses[agent] = { availability, changedOn: changedOn ?? "" };
}
const { $socket } = globalStore();
$socket.on("agent_availability_updated", (data: AvailabilityEvent) => {
liveStatuses[data.agent] = {
availability: data.availability,
changedOn: data.availability_changed_on,
};
$socket.on("agent_availability_updated", (data: AvailabilityEvent) =>
applyLive(data.agent, data.availability, data.availability_changed_on)
);
// Seed our own status from the session payload (auth.get_user already resolves
// the agent), then let the socket and the optimistic write keep it current — no
// dedicated fetch and no reload.
const auth = useAuthStore();
watch(
() => auth.availability,
(availability) =>
applyLive(myAgentName ?? "", availability, auth.availabilityChangedOn),
{ immediate: true }
);
const setMyAvailability = createResource({
url: "helpdesk.api.agent.set_my_availability",
onSuccess: () => toast.success(__("Status updated successfully.")),
onError: () => toast.error(__("Could not update status.")),
});
const myStatus = computed(
() => (myAgentName && liveStatuses[myAgentName]?.availability) || ""
);
// Selectable statuses, derived from the same list the presence dots use so the
// menu and the dots never disagree and we avoid a second server round-trip.
const statusOptions = computed<string[]>(() =>
(statuses.data ?? [])
.filter((s: HDAgentStatus) => s.enable)
.map((s: HDAgentStatus) => s.agent_status)
);
function setMyStatus(status: string) {
if (!myAgentName || !status || status === myStatus.value) return;
// Optimistic: update immediately, then roll back if the server rejects it.
// changedOn is left empty — it is never rendered for our own dot and the
// socket echo overwrites it with the authoritative server timestamp.
const previous = liveStatuses[myAgentName];
applyLive(myAgentName, status);
setMyAvailability.submit({ availability: status }).catch(() => {
if (previous) liveStatuses[myAgentName] = previous;
else delete liveStatuses[myAgentName];
});
}
function getStatus(name: string): HDAgentStatus | undefined {
return statuses.data?.find((s: HDAgentStatus) => s.agent_status === name);
}
@@ -66,5 +115,13 @@ export const useAgentStatusStore = defineStore("agentStatus", () => {
return (color && dotColorMap[color]) || defaultColor;
}
return { statuses, liveStatuses, getStatus, statusColor };
return {
statuses,
liveStatuses,
myStatus,
statusOptions,
setMyStatus,
getStatus,
statusColor,
};
});
+8
View File
@@ -44,6 +44,12 @@ export const useAuthStore = defineStore("auth", () => {
);
const userName: ComputedRef<string> = computed(() => user__.value.user_name);
const username: ComputedRef<string> = computed(() => user__.value.username);
const availability: ComputedRef<string> = computed(
() => user__.value.availability || ""
);
const availabilityChangedOn: ComputedRef<string> = computed(
() => user__.value.availability_changed_on || ""
);
const timezone: ComputedRef<string> = computed(() => user__.value.time_zone);
const userTeams: ComputedRef<string[]> = computed(
() => user__.value.user_teams
@@ -94,6 +100,8 @@ export const useAuthStore = defineStore("auth", () => {
userImage,
userName,
username,
availability,
availabilityChangedOn,
timezone,
userTeams,
language,
-32
View File
@@ -30,38 +30,6 @@ def sent_invites(emails: list[str], send_welcome_mail_to_user: bool = True):
return
@frappe.whitelist()
@agent_only
def get_availability_options() -> list[str]:
return frappe.get_all(
"HD Agent Status",
filters={"enable": 1},
order_by="`tabHD Agent Status`.status_order asc",
pluck="agent_status",
)
@frappe.whitelist()
@agent_only
def get_my_availability() -> dict:
name = get_agent_name()
values = (
frappe.db.get_value(
"HD Agent",
name,
["availability", "availability_changed_on"],
as_dict=True,
)
if name
else {}
) or {}
return {
"availability": values.get("availability"),
"availability_changed_on": values.get("availability_changed_on"),
"options": get_availability_options(),
}
@frappe.whitelist()
@agent_only
def set_my_availability(availability: str) -> dict:
+14 -1
View File
@@ -28,8 +28,19 @@ def get_user():
is_admin = "System Manager" in roles or "Administrator" in roles
is_agent = _is_agent()
has_agent_record = bool(get_agent_name())
agent_name = get_agent_name()
has_agent_record = bool(agent_name)
has_desk_access = is_agent or is_admin
availability = (
frappe.db.get_value(
"HD Agent",
agent_name,
["availability", "availability_changed_on"],
as_dict=True,
)
if agent_name
else None
) or {}
user_image = user.user_image
user_first_name = user.first_name
user_name = user.full_name
@@ -56,6 +67,8 @@ def get_user():
"time_zone": user.time_zone,
"user_teams": user_team_names,
"language": language,
"availability": availability.get("availability"),
"availability_changed_on": availability.get("availability_changed_on"),
}