ADDED: new add notes feature for orders backend and frontend
Runner Test / test (push) Failing after 17s
Runner Test / test (pull_request) Failing after 16s

This commit is contained in:
Lena Deriso
2026-09-22 23:06:01 -04:00
parent e493d1592b
commit c5215daedb
12 changed files with 425 additions and 8 deletions
File diff suppressed because one or more lines are too long
+8 -1
View File
@@ -1,4 +1,4 @@
import type { Address, AuditEvent, OrderDetail, OrderSummary } from "@orderops/shared";
import type { Address, AuditEvent, Note, OrderDetail, OrderSummary } from "@orderops/shared";
import { apiRequest } from "../lib/api-client";
export function listOrders(query: string) {
@@ -14,6 +14,13 @@ export function listAuditEvents(orderId: string) {
return apiRequest<AuditEvent[]>(`/api/orders/${orderId}/audit-events`);
}
export function addOrderNote(orderId: string, body: string) {
return apiRequest<Note>(`/api/orders/${orderId}/notes`, {
method: "POST",
body: JSON.stringify({ body })
});
}
export function updateOrderItemQuantity(orderId: string, itemId: string, quantity: number) {
return apiRequest<OrderDetail>(`/api/orders/${orderId}/items/${itemId}/quantity`, {
method: "POST",
+5 -1
View File
@@ -1,6 +1,7 @@
import type { Address, AuditEvent, OrderDetail } from "@orderops/shared";
import { AuditTimeline } from "./audit-timeline";
import { OrderItemsPanel } from "./order-items-panel";
import { OrderNotesPanel } from "./order-notes-panel";
import { OrderSummaryPanel } from "./order-summary";
import { ShippingAddressForm } from "./shipping-address-form";
@@ -10,6 +11,7 @@ interface OrderDetailPageProps {
onUpdateQuantity: (itemId: string, quantity: number) => Promise<void>;
onSaveAddress: (address: Address) => Promise<void>;
onCancel: () => Promise<void>;
onAddNote: (body: string) => Promise<void>;
}
export function OrderDetailPage({
@@ -17,7 +19,8 @@ export function OrderDetailPage({
auditEvents,
onUpdateQuantity,
onSaveAddress,
onCancel
onCancel,
onAddNote
}: OrderDetailPageProps) {
return (
<div className="space-y-4">
@@ -29,6 +32,7 @@ export function OrderDetailPage({
</div>
<OrderItemsPanel order={order} onUpdateQuantity={onUpdateQuantity} />
<ShippingAddressForm order={order} onSave={onSaveAddress} />
<OrderNotesPanel key={order.id} order={order} onAddNote={onAddNote} />
<AuditTimeline events={auditEvents} />
</div>
);
+90
View File
@@ -0,0 +1,90 @@
import { useState } from "react";
import type { OrderDetail } from "@orderops/shared";
interface OrderNotesPanelProps {
order: OrderDetail;
onAddNote: (body: string) => Promise<void>;
}
export function OrderNotesPanel({ order, onAddNote }: OrderNotesPanelProps) {
const [body, setBody] = useState("");
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const notes = [...order.notes].sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
return (
<section className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
<h3 className="mb-3 text-sm font-semibold text-slate-700">Internal notes</h3>
{notes.length ? (
<ul className="space-y-3">
{notes.map((note) => (
<li key={note.id} className="rounded-lg border border-slate-200 p-3">
<div className="text-xs text-slate-500">
Author: {note.authorName} {"\u00b7"} <time dateTime={note.createdAt}>{new Date(note.createdAt).toLocaleString()}</time>
</div>
<p className="mt-1 whitespace-pre-wrap [overflow-wrap:anywhere] text-sm text-slate-900">{note.body}</p>
</li>
))}
</ul>
) : (
<p className="text-sm text-slate-500">No internal notes yet.</p>
)}
<form
className="mt-4 space-y-3"
aria-busy={isPending}
onSubmit={async (event) => {
event.preventDefault();
if (isPending) {
return;
}
if (!body.trim() || body.length >= 2000) {
setError("Note must be non-empty and less than 2000 characters.");
return;
}
setIsPending(true);
setError(null);
try {
await onAddNote(body);
setBody("");
} catch (caught: unknown) {
setError(caught instanceof Error ? caught.message : "Failed to add note.");
} finally {
setIsPending(false);
}
}}
>
<label className="block text-sm text-slate-700">
<span className="mb-1 block">Add an internal note</span>
<textarea
className="w-full rounded-lg border border-slate-300 px-3 py-2 disabled:opacity-50"
rows={4}
maxLength={1999}
required
disabled={isPending}
value={body}
onChange={(event) => {
setBody(event.target.value);
setError(null);
}}
/>
</label>
{error ? (
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
{error}
</div>
) : null}
<button
className="rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50"
type="submit"
disabled={isPending || !body.trim()}
>
{isPending ? "Adding note..." : "Add note"}
</button>
</form>
</section>
);
}
+29 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import type { Address, AuditEvent, OrderDetail } from "@orderops/shared";
import { Link, useParams } from "react-router-dom";
import { Toast } from "../ui/toast";
import { cancelOrder, getOrder, listAuditEvents, updateOrderItemQuantity, updateShippingAddress } from "./order-api";
import { addOrderNote, cancelOrder, getOrder, listAuditEvents, updateOrderItemQuantity, updateShippingAddress } from "./order-api";
import { OrderDetailPage } from "./order-detail-page";
interface ToastState {
@@ -72,6 +72,33 @@ export function OrderPage() {
}
}
async function handleAddNote(body: string) {
try {
if (!orderId) {
throw new Error("Order not found.");
}
setError(null);
await addOrderNote(orderId, body);
} catch (caught) {
setToast({
message: caught instanceof Error ? caught.message : "Failed to add note.",
variant: "error"
});
throw caught;
}
try {
await refreshOrder(orderId);
setToast({ message: "Note added.", variant: "success" });
} catch {
setToast({
message: "Note saved, but the page didn't refresh — reload to see it.",
variant: "error"
});
}
}
return (
<div className="space-y-4">
<Link className="inline-flex text-sm font-medium text-blue-600 hover:text-blue-700" to="/orders">
@@ -87,6 +114,7 @@ export function OrderPage() {
onUpdateQuantity={(itemId, quantity) => runAction(() => updateOrderItemQuantity(order.id, itemId, quantity), "Quantity saved.")}
onSaveAddress={(address: Address) => runAction(() => updateShippingAddress(order.id, address), "Shipping address saved.")}
onCancel={() => runAction(() => cancelOrder(order.id), "Order cancelled.")}
onAddNote={handleAddNote}
/>
) : null}
</div>