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" ? ( ) : (