ComplaintPreview
The right-hand pane of the inbox: ComplaintHeader over ComplaintThread, or an empty state when nothing is selected. It is a relay — it renders the two children and forwards their setStatus / send events untouched.
vue
<script setup lang="ts">
import RyderComplaintPreview from "@opeolluwa/ryder/components/complaints/ComplaintPreview.vue"
</script>
<template>
<RyderComplaintPreview
:row="selectedRow"
:replies="replies"
:self="staff"
:counterpart="customer"
:sending="sending"
:sent-count="sentCount"
@set-status="setStatus"
@send="sendReply"
/>
</template>Examples
vue
<script setup lang="ts">
import { ref } from "vue";
import ComplaintPreview from "../../../src/components/complaints/ComplaintPreview.vue";
import {
counterpart,
replies as seed,
row,
self,
} from "./fixtures";
import type {
ComplaintReply,
ComplaintRow,
ComplaintWritableStatus,
} from "../../../src/types/complaints";
const selected = ref<ComplaintRow | null>({ ...row });
const thread = ref<ComplaintReply[]>([...seed]);
const sending = ref(false);
const sentCount = ref(0);
const lastSend = ref("—");
const lastStatus = ref<ComplaintWritableStatus | null>(null);
async function onSend(body: string) {
lastSend.value = body;
sending.value = true;
await new Promise((resolve) => setTimeout(resolve, 400));
thread.value = [
...thread.value,
{
identifier: `RPL-${Date.now()}`,
complaintIdentifier: selected.value?.complaint.identifier ?? row.complaint.identifier,
body,
senderEmail: self.email ?? "support@ryder.example",
createdAt: new Date().toISOString(),
updatedAt: null,
},
];
sending.value = false;
sentCount.value += 1;
}
function onSetStatus(status: ComplaintWritableStatus) {
lastStatus.value = status;
if (selected.value) {
selected.value = {
...selected.value,
complaint: { ...selected.value.complaint, status },
};
}
}
</script>
<template>
<div>
<div class="flex h-[32rem]">
<ComplaintPreview
:row="selected"
:replies="thread"
:self="self"
:counterpart="counterpart"
:sending="sending"
:sent-count="sentCount"
@send="onSend"
@set-status="onSetStatus"
/>
</div>
<div class="mt-3 flex flex-wrap items-center gap-3">
<button
type="button"
class="rounded-lg border border-gray-200 px-3 py-1.5 text-xs dark:border-white/10"
@click="selected = { ...row }"
>
Select complaint
</button>
<button
type="button"
class="rounded-lg border border-gray-200 px-3 py-1.5 text-xs dark:border-white/10"
@click="selected = null"
>
Clear selection
</button>
<span class="text-xs text-gray-500 dark:text-white/40">
Last send: {{ lastSend }} — last setStatus: {{ lastStatus ?? "—" }}
</span>
</div>
</div>
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
row | ComplaintRow | null | — | The selected complaint. null renders the empty state instead of header + thread. |
replies | ComplaintReply[] | — | Thread messages, passed straight to the thread. |
loadingReplies | boolean | false | Loader while the thread is fetching. |
self | ThreadParty | — | The replier's side of the conversation. |
counterpart | ThreadParty | — | The other side. |
sending | boolean | false | A reply POST is in flight. |
sentCount | number | 0 | Bumped by the parent on each successful send; clears the composer. |
statusMenu | boolean | true | Forwarded to the header: offer the status menu. |
showCustomer | boolean | true | Forwarded to the header: show the complainer's identity. |
Emits
| Event | Payload | Description |
|---|---|---|
setStatus | [status: ComplaintWritableStatus] | Relayed unchanged from the header's status menu. |
send | [body: string] | Relayed unchanged from the thread composer. |
Slots
None.
Notes
row === nullswaps the whole pane forEmptyState("No conversation selected") — the header and thread are not rendered at all, so no thread state is mounted for a deselected complaint.- No data fetching happens here:
replies,sendingandsentCountbelong to the parent, exactly as in the playground'scomplaints.vuepage, which pairs this with ComplaintList. - Because
statusMenu/showCustomeronly reach the header, a customer build turns both off in one place and the thread is unaffected.