diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index 304440b..29a8390 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"xebjc5KsFAyBY+NS\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"xebjc5KsFAyBY+NS\",\"encryptedData\":\"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\"}", + "Updated via schema editor on 2025-08-15 05:24": "{\"iv\":\"QIy/rBis2hnxaOdq\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/aircrafts.js b/backend/src/db/api/aircrafts.js index 1c24d73..b7f6ecc 100644 --- a/backend/src/db/api/aircrafts.js +++ b/backend/src/db/api/aircrafts.js @@ -18,6 +18,36 @@ module.exports = class AircraftsDBApi { registration_number: data.registration_number || + null + , + + make: data.make + || + null + , + + model: data.model + || + null + , + + year: data.year + || + null + , + + serial_number: data.serial_number + || + null + , + + engine_count: data.engine_count + || + null + , + + engine_type: data.engine_type + || null , @@ -42,6 +72,36 @@ module.exports = class AircraftsDBApi { registration_number: item.registration_number || null + , + + make: item.make + || + null + , + + model: item.model + || + null + , + + year: item.year + || + null + , + + serial_number: item.serial_number + || + null + , + + engine_count: item.engine_count + || + null + , + + engine_type: item.engine_type + || + null , importHash: item.importHash || null, @@ -66,6 +126,18 @@ module.exports = class AircraftsDBApi { if (data.registration_number !== undefined) updatePayload.registration_number = data.registration_number; + if (data.make !== undefined) updatePayload.make = data.make; + + if (data.model !== undefined) updatePayload.model = data.model; + + if (data.year !== undefined) updatePayload.year = data.year; + + if (data.serial_number !== undefined) updatePayload.serial_number = data.serial_number; + + if (data.engine_count !== undefined) updatePayload.engine_count = data.engine_count; + + if (data.engine_type !== undefined) updatePayload.engine_type = data.engine_type; + updatePayload.updatedById = currentUser.id; await aircrafts.update(updatePayload, {transaction}); @@ -172,6 +244,98 @@ module.exports = class AircraftsDBApi { }; } + if (filter.make) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'aircrafts', + 'make', + filter.make, + ), + }; + } + + if (filter.model) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'aircrafts', + 'model', + filter.model, + ), + }; + } + + if (filter.serial_number) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'aircrafts', + 'serial_number', + filter.serial_number, + ), + }; + } + + if (filter.engine_type) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'aircrafts', + 'engine_type', + filter.engine_type, + ), + }; + } + + if (filter.yearRange) { + const [start, end] = filter.yearRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + year: { + ...where.year, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + year: { + ...where.year, + [Op.lte]: end, + }, + }; + } + } + + if (filter.engine_countRange) { + const [start, end] = filter.engine_countRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + engine_count: { + ...where.engine_count, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + engine_count: { + ...where.engine_count, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/api/flights.js b/backend/src/db/api/flights.js index acfed19..46d8591 100644 --- a/backend/src/db/api/flights.js +++ b/backend/src/db/api/flights.js @@ -53,6 +53,11 @@ module.exports = class FlightsDBApi { remarks: data.remarks || + null + , + + time_recorded_for: data.time_recorded_for + || null , @@ -112,6 +117,11 @@ module.exports = class FlightsDBApi { remarks: item.remarks || null + , + + time_recorded_for: item.time_recorded_for + || + null , importHash: item.importHash || null, @@ -150,6 +160,8 @@ module.exports = class FlightsDBApi { if (data.remarks !== undefined) updatePayload.remarks = data.remarks; + if (data.time_recorded_for !== undefined) updatePayload.time_recorded_for = data.time_recorded_for; + updatePayload.updatedById = currentUser.id; await flights.update(updatePayload, {transaction}); @@ -405,6 +417,13 @@ module.exports = class FlightsDBApi { }; } + if (filter.time_recorded_for) { + where = { + ...where, + time_recorded_for: filter.time_recorded_for, + }; + } + if (filter.createdAtRange) { const [start, end] = filter.createdAtRange; diff --git a/backend/src/db/api/inspection_intervals.js b/backend/src/db/api/inspection_intervals.js new file mode 100644 index 0000000..3873c83 --- /dev/null +++ b/backend/src/db/api/inspection_intervals.js @@ -0,0 +1,335 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Inspection_intervalsDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const inspection_intervals = await db.inspection_intervals.create( + { + id: data.id || undefined, + + type: data.type + || + null + , + + interval_hours: data.interval_hours + || + null + , + + interval_months: data.interval_months + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return inspection_intervals; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const inspection_intervalsData = data.map((item, index) => ({ + id: item.id || undefined, + + type: item.type + || + null + , + + interval_hours: item.interval_hours + || + null + , + + interval_months: item.interval_months + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const inspection_intervals = await db.inspection_intervals.bulkCreate(inspection_intervalsData, { transaction }); + + return inspection_intervals; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const inspection_intervals = await db.inspection_intervals.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.type !== undefined) updatePayload.type = data.type; + + if (data.interval_hours !== undefined) updatePayload.interval_hours = data.interval_hours; + + if (data.interval_months !== undefined) updatePayload.interval_months = data.interval_months; + + updatePayload.updatedById = currentUser.id; + + await inspection_intervals.update(updatePayload, {transaction}); + + return inspection_intervals; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const inspection_intervals = await db.inspection_intervals.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of inspection_intervals) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of inspection_intervals) { + await record.destroy({transaction}); + } + }); + + return inspection_intervals; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const inspection_intervals = await db.inspection_intervals.findByPk(id, options); + + await inspection_intervals.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await inspection_intervals.destroy({ + transaction + }); + + return inspection_intervals; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const inspection_intervals = await db.inspection_intervals.findOne( + { where }, + { transaction }, + ); + + if (!inspection_intervals) { + return inspection_intervals; + } + + const output = inspection_intervals.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.interval_hoursRange) { + const [start, end] = filter.interval_hoursRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + interval_hours: { + ...where.interval_hours, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + interval_hours: { + ...where.interval_hours, + [Op.lte]: end, + }, + }; + } + } + + if (filter.interval_monthsRange) { + const [start, end] = filter.interval_monthsRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + interval_months: { + ...where.interval_months, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + interval_months: { + ...where.interval_months, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.type) { + where = { + ...where, + type: filter.type, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.inspection_intervals.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'inspection_intervals', + 'id', + query, + ), + ], + }; + } + + const records = await db.inspection_intervals.findAll({ + attributes: [ 'id', 'id' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['id', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.id, + })); + } + +}; + diff --git a/backend/src/db/api/inspections.js b/backend/src/db/api/inspections.js index 29495d3..02dbdaf 100644 --- a/backend/src/db/api/inspections.js +++ b/backend/src/db/api/inspections.js @@ -28,6 +28,11 @@ module.exports = class InspectionsDBApi { type: data.type || + null + , + + performed_at: data.performed_at + || null , @@ -62,6 +67,11 @@ module.exports = class InspectionsDBApi { type: item.type || null + , + + performed_at: item.performed_at + || + null , importHash: item.importHash || null, @@ -90,6 +100,8 @@ module.exports = class InspectionsDBApi { if (data.type !== undefined) updatePayload.type = data.type; + if (data.performed_at !== undefined) updatePayload.performed_at = data.performed_at; + updatePayload.updatedById = currentUser.id; await inspections.update(updatePayload, {transaction}); @@ -233,6 +245,30 @@ module.exports = class InspectionsDBApi { } } + if (filter.performed_atRange) { + const [start, end] = filter.performed_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + performed_at: { + ...where.performed_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + performed_at: { + ...where.performed_at, + [Op.lte]: end, + }, + }; + } + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/api/organizations.js b/backend/src/db/api/organizations.js index 3316248..3fa15ba 100644 --- a/backend/src/db/api/organizations.js +++ b/backend/src/db/api/organizations.js @@ -18,6 +18,26 @@ module.exports = class OrganizationsDBApi { name: data.name || + null + , + + address: data.address + || + null + , + + phone: data.phone + || + null + , + + website: data.website + || + null + , + + contact_email: data.contact_email + || null , @@ -42,6 +62,26 @@ module.exports = class OrganizationsDBApi { name: item.name || null + , + + address: item.address + || + null + , + + phone: item.phone + || + null + , + + website: item.website + || + null + , + + contact_email: item.contact_email + || + null , importHash: item.importHash || null, @@ -66,6 +106,14 @@ module.exports = class OrganizationsDBApi { if (data.name !== undefined) updatePayload.name = data.name; + if (data.address !== undefined) updatePayload.address = data.address; + + if (data.phone !== undefined) updatePayload.phone = data.phone; + + if (data.website !== undefined) updatePayload.website = data.website; + + if (data.contact_email !== undefined) updatePayload.contact_email = data.contact_email; + updatePayload.updatedById = currentUser.id; await organizations.update(updatePayload, {transaction}); @@ -172,6 +220,50 @@ module.exports = class OrganizationsDBApi { }; } + if (filter.address) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'organizations', + 'address', + filter.address, + ), + }; + } + + if (filter.phone) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'organizations', + 'phone', + filter.phone, + ), + }; + } + + if (filter.website) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'organizations', + 'website', + filter.website, + ), + }; + } + + if (filter.contact_email) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'organizations', + 'contact_email', + filter.contact_email, + ), + }; + } + if (filter.active !== undefined) { where = { ...where, diff --git a/backend/src/db/api/squawk_notes.js b/backend/src/db/api/squawk_notes.js new file mode 100644 index 0000000..2110d5e --- /dev/null +++ b/backend/src/db/api/squawk_notes.js @@ -0,0 +1,339 @@ + +const db = require('../models'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class Squawk_notesDBApi { + + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const squawk_notes = await db.squawk_notes.create( + { + id: data.id || undefined, + + content: data.content + || + null + , + + created_at: data.created_at + || + null + , + + updated_at: data.updated_at + || + null + , + + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return squawk_notes; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const squawk_notesData = data.map((item, index) => ({ + id: item.id || undefined, + + content: item.content + || + null + , + + created_at: item.created_at + || + null + , + + updated_at: item.updated_at + || + null + , + + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const squawk_notes = await db.squawk_notes.bulkCreate(squawk_notesData, { transaction }); + + return squawk_notes; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const squawk_notes = await db.squawk_notes.findByPk(id, {}, {transaction}); + + const updatePayload = {}; + + if (data.content !== undefined) updatePayload.content = data.content; + + if (data.created_at !== undefined) updatePayload.created_at = data.created_at; + + if (data.updated_at !== undefined) updatePayload.updated_at = data.updated_at; + + updatePayload.updatedById = currentUser.id; + + await squawk_notes.update(updatePayload, {transaction}); + + return squawk_notes; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const squawk_notes = await db.squawk_notes.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of squawk_notes) { + await record.update( + {deletedBy: currentUser.id}, + {transaction} + ); + } + for (const record of squawk_notes) { + await record.destroy({transaction}); + } + }); + + return squawk_notes; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || {id: null}; + const transaction = (options && options.transaction) || undefined; + + const squawk_notes = await db.squawk_notes.findByPk(id, options); + + await squawk_notes.update({ + deletedBy: currentUser.id + }, { + transaction, + }); + + await squawk_notes.destroy({ + transaction + }); + + return squawk_notes; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const squawk_notes = await db.squawk_notes.findOne( + { where }, + { transaction }, + ); + + if (!squawk_notes) { + return squawk_notes; + } + + const output = squawk_notes.get({plain: true}); + + return output; + } + + static async findAll(filter, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = []; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.content) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'squawk_notes', + 'content', + filter.content, + ), + }; + } + + if (filter.created_atRange) { + const [start, end] = filter.created_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + created_at: { + ...where.created_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + created_at: { + ...where.created_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.updated_atRange) { + const [start, end] = filter.updated_atRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + updated_at: { + ...where.updated_at, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + updated_at: { + ...where.updated_at, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true' + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + const queryOptions = { + where, + include, + distinct: true, + order: filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.squawk_notes.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete(query, limit, offset) { + let where = {}; + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike( + 'squawk_notes', + 'id', + query, + ), + ], + }; + } + + const records = await db.squawk_notes.findAll({ + attributes: [ 'id', 'id' ], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['id', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.id, + })); + } + +}; + diff --git a/backend/src/db/migrations/1755235073089.js b/backend/src/db/migrations/1755235073089.js new file mode 100644 index 0000000..af574b6 --- /dev/null +++ b/backend/src/db/migrations/1755235073089.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'aircrafts', + 'make', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'aircrafts', + 'make', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235092122.js b/backend/src/db/migrations/1755235092122.js new file mode 100644 index 0000000..05522ee --- /dev/null +++ b/backend/src/db/migrations/1755235092122.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'aircrafts', + 'model', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'aircrafts', + 'model', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235099304.js b/backend/src/db/migrations/1755235099304.js new file mode 100644 index 0000000..54bf4fc --- /dev/null +++ b/backend/src/db/migrations/1755235099304.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'aircrafts', + 'year', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'aircrafts', + 'year', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235116132.js b/backend/src/db/migrations/1755235116132.js new file mode 100644 index 0000000..7c3527d --- /dev/null +++ b/backend/src/db/migrations/1755235116132.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'aircrafts', + 'serial_number', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'aircrafts', + 'serial_number', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235129055.js b/backend/src/db/migrations/1755235129055.js new file mode 100644 index 0000000..d53eb7f --- /dev/null +++ b/backend/src/db/migrations/1755235129055.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'aircrafts', + 'engine_count', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'aircrafts', + 'engine_count', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235139703.js b/backend/src/db/migrations/1755235139703.js new file mode 100644 index 0000000..12a8305 --- /dev/null +++ b/backend/src/db/migrations/1755235139703.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'aircrafts', + 'engine_type', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'aircrafts', + 'engine_type', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235148389.js b/backend/src/db/migrations/1755235148389.js new file mode 100644 index 0000000..75ee8e7 --- /dev/null +++ b/backend/src/db/migrations/1755235148389.js @@ -0,0 +1,70 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.createTable('inspection_intervals', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.dropTable('inspection_intervals', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235167753.js b/backend/src/db/migrations/1755235167753.js new file mode 100644 index 0000000..6cb6148 --- /dev/null +++ b/backend/src/db/migrations/1755235167753.js @@ -0,0 +1,59 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'inspection_intervals', + 'aircraftId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'aircrafts', + key: 'id', + }, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'inspection_intervals', + 'aircraftId', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235182170.js b/backend/src/db/migrations/1755235182170.js new file mode 100644 index 0000000..40eb131 --- /dev/null +++ b/backend/src/db/migrations/1755235182170.js @@ -0,0 +1,56 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'inspection_intervals', + 'type', + { + type: Sequelize.DataTypes.ENUM, + + values: ['value'], + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'inspection_intervals', + 'type', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235190221.js b/backend/src/db/migrations/1755235190221.js new file mode 100644 index 0000000..5443ce5 --- /dev/null +++ b/backend/src/db/migrations/1755235190221.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'inspection_intervals', + 'interval_hours', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'inspection_intervals', + 'interval_hours', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235200131.js b/backend/src/db/migrations/1755235200131.js new file mode 100644 index 0000000..e53f754 --- /dev/null +++ b/backend/src/db/migrations/1755235200131.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'inspection_intervals', + 'interval_months', + { + type: Sequelize.DataTypes.INTEGER, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'inspection_intervals', + 'interval_months', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235235761.js b/backend/src/db/migrations/1755235235761.js new file mode 100644 index 0000000..46b05db --- /dev/null +++ b/backend/src/db/migrations/1755235235761.js @@ -0,0 +1,56 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'flights', + 'time_recorded_for', + { + type: Sequelize.DataTypes.ENUM, + + values: ['value'], + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'flights', + 'time_recorded_for', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235278015.js b/backend/src/db/migrations/1755235278015.js new file mode 100644 index 0000000..cbf4e9a --- /dev/null +++ b/backend/src/db/migrations/1755235278015.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'inspections', + 'performed_at', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'inspections', + 'performed_at', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235284654.js b/backend/src/db/migrations/1755235284654.js new file mode 100644 index 0000000..de1c6d4 --- /dev/null +++ b/backend/src/db/migrations/1755235284654.js @@ -0,0 +1,59 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'inspections', + 'performed_byId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'inspections', + 'performed_byId', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235302058.js b/backend/src/db/migrations/1755235302058.js new file mode 100644 index 0000000..a1c6034 --- /dev/null +++ b/backend/src/db/migrations/1755235302058.js @@ -0,0 +1,38 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235314511.js b/backend/src/db/migrations/1755235314511.js new file mode 100644 index 0000000..7652474 --- /dev/null +++ b/backend/src/db/migrations/1755235314511.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'organizations', + 'address', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'organizations', + 'address', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235321727.js b/backend/src/db/migrations/1755235321727.js new file mode 100644 index 0000000..b904c40 --- /dev/null +++ b/backend/src/db/migrations/1755235321727.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'organizations', + 'phone', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'organizations', + 'phone', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235329555.js b/backend/src/db/migrations/1755235329555.js new file mode 100644 index 0000000..aa33c79 --- /dev/null +++ b/backend/src/db/migrations/1755235329555.js @@ -0,0 +1,70 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.createTable('squawk_notes', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.dropTable('squawk_notes', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235335413.js b/backend/src/db/migrations/1755235335413.js new file mode 100644 index 0000000..17e7019 --- /dev/null +++ b/backend/src/db/migrations/1755235335413.js @@ -0,0 +1,59 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'squawk_notes', + 'squawkId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'squawks', + key: 'id', + }, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'squawk_notes', + 'squawkId', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235341823.js b/backend/src/db/migrations/1755235341823.js new file mode 100644 index 0000000..31dfb28 --- /dev/null +++ b/backend/src/db/migrations/1755235341823.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'squawk_notes', + 'content', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'squawk_notes', + 'content', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235349373.js b/backend/src/db/migrations/1755235349373.js new file mode 100644 index 0000000..a42dbed --- /dev/null +++ b/backend/src/db/migrations/1755235349373.js @@ -0,0 +1,59 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'squawk_notes', + 'created_byId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'squawk_notes', + 'created_byId', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235356802.js b/backend/src/db/migrations/1755235356802.js new file mode 100644 index 0000000..bebf405 --- /dev/null +++ b/backend/src/db/migrations/1755235356802.js @@ -0,0 +1,59 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'squawk_notes', + 'updated_byId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'users', + key: 'id', + }, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'squawk_notes', + 'updated_byId', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235372885.js b/backend/src/db/migrations/1755235372885.js new file mode 100644 index 0000000..4873d4e --- /dev/null +++ b/backend/src/db/migrations/1755235372885.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'squawk_notes', + 'created_at', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'squawk_notes', + 'created_at', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235386208.js b/backend/src/db/migrations/1755235386208.js new file mode 100644 index 0000000..c5baccc --- /dev/null +++ b/backend/src/db/migrations/1755235386208.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'squawk_notes', + 'updated_at', + { + type: Sequelize.DataTypes.DATE, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'squawk_notes', + 'updated_at', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235397461.js b/backend/src/db/migrations/1755235397461.js new file mode 100644 index 0000000..18aa231 --- /dev/null +++ b/backend/src/db/migrations/1755235397461.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'organizations', + 'website', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'organizations', + 'website', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235464158.js b/backend/src/db/migrations/1755235464158.js new file mode 100644 index 0000000..534a61b --- /dev/null +++ b/backend/src/db/migrations/1755235464158.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.addColumn( + 'organizations', + 'contact_email', + { + type: Sequelize.DataTypes.TEXT, + + }, + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await queryInterface.removeColumn( + 'organizations', + 'contact_email', + { transaction } + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/migrations/1755235464384.js b/backend/src/db/migrations/1755235464384.js new file mode 100644 index 0000000..a1c6034 --- /dev/null +++ b/backend/src/db/migrations/1755235464384.js @@ -0,0 +1,38 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/models/aircrafts.js b/backend/src/db/models/aircrafts.js index 58651d2..8764ca5 100644 --- a/backend/src/db/models/aircrafts.js +++ b/backend/src/db/models/aircrafts.js @@ -19,6 +19,36 @@ registration_number: { }, +make: { + type: DataTypes.TEXT, + + }, + +model: { + type: DataTypes.TEXT, + + }, + +year: { + type: DataTypes.INTEGER, + + }, + +serial_number: { + type: DataTypes.TEXT, + + }, + +engine_count: { + type: DataTypes.INTEGER, + + }, + +engine_type: { + type: DataTypes.TEXT, + + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, diff --git a/backend/src/db/models/flights.js b/backend/src/db/models/flights.js index c687584..cb359db 100644 --- a/backend/src/db/models/flights.js +++ b/backend/src/db/models/flights.js @@ -54,6 +54,17 @@ remarks: { }, +time_recorded_for: { + type: DataTypes.ENUM, + + values: [ + +"value" + + ], + + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, diff --git a/backend/src/db/models/inspection_intervals.js b/backend/src/db/models/inspection_intervals.js new file mode 100644 index 0000000..dff1774 --- /dev/null +++ b/backend/src/db/models/inspection_intervals.js @@ -0,0 +1,64 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const inspection_intervals = sequelize.define( + 'inspection_intervals', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +type: { + type: DataTypes.ENUM, + + values: [ + +"value" + + ], + + }, + +interval_hours: { + type: DataTypes.INTEGER, + + }, + +interval_months: { + type: DataTypes.INTEGER, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + inspection_intervals.associate = (db) => { + + db.inspection_intervals.belongsTo(db.users, { + as: 'createdBy', + }); + + db.inspection_intervals.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return inspection_intervals; +}; + diff --git a/backend/src/db/models/inspections.js b/backend/src/db/models/inspections.js index 4a4119b..f0d5451 100644 --- a/backend/src/db/models/inspections.js +++ b/backend/src/db/models/inspections.js @@ -41,6 +41,11 @@ type: { }, +performed_at: { + type: DataTypes.DATE, + + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, diff --git a/backend/src/db/models/organizations.js b/backend/src/db/models/organizations.js index 708ca1e..4ada4d6 100644 --- a/backend/src/db/models/organizations.js +++ b/backend/src/db/models/organizations.js @@ -19,6 +19,26 @@ name: { }, +address: { + type: DataTypes.TEXT, + + }, + +phone: { + type: DataTypes.TEXT, + + }, + +website: { + type: DataTypes.TEXT, + + }, + +contact_email: { + type: DataTypes.TEXT, + + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, diff --git a/backend/src/db/models/squawk_notes.js b/backend/src/db/models/squawk_notes.js new file mode 100644 index 0000000..90803b1 --- /dev/null +++ b/backend/src/db/models/squawk_notes.js @@ -0,0 +1,58 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function(sequelize, DataTypes) { + const squawk_notes = sequelize.define( + 'squawk_notes', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + +content: { + type: DataTypes.TEXT, + + }, + +created_at: { + type: DataTypes.DATE, + + }, + +updated_at: { + type: DataTypes.DATE, + + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + squawk_notes.associate = (db) => { + + db.squawk_notes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.squawk_notes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return squawk_notes; +}; + diff --git a/backend/src/db/seeders/20250815051908.js b/backend/src/db/seeders/20250815051908.js new file mode 100644 index 0000000..55961c8 --- /dev/null +++ b/backend/src/db/seeders/20250815051908.js @@ -0,0 +1,71 @@ + +const { v4: uuid } = require("uuid"); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require("../../config"); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { id: getId(`CREATE_${name.toUpperCase()}`), createdAt, updatedAt, name: `CREATE_${name.toUpperCase()}` }, + { id: getId(`READ_${name.toUpperCase()}`), createdAt, updatedAt, name: `READ_${name.toUpperCase()}` }, + { id: getId(`UPDATE_${name.toUpperCase()}`), createdAt, updatedAt, name: `UPDATE_${name.toUpperCase()}` }, + { id: getId(`DELETE_${name.toUpperCase()}`), createdAt, updatedAt, name: `DELETE_${name.toUpperCase()}` } + ]; + } + + const entities = [ + + "inspection_intervals", + + ]; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert("permissions", createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: {name: config.roles.admin} + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete("permissions", entities.flatMap(createPermissions)); + } + }; + diff --git a/backend/src/db/seeders/20250815052209.js b/backend/src/db/seeders/20250815052209.js new file mode 100644 index 0000000..f9953c8 --- /dev/null +++ b/backend/src/db/seeders/20250815052209.js @@ -0,0 +1,71 @@ + +const { v4: uuid } = require("uuid"); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require("../../config"); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { id: getId(`CREATE_${name.toUpperCase()}`), createdAt, updatedAt, name: `CREATE_${name.toUpperCase()}` }, + { id: getId(`READ_${name.toUpperCase()}`), createdAt, updatedAt, name: `READ_${name.toUpperCase()}` }, + { id: getId(`UPDATE_${name.toUpperCase()}`), createdAt, updatedAt, name: `UPDATE_${name.toUpperCase()}` }, + { id: getId(`DELETE_${name.toUpperCase()}`), createdAt, updatedAt, name: `DELETE_${name.toUpperCase()}` } + ]; + } + + const entities = [ + + "squawk_notes", + + ]; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert("permissions", createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: {name: config.roles.admin} + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete("permissions", entities.flatMap(createPermissions)); + } + }; + diff --git a/backend/src/index.js b/backend/src/index.js index 29f88f3..5b92881 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -34,6 +34,10 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const inspection_intervalsRoutes = require('./routes/inspection_intervals'); + +const squawk_notesRoutes = require('./routes/squawk_notes'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -106,6 +110,10 @@ app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoute app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes); +app.use('/api/inspection_intervals', passport.authenticate('jwt', {session: false}), inspection_intervalsRoutes); + +app.use('/api/squawk_notes', passport.authenticate('jwt', {session: false}), squawk_notesRoutes); + app.use('/api/contact-form', contactFormRoutes); app.use( diff --git a/backend/src/routes/aircrafts.js b/backend/src/routes/aircrafts.js index a367a8d..3576cc7 100644 --- a/backend/src/routes/aircrafts.js +++ b/backend/src/routes/aircrafts.js @@ -20,6 +20,25 @@ const { parse } = require('json2csv'); * registration_number: * type: string * default: registration_number + * make: + * type: string + * default: make + * model: + * type: string + * default: model + * serial_number: + * type: string + * default: serial_number + * engine_type: + * type: string + * default: engine_type + + * year: + * type: integer + * format: int64 + * engine_count: + * type: integer + * format: int64 */ @@ -276,7 +295,8 @@ router.get('/', wrapAsync(async (req, res) => { req.query, { currentUser } ); if (filetype && filetype === 'csv') { - const fields = ['id','registration_number', + const fields = ['id','registration_number','make','model','serial_number','engine_type', + 'year','engine_count', ]; const opts = { fields }; diff --git a/backend/src/routes/flights.js b/backend/src/routes/flights.js index 0bc106d..1515f8f 100644 --- a/backend/src/routes/flights.js +++ b/backend/src/routes/flights.js @@ -37,6 +37,7 @@ const { parse } = require('json2csv'); * type: integer * format: int64 + * */ /** diff --git a/backend/src/routes/inspection_intervals.js b/backend/src/routes/inspection_intervals.js new file mode 100644 index 0000000..c0bd866 --- /dev/null +++ b/backend/src/routes/inspection_intervals.js @@ -0,0 +1,415 @@ + +const express = require('express'); + +const Inspection_intervalsService = require('../services/inspection_intervals'); +const Inspection_intervalsDBApi = require('../db/api/inspection_intervals'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Inspection_intervals: + * type: object + * properties: + + * interval_hours: + * type: integer + * format: int64 + * interval_months: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Inspection_intervals + * description: The Inspection_intervals managing API + */ + +/** +* @swagger +* /api/inspection_intervals: +* post: +* security: +* - bearerAuth: [] +* tags: [Inspection_intervals] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Inspection_intervals" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Inspection_intervals" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Inspection_intervalsService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Inspection_intervals] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Inspection_intervals" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Inspection_intervals" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Inspection_intervalsService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/inspection_intervals/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Inspection_intervals] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Inspection_intervals" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Inspection_intervals" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Inspection_intervalsService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/inspection_intervals/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Inspection_intervals] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Inspection_intervals" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Inspection_intervalsService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/inspection_intervals/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Inspection_intervals] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Inspection_intervals" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Inspection_intervalsService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/inspection_intervals: + * get: + * security: + * - bearerAuth: [] + * tags: [Inspection_intervals] + * summary: Get all inspection_intervals + * description: Get all inspection_intervals + * responses: + * 200: + * description: Inspection_intervals list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Inspection_intervals" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Inspection_intervalsDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id', + 'interval_hours','interval_months', + + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/inspection_intervals/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Inspection_intervals] + * summary: Count all inspection_intervals + * description: Count all inspection_intervals + * responses: + * 200: + * description: Inspection_intervals count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Inspection_intervals" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Inspection_intervalsDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/inspection_intervals/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Inspection_intervals] + * summary: Find all inspection_intervals that match search criteria + * description: Find all inspection_intervals that match search criteria + * responses: + * 200: + * description: Inspection_intervals list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Inspection_intervals" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Inspection_intervalsDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/inspection_intervals/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Inspection_intervals] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Inspection_intervals" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Inspection_intervalsDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/inspections.js b/backend/src/routes/inspections.js index 2e352ab..11ed65a 100644 --- a/backend/src/routes/inspections.js +++ b/backend/src/routes/inspections.js @@ -275,7 +275,7 @@ router.get('/', wrapAsync(async (req, res) => { if (filetype && filetype === 'csv') { const fields = ['id', - 'last_performed','due_date', + 'last_performed','due_date','performed_at', ]; const opts = { fields }; try { diff --git a/backend/src/routes/organizations.js b/backend/src/routes/organizations.js index 536b890..c5244f5 100644 --- a/backend/src/routes/organizations.js +++ b/backend/src/routes/organizations.js @@ -20,6 +20,18 @@ const { parse } = require('json2csv'); * name: * type: string * default: name + * address: + * type: string + * default: address + * phone: + * type: string + * default: phone + * website: + * type: string + * default: website + * contact_email: + * type: string + * default: contact_email */ @@ -276,7 +288,7 @@ router.get('/', wrapAsync(async (req, res) => { req.query, { currentUser } ); if (filetype && filetype === 'csv') { - const fields = ['id','name', + const fields = ['id','name','address','phone','website','contact_email', ]; const opts = { fields }; diff --git a/backend/src/routes/squawk_notes.js b/backend/src/routes/squawk_notes.js new file mode 100644 index 0000000..482772e --- /dev/null +++ b/backend/src/routes/squawk_notes.js @@ -0,0 +1,411 @@ + +const express = require('express'); + +const Squawk_notesService = require('../services/squawk_notes'); +const Squawk_notesDBApi = require('../db/api/squawk_notes'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +/** + * @swagger + * components: + * schemas: + * Squawk_notes: + * type: object + * properties: + + * content: + * type: string + * default: content + + */ + +/** + * @swagger + * tags: + * name: Squawk_notes + * description: The Squawk_notes managing API + */ + +/** +* @swagger +* /api/squawk_notes: +* post: +* security: +* - bearerAuth: [] +* tags: [Squawk_notes] +* summary: Add new item +* description: Add new item +* requestBody: +* required: true +* content: +* application/json: +* schema: +* properties: +* data: +* description: Data of the updated item +* type: object +* $ref: "#/components/schemas/Squawk_notes" +* responses: +* 200: +* description: The item was successfully added +* content: +* application/json: +* schema: +* $ref: "#/components/schemas/Squawk_notes" +* 401: +* $ref: "#/components/responses/UnauthorizedError" +* 405: +* description: Invalid input data +* 500: +* description: Some server error +*/ +router.post('/', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Squawk_notesService.create(req.body.data, req.currentUser, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/budgets/bulk-import: + * post: + * security: + * - bearerAuth: [] + * tags: [Squawk_notes] + * summary: Bulk import items + * description: Bulk import items + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * data: + * description: Data of the updated items + * type: array + * items: + * $ref: "#/components/schemas/Squawk_notes" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Squawk_notes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 405: + * description: Invalid input data + * 500: + * description: Some server error + * + */ +router.post('/bulk-import', wrapAsync(async (req, res) => { + const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; + const link = new URL(referer); + await Squawk_notesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/squawk_notes/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Squawk_notes] + * summary: Update the data of the selected item + * description: Update the data of the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to update + * required: true + * schema: + * type: string + * requestBody: + * description: Set new item data + * required: true + * content: + * application/json: + * schema: + * properties: + * id: + * description: ID of the updated item + * type: string + * data: + * description: Data of the updated item + * type: object + * $ref: "#/components/schemas/Squawk_notes" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Squawk_notes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.put('/:id', wrapAsync(async (req, res) => { + await Squawk_notesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/squawk_notes/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Squawk_notes] + * summary: Delete the selected item + * description: Delete the selected item + * parameters: + * - in: path + * name: id + * description: Item ID to delete + * required: true + * schema: + * type: string + * responses: + * 200: + * description: The item was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Squawk_notes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.delete('/:id', wrapAsync(async (req, res) => { + await Squawk_notesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/squawk_notes/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Squawk_notes] + * summary: Delete the selected item list + * description: Delete the selected item list + * requestBody: + * required: true + * content: + * application/json: + * schema: + * properties: + * ids: + * description: IDs of the updated items + * type: array + * responses: + * 200: + * description: The items was successfully deleted + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Squawk_notes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post('/deleteByIds', wrapAsync(async (req, res) => { + await Squawk_notesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + })); + +/** + * @swagger + * /api/squawk_notes: + * get: + * security: + * - bearerAuth: [] + * tags: [Squawk_notes] + * summary: Get all squawk_notes + * description: Get all squawk_notes + * responses: + * 200: + * description: Squawk_notes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Squawk_notes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error +*/ +router.get('/', wrapAsync(async (req, res) => { + const filetype = req.query.filetype + const currentUser = req.currentUser; + const payload = await Squawk_notesDBApi.findAll( + req.query, { currentUser } + ); + if (filetype && filetype === 'csv') { + const fields = ['id','content', + + 'created_at','updated_at', + ]; + const opts = { fields }; + try { + const csv = parse(payload.rows, opts); + res.status(200).attachment(csv); + res.send(csv) + + } catch (err) { + console.error(err); + } + } else { + res.status(200).send(payload); + } + +})); + +/** + * @swagger + * /api/squawk_notes/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Squawk_notes] + * summary: Count all squawk_notes + * description: Count all squawk_notes + * responses: + * 200: + * description: Squawk_notes count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Squawk_notes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/count', wrapAsync(async (req, res) => { + const currentUser = req.currentUser; + const payload = await Squawk_notesDBApi.findAll( + req.query, + null, + { countOnly: true, currentUser } + ); + + res.status(200).send(payload); +})); + +/** + * @swagger + * /api/squawk_notes/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Squawk_notes] + * summary: Find all squawk_notes that match search criteria + * description: Find all squawk_notes that match search criteria + * responses: + * 200: + * description: Squawk_notes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Squawk_notes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await Squawk_notesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/squawk_notes/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Squawk_notes] + * summary: Get selected item + * description: Get selected item + * parameters: + * - in: path + * name: id + * description: ID of item to get + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Selected item successfully received + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Squawk_notes" + * 400: + * description: Invalid ID supplied + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Item not found + * 500: + * description: Some server error + */ +router.get('/:id', wrapAsync(async (req, res) => { + const payload = await Squawk_notesDBApi.findBy( + { id: req.params.id }, + ); + + res.status(200).send(payload); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/inspection_intervals.js b/backend/src/services/inspection_intervals.js new file mode 100644 index 0000000..d11829f --- /dev/null +++ b/backend/src/services/inspection_intervals.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Inspection_intervalsDBApi = require('../db/api/inspection_intervals'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Inspection_intervalsService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Inspection_intervalsDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Inspection_intervalsDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let inspection_intervals = await Inspection_intervalsDBApi.findBy( + {id}, + {transaction}, + ); + + if (!inspection_intervals) { + throw new ValidationError( + 'inspection_intervalsNotFound', + ); + } + + const updatedInspection_intervals = await Inspection_intervalsDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedInspection_intervals; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Inspection_intervalsDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Inspection_intervalsDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/backend/src/services/search.js b/backend/src/services/search.js index c63874e..cf484b5 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -59,6 +59,14 @@ module.exports = class SearchService { "registration_number", + "make", + + "model", + + "serial_number", + + "engine_type", + ], "flights": [ @@ -75,6 +83,14 @@ module.exports = class SearchService { "name", + "address", + + "phone", + + "website", + + "contact_email", + ], "squawks": [ @@ -91,9 +107,23 @@ module.exports = class SearchService { ], + "squawk_notes": [ + + "content", + + ], + }; const columnsInt = { + "aircrafts": [ + + "year", + + "engine_count", + + ], + "flights": [ "start_tach_time", @@ -104,6 +134,14 @@ module.exports = class SearchService { ], + "inspection_intervals": [ + + "interval_hours", + + "interval_months", + + ], + }; let allFoundRecords = []; diff --git a/backend/src/services/squawk_notes.js b/backend/src/services/squawk_notes.js new file mode 100644 index 0000000..d0e9e09 --- /dev/null +++ b/backend/src/services/squawk_notes.js @@ -0,0 +1,131 @@ +const db = require('../db/models'); +const Squawk_notesDBApi = require('../db/api/squawk_notes'); +const processFile = require("../middlewares/upload"); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class Squawk_notesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await Squawk_notesDBApi.create( + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }) + + await Squawk_notesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let squawk_notes = await Squawk_notesDBApi.findBy( + {id}, + {transaction}, + ); + + if (!squawk_notes) { + throw new ValidationError( + 'squawk_notesNotFound', + ); + } + + const updatedSquawk_notes = await Squawk_notesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedSquawk_notes; + + } catch (error) { + await transaction.rollback(); + throw error; + } + }; + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Squawk_notesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await Squawk_notesDBApi.remove( + id, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; + diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Aircrafts/CardAircrafts.tsx b/frontend/src/components/Aircrafts/CardAircrafts.tsx index bb5af85..7c986ac 100644 --- a/frontend/src/components/Aircrafts/CardAircrafts.tsx +++ b/frontend/src/components/Aircrafts/CardAircrafts.tsx @@ -88,6 +88,60 @@ const CardAircrafts = ({ +
+
Make
+
+
+ { item.make } +
+
+
+ +
+
Model
+
+
+ { item.model } +
+
+
+ +
+
Year
+
+
+ { item.year } +
+
+
+ +
+
Serial number
+
+
+ { item.serial_number } +
+
+
+ +
+
Engine count
+
+
+ { item.engine_count } +
+
+
+ +
+
Engine type
+
+
+ { item.engine_type } +
+
+
+ ))} diff --git a/frontend/src/components/Aircrafts/ListAircrafts.tsx b/frontend/src/components/Aircrafts/ListAircrafts.tsx index 6aa65f7..a5fa981 100644 --- a/frontend/src/components/Aircrafts/ListAircrafts.tsx +++ b/frontend/src/components/Aircrafts/ListAircrafts.tsx @@ -50,6 +50,36 @@ const ListAircrafts = ({ aircrafts, loading, onDelete, currentPage, numPages, on

{ item.technicians }

+
+

Make

+

{ item.make }

+
+ +
+

Model

+

{ item.model }

+
+ +
+

Year

+

{ item.year }

+
+ +
+

Serial number

+

{ item.serial_number }

+
+ +
+

Engine count

+

{ item.engine_count }

+
+ +
+

Engine type

+

{ item.engine_type }

+
+ +
+
Time recorded for
+
+
+ { item.time_recorded_for } +
+
+
+ ))} diff --git a/frontend/src/components/Flights/ListFlights.tsx b/frontend/src/components/Flights/ListFlights.tsx index 15bd941..459e54a 100644 --- a/frontend/src/components/Flights/ListFlights.tsx +++ b/frontend/src/components/Flights/ListFlights.tsx @@ -80,6 +80,11 @@ const ListFlights = ({ flights, loading, onDelete, currentPage, numPages, onPage

{ item.remarks }

+
+

Time recorded for

+

{ item.time_recorded_for }

+
+ void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardInspection_intervals = ({ + inspection_intervals, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
+ {loading && } +
    + {!loading && inspection_intervals.map((item, index) => ( +
  • + + {item.id} + + +
    + +
    +
+
+ +
+
Aircraft
+
+
+ { dataFormatter.aircraftsOneListFormatter(item.aircraft) } +
+
+
+ +
+
Type
+
+
+ { item.type } +
+
+
+ +
+
Interval hours
+
+
+ { item.interval_hours } +
+
+
+ +
+
Interval months
+
+
+ { item.interval_months } +
+
+
+ +
+ + ))} + {!loading && inspection_intervals.length === 0 && ( +
+

No data to display

+
+ )} + +
+ +
+ + ); +}; + +export default CardInspection_intervals; diff --git a/frontend/src/components/Inspection_intervals/ListInspection_intervals.tsx b/frontend/src/components/Inspection_intervals/ListInspection_intervals.tsx new file mode 100644 index 0000000..1e23a4a --- /dev/null +++ b/frontend/src/components/Inspection_intervals/ListInspection_intervals.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + inspection_intervals: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListInspection_intervals = ({ inspection_intervals, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && inspection_intervals.map((item) => ( +
+ +
+ + +
+

Aircraft

+

{ dataFormatter.aircraftsOneListFormatter(item.aircraft) }

+
+ +
+

Type

+

{ item.type }

+
+ +
+

Interval hours

+

{ item.interval_hours }

+
+ +
+

Interval months

+

{ item.interval_months }

+
+ + + +
+
+
+ ))} + {!loading && inspection_intervals.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ) +}; + +export default ListInspection_intervals diff --git a/frontend/src/components/Inspection_intervals/TableInspection_intervals.tsx b/frontend/src/components/Inspection_intervals/TableInspection_intervals.tsx new file mode 100644 index 0000000..a157698 --- /dev/null +++ b/frontend/src/components/Inspection_intervals/TableInspection_intervals.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/inspection_intervals/inspection_intervalsSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureInspection_intervalsCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleInspection_intervals = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { inspection_intervals, loading, count, notify: inspection_intervalsNotify, refetch } = useAppSelector((state) => state.inspection_intervals) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (inspection_intervalsNotify.showNotification) { + notify(inspection_intervalsNotify.typeNotification, inspection_intervalsNotify.textNotification); + } + }, [inspection_intervalsNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `inspection_intervals`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={inspection_intervals ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
+ <> + {filterItems && filterItems.map((filterItem) => { + return ( +
+
+
Filter
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
+
+ Value +
+ + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
+ ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
+
+
From
+ +
+
+
To
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
+
+
+ From +
+ +
+
+
To
+ +
+
+ ) : ( +
+
Contains
+ +
+ )} +
+
Action
+ { + deleteFilter(filterItem.id) + }} + /> +
+
+ ) + })} +
+ + +
+ +
+
+
: null + } + +

Are you sure you want to delete this item?

+
+ {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleInspection_intervals diff --git a/frontend/src/components/Inspection_intervals/configureInspection_intervalsCols.tsx b/frontend/src/components/Inspection_intervals/configureInspection_intervalsCols.tsx new file mode 100644 index 0000000..d64fd76 --- /dev/null +++ b/frontend/src/components/Inspection_intervals/configureInspection_intervalsCols.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'aircraft', + headerName: 'Aircraft', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('aircrafts'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'type', + headerName: 'Type', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'singleSelect', + valueOptions: [ + + "value" , + + ], + + }, + + { + field: 'interval_hours', + headerName: 'Interval hours', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'interval_months', + headerName: 'Interval months', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'number', + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
+ +
, + ] + }, + }, + ]; +}; diff --git a/frontend/src/components/Inspections/CardInspections.tsx b/frontend/src/components/Inspections/CardInspections.tsx index 7f86e7b..152da47 100644 --- a/frontend/src/components/Inspections/CardInspections.tsx +++ b/frontend/src/components/Inspections/CardInspections.tsx @@ -97,6 +97,24 @@ const CardInspections = ({ +
+
Performed at
+
+
+ { dataFormatter.dateTimeFormatter(item.performed_at) } +
+
+
+ +
+
Performed_by
+
+
+ { dataFormatter.usersOneListFormatter(item.performed_by) } +
+
+
+ ))} diff --git a/frontend/src/components/Inspections/ListInspections.tsx b/frontend/src/components/Inspections/ListInspections.tsx index c85d203..58d923c 100644 --- a/frontend/src/components/Inspections/ListInspections.tsx +++ b/frontend/src/components/Inspections/ListInspections.tsx @@ -55,6 +55,16 @@ const ListInspections = ({ inspections, loading, onDelete, currentPage, numPages

{ item.type }

+
+

Performed at

+

{ dataFormatter.dateTimeFormatter(item.performed_at) }

+
+ +
+

Performed_by

+

{ dataFormatter.usersOneListFormatter(item.performed_by) }

+
+ + new Date(params.row.performed_at), + + }, + + { + field: 'performed_by', + headerName: 'Performed_by', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/components/Organizations/CardOrganizations.tsx b/frontend/src/components/Organizations/CardOrganizations.tsx index 01715bc..d538b54 100644 --- a/frontend/src/components/Organizations/CardOrganizations.tsx +++ b/frontend/src/components/Organizations/CardOrganizations.tsx @@ -79,6 +79,51 @@ const CardOrganizations = ({ +
+
Avatar
+
+
+ { item.avatar } +
+
+
+ +
+
Address
+
+
+ { item.address } +
+
+
+ +
+
Phone
+
+
+ { item.phone } +
+
+
+ +
+
Website
+
+
+ { item.website } +
+
+
+ +
+
Contact email
+
+
+ { item.contact_email } +
+
+
+ ))} diff --git a/frontend/src/components/Organizations/ListOrganizations.tsx b/frontend/src/components/Organizations/ListOrganizations.tsx index c7af032..90a8b4d 100644 --- a/frontend/src/components/Organizations/ListOrganizations.tsx +++ b/frontend/src/components/Organizations/ListOrganizations.tsx @@ -45,6 +45,31 @@ const ListOrganizations = ({ organizations, loading, onDelete, currentPage, numP

{ item.members }

+
+

Avatar

+

{ item.avatar }

+
+ +
+

Address

+

{ item.address }

+
+ +
+

Phone

+

{ item.phone }

+
+ +
+

Website

+

{ item.website }

+
+ +
+

Contact email

+

{ item.contact_email }

+
+ void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardSquawk_notes = ({ + squawk_notes, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + return ( +
+ {loading && } +
    + {!loading && squawk_notes.map((item, index) => ( +
  • + + {item.id} + + +
    + +
    +
+
+ +
+
Squawk
+
+
+ { dataFormatter.squawksOneListFormatter(item.squawk) } +
+
+
+ +
+
Content
+
+
+ { item.content } +
+
+
+ +
+
Created_by
+
+
+ { dataFormatter.usersOneListFormatter(item.created_by) } +
+
+
+ +
+
Updated_by
+
+
+ { dataFormatter.usersOneListFormatter(item.updated_by) } +
+
+
+ +
+
Created at
+
+
+ { dataFormatter.dateTimeFormatter(item.created_at) } +
+
+
+ +
+
Updated at
+
+
+ { dataFormatter.dateTimeFormatter(item.updated_at) } +
+
+
+ +
+ + ))} + {!loading && squawk_notes.length === 0 && ( +
+

No data to display

+
+ )} + +
+ +
+ + ); +}; + +export default CardSquawk_notes; diff --git a/frontend/src/components/Squawk_notes/ListSquawk_notes.tsx b/frontend/src/components/Squawk_notes/ListSquawk_notes.tsx new file mode 100644 index 0000000..efd3cda --- /dev/null +++ b/frontend/src/components/Squawk_notes/ListSquawk_notes.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import dataFormatter from '../../helpers/dataFormatter'; +import ListActionsPopover from "../ListActionsPopover"; +import {useAppSelector} from "../../stores/hooks"; +import {Pagination} from "../Pagination"; +import LoadingSpinner from "../LoadingSpinner"; +import Link from 'next/link'; + +type Props = { + squawk_notes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListSquawk_notes = ({ squawk_notes, loading, onDelete, currentPage, numPages, onPageChange }: Props) => { + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && squawk_notes.map((item) => ( +
+ +
+ + +
+

Squawk

+

{ dataFormatter.squawksOneListFormatter(item.squawk) }

+
+ +
+

Content

+

{ item.content }

+
+ +
+

Created_by

+

{ dataFormatter.usersOneListFormatter(item.created_by) }

+
+ +
+

Updated_by

+

{ dataFormatter.usersOneListFormatter(item.updated_by) }

+
+ +
+

Created at

+

{ dataFormatter.dateTimeFormatter(item.created_at) }

+
+ +
+

Updated at

+

{ dataFormatter.dateTimeFormatter(item.updated_at) }

+
+ + + +
+
+
+ ))} + {!loading && squawk_notes.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ) +}; + +export default ListSquawk_notes diff --git a/frontend/src/components/Squawk_notes/TableSquawk_notes.tsx b/frontend/src/components/Squawk_notes/TableSquawk_notes.tsx new file mode 100644 index 0000000..d3c1541 --- /dev/null +++ b/frontend/src/components/Squawk_notes/TableSquawk_notes.tsx @@ -0,0 +1,441 @@ +import React, { useEffect, useState, useMemo } from 'react' +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton' +import CardBoxModal from '../CardBoxModal' +import CardBox from "../CardBox"; +import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/squawk_notes/squawk_notesSlice' +import { useAppDispatch, useAppSelector } from '../../stores/hooks' +import { useRouter } from 'next/router' +import { Field, Form, Formik } from "formik"; +import { + DataGrid, + GridColDef, +} from '@mui/x-data-grid'; +import {loadColumns} from "./configureSquawk_notesCols"; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter' +import {dataGridStyles} from "../../styles"; +import axios from 'axios'; + +const perPage = 10; + +const TableSampleSquawk_notes = ({ filterItems, setFilterItems, filters, showGrid }) => { + const notify = (type, msg) => toast( msg, {type, position: "bottom-center"}); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + const { squawk_notes, loading, count, notify: squawk_notesNotify, refetch } = useAppSelector((state) => state.squawk_notes) + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (squawk_notesNotify.showNotification) { + notify(squawk_notesNotify.typeNotification, squawk_notesNotify.textNotification); + } + }, [squawk_notesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false) + const [isModalTrashActive, setIsModalTrashActive] = useState(false) + + const handleModalAction = () => { + setIsModalInfoActive(false) + setIsModalTrashActive(false) + } + + const handleDeleteModalAction = (id: string) => { + setId(id) + setIsModalTrashActive(true) + } + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } } + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + loadColumns(handleDeleteModalAction, `squawk_notes`).then((newCols) => + setColumns(newCols), + ); + }, []); + + const handleTableSubmit = async (id: string, data) => { + + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 border-gray-700 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={squawk_notes ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids) + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ) + + return ( + <> + {filterItems && Array.isArray( filterItems ) && filterItems.length ? + + null} + > +
+ <> + {filterItems && filterItems.map((filterItem) => { + return ( +
+
+
Filter
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.type === 'enum' ? ( +
+
+ Value +
+ + + {filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.options?.map((option) => ( + + ))} + +
+ ) : filters.find((filter) => + filter.title === filterItem?.fields?.selectedField + )?.number ? ( +
+
+
From
+ +
+
+
To
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField + )?.date ? ( +
+
+
+ From +
+ +
+
+
To
+ +
+
+ ) : ( +
+
Contains
+ +
+ )} +
+
Action
+ { + deleteFilter(filterItem.id) + }} + /> +
+
+ ) + })} +
+ + +
+ +
+
+
: null + } + +

Are you sure you want to delete this item?

+
+ {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ) +} + +export default TableSampleSquawk_notes diff --git a/frontend/src/components/Squawk_notes/configureSquawk_notesCols.tsx b/frontend/src/components/Squawk_notes/configureSquawk_notesCols.tsx new file mode 100644 index 0000000..f690b4e --- /dev/null +++ b/frontend/src/components/Squawk_notes/configureSquawk_notesCols.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import dataFormatter from '../../helpers/dataFormatter' +import DataGridMultiSelect from "../DataGridMultiSelect"; +import ListActionsPopover from '../ListActionsPopover'; +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, +) => { + async function callOptionsApi(entityName: string) { + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + return [ + + { + field: 'squawk', + headerName: 'Squawk', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('squawks'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'content', + headerName: 'Content', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + + }, + + { + field: 'created_by', + headerName: 'Created_by', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'updated_by', + headerName: 'Updated_by', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + sortable: false, + type: 'singleSelect', + getOptionValue: (value: any) => value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('users'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + + }, + + { + field: 'created_at', + headerName: 'Created at', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.created_at), + + }, + + { + field: 'updated_at', + headerName: 'Updated at', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: true, + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.updated_at), + + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + + return [ +
+ +
, + ] + }, + }, + ]; +}; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index d29200a..39ffea1 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -64,6 +64,22 @@ const menuAside: MenuAsideItem[] = [ icon: 'mdiFileDocumentEditOutline' in icon ? icon['mdiFileDocumentEditOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, permissions: 'READ_WORK_ORDERS' }, + { + href: '/inspection_intervals/inspection_intervals-list', + label: 'Inspection intervals', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_INSPECTION_INTERVALS' + }, + { + href: '/squawk_notes/squawk_notes-list', + label: 'Squawk notes', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_SQUAWK_NOTES' + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/aircrafts/[aircraftsId].tsx b/frontend/src/pages/aircrafts/[aircraftsId].tsx index 7436563..5a2d5f5 100644 --- a/frontend/src/pages/aircrafts/[aircraftsId].tsx +++ b/frontend/src/pages/aircrafts/[aircraftsId].tsx @@ -36,6 +36,18 @@ const EditAircrafts = () => { owner: null, + 'make': '', + + 'model': '', + + year: '', + + 'serial_number': '', + + engine_count: '', + + 'engine_type': '', + } const [initialValues, setInitialValues] = useState(initVals) @@ -108,6 +120,62 @@ const EditAircrafts = () => { > + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/aircrafts/aircrafts-edit.tsx b/frontend/src/pages/aircrafts/aircrafts-edit.tsx index 87e0935..fa11936 100644 --- a/frontend/src/pages/aircrafts/aircrafts-edit.tsx +++ b/frontend/src/pages/aircrafts/aircrafts-edit.tsx @@ -37,6 +37,18 @@ const EditAircraftsPage = () => { owner: null, + 'make': '', + + 'model': '', + + year: '', + + 'serial_number': '', + + engine_count: '', + + 'engine_type': '', + } const [initialValues, setInitialValues] = useState(initVals) @@ -106,6 +118,62 @@ const EditAircraftsPage = () => { > + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/aircrafts/aircrafts-list.tsx b/frontend/src/pages/aircrafts/aircrafts-list.tsx index d280b12..92e2b60 100644 --- a/frontend/src/pages/aircrafts/aircrafts-list.tsx +++ b/frontend/src/pages/aircrafts/aircrafts-list.tsx @@ -22,7 +22,8 @@ const AircraftsTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{label: 'RegistrationNumber', title: 'registration_number'}, + const [filters] = useState([{label: 'RegistrationNumber', title: 'registration_number'},{label: 'Make', title: 'make'},{label: 'Model', title: 'model'},{label: 'Serial number', title: 'serial_number'},{label: 'Engine type', title: 'engine_type'}, + {label: 'Year', title: 'year', number: 'true'},{label: 'Engine count', title: 'engine_count', number: 'true'}, ]); const addFilter = () => { diff --git a/frontend/src/pages/aircrafts/aircrafts-new.tsx b/frontend/src/pages/aircrafts/aircrafts-new.tsx index da34a0f..7fc5c8e 100644 --- a/frontend/src/pages/aircrafts/aircrafts-new.tsx +++ b/frontend/src/pages/aircrafts/aircrafts-new.tsx @@ -29,6 +29,18 @@ const initialValues = { owner: '', + make: '', + + model: '', + + year: '', + + serial_number: '', + + engine_count: '', + + engine_type: '', + } const AircraftsNew = () => { @@ -70,6 +82,62 @@ const AircraftsNew = () => { + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/aircrafts/aircrafts-table.tsx b/frontend/src/pages/aircrafts/aircrafts-table.tsx index bc97d99..033db8c 100644 --- a/frontend/src/pages/aircrafts/aircrafts-table.tsx +++ b/frontend/src/pages/aircrafts/aircrafts-table.tsx @@ -22,7 +22,8 @@ const AircraftsTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([{label: 'RegistrationNumber', title: 'registration_number'}, + const [filters] = useState([{label: 'RegistrationNumber', title: 'registration_number'},{label: 'Make', title: 'make'},{label: 'Model', title: 'model'},{label: 'Serial number', title: 'serial_number'},{label: 'Engine type', title: 'engine_type'}, + {label: 'Year', title: 'year', number: 'true'},{label: 'Engine count', title: 'engine_count', number: 'true'}, ]); const addFilter = () => { diff --git a/frontend/src/pages/aircrafts/aircrafts-view.tsx b/frontend/src/pages/aircrafts/aircrafts-view.tsx index 8248bc8..59d11e1 100644 --- a/frontend/src/pages/aircrafts/aircrafts-view.tsx +++ b/frontend/src/pages/aircrafts/aircrafts-view.tsx @@ -61,6 +61,36 @@ const AircraftsView = () => { +
+

Make

+

{aircrafts?.make}

+
+ +
+

Model

+

{aircrafts?.model}

+
+ +
+

Year

+

{aircrafts?.year || 'No data'}

+
+ +
+

Serial number

+

{aircrafts?.serial_number}

+
+ +
+

Engine count

+

{aircrafts?.engine_count || 'No data'}

+
+ +
+

Engine type

+

{aircrafts?.engine_type}

+
+ <>

Flights Aircraft

{ Remarks + Time recorded for + @@ -127,6 +159,10 @@ const AircraftsView = () => { { item.remarks } + + { item.time_recorded_for } + + ))} @@ -153,6 +189,8 @@ const AircraftsView = () => { Type + Performed at + @@ -172,6 +210,10 @@ const AircraftsView = () => { { item.type } + + { dataFormatter.dateTimeFormatter(item.performed_at) } + + ))} @@ -259,6 +301,51 @@ const AircraftsView = () => { + <> +

Inspection_intervals Aircraft

+ +
+ + + + + + + + + + + + + + {aircrafts.inspection_intervals_aircraft && Array.isArray(aircrafts.inspection_intervals_aircraft) && + aircrafts.inspection_intervals_aircraft.map((item: any) => ( + router.push(`/inspection_intervals/inspection_intervals-view/?id=${item.id}`)}> + + + + + + + + + ))} + +
TypeInterval hoursInterval months
+ { item.type } + + { item.interval_hours } + + { item.interval_months } +
+
+ {!aircrafts?.inspection_intervals_aircraft?.length &&
No data
} +
+ + { const [work_orders, setWork_orders] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [inspection_intervals, setInspection_intervals] = React.useState(loadingMessage); + const [squawk_notes, setSquawk_notes] = React.useState(loadingMessage); async function loadData() { - const entities = ['users','aircrafts','flights','inspections','organizations','squawks','work_orders','roles','permissions',]; - const fns = [setUsers,setAircrafts,setFlights,setInspections,setOrganizations,setSquawks,setWork_orders,setRoles,setPermissions,]; + const entities = ['users','aircrafts','flights','inspections','organizations','squawks','work_orders','roles','permissions','inspection_intervals','squawk_notes',]; + const fns = [setUsers,setAircrafts,setFlights,setInspections,setOrganizations,setSquawks,setWork_orders,setRoles,setPermissions,setInspection_intervals,setSquawk_notes,]; const requests = entities.map((entity, index) => { return axios.get(`/${entity.toLowerCase()}/count`); @@ -263,6 +265,62 @@ const Dashboard = () => { + +
+
+
+
+ Inspection intervals +
+
+ {inspection_intervals} +
+
+
+ +
+
+
+ + + +
+
+
+
+ Squawk notes +
+
+ {squawk_notes} +
+
+
+ +
+
+
+ + diff --git a/frontend/src/pages/flights/[flightsId].tsx b/frontend/src/pages/flights/[flightsId].tsx index 96bd54c..445897f 100644 --- a/frontend/src/pages/flights/[flightsId].tsx +++ b/frontend/src/pages/flights/[flightsId].tsx @@ -50,6 +50,8 @@ const EditFlights = () => { remarks: '', + time_recorded_for: '', + } const [initialValues, setInitialValues] = useState(initVals) @@ -195,6 +197,16 @@ const EditFlights = () => { + + + + + + + + + + diff --git a/frontend/src/pages/flights/flights-edit.tsx b/frontend/src/pages/flights/flights-edit.tsx index fbb8b66..1e7bbf3 100644 --- a/frontend/src/pages/flights/flights-edit.tsx +++ b/frontend/src/pages/flights/flights-edit.tsx @@ -51,6 +51,8 @@ const EditFlightsPage = () => { remarks: '', + time_recorded_for: '', + } const [initialValues, setInitialValues] = useState(initVals) @@ -193,6 +195,16 @@ const EditFlightsPage = () => { + + + + + + + + + + diff --git a/frontend/src/pages/flights/flights-list.tsx b/frontend/src/pages/flights/flights-list.tsx index ae27aa1..38342ea 100644 --- a/frontend/src/pages/flights/flights-list.tsx +++ b/frontend/src/pages/flights/flights-list.tsx @@ -27,6 +27,7 @@ const FlightsTablesPage = () => { {label: 'StartTachTime', title: 'start_tach_time', number: 'true'},{label: 'EndTachTime', title: 'end_tach_time', number: 'true'},{label: 'HobbsTime', title: 'hobbs_time', number: 'true'}, {label: 'StartTime', title: 'start_time', date: 'true'},{label: 'EndTime', title: 'end_time', date: 'true'}, + {label: 'Time recorded for', title: 'time_recorded_for', type: 'enum', options: ['value']}, ]); const addFilter = () => { const newItem = { diff --git a/frontend/src/pages/flights/flights-new.tsx b/frontend/src/pages/flights/flights-new.tsx index efbd5cb..965a5ae 100644 --- a/frontend/src/pages/flights/flights-new.tsx +++ b/frontend/src/pages/flights/flights-new.tsx @@ -43,6 +43,8 @@ const initialValues = { remarks: '', + time_recorded_for: '', + } const FlightsNew = () => { @@ -147,6 +149,16 @@ const FlightsNew = () => { + + + + + + + + + + diff --git a/frontend/src/pages/flights/flights-table.tsx b/frontend/src/pages/flights/flights-table.tsx index afb2606..ddc7249 100644 --- a/frontend/src/pages/flights/flights-table.tsx +++ b/frontend/src/pages/flights/flights-table.tsx @@ -27,6 +27,7 @@ const FlightsTablesPage = () => { {label: 'StartTachTime', title: 'start_tach_time', number: 'true'},{label: 'EndTachTime', title: 'end_tach_time', number: 'true'},{label: 'HobbsTime', title: 'hobbs_time', number: 'true'}, {label: 'StartTime', title: 'start_time', date: 'true'},{label: 'EndTime', title: 'end_time', date: 'true'}, + {label: 'Time recorded for', title: 'time_recorded_for', type: 'enum', options: ['value']}, ]); const addFilter = () => { const newItem = { diff --git a/frontend/src/pages/flights/flights-view.tsx b/frontend/src/pages/flights/flights-view.tsx index 11ba527..1331c6e 100644 --- a/frontend/src/pages/flights/flights-view.tsx +++ b/frontend/src/pages/flights/flights-view.tsx @@ -111,6 +111,11 @@ const FlightsView = () => {