Update keywords

This commit is contained in:
nguyentrungthat 2026-01-28 16:34:28 +07:00
parent e6a5713c18
commit d0022eb7d5
9 changed files with 690 additions and 18 deletions

View File

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

View File

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

View File

@ -1614,11 +1614,20 @@ ${log}
<p>Station: <b>${this.config.stationName}</b></p> <p>Station: <b>${this.config.stationName}</b></p>
<p>Line: <b>${this.config.lineNumber}</b></p> <p>Line: <b>${this.config.lineNumber}</b></p>
<p>Model: <b>${pid}</b></p> <p>Model: <b>${pid}</b></p>
<p>RAM: ${mem ? `<b>${mem + ' bytes'} (<span style="color: ${isWarningRAM ? "red" : "black"};">default: ${configRam.ram}</span>)</b>` : ''}</p> <p>RAM: ${mem ? `<b>${mem + ' bytes'} (<span style="color: ${isWarningRAM ? 'red' : 'black'};">default: ${configRam.ram}</span>)</b>` : ''}</p>
<p>FLASH: ${flash ? `<b>${flash + ' bytes'} (<span style="color: ${isWarningFlash ? "red" : "black"};">default: ${configRam.flash}</span>)</b>` : ''}</p> <p>FLASH: ${flash ? `<b>${flash + ' bytes'} (<span style="color: ${isWarningFlash ? 'red' : 'black'};">default: ${configRam.flash}</span>)</b>` : ''}</p>
<hr /> <hr />
<div style="white-space: break-spaces; background-color: #f5f5f5; color: black; padding: 8px; max-height: 500px; overflow-y: scroll; border: 1px #ccc solid;"><span style="color: black;"> <div style="white-space: break-spaces; background-color: #f5f5f5; color: black; padding: 8px; max-height: 500px; overflow-y: scroll; border: 1px #ccc solid;"><span style="color: black;">
${escapeHtml(output).replace('show ver', '').replace('sh ver', '').replace('show version', '').replace('sh version', '').replace(mem, `<span style="color: ${isWarningRAM ? "red" : "black"};">${mem}</span>`).replace(flash, `<span style="color: ${isWarningFlash ? "red" : "black"};">${flash}</span>`)}</span></div> ${escapeHtml(output)
.replace('show ver', '')
.replace('sh ver', '')
.replace('show version', '')
.replace('sh version', '')
.replace(mem, `<span style="color: ${isWarningRAM ? 'red' : 'black'};">${mem}</span>`)
.replace(
flash,
`<span style="color: ${isWarningFlash ? 'red' : 'black'};">${flash}</span>`
)}</span></div>
` `
await sendMessageToMail(subject, body) await sendMessageToMail(subject, body)
} }

View File

@ -166,21 +166,21 @@ export function mapToLineFormat(input: InputData) {
const vid = input.inventory?.vid || '' const vid = input.inventory?.vid || ''
const sn = input.inventory?.sn || '' const sn = input.inventory?.sn || ''
if (!pid || !sn) { // if (!pid || !sn) {
return { // return {
line, // line,
pid: '', // pid: '',
vid: '', // vid: '',
sn: '', // sn: '',
ios: '', // ios: '',
mac: '', // mac: '',
ram: '', // ram: '',
flash: '', // flash: '',
license: [], // license: [],
issues: ['No data'], // issues: ['No data'],
summary: '', // summary: '',
} // }
} // }
// MAC // MAC
let mac = '' let mac = ''

View File

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

View File

@ -121,3 +121,12 @@ router
router.post('/delete', '#controllers/config_ram_controller.delete') router.post('/delete', '#controllers/config_ram_controller.delete')
}) })
.prefix('/api/config-ram') .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')

View File

@ -49,6 +49,7 @@ import ModalHistory from "./Modal/ModalHistory";
import ModalConfig from "./Modal/ModalConfig"; import ModalConfig from "./Modal/ModalConfig";
import DrawerScenario from "./Modal/ModalScenario"; import DrawerScenario from "./Modal/ModalScenario";
import ModalConfigRamFlash from "./Modal/ModalConfigRamFlash"; import ModalConfigRamFlash from "./Modal/ModalConfigRamFlash";
import ModalKeywords from "./Modal/ModalKeywords";
interface DraggableTabsProps { interface DraggableTabsProps {
tabsData: TStation[]; tabsData: TStation[];
@ -153,6 +154,7 @@ export default function DraggableTabs({
const [openConfig, setOpenConfig] = useState<boolean>(false); const [openConfig, setOpenConfig] = useState<boolean>(false);
const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false); const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false);
const [openConfigRam, setOpenConfigRam] = useState<boolean>(false); const [openConfigRam, setOpenConfigRam] = useState<boolean>(false);
const [openKeywords, setOpenKeywords] = useState<boolean>(false);
const sensors = useSensors(useSensor(PointerSensor)); const sensors = useSensors(useSensor(PointerSensor));
@ -282,6 +284,15 @@ export default function DraggableTabs({
> >
Config Ram Config Ram
</Button> </Button>
<Button
color="lime"
variant="filled"
size="xs"
leftSection={<IconSettings size={16} />}
onClick={() => setOpenKeywords(true)}
>
Keywords
</Button>
</Flex> </Flex>
<Tabs.List className={classes.list}> <Tabs.List className={classes.list}>
<SortableContext <SortableContext
@ -430,6 +441,11 @@ export default function DraggableTabs({
opened={openConfigRam} opened={openConfigRam}
onClose={() => setOpenConfigRam(false)} onClose={() => setOpenConfigRam(false)}
/> />
<ModalKeywords
opened={openKeywords}
onClose={() => setOpenKeywords(false)}
/>
</DndContext> </DndContext>
); );
} }

View File

@ -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<Keywords[]>([]);
const [dataKeywords, setDataKeywords] = useState<Keywords | null>(null);
const [newKeywords, setNewKeywords] = useState<Keywords>({
name: "",
type: "special_model",
match_type: "include",
is_active: true,
});
const [inputModel, setInputModel] = useState<string>("");
const [disabled, setDisabled] = useState<boolean>(false);
const [openConfirm, setOpenConfirm] = useState<boolean>(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 (
<Modal
size={"70%"}
style={{ position: "absolute", left: 0 }}
opened={opened}
onClose={onClose}
title="Config Keywords"
>
<Grid>
<Grid.Col span={12}>
<Box style={{ width: "300px", marginBottom: "10px" }}>
<TextInput
autoCapitalize="on"
label="Search model"
placeholder="Enter Model"
value={inputModel}
onChange={(e) => setInputModel(e.currentTarget.value)}
size="xs"
rightSection={
inputModel ? (
<IconX
size={14}
style={{ cursor: "pointer" }}
onClick={() => setInputModel("")}
/>
) : null
}
rightSectionPointerEvents="auto"
/>
</Box>
<hr />
<Flex
align={"center"}
justify={"space-between"}
style={{ marginBottom: "10px", marginTop: "10px" }}
>
<Flex align={"center"} gap={10}>
<TextInput
label="Name"
placeholder="Enter name"
style={{ width: "200px" }}
value={newKeywords?.name}
onChange={(e) =>
setNewKeywords({
...newKeywords,
name: e.currentTarget.value,
})
}
size="xs"
/>
<Select
clearable={false}
size="xs"
label="Type"
placeholder="Select type"
data={LIST_TYPE}
value={newKeywords?.type}
onChange={(value) =>
setNewKeywords({
...newKeywords,
type: value || "",
})
}
/>
<Select
clearable={false}
size="xs"
label="Match Type"
placeholder="Select match type"
data={LIST_MATCH_TYPE}
value={newKeywords?.match_type}
onChange={(value) =>
setNewKeywords({
...newKeywords,
match_type: value || "",
})
}
/>
</Flex>
<Button
disabled={disabled || newKeywords.name.length === 0}
size="xs"
color="green"
onClick={() => handleAdd()}
variant="filled"
>
Add
</Button>
</Flex>
<Box>
<ScrollArea
h={"calc(75vh - 150px)"}
style={{ marginBottom: "20px" }}
>
<Table
stickyHeader
stickyHeaderOffset={-1}
striped
highlightOnHover
withRowBorders
withTableBorder
withColumnBorders
>
<Table.Thead style={{ zIndex: 100 }}>
<Table.Tr>
<Table.Th style={{ textAlign: "center" }}>Name</Table.Th>
<Table.Th w={300} style={{ textAlign: "center" }}>
Type
</Table.Th>
<Table.Th w={300} style={{ textAlign: "center" }}>
Match Type
</Table.Th>
<Table.Th w={200} style={{ textAlign: "center" }}>
Action
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filteredConfigs().length > 0 ? (
filteredConfigs().map((element) => {
const isEditing =
dataKeywords?.id === element.id && !openConfirm;
return (
<Table.Tr key={element.id}>
{/* MODELS */}
<Table.Td>
{" "}
<Flex wrap={"wrap"} gap={"4px"}>
{isEditing ? (
<TextInput
w={"100%"}
label=""
placeholder="Enter model"
value={dataKeywords?.name || ""}
onChange={(e) =>
handleChange("name", e.currentTarget.value)
}
/>
) : (
element.name
)}
</Flex>
</Table.Td>
{/* Type */}
<Table.Td style={{ textAlign: "center" }}>
{isEditing ? (
<Select
clearable={false}
label="Type"
placeholder="Select type"
data={LIST_TYPE}
value={dataKeywords?.type}
onChange={(value) =>
handleChange("type", value || "")
}
/>
) : (
LIST_TYPE.find(
(item) => item.value === element.type
)?.label || element.type
)}
</Table.Td>
{/* Match Type */}
<Table.Td style={{ textAlign: "center" }}>
{isEditing ? (
<Select
clearable={false}
label="Match Type"
placeholder="Select match type"
data={LIST_MATCH_TYPE}
value={dataKeywords?.match_type}
onChange={(value) =>
handleChange("match_type", value || "")
}
/>
) : (
LIST_MATCH_TYPE.find(
(item) => item.value === element.match_type
)?.label || element.match_type
)}
</Table.Td>
{/* ACTION */}
<Table.Td>
<Flex gap={10} justify="center">
{isEditing ? (
<>
<Button
disabled={disabled}
size="xs"
color="green"
onClick={handleSave}
variant="outline"
>
Save
</Button>
<Button
disabled={disabled}
size="xs"
color="gray"
onClick={handleCancel}
variant="outline"
>
Cancel
</Button>
</>
) : (
<>
<Button
disabled={disabled}
size="xs"
onClick={() =>
setDataKeywords({ ...element })
}
variant="outline"
>
Edit
</Button>
<Button
disabled={disabled}
size="xs"
color={"red"}
onClick={() => {
setDataKeywords({ ...element });
setOpenConfirm(true);
}}
variant="outline"
>
Delete
</Button>
</>
)}
</Flex>
</Table.Td>
</Table.Tr>
);
})
) : (
<Table.Tr>
<Table.Td colSpan={4} style={{ textAlign: "center" }}>
No keywords found.
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</ScrollArea>
</Box>
</Grid.Col>
</Grid>
<DialogConfirm
opened={openConfirm}
close={() => {
setDataKeywords(null);
setOpenConfirm(false);
}}
message={"Are you sure delete this keyword?"}
handle={() => {
setOpenConfirm(false);
handleDelete();
}}
centered={true}
/>
</Modal>
);
}

View File

@ -288,3 +288,11 @@ export type ConfigRam = {
ram: string; ram: string;
flash: string; flash: string;
}; };
export type Keywords = {
id?: number;
name: string;
type: string;
match_type: string;
is_active: boolean;
};