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'.
This commit is contained in:
parent
d908cf204c
commit
c36b9f69df
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -448,6 +448,10 @@ export class WebSocketIo {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('update_ticket', async (data) => {
|
||||
io.emit('update_ticket', data)
|
||||
})
|
||||
})
|
||||
|
||||
socketServer.listen(SOCKET_IO_PORT, () => {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ function App() {
|
|||
const [usersConnecting, setUsersConnecting] = useState<TUser[]>([]);
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<ScrollArea h={"73vh"}>
|
||||
<ScrollArea h={expandedBottomBar ? "73vh" : "85vh"}>
|
||||
{station.lines.length > 8 ? (
|
||||
<Grid
|
||||
style={{
|
||||
|
|
@ -492,6 +506,7 @@ function App() {
|
|||
setIsLogModalOpen={setIsLogModalOpen}
|
||||
setTestLogContent={setTestLogContent}
|
||||
scenarios={scenarios}
|
||||
setExpanded={setExpandedBottomBar}
|
||||
/>
|
||||
</Flex>
|
||||
</Tabs.Panel>
|
||||
|
|
|
|||
|
|
@ -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<boolean>) => void;
|
||||
setTestLogContent: (value: React.SetStateAction<string>) => void;
|
||||
scenarios: IScenario[];
|
||||
setExpanded: (value: React.SetStateAction<boolean>) => 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<string>("");
|
||||
const [activeTabBottom, setActiveBottom] = useState<string>("command");
|
||||
const [isExpand, setIsExpand] = useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col span={1}></Grid.Col>
|
||||
<Grid.Col span={10}>
|
||||
<Tabs
|
||||
defaultValue="command"
|
||||
orientation="vertical"
|
||||
value={activeTabBottom}
|
||||
onChange={(val) => {
|
||||
setActiveBottom(val || "command");
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{
|
||||
height: isExpand ? "15vh" : 0,
|
||||
y: 0, // đẩy xuống khi thu nhỏ
|
||||
}}
|
||||
transition={{ type: "spring", stiffness: 180, damping: 20 }}
|
||||
style={{
|
||||
width: "100%",
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Box style={{ position: "relative" }}>
|
||||
<ActionIcon
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: isExpand ? -4 : -24,
|
||||
left: "50%",
|
||||
translate: "-19px 0",
|
||||
backgroundColor: "#e3e0e0",
|
||||
width: "55px",
|
||||
}}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setIsExpand((prev) => !prev);
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
className={classes.containerBottom}
|
||||
style={{ height: "14vh" }}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab
|
||||
style={{
|
||||
backgroundColor: activeTabBottom === "command" ? "#c8d9fd" : "",
|
||||
fontSize: "13px",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "8px",
|
||||
{isExpand ? (
|
||||
<IconCaretDown color="green" />
|
||||
) : (
|
||||
<IconCaretUp color="green" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
<Grid>
|
||||
<Grid.Col span={1}></Grid.Col>
|
||||
<Grid.Col span={10}>
|
||||
<Tabs
|
||||
defaultValue="command"
|
||||
orientation="vertical"
|
||||
value={activeTabBottom}
|
||||
onChange={(val) => {
|
||||
setActiveBottom(val || "command");
|
||||
}}
|
||||
value="command"
|
||||
className={classes.containerBottom}
|
||||
style={{ height: "14vh" }}
|
||||
>
|
||||
Command Line
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
style={{
|
||||
backgroundColor: activeTabBottom === "apc" ? "#c8d9fd" : "",
|
||||
fontSize: "13px",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "8px",
|
||||
}}
|
||||
value="apc"
|
||||
>
|
||||
APC
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
style={{
|
||||
backgroundColor: activeTabBottom === "switch" ? "#c8d9fd" : "",
|
||||
fontSize: "13px",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "8px",
|
||||
}}
|
||||
value="switch"
|
||||
>
|
||||
Switch
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab
|
||||
style={{
|
||||
backgroundColor:
|
||||
activeTabBottom === "command" ? "#c8d9fd" : "",
|
||||
fontSize: "13px",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "8px",
|
||||
}}
|
||||
value="command"
|
||||
>
|
||||
Command Line
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
style={{
|
||||
backgroundColor: activeTabBottom === "apc" ? "#c8d9fd" : "",
|
||||
fontSize: "13px",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "8px",
|
||||
}}
|
||||
value="apc"
|
||||
>
|
||||
APC
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
style={{
|
||||
backgroundColor:
|
||||
activeTabBottom === "switch" ? "#c8d9fd" : "",
|
||||
fontSize: "13px",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "8px",
|
||||
}}
|
||||
value="switch"
|
||||
>
|
||||
Switch
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="command" p={"xs"}>
|
||||
<Flex justify={"space-between"}>
|
||||
<ScrollArea h={"10vh"}>
|
||||
<Flex wrap={"wrap"} gap={"xs"} w={"400px"}>
|
||||
{selectedLines.map((el) => (
|
||||
<Box
|
||||
key={el.id}
|
||||
style={{
|
||||
paddingLeft: "4px",
|
||||
height: "30px",
|
||||
width: "80px",
|
||||
backgroundColor: "#d4e3ff",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
<Flex align={"center"} justify={"center"} gap={"4px"}>
|
||||
<Text fz={"12px"}>Line {el.lineNumber}</Text>
|
||||
<CloseButton
|
||||
style={{ minWidth: "24px" }}
|
||||
aria-label="Clear input"
|
||||
onClick={() => {
|
||||
setSelectedLines(
|
||||
selectedLines.filter((line) => line.id !== el.id)
|
||||
);
|
||||
socket?.emit("close_cli", {
|
||||
lineId: el?.id,
|
||||
stationId: el.stationId || el.station_id,
|
||||
});
|
||||
<Tabs.Panel value="command" p={"xs"}>
|
||||
<Flex justify={"space-between"}>
|
||||
<ScrollArea h={"10vh"}>
|
||||
<Flex wrap={"wrap"} gap={"xs"} w={"400px"}>
|
||||
{selectedLines.map((el) => (
|
||||
<Box
|
||||
key={el.id}
|
||||
style={{
|
||||
paddingLeft: "4px",
|
||||
height: "30px",
|
||||
width: "80px",
|
||||
backgroundColor: "#d4e3ff",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
<Box pl={"md"} pr={"md"}>
|
||||
<Flex justify={"space-between"} mb={"xs"}>
|
||||
<Flex></Flex>
|
||||
<Button
|
||||
fw={400}
|
||||
disabled={isDisable || selectedLines.length === 0}
|
||||
variant="filled"
|
||||
color="orange"
|
||||
size="xs"
|
||||
radius="md"
|
||||
onClick={() => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send Break
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Input
|
||||
style={{
|
||||
width: "30vw",
|
||||
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
placeholder={"Send command to port(s)"}
|
||||
value={valueInput}
|
||||
onChange={(event) => {
|
||||
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={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValueInput("")}
|
||||
style={{
|
||||
display: valueInput ? undefined : "none",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box style={{ width: "220px" }}>
|
||||
<Flex align={"center"} wrap={"wrap"} gap={"xs"}>
|
||||
<ButtonSelect
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
station={station}
|
||||
userName={user?.userName}
|
||||
onClick={() => {
|
||||
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([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ButtonDPELP
|
||||
socket={socket}
|
||||
selectedLines={selectedLines}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
onClick={() => {
|
||||
// setSelectedLines([]);
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 5000);
|
||||
}}
|
||||
/>
|
||||
<Menu shadow="md" position="top">
|
||||
<Menu.Target>
|
||||
>
|
||||
<Flex align={"center"} justify={"center"} gap={"4px"}>
|
||||
<Text fz={"12px"}>Line {el.lineNumber}</Text>
|
||||
<CloseButton
|
||||
style={{ minWidth: "24px" }}
|
||||
aria-label="Clear input"
|
||||
onClick={() => {
|
||||
setSelectedLines(
|
||||
selectedLines.filter(
|
||||
(line) => line.id !== el.id
|
||||
)
|
||||
);
|
||||
socket?.emit("close_cli", {
|
||||
lineId: el?.id,
|
||||
stationId: el.stationId || el.station_id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
<Box pl={"md"} pr={"md"}>
|
||||
<Flex justify={"space-between"} mb={"xs"}>
|
||||
<Flex></Flex>
|
||||
<Button
|
||||
fw={400}
|
||||
disabled={isDisable || selectedLines.length === 0}
|
||||
variant="filled"
|
||||
color="yellow"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {}}
|
||||
>
|
||||
Scenario
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Box
|
||||
px="xs"
|
||||
py="sm"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: "12px",
|
||||
color="orange"
|
||||
size="xs"
|
||||
radius="md"
|
||||
onClick={() => {
|
||||
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) => (
|
||||
<ButtonScenario
|
||||
key={i}
|
||||
socket={socket}
|
||||
selectedLines={selectedLines.filter(
|
||||
(el) =>
|
||||
!el?.userEmailOpenCLI ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
)}
|
||||
isDisable={
|
||||
isDisable ||
|
||||
selectedLines.filter(
|
||||
(el) =>
|
||||
!el?.userEmailOpenCLI ||
|
||||
el?.userEmailOpenCLI === user?.email
|
||||
).length === 0
|
||||
Send Break
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Input
|
||||
style={{
|
||||
width: "30vw",
|
||||
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
placeholder={"Send command to port(s)"}
|
||||
value={valueInput}
|
||||
onChange={(event) => {
|
||||
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={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValueInput("")}
|
||||
style={{
|
||||
display: valueInput ? undefined : "none",
|
||||
}}
|
||||
scenario={el}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<DrawerLogs
|
||||
socket={socket}
|
||||
isLogModalOpen={isLogModalOpen}
|
||||
setIsLogModalOpen={setIsLogModalOpen}
|
||||
testLogContent={testLogContent}
|
||||
setTestLogContent={setTestLogContent}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box style={{ width: "220px" }}>
|
||||
<Flex align={"center"} wrap={"wrap"} gap={"xs"}>
|
||||
<ButtonSelect
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
station={station}
|
||||
userName={user?.userName}
|
||||
onClick={() => {
|
||||
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([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ButtonDPELP
|
||||
socket={socket}
|
||||
selectedLines={selectedLines}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
onClick={() => {
|
||||
// setSelectedLines([]);
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 5000);
|
||||
}}
|
||||
/>
|
||||
<Menu shadow="md" position="top">
|
||||
<Menu.Target>
|
||||
<Button
|
||||
fw={400}
|
||||
disabled={isDisable || selectedLines.length === 0}
|
||||
variant="filled"
|
||||
color="yellow"
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {}}
|
||||
>
|
||||
Scenario
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Box
|
||||
px="xs"
|
||||
py="sm"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: "12px",
|
||||
}}
|
||||
>
|
||||
{scenarios.map((el, i) => (
|
||||
<ButtonScenario
|
||||
key={i}
|
||||
socket={socket}
|
||||
selectedLines={selectedLines.filter(
|
||||
(el) =>
|
||||
!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}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<DrawerLogs
|
||||
socket={socket}
|
||||
isLogModalOpen={isLogModalOpen}
|
||||
setIsLogModalOpen={setIsLogModalOpen}
|
||||
testLogContent={testLogContent}
|
||||
setTestLogContent={setTestLogContent}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="apc" ps={"xs"}>
|
||||
<DrawerAPCControl socket={socket} stationAPI={station} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="switch" ps={"xs"}>
|
||||
<DrawerSwitchControl socket={socket} stationAPI={station} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={1}></Grid.Col>
|
||||
</Grid>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="apc" ps={"xs"}>
|
||||
<DrawerAPCControl socket={socket} stationAPI={station} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="switch" ps={"xs"}>
|
||||
<DrawerSwitchControl socket={socket} stationAPI={station} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={1}></Grid.Col>
|
||||
</Grid>
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const CardLine = ({
|
|||
}, []);
|
||||
const [isDisabled, setIsDisabled] = useState<boolean>(false);
|
||||
const [valueBaud, setValueBaud] = useState<string>("");
|
||||
const [focusTerminal, setFocusTerminal] = useState<boolean>(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}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ const ModalTerminal = ({
|
|||
? JSON.parse(localStorage.getItem("user") || "")
|
||||
: null;
|
||||
}, []);
|
||||
const [inputTicket, setInputTicket] = useState<string>("");
|
||||
const [isDisable, setIsDisable] = useState<boolean>(false);
|
||||
const [isDisableTicket, setIsDisableTicket] = useState<boolean>(false);
|
||||
const [latestTicket, setLatestTicket] = useState<TDataTicket>({
|
||||
|
|
@ -116,7 +115,7 @@ const ModalTerminal = ({
|
|||
}}
|
||||
>
|
||||
<Text style={{ fontSize: "14px", fontWeight: "bold" }} fw={600}>
|
||||
@{item?.userName}:
|
||||
{item?.status === "closed" ? "*" : ""}@{item?.userName}:
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
|
|
@ -168,14 +167,18 @@ const ModalTerminal = ({
|
|||
description: dataTicket.description.trim(),
|
||||
model: dataTicket.model.trim(),
|
||||
sn: dataTicket.sn.trim(),
|
||||
station_id: Number(dataTicket.station_id),
|
||||
station_id: Number(stationItem?.id),
|
||||
line_id: Number(line?.id),
|
||||
status: "open",
|
||||
userName: user?.userName,
|
||||
userId: user?.id,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axios.post(apiUrl + "api/ticket/create", payload);
|
||||
if (res.status) {
|
||||
// setDataTicket(res.data)
|
||||
// setLatestTicket(res.data);
|
||||
// setDataTicket({ ...res.data, description: "" });
|
||||
|
||||
// notifications.show({
|
||||
// title: 'Success',
|
||||
|
|
@ -183,10 +186,11 @@ const ModalTerminal = ({
|
|||
// color: 'green',
|
||||
// })
|
||||
|
||||
// socket?.emit(
|
||||
// "create_ticket",
|
||||
// payload,
|
||||
// )
|
||||
socket?.emit("update_ticket", {
|
||||
lineId: Number(line?.id),
|
||||
data: [res.data.data, ...(line?.tickets || [])],
|
||||
stationId: Number(stationItem?.id),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -218,38 +222,44 @@ const ModalTerminal = ({
|
|||
: "",
|
||||
model: dataTicket.model.trim(),
|
||||
sn: dataTicket.sn.trim(),
|
||||
station_id: Number(dataTicket.station_id),
|
||||
station_id: Number(stationItem?.id),
|
||||
line_id: Number(line?.id),
|
||||
status: status,
|
||||
userName: user?.userName,
|
||||
userId: user?.id,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axios.put(
|
||||
`${apiUrl + "api/ticket/create" + "/" + dataTicket.id}`,
|
||||
const res = await axios.post(
|
||||
`${apiUrl + "api/ticket/update" + "/" + dataTicket.id}`,
|
||||
payload
|
||||
);
|
||||
if (res.status) {
|
||||
if (res?.data?.status !== "closed")
|
||||
setDataTicket({ ...res.data, description: "" });
|
||||
else
|
||||
setDataTicket({
|
||||
id: 0,
|
||||
description: "",
|
||||
model: latestTicket.model.trim(),
|
||||
sn: latestTicket.sn.trim(),
|
||||
station_id: latestTicket.station_id,
|
||||
history: "",
|
||||
status: "open",
|
||||
});
|
||||
// if (res?.data?.status !== "closed")
|
||||
// setDataTicket({ ...res.data, description: "" });
|
||||
// else
|
||||
// setDataTicket({
|
||||
// id: 0,
|
||||
// description: "",
|
||||
// model: latestTicket.model.trim(),
|
||||
// sn: latestTicket.sn.trim(),
|
||||
// station_id: latestTicket.station_id,
|
||||
// history: "",
|
||||
// status: "open",
|
||||
// });
|
||||
|
||||
// notifications.show({
|
||||
// title: 'Success',
|
||||
// message: res.message,
|
||||
// color: 'green',
|
||||
// })
|
||||
// socket?.emit(
|
||||
// SOCKET_EVENTS.RELOAD_TICKET.RELOAD_TICKET_FROM_WEB,
|
||||
// payload,
|
||||
// )
|
||||
socket?.emit("update_ticket", {
|
||||
lineId: Number(line?.id),
|
||||
data: line?.tickets?.map((el) =>
|
||||
el.id === dataTicket.id ? res.data.data : el
|
||||
),
|
||||
stationId: Number(stationItem?.id),
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -308,7 +318,7 @@ const ModalTerminal = ({
|
|||
>
|
||||
<Grid>
|
||||
<Grid.Col span={2}>
|
||||
<Flex justify={"space-between"} direction={"column"} h={"100%"}>
|
||||
<Flex justify={"space-between"} direction={"column"} h={"95%"}>
|
||||
<Box>
|
||||
<Flex gap={"sm"} justify={"center"} align={"center"}>
|
||||
<Text size="xl">
|
||||
|
|
@ -324,7 +334,7 @@ const ModalTerminal = ({
|
|||
)}
|
||||
</Flex>
|
||||
<Flex mt="4px">
|
||||
<Text size="md" w={"50px"}>
|
||||
<Text size="md" mr="6px">
|
||||
BAUD:
|
||||
</Text>
|
||||
<Text size="md">
|
||||
|
|
@ -332,7 +342,7 @@ const ModalTerminal = ({
|
|||
</Text>
|
||||
</Flex>
|
||||
<Flex mt="4px">
|
||||
<Text size="md" w={"50px"}>
|
||||
<Text size="md" mr="6px">
|
||||
PID:
|
||||
</Text>
|
||||
<Text size="md">{line?.inventory?.pid || ""}</Text>
|
||||
|
|
@ -345,13 +355,13 @@ const ModalTerminal = ({
|
|||
)}
|
||||
</Flex>
|
||||
<Flex mt="4px">
|
||||
<Text size="md" w={"50px"}>
|
||||
<Text size="md" mr="6px">
|
||||
SN:
|
||||
</Text>
|
||||
<Text size="md">{line?.inventory?.sn || ""}</Text>
|
||||
</Flex>
|
||||
<Flex mt="4px">
|
||||
<Text size="md" mr={"sm"} fw={"bold"} w={"50px"}>
|
||||
<Text size="md" mr={"6px"} fw={"bold"}>
|
||||
IOS:
|
||||
</Text>
|
||||
<Text size="md">{""}</Text>
|
||||
|
|
@ -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 = ({
|
|||
</Flex>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<ScrollArea h={600} style={{ border: "1px solid #ccc" }} p={"4px"}>
|
||||
<ScrollArea
|
||||
h={"65vh"}
|
||||
style={{ border: "1px solid #ccc" }}
|
||||
p={"4px"}
|
||||
>
|
||||
{renderHistory(latestTicket)}
|
||||
</ScrollArea>
|
||||
<Box mt={"8px"}>
|
||||
|
|
@ -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={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setInputTicket("")}
|
||||
onClick={() =>
|
||||
setDataTicket((pre) => ({ ...pre, description: "" }))
|
||||
}
|
||||
style={{
|
||||
display: inputTicket ? undefined : "none",
|
||||
display: dataTicket?.description ? undefined : "none",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
@ -691,12 +708,13 @@ const ModalTerminal = ({
|
|||
<Flex justify={"end"} mt={"4px"}>
|
||||
{dataTicket?.status === "closed" ? (
|
||||
<Button
|
||||
disabled={isDisableTicket}
|
||||
disabled={isDisableTicket || !dataTicket.description}
|
||||
mr={"8px"}
|
||||
fw={400}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setDataTicket((pre) => ({ ...pre, description: "" }));
|
||||
handleCreate();
|
||||
setIsDisableTicket(true);
|
||||
setTimeout(() => {
|
||||
|
|
@ -708,7 +726,7 @@ const ModalTerminal = ({
|
|||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
disabled={isDisableTicket}
|
||||
disabled={isDisableTicket || !dataTicket.description}
|
||||
mr={"8px"}
|
||||
fw={400}
|
||||
variant="outline"
|
||||
|
|
@ -725,13 +743,18 @@ const ModalTerminal = ({
|
|||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={isDisableTicket || dataTicket?.status === "closed"}
|
||||
disabled={
|
||||
isDisableTicket ||
|
||||
dataTicket?.status === "closed" ||
|
||||
!dataTicket.description
|
||||
}
|
||||
mr={"8px"}
|
||||
fw={400}
|
||||
variant="outline"
|
||||
color="orange"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setDataTicket((pre) => ({ ...pre, description: "" }));
|
||||
handleUpdate("issue");
|
||||
setIsDisableTicket(true);
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ interface TerminalCLIProps {
|
|||
fontSize?: number;
|
||||
miniSize?: boolean;
|
||||
loadingContent?: boolean;
|
||||
focusTerminal?: boolean;
|
||||
}
|
||||
|
||||
const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||
|
|
@ -46,6 +47,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
loadingContent = false,
|
||||
onFocus,
|
||||
onBlur,
|
||||
focusTerminal,
|
||||
}) => {
|
||||
const xtermRef = useRef<HTMLDivElement>(null);
|
||||
const terminal = useRef<Terminal>(null);
|
||||
|
|
@ -177,6 +179,12 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusTerminal && terminal.current) {
|
||||
terminal.current?.focus();
|
||||
}
|
||||
}, [focusTerminal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -185,7 +193,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
height: "100%",
|
||||
backgroundColor: "black",
|
||||
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||
maxHeight: customStyle.maxHeight ?? "73vh",
|
||||
maxHeight: customStyle.maxHeight ?? "70vh",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
|
@ -206,8 +214,8 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
paddingLeft: customStyle.paddingLeft ?? "10px",
|
||||
paddingBottom: customStyle.paddingBottom ?? "10px",
|
||||
fontSize: customStyle.fontSize ?? "9px",
|
||||
maxHeight: customStyle.maxHeight ?? "73vh",
|
||||
height: customStyle.height ?? "73vh",
|
||||
maxHeight: customStyle.maxHeight ?? "70vh",
|
||||
height: customStyle.height ?? "70vh",
|
||||
padding: customStyle.padding ?? "4px",
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue