diff --git a/BACKEND/app/controllers/brands_controller.ts b/BACKEND/app/controllers/brands_controller.ts new file mode 100644 index 0000000..6df342b --- /dev/null +++ b/BACKEND/app/controllers/brands_controller.ts @@ -0,0 +1,38 @@ +import Brand from '#models/brand' +import type { HttpContext } from '@adonisjs/core/http' + +export default class BrandsController { + // GET /models + async index({}: HttpContext) { + return await Brand.all() + } + + // POST /models + async store({ request }: HttpContext) { + const data = request.only(['name']) + const model = await Brand.create(data) + return model + } + + // GET /models/:id + async show({ params }: HttpContext) { + const model = await Brand.findOrFail(params.id) + return model + } + + // PUT /models/:id + async update({ params, request }: HttpContext) { + const model = await Brand.findOrFail(params.id) + const data = request.only(['name']) + model.merge(data) + await model.save() + return model + } + + // DELETE /models/:id + async destroy({ params }: HttpContext) { + const model = await Brand.findOrFail(params.id) + await model.delete() + return { success: true } + } +} diff --git a/BACKEND/app/controllers/categories_controller.ts b/BACKEND/app/controllers/categories_controller.ts new file mode 100644 index 0000000..0039c9b --- /dev/null +++ b/BACKEND/app/controllers/categories_controller.ts @@ -0,0 +1,38 @@ +import Category from '#models/category' +import type { HttpContext } from '@adonisjs/core/http' + +export default class CategoriesController { + // GET /models + async index({}: HttpContext) { + return await Category.all() + } + + // POST /models + async store({ request }: HttpContext) { + const data = request.only(['name']) + const model = await Category.create(data) + return model + } + + // GET /models/:id + async show({ params }: HttpContext) { + const model = await Category.findOrFail(params.id) + return model + } + + // PUT /models/:id + async update({ params, request }: HttpContext) { + const model = await Category.findOrFail(params.id) + const data = request.only(['name']) + model.merge(data) + await model.save() + return model + } + + // DELETE /models/:id + async destroy({ params }: HttpContext) { + const model = await Category.findOrFail(params.id) + await model.delete() + return { success: true } + } +} diff --git a/BACKEND/app/controllers/scenarios_controller.ts b/BACKEND/app/controllers/scenarios_controller.ts index 8c1a870..a05d17e 100644 --- a/BACKEND/app/controllers/scenarios_controller.ts +++ b/BACKEND/app/controllers/scenarios_controller.ts @@ -36,6 +36,33 @@ export default class ScenariosController { const payload = await request.all() const trx = await db.transaction() try { + // Check exist series + const inputSeries: string[] = payload.series.map((s: string) => s.trim().toUpperCase()) + + const existedScenarios = await Scenario.query().select('id', 'series') + + const duplicatedSeries: string[] = [] + + for (const sc of existedScenarios) { + const scSeries: string[] = JSON.parse(sc.series || '[]').map((s: string) => + s.trim().toUpperCase() + ) + + for (const s of inputSeries) { + if (scSeries.includes(s)) { + duplicatedSeries.push(s) + } + } + } + + if (duplicatedSeries.length) { + return response.badRequest({ + status: false, + message: 'Series already exists in another scenario', + duplicatedSeries: [...new Set(duplicatedSeries)], + }) + } + const scenario = await Scenario.create( { title: payload.title.trim(), @@ -43,6 +70,10 @@ export default class ScenariosController { timeout: payload.timeout, isReboot: payload.isReboot, send_result: payload.send_result, + brandId: payload.brandId, + categoryId: payload.categoryId, + note: payload.note, + series: JSON.stringify(payload.series), }, { client: trx } ) @@ -93,7 +124,32 @@ export default class ScenariosController { const payload = request.body() const scenario = await Scenario.find(scenarioId) if (!scenario) return response.status(404).json({ message: 'Scenario not found' }) + // Check exist series + const inputSeries: string[] = payload.series.map((s: string) => s.trim().toUpperCase()) + const existedScenarios = await Scenario.query().select('id', 'series') + const duplicatedSeries: string[] = [] + for (const sc of existedScenarios) { + const scSeries: string[] = JSON.parse(sc.series || '[]').map((s: string) => + s.trim().toUpperCase() + ) + + for (const s of inputSeries) { + if (scSeries.includes(s)) { + duplicatedSeries.push(s) + } + } + } + + if (duplicatedSeries.length) { + return response.badRequest({ + status: false, + message: 'Series already exists in another scenario', + duplicatedSeries: [...new Set(duplicatedSeries)], + }) + } + payload.body = JSON.stringify(payload.body) + payload.series = JSON.stringify(payload.series) scenario.merge(payload) await scenario.save() diff --git a/BACKEND/app/models/brand.ts b/BACKEND/app/models/brand.ts new file mode 100644 index 0000000..7c8f7c3 --- /dev/null +++ b/BACKEND/app/models/brand.ts @@ -0,0 +1,16 @@ +import { DateTime } from 'luxon' +import { BaseModel, column } from '@adonisjs/lucid/orm' + +export default class Brand extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare name: string + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + declare updatedAt: DateTime +} diff --git a/BACKEND/app/models/category.ts b/BACKEND/app/models/category.ts new file mode 100644 index 0000000..76cc354 --- /dev/null +++ b/BACKEND/app/models/category.ts @@ -0,0 +1,16 @@ +import { DateTime } from 'luxon' +import { BaseModel, column } from '@adonisjs/lucid/orm' + +export default class Category extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare name: string + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + declare updatedAt: DateTime +} diff --git a/BACKEND/app/models/scenario.ts b/BACKEND/app/models/scenario.ts index 1dbfcab..c3a6c03 100644 --- a/BACKEND/app/models/scenario.ts +++ b/BACKEND/app/models/scenario.ts @@ -1,5 +1,8 @@ -import { BaseModel, column } from '@adonisjs/lucid/orm' +import { BaseModel, belongsTo, column } from '@adonisjs/lucid/orm' +import type { BelongsTo } from '@adonisjs/lucid/types/relations' import { DateTime } from 'luxon' +import Brand from './brand.js' +import Category from './category.js' export default class Scenario extends BaseModel { @column({ isPrimary: true }) @@ -25,4 +28,22 @@ export default class Scenario extends BaseModel { @column() declare send_result: boolean + + @column() + declare brandId: number + + @belongsTo(() => Brand) + declare brand: BelongsTo + + @column() + declare categoryId: number + + @belongsTo(() => Category) + declare category: BelongsTo + + @column() + declare note: string + + @column() + declare series: string } diff --git a/BACKEND/database/migrations/1765782918517_create_brands_table.ts b/BACKEND/database/migrations/1765782918517_create_brands_table.ts new file mode 100644 index 0000000..3bed752 --- /dev/null +++ b/BACKEND/database/migrations/1765782918517_create_brands_table.ts @@ -0,0 +1,22 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class CreateBrandsTable extends BaseSchema { + protected tableName = 'brands' + + public async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + table.string('name').notNullable() + table.timestamps() + }) + + // Defer the insert until after the table creation is complete + this.defer(async (db) => { + await db.table(this.tableName).insert([{ name: 'Cisco' }, { name: 'Juniper' }]) + }) + } + + public async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/BACKEND/database/migrations/1765783009633_create_categories_table.ts b/BACKEND/database/migrations/1765783009633_create_categories_table.ts new file mode 100644 index 0000000..11cfaac --- /dev/null +++ b/BACKEND/database/migrations/1765783009633_create_categories_table.ts @@ -0,0 +1,31 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class CreateBrandsTable extends BaseSchema { + protected tableName = 'categories' + + public async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + table.string('name').notNullable() + table.timestamps() + }) + + // Defer the insert until after the table creation is complete + this.defer(async (db) => { + await db + .table(this.tableName) + .insert([ + { name: 'Switch' }, + { name: 'Router' }, + { name: 'Server/PC' }, + { name: 'Access Point' }, + { name: 'Firewall' }, + { name: 'Other' }, + ]) + }) + } + + public async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/BACKEND/database/migrations/1765783400357_add_brand_id_category_id_series_note_to_scenarios_table.ts b/BACKEND/database/migrations/1765783400357_add_brand_id_category_id_series_note_to_scenarios_table.ts new file mode 100644 index 0000000..65fd1eb --- /dev/null +++ b/BACKEND/database/migrations/1765783400357_add_brand_id_category_id_series_note_to_scenarios_table.ts @@ -0,0 +1,23 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'scenarios' + + async up() { + this.schema.alterTable(this.tableName, (table) => { + table.integer('brand_id').unsigned().references('id').inTable('brands') + table.integer('category_id').unsigned().references('id').inTable('categories') + table.string('note').defaultTo('') + table.text('series').defaultTo('') + }) + } + + async down() { + this.schema.alterTable(this.tableName, (table) => { + table.dropColumn('brand_id') + table.dropColumn('category_id') + table.dropColumn('note') + table.dropColumn('series') + }) + } +} diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index b7b34b5..1a7b670 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -572,23 +572,22 @@ export class WebSocketIo { const linkWiki = process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test' - // await axios.post(linkWiki, { - // data: tableHTML, - // titleAuto: `[DPELP] - ${stationName} - ` + dataFormat, - // }) + await axios.post(linkWiki, { + data: tableHTML, + titleAuto: `[DPELP] - ${stationName} - ` + dataFormat, + }) await sendMessageToMail( 'andrew.ng@apactech.io', `[DPELP] - ${stationName} - ${dataFormat}`, - tableHTML - // , - // ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io'] + tableHTML, + ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io'] + ) + await sendMessageToZulip( + 'stream', + 'ATC_Report', + station.name, + `\n\n---\n**[DPELP] - ${stationName} - ${dataFormat}**\n\n` + zulipMess ) - // await sendMessageToZulip( - // 'stream', - // 'ATC_Report', - // station.name, - // `\n\n---\n**[DPELP] - ${stationName} - ${dataFormat}**\n\n` + zulipMess - // ) } catch (error) { console.log(error) } diff --git a/BACKEND/start/routes.ts b/BACKEND/start/routes.ts index 07c2b50..123a8e0 100644 --- a/BACKEND/start/routes.ts +++ b/BACKEND/start/routes.ts @@ -81,3 +81,21 @@ router router.delete('delete/:id', '#controllers/tickets_controller.delete') }) .prefix('api/ticket') + +router + .group(() => { + router.get('/', '#controllers/brands_controller.index') + router.post('create', '#controllers/brands_controller.store') + router.post('update', '#controllers/brands_controller.update') + router.post('delete', '#controllers/brands_controller.destroy') + }) + .prefix('api/brands') + +router + .group(() => { + router.get('/', '#controllers/categories_controller.index') + router.post('create', '#controllers/categories_controller.store') + router.post('update', '#controllers/categories_controller.update') + router.post('delete', '#controllers/categories_controller.destroy') + }) + .prefix('api/categories')