From c7263bf28d49b0054e21344062fdec3c5e26aec0 Mon Sep 17 00:00:00 2001 From: Samuel Shuert Date: Wed, 22 Jul 2026 03:23:30 +0000 Subject: [PATCH] fix(frontend/orders): finish up frontend orders page --- backend/src/routes/order.rs | 25 ++++++++ frontend/components/forms/order.tsx | 7 +- frontend/components/pages/orders.tsx | 89 ++++++++++++++++++++------ frontend/components/ui/date-picker.tsx | 22 +++++-- frontend/components/ui/table.tsx | 18 +++++- 5 files changed, 133 insertions(+), 28 deletions(-) diff --git a/backend/src/routes/order.rs b/backend/src/routes/order.rs index d03b6f9..6d4ff70 100644 --- a/backend/src/routes/order.rs +++ b/backend/src/routes/order.rs @@ -8,6 +8,7 @@ use actix_web::{ HttpResponse, Responder, Scope, web::{Data, Json, Path, Query}, }; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{PgPool, error::DatabaseError}; use uuid::Uuid; @@ -24,6 +25,8 @@ struct QueryParams { pub invoice_number: Option, pub customer_id: Option, pub batch_id: Option, + pub created_at_from: Option>, + pub created_at_to: Option>, } #[actix_web::post("")] @@ -67,6 +70,8 @@ async fn get_all(pool: Data, options: Query, user: User) -> let invoice_like = options.invoice_number.as_ref().map(|n| format!("%{}%", n)); let customer_id = options.customer_id.as_ref(); let batch_id = options.batch_id.as_ref(); + let created_at_from = options.created_at_from.as_ref(); + let created_at_to = options.created_at_to.as_ref(); let mut count_query = sqlx::QueryBuilder::new(&format!("SELECT COUNT(*) FROM {}", Main::TABLE_NAME)); @@ -82,6 +87,16 @@ async fn get_all(pool: Data, options: Query, user: User) -> count_query.push_bind(cid); } + if let Some(from) = &created_at_from { + count_query.push(" AND created_at >= "); + count_query.push_bind(from); + } + + if let Some(to) = &created_at_to { + count_query.push(" AND created_at <= "); + count_query.push_bind(to); + } + if let Some(bid) = &batch_id { let table = Main::TABLE_NAME; count_query.push(&format!(" AND EXISTS (SELECT 1 FROM order_items WHERE order_items.order_id = {table}.id AND order_items.batch_id = ")); @@ -114,6 +129,16 @@ async fn get_all(pool: Data, options: Query, user: User) -> query.push_bind(cid); } + if let Some(from) = &created_at_from { + query.push(" AND created_at >= "); + query.push_bind(from); + } + + if let Some(to) = &created_at_to { + query.push(" AND created_at <= "); + query.push_bind(to); + } + if let Some(bid) = &batch_id { let table = Main::TABLE_NAME; query.push(&format!(" AND EXISTS (SELECT 1 FROM order_items WHERE order_items.order_id = {table}.id AND order_items.batch_id = ")); diff --git a/frontend/components/forms/order.tsx b/frontend/components/forms/order.tsx index 33ea3a3..8672a2f 100644 --- a/frontend/components/forms/order.tsx +++ b/frontend/components/forms/order.tsx @@ -46,12 +46,12 @@ async function getCustomerOptions( async function getBatchOptions(query: string, page: number): Promise { let url = `/api/batches?page=${page}&limit=10` - if (query) url += `&name=${query}` + if (query) url += `&lot_number=${query}` const res = await fetch(url) const data = await res.json() - return data.rows.map((c: { id: string; name: string }) => ({ + return data.rows.map((c: { id: string; lot_number: string }) => ({ value: c.id, - label: c.name, + label: c.lot_number, })) } @@ -74,6 +74,7 @@ export function OrderForm({ control, } = useForm({ resolver: zodResolver(schema), + defaultValues: orderEdit ?? {}, }) async function onSubmit(values: FormValues) { diff --git a/frontend/components/pages/orders.tsx b/frontend/components/pages/orders.tsx index 09cb4f9..5ee1875 100644 --- a/frontend/components/pages/orders.tsx +++ b/frontend/components/pages/orders.tsx @@ -34,7 +34,7 @@ import { Card, CardContent } from "@/components/ui/card" import { Modal } from "@/components/modal" import { UserPermissions } from "@/lib/userPermissions" -import { MoreHorizontal, UserPlus2 } from "lucide-react" +import { MoreHorizontal, ShoppingCart } from "lucide-react" import { useQuery } from "@tanstack/react-query" import { useDebounce } from "use-debounce" import { TablePaginator } from "../tablePages" @@ -44,14 +44,14 @@ const ENTITY_NAME = "Order" const ENTITY_PLURAL = "orders" const API_PATH = "/api/orders" -const SORTABLE_COLUMNS = ["invoice_number"] -const DEFAULT_SORT_BY = "invoice_number" +const SORTABLE_COLUMNS = ["created_at"] const PERM_CREATE = UserPermissions.OrderCreate const PERM_UPDATE = UserPermissions.OrderUpdate const PERM_DELETE = UserPermissions.OrderDelete import { OrderForm as EntityForm, Order as Entity } from "../forms/order" +import { Badge } from "../ui/badge" interface PaginatedResponse { rows: Entity[] @@ -81,6 +81,24 @@ function CustomerCell({ customerId }: { customerId: string }) { return {customer.name} } +function ItemsCell({ orderId }: { orderId: string }) { + const { data: items, isLoading } = useQuery({ + queryKey: ["order", orderId, "items"], + queryFn: async () => { + const res = await fetch(`/api/orders/${orderId}/items`) + return res.json() + }, + staleTime: 5 * 60 * 1000, + }) + + if (isLoading) return Loading... + if (!items) return Unknown + + return items.map((item: { lot_number: string }) => ( + {item.lot_number} + )) +} + async function getCustomerOptions( query: string, page: number @@ -97,12 +115,12 @@ async function getCustomerOptions( async function getBatchOptions(query: string, page: number): Promise { let url = `/api/batches?page=${page}&limit=10` - if (query) url += `&name=${query}` + if (query) url += `&lot_number=${query}` const res = await fetch(url) const data = await res.json() - return data.rows.map((c: { id: string; name: string }) => ({ + return data.rows.map((c: { id: string; lot_number: string }) => ({ value: c.id, - label: c.name, + label: c.lot_number, })) } @@ -121,8 +139,19 @@ export function OrdersPage({ userPerms }: Props) { const [debouncedFilters] = useDebounce(columnFilters, 300) - const nameFilter = - (debouncedFilters.find((f) => f.id === "name")?.value as string) ?? "" + const createdAtFilter = + (debouncedFilters.find((f) => f.id === "created_at")?.value as { + from: Date | undefined + to?: Date + }) ?? null + const invoiceFilter = + (debouncedFilters.find((f) => f.id === "invoice_number") + ?.value as string) ?? "" + const customerFilter = + (debouncedFilters.find((f) => f.id === "customer_id")?.value as string) ?? + "" + const batchIdsFilter = + (debouncedFilters.find((f) => f.id === "batch_ids")?.value as string) ?? "" useEffect(() => { setPagination((p) => ({ ...p, pageIndex: 0 })) @@ -131,8 +160,6 @@ export function OrdersPage({ userPerms }: Props) { const limit = pagination.pageSize const offset = pagination.pageIndex * pagination.pageSize - const sort = sorting.find((s) => SORTABLE_COLUMNS.includes(s.id)) - const sortBy = sort?.id ?? DEFAULT_SORT_BY const sortOrder = sorting.find((s) => SORTABLE_COLUMNS.includes(s.id))?.desc ? "desc" : "asc" @@ -142,20 +169,28 @@ export function OrdersPage({ userPerms }: Props) { ENTITY_PLURAL, limit, offset, - sortBy, sortOrder, - nameFilter, + customerFilter, debouncedFilters, + createdAtFilter, + batchIdsFilter, ], queryFn: async () => { const params = new URLSearchParams({ limit: String(limit), offset: String(offset), - sort_by: sortBy, sort_order: sortOrder, }) - if (nameFilter) params.set("name", nameFilter) + if (invoiceFilter) params.set("invoice_number", invoiceFilter) + if (customerFilter) params.set("customer_id", customerFilter) + if (createdAtFilter) { + const from = createdAtFilter.from?.toISOString() + const to = createdAtFilter.to?.toISOString() + if (from) params.set("created_at_from", from) + if (to) params.set("created_at_to", to) + } + if (batchIdsFilter) params.set("batch_id", batchIdsFilter) const res = await fetch(`${API_PATH}?${params.toString()}`) if (!res.ok) throw new Error(`Failed to fetch ${ENTITY_PLURAL}`) @@ -183,7 +218,7 @@ export function OrdersPage({ userPerms }: Props) { /> ), size: 150, - enableSorting: true, + enableSorting: false, enableColumnFilter: true, }, { @@ -221,9 +256,27 @@ export function OrdersPage({ userPerms }: Props) { size: 150, enableSorting: false, enableColumnFilter: true, + cell: ({ row }) => , + }, + { + accessorKey: "created_at", + header: ({ column }) => ( + + ), + size: 150, + enableSorting: true, + enableColumnFilter: true, cell: ({ row }) => { - const batchIds = row.getValue("batch_ids") as string[] - return batchIds.map((id) => {id}) + const createdAt = row.getValue("created_at") as Date + return {new Date(createdAt).toLocaleDateString()} }, }, { @@ -329,7 +382,7 @@ export function OrdersPage({ userPerms }: Props) { disabled={!userPerms.contains(PERM_CREATE)} onClick={() => setEditOpen(true)} > - New {ENTITY_NAME} + New {ENTITY_NAME} diff --git a/frontend/components/ui/date-picker.tsx b/frontend/components/ui/date-picker.tsx index 158ea81..e57f80f 100644 --- a/frontend/components/ui/date-picker.tsx +++ b/frontend/components/ui/date-picker.tsx @@ -35,10 +35,6 @@ export function DatePicker(props: DatePickerProps | DatePickerRangeProps) { variant="outline" data-empty={!value} className="w-full justify-center text-left text-xs font-normal data-[empty=true]:text-muted-foreground" - onContextMenu={(e) => { - e.preventDefault() - onDateChange(undefined) - }} > {value && !range && format(value, "PPP")} @@ -53,17 +49,35 @@ export function DatePicker(props: DatePickerProps | DatePickerRangeProps) { onDateChange(undefined)} + > + Clear + + } /> )} {range && ( onDateChange(undefined)} + > + Clear + + } /> )} diff --git a/frontend/components/ui/table.tsx b/frontend/components/ui/table.tsx index 5e35062..395b3ab 100644 --- a/frontend/components/ui/table.tsx +++ b/frontend/components/ui/table.tsx @@ -201,7 +201,6 @@ function SortableFilterableColumnHeader(props: SFCHP) { const canFilter = column.getCanFilter() function getDateFilterInput() { - console.log(from, to) if (type !== "date") return null if (from && to) return ( @@ -210,7 +209,16 @@ function SortableFilterableColumnHeader(props: SFCHP) { column.getFilterValue() as { from: Date | undefined; to?: Date } | undefined } - onDateChange={(dates) => column.setFilterValue(dates)} + onDateChange={(dates) => + !dates + ? column.setFilterValue(undefined) + : column.setFilterValue({ + ...dates, + to: dates?.to + ? new Date(dates.to.setHours(23, 59, 59, 0)) + : undefined, + }) + } range /> ) @@ -230,7 +238,11 @@ function SortableFilterableColumnHeader(props: SFCHP) { <> column.setFilterValue({ to: date })} + onDateChange={(date) => + column.setFilterValue({ + to: date ? new Date(date.setHours(23, 59, 59, 0)) : undefined, + }) + } />{" "} - -- 2.51.2