Add Reorder shortcut to finished orders on /my-orders

## Original task (Task #66)
Let users quickly re-order a service from their order history. On every
order card whose status is `completed` or `cancelled`, show a "Reorder"
button (Arabic: "اطلب مجددًا") that opens the existing OrderServiceModal
pre-filled with the original notes. Submitting creates a brand-new
order via the existing endpoint; the original order stays unchanged.

## Implementation
- `artifacts/tx-os/src/components/order-service-modal.tsx`: added an
  optional `initialNotes` prop and seeded the notes textarea with it
  (truncated to NOTES_MAX) whenever the modal opens. Existing services
  page callers are unaffected (prop defaults to undefined => empty notes).
- `artifacts/tx-os/src/pages/my-orders.tsx`:
  - Imported the `RotateCcw` icon and `OrderServiceModal`.
  - Added a "Reorder" button (with icon) on `OrderCard` that shows
    only for finished orders, alongside the existing
    cancel/delete actions.
  - Hoisted reorder modal state to `MyOrdersPage` via a new
    `reorderTarget` state and an `onReorder` callback wired through
    `OrderCard`.
  - Render `OrderServiceModal` at the page level using the original
    order's service info and notes; closing/submitting clears state.
- `artifacts/tx-os/src/locales/{en,ar}.json`: added `myOrders.reorder`
  ("Reorder" / "اطلب مجددًا").

## Notes / non-deviations
- Task description references `artifacts/teaboy-os/...`; the actual
  artifact in this repo is `artifacts/tx-os/`. Same files, same intent.
- Submitting reuses `useCreateServiceOrder` so the original order is
  untouched and standard error/success toasts fire from the modal.

## Verification
- `pnpm exec tsc --noEmit` shows no new errors in the touched files.
- E2E test: created a fresh user with one completed and one cancelled
  order, logged in, opened /my-orders, confirmed the Reorder button
  appears with notes prefilled in the modal, edited the notes,
  submitted, and verified a new pending order with the edited notes
  appeared while the original order remained unchanged.

Replit-Task-Id: 2140b360-9d2f-45d9-9ddc-267ea29f7d7c
This commit is contained in:
riyadhafraa
2026-04-28 05:46:18 +00:00
parent 6c160530fa
commit f7b8093d04
5 changed files with 55 additions and 4 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

@@ -30,6 +30,7 @@ type Props = {
imageUrl?: string | null;
} | null;
resolveImage: (url: string | null | undefined) => string | null;
initialNotes?: string | null;
};
export function OrderServiceModal({
@@ -37,6 +38,7 @@ export function OrderServiceModal({
onOpenChange,
service,
resolveImage,
initialNotes,
}: Props) {
const { t, i18n } = useTranslation();
const lang = i18n.language;
@@ -49,10 +51,10 @@ export function OrderServiceModal({
useEffect(() => {
if (open) {
setNotes("");
setNotes((initialNotes ?? "").slice(0, NOTES_MAX));
setImgErrored(false);
}
}, [open, service?.id]);
}, [open, service?.id, initialNotes]);
if (!service) return null;
const name = lang === "ar" ? service.nameAr : service.nameEn;
+1
View File
@@ -134,6 +134,7 @@
"cancelFailed": "تعذّر إلغاء الطلب",
"restoreFailed": "تعذّر استعادة الطلب",
"delete": "حذف",
"reorder": "اطلب مجددًا",
"deleteConfirmTitle": "حذف هذا الطلب؟",
"deleteConfirmBody": "سيتم حذف الطلب نهائياً من سجلّك.",
"deleteConfirm": "نعم، احذف",
+1
View File
@@ -134,6 +134,7 @@
"cancelFailed": "Could not cancel the order",
"restoreFailed": "Could not restore the order",
"delete": "Delete",
"reorder": "Reorder",
"deleteConfirmTitle": "Delete this order?",
"deleteConfirmBody": "This will permanently remove the order from your history.",
"deleteConfirm": "Yes, delete",
+49 -2
View File
@@ -21,8 +21,10 @@ import {
PackageCheck,
XCircle,
Trash2,
RotateCcw,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { OrderServiceModal } from "@/components/order-service-modal";
import {
AlertDialog,
AlertDialogAction,
@@ -156,9 +158,11 @@ function OrderImage({
function OrderCard({
order,
onDelete,
onReorder,
}: {
order: ServiceOrder;
onDelete: (id: number) => void;
onReorder: (order: ServiceOrder) => void;
}) {
const { t, i18n } = useTranslation();
const queryClient = useQueryClient();
@@ -172,7 +176,9 @@ function OrderCard({
const img = resolveServiceImageUrl(order.service.imageUrl);
const cancellable =
order.status === "pending" || order.status === "received";
const deletable = isFinished(order.status);
const finished = isFinished(order.status);
const deletable = finished;
const reorderable = finished;
const handleCancel = () => {
const previousStatus = order.status;
@@ -258,8 +264,19 @@ function OrderCard({
<StatusTimeline status={order.status} />
{(cancellable || deletable) && (
{(cancellable || finished) && (
<div className="flex justify-end gap-1">
{reorderable && (
<Button
variant="ghost"
size="sm"
className="text-primary hover:text-primary hover:bg-primary/10 gap-1.5"
onClick={() => onReorder(order)}
>
<RotateCcw size={14} />
{t("myOrders.reorder")}
</Button>
)}
{cancellable && (
<Button
variant="ghost"
@@ -357,6 +374,15 @@ export default function MyOrdersPage() {
const [pendingDeleteIds, setPendingDeleteIds] = useState<Set<number>>(
() => new Set(),
);
const [reorderTarget, setReorderTarget] = useState<{
service: {
id: number;
nameAr: string;
nameEn: string;
imageUrl?: string | null;
};
notes: string | null;
} | null>(null);
// Track scheduled timers and queued IDs so we can flush on unmount.
const timersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(
@@ -553,6 +579,17 @@ export default function MyOrdersPage() {
key={o.id}
order={o}
onDelete={(id) => scheduleDelete([id])}
onReorder={(order) =>
setReorderTarget({
service: {
id: order.service.id,
nameAr: order.service.nameAr,
nameEn: order.service.nameEn,
imageUrl: order.service.imageUrl,
},
notes: order.notes ?? null,
})
}
/>
))}
</div>
@@ -585,6 +622,16 @@ export default function MyOrdersPage() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<OrderServiceModal
open={reorderTarget !== null}
onOpenChange={(o) => {
if (!o) setReorderTarget(null);
}}
service={reorderTarget?.service ?? null}
resolveImage={resolveServiceImageUrl}
initialNotes={reorderTarget?.notes ?? null}
/>
</div>
);
}