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:
nguyentrungthat 2025-11-19 13:57:52 +07:00
parent d908cf204c
commit c36b9f69df
11 changed files with 501 additions and 320 deletions

View File

@ -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',

View File

@ -448,6 +448,10 @@ export class WebSocketIo {
}
}
})
socket.on('update_ticket', async (data) => {
io.emit('update_ticket', data)
})
})
socketServer.listen(SOCKET_IO_PORT, () => {

View File

@ -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')

View File

@ -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",

View File

@ -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",

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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: "",
},

View File

@ -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>

View File

@ -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(() => {

View File

@ -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) => {