> ## Documentation Index
> Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Case Manager Dashboard

> Tools for social workers and navigators to help clients discover and apply for benefits

# Case Manager Dashboard

> Empower case managers, social workers, and navigators to help multiple clients discover and access benefits efficiently.

***

## Overview

Case managers multiply impact by helping many clients navigate benefits. Pathfinder provides specialized tools:

```mermaid theme={null}
flowchart TB
    subgraph Dashboard
        Clients[Client List]
        Search[Client Search]
        Bulk[Bulk Actions]
    end

    subgraph ClientManagement
        Profile[Client Profile]
        Notes[Case Notes]
        Timeline[Activity Timeline]
    end

    subgraph Workflow
        Screen[Run Screener]
        Refer[Make Referrals]
        Track[Track Outcomes]
    end

    subgraph Reports
        Stats[Statistics]
        Export[Data Export]
    end

    Clients --> Profile
    Search --> Profile
    Profile --> Notes
    Profile --> Timeline
    Profile --> Screen
    Screen --> Refer
    Refer --> Track
    Track --> Stats
    Bulk --> Refer
```

***

## Role-Based Access

### Database Schema

```sql theme={null}
-- Organizations (agencies, nonprofits)
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  type TEXT NOT NULL, -- agency, nonprofit, navigator_org
  settings JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- User roles within organizations
CREATE TABLE user_roles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  role TEXT NOT NULL, -- admin, case_manager, viewer
  permissions JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(user_id, organization_id)
);

-- Case manager to client relationships
CREATE TABLE case_manager_clients (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  case_manager_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  client_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
  status TEXT DEFAULT 'active', -- active, inactive, transferred
  assigned_at TIMESTAMPTZ DEFAULT NOW(),
  notes TEXT,
  UNIQUE(case_manager_id, client_id)
);

CREATE INDEX idx_cm_clients_manager ON case_manager_clients(case_manager_id)
  WHERE status = 'active';
CREATE INDEX idx_cm_clients_org ON case_manager_clients(organization_id);
```

### Permission Checks

```typescript theme={null}
// lib/auth-guards.ts
export async function requireCaseManager() {
  const { user } = await safeAuth();
  if (!user) throw new Error("Not authenticated");

  const { data: roles } = await supabaseAdmin
    .from("user_roles")
    .select("role, organization_id")
    .eq("user_id", user.id)
    .in("role", ["admin", "case_manager"]);

  if (!roles?.length) {
    throw new Error("Case manager access required");
  }

  return { user, roles };
}

export async function requireClientAccess(clientId: string) {
  const { user, roles } = await requireCaseManager();

  const { data: relationship } = await supabaseAdmin
    .from("case_manager_clients")
    .select("id")
    .eq("case_manager_id", user.id)
    .eq("client_id", clientId)
    .eq("status", "active")
    .single();

  if (!relationship) {
    throw new Error("Client access denied");
  }

  return { user, roles };
}
```

***

## Client List

View and manage assigned clients.

```tsx theme={null}
// components/case-manager/client-list.tsx
interface ClientListProps {
  organizationId: string;
}

export function ClientList({ organizationId }: ClientListProps) {
  const [clients, setClients] = useState<Client[]>([]);
  const [filter, setFilter] = useState<ClientFilter>({
    status: "active",
    search: "",
    sortBy: "name",
  });
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

  return (
    <div className="space-y-4">
      {/* Filters */}
      <div className="flex items-center gap-4">
        <Input
          placeholder="Search clients..."
          value={filter.search}
          onChange={e => setFilter(f => ({ ...f, search: e.target.value }))}
          className="max-w-xs"
        />

        <Select
          value={filter.status}
          onValueChange={status => setFilter(f => ({ ...f, status }))}
        >
          <SelectTrigger className="w-40">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="active">Active</SelectItem>
            <SelectItem value="inactive">Inactive</SelectItem>
            <SelectItem value="all">All</SelectItem>
          </SelectContent>
        </Select>

        {selectedIds.size > 0 && (
          <BulkReferralDialog
            clientIds={Array.from(selectedIds)}
            onComplete={() => setSelectedIds(new Set())}
          />
        )}
      </div>

      {/* Client table */}
      <div className="border rounded-lg">
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead className="w-12">
                <Checkbox
                  checked={selectedIds.size === clients.length}
                  onCheckedChange={checked => {
                    setSelectedIds(
                      checked ? new Set(clients.map(c => c.id)) : new Set()
                    );
                  }}
                />
              </TableHead>
              <TableHead>Name</TableHead>
              <TableHead>Applications</TableHead>
              <TableHead>Last Activity</TableHead>
              <TableHead>Status</TableHead>
              <TableHead></TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {clients.map(client => (
              <TableRow key={client.id}>
                <TableCell>
                  <Checkbox
                    checked={selectedIds.has(client.id)}
                    onCheckedChange={checked => {
                      const next = new Set(selectedIds);
                      if (checked) {
                        next.add(client.id);
                      } else {
                        next.delete(client.id);
                      }
                      setSelectedIds(next);
                    }}
                  />
                </TableCell>
                <TableCell>
                  <div>
                    <p className="font-medium">{client.fullName}</p>
                    <p className="text-sm text-stone-500">{client.email}</p>
                  </div>
                </TableCell>
                <TableCell>
                  <div className="flex gap-1">
                    <Badge variant="success">{client.approvedCount}</Badge>
                    <Badge variant="warning">{client.pendingCount}</Badge>
                  </div>
                </TableCell>
                <TableCell>
                  {formatDistanceToNow(client.lastActivity)} ago
                </TableCell>
                <TableCell>
                  <Badge variant={client.status === "active" ? "default" : "secondary"}>
                    {client.status}
                  </Badge>
                </TableCell>
                <TableCell>
                  <Button variant="ghost" size="sm" asChild>
                    <Link href={`/case-manager/clients/${client.id}`}>
                      View
                    </Link>
                  </Button>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </div>
    </div>
  );
}
```

***

## Bulk Referrals

Refer multiple clients to programs at once.

```tsx theme={null}
// components/case-manager/bulk-referral-dialog.tsx
interface BulkReferralDialogProps {
  clientIds: string[];
  onComplete: () => void;
}

export function BulkReferralDialog({ clientIds, onComplete }: BulkReferralDialogProps) {
  const [step, setStep] = useState<"programs" | "message" | "confirm">("programs");
  const [selectedPrograms, setSelectedPrograms] = useState<string[]>([]);
  const [message, setMessage] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async () => {
    setIsSubmitting(true);
    try {
      await createBulkReferrals({
        clientIds,
        programIds: selectedPrograms,
        message,
      });
      onComplete();
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button>
          <Send className="h-4 w-4 mr-2" />
          Refer {clientIds.length} clients
        </Button>
      </DialogTrigger>

      <DialogContent className="max-w-2xl">
        <DialogHeader>
          <DialogTitle>Bulk Referral</DialogTitle>
          <DialogDescription>
            Refer {clientIds.length} clients to benefit programs
          </DialogDescription>
        </DialogHeader>

        {/* Progress indicator */}
        <div className="flex items-center gap-2 mb-6">
          {["programs", "message", "confirm"].map((s, i) => (
            <React.Fragment key={s}>
              <div className={cn(
                "w-8 h-8 rounded-full flex items-center justify-center text-sm",
                step === s ? "bg-blue-600 text-white" :
                i < ["programs", "message", "confirm"].indexOf(step)
                  ? "bg-emerald-600 text-white"
                  : "bg-stone-200 text-stone-600"
              )}>
                {i + 1}
              </div>
              {i < 2 && <div className="flex-1 h-px bg-stone-200" />}
            </React.Fragment>
          ))}
        </div>

        {/* Step content */}
        {step === "programs" && (
          <div className="space-y-4">
            <p className="text-sm text-stone-500">
              Select programs to refer clients to:
            </p>
            <ProgramSelector
              selected={selectedPrograms}
              onChange={setSelectedPrograms}
            />
            <div className="flex justify-end">
              <Button
                onClick={() => setStep("message")}
                disabled={selectedPrograms.length === 0}
              >
                Next
              </Button>
            </div>
          </div>
        )}

        {step === "message" && (
          <div className="space-y-4">
            <div>
              <Label>Personal Message (Optional)</Label>
              <Textarea
                value={message}
                onChange={e => setMessage(e.target.value)}
                placeholder="Add a personal note to include with the referral..."
                rows={4}
              />
            </div>
            <div className="flex justify-between">
              <Button variant="outline" onClick={() => setStep("programs")}>
                Back
              </Button>
              <Button onClick={() => setStep("confirm")}>
                Next
              </Button>
            </div>
          </div>
        )}

        {step === "confirm" && (
          <div className="space-y-4">
            <div className="p-4 bg-stone-50 rounded-lg">
              <h4 className="font-medium mb-2">Referral Summary</h4>
              <ul className="text-sm text-stone-600 space-y-1">
                <li>Clients: {clientIds.length}</li>
                <li>Programs: {selectedPrograms.length}</li>
                <li>Total referrals: {clientIds.length * selectedPrograms.length}</li>
              </ul>
            </div>
            <div className="flex justify-between">
              <Button variant="outline" onClick={() => setStep("message")}>
                Back
              </Button>
              <Button onClick={handleSubmit} disabled={isSubmitting}>
                {isSubmitting ? "Sending..." : "Send Referrals"}
              </Button>
            </div>
          </div>
        )}
      </DialogContent>
    </Dialog>
  );
}
```

***

## Case Notes

Track client interactions and progress.

```tsx theme={null}
// components/case-manager/client-notes.tsx
const NOTE_TYPES = [
  { value: "general", label: "General Note", icon: StickyNote },
  { value: "phone_call", label: "Phone Call", icon: Phone },
  { value: "email", label: "Email", icon: Mail },
  { value: "in_person", label: "In-Person Visit", icon: Users },
  { value: "document", label: "Document Review", icon: FileText },
  { value: "application_help", label: "Application Help", icon: ClipboardCheck },
  { value: "referral", label: "Referral Made", icon: Send },
  { value: "follow_up", label: "Follow-up Needed", icon: Clock },
  { value: "milestone", label: "Milestone", icon: Trophy },
  { value: "issue", label: "Issue/Concern", icon: AlertTriangle },
  { value: "resolution", label: "Resolution", icon: CheckCircle2 },
];

export function ClientNotes({ clientId }: { clientId: string }) {
  const [notes, setNotes] = useState<Note[]>([]);
  const [newNote, setNewNote] = useState({ type: "general", content: "" });
  const [followUpDate, setFollowUpDate] = useState<Date | null>(null);

  const handleAddNote = async () => {
    await addClientNote({
      clientId,
      type: newNote.type,
      content: newNote.content,
      followUpDate: followUpDate?.toISOString(),
    });
    setNewNote({ type: "general", content: "" });
    setFollowUpDate(null);
    refreshNotes();
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Case Notes</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Add note form */}
        <div className="space-y-3 pb-4 border-b mb-4">
          <Select
            value={newNote.type}
            onValueChange={type => setNewNote(n => ({ ...n, type }))}
          >
            <SelectTrigger>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {NOTE_TYPES.map(type => (
                <SelectItem key={type.value} value={type.value}>
                  <div className="flex items-center gap-2">
                    <type.icon className="h-4 w-4" />
                    {type.label}
                  </div>
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          <Textarea
            value={newNote.content}
            onChange={e => setNewNote(n => ({ ...n, content: e.target.value }))}
            placeholder="Add a note..."
            rows={3}
          />

          {newNote.type === "follow_up" && (
            <div>
              <Label>Follow-up Date</Label>
              <DatePicker
                value={followUpDate}
                onChange={setFollowUpDate}
              />
            </div>
          )}

          <Button onClick={handleAddNote} disabled={!newNote.content.trim()}>
            Add Note
          </Button>
        </div>

        {/* Notes timeline */}
        <div className="space-y-4">
          {notes.map(note => {
            const noteType = NOTE_TYPES.find(t => t.value === note.type);
            const Icon = noteType?.icon || StickyNote;

            return (
              <div key={note.id} className="flex gap-3">
                <div className={cn(
                  "w-8 h-8 rounded-full flex items-center justify-center shrink-0",
                  note.type === "milestone" && "bg-emerald-100 text-emerald-600",
                  note.type === "issue" && "bg-red-100 text-red-600",
                  note.type === "follow_up" && "bg-amber-100 text-amber-600",
                  !["milestone", "issue", "follow_up"].includes(note.type) &&
                    "bg-stone-100 text-stone-600"
                )}>
                  <Icon className="h-4 w-4" />
                </div>

                <div className="flex-1">
                  <div className="flex items-center gap-2 mb-1">
                    <span className="text-sm font-medium">
                      {noteType?.label}
                    </span>
                    <span className="text-xs text-stone-400">
                      {formatDistanceToNow(note.createdAt)} ago
                    </span>
                  </div>
                  <p className="text-sm text-stone-600">{note.content}</p>
                  {note.followUpDate && (
                    <div className="flex items-center gap-1 mt-1 text-xs text-amber-600">
                      <Clock className="h-3 w-3" />
                      Follow-up: {formatDate(note.followUpDate)}
                    </div>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </CardContent>
    </Card>
  );
}
```

***

## Reports Dashboard

Track outcomes and measure impact.

```tsx theme={null}
// app/(dashboard)/case-manager/reports/page.tsx
export default async function ReportsPage() {
  const { user } = await requireCaseManager();
  const stats = await getCaseManagerStats(user.id);

  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">Reports</h1>

      {/* Summary stats */}
      <div className="grid gap-4 md:grid-cols-4">
        <StatCard
          title="Active Clients"
          value={stats.activeClients}
          icon={Users}
        />
        <StatCard
          title="Applications Submitted"
          value={stats.applicationsSubmitted}
          change={`+${stats.applicationsThisMonth} this month`}
          icon={FileText}
        />
        <StatCard
          title="Benefits Approved"
          value={stats.benefitsApproved}
          icon={CheckCircle2}
          variant="success"
        />
        <StatCard
          title="Est. Monthly Value"
          value={formatCurrency(stats.estimatedMonthlyValue)}
          icon={DollarSign}
          variant="success"
        />
      </div>

      {/* Outcomes by program */}
      <Card>
        <CardHeader>
          <CardTitle>Outcomes by Program</CardTitle>
        </CardHeader>
        <CardContent>
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>Program</TableHead>
                <TableHead>Referred</TableHead>
                <TableHead>Applied</TableHead>
                <TableHead>Approved</TableHead>
                <TableHead>Success Rate</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {stats.programOutcomes.map(program => (
                <TableRow key={program.id}>
                  <TableCell className="font-medium">{program.name}</TableCell>
                  <TableCell>{program.referred}</TableCell>
                  <TableCell>{program.applied}</TableCell>
                  <TableCell>{program.approved}</TableCell>
                  <TableCell>
                    <Badge variant={program.successRate > 50 ? "success" : "secondary"}>
                      {program.successRate}%
                    </Badge>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </CardContent>
      </Card>

      {/* Activity over time */}
      <Card>
        <CardHeader>
          <CardTitle>Activity Over Time</CardTitle>
        </CardHeader>
        <CardContent>
          <ActivityChart data={stats.activityTimeline} />
        </CardContent>
      </Card>
    </div>
  );
}
```

***

## Database Schema: Client Notes

```sql theme={null}
CREATE TABLE client_notes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  client_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  case_manager_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,

  -- Note content
  type TEXT NOT NULL, -- general, phone_call, email, etc.
  content TEXT NOT NULL,

  -- Follow-up tracking
  follow_up_date DATE,
  follow_up_completed BOOLEAN DEFAULT false,
  follow_up_completed_at TIMESTAMPTZ,

  -- Metadata
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_client_notes_client ON client_notes(client_id, created_at DESC);
CREATE INDEX idx_client_notes_follow_up ON client_notes(follow_up_date)
  WHERE follow_up_date IS NOT NULL AND follow_up_completed = false;

-- Activity log for audit trail
CREATE TABLE client_activity_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  client_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
  actor_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE SET NULL,
  action TEXT NOT NULL, -- viewed, screened, referred, note_added, etc.
  details JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_activity_log_client ON client_activity_log(client_id, created_at DESC);
```

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Success Stories" icon="sparkles" href="/pathfinder/features/success-stories">
    Share client success stories
  </Card>

  <Card title="PWA & Offline" icon="wifi-off" href="/pathfinder/features/pwa-offline">
    Work with limited connectivity
  </Card>
</CardGroup>
