From 2ff26241d776e956964cbb0dcaf0d8394807cee8 Mon Sep 17 00:00:00 2001 From: BOHA Date: Sun, 7 Jun 2026 16:32:08 +0200 Subject: [PATCH] feat(offer-detail): mobile card layout for the line-items editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On phones (< sm) the editable items grid was a wide horizontally-scrolling table of inputs — unusable. SortableItemRow now renders each item as a stacked card (labeled Popis / Množství / Jednotka / Cena/ks inputs, V ceně checkbox + Celkem, drag handle + remove in the header), and the wrapper renders a card stack instead of TableContainer/Table on mobile (DndContext/SortableContext preserved so drag still works via TouchSensor). Desktop keeps the table. Verified: no inner table on mobile, no page horizontal scroll. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/admin/pages/OfferDetail.tsx | 303 ++++++++++++++++++++++++-------- 1 file changed, 234 insertions(+), 69 deletions(-) diff --git a/src/admin/pages/OfferDetail.tsx b/src/admin/pages/OfferDetail.tsx index 1e7b910..16b9472 100644 --- a/src/admin/pages/OfferDetail.tsx +++ b/src/admin/pages/OfferDetail.tsx @@ -21,6 +21,8 @@ import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import Checkbox from "@mui/material/Checkbox"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; import { offerDetailOptions, offerCustomersOptions, @@ -229,6 +231,8 @@ function SortableItemRow({ transition, isDragging, } = useSortable({ id: item._key, disabled: readOnly }); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const style = { transform: CSS.Transform.toString(transform), transition, @@ -239,6 +243,146 @@ function SortableItemRow({ }; const lineTotal = (Number(item.quantity) || 0) * (Number(item.unit_price) || 0); + + if (isMobile) { + return ( + + + {!readOnly && ( + + {DragIcon} + + )} + + Položka {index + 1} + + + {!readOnly && ( + + {RemoveIcon} + + )} + + onUpdate("description", e.target.value)} + placeholder="Název položky" + InputProps={{ readOnly }} + /> + onUpdate("item_description", e.target.value)} + placeholder="Volitelný" + InputProps={{ readOnly }} + /> + + onUpdate("quantity", parseInt(e.target.value, 10))} + slotProps={{ htmlInput: { step: "1" } }} + InputProps={{ readOnly }} + /> + onUpdate("unit", e.target.value)} + InputProps={{ readOnly }} + /> + onUpdate("unit_price", parseFloat(e.target.value))} + slotProps={{ htmlInput: { step: "0.01" } }} + InputProps={{ readOnly }} + /> + + + + + onUpdate("is_included_in_total", e.target.checked) + } + disabled={readOnly} + /> + V ceně + + + + Celkem + + + {formatCurrency(lineTotal, currency)} + + + + + ); + } + return ( {!readOnly && ( @@ -352,6 +496,8 @@ export default function OfferDetail() { const alert = useAlert(); const { hasPermission } = useAuth(); const navigate = useNavigate(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const dndSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(TouchSensor, { @@ -1392,76 +1538,95 @@ export default function OfferDetail() { )} - - { - const { active, over } = event; - if (!over || active.id === over.id) return; - setItems((prev) => { - const oldIndex = prev.findIndex( - (i) => i._key === String(active.id), - ); - const newIndex = prev.findIndex( - (i) => i._key === String(over.id), - ); - if (oldIndex === -1 || newIndex === -1) return prev; - return arrayMove(prev, oldIndex, newIndex); - }); - }} + { + const { active, over } = event; + if (!over || active.id === over.id) return; + setItems((prev) => { + const oldIndex = prev.findIndex( + (i) => i._key === String(active.id), + ); + const newIndex = prev.findIndex( + (i) => i._key === String(over.id), + ); + if (oldIndex === -1 || newIndex === -1) return prev; + return arrayMove(prev, oldIndex, newIndex); + }); + }} + > + i._key)} + strategy={verticalListSortingStrategy} > - i._key)} - strategy={verticalListSortingStrategy} - > - - - - {!readOnly && } - - # - - Popis - Množství - Jednotka - Cena/ks - - V ceně - - - Celkem - - {!readOnly && } - - - - {items.map((item, index) => ( - 1} - onUpdate={(field, value) => - updateItem(index, field as keyof OfferItem, value) - } - onRemove={() => removeItem(index)} - /> - ))} - -
-
-
-
+ {isMobile ? ( + + {items.map((item, index) => ( + 1} + onUpdate={(field, value) => + updateItem(index, field as keyof OfferItem, value) + } + onRemove={() => removeItem(index)} + /> + ))} + + ) : ( + + + + + {!readOnly && } + + # + + Popis + Množství + Jednotka + Cena/ks + + V ceně + + + Celkem + + {!readOnly && } + + + + {items.map((item, index) => ( + 1} + onUpdate={(field, value) => + updateItem(index, field as keyof OfferItem, value) + } + onRemove={() => removeItem(index)} + /> + ))} + +
+
+ )} + + {/* Totals */}