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 + 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" + /> + + setNewKeywords({ + ...newKeywords, + match_type: value || "", + }) + } + /> + + + + + + + + + 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("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; +};