From 01c30269c2ae5e56776f0efb60c7e7fae75f5505 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 9 Sep 2025 20:55:14 +0000 Subject: [PATCH] 1.1 --- .gitignore | 5 + 502.html | 2 +- README.md | 2 +- app-shell/src/_schema.json | 7 +- backend/README.md | 6 +- backend/package.json | 4 +- backend/src/config.js | 4 +- backend/src/db/api/bookings.js | 42 ++ backend/src/db/api/clients.js | 5 + backend/src/db/api/powersportvehicles.js | 609 ++++++++++++++++++ backend/src/db/api/vehicles.js | 12 + backend/src/db/db.config.js | 2 +- backend/src/db/migrations/1757449078196.js | 51 ++ backend/src/db/migrations/1757450562030.js | 90 +++ backend/src/db/migrations/1757450589992.js | 49 ++ backend/src/db/migrations/1757450623285.js | 49 ++ backend/src/db/migrations/1757450719241.js | 36 ++ backend/src/db/migrations/1757450790548.js | 49 ++ backend/src/db/migrations/1757450827833.js | 49 ++ backend/src/db/migrations/1757450860278.js | 49 ++ backend/src/db/migrations/1757450889961.js | 51 ++ backend/src/db/migrations/1757450927629.js | 51 ++ backend/src/db/migrations/1757450953122.js | 51 ++ backend/src/db/migrations/1757450984490.js | 54 ++ backend/src/db/migrations/1757451017415.js | 51 ++ backend/src/db/migrations/1757451046441.js | 51 ++ backend/src/db/migrations/1757451083313.js | 51 ++ backend/src/db/models/bookings.js | 8 + backend/src/db/models/clients.js | 8 + backend/src/db/models/powersportvehicles.js | 117 ++++ backend/src/db/models/vehicles.js | 6 + .../db/seeders/20200430130760-user-roles.js | 51 ++ .../db/seeders/20231127130745-sample-data.js | 336 +++++----- backend/src/db/seeders/20250909204242.js | 87 +++ backend/src/index.js | 12 +- backend/src/routes/powersportvehicles.js | 495 ++++++++++++++ backend/src/routes/vehicles.js | 1 + backend/src/services/notifications/list.js | 2 +- backend/src/services/powersportvehicles.js | 121 ++++ backend/src/services/search.js | 14 + docker/docker-compose.yml | 2 +- frontend/README.md | 2 +- frontend/json/runtimeError.json | 1 + frontend/src/components/AsideMenuLayer.tsx | 2 +- .../src/components/Bookings/CardBookings.tsx | 13 + .../src/components/Bookings/ListBookings.tsx | 11 + .../Bookings/configureBookingsCols.tsx | 20 + .../CardPowersportvehicles.tsx | 239 +++++++ .../ListPowersportvehicles.tsx | 176 +++++ .../TablePowersportvehicles.tsx | 489 ++++++++++++++ .../configurePowersportvehiclesCols.tsx | 235 +++++++ .../src/components/Vehicles/CardVehicles.tsx | 11 + .../src/components/Vehicles/ListVehicles.tsx | 5 + .../Vehicles/configureVehiclesCols.tsx | 15 + .../components/WebPageComponents/Footer.tsx | 4 +- .../components/WebPageComponents/Header.tsx | 4 +- frontend/src/helpers/dataFormatter.js | 19 + frontend/src/menuAside.ts | 8 + frontend/src/pages/_app.tsx | 4 +- frontend/src/pages/bookings/[bookingsId].tsx | 13 + frontend/src/pages/bookings/bookings-edit.tsx | 13 + frontend/src/pages/bookings/bookings-list.tsx | 2 + frontend/src/pages/bookings/bookings-new.tsx | 12 + .../src/pages/bookings/bookings-table.tsx | 2 + frontend/src/pages/bookings/bookings-view.tsx | 6 + frontend/src/pages/clients/clients-view.tsx | 95 +++ frontend/src/pages/dashboard.tsx | 36 ++ frontend/src/pages/index.tsx | 16 +- frontend/src/pages/login.tsx | 2 +- .../[powersportvehiclesId].tsx | 288 +++++++++ .../powersportvehicles-edit.tsx | 286 ++++++++ .../powersportvehicles-list.tsx | 185 ++++++ .../powersportvehicles-new.tsx | 222 +++++++ .../powersportvehicles-table.tsx | 184 ++++++ .../powersportvehicles-view.tsx | 249 +++++++ frontend/src/pages/privacy-policy.tsx | 2 +- frontend/src/pages/terms-of-use.tsx | 2 +- frontend/src/pages/users/users-view.tsx | 4 + frontend/src/pages/vehicles/[vehiclesId].tsx | 10 + frontend/src/pages/vehicles/vehicles-edit.tsx | 10 + frontend/src/pages/vehicles/vehicles-list.tsx | 6 + frontend/src/pages/vehicles/vehicles-new.tsx | 10 + .../src/pages/vehicles/vehicles-table.tsx | 6 + frontend/src/pages/vehicles/vehicles-view.tsx | 5 + frontend/src/pages/web_pages/contact.tsx | 12 +- frontend/src/pages/web_pages/faq.tsx | 10 +- frontend/src/pages/web_pages/home.tsx | 16 +- .../powersportvehiclesSlice.ts | 250 +++++++ frontend/src/stores/store.ts | 2 + 89 files changed, 5713 insertions(+), 243 deletions(-) create mode 100644 backend/src/db/api/powersportvehicles.js create mode 100644 backend/src/db/migrations/1757449078196.js create mode 100644 backend/src/db/migrations/1757450562030.js create mode 100644 backend/src/db/migrations/1757450589992.js create mode 100644 backend/src/db/migrations/1757450623285.js create mode 100644 backend/src/db/migrations/1757450719241.js create mode 100644 backend/src/db/migrations/1757450790548.js create mode 100644 backend/src/db/migrations/1757450827833.js create mode 100644 backend/src/db/migrations/1757450860278.js create mode 100644 backend/src/db/migrations/1757450889961.js create mode 100644 backend/src/db/migrations/1757450927629.js create mode 100644 backend/src/db/migrations/1757450953122.js create mode 100644 backend/src/db/migrations/1757450984490.js create mode 100644 backend/src/db/migrations/1757451017415.js create mode 100644 backend/src/db/migrations/1757451046441.js create mode 100644 backend/src/db/migrations/1757451083313.js create mode 100644 backend/src/db/models/powersportvehicles.js create mode 100644 backend/src/db/seeders/20250909204242.js create mode 100644 backend/src/routes/powersportvehicles.js create mode 100644 backend/src/services/powersportvehicles.js create mode 100644 frontend/json/runtimeError.json create mode 100644 frontend/src/components/Powersportvehicles/CardPowersportvehicles.tsx create mode 100644 frontend/src/components/Powersportvehicles/ListPowersportvehicles.tsx create mode 100644 frontend/src/components/Powersportvehicles/TablePowersportvehicles.tsx create mode 100644 frontend/src/components/Powersportvehicles/configurePowersportvehiclesCols.tsx create mode 100644 frontend/src/pages/powersportvehicles/[powersportvehiclesId].tsx create mode 100644 frontend/src/pages/powersportvehicles/powersportvehicles-edit.tsx create mode 100644 frontend/src/pages/powersportvehicles/powersportvehicles-list.tsx create mode 100644 frontend/src/pages/powersportvehicles/powersportvehicles-new.tsx create mode 100644 frontend/src/pages/powersportvehicles/powersportvehicles-table.tsx create mode 100644 frontend/src/pages/powersportvehicles/powersportvehicles-view.tsx create mode 100644 frontend/src/stores/powersportvehicles/powersportvehiclesSlice.ts 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/502.html b/502.html index 237b399..06f93db 100644 --- a/502.html +++ b/502.html @@ -129,7 +129,7 @@

The application is currently launching. The page will automatically refresh once site is available.

-

Etherra

+

Powersport Rentals

A multitenant P2P RV rental marketplace with booking and search features.

diff --git a/README.md b/README.md index 112c637..0a8c0ab 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Etherra +# Powersport Rentals ## This project was generated by [Flatlogic Platform](https://flatlogic.com). diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index cd34342..94379ee 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"6B24Z7uNdZXRI9iT\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"6B24Z7uNdZXRI9iT\",\"encryptedData\":\"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\"}", + "1.1": "{\"iv\":\"+PGLW8HbB2oCs3XG\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 81f9be4..25137be 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,4 +1,4 @@ -#Etherra - template backend, +#Powersport Rentals - template backend, #### Run App on local machine: @@ -38,10 +38,10 @@ - Type this command to creating a new database. - - `postgres=> CREATE DATABASE db_etherra;` + - `postgres=> CREATE DATABASE db_powersport_rentals;` - Then give that new user privileges to the new database then quit the `psql`. - - `postgres=> GRANT ALL PRIVILEGES ON DATABASE db_etherra TO admin;` + - `postgres=> GRANT ALL PRIVILEGES ON DATABASE db_powersport_rentals TO admin;` - `postgres=> \q` --- diff --git a/backend/package.json b/backend/package.json index f77dc40..397506a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { - "name": "etherra", - "description": "Etherra - template backend", + "name": "powersportrentals", + "description": "Powersport Rentals - template backend", "scripts": { "start": "npm run db:migrate && npm run db:seed && npm run watch", "db:migrate": "sequelize-cli db:migrate", diff --git a/backend/src/config.js b/backend/src/config.js index ecf9953..39642f1 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -3,7 +3,7 @@ const os = require('os'); const config = { gcloud: { bucket: 'fldemo-files', - hash: '7b2b237a7ca2d27371edcb8650f737ac', + hash: 'afeefb9d49f5b7977577876b99532ac7', }, bcrypt: { saltRounds: 12, @@ -36,7 +36,7 @@ const config = { }, uploadDir: os.tmpdir(), email: { - from: 'Etherra ', + from: 'Powersport Rentals ', host: 'email-smtp.us-east-1.amazonaws.com', port: 587, auth: { diff --git a/backend/src/db/api/bookings.js b/backend/src/db/api/bookings.js index 8ee8e99..4de030b 100644 --- a/backend/src/db/api/bookings.js +++ b/backend/src/db/api/bookings.js @@ -38,6 +38,10 @@ module.exports = class BookingsDBApi { transaction, }); + await bookings.setPowersportvehicle(data.powersportvehicle || null, { + transaction, + }); + return bookings; } @@ -116,6 +120,14 @@ module.exports = class BookingsDBApi { ); } + if (data.powersportvehicle !== undefined) { + await bookings.setPowersportvehicle( + data.powersportvehicle, + + { transaction }, + ); + } + return bookings; } @@ -189,6 +201,10 @@ module.exports = class BookingsDBApi { transaction, }); + output.powersportvehicle = await bookings.getPowersportvehicle({ + transaction, + }); + return output; } @@ -270,6 +286,32 @@ module.exports = class BookingsDBApi { model: db.clients, as: 'clients', }, + + { + model: db.powersportvehicles, + as: 'powersportvehicle', + + where: filter.powersportvehicle + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.powersportvehicle + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.powersportvehicle + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, ]; if (filter) { diff --git a/backend/src/db/api/clients.js b/backend/src/db/api/clients.js index 80aad43..4b1cb4a 100644 --- a/backend/src/db/api/clients.js +++ b/backend/src/db/api/clients.js @@ -137,6 +137,11 @@ module.exports = class ClientsDBApi { transaction, }); + output.powersportvehicles_clients = + await clients.getPowersportvehicles_clients({ + transaction, + }); + return output; } diff --git a/backend/src/db/api/powersportvehicles.js b/backend/src/db/api/powersportvehicles.js new file mode 100644 index 0000000..d71627f --- /dev/null +++ b/backend/src/db/api/powersportvehicles.js @@ -0,0 +1,609 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class PowersportvehiclesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const powersportvehicles = await db.powersportvehicles.create( + { + id: data.id || undefined, + + name: data.name || null, + description: data.description || null, + location: data.location || null, + pricehourly: data.pricehourly || null, + pricedaily: data.pricedaily || null, + securitydeposit: data.securitydeposit || null, + availabilitystart: data.availabilitystart || null, + availabilityend: data.availabilityend || null, + vehicletype: data.vehicletype || null, + preptimebefore: data.preptimebefore || null, + preptimebetween: data.preptimebetween || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await powersportvehicles.setClients(data.clients || null, { + transaction, + }); + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.powersportvehicles.getTableName(), + belongsToColumn: 'photos', + belongsToId: powersportvehicles.id, + }, + data.photos, + options, + ); + + return powersportvehicles; + } + + 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 powersportvehiclesData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + description: item.description || null, + location: item.location || null, + pricehourly: item.pricehourly || null, + pricedaily: item.pricedaily || null, + securitydeposit: item.securitydeposit || null, + availabilitystart: item.availabilitystart || null, + availabilityend: item.availabilityend || null, + vehicletype: item.vehicletype || null, + preptimebefore: item.preptimebefore || null, + preptimebetween: item.preptimebetween || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const powersportvehicles = await db.powersportvehicles.bulkCreate( + powersportvehiclesData, + { transaction }, + ); + + // For each item created, replace relation files + + for (let i = 0; i < powersportvehicles.length; i++) { + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.powersportvehicles.getTableName(), + belongsToColumn: 'photos', + belongsToId: powersportvehicles[i].id, + }, + data[i].photos, + options, + ); + } + + return powersportvehicles; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const powersportvehicles = await db.powersportvehicles.findByPk( + id, + {}, + { transaction }, + ); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.description !== undefined) + updatePayload.description = data.description; + + if (data.location !== undefined) updatePayload.location = data.location; + + if (data.pricehourly !== undefined) + updatePayload.pricehourly = data.pricehourly; + + if (data.pricedaily !== undefined) + updatePayload.pricedaily = data.pricedaily; + + if (data.securitydeposit !== undefined) + updatePayload.securitydeposit = data.securitydeposit; + + if (data.availabilitystart !== undefined) + updatePayload.availabilitystart = data.availabilitystart; + + if (data.availabilityend !== undefined) + updatePayload.availabilityend = data.availabilityend; + + if (data.vehicletype !== undefined) + updatePayload.vehicletype = data.vehicletype; + + if (data.preptimebefore !== undefined) + updatePayload.preptimebefore = data.preptimebefore; + + if (data.preptimebetween !== undefined) + updatePayload.preptimebetween = data.preptimebetween; + + updatePayload.updatedById = currentUser.id; + + await powersportvehicles.update(updatePayload, { transaction }); + + if (data.clients !== undefined) { + await powersportvehicles.setClients( + data.clients, + + { transaction }, + ); + } + + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.powersportvehicles.getTableName(), + belongsToColumn: 'photos', + belongsToId: powersportvehicles.id, + }, + data.photos, + options, + ); + + return powersportvehicles; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const powersportvehicles = await db.powersportvehicles.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of powersportvehicles) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of powersportvehicles) { + await record.destroy({ transaction }); + } + }); + + return powersportvehicles; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const powersportvehicles = await db.powersportvehicles.findByPk( + id, + options, + ); + + await powersportvehicles.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await powersportvehicles.destroy({ + transaction, + }); + + return powersportvehicles; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const powersportvehicles = await db.powersportvehicles.findOne( + { where }, + { transaction }, + ); + + if (!powersportvehicles) { + return powersportvehicles; + } + + const output = powersportvehicles.get({ plain: true }); + + output.bookings_powersportvehicle = + await powersportvehicles.getBookings_powersportvehicle({ + transaction, + }); + + output.clients = await powersportvehicles.getClients({ + transaction, + }); + + output.photos = await powersportvehicles.getPhotos({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userClients = (user && user.clients?.id) || null; + + if (userClients) { + if (options?.currentUser?.clientsId) { + where.clientsId = options.currentUser.clientsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.clients, + as: 'clients', + }, + + { + model: db.file, + as: 'photos', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('powersportvehicles', 'name', filter.name), + }; + } + + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'powersportvehicles', + 'description', + filter.description, + ), + }; + } + + if (filter.location) { + where = { + ...where, + [Op.and]: Utils.ilike( + 'powersportvehicles', + 'location', + filter.location, + ), + }; + } + + if (filter.pricehourlyRange) { + const [start, end] = filter.pricehourlyRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pricehourly: { + ...where.pricehourly, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pricehourly: { + ...where.pricehourly, + [Op.lte]: end, + }, + }; + } + } + + if (filter.pricedailyRange) { + const [start, end] = filter.pricedailyRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + pricedaily: { + ...where.pricedaily, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + pricedaily: { + ...where.pricedaily, + [Op.lte]: end, + }, + }; + } + } + + if (filter.securitydepositRange) { + const [start, end] = filter.securitydepositRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + securitydeposit: { + ...where.securitydeposit, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + securitydeposit: { + ...where.securitydeposit, + [Op.lte]: end, + }, + }; + } + } + + if (filter.availabilitystartRange) { + const [start, end] = filter.availabilitystartRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + availabilitystart: { + ...where.availabilitystart, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + availabilitystart: { + ...where.availabilitystart, + [Op.lte]: end, + }, + }; + } + } + + if (filter.availabilityendRange) { + const [start, end] = filter.availabilityendRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + availabilityend: { + ...where.availabilityend, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + availabilityend: { + ...where.availabilityend, + [Op.lte]: end, + }, + }; + } + } + + if (filter.preptimebeforeRange) { + const [start, end] = filter.preptimebeforeRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + preptimebefore: { + ...where.preptimebefore, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + preptimebefore: { + ...where.preptimebefore, + [Op.lte]: end, + }, + }; + } + } + + if (filter.preptimebetweenRange) { + const [start, end] = filter.preptimebetweenRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + preptimebetween: { + ...where.preptimebetween, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + preptimebetween: { + ...where.preptimebetween, + [Op.lte]: end, + }, + }; + } + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.vehicletype) { + where = { + ...where, + vehicletype: filter.vehicletype, + }; + } + + if (filter.clients) { + const listItems = filter.clients.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + clientsId: { [Op.or]: listItems }, + }; + } + + 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, + }, + }; + } + } + } + + if (globalAccess) { + delete where.clientsId; + } + + 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.powersportvehicles.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, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('powersportvehicles', 'name', query), + ], + }; + } + + const records = await db.powersportvehicles.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/api/vehicles.js b/backend/src/db/api/vehicles.js index 936858e..81d1ff1 100644 --- a/backend/src/db/api/vehicles.js +++ b/backend/src/db/api/vehicles.js @@ -21,6 +21,7 @@ module.exports = class VehiclesDBApi { price_hourly: data.price_hourly || null, price_daily: data.price_daily || null, security_deposit: data.security_deposit || null, + vehicletype: data.vehicletype || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -67,6 +68,7 @@ module.exports = class VehiclesDBApi { price_hourly: item.price_hourly || null, price_daily: item.price_daily || null, security_deposit: item.security_deposit || null, + vehicletype: item.vehicletype || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -120,6 +122,9 @@ module.exports = class VehiclesDBApi { if (data.security_deposit !== undefined) updatePayload.security_deposit = data.security_deposit; + if (data.vehicletype !== undefined) + updatePayload.vehicletype = data.vehicletype; + updatePayload.updatedById = currentUser.id; await vehicles.update(updatePayload, { transaction }); @@ -411,6 +416,13 @@ module.exports = class VehiclesDBApi { }; } + if (filter.vehicletype) { + where = { + ...where, + vehicletype: filter.vehicletype, + }; + } + if (filter.clients) { const listItems = filter.clients.split('|').map((item) => { return Utils.uuid(item); diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index d635487..9f1e2d9 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -13,7 +13,7 @@ module.exports = { username: 'postgres', dialect: 'postgres', password: '', - database: 'db_etherra', + database: 'db_powersport_rentals', host: process.env.DB_HOST || 'localhost', logging: console.log, seederStorage: 'sequelize', diff --git a/backend/src/db/migrations/1757449078196.js b/backend/src/db/migrations/1757449078196.js new file mode 100644 index 0000000..a8a92a8 --- /dev/null +++ b/backend/src/db/migrations/1757449078196.js @@ -0,0 +1,51 @@ +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( + 'vehicles', + 'vehicletype', + { + 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('vehicles', 'vehicletype', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450562030.js b/backend/src/db/migrations/1757450562030.js new file mode 100644 index 0000000..7f9a1f9 --- /dev/null +++ b/backend/src/db/migrations/1757450562030.js @@ -0,0 +1,90 @@ +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( + 'powersportvehicles', + { + 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 queryInterface.addColumn( + 'powersportvehicles', + 'clientsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'clients', + 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('powersportvehicles', 'clientsId', { + transaction, + }); + + await queryInterface.dropTable('powersportvehicles', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450589992.js b/backend/src/db/migrations/1757450589992.js new file mode 100644 index 0000000..551f0d5 --- /dev/null +++ b/backend/src/db/migrations/1757450589992.js @@ -0,0 +1,49 @@ +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( + 'powersportvehicles', + 'name', + { + 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('powersportvehicles', 'name', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450623285.js b/backend/src/db/migrations/1757450623285.js new file mode 100644 index 0000000..0aa8f76 --- /dev/null +++ b/backend/src/db/migrations/1757450623285.js @@ -0,0 +1,49 @@ +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( + 'powersportvehicles', + 'description', + { + 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('powersportvehicles', 'description', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450719241.js b/backend/src/db/migrations/1757450719241.js new file mode 100644 index 0000000..e6bfba3 --- /dev/null +++ b/backend/src/db/migrations/1757450719241.js @@ -0,0 +1,36 @@ +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/1757450790548.js b/backend/src/db/migrations/1757450790548.js new file mode 100644 index 0000000..ccccb2c --- /dev/null +++ b/backend/src/db/migrations/1757450790548.js @@ -0,0 +1,49 @@ +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( + 'powersportvehicles', + 'location', + { + 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('powersportvehicles', 'location', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450827833.js b/backend/src/db/migrations/1757450827833.js new file mode 100644 index 0000000..157f544 --- /dev/null +++ b/backend/src/db/migrations/1757450827833.js @@ -0,0 +1,49 @@ +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( + 'powersportvehicles', + 'pricehourly', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { 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('powersportvehicles', 'pricehourly', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450860278.js b/backend/src/db/migrations/1757450860278.js new file mode 100644 index 0000000..fd99666 --- /dev/null +++ b/backend/src/db/migrations/1757450860278.js @@ -0,0 +1,49 @@ +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( + 'powersportvehicles', + 'pricedaily', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { 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('powersportvehicles', 'pricedaily', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450889961.js b/backend/src/db/migrations/1757450889961.js new file mode 100644 index 0000000..bf87e2f --- /dev/null +++ b/backend/src/db/migrations/1757450889961.js @@ -0,0 +1,51 @@ +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( + 'powersportvehicles', + 'securitydeposit', + { + type: Sequelize.DataTypes.DECIMAL, + }, + { 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( + 'powersportvehicles', + 'securitydeposit', + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450927629.js b/backend/src/db/migrations/1757450927629.js new file mode 100644 index 0000000..fedf035 --- /dev/null +++ b/backend/src/db/migrations/1757450927629.js @@ -0,0 +1,51 @@ +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( + 'powersportvehicles', + 'availabilitystart', + { + 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( + 'powersportvehicles', + 'availabilitystart', + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450953122.js b/backend/src/db/migrations/1757450953122.js new file mode 100644 index 0000000..d2819f6 --- /dev/null +++ b/backend/src/db/migrations/1757450953122.js @@ -0,0 +1,51 @@ +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( + 'powersportvehicles', + 'availabilityend', + { + 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( + 'powersportvehicles', + 'availabilityend', + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757450984490.js b/backend/src/db/migrations/1757450984490.js new file mode 100644 index 0000000..05571e4 --- /dev/null +++ b/backend/src/db/migrations/1757450984490.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( + 'bookings', + 'powersportvehicleId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'powersportvehicles', + 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('bookings', 'powersportvehicleId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757451017415.js b/backend/src/db/migrations/1757451017415.js new file mode 100644 index 0000000..98ed1ad --- /dev/null +++ b/backend/src/db/migrations/1757451017415.js @@ -0,0 +1,51 @@ +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( + 'powersportvehicles', + 'vehicletype', + { + 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('powersportvehicles', 'vehicletype', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757451046441.js b/backend/src/db/migrations/1757451046441.js new file mode 100644 index 0000000..a46bf12 --- /dev/null +++ b/backend/src/db/migrations/1757451046441.js @@ -0,0 +1,51 @@ +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( + 'powersportvehicles', + 'preptimebefore', + { + 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( + 'powersportvehicles', + 'preptimebefore', + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1757451083313.js b/backend/src/db/migrations/1757451083313.js new file mode 100644 index 0000000..d76fd31 --- /dev/null +++ b/backend/src/db/migrations/1757451083313.js @@ -0,0 +1,51 @@ +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( + 'powersportvehicles', + 'preptimebetween', + { + 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( + 'powersportvehicles', + 'preptimebetween', + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/bookings.js b/backend/src/db/models/bookings.js index ba50c4c..ea0dbfc 100644 --- a/backend/src/db/models/bookings.js +++ b/backend/src/db/models/bookings.js @@ -74,6 +74,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.bookings.belongsTo(db.powersportvehicles, { + as: 'powersportvehicle', + foreignKey: { + name: 'powersportvehicleId', + }, + constraints: false, + }); + db.bookings.belongsTo(db.users, { as: 'createdBy', }); diff --git a/backend/src/db/models/clients.js b/backend/src/db/models/clients.js index 266a6f9..7bcfc98 100644 --- a/backend/src/db/models/clients.js +++ b/backend/src/db/models/clients.js @@ -58,6 +58,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.clients.hasMany(db.powersportvehicles, { + as: 'powersportvehicles_clients', + foreignKey: { + name: 'clientsId', + }, + constraints: false, + }); + //end loop db.clients.belongsTo(db.users, { diff --git a/backend/src/db/models/powersportvehicles.js b/backend/src/db/models/powersportvehicles.js new file mode 100644 index 0000000..59c003e --- /dev/null +++ b/backend/src/db/models/powersportvehicles.js @@ -0,0 +1,117 @@ +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 powersportvehicles = sequelize.define( + 'powersportvehicles', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + description: { + type: DataTypes.TEXT, + }, + + location: { + type: DataTypes.TEXT, + }, + + pricehourly: { + type: DataTypes.DECIMAL, + }, + + pricedaily: { + type: DataTypes.DECIMAL, + }, + + securitydeposit: { + type: DataTypes.DECIMAL, + }, + + availabilitystart: { + type: DataTypes.DATE, + }, + + availabilityend: { + type: DataTypes.DATE, + }, + + vehicletype: { + type: DataTypes.ENUM, + + values: ['value'], + }, + + preptimebefore: { + type: DataTypes.INTEGER, + }, + + preptimebetween: { + type: DataTypes.INTEGER, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + powersportvehicles.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.powersportvehicles.hasMany(db.bookings, { + as: 'bookings_powersportvehicle', + foreignKey: { + name: 'powersportvehicleId', + }, + constraints: false, + }); + + //end loop + + db.powersportvehicles.belongsTo(db.clients, { + as: 'clients', + foreignKey: { + name: 'clientsId', + }, + constraints: false, + }); + + db.powersportvehicles.hasMany(db.file, { + as: 'photos', + foreignKey: 'belongsToId', + constraints: false, + scope: { + belongsTo: db.powersportvehicles.getTableName(), + belongsToColumn: 'photos', + }, + }); + + db.powersportvehicles.belongsTo(db.users, { + as: 'createdBy', + }); + + db.powersportvehicles.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return powersportvehicles; +}; diff --git a/backend/src/db/models/vehicles.js b/backend/src/db/models/vehicles.js index 09556e4..58a0093 100644 --- a/backend/src/db/models/vehicles.js +++ b/backend/src/db/models/vehicles.js @@ -38,6 +38,12 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.DECIMAL, }, + vehicletype: { + type: DataTypes.ENUM, + + values: ['value'], + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 7f62a4e..9e1ace0 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -107,6 +107,7 @@ module.exports = { 'roles', 'permissions', 'clients', + 'powersportvehicles', , ]; await queryInterface.bulkInsert( @@ -537,6 +538,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_VEHICLES'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_POWERSPORTVEHICLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_POWERSPORTVEHICLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_POWERSPORTVEHICLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_POWERSPORTVEHICLES'), + }, + { createdAt, updatedAt, @@ -687,6 +713,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_CLIENTS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_POWERSPORTVEHICLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_POWERSPORTVEHICLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_POWERSPORTVEHICLES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_POWERSPORTVEHICLES'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index e430a85..dbf49f0 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -7,6 +7,8 @@ const Vehicles = db.vehicles; const Clients = db.clients; +const Powersportvehicles = db.powersportvehicles; + const BookingsData = [ { // type code here for "relation_one" field @@ -22,6 +24,8 @@ const BookingsData = [ total_price: 1200, // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -33,11 +37,13 @@ const BookingsData = [ end_date: new Date('2023-11-12T09:00:00Z'), - status: 'completed', + status: 'cancelled', total_price: 360, // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -49,41 +55,11 @@ const BookingsData = [ end_date: new Date('2023-11-20T08:00:00Z'), - status: 'denied', + status: 'approved', total_price: 1200, // type code here for "relation_one" field - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - start_date: new Date('2023-11-25T07:00:00Z'), - - end_date: new Date('2023-11-30T07:00:00Z'), - - status: 'completed', - - total_price: 1350, - - // type code here for "relation_one" field - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - start_date: new Date('2023-12-01T06:00:00Z'), - - end_date: new Date('2023-12-05T06:00:00Z'), - - status: 'cancelled', - - total_price: 840, // type code here for "relation_one" field }, @@ -110,6 +86,8 @@ const VehiclesData = [ // type code here for "relation_many" field // type code here for "relation_one" field + + vehicletype: 'value', }, { @@ -132,6 +110,8 @@ const VehiclesData = [ // type code here for "relation_many" field // type code here for "relation_one" field + + vehicletype: 'value', }, { @@ -154,50 +134,8 @@ const VehiclesData = [ // type code here for "relation_many" field // type code here for "relation_one" field - }, - { - title: 'Adventure Van', - - description: 'A rugged van equipped for off-road adventures.', - - // type code here for "images" field - - location: 'Denver, CO', - - price_hourly: 45, - - price_daily: 270, - - security_deposit: 450, - - // type code here for "relation_one" field - - // type code here for "relation_many" field - - // type code here for "relation_one" field - }, - - { - title: 'Classic Trailer', - - description: 'A vintage trailer with a charming retro feel.', - - // type code here for "images" field - - location: 'Portland, OR', - - price_hourly: 35, - - price_daily: 210, - - security_deposit: 350, - - // type code here for "relation_one" field - - // type code here for "relation_many" field - - // type code here for "relation_one" field + vehicletype: 'value', }, ]; @@ -213,13 +151,91 @@ const ClientsData = [ { name: 'Luxury Escapes', }, +]; +const PowersportvehiclesData = [ { - name: 'Budget Travels', + // type code here for "relation_one" field + + name: 'Carl Linnaeus', + + description: 'Albrecht von Haller', + + // type code here for "images" field + + location: 'Edwin Hubble', + + pricehourly: 35.85, + + pricedaily: 86.25, + + securitydeposit: 32.01, + + availabilitystart: new Date(Date.now()), + + availabilityend: new Date(Date.now()), + + vehicletype: 'value', + + preptimebefore: 7, + + preptimebetween: 4, }, { - name: 'Eco-Friendly Tours', + // type code here for "relation_one" field + + name: 'Christiaan Huygens', + + description: 'Lucretius', + + // type code here for "images" field + + location: 'Richard Feynman', + + pricehourly: 73.21, + + pricedaily: 91.22, + + securitydeposit: 75.97, + + availabilitystart: new Date(Date.now()), + + availabilityend: new Date(Date.now()), + + vehicletype: 'value', + + preptimebefore: 3, + + preptimebetween: 9, + }, + + { + // type code here for "relation_one" field + + name: 'J. Robert Oppenheimer', + + description: 'Richard Feynman', + + // type code here for "images" field + + location: 'Charles Lyell', + + pricehourly: 89.83, + + pricedaily: 34.17, + + securitydeposit: 94.42, + + availabilitystart: new Date(Date.now()), + + availabilityend: new Date(Date.now()), + + vehicletype: 'value', + + preptimebefore: 8, + + preptimebetween: 9, }, ]; @@ -258,28 +274,6 @@ async function associateUserWithClient() { if (User2?.setClient) { await User2.setClient(relatedClient2); } - - const relatedClient3 = await Clients.findOne({ - offset: Math.floor(Math.random() * (await Clients.count())), - }); - const User3 = await Users.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (User3?.setClient) { - await User3.setClient(relatedClient3); - } - - const relatedClient4 = await Clients.findOne({ - offset: Math.floor(Math.random() * (await Clients.count())), - }); - const User4 = await Users.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (User4?.setClient) { - await User4.setClient(relatedClient4); - } } async function associateBookingWithVehicle() { @@ -315,28 +309,6 @@ async function associateBookingWithVehicle() { if (Booking2?.setVehicle) { await Booking2.setVehicle(relatedVehicle2); } - - const relatedVehicle3 = await Vehicles.findOne({ - offset: Math.floor(Math.random() * (await Vehicles.count())), - }); - const Booking3 = await Bookings.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Booking3?.setVehicle) { - await Booking3.setVehicle(relatedVehicle3); - } - - const relatedVehicle4 = await Vehicles.findOne({ - offset: Math.floor(Math.random() * (await Vehicles.count())), - }); - const Booking4 = await Bookings.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Booking4?.setVehicle) { - await Booking4.setVehicle(relatedVehicle4); - } } async function associateBookingWithRentee() { @@ -372,28 +344,6 @@ async function associateBookingWithRentee() { if (Booking2?.setRentee) { await Booking2.setRentee(relatedRentee2); } - - const relatedRentee3 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Booking3 = await Bookings.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Booking3?.setRentee) { - await Booking3.setRentee(relatedRentee3); - } - - const relatedRentee4 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Booking4 = await Bookings.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Booking4?.setRentee) { - await Booking4.setRentee(relatedRentee4); - } } async function associateBookingWithClient() { @@ -429,27 +379,40 @@ async function associateBookingWithClient() { if (Booking2?.setClient) { await Booking2.setClient(relatedClient2); } +} - const relatedClient3 = await Clients.findOne({ - offset: Math.floor(Math.random() * (await Clients.count())), +async function associateBookingWithPowersportvehicle() { + const relatedPowersportvehicle0 = await Powersportvehicles.findOne({ + offset: Math.floor(Math.random() * (await Powersportvehicles.count())), }); - const Booking3 = await Bookings.findOne({ + const Booking0 = await Bookings.findOne({ order: [['id', 'ASC']], - offset: 3, + offset: 0, }); - if (Booking3?.setClient) { - await Booking3.setClient(relatedClient3); + if (Booking0?.setPowersportvehicle) { + await Booking0.setPowersportvehicle(relatedPowersportvehicle0); } - const relatedClient4 = await Clients.findOne({ - offset: Math.floor(Math.random() * (await Clients.count())), + const relatedPowersportvehicle1 = await Powersportvehicles.findOne({ + offset: Math.floor(Math.random() * (await Powersportvehicles.count())), }); - const Booking4 = await Bookings.findOne({ + const Booking1 = await Bookings.findOne({ order: [['id', 'ASC']], - offset: 4, + offset: 1, }); - if (Booking4?.setClient) { - await Booking4.setClient(relatedClient4); + if (Booking1?.setPowersportvehicle) { + await Booking1.setPowersportvehicle(relatedPowersportvehicle1); + } + + const relatedPowersportvehicle2 = await Powersportvehicles.findOne({ + offset: Math.floor(Math.random() * (await Powersportvehicles.count())), + }); + const Booking2 = await Bookings.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Booking2?.setPowersportvehicle) { + await Booking2.setPowersportvehicle(relatedPowersportvehicle2); } } @@ -486,28 +449,6 @@ async function associateVehicleWithOwner() { if (Vehicle2?.setOwner) { await Vehicle2.setOwner(relatedOwner2); } - - const relatedOwner3 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Vehicle3 = await Vehicles.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Vehicle3?.setOwner) { - await Vehicle3.setOwner(relatedOwner3); - } - - const relatedOwner4 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Vehicle4 = await Vehicles.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Vehicle4?.setOwner) { - await Vehicle4.setOwner(relatedOwner4); - } } // Similar logic for "relation_many" @@ -545,27 +486,40 @@ async function associateVehicleWithClient() { if (Vehicle2?.setClient) { await Vehicle2.setClient(relatedClient2); } +} - const relatedClient3 = await Clients.findOne({ +async function associatePowersportvehicleWithClient() { + const relatedClient0 = await Clients.findOne({ offset: Math.floor(Math.random() * (await Clients.count())), }); - const Vehicle3 = await Vehicles.findOne({ + const Powersportvehicle0 = await Powersportvehicles.findOne({ order: [['id', 'ASC']], - offset: 3, + offset: 0, }); - if (Vehicle3?.setClient) { - await Vehicle3.setClient(relatedClient3); + if (Powersportvehicle0?.setClient) { + await Powersportvehicle0.setClient(relatedClient0); } - const relatedClient4 = await Clients.findOne({ + const relatedClient1 = await Clients.findOne({ offset: Math.floor(Math.random() * (await Clients.count())), }); - const Vehicle4 = await Vehicles.findOne({ + const Powersportvehicle1 = await Powersportvehicles.findOne({ order: [['id', 'ASC']], - offset: 4, + offset: 1, }); - if (Vehicle4?.setClient) { - await Vehicle4.setClient(relatedClient4); + if (Powersportvehicle1?.setClient) { + await Powersportvehicle1.setClient(relatedClient1); + } + + const relatedClient2 = await Clients.findOne({ + offset: Math.floor(Math.random() * (await Clients.count())), + }); + const Powersportvehicle2 = await Powersportvehicles.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Powersportvehicle2?.setClient) { + await Powersportvehicle2.setClient(relatedClient2); } } @@ -577,6 +531,8 @@ module.exports = { await Clients.bulkCreate(ClientsData); + await Powersportvehicles.bulkCreate(PowersportvehiclesData); + await Promise.all([ // Similar logic for "relation_many" @@ -588,11 +544,15 @@ module.exports = { await associateBookingWithClient(), + await associateBookingWithPowersportvehicle(), + await associateVehicleWithOwner(), // Similar logic for "relation_many" await associateVehicleWithClient(), + + await associatePowersportvehicleWithClient(), ]); }, @@ -602,5 +562,7 @@ module.exports = { await queryInterface.bulkDelete('vehicles', null, {}); await queryInterface.bulkDelete('clients', null, {}); + + await queryInterface.bulkDelete('powersportvehicles', null, {}); }, }; diff --git a/backend/src/db/seeders/20250909204242.js b/backend/src/db/seeders/20250909204242.js new file mode 100644 index 0000000..07e0fc4 --- /dev/null +++ b/backend/src/db/seeders/20250909204242.js @@ -0,0 +1,87 @@ +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 = ['powersportvehicles']; + + 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.super_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 34ab168..d877ada 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -33,6 +33,8 @@ const permissionsRoutes = require('./routes/permissions'); const clientsRoutes = require('./routes/clients'); +const powersportvehiclesRoutes = require('./routes/powersportvehicles'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -43,9 +45,9 @@ const options = { openapi: '3.0.0', info: { version: '1.0.0', - title: 'Etherra', + title: 'Powersport Rentals', description: - 'Etherra Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', + 'Powersport Rentals Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.', }, servers: [ { @@ -134,6 +136,12 @@ app.use( clientsRoutes, ); +app.use( + '/api/powersportvehicles', + passport.authenticate('jwt', { session: false }), + powersportvehiclesRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/powersportvehicles.js b/backend/src/routes/powersportvehicles.js new file mode 100644 index 0000000..96a3111 --- /dev/null +++ b/backend/src/routes/powersportvehicles.js @@ -0,0 +1,495 @@ +const express = require('express'); + +const PowersportvehiclesService = require('../services/powersportvehicles'); +const PowersportvehiclesDBApi = require('../db/api/powersportvehicles'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('powersportvehicles')); + +/** + * @swagger + * components: + * schemas: + * Powersportvehicles: + * type: object + * properties: + + * name: + * type: string + * default: name + * description: + * type: string + * default: description + * location: + * type: string + * default: location + + * preptimebefore: + * type: integer + * format: int64 + * preptimebetween: + * type: integer + * format: int64 + + * pricehourly: + * type: integer + * format: int64 + * pricedaily: + * type: integer + * format: int64 + * securitydeposit: + * type: integer + * format: int64 + + * + */ + +/** + * @swagger + * tags: + * name: Powersportvehicles + * description: The Powersportvehicles managing API + */ + +/** + * @swagger + * /api/powersportvehicles: + * post: + * security: + * - bearerAuth: [] + * tags: [Powersportvehicles] + * 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/Powersportvehicles" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Powersportvehicles" + * 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 PowersportvehiclesService.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: [Powersportvehicles] + * 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/Powersportvehicles" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Powersportvehicles" + * 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 PowersportvehiclesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/powersportvehicles/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Powersportvehicles] + * 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/Powersportvehicles" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Powersportvehicles" + * 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 PowersportvehiclesService.update( + req.body.data, + req.body.id, + req.currentUser, + ); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/powersportvehicles/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Powersportvehicles] + * 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/Powersportvehicles" + * 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 PowersportvehiclesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/powersportvehicles/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Powersportvehicles] + * 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/Powersportvehicles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await PowersportvehiclesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/powersportvehicles: + * get: + * security: + * - bearerAuth: [] + * tags: [Powersportvehicles] + * summary: Get all powersportvehicles + * description: Get all powersportvehicles + * responses: + * 200: + * description: Powersportvehicles list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Powersportvehicles" + * 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 globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await PowersportvehiclesDBApi.findAll( + req.query, + globalAccess, + { currentUser }, + ); + if (filetype && filetype === 'csv') { + const fields = [ + 'id', + 'name', + 'description', + 'location', + 'preptimebefore', + 'preptimebetween', + 'pricehourly', + 'pricedaily', + 'securitydeposit', + 'availabilitystart', + 'availabilityend', + ]; + 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/powersportvehicles/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Powersportvehicles] + * summary: Count all powersportvehicles + * description: Count all powersportvehicles + * responses: + * 200: + * description: Powersportvehicles count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Powersportvehicles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get( + '/count', + wrapAsync(async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const currentUser = req.currentUser; + const payload = await PowersportvehiclesDBApi.findAll( + req.query, + globalAccess, + { countOnly: true, currentUser }, + ); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/powersportvehicles/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Powersportvehicles] + * summary: Find all powersportvehicles that match search criteria + * description: Find all powersportvehicles that match search criteria + * responses: + * 200: + * description: Powersportvehicles list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Powersportvehicles" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const globalAccess = req.currentUser.app_role.globalAccess; + + const organizationId = req.currentUser.organization?.id; + + const payload = await PowersportvehiclesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/powersportvehicles/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Powersportvehicles] + * 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/Powersportvehicles" + * 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 PowersportvehiclesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/vehicles.js b/backend/src/routes/vehicles.js index ecc9f15..77cf18c 100644 --- a/backend/src/routes/vehicles.js +++ b/backend/src/routes/vehicles.js @@ -42,6 +42,7 @@ router.use(checkCrudPermissions('vehicles')); * type: integer * format: int64 + * */ /** diff --git a/backend/src/services/notifications/list.js b/backend/src/services/notifications/list.js index e108356..beac58e 100644 --- a/backend/src/services/notifications/list.js +++ b/backend/src/services/notifications/list.js @@ -1,6 +1,6 @@ const errors = { app: { - title: 'Etherra', + title: 'Powersport Rentals', }, auth: { diff --git a/backend/src/services/powersportvehicles.js b/backend/src/services/powersportvehicles.js new file mode 100644 index 0000000..481f95c --- /dev/null +++ b/backend/src/services/powersportvehicles.js @@ -0,0 +1,121 @@ +const db = require('../db/models'); +const PowersportvehiclesDBApi = require('../db/api/powersportvehicles'); +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 PowersportvehiclesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await PowersportvehiclesDBApi.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 PowersportvehiclesDBApi.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 powersportvehicles = await PowersportvehiclesDBApi.findBy( + { id }, + { transaction }, + ); + + if (!powersportvehicles) { + throw new ValidationError('powersportvehiclesNotFound'); + } + + const updatedPowersportvehicles = await PowersportvehiclesDBApi.update( + id, + data, + { + currentUser, + transaction, + }, + ); + + await transaction.commit(); + return updatedPowersportvehicles; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await PowersportvehiclesDBApi.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 PowersportvehiclesDBApi.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 6a2be75..aa390ef 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -46,11 +46,25 @@ module.exports = class SearchService { vehicles: ['title', 'description', 'location'], clients: ['name'], + + powersportvehicles: ['name', 'description', 'location'], }; const columnsInt = { bookings: ['total_price'], vehicles: ['price_hourly', 'price_daily', 'security_deposit'], + + powersportvehicles: [ + 'pricehourly', + + 'pricedaily', + + 'securitydeposit', + + 'preptimebefore', + + 'preptimebetween', + ], }; let allFoundRecords = []; diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 606bb2f..4e9f710 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -25,7 +25,7 @@ services: - ./data/db:/var/lib/postgresql/data environment: - POSTGRES_HOST_AUTH_METHOD=trust - - POSTGRES_DB=db_etherra + - POSTGRES_DB=db_powersport_rentals ports: - "5432:5432" logging: diff --git a/frontend/README.md b/frontend/README.md index ec63cd6..9adeebb 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,4 +1,4 @@ -# Etherra +# Powersport Rentals ## This project was generated by Flatlogic Platform. 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/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index c4e300c..0aaf2d4 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -76,7 +76,7 @@ export default function AsideMenuLayer({ >
- Etherra + Powersport Rentals {organizationName &&

{organizationName}

} diff --git a/frontend/src/components/Bookings/CardBookings.tsx b/frontend/src/components/Bookings/CardBookings.tsx index 73312a3..8f70913 100644 --- a/frontend/src/components/Bookings/CardBookings.tsx +++ b/frontend/src/components/Bookings/CardBookings.tsx @@ -141,6 +141,19 @@ const CardBookings = ({
+ +
+
+ Powersportvehicle +
+
+
+ {dataFormatter.powersportvehiclesOneListFormatter( + item.powersportvehicle, + )} +
+
+
))} diff --git a/frontend/src/components/Bookings/ListBookings.tsx b/frontend/src/components/Bookings/ListBookings.tsx index 20fc161..af053d0 100644 --- a/frontend/src/components/Bookings/ListBookings.tsx +++ b/frontend/src/components/Bookings/ListBookings.tsx @@ -90,6 +90,17 @@ const ListBookings = ({

TotalPrice

{item.total_price}

+ +
+

+ Powersportvehicle +

+

+ {dataFormatter.powersportvehiclesOneListFormatter( + item.powersportvehicle, + )} +

+
value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('powersportvehicles'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/components/Powersportvehicles/CardPowersportvehicles.tsx b/frontend/src/components/Powersportvehicles/CardPowersportvehicles.tsx new file mode 100644 index 0000000..a684610 --- /dev/null +++ b/frontend/src/components/Powersportvehicles/CardPowersportvehicles.tsx @@ -0,0 +1,239 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + powersportvehicles: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardPowersportvehicles = ({ + powersportvehicles, + 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); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_POWERSPORTVEHICLES', + ); + + return ( +
+ {loading && } +
    + {!loading && + powersportvehicles.map((item, index) => ( +
  • +
    + + +

    {item.name}

    + + +
    + +
    +
    +
    +
    +
    Name
    +
    +
    {item.name}
    +
    +
    + +
    +
    + Description +
    +
    +
    + {item.description} +
    +
    +
    + +
    +
    + Photos +
    +
    +
    + +
    +
    +
    + +
    +
    + Location +
    +
    +
    + {item.location} +
    +
    +
    + +
    +
    + Pricehourly +
    +
    +
    + {item.pricehourly} +
    +
    +
    + +
    +
    + Pricedaily +
    +
    +
    + {item.pricedaily} +
    +
    +
    + +
    +
    + Securitydeposit +
    +
    +
    + {item.securitydeposit} +
    +
    +
    + +
    +
    + Availabilitystart +
    +
    +
    + {dataFormatter.dateTimeFormatter(item.availabilitystart)} +
    +
    +
    + +
    +
    + Availabilityend +
    +
    +
    + {dataFormatter.dateTimeFormatter(item.availabilityend)} +
    +
    +
    + +
    +
    + Vehicletype +
    +
    +
    + {item.vehicletype} +
    +
    +
    + +
    +
    + Preptimebefore +
    +
    +
    + {item.preptimebefore} +
    +
    +
    + +
    +
    + Preptimebetween +
    +
    +
    + {item.preptimebetween} +
    +
    +
    +
    +
  • + ))} + {!loading && powersportvehicles.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardPowersportvehicles; diff --git a/frontend/src/components/Powersportvehicles/ListPowersportvehicles.tsx b/frontend/src/components/Powersportvehicles/ListPowersportvehicles.tsx new file mode 100644 index 0000000..d647558 --- /dev/null +++ b/frontend/src/components/Powersportvehicles/ListPowersportvehicles.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + powersportvehicles: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListPowersportvehicles = ({ + powersportvehicles, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission( + currentUser, + 'UPDATE_POWERSPORTVEHICLES', + ); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + powersportvehicles.map((item) => ( +
+ +
+ + + dark:divide-dark-700 overflow-x-auto' + } + > +
+

Name

+

{item.name}

+
+ +
+

Description

+

{item.description}

+
+ +
+

Photos

+ +
+ +
+

Location

+

{item.location}

+
+ +
+

Pricehourly

+

{item.pricehourly}

+
+ +
+

Pricedaily

+

{item.pricedaily}

+
+ +
+

+ Securitydeposit +

+

{item.securitydeposit}

+
+ +
+

+ Availabilitystart +

+

+ {dataFormatter.dateTimeFormatter( + item.availabilitystart, + )} +

+
+ +
+

+ Availabilityend +

+

+ {dataFormatter.dateTimeFormatter(item.availabilityend)} +

+
+ +
+

Vehicletype

+

{item.vehicletype}

+
+ +
+

+ Preptimebefore +

+

{item.preptimebefore}

+
+ +
+

+ Preptimebetween +

+

{item.preptimebetween}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListPowersportvehicles; diff --git a/frontend/src/components/Powersportvehicles/TablePowersportvehicles.tsx b/frontend/src/components/Powersportvehicles/TablePowersportvehicles.tsx new file mode 100644 index 0000000..3963f61 --- /dev/null +++ b/frontend/src/components/Powersportvehicles/TablePowersportvehicles.tsx @@ -0,0 +1,489 @@ +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/powersportvehicles/powersportvehiclesSlice'; +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 './configurePowersportvehiclesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSamplePowersportvehicles = ({ + 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 { + powersportvehicles, + loading, + count, + notify: powersportvehiclesNotify, + refetch, + } = useAppSelector((state) => state.powersportvehicles); + 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 (powersportvehiclesNotify.showNotification) { + notify( + powersportvehiclesNotify.typeNotification, + powersportvehiclesNotify.textNotification, + ); + } + }, [powersportvehiclesNotify.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(() => { + if (!currentUser) return; + + loadColumns( + handleDeleteModalAction, + `powersportvehicles`, + currentUser, + ).then((newCols) => setColumns(newCols)); + }, [currentUser]); + + 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 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={powersportvehicles ?? []} + 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 TableSamplePowersportvehicles; diff --git a/frontend/src/components/Powersportvehicles/configurePowersportvehiclesCols.tsx b/frontend/src/components/Powersportvehicles/configurePowersportvehiclesCols.tsx new file mode 100644 index 0000000..f3188ac --- /dev/null +++ b/frontend/src/components/Powersportvehicles/configurePowersportvehiclesCols.tsx @@ -0,0 +1,235 @@ +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 ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_POWERSPORTVEHICLES'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'description', + headerName: 'Description', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'photos', + headerName: 'Photos', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: false, + sortable: false, + renderCell: (params: GridValueGetterParams) => ( + + ), + }, + + { + field: 'location', + headerName: 'Location', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'pricehourly', + headerName: 'Pricehourly', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'pricedaily', + headerName: 'Pricedaily', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'securitydeposit', + headerName: 'Securitydeposit', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'availabilitystart', + headerName: 'Availabilitystart', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.availabilitystart), + }, + + { + field: 'availabilityend', + headerName: 'Availabilityend', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'dateTime', + valueGetter: (params: GridValueGetterParams) => + new Date(params.row.availabilityend), + }, + + { + field: 'vehicletype', + headerName: 'Vehicletype', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'singleSelect', + valueOptions: ['value'], + }, + + { + field: 'preptimebefore', + headerName: 'Preptimebefore', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'preptimebetween', + headerName: 'Preptimebetween', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + + type: 'number', + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Vehicles/CardVehicles.tsx b/frontend/src/components/Vehicles/CardVehicles.tsx index 786cf88..45d0b8d 100644 --- a/frontend/src/components/Vehicles/CardVehicles.tsx +++ b/frontend/src/components/Vehicles/CardVehicles.tsx @@ -180,6 +180,17 @@ const CardVehicles = ({ + +
+
+ Vehicletype +
+
+
+ {item.vehicletype} +
+
+
))} diff --git a/frontend/src/components/Vehicles/ListVehicles.tsx b/frontend/src/components/Vehicles/ListVehicles.tsx index 974684c..66f91d5 100644 --- a/frontend/src/components/Vehicles/ListVehicles.tsx +++ b/frontend/src/components/Vehicles/ListVehicles.tsx @@ -118,6 +118,11 @@ const ListVehicles = ({ .join(', ')}

+ +
+

Vehicletype

+

{item.vehicletype}

+
state.style.borders); const websiteHeder = useAppSelector((state) => state.style.websiteHeder); - const style = FooterStyle.WITH_PAGES; + const style = FooterStyle.WITH_PROJECT_NAME; - const design = FooterDesigns.DEFAULT_DESIGN; + const design = FooterDesigns.DESIGN_DIVERSITY; return (
state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_RIGHT; + const style = HeaderStyle.PAGES_LEFT; - const design = HeaderDesigns.DEFAULT_DESIGN; + const design = HeaderDesigns.DESIGN_DIVERSITY; return (
item.name); + }, + powersportvehiclesOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + powersportvehiclesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + powersportvehiclesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, }; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 0077bf9..6500179 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -62,6 +62,14 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiTable ?? icon.mdiTable, permissions: 'READ_CLIENTS', }, + { + href: '/powersportvehicles/powersportvehicles-list', + label: 'Powersportvehicles', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_POWERSPORTVEHICLES', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 74041ad..70d91c3 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -140,8 +140,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { setStepsEnabled(false); }; - const title = 'Etherra'; - const description = 'Etherra generated by Flatlogic'; + const title = 'Powersport Rentals'; + const description = 'Powersport Rentals generated by Flatlogic'; const url = 'https://flatlogic.com/'; const image = `https://flatlogic.com/logo.svg`; const imageWidth = '1920'; diff --git a/frontend/src/pages/bookings/[bookingsId].tsx b/frontend/src/pages/bookings/[bookingsId].tsx index 47ff4a9..8351795 100644 --- a/frontend/src/pages/bookings/[bookingsId].tsx +++ b/frontend/src/pages/bookings/[bookingsId].tsx @@ -51,6 +51,8 @@ const EditBookings = () => { total_price: '', clients: null, + + powersportvehicle: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -198,6 +200,17 @@ const EditBookings = () => { > + + + + diff --git a/frontend/src/pages/bookings/bookings-edit.tsx b/frontend/src/pages/bookings/bookings-edit.tsx index 96f078a..504d88d 100644 --- a/frontend/src/pages/bookings/bookings-edit.tsx +++ b/frontend/src/pages/bookings/bookings-edit.tsx @@ -51,6 +51,8 @@ const EditBookingsPage = () => { total_price: '', clients: null, + + powersportvehicle: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -196,6 +198,17 @@ const EditBookingsPage = () => { > + + + + diff --git a/frontend/src/pages/bookings/bookings-list.tsx b/frontend/src/pages/bookings/bookings-list.tsx index 4504d4e..0055582 100644 --- a/frontend/src/pages/bookings/bookings-list.tsx +++ b/frontend/src/pages/bookings/bookings-list.tsx @@ -37,6 +37,8 @@ const BookingsTablesPage = () => { { label: 'Rentee', title: 'rentee' }, + { label: 'Powersportvehicle', title: 'powersportvehicle' }, + { label: 'Status', title: 'status', diff --git a/frontend/src/pages/bookings/bookings-new.tsx b/frontend/src/pages/bookings/bookings-new.tsx index ee6ba66..7c95716 100644 --- a/frontend/src/pages/bookings/bookings-new.tsx +++ b/frontend/src/pages/bookings/bookings-new.tsx @@ -46,6 +46,8 @@ const initialValues = { total_price: '', clients: '', + + powersportvehicle: '', }; const BookingsNew = () => { @@ -155,6 +157,16 @@ const BookingsNew = () => { > + + + + diff --git a/frontend/src/pages/bookings/bookings-table.tsx b/frontend/src/pages/bookings/bookings-table.tsx index 92aeaab..159fd70 100644 --- a/frontend/src/pages/bookings/bookings-table.tsx +++ b/frontend/src/pages/bookings/bookings-table.tsx @@ -37,6 +37,8 @@ const BookingsTablesPage = () => { { label: 'Rentee', title: 'rentee' }, + { label: 'Powersportvehicle', title: 'powersportvehicle' }, + { label: 'Status', title: 'status', diff --git a/frontend/src/pages/bookings/bookings-view.tsx b/frontend/src/pages/bookings/bookings-view.tsx index 61eb74f..3e7666b 100644 --- a/frontend/src/pages/bookings/bookings-view.tsx +++ b/frontend/src/pages/bookings/bookings-view.tsx @@ -124,6 +124,12 @@ const BookingsView = () => {

{bookings?.clients?.name ?? 'No data'}

+
+

Powersportvehicle

+ +

{bookings?.powersportvehicle?.name ?? 'No data'}

+
+ { PriceDaily SecurityDeposit + + Vehicletype @@ -213,6 +215,8 @@ const ClientsView = () => { {item.security_deposit} + + {item.vehicletype} ))} @@ -224,6 +228,97 @@ const ClientsView = () => { + <> +

Powersportvehicles clients

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + {clients.powersportvehicles_clients && + Array.isArray(clients.powersportvehicles_clients) && + clients.powersportvehicles_clients.map((item: any) => ( + + router.push( + `/powersportvehicles/powersportvehicles-view/?id=${item.id}`, + ) + } + > + + + + + + + + + + + + + + + + + + + + + + + ))} + +
NameDescriptionLocationPricehourlyPricedailySecuritydepositAvailabilitystartAvailabilityendVehicletypePreptimebeforePreptimebetween
{item.name}{item.description}{item.location}{item.pricehourly}{item.pricedaily} + {item.securitydeposit} + + {dataFormatter.dateTimeFormatter( + item.availabilitystart, + )} + + {dataFormatter.dateTimeFormatter( + item.availabilityend, + )} + {item.vehicletype} + {item.preptimebefore} + + {item.preptimebetween} +
+
+ {!clients?.powersportvehicles_clients?.length && ( +
No data
+ )} +
+ + { const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); const [clients, setClients] = React.useState(loadingMessage); + const [powersportvehicles, setPowersportvehicles] = + React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -53,6 +55,7 @@ const Dashboard = () => { 'roles', 'permissions', 'clients', + 'powersportvehicles', ]; const fns = [ setUsers, @@ -61,6 +64,7 @@ const Dashboard = () => { setRoles, setPermissions, setClients, + setPowersportvehicles, ]; const requests = entities.map((entity, index) => { @@ -376,6 +380,38 @@ const Dashboard = () => {
)} + + {hasPermission(currentUser, 'READ_POWERSPORTVEHICLES') && ( + +
+
+
+
+ Powersportvehicles +
+
+ {powersportvehicles} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 3f7a725..300faee 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -24,7 +24,7 @@ import ContactFormSection from '../components/WebPageComponents/ContactFormCompo export default function WebSite() { const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const projectName = 'Etherra'; + const projectName = 'Powersport Rentals'; useEffect(() => { const darkElement = document.querySelector('body .dark'); @@ -112,10 +112,10 @@ export default function WebSite() { content={`Discover and rent recreational vehicles with ease on our peer-to-peer RV rental platform. List your RV or find the perfect one for your next adventure.`} /> - +
- + ); } diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index ad612a3..215fd4e 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -52,7 +52,7 @@ export default function Login() { remember: true, }); - const title = 'Etherra'; + const title = 'Powersport Rentals'; // Fetch Pexels image/video useEffect(() => { diff --git a/frontend/src/pages/powersportvehicles/[powersportvehiclesId].tsx b/frontend/src/pages/powersportvehicles/[powersportvehiclesId].tsx new file mode 100644 index 0000000..4fe06b0 --- /dev/null +++ b/frontend/src/pages/powersportvehicles/[powersportvehiclesId].tsx @@ -0,0 +1,288 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { + update, + fetch, +} from '../../stores/powersportvehicles/powersportvehiclesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditPowersportvehicles = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + clients: null, + + name: '', + + description: '', + + photos: [], + + location: '', + + pricehourly: '', + + pricedaily: '', + + securitydeposit: '', + + availabilitystart: new Date(), + + availabilityend: new Date(), + + vehicletype: '', + + preptimebefore: '', + + preptimebetween: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { powersportvehicles } = useAppSelector( + (state) => state.powersportvehicles, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { powersportvehiclesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: powersportvehiclesId })); + }, [powersportvehiclesId]); + + useEffect(() => { + if (typeof powersportvehicles === 'object') { + setInitialValues(powersportvehicles); + } + }, [powersportvehicles]); + + useEffect(() => { + if (typeof powersportvehicles === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = powersportvehicles[el]), + ); + + setInitialValues(newInitialVal); + } + }, [powersportvehicles]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: powersportvehiclesId, data })); + await router.push('/powersportvehicles/powersportvehicles-list'); + }; + + return ( + <> + + {getPageTitle('Edit powersportvehicles')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ + ...initialValues, + availabilitystart: date, + }) + } + /> + + + + + setInitialValues({ + ...initialValues, + availabilityend: date, + }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + router.push('/powersportvehicles/powersportvehicles-list') + } + /> + + +
+
+
+ + ); +}; + +EditPowersportvehicles.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPowersportvehicles; diff --git a/frontend/src/pages/powersportvehicles/powersportvehicles-edit.tsx b/frontend/src/pages/powersportvehicles/powersportvehicles-edit.tsx new file mode 100644 index 0000000..bf04fbc --- /dev/null +++ b/frontend/src/pages/powersportvehicles/powersportvehicles-edit.tsx @@ -0,0 +1,286 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { + update, + fetch, +} from '../../stores/powersportvehicles/powersportvehiclesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditPowersportvehiclesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + clients: null, + + name: '', + + description: '', + + photos: [], + + location: '', + + pricehourly: '', + + pricedaily: '', + + securitydeposit: '', + + availabilitystart: new Date(), + + availabilityend: new Date(), + + vehicletype: '', + + preptimebefore: '', + + preptimebetween: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { powersportvehicles } = useAppSelector( + (state) => state.powersportvehicles, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof powersportvehicles === 'object') { + setInitialValues(powersportvehicles); + } + }, [powersportvehicles]); + + useEffect(() => { + if (typeof powersportvehicles === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach( + (el) => (newInitialVal[el] = powersportvehicles[el]), + ); + setInitialValues(newInitialVal); + } + }, [powersportvehicles]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/powersportvehicles/powersportvehicles-list'); + }; + + return ( + <> + + {getPageTitle('Edit powersportvehicles')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setInitialValues({ + ...initialValues, + availabilitystart: date, + }) + } + /> + + + + + setInitialValues({ + ...initialValues, + availabilityend: date, + }) + } + /> + + + + + + + + + + + + + + + + + + + + + + + + router.push('/powersportvehicles/powersportvehicles-list') + } + /> + + +
+
+
+ + ); +}; + +EditPowersportvehiclesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditPowersportvehiclesPage; diff --git a/frontend/src/pages/powersportvehicles/powersportvehicles-list.tsx b/frontend/src/pages/powersportvehicles/powersportvehicles-list.tsx new file mode 100644 index 0000000..d5a7f44 --- /dev/null +++ b/frontend/src/pages/powersportvehicles/powersportvehicles-list.tsx @@ -0,0 +1,185 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePowersportvehicles from '../../components/Powersportvehicles/TablePowersportvehicles'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/powersportvehicles/powersportvehiclesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PowersportvehiclesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Description', title: 'description' }, + { label: 'Location', title: 'location' }, + { label: 'Preptimebefore', title: 'preptimebefore', number: 'true' }, + { label: 'Preptimebetween', title: 'preptimebetween', number: 'true' }, + { label: 'Pricehourly', title: 'pricehourly', number: 'true' }, + { label: 'Pricedaily', title: 'pricedaily', number: 'true' }, + { label: 'Securitydeposit', title: 'securitydeposit', number: 'true' }, + { label: 'Availabilitystart', title: 'availabilitystart', date: 'true' }, + { label: 'Availabilityend', title: 'availabilityend', date: 'true' }, + + { + label: 'Vehicletype', + title: 'vehicletype', + type: 'enum', + options: ['value'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_POWERSPORTVEHICLES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPowersportvehiclesCSV = async () => { + const response = await axios({ + url: '/powersportvehicles?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'powersportvehiclesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Powersportvehicles')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +PowersportvehiclesTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default PowersportvehiclesTablesPage; diff --git a/frontend/src/pages/powersportvehicles/powersportvehicles-new.tsx b/frontend/src/pages/powersportvehicles/powersportvehicles-new.tsx new file mode 100644 index 0000000..2c9f5fe --- /dev/null +++ b/frontend/src/pages/powersportvehicles/powersportvehicles-new.tsx @@ -0,0 +1,222 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/powersportvehicles/powersportvehiclesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + clients: '', + + name: '', + + description: '', + + photos: [], + + location: '', + + pricehourly: '', + + pricedaily: '', + + securitydeposit: '', + + availabilitystart: '', + + availabilityend: '', + + vehicletype: '', + + preptimebefore: '', + + preptimebetween: '', +}; + +const PowersportvehiclesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/powersportvehicles/powersportvehicles-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + router.push('/powersportvehicles/powersportvehicles-list') + } + /> + + +
+
+
+ + ); +}; + +PowersportvehiclesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PowersportvehiclesNew; diff --git a/frontend/src/pages/powersportvehicles/powersportvehicles-table.tsx b/frontend/src/pages/powersportvehicles/powersportvehicles-table.tsx new file mode 100644 index 0000000..6d6d2a5 --- /dev/null +++ b/frontend/src/pages/powersportvehicles/powersportvehicles-table.tsx @@ -0,0 +1,184 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TablePowersportvehicles from '../../components/Powersportvehicles/TablePowersportvehicles'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { + setRefetch, + uploadCsv, +} from '../../stores/powersportvehicles/powersportvehiclesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PowersportvehiclesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Description', title: 'description' }, + { label: 'Location', title: 'location' }, + { label: 'Preptimebefore', title: 'preptimebefore', number: 'true' }, + { label: 'Preptimebetween', title: 'preptimebetween', number: 'true' }, + { label: 'Pricehourly', title: 'pricehourly', number: 'true' }, + { label: 'Pricedaily', title: 'pricedaily', number: 'true' }, + { label: 'Securitydeposit', title: 'securitydeposit', number: 'true' }, + { label: 'Availabilitystart', title: 'availabilitystart', date: 'true' }, + { label: 'Availabilityend', title: 'availabilityend', date: 'true' }, + + { + label: 'Vehicletype', + title: 'vehicletype', + type: 'enum', + options: ['value'], + }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_POWERSPORTVEHICLES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getPowersportvehiclesCSV = async () => { + const response = await axios({ + url: '/powersportvehicles?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'powersportvehiclesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Powersportvehicles')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +PowersportvehiclesTablesPage.getLayout = function getLayout( + page: ReactElement, +) { + return ( + + {page} + + ); +}; + +export default PowersportvehiclesTablesPage; diff --git a/frontend/src/pages/powersportvehicles/powersportvehicles-view.tsx b/frontend/src/pages/powersportvehicles/powersportvehicles-view.tsx new file mode 100644 index 0000000..08aee32 --- /dev/null +++ b/frontend/src/pages/powersportvehicles/powersportvehicles-view.tsx @@ -0,0 +1,249 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/powersportvehicles/powersportvehiclesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const PowersportvehiclesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { powersportvehicles } = useAppSelector( + (state) => state.powersportvehicles, + ); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View powersportvehicles')} + + + + + + +
+

clients

+ +

{powersportvehicles?.clients?.name ?? 'No data'}

+
+ +
+

Name

+

{powersportvehicles?.name}

+
+ +
+

Description

+

{powersportvehicles?.description}

+
+ +
+

Photos

+ {powersportvehicles?.photos?.length ? ( + + ) : ( +

No Photos

+ )} +
+ +
+

Location

+

{powersportvehicles?.location}

+
+ +
+

Pricehourly

+

{powersportvehicles?.pricehourly || 'No data'}

+
+ +
+

Pricedaily

+

{powersportvehicles?.pricedaily || 'No data'}

+
+ +
+

Securitydeposit

+

{powersportvehicles?.securitydeposit || 'No data'}

+
+ + + {powersportvehicles.availabilitystart ? ( + + ) : ( +

No Availabilitystart

+ )} +
+ + + {powersportvehicles.availabilityend ? ( + + ) : ( +

No Availabilityend

+ )} +
+ +
+

Vehicletype

+

{powersportvehicles?.vehicletype ?? 'No data'}

+
+ +
+

Preptimebefore

+

{powersportvehicles?.preptimebefore || 'No data'}

+
+ +
+

Preptimebetween

+

{powersportvehicles?.preptimebetween || 'No data'}

+
+ + <> +

Bookings Powersportvehicle

+ +
+ + + + + + + + + + + + + + {powersportvehicles.bookings_powersportvehicle && + Array.isArray( + powersportvehicles.bookings_powersportvehicle, + ) && + powersportvehicles.bookings_powersportvehicle.map( + (item: any) => ( + + router.push( + `/bookings/bookings-view/?id=${item.id}`, + ) + } + > + + + + + + + + + ), + )} + +
StartDateEndDateStatusTotalPrice
+ {dataFormatter.dateTimeFormatter(item.start_date)} + + {dataFormatter.dateTimeFormatter(item.end_date)} + {item.status}{item.total_price}
+
+ {!powersportvehicles?.bookings_powersportvehicle?.length && ( +
No data
+ )} +
+ + + + + + router.push('/powersportvehicles/powersportvehicles-list') + } + /> +
+
+ + ); +}; + +PowersportvehiclesView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default PowersportvehiclesView; diff --git a/frontend/src/pages/privacy-policy.tsx b/frontend/src/pages/privacy-policy.tsx index 0925e13..6658113 100644 --- a/frontend/src/pages/privacy-policy.tsx +++ b/frontend/src/pages/privacy-policy.tsx @@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest'; import { getPageTitle } from '../config'; export default function PrivacyPolicy() { - const title = 'Etherra'; + const title = 'Powersport Rentals'; const [projectUrl, setProjectUrl] = useState(''); useEffect(() => { diff --git a/frontend/src/pages/terms-of-use.tsx b/frontend/src/pages/terms-of-use.tsx index 5b7c74b..ee14fac 100644 --- a/frontend/src/pages/terms-of-use.tsx +++ b/frontend/src/pages/terms-of-use.tsx @@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest'; import { getPageTitle } from '../config'; export default function PrivacyPolicy() { - const title = 'Etherra'; + const title = 'Powersport Rentals'; const [projectUrl, setProjectUrl] = useState(''); useEffect(() => { diff --git a/frontend/src/pages/users/users-view.tsx b/frontend/src/pages/users/users-view.tsx index 78ee0c7..13f3e2e 100644 --- a/frontend/src/pages/users/users-view.tsx +++ b/frontend/src/pages/users/users-view.tsx @@ -220,6 +220,8 @@ const UsersView = () => { PriceDaily SecurityDeposit + + Vehicletype @@ -245,6 +247,8 @@ const UsersView = () => { {item.security_deposit} + + {item.vehicletype} ))} diff --git a/frontend/src/pages/vehicles/[vehiclesId].tsx b/frontend/src/pages/vehicles/[vehiclesId].tsx index 803a921..9e02209 100644 --- a/frontend/src/pages/vehicles/[vehiclesId].tsx +++ b/frontend/src/pages/vehicles/[vehiclesId].tsx @@ -57,6 +57,8 @@ const EditVehicles = () => { bookings: [], clients: null, + + vehicletype: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -200,6 +202,14 @@ const EditVehicles = () => { > + + + + + + + + diff --git a/frontend/src/pages/vehicles/vehicles-edit.tsx b/frontend/src/pages/vehicles/vehicles-edit.tsx index c60649d..d5e684b 100644 --- a/frontend/src/pages/vehicles/vehicles-edit.tsx +++ b/frontend/src/pages/vehicles/vehicles-edit.tsx @@ -57,6 +57,8 @@ const EditVehiclesPage = () => { bookings: [], clients: null, + + vehicletype: '', }; const [initialValues, setInitialValues] = useState(initVals); @@ -198,6 +200,14 @@ const EditVehiclesPage = () => { > + + + + + + + + diff --git a/frontend/src/pages/vehicles/vehicles-list.tsx b/frontend/src/pages/vehicles/vehicles-list.tsx index 35b613e..d2624a5 100644 --- a/frontend/src/pages/vehicles/vehicles-list.tsx +++ b/frontend/src/pages/vehicles/vehicles-list.tsx @@ -40,6 +40,12 @@ const VehiclesTablesPage = () => { { label: 'Owner', title: 'owner' }, { label: 'Bookings', title: 'bookings' }, + { + label: 'Vehicletype', + title: 'vehicletype', + type: 'enum', + options: ['value'], + }, ]); const hasCreatePermission = diff --git a/frontend/src/pages/vehicles/vehicles-new.tsx b/frontend/src/pages/vehicles/vehicles-new.tsx index 1c7eb18..0100e12 100644 --- a/frontend/src/pages/vehicles/vehicles-new.tsx +++ b/frontend/src/pages/vehicles/vehicles-new.tsx @@ -52,6 +52,8 @@ const initialValues = { bookings: [], clients: '', + + vehicletype: '', }; const VehiclesNew = () => { @@ -167,6 +169,14 @@ const VehiclesNew = () => { > + + + + + + + + diff --git a/frontend/src/pages/vehicles/vehicles-table.tsx b/frontend/src/pages/vehicles/vehicles-table.tsx index 8c02eb9..cd8ab89 100644 --- a/frontend/src/pages/vehicles/vehicles-table.tsx +++ b/frontend/src/pages/vehicles/vehicles-table.tsx @@ -40,6 +40,12 @@ const VehiclesTablesPage = () => { { label: 'Owner', title: 'owner' }, { label: 'Bookings', title: 'bookings' }, + { + label: 'Vehicletype', + title: 'vehicletype', + type: 'enum', + options: ['value'], + }, ]); const hasCreatePermission = diff --git a/frontend/src/pages/vehicles/vehicles-view.tsx b/frontend/src/pages/vehicles/vehicles-view.tsx index 71b88be..76ba189 100644 --- a/frontend/src/pages/vehicles/vehicles-view.tsx +++ b/frontend/src/pages/vehicles/vehicles-view.tsx @@ -170,6 +170,11 @@ const VehiclesView = () => {

{vehicles?.clients?.name ?? 'No data'}

+
+

Vehicletype

+

{vehicles?.vehicletype ?? 'No data'}

+
+ <>

Bookings Vehicle

state.style.cardsStyle); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const projectName = 'Etherra'; + const projectName = 'Powersport Rentals'; useEffect(() => { const darkElement = document.querySelector('body .dark'); @@ -77,10 +77,10 @@ export default function WebSite() { content={`Get in touch with our team for any inquiries or support related to our peer-to-peer RV rental platform. We're here to help you with your adventure needs.`} /> - +
- + ); } diff --git a/frontend/src/pages/web_pages/faq.tsx b/frontend/src/pages/web_pages/faq.tsx index 075e9a7..b4e8f63 100644 --- a/frontend/src/pages/web_pages/faq.tsx +++ b/frontend/src/pages/web_pages/faq.tsx @@ -18,7 +18,7 @@ import FaqSection from '../../components/WebPageComponents/FaqComponent'; export default function WebSite() { const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const projectName = 'Etherra'; + const projectName = 'Powersport Rentals'; useEffect(() => { const darkElement = document.querySelector('body .dark'); @@ -69,10 +69,10 @@ export default function WebSite() { content={`Find answers to common questions about our peer-to-peer RV rental platform. Learn about listing, booking, security, and more to enhance your rental experience.`} /> - +
- + ); } diff --git a/frontend/src/pages/web_pages/home.tsx b/frontend/src/pages/web_pages/home.tsx index 8c95faf..a28a8e8 100644 --- a/frontend/src/pages/web_pages/home.tsx +++ b/frontend/src/pages/web_pages/home.tsx @@ -24,7 +24,7 @@ import ContactFormSection from '../../components/WebPageComponents/ContactFormCo export default function WebSite() { const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const projectName = 'Etherra'; + const projectName = 'Powersport Rentals'; useEffect(() => { const darkElement = document.querySelector('body .dark'); @@ -96,10 +96,10 @@ export default function WebSite() { content={`Discover and rent recreational vehicles with ease on our peer-to-peer RV rental platform. List your RV or find the perfect one for your next adventure.`} /> - +
- + ); } diff --git a/frontend/src/stores/powersportvehicles/powersportvehiclesSlice.ts b/frontend/src/stores/powersportvehicles/powersportvehiclesSlice.ts new file mode 100644 index 0000000..73c16f1 --- /dev/null +++ b/frontend/src/stores/powersportvehicles/powersportvehiclesSlice.ts @@ -0,0 +1,250 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + powersportvehicles: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + powersportvehicles: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk( + 'powersportvehicles/fetch', + async (data: any) => { + const { id, query } = data; + const result = await axios.get( + `powersportvehicles${query || (id ? `/${id}` : '')}`, + ); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; + }, +); + +export const deleteItemsByIds = createAsyncThunk( + 'powersportvehicles/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('powersportvehicles/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'powersportvehicles/deletePowersportvehicles', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`powersportvehicles/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'powersportvehicles/createPowersportvehicles', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('powersportvehicles', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'powersportvehicles/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('powersportvehicles/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'powersportvehicles/updatePowersportvehicles', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`powersportvehicles/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const powersportvehiclesSlice = createSlice({ + name: 'powersportvehicles', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.powersportvehicles = action.payload.rows; + state.count = action.payload.count; + } else { + state.powersportvehicles = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Powersportvehicles has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Powersportvehicles'.slice(0, -1)} has been deleted`, + ); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Powersportvehicles'.slice(0, -1)} has been created`, + ); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify( + state, + `${'Powersportvehicles'.slice(0, -1)} has been updated`, + ); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Powersportvehicles has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = powersportvehiclesSlice.actions; + +export default powersportvehiclesSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 8a42a7c..3357126 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -10,6 +10,7 @@ import vehiclesSlice from './vehicles/vehiclesSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import clientsSlice from './clients/clientsSlice'; +import powersportvehiclesSlice from './powersportvehicles/powersportvehiclesSlice'; export const store = configureStore({ reducer: { @@ -24,6 +25,7 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, clients: clientsSlice, + powersportvehicles: powersportvehiclesSlice, }, });