Skip to contentVibraUI
Dashboards

People actions

The People dashboard's shared server actions: approving or declining a leave request, imported by every People page that offers it.

A "use server" module that exports async functions and types only, installed under the consumer's lib alias beside the dashboard's nav, where the shadcn CLI rewrites a page's @/registry/vibra/lib/dashboards/people/actions import to. The overview's leave queue and the leave page both decide a request, so the decision lives here once: every page that imports it lists actions-people in its registry dependencies, and so the people dashboard item, whose registry dependencies are the union of its pages', installs it with the rest. The employee is read off the row, never the form.

Install

npx shadcn@latest add @vibra/actions-people

Needs the @vibra registry in your components.json — set it up once.

Props

PropTypeDefaultDescription
decideLeave(id: string, decision: "approved" | "declined") => Promise<Result<DecidedLeave>>—Grants or refuses a pending request; refuses one already decided with the decision it got (conflict), any other word (invalid) and an unknown id (not_found).
DecidedLeave{ id; status: "approved" | "declined"; employeeName; kind }—What a decision answers: the request, the decision it took, whose it is and what kind of leave.

Dependencies

Source

lib/dashboards/people/actions.ts
"use server"

import { db, type Leave, type Result } from "@/lib/sample-data"

/**
 * The People dashboard's shared server actions: what more than one of its
 * pages does to the same rows. The overview's leave queue and the leave page
 * both decide a request, so the decision lives here, once, and both import
 * it — one action name, one behaviour, everywhere the dashboard offers it.
 *
 * Installed as `@lib/dashboards/people/actions.ts` by the `actions-people`
 * item, which every page that imports it lists in its registry dependencies,
 * so installing the dashboard installs it too. Every answer is a `Result`,
 * and the name in it is read off the rows the action looked up.
 */

export type LeaveDecision = Extract<Leave["status"], "approved" | "declined">

export type DecidedLeave = { id: string; status: LeaveDecision; employeeName: string; kind: Leave["kind"] }

/**
 * Grants or refuses a request. Only a pending one can be decided: a request
 * that was already approved or declined is refused with the decision it got,
 * because a manager pressing the other button a second time — or a second
 * manager on another page — is the race this guards against. The employee is
 * read off the row, never the form.
 */
export async function decideLeave(id: string, decision: LeaveDecision): Promise<Result<DecidedLeave>> {
  // The type is erased at the action boundary; only the two words a page offers may be written.
  if (decision !== "approved" && decision !== "declined") {
    return { ok: false, error: { code: "invalid", message: `"${String(decision)}" is not a decision a request can take.` } }
  }

  const leave = await db.leaves.get(id)
  if (!leave) {
    return { ok: false, error: { code: "not_found", message: `No leave request with id "${id}".` } }
  }
  const employee = await db.employees.get(leave.employeeId)
  const name = employee?.name ?? "That colleague"

  if (leave.status !== "pending") {
    return {
      ok: false,
      error: { code: "conflict", message: `${name}'s ${leave.kind} leave was already ${leave.status}.` },
    }
  }

  const updated = await db.leaves.update(id, { status: decision })
  if (!updated.ok) return updated
  return { ok: true, data: { id, status: decision, employeeName: name, kind: leave.kind } }
}