Skip to content

ComplaintList ​

The complaints inbox sidebar: a tab bar over the rows, a loader while fetching and two empty states, all in one column. Rows render through ComplaintListItem; the parent owns filtering — the component renders rows exactly as handed over.

vue
<script setup lang="ts">
import { ref } from "vue"
import RyderComplaintList from "@opeolluwa/ryder/components/complaints/ComplaintList.vue"
import type { ComplaintsInboxTab } from "@opeolluwa/ryder/types"

const activeTab = ref<ComplaintsInboxTab>("all")
const selectedId = ref<string | null>(null)
</script>

<template>
  <RyderComplaintList
    :rows="rows"
    :loading="loading"
    :inbox-empty="inboxEmpty"
    :active-tab="activeTab"
    :tab-count="tabCount"
    :reply-counts="replyCounts"
    :selected-id="selectedId"
    @update:active-tab="activeTab = $event"
    @select="selectedId = $event"
  />
</template>

Examples ​

vue
<script setup lang="ts">
import { computed, ref } from "vue";
import ComplaintList from "../../../src/components/complaints/ComplaintList.vue";
import { complaintMatchesTab } from "../../../src/utils/complaintStatus";
import { replyCounts, rows } from "./fixtures";
import type { ComplaintsInboxTab } from "../../../src/types/complaints";

const activeTab = ref<ComplaintsInboxTab>("all");
const selectedId = ref<string | null>(rows[0].complaint.identifier);

// The component renders `rows` verbatim — filtering to the active tab is the
// parent's job, here with the shared tab matcher.
const visibleRows = computed(() =>
  rows.filter((item) =>
    complaintMatchesTab(activeTab.value, item.complaint.status),
  ),
);

const tabCount = computed(() => ({
  all: rows.length,
  open: rows.filter((item) =>
    complaintMatchesTab("open", item.complaint.status),
  ).length,
  resolved: rows.filter((item) =>
    complaintMatchesTab("resolved", item.complaint.status),
  ).length,
}));
</script>

<template>
  <div>
    <div class="h-[28rem]">
      <ComplaintList
        :rows="visibleRows"
        :loading="false"
        :inbox-empty="false"
        :active-tab="activeTab"
        :tab-count="tabCount"
        :reply-counts="replyCounts"
        :selected-id="selectedId"
        @update:active-tab="activeTab = $event"
        @select="selectedId = $event"
      />
    </div>

    <p class="mt-3 text-xs text-gray-500 dark:text-white/40">
      Active tab: {{ activeTab }} — selected: {{ selectedId ?? "none" }}
    </p>
  </div>
</template>

Props ​

PropTypeDefaultDescription
rowsComplaintRow[]—Rows to render, already filtered by the parent.
loadingboolean—Shows PageLoader and disables every tab.
inboxEmptyboolean—The account has no complaints at all: hides the tab bar and shows the "No complaints yet" empty state.
activeTab"all" | "open" | "resolved"—Which tab is on; controlled through update:activeTab.
tabCountRecord<"all" | "open" | "resolved", number>—Per-tab counts. Declared required but never rendered — see Notes.
replyCountsRecord<string, number>—Reply badge per complaint identifier; a missing key renders 0 (no badge).
selectedIdstring | null—Identifier of the highlighted row.
tabsComplaintsInboxTab[][...COMPLAINTS_INBOX_TABS]Which tabs exist — by default all, open, resolved. Pass [] to drop the tab bar (e.g. a customer's short history).
showCustomerbooleantrueForwarded to each row: headline by customer vs by subject.

Emits ​

EventPayloadDescription
update:activeTab[value: ComplaintsInboxTab]A tab was picked; write it back into activeTab.
select[identifier: string]A row was clicked.

Slots ​

None.

Notes ​

  • tabCount is declared required but never rendered. No template reference reads it (its only use would be labelling the tabs). Keep passing it — callers usually derive it from the same rows — but know it currently has no effect.
  • Tab bar visibility: hidden when inboxEmpty is true or tabs is []; every tab is disabled while loading.
  • Two distinct empty states: inboxEmpty → "No complaints yet" (the account is empty); rows.length === 0 with inboxEmpty false → "No complaints in this tab" (filtering emptied the view).
  • Filtering is the caller's job — the demo filters with the shared complaintMatchesTab helper from src/utils/complaintStatus.ts.
  • Layout: full width below lg, then a fixed w-80 sidebar with a scrolling row area (lg:h-full lg:overflow-y-auto), so give it a sized parent.