Update modal Scenario

This commit is contained in:
nguyentrungthat 2025-12-15 17:10:50 +07:00
parent 5a8cf4b999
commit 2729feed4e
7 changed files with 915 additions and 630 deletions

View File

@ -4,7 +4,8 @@ import type { HttpContext } from '@adonisjs/core/http'
export default class BrandsController {
// GET /models
async index({}: HttpContext) {
return await Brand.all()
const brands = await Brand.all()
return brands.sort((a, b) => a.id - b.id)
}
// POST /models

View File

@ -4,7 +4,8 @@ import type { HttpContext } from '@adonisjs/core/http'
export default class CategoriesController {
// GET /models
async index({}: HttpContext) {
return await Category.all()
const categories = await Category.all()
return categories.sort((a, b) => a.id - b.id)
}
// POST /models

View File

@ -5,9 +5,34 @@ import "./App.css";
import classes from "./App.module.css";
import componentClasses from "./components/Component.module.css";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Tabs, Text, Container, Flex, MantineProvider, Grid, ScrollArea, LoadingOverlay } from "@mantine/core";
import type { IScenario, ReceivedFile, ResponseData, TLine, TStation, TUser } from "./untils/types";
import {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Tabs,
Text,
Container,
Flex,
MantineProvider,
Grid,
ScrollArea,
LoadingOverlay,
} from "@mantine/core";
import type {
IScenario,
ReceivedFile,
ResponseData,
TBrands,
TCategories,
TLine,
TStation,
TUser,
} from "./untils/types";
import axios from "axios";
import CardLine from "./components/CardLine";
import { SocketProvider, useSocket } from "./context/SocketContext";
@ -44,7 +69,10 @@ const chunkArray = <T,>(array: T[], size: number): T[][] => {
*/
function App() {
const user = useMemo(() => {
return localStorage.getItem("user") && isJsonString(localStorage.getItem("user")) ? JSON.parse(localStorage.getItem("user") || "") : null;
return localStorage.getItem("user") &&
isJsonString(localStorage.getItem("user"))
? JSON.parse(localStorage.getItem("user") || "")
: null;
}, []);
if (!user) {
@ -72,6 +100,8 @@ function App() {
const [activeTabBottom, setActiveTabBottom] = useState<string>("command");
const lineBuffersRef = useRef(new Map<number, string>());
const flushScheduledRef = useRef(false);
const [listBrands, setListBrands] = useState<TBrands[]>([]);
const [listCategories, setListCategories] = useState<TCategories[]>([]);
const connectApcSwitch = (station: TStation) => {
if (station?.apc_1_ip && station?.apc_1_port) {
@ -105,7 +135,9 @@ function App() {
setStations(
response.data.map((station) => {
connectApcSwitch(station);
const lines = (station?.lines || []).sort((a: TLine, b: TLine) => a?.lineNumber - b?.lineNumber);
const lines = (station?.lines || []).sort(
(a: TLine, b: TLine) => a?.lineNumber - b?.lineNumber
);
return { ...station, lines };
})
);
@ -130,18 +162,58 @@ function App() {
}
};
// function get list brand
const getBrands = async () => {
try {
const response = await axios.get(apiUrl + "api/brands");
if (response.data) {
if (response.data && Array.isArray(response.data)) {
setListBrands(response.data);
}
}
} catch (error) {
console.log("Error get brand", error);
}
};
// function get list brand
const getCategories = async () => {
try {
const response = await axios.get(apiUrl + "api/categories");
if (response.data && Array.isArray(response.data)) {
setListCategories(response.data);
}
} catch (error) {
console.log("Error get brand", error);
}
};
useEffect(() => {
if (!socket) return;
getStation();
getScenarios();
getBrands();
getCategories();
}, [socket]);
useEffect(() => {
if (!socket || !stations?.length) return;
socket.on("line_connected", (data) => updateValueLineStation(data?.lineId, { status: data.status, connecting: false }, data?.stationId));
socket.on("line_connected", (data) =>
updateValueLineStation(
data?.lineId,
{ status: data.status, connecting: false },
data?.stationId
)
);
socket.on("line_disconnected", (data) => updateValueLineStation(data?.lineId, { status: data.status, connecting: false }, data?.stationId));
socket.on("line_disconnected", (data) =>
updateValueLineStation(
data?.lineId,
{ status: data.status, connecting: false },
data?.stationId
)
);
socket?.on("line_output", (data) => {
const { lineId, data: text } = data;
@ -161,7 +233,11 @@ function App() {
});
socket?.on("line_error", (data) => {
updateValueLineStation(data?.lineId, { netOutput: data.error, connecting: false }, data?.stationId);
updateValueLineStation(
data?.lineId,
{ netOutput: data.error, connecting: false },
data?.stationId
);
});
socket?.on("init", (data) => {
@ -169,7 +245,11 @@ function App() {
// console.log(data);
setLoadingTerminal(true);
data.forEach((value) => {
updateValueLineStation(value?.id, { ...value, netOutput: value.output }, value?.stationId);
updateValueLineStation(
value?.id,
{ ...value, netOutput: value.output },
value?.stationId
);
});
}
});
@ -290,13 +370,21 @@ function App() {
socket?.on("line_connecting", (data) => {
setTimeout(() => {
updateValueLineStation(data?.lineId, { connecting: true }, data?.stationId);
updateValueLineStation(
data?.lineId,
{ connecting: true },
data?.stationId
);
}, 100);
});
socket?.on("running_scenario", (data) => {
setTimeout(() => {
updateValueLineStation(data?.lineId, { runningScenario: data?.title || "" }, data?.stationId);
updateValueLineStation(
data?.lineId,
{ runningScenario: data?.title || "" },
data?.stationId
);
}, 100);
});
@ -341,7 +429,8 @@ function App() {
flushScheduledRef.current = false;
}, []);
const updateValueLineStation = useCallback((lineId: number, updates: Partial<TLine>, stationId?: number) => {
const updateValueLineStation = useCallback(
(lineId: number, updates: Partial<TLine>, stationId?: number) => {
setStations((prevStations) =>
prevStations?.map((station: TStation) =>
station.id === stationId
@ -358,7 +447,8 @@ function App() {
lineNumber: lineItem.lineNumber,
line_number: lineItem.line_number,
...(isNetOutput && {
netOutput: (lineItem.netOutput || "") + (updates.netOutput || ""),
netOutput:
(lineItem.netOutput || "") + (updates.netOutput || ""),
output: updates.netOutput, // Nếu netOutput thì update luôn output
loadingOutput: lineItem.loadingOutput ? false : true,
}),
@ -379,15 +469,19 @@ function App() {
...prevSelected,
...updates,
...(isNetOutput && {
netOutput: (prevSelected.netOutput || "") + (updates.netOutput || ""),
netOutput:
(prevSelected.netOutput || "") + (updates.netOutput || ""),
output: updates.netOutput,
loadingOutput: prevSelected.loadingOutput ? false : true,
}),
};
});
}, []);
},
[]
);
const updateValueSelectedLine = useCallback((lineId: number, updates: Partial<TLine>) => {
const updateValueSelectedLine = useCallback(
(lineId: number, updates: Partial<TLine>) => {
// Update selectedLine nếu nó đang được chọn
setSelectedLine((prevSelected) => {
if (!prevSelected || prevSelected.id !== lineId) return prevSelected;
@ -398,13 +492,16 @@ function App() {
...prevSelected,
...updates,
...(isNetOutput && {
netOutput: (prevSelected.netOutput || "") + (updates.netOutput || ""),
netOutput:
(prevSelected.netOutput || "") + (updates.netOutput || ""),
output: updates.netOutput,
loadingOutput: prevSelected.loadingOutput ? false : true,
}),
};
});
}, []);
},
[]
);
// const getLine = (lineId: number, stationId: number) => {
// const station = stations?.find((sta) => sta.id === stationId);
@ -449,21 +546,28 @@ function App() {
scenarios={scenarios}
setScenarios={setScenarios}
panels={stations.map((station) => (
<Tabs.Panel className={classes.content} key={station.id} value={station.id.toString()} pt="md">
<Tabs.Panel
className={classes.content}
key={station.id}
value={station.id.toString()}
pt="md"
>
<Flex className={classes.containerMain}>
<Grid>
<Grid.Col
span={12}
style={{
borderRadius: 8,
}}>
}}
>
<ScrollArea
// h={expandedBottomBar ? "80vh" : "85vh"}
h={expandedBottomBar ? "73vh" : "83vh"}
type="scroll"
scrollbars="y"
style={{ overflowX: "hidden" }}
className={componentClasses.hideScrollBar}>
className={componentClasses.hideScrollBar}
>
{station.lines.length > 0 ? (
station.lines.length < 9 ? (
<Grid
@ -471,7 +575,8 @@ function App() {
style={{
margin: 0,
width: "100%",
}}>
}}
>
{station.lines.map((line, i) => (
<Grid.Col
key={line.id ?? i}
@ -481,7 +586,8 @@ function App() {
height: "100%",
maxWidth: "100%",
overflow: "hidden",
}}>
}}
>
<CardLine
socket={socket}
stationItem={station}
@ -489,7 +595,10 @@ function App() {
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
openTerminal={openTerminal}
loadTerminal={loadingTerminal && Number(station.id) === Number(activeTab)}
loadTerminal={
loadingTerminal &&
Number(station.id) === Number(activeTab)
}
scenarios={scenarios}
/>
</Grid.Col>
@ -506,11 +615,15 @@ function App() {
const leftChunks = chunkArray(leftLines, 2);
const rightChunks = chunkArray(rightLines, 2);
const numRows = Math.max(leftChunks.length, rightChunks.length);
const numRows = Math.max(
leftChunks.length,
rightChunks.length
);
return (
<>
{Array.from({ length: numRows }).map((_, rowIndex) => {
{Array.from({ length: numRows }).map(
(_, rowIndex) => {
const leftRow = leftChunks[rowIndex] || [];
const rightRow = rightChunks[rowIndex] || [];
@ -521,28 +634,38 @@ function App() {
style={{
margin: 0,
width: "100%",
}}>
}}
>
{/* Cột A */}
<Grid.Col span={6}>
<Grid gutter="md">
{leftRow.map((line, i) => (
<Grid.Col
key={line.id ?? `L-${rowIndex}-${i}`}
key={
line.id ?? `L-${rowIndex}-${i}`
}
span={6}
style={{
display: "flex",
height: "100%",
maxWidth: "100%",
overflow: "hidden",
}}>
}}
>
<CardLine
socket={socket}
stationItem={station}
line={line}
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
setSelectedLines={
setSelectedLines
}
openTerminal={openTerminal}
loadTerminal={loadingTerminal && Number(station.id) === Number(activeTab)}
loadTerminal={
loadingTerminal &&
Number(station.id) ===
Number(activeTab)
}
scenarios={scenarios}
/>
</Grid.Col>
@ -555,22 +678,31 @@ function App() {
<Grid gutter="md">
{rightRow.map((line, i) => (
<Grid.Col
key={line.id ?? `R-${rowIndex}-${i}`}
key={
line.id ?? `R-${rowIndex}-${i}`
}
span={6}
style={{
display: "flex",
height: "100%",
maxWidth: "100%",
overflow: "hidden",
}}>
}}
>
<CardLine
socket={socket}
stationItem={station}
line={line}
selectedLines={selectedLines}
setSelectedLines={setSelectedLines}
setSelectedLines={
setSelectedLines
}
openTerminal={openTerminal}
loadTerminal={loadingTerminal && Number(station.id) === Number(activeTab)}
loadTerminal={
loadingTerminal &&
Number(station.id) ===
Number(activeTab)
}
scenarios={scenarios}
/>
</Grid.Col>
@ -579,7 +711,8 @@ function App() {
</Grid.Col>
</Grid>
);
})}
}
)}
</>
);
})()
@ -610,6 +743,8 @@ function App() {
activeTabBottom={activeTabBottom}
setActiveTabBottom={setActiveTabBottom}
isExpand={expandedBottomBar}
listBrands={listBrands}
listCategories={listCategories}
/>
</Flex>
</Tabs.Panel>
@ -635,7 +770,9 @@ function App() {
setActive={setActiveTab}
active={activeTab}
onSendCommand={(value) => {
const listLine = selectedLines.length ? selectedLines : stations.find((el) => el.id === Number(activeTab))?.lines;
const listLine = selectedLines.length
? selectedLines
: stations.find((el) => el.id === Number(activeTab))?.lines;
if (listLine?.length) {
socket?.emit("write_command_line_from_web", {
lineIds: listLine.map((line) => line.id),
@ -651,6 +788,8 @@ function App() {
}, 1000);
}
}}
listBrands={listBrands}
listCategories={listCategories}
/>
<StationSetting
@ -692,13 +831,24 @@ function App() {
export default function Main() {
const user = useMemo(() => {
return localStorage.getItem("user") && isJsonString(localStorage.getItem("user")) ? JSON.parse(localStorage.getItem("user") || "") : null;
return localStorage.getItem("user") &&
isJsonString(localStorage.getItem("user"))
? JSON.parse(localStorage.getItem("user") || "")
: null;
}, []);
return (
<MantineProvider>
<SocketProvider>
<Suspense fallback={<LoadingOverlay visible={true} zIndex={1000} overlayProps={{ radius: "sm", blur: 1 }} />}>
<Suspense
fallback={
<LoadingOverlay
visible={true}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 1 }}
/>
}
>
<Notifications position="top-right" autoClose={5000} />
{user ? (
<App />

View File

@ -13,7 +13,14 @@ import {
} from "@mantine/core";
import { useEffect, useMemo, useRef, useState } from "react";
import classes from "./Component.module.css";
import type { IScenario, TLine, TStation, TUser } from "../untils/types";
import type {
IScenario,
TBrands,
TCategories,
TLine,
TStation,
TUser,
} from "../untils/types";
import type { Socket } from "socket.io-client";
import { ButtonDPELP, ButtonSelect } from "./ButtonAction";
import DrawerLogs from "./Drawer/DrawerLogs";
@ -47,6 +54,8 @@ interface TabsProps {
activeTabBottom: string;
setActiveTabBottom: (value: React.SetStateAction<string>) => void;
isExpand: boolean;
listBrands: TBrands[];
listCategories: TCategories[];
}
// Component cho từng Scenario Card
@ -379,6 +388,8 @@ const BottomToolBar = ({
activeTabBottom,
isExpand,
stationId,
listBrands,
listCategories,
}: TabsProps) => {
const user = useMemo(() => {
return localStorage.getItem("user") &&
@ -1067,6 +1078,8 @@ const BottomToolBar = ({
setScenarios={setScenarios}
externalOpened={openDrawerScenario}
onExternalClose={() => setOpenDrawerScenario(false)}
listBrands={listBrands}
listCategories={listCategories}
/>
</>
);

View File

@ -37,7 +37,13 @@ import {
IconUsersGroup,
} from "@tabler/icons-react";
import classes from "./Component.module.css";
import type { IScenario, TStation, TUser } from "../untils/types";
import type {
IScenario,
TBrands,
TCategories,
TStation,
TUser,
} from "../untils/types";
import type { Socket } from "socket.io-client";
import ModalHistory from "./Modal/ModalHistory";
import ModalConfig from "./Modal/ModalConfig";
@ -60,6 +66,8 @@ interface DraggableTabsProps {
onSendCommand: (value: string) => void;
scenarios: IScenario[];
setScenarios: (value: React.SetStateAction<IScenario[]>) => void;
listBrands: TBrands[];
listCategories: TCategories[];
}
function SortableTab({
@ -123,6 +131,8 @@ export default function DraggableTabs({
setActive,
scenarios,
setScenarios,
listBrands,
listCategories,
}: DraggableTabsProps) {
const user = useMemo(() => {
return localStorage.getItem("user") &&
@ -396,6 +406,8 @@ export default function DraggableTabs({
setScenarios={setScenarios}
externalOpened={openDrawerScenario}
onExternalClose={() => setOpenDrawerScenario(false)}
listBrands={listBrands}
listCategories={listCategories}
/>
</DndContext>
);

View File

@ -10,15 +10,23 @@ import {
Flex,
CloseButton,
Checkbox,
Select,
TagsInput,
} from "@mantine/core";
import classes from "../Component.module.css";
import TableRows from "./Scenario/TableRows";
import { useEffect, useState } from "react";
import { useForm } from "@mantine/form";
import DialogConfirm from "../DialogConfirm";
import type { IBodyScenario, IScenario } from "../../untils/types";
import type {
IBodyScenario,
IScenario,
TBrands,
TCategories,
} from "../../untils/types";
import axios from "axios";
import { notifications } from "@mantine/notifications";
import { isJsonString } from "../../untils/helper";
const apiUrl = import.meta.env.VITE_BACKEND_URL;
function ModalScenario({
@ -26,11 +34,15 @@ function ModalScenario({
setScenarios,
externalOpened,
onExternalClose,
listBrands,
listCategories,
}: {
scenarios: IScenario[];
setScenarios: (value: React.SetStateAction<IScenario[]>) => void;
externalOpened?: boolean;
onExternalClose?: () => void;
listBrands: TBrands[];
listCategories: TCategories[];
}) {
const [opened, { close }] = useDisclosure(false);
@ -60,9 +72,13 @@ function ModalScenario({
repeat: "1",
},
] as IBodyScenario[],
timeout: "30000",
timeout: "30",
isReboot: false,
send_result: false,
note: "",
series: [] as string[],
brandId: "",
categoryId: "",
},
validate: {
title: (value) => {
@ -129,6 +145,10 @@ function ModalScenario({
send_result: form.values.send_result,
body: body,
timeout: Number(form.values.timeout),
categoryId: Number(form.values.categoryId),
brandId: Number(form.values.brandId),
note: form.values.note,
series: form.values.series,
};
const url = isEdit ? "api/scenarios/update" : "api/scenarios/create";
const res = await axios.post(
@ -219,7 +239,7 @@ function ModalScenario({
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.6)",
zIndex: 100000,
zIndex: 100,
display: "flex",
alignItems: "center",
justifyContent: "center",
@ -248,13 +268,13 @@ function ModalScenario({
<Flex
justify="space-between"
align="center"
p="lg"
p="sm"
style={{
borderBottom: "1px solid #e9ecef",
flexShrink: 0,
}}
>
<Text fw={700} size="xl">
<Text fw={700} size="md">
{isEdit ? "✏️ Edit Scenarios" : " Add Scenarios"}
</Text>
<CloseButton size="lg" onClick={handleClose} />
@ -319,6 +339,25 @@ function ModalScenario({
"send_result",
scenario.send_result
);
form.setFieldValue("note", scenario.note);
form.setFieldValue(
"brandId",
scenario.brand_id
? scenario.brand_id.toString()
: scenario?.brandId?.toString()
);
form.setFieldValue(
"categoryId",
scenario.category_id
? scenario.category_id.toString()
: scenario.categoryId.toString()
);
form.setFieldValue(
"series",
isJsonString(scenario?.series)
? JSON.parse(scenario.series)
: []
);
}
}}
>
@ -425,6 +464,60 @@ function ModalScenario({
</Grid.Col>
</Grid>
</Box>
<Box>
<Grid>
<Grid.Col span={2}>
<Select
label="Brand"
placeholder="Select Brand"
data={listBrands?.map((el) => ({
value: el.id.toString(),
label: el.name,
}))}
value={form.values.brandId || null}
onChange={(value) =>
form.setFieldValue("brandId", value || "")
}
/>
</Grid.Col>
<Grid.Col span={2}>
<Select
label="Category"
placeholder="Select Category"
data={listCategories?.map((el) => ({
value: el.id.toString(),
label: el.name,
}))}
value={form.values.categoryId || null}
onChange={(value) =>
form.setFieldValue("categoryId", value || "")
}
/>
</Grid.Col>
<Grid.Col span={2}>
<TextInput
label="Note"
placeholder="Input note"
value={form.values.note}
onChange={(e) =>
form.setFieldValue("note", e.target.value)
}
required
/>
</Grid.Col>
<Grid.Col span={6}>
<TagsInput
label="Series"
placeholder="Enter Series"
data={[]}
value={form.values.series || []}
onChange={(value: string[]) =>
form.setFieldValue("series", value)
}
/>
</Grid.Col>
</Grid>
</Box>
<hr style={{ width: "100%" }} />
<Box>
<ScrollArea

View File

@ -165,6 +165,12 @@ export type IScenario = {
isReboot: boolean;
is_reboot: boolean;
send_result: boolean;
brandId: number;
brand_id?: number;
categoryId: number;
category_id?: number;
note: string;
series: string;
updated_at: string;
};
@ -241,3 +247,12 @@ export type TextTSMLicense = {
LICENSE_COUNT: string;
LICENSE_PRIORITY: string;
};
export type TBrands = {
id: number;
name: string;
};
export type TCategories = {
id: number;
name: string;
};