From d0022eb7d560f66c993123fdcf1ec1214ec2b71e Mon Sep 17 00:00:00 2001
From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com>
Date: Wed, 28 Jan 2026 16:34:28 +0700
Subject: [PATCH] Update keywords
---
.../app/controllers/keywords_controller.ts | 95 ++++
BACKEND/app/models/keywords.ts | 25 +
BACKEND/app/services/line_connection.ts | 15 +-
BACKEND/app/ultils/helper.ts | 30 +-
.../1769566064931_create_keywords_table.ts | 20 +
BACKEND/start/routes.ts | 9 +
FRONTEND/src/components/DragTabs.tsx | 16 +
.../src/components/Modal/ModalKeywords.tsx | 490 ++++++++++++++++++
FRONTEND/src/untils/types.ts | 8 +
9 files changed, 690 insertions(+), 18 deletions(-)
create mode 100644 BACKEND/app/controllers/keywords_controller.ts
create mode 100644 BACKEND/app/models/keywords.ts
create mode 100644 BACKEND/database/migrations/1769566064931_create_keywords_table.ts
create mode 100644 FRONTEND/src/components/Modal/ModalKeywords.tsx
diff --git a/BACKEND/app/controllers/keywords_controller.ts b/BACKEND/app/controllers/keywords_controller.ts
new file mode 100644
index 0000000..2f928e4
--- /dev/null
+++ b/BACKEND/app/controllers/keywords_controller.ts
@@ -0,0 +1,95 @@
+import Keyword from '#models/keywords'
+import type { HttpContext } from '@adonisjs/core/http'
+
+export default class KeywordsController {
+ /**
+ * Display a list of resource
+ */
+ async get({}: HttpContext) {
+ const keywords = await Keyword.all()
+ return { status: true, data: keywords }
+ }
+
+ /**
+ * Display form to create a new record
+ */
+ async create({ auth, request, response }: HttpContext) {
+ let payload = request.only([...Array.from(Keyword.$columnsDefinitions.keys())])
+ try {
+ // Check exist model
+ const existedKeyword = await Keyword.findBy('name', payload.name)
+ if (existedKeyword) {
+ return response.badRequest({
+ status: false,
+ message: 'Keyword already exists',
+ })
+ }
+
+ const keyword = await Keyword.create({
+ ...payload,
+ })
+ return response.created({
+ status: true,
+ message: 'Keyword created successfully',
+ data: keyword,
+ })
+ } catch (error) {
+ return response.badRequest({ error: error, message: 'Keyword create failed', status: false })
+ }
+ }
+
+ async update({ request, response, auth }: HttpContext) {
+ let payload = request.only(
+ Array.from(Keyword.$columnsDefinitions.keys()).filter(
+ (f) => f !== 'created_at' && f !== 'updated_at'
+ )
+ )
+ try {
+ const keyword = await Keyword.find(request.body().id)
+ if (!keyword) {
+ return response.status(404).json({ message: 'Keyword not found' })
+ }
+
+ // Check exist model
+ const existedKeyword = await Keyword.findBy('name', payload.name)
+ if (existedKeyword && existedKeyword.id !== keyword.id) {
+ return response.badRequest({
+ status: false,
+ message: 'Keyword already exists',
+ })
+ }
+
+ Object.assign(keyword, { ...payload })
+ await keyword.save()
+ return response.ok({ status: true, message: 'Keyword update successfully', data: keyword })
+ } catch (error) {
+ return response.badRequest({ error: error, message: 'Keyword update failed', status: false })
+ }
+ }
+
+ /**
+ * Delete record
+ */
+ async delete({ auth, request, response }: HttpContext) {
+ try {
+ const keyword = await Keyword.find(request.body().id)
+ if (!keyword) {
+ return response.status(404).json({ message: 'Keyword not found' })
+ }
+
+ // Delete the keyword
+ await keyword.delete()
+ return response.ok({
+ status: true,
+ message: 'Keyword delete successfully',
+ data: keyword,
+ })
+ } catch (error) {
+ return response.badRequest({
+ error: error,
+ message: 'Keyword delete failed',
+ status: false,
+ })
+ }
+ }
+}
diff --git a/BACKEND/app/models/keywords.ts b/BACKEND/app/models/keywords.ts
new file mode 100644
index 0000000..1811d60
--- /dev/null
+++ b/BACKEND/app/models/keywords.ts
@@ -0,0 +1,25 @@
+import { DateTime } from 'luxon'
+import { BaseModel, column } from '@adonisjs/lucid/orm'
+
+export default class Keyword extends BaseModel {
+ @column({ isPrimary: true })
+ declare id: number
+
+ @column()
+ declare name: string
+
+ @column()
+ declare type: string
+
+ @column()
+ declare match_type: string
+
+ @column()
+ declare is_active: boolean
+
+ @column.dateTime({ autoCreate: true })
+ declare createdAt: DateTime
+
+ @column.dateTime({ autoCreate: true, autoUpdate: true })
+ declare updatedAt: DateTime
+}
diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts
index f73ec3b..7863d78 100644
--- a/BACKEND/app/services/line_connection.ts
+++ b/BACKEND/app/services/line_connection.ts
@@ -1614,11 +1614,20 @@ ${log}
Station: ${this.config.stationName}
Line: ${this.config.lineNumber}
Model: ${pid}
-RAM: ${mem ? `${mem + ' bytes'} (default: ${configRam.ram})` : ''}
-FLASH: ${flash ? `${flash + ' bytes'} (default: ${configRam.flash})` : ''}
+RAM: ${mem ? `${mem + ' bytes'} (default: ${configRam.ram})` : ''}
+FLASH: ${flash ? `${flash + ' bytes'} (default: ${configRam.flash})` : ''}
- ${escapeHtml(output).replace('show ver', '').replace('sh ver', '').replace('show version', '').replace('sh version', '').replace(mem, `${mem}`).replace(flash, `${flash}`)}
+ ${escapeHtml(output)
+ .replace('show ver', '')
+ .replace('sh ver', '')
+ .replace('show version', '')
+ .replace('sh version', '')
+ .replace(mem, `${mem}`)
+ .replace(
+ flash,
+ `${flash}`
+ )}
`
await sendMessageToMail(subject, body)
}
diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts
index eaf473c..9b78346 100644
--- a/BACKEND/app/ultils/helper.ts
+++ b/BACKEND/app/ultils/helper.ts
@@ -166,21 +166,21 @@ export function mapToLineFormat(input: InputData) {
const vid = input.inventory?.vid || ''
const sn = input.inventory?.sn || ''
- if (!pid || !sn) {
- return {
- line,
- pid: '',
- vid: '',
- sn: '',
- ios: '',
- mac: '',
- ram: '',
- flash: '',
- license: [],
- issues: ['No data'],
- summary: '',
- }
- }
+ // if (!pid || !sn) {
+ // return {
+ // line,
+ // pid: '',
+ // vid: '',
+ // sn: '',
+ // ios: '',
+ // mac: '',
+ // ram: '',
+ // flash: '',
+ // license: [],
+ // issues: ['No data'],
+ // summary: '',
+ // }
+ // }
// MAC
let mac = ''
diff --git a/BACKEND/database/migrations/1769566064931_create_keywords_table.ts b/BACKEND/database/migrations/1769566064931_create_keywords_table.ts
new file mode 100644
index 0000000..7fcab17
--- /dev/null
+++ b/BACKEND/database/migrations/1769566064931_create_keywords_table.ts
@@ -0,0 +1,20 @@
+import { BaseSchema } from '@adonisjs/lucid/schema'
+
+export default class extends BaseSchema {
+ protected tableName = 'keywords'
+
+ async up() {
+ this.schema.createTable(this.tableName, (table) => {
+ table.increments('id')
+ table.string('name').notNullable()
+ table.string('type').notNullable()
+ table.string('match_type').defaultTo('include')
+ table.boolean('is_active').defaultTo(true)
+ table.timestamps()
+ })
+ }
+
+ async down() {
+ this.schema.dropTable(this.tableName)
+ }
+}
diff --git a/BACKEND/start/routes.ts b/BACKEND/start/routes.ts
index 247278d..bc1efc8 100644
--- a/BACKEND/start/routes.ts
+++ b/BACKEND/start/routes.ts
@@ -121,3 +121,12 @@ router
router.post('/delete', '#controllers/config_ram_controller.delete')
})
.prefix('/api/config-ram')
+
+router
+ .group(() => {
+ router.get('/', '#controllers/keywords_controller.get')
+ router.post('/create', '#controllers/keywords_controller.create')
+ router.post('/update', '#controllers/keywords_controller.update')
+ router.post('/delete', '#controllers/keywords_controller.delete')
+ })
+ .prefix('/api/keywords')
diff --git a/FRONTEND/src/components/DragTabs.tsx b/FRONTEND/src/components/DragTabs.tsx
index de99f44..ed20630 100644
--- a/FRONTEND/src/components/DragTabs.tsx
+++ b/FRONTEND/src/components/DragTabs.tsx
@@ -49,6 +49,7 @@ import ModalHistory from "./Modal/ModalHistory";
import ModalConfig from "./Modal/ModalConfig";
import DrawerScenario from "./Modal/ModalScenario";
import ModalConfigRamFlash from "./Modal/ModalConfigRamFlash";
+import ModalKeywords from "./Modal/ModalKeywords";
interface DraggableTabsProps {
tabsData: TStation[];
@@ -153,6 +154,7 @@ export default function DraggableTabs({
const [openConfig, setOpenConfig] = useState(false);
const [openDrawerScenario, setOpenDrawerScenario] = useState(false);
const [openConfigRam, setOpenConfigRam] = useState(false);
+ const [openKeywords, setOpenKeywords] = useState(false);
const sensors = useSensors(useSensor(PointerSensor));
@@ -282,6 +284,15 @@ export default function DraggableTabs({
>
Config Ram
+ }
+ onClick={() => setOpenKeywords(true)}
+ >
+ Keywords
+
setOpenConfigRam(false)}
/>
+
+ setOpenKeywords(false)}
+ />
);
}
diff --git a/FRONTEND/src/components/Modal/ModalKeywords.tsx b/FRONTEND/src/components/Modal/ModalKeywords.tsx
new file mode 100644
index 0000000..75ab6b6
--- /dev/null
+++ b/FRONTEND/src/components/Modal/ModalKeywords.tsx
@@ -0,0 +1,490 @@
+import { useEffect, useState } from "react";
+import {
+ Modal,
+ Button,
+ Grid,
+ ScrollArea,
+ Table,
+ Box,
+ Flex,
+ TextInput,
+ Select,
+} from "@mantine/core";
+import axios from "axios";
+import type { Keywords } from "../../untils/types";
+import { IconX } from "@tabler/icons-react";
+import { notifications } from "@mantine/notifications";
+import DialogConfirm from "../DialogConfirm";
+
+const apiUrl = import.meta.env.VITE_BACKEND_URL;
+const LIST_TYPE = [
+ { value: "error", label: "Error" },
+ { value: "warning", label: "Warning" },
+ { value: "special_model", label: "Special model" },
+];
+const LIST_MATCH_TYPE = [
+ { value: "exact", label: "Exact" },
+ { value: "include", label: "Include" },
+];
+interface Props {
+ opened: boolean;
+ onClose: () => void;
+}
+
+export default function ModalKeywords({ opened, onClose }: Props) {
+ const [keywords, setKeywords] = useState([]);
+ const [dataKeywords, setDataKeywords] = useState(null);
+ const [newKeywords, setNewKeywords] = useState({
+ name: "",
+ type: "special_model",
+ match_type: "include",
+ is_active: true,
+ });
+ const [inputModel, setInputModel] = useState("");
+ const [disabled, setDisabled] = useState(false);
+ const [openConfirm, setOpenConfirm] = useState(false);
+
+ // get list keyword
+ const getListConfig = async () => {
+ try {
+ const response = await axios.get(apiUrl + "api/keywords");
+ if (response.data.data && Array.isArray(response.data.data)) {
+ setKeywords(response.data.data);
+ }
+ } catch (error) {
+ console.log("Error get keyword", error);
+ }
+ };
+
+ useEffect(() => {
+ if (opened) getListConfig();
+ else {
+ setDataKeywords(null);
+ setInputModel("");
+ setNewKeywords({
+ name: "",
+ type: "special_model",
+ match_type: "include",
+ is_active: true,
+ });
+ }
+ }, [opened]);
+
+ const handleChange = (field: keyof Keywords, value: string | string[]) => {
+ if (!dataKeywords) return;
+ setDataKeywords({
+ ...dataKeywords,
+ [field]: value,
+ });
+ };
+
+ const handleAdd = async () => {
+ setDisabled(true);
+ try {
+ const response = await axios.post(apiUrl + `api/keywords/create`, {
+ ...newKeywords,
+ });
+
+ // update local list
+ setKeywords((prev) => [
+ ...prev,
+ {
+ ...response.data.data,
+ models:
+ typeof response.data.data.models === "string"
+ ? JSON.parse(response.data.data.models)
+ : response.data.data.models,
+ },
+ ]);
+ notifications.show({
+ title: "Success",
+ message: response.data.message,
+ color: "green",
+ });
+ setNewKeywords({
+ name: "",
+ type: "special_model",
+ match_type: "include",
+ is_active: true,
+ });
+ setDisabled(false);
+ } catch (error) {
+ console.log("Error save keyword", error);
+ setDisabled(false);
+ if (axios.isAxiosError(error)) {
+ notifications.show({
+ title: "Error",
+ message: error?.response?.data?.message,
+ color: "red",
+ });
+ } else {
+ notifications.show({
+ title: "Error",
+ message: "Create fail, please try again!",
+ color: "red",
+ });
+ }
+ }
+ };
+
+ const handleSave = async () => {
+ setDisabled(true);
+ if (!dataKeywords) return;
+
+ try {
+ const response = await axios.post(apiUrl + `api/keywords/update`, {
+ ...dataKeywords,
+ });
+
+ // update local list
+ setKeywords((prev) =>
+ prev.map((item) => (item.id === dataKeywords.id ? dataKeywords : item))
+ );
+
+ setDataKeywords(null);
+ setDisabled(false);
+ notifications.show({
+ title: "Success",
+ message: response.data.message,
+ color: "green",
+ });
+ } catch (error) {
+ console.log("Error save keyword", error);
+ setDisabled(false);
+ if (axios.isAxiosError(error)) {
+ notifications.show({
+ title: "Error",
+ message: error?.response?.data?.message,
+ color: "red",
+ });
+ } else {
+ notifications.show({
+ title: "Error",
+ message: "Update fail, please try again!",
+ color: "red",
+ });
+ }
+ }
+ };
+
+ const handleDelete = async () => {
+ setDisabled(true);
+ if (!dataKeywords) return;
+
+ try {
+ const response = await axios.post(apiUrl + `api/keywords/delete`, {
+ ...dataKeywords,
+ });
+
+ // update local list
+ setKeywords((prev) => prev.filter((item) => item.id !== dataKeywords.id));
+
+ setDataKeywords(null);
+ setDisabled(false);
+ notifications.show({
+ title: "Success",
+ message: response.data.message,
+ color: "green",
+ });
+ } catch (error) {
+ console.log("Error save keyword", error);
+ setDisabled(false);
+ if (axios.isAxiosError(error)) {
+ notifications.show({
+ title: "Error",
+ message: error?.response?.data?.message,
+ color: "red",
+ });
+ } else {
+ notifications.show({
+ title: "Error",
+ message: "Delete fail, please try again!",
+ color: "red",
+ });
+ }
+ }
+ };
+
+ const handleCancel = () => {
+ setDataKeywords(null);
+ };
+
+ const filteredConfigs = () => {
+ const listConfigs = [...keywords].filter((el) => {
+ const model: string = el.name || "";
+ return model.toLowerCase().includes(inputModel.trim().toLowerCase());
+ });
+ return listConfigs;
+ };
+
+ return (
+
+
+
+
+ setInputModel(e.currentTarget.value)}
+ size="xs"
+ rightSection={
+ inputModel ? (
+ setInputModel("")}
+ />
+ ) : null
+ }
+ rightSectionPointerEvents="auto"
+ />
+
+
+
+
+
+ setNewKeywords({
+ ...newKeywords,
+ name: e.currentTarget.value,
+ })
+ }
+ size="xs"
+ />
+
+
+
+
+
+
+
+
+ Name
+
+ Type
+
+
+ Match Type
+
+
+ Action
+
+
+
+
+
+ {filteredConfigs().length > 0 ? (
+ filteredConfigs().map((element) => {
+ const isEditing =
+ dataKeywords?.id === element.id && !openConfirm;
+
+ return (
+
+ {/* MODELS */}
+
+ {" "}
+
+ {isEditing ? (
+
+ handleChange("name", e.currentTarget.value)
+ }
+ />
+ ) : (
+ element.name
+ )}
+
+
+
+ {/* Type */}
+
+ {isEditing ? (
+
+ handleChange("type", value || "")
+ }
+ />
+ ) : (
+ LIST_TYPE.find(
+ (item) => item.value === element.type
+ )?.label || element.type
+ )}
+
+
+ {/* Match Type */}
+
+ {isEditing ? (
+
+ handleChange("match_type", value || "")
+ }
+ />
+ ) : (
+ LIST_MATCH_TYPE.find(
+ (item) => item.value === element.match_type
+ )?.label || element.match_type
+ )}
+
+
+ {/* ACTION */}
+
+
+ {isEditing ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+ );
+ })
+ ) : (
+
+
+ No keywords found.
+
+
+ )}
+
+
+
+
+
+
+
+ {
+ setDataKeywords(null);
+ setOpenConfirm(false);
+ }}
+ message={"Are you sure delete this keyword?"}
+ handle={() => {
+ setOpenConfirm(false);
+ handleDelete();
+ }}
+ centered={true}
+ />
+
+ );
+}
diff --git a/FRONTEND/src/untils/types.ts b/FRONTEND/src/untils/types.ts
index f7cca6c..f964fdf 100644
--- a/FRONTEND/src/untils/types.ts
+++ b/FRONTEND/src/untils/types.ts
@@ -288,3 +288,11 @@ export type ConfigRam = {
ram: string;
flash: string;
};
+
+export type Keywords = {
+ id?: number;
+ name: string;
+ type: string;
+ match_type: string;
+ is_active: boolean;
+};