From c36b9f69df9b572eb7933ce6361acc9ea6702e2c Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:57:52 +0700 Subject: [PATCH] Enhance ticket handling and UI interactions Backend: Added lineId to ticket creation, improved ticket update logic, and switched ticket update route to POST. Added 'update_ticket' event to socket.io provider. Frontend: Integrated 'motion' for animated bottom toolbar, added expand/collapse functionality, improved ticket creation and update flows, and ensured terminal focus on CLI open. Adjusted delays in ButtonDPELP, improved ticket info copy, and enhanced input handling in ModalTerminal. Updated dependencies to include 'motion'. --- BACKEND/app/controllers/tickets_controller.ts | 4 + BACKEND/providers/socket_io_provider.ts | 4 + BACKEND/start/routes.ts | 2 +- FRONTEND/package-lock.json | 69 +++ FRONTEND/package.json | 1 + FRONTEND/src/App.tsx | 17 +- FRONTEND/src/components/BottomToolBar.tsx | 579 ++++++++++-------- FRONTEND/src/components/ButtonAction.tsx | 12 +- FRONTEND/src/components/CardLine.tsx | 8 + FRONTEND/src/components/ModalTerminal.tsx | 111 ++-- FRONTEND/src/components/TerminalXTerm.tsx | 14 +- 11 files changed, 501 insertions(+), 320 deletions(-) diff --git a/BACKEND/app/controllers/tickets_controller.ts b/BACKEND/app/controllers/tickets_controller.ts index 8adb28e..c4cba77 100644 --- a/BACKEND/app/controllers/tickets_controller.ts +++ b/BACKEND/app/controllers/tickets_controller.ts @@ -85,6 +85,7 @@ export default class TicketsController { model: payload.model.trim(), sn: payload.sn.trim(), stationId: payload.station_id, + lineId: payload.line_id, status: 'open', history: JSON.stringify(history), }, @@ -149,11 +150,14 @@ export default class TicketsController { const listHistory = ticket.history ? JSON.parse(ticket.history) : [] listHistory.unshift(history) payload.history = JSON.stringify(listHistory) + delete payload.userName + delete payload.userId ticket.merge(payload) await ticket.save() return response.ok({ status: true, message: 'Ticket updated successfully', data: ticket }) } catch (error) { + console.log(error) return response.internalServerError({ status: false, message: 'Failed to update ticket', diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 62a1bba..3cc081c 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -448,6 +448,10 @@ export class WebSocketIo { } } }) + + socket.on('update_ticket', async (data) => { + io.emit('update_ticket', data) + }) }) socketServer.listen(SOCKET_IO_PORT, () => { diff --git a/BACKEND/start/routes.ts b/BACKEND/start/routes.ts index 6981a3a..07c2b50 100644 --- a/BACKEND/start/routes.ts +++ b/BACKEND/start/routes.ts @@ -77,7 +77,7 @@ router router.post('/all', '#controllers/tickets_controller.getAll') router.post('create', '#controllers/tickets_controller.create') - router.put('update/:id', '#controllers/tickets_controller.update') + router.post('update/:id', '#controllers/tickets_controller.update') router.delete('delete/:id', '#controllers/tickets_controller.delete') }) .prefix('api/ticket') diff --git a/FRONTEND/package-lock.json b/FRONTEND/package-lock.json index 7291382..59dfcda 100644 --- a/FRONTEND/package-lock.json +++ b/FRONTEND/package-lock.json @@ -20,6 +20,7 @@ "@xterm/addon-fit": "^0.10.0", "axios": "^1.12.2", "moment": "^2.30.1", + "motion": "^12.23.24", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.4", @@ -2868,6 +2869,33 @@ "node": ">= 6" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3339,6 +3367,47 @@ "node": "*" } }, + "node_modules/motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", + "integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.24", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/FRONTEND/package.json b/FRONTEND/package.json index 00abdef..d3a625c 100644 --- a/FRONTEND/package.json +++ b/FRONTEND/package.json @@ -22,6 +22,7 @@ "@xterm/addon-fit": "^0.10.0", "axios": "^1.12.2", "moment": "^2.30.1", + "motion": "^12.23.24", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.4", diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index 20a5ade..34240d4 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -77,6 +77,7 @@ function App() { const [usersConnecting, setUsersConnecting] = useState([]); const [testLogContent, setTestLogContent] = useState(""); const [isLogModalOpen, setIsLogModalOpen] = useState(false); + const [expandedBottomBar, setExpandedBottomBar] = useState(true); const connectApcSwitch = (station: TStation) => { if (station?.apc_1_ip && station?.apc_1_port) { @@ -281,6 +282,18 @@ function App() { }, 100); }); + socket?.on("update_ticket", (data) => { + setTimeout(() => { + updateValueLineStation( + data.lineId, + { + tickets: data.data, + }, + data?.stationId + ); + }, 100); + }); + // ✅ cleanup on unmount or when socket changes return () => { socket.off("init"); @@ -293,6 +306,7 @@ function App() { socket.off("user_close_cli"); socket.off("response_content_log"); socket.off("data_textfsm"); + socket.off("update_ticket"); }; }, [socket, stations, selectedLine]); @@ -396,7 +410,7 @@ function App() { borderRadius: 8, }} > - + {station.lines.length > 8 ? ( diff --git a/FRONTEND/src/components/BottomToolBar.tsx b/FRONTEND/src/components/BottomToolBar.tsx index 6711248..e10db78 100644 --- a/FRONTEND/src/components/BottomToolBar.tsx +++ b/FRONTEND/src/components/BottomToolBar.tsx @@ -1,4 +1,5 @@ import { + ActionIcon, Box, Button, CloseButton, @@ -18,6 +19,8 @@ import { ButtonDPELP, ButtonScenario, ButtonSelect } from "./ButtonAction"; import DrawerLogs from "./DrawerLogs"; import { DrawerAPCControl, DrawerSwitchControl } from "./DrawerControl"; import { isJsonString } from "../untils/helper"; +import { motion } from "motion/react"; +import { IconCaretDown, IconCaretUp } from "@tabler/icons-react"; interface TabsProps { selectedLines: TLine[]; @@ -31,6 +34,7 @@ interface TabsProps { setIsLogModalOpen: (value: React.SetStateAction) => void; setTestLogContent: (value: React.SetStateAction) => void; scenarios: IScenario[]; + setExpanded: (value: React.SetStateAction) => void; } const BottomToolBar = ({ @@ -45,6 +49,7 @@ const BottomToolBar = ({ setIsLogModalOpen, setTestLogContent, scenarios, + setExpanded, }: TabsProps) => { const user = useMemo(() => { return localStorage.getItem("user") && @@ -54,289 +59,333 @@ const BottomToolBar = ({ }, []); const [valueInput, setValueInput] = useState(""); const [activeTabBottom, setActiveBottom] = useState("command"); + const [isExpand, setIsExpand] = useState(true); return ( - - - - { - setActiveBottom(val || "command"); + + + { + setIsExpand((prev) => !prev); + setExpanded((prev) => !prev); }} - className={classes.containerBottom} - style={{ height: "14vh" }} > - - + ) : ( + + )} + + + + + { + setActiveBottom(val || "command"); }} - value="command" + className={classes.containerBottom} + style={{ height: "14vh" }} > - Command Line - - - APC - - - Switch - - + + + Command Line + + + APC + + + Switch + + - - - - - {selectedLines.map((el) => ( - - - Line {el.lineNumber} - { - setSelectedLines( - selectedLines.filter((line) => line.id !== el.id) - ); - socket?.emit("close_cli", { - lineId: el?.id, - stationId: el.stationId || el.station_id, - }); + + + + + {selectedLines.map((el) => ( + - - - ))} - - - - - - - - - { - const newValue = event.currentTarget.value; - setValueInput(newValue); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - const listLine = selectedLines.length - ? selectedLines - : station?.lines; - if (listLine?.length) { - socket?.emit("write_command_line_from_web", { - lineIds: listLine.map((line) => line.id), - stationId: station.id, - command: valueInput + "\n", - }); - // setTimeout(() => { - // socket?.emit("write_command_line_from_web", { - // lineIds: listLine.map((line) => line.id), - // stationId: station.id, - // command: " \n", - // }); - // }, 1000); - } - setValueInput(""); - } - }} - rightSectionPointerEvents="all" - rightSection={ - setValueInput("")} - style={{ - display: valueInput ? undefined : "none", - }} - /> - } - /> - - - - - { - const lines = station.lines.filter( - (line) => - !line?.userOpenCLI || - line?.userOpenCLI === user?.userName - ); - if (selectedLines.length !== lines.length) { - setSelectedLines(lines); - lines.forEach((line) => { - socket?.emit("open_cli", { - lineId: line.id, - stationId: line.stationId || line.station_id, - userEmail: user?.email, - userName: user?.userName, - }); - }); - } else { - lines.forEach((line) => { - socket?.emit("close_cli", { - lineId: line?.id, - stationId: line.stationId || line.station_id, - }); - }); - setSelectedLines([]); - } - }} - /> - { - // setSelectedLines([]); - setIsDisable(true); - setTimeout(() => { - setIsDisable(false); - }, 5000); - }} - /> - - + > + + Line {el.lineNumber} + { + setSelectedLines( + selectedLines.filter( + (line) => line.id !== el.id + ) + ); + socket?.emit("close_cli", { + lineId: el?.id, + stationId: el.stationId || el.station_id, + }); + }} + /> + + + ))} + + + + + - - - { + const listLine = selectedLines.length + ? selectedLines + : station?.lines; + if (listLine.length) { + socket?.emit("write_command_line_from_web", { + lineIds: listLine.map((line) => line.id), + stationId: station.id, + command: "spam_break", + }); + setIsDisable(true); + setTimeout(() => { + setIsDisable(false); + }, 5000); + } }} > - {scenarios.map((el, i) => ( - - !el?.userEmailOpenCLI || - el?.userEmailOpenCLI === user?.email - )} - isDisable={ - isDisable || - selectedLines.filter( - (el) => - !el?.userEmailOpenCLI || - el?.userEmailOpenCLI === user?.email - ).length === 0 + Send Break + + + + { + const newValue = event.currentTarget.value; + setValueInput(newValue); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + const listLine = selectedLines.length + ? selectedLines + : station?.lines; + if (listLine?.length) { + socket?.emit("write_command_line_from_web", { + lineIds: listLine.map((line) => line.id), + stationId: station.id, + command: valueInput + "\n", + }); + // setTimeout(() => { + // socket?.emit("write_command_line_from_web", { + // lineIds: listLine.map((line) => line.id), + // stationId: station.id, + // command: " \n", + // }); + // }, 1000); } - onClick={() => { - // setSelectedLines([]); - setIsDisable(true); - setTimeout(() => { - setIsDisable(false); - }, 5000); + setValueInput(""); + } + }} + rightSectionPointerEvents="all" + rightSection={ + setValueInput("")} + style={{ + display: valueInput ? undefined : "none", }} - scenario={el} /> - ))} - - - - + } + /> + + + + + { + const lines = station.lines.filter( + (line) => + !line?.userOpenCLI || + line?.userOpenCLI === user?.userName + ); + if (selectedLines.length !== lines.length) { + setSelectedLines(lines); + lines.forEach((line) => { + socket?.emit("open_cli", { + lineId: line.id, + stationId: line.stationId || line.station_id, + userEmail: user?.email, + userName: user?.userName, + }); + }); + } else { + lines.forEach((line) => { + socket?.emit("close_cli", { + lineId: line?.id, + stationId: line.stationId || line.station_id, + }); + }); + setSelectedLines([]); + } + }} + /> + { + // setSelectedLines([]); + setIsDisable(true); + setTimeout(() => { + setIsDisable(false); + }, 5000); + }} + /> + + + + + + + {scenarios.map((el, i) => ( + + !el?.userEmailOpenCLI || + el?.userEmailOpenCLI === user?.email + )} + isDisable={ + isDisable || + selectedLines.filter( + (el) => + !el?.userEmailOpenCLI || + el?.userEmailOpenCLI === user?.email + ).length === 0 + } + onClick={() => { + // setSelectedLines([]); + setIsDisable(true); + setTimeout(() => { + setIsDisable(false); + }, 5000); + }} + scenario={el} + /> + ))} + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + ); }; diff --git a/FRONTEND/src/components/ButtonAction.tsx b/FRONTEND/src/components/ButtonAction.tsx index 207a3d8..7140b6b 100644 --- a/FRONTEND/src/components/ButtonAction.tsx +++ b/FRONTEND/src/components/ButtonAction.tsx @@ -68,42 +68,42 @@ export const ButtonDPELP = ({ { expect: "", send: "show diag", - delay: "2000", + delay: "1500", repeat: "1", note: "", }, { expect: "", send: "show post", - delay: "3000", + delay: "1500", repeat: "1", note: "", }, { expect: "", send: "show env all", - delay: "3000", + delay: "1500", repeat: "1", note: "", }, { expect: "", send: "show license", - delay: "3000", + delay: "1500", repeat: "1", note: "", }, { expect: "", send: "show log", - delay: "3000", + delay: "1500", repeat: "1", note: "", }, { expect: "", send: "show platform", - delay: "3000", + delay: "1500", repeat: "1", note: "", }, diff --git a/FRONTEND/src/components/CardLine.tsx b/FRONTEND/src/components/CardLine.tsx index 1dd267e..ba42c9a 100644 --- a/FRONTEND/src/components/CardLine.tsx +++ b/FRONTEND/src/components/CardLine.tsx @@ -37,6 +37,7 @@ const CardLine = ({ }, []); const [isDisabled, setIsDisabled] = useState(false); const [valueBaud, setValueBaud] = useState(""); + const [focusTerminal, setFocusTerminal] = useState(false); useEffect(() => { if ( @@ -109,6 +110,7 @@ const CardLine = ({ } } else { setSelectedLines((pre) => [...pre, line]); + setFocusTerminal(true); socket?.emit("open_cli", { lineId: line.id, stationId: line.stationId || line.station_id, @@ -177,6 +179,10 @@ const CardLine = ({ navigator.clipboard.writeText( `PID: ${line?.inventory?.pid || ""} | SN: ${ line?.inventory?.sn || "" + } | Ticket: ${ + line?.tickets && line?.tickets?.length > 0 + ? line?.tickets[0].description + : "" }` ); }} @@ -541,6 +547,7 @@ const CardLine = ({ handleClick(); }} onBlur={() => { + setFocusTerminal(false); if ( !selectedLines.find((value) => value.id === line?.id) && line?.userOpenCLI === user?.userName @@ -550,6 +557,7 @@ const CardLine = ({ stationId: line.stationId || line.station_id, }); }} + focusTerminal={focusTerminal} /> diff --git a/FRONTEND/src/components/ModalTerminal.tsx b/FRONTEND/src/components/ModalTerminal.tsx index 5a503cf..bb6bc9c 100644 --- a/FRONTEND/src/components/ModalTerminal.tsx +++ b/FRONTEND/src/components/ModalTerminal.tsx @@ -55,7 +55,6 @@ const ModalTerminal = ({ ? JSON.parse(localStorage.getItem("user") || "") : null; }, []); - const [inputTicket, setInputTicket] = useState(""); const [isDisable, setIsDisable] = useState(false); const [isDisableTicket, setIsDisableTicket] = useState(false); const [latestTicket, setLatestTicket] = useState({ @@ -116,7 +115,7 @@ const ModalTerminal = ({ }} > - @{item?.userName}: + {item?.status === "closed" ? "*" : ""}@{item?.userName}: + el.id === dataTicket.id ? res.data.data : el + ), + stationId: Number(stationItem?.id), + }); return; } } catch (error) { @@ -308,7 +318,7 @@ const ModalTerminal = ({ > - + @@ -324,7 +334,7 @@ const ModalTerminal = ({ )} - + BAUD: @@ -332,7 +342,7 @@ const ModalTerminal = ({ - + PID: {line?.inventory?.pid || ""} @@ -345,13 +355,13 @@ const ModalTerminal = ({ )} - + SN: {line?.inventory?.sn || ""} - + IOS: {""} @@ -543,7 +553,6 @@ const ModalTerminal = ({ variant="filled" color="orange" size="xs" - radius="md" onClick={() => { socket?.emit("write_command_line_from_web", { lineIds: [line?.id], @@ -648,7 +657,11 @@ const ModalTerminal = ({ - + {renderHistory(latestTicket)} @@ -658,14 +671,16 @@ const ModalTerminal = ({ boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)", }} placeholder={"Input description ticket"} - value={inputTicket} + value={dataTicket.description || ""} onChange={(event) => { - const newValue = event.currentTarget.value; - setInputTicket(newValue); + setDataTicket((pre) => ({ + ...pre, + description: event.target.value, + })); }} onKeyDown={(event) => { - if (event.key === "Enter") { - setInputTicket(""); + if (event.key === "Enter" && dataTicket.description) { + setDataTicket((pre) => ({ ...pre, description: "" })); if (dataTicket?.status === "closed") { handleCreate(); } else handleUpdate("open"); @@ -679,9 +694,11 @@ const ModalTerminal = ({ rightSection={ setInputTicket("")} + onClick={() => + setDataTicket((pre) => ({ ...pre, description: "" })) + } style={{ - display: inputTicket ? undefined : "none", + display: dataTicket?.description ? undefined : "none", }} /> } @@ -691,12 +708,13 @@ const ModalTerminal = ({ {dataTicket?.status === "closed" ? ( ) : (