From 9777272bfc9b954554cca918f9b141de0ade0d89 Mon Sep 17 00:00:00 2001
From: Flatlogic Bot
Date: Tue, 26 Aug 2025 19:55:03 +0000
Subject: [PATCH] Complete restart
---
app-shell/src/_schema.json | 3 +-
backend/src/db/api/bottles.js | 579 +++---
backend/src/db/api/brands.js | 30 +-
.../src/db/api/conversationparticipants.js | 335 ++++
backend/src/db/api/conversations.js | 285 +++
backend/src/db/api/distilleries.js | 54 +-
backend/src/db/api/locations.js | 294 +++
backend/src/db/api/messages.js | 321 ++++
backend/src/db/api/photos.js | 303 ++++
backend/src/db/api/products.js | 484 +++++
backend/src/db/api/reviews.js | 386 ++++
backend/src/db/api/users.js | 39 +
backend/src/db/migrations/1756232919789.js | 72 +
backend/src/db/migrations/1756232938531.js | 72 +
backend/src/db/migrations/1756232961734.js | 72 +
backend/src/db/migrations/1756232993973.js | 72 +
backend/src/db/migrations/1756233026221.js | 49 +
backend/src/db/migrations/1756233047456.js | 49 +
backend/src/db/migrations/1756233083420.js | 49 +
backend/src/db/migrations/1756233114262.js | 72 +
backend/src/db/migrations/1756233134331.js | 47 +
backend/src/db/migrations/1756233161698.js | 72 +
backend/src/db/migrations/1756233186114.js | 47 +
backend/src/db/migrations/1756233219308.js | 36 +
backend/src/db/migrations/1756233254738.js | 49 +
backend/src/db/migrations/1756233282118.js | 72 +
backend/src/db/migrations/1756233314139.js | 47 +
backend/src/db/migrations/1756233343308.js | 47 +
backend/src/db/migrations/1756233376774.js | 52 +
backend/src/db/migrations/1756233417905.js | 47 +
backend/src/db/migrations/1756233460513.js | 72 +
backend/src/db/migrations/1756233493388.js | 47 +
backend/src/db/migrations/1756233535083.js | 54 +
backend/src/db/migrations/1756233583823.js | 47 +
backend/src/db/migrations/1756233847816.js | 47 +
backend/src/db/migrations/1756233872842.js | 47 +
backend/src/db/migrations/1756233895732.js | 47 +
backend/src/db/migrations/1756233923545.js | 47 +
backend/src/db/migrations/1756233946141.js | 47 +
backend/src/db/migrations/1756233969555.js | 54 +
backend/src/db/migrations/1756233991015.js | 54 +
backend/src/db/migrations/1756234013059.js | 52 +
backend/src/db/migrations/1756234037032.js | 72 +
backend/src/db/migrations/1756234060391.js | 52 +
backend/src/db/migrations/1756234087868.js | 54 +
backend/src/db/migrations/1756234116962.js | 54 +
backend/src/db/migrations/1756234140311.js | 47 +
backend/src/db/migrations/1756234165049.js | 47 +
backend/src/db/migrations/1756234187286.js | 47 +
backend/src/db/migrations/1756234208774.js | 52 +
backend/src/db/migrations/1756234237031.js | 49 +
backend/src/db/migrations/1756234265034.js | 47 +
backend/src/db/migrations/1756234286983.js | 47 +
backend/src/db/migrations/1756234308313.js | 49 +
backend/src/db/migrations/1756234331914.js | 49 +
backend/src/db/migrations/1756234359186.js | 49 +
backend/src/db/migrations/1756234382031.js | 49 +
backend/src/db/migrations/1756234410684.js | 47 +
backend/src/db/migrations/1756234436241.js | 47 +
backend/src/db/migrations/1756234471854.js | 54 +
backend/src/db/migrations/1756234493098.js | 54 +
backend/src/db/migrations/1756234515450.js | 72 +
backend/src/db/migrations/1756234537835.js | 52 +
backend/src/db/migrations/1756234563408.js | 52 +
backend/src/db/migrations/1756234596232.js | 47 +
backend/src/db/migrations/1756234618834.js | 47 +
backend/src/db/migrations/1756234642956.js | 49 +
backend/src/db/migrations/1756234669525.js | 72 +
backend/src/db/migrations/1756234695418.js | 49 +
backend/src/db/migrations/1756234721487.js | 72 +
backend/src/db/migrations/1756234750821.js | 54 +
backend/src/db/migrations/1756234775299.js | 54 +
backend/src/db/migrations/1756234799569.js | 74 +
backend/src/db/migrations/1756234822685.js | 56 +
backend/src/db/migrations/1756234849724.js | 54 +
backend/src/db/models/bottles.js | 138 +-
backend/src/db/models/brands.js | 8 +-
.../src/db/models/conversationparticipants.js | 61 +
backend/src/db/models/conversations.js | 65 +
backend/src/db/models/distilleries.js | 20 +-
backend/src/db/models/locations.js | 65 +
backend/src/db/models/messages.js | 61 +
backend/src/db/models/photos.js | 91 +
backend/src/db/models/products.js | 101 ++
backend/src/db/models/reviews.js | 73 +
backend/src/db/models/users.js | 40 +
.../db/seeders/20200430130760-user-roles.js | 430 +++--
.../db/seeders/20231127130745-sample-data.js | 1612 ++++++++++++++---
backend/src/db/seeders/20250826182953.js | 87 +
backend/src/db/seeders/20250826183154.js | 87 +
backend/src/db/seeders/20250826183241.js | 87 +
backend/src/db/seeders/20250826183442.js | 87 +
backend/src/db/seeders/20250826183740.js | 87 +
backend/src/db/seeders/20250826184717.js | 87 +
backend/src/db/seeders/20250826185515.js | 87 +
backend/src/db/seeders/20250826185749.js | 87 +
backend/src/db/seeders/20250826185841.js | 87 +
backend/src/db/seeders/20250826185959.js | 87 +
backend/src/index.js | 104 +-
backend/src/routes/bottles.js | 58 +-
backend/src/routes/brands.js | 6 +-
.../src/routes/conversationparticipants.js | 449 +++++
backend/src/routes/conversations.js | 440 +++++
backend/src/routes/distilleries.js | 12 +-
backend/src/routes/locations.js | 438 +++++
backend/src/routes/messages.js | 434 +++++
backend/src/routes/photos.js | 433 +++++
backend/src/routes/products.js | 463 +++++
backend/src/routes/reviews.js | 442 +++++
backend/src/routes/users.js | 16 +-
.../src/services/conversationparticipants.js | 118 ++
backend/src/services/conversations.js | 117 ++
backend/src/services/locations.js | 114 ++
backend/src/services/messages.js | 114 ++
backend/src/services/photos.js | 114 ++
backend/src/services/products.js | 114 ++
backend/src/services/reviews.js | 114 ++
backend/src/services/search.js | 50 +-
.../src/components/Bottles/CardBottles.tsx | 245 +--
.../src/components/Bottles/ListBottles.tsx | 150 +-
.../src/components/Bottles/TableBottles.tsx | 15 +-
.../Bottles/configureBottlesCols.tsx | 320 ++--
frontend/src/components/Brands/CardBrands.tsx | 11 +
frontend/src/components/Brands/ListBrands.tsx | 5 +
.../src/components/Brands/TableBrands.tsx | 15 +-
.../components/Brands/configureBrandsCols.tsx | 14 +
.../CardConversationparticipants.tsx | 123 ++
.../ListConversationparticipants.tsx | 101 ++
.../TableConversationparticipants.tsx | 486 +++++
.../configureConversationparticipantsCols.tsx | 105 ++
.../Conversations/CardConversations.tsx | 112 ++
.../Conversations/ListConversations.tsx | 92 +
.../Conversations/TableConversations.tsx | 484 +++++
.../configureConversationsCols.tsx | 78 +
.../Distilleries/CardDistilleries.tsx | 25 +
.../Distilleries/ListDistilleries.tsx | 15 +
.../Distilleries/TableDistilleries.tsx | 15 +-
.../configureDistilleriesCols.tsx | 38 +
.../components/Locations/CardLocations.tsx | 114 ++
.../components/Locations/ListLocations.tsx | 94 +
.../components/Locations/TableLocations.tsx | 484 +++++
.../Locations/configureLocationsCols.tsx | 94 +
.../src/components/Messages/CardMessages.tsx | 122 ++
.../src/components/Messages/ListMessages.tsx | 98 +
.../src/components/Messages/TableMessages.tsx | 481 +++++
.../Messages/configureMessagesCols.tsx | 102 ++
frontend/src/components/Photos/CardPhotos.tsx | 128 ++
frontend/src/components/Photos/ListPhotos.tsx | 105 ++
.../src/components/Photos/TablePhotos.tsx | 481 +++++
.../components/Photos/configurePhotosCols.tsx | 94 +
.../src/components/Products/CardProducts.tsx | 179 ++
.../src/components/Products/ListProducts.tsx | 133 ++
.../src/components/Products/TableProducts.tsx | 481 +++++
.../Products/configureProductsCols.tsx | 200 ++
.../src/components/Reviews/CardReviews.tsx | 147 ++
.../src/components/Reviews/ListReviews.tsx | 113 ++
.../src/components/Reviews/TableReviews.tsx | 481 +++++
.../Reviews/configureReviewsCols.tsx | 144 ++
frontend/src/components/Users/CardUsers.tsx | 22 +
frontend/src/components/Users/ListUsers.tsx | 10 +
.../components/Users/configureUsersCols.tsx | 24 +
.../components/WebPageComponents/Header.tsx | 2 +-
frontend/src/helpers/dataFormatter.js | 171 +-
frontend/src/menuAside.ts | 113 +-
frontend/src/pages/bottles/[bottlesId].tsx | 286 +--
frontend/src/pages/bottles/bottles-edit.tsx | 288 +--
frontend/src/pages/bottles/bottles-list.tsx | 36 +-
frontend/src/pages/bottles/bottles-new.tsx | 264 +--
frontend/src/pages/bottles/bottles-table.tsx | 36 +-
frontend/src/pages/bottles/bottles-view.tsx | 177 +-
frontend/src/pages/brands/[brandsId].tsx | 6 +
frontend/src/pages/brands/brands-edit.tsx | 6 +
frontend/src/pages/brands/brands-list.tsx | 5 +-
frontend/src/pages/brands/brands-new.tsx | 6 +
frontend/src/pages/brands/brands-table.tsx | 5 +-
frontend/src/pages/brands/brands-view.tsx | 53 +-
.../[conversationparticipantsId].tsx | 159 ++
.../conversationparticipants-edit.tsx | 157 ++
.../conversationparticipants-list.tsx | 172 ++
.../conversationparticipants-new.tsx | 122 ++
.../conversationparticipants-table.tsx | 171 ++
.../conversationparticipants-view.tsx | 98 +
.../pages/conversations/[conversationsId].tsx | 143 ++
.../conversations/conversations-edit.tsx | 141 ++
.../conversations/conversations-list.tsx | 167 ++
.../pages/conversations/conversations-new.tsx | 104 ++
.../conversations/conversations-table.tsx | 166 ++
.../conversations/conversations-view.tsx | 170 ++
frontend/src/pages/dashboard.tsx | 470 +++--
.../pages/distilleries/[distilleriesId].tsx | 18 +
.../pages/distilleries/distilleries-edit.tsx | 18 +
.../pages/distilleries/distilleries-list.tsx | 13 +-
.../pages/distilleries/distilleries-new.tsx | 18 +
.../pages/distilleries/distilleries-table.tsx | 11 +-
.../pages/distilleries/distilleries-view.tsx | 86 +-
.../src/pages/locations/[locationsId].tsx | 139 ++
.../src/pages/locations/locations-edit.tsx | 137 ++
.../src/pages/locations/locations-list.tsx | 166 ++
.../src/pages/locations/locations-new.tsx | 110 ++
.../src/pages/locations/locations-table.tsx | 165 ++
.../src/pages/locations/locations-view.tsx | 178 ++
frontend/src/pages/messages/[messagesId].tsx | 144 ++
frontend/src/pages/messages/messages-edit.tsx | 142 ++
frontend/src/pages/messages/messages-list.tsx | 166 ++
frontend/src/pages/messages/messages-new.tsx | 116 ++
.../src/pages/messages/messages-table.tsx | 165 ++
frontend/src/pages/messages/messages-view.tsx | 90 +
frontend/src/pages/photos/[photosId].tsx | 142 ++
frontend/src/pages/photos/photos-edit.tsx | 140 ++
frontend/src/pages/photos/photos-list.tsx | 160 ++
frontend/src/pages/photos/photos-new.tsx | 116 ++
frontend/src/pages/photos/photos-table.tsx | 159 ++
frontend/src/pages/photos/photos-view.tsx | 386 ++++
frontend/src/pages/products/[productsId].tsx | 193 ++
frontend/src/pages/products/products-edit.tsx | 191 ++
frontend/src/pages/products/products-list.tsx | 175 ++
frontend/src/pages/products/products-new.tsx | 164 ++
.../src/pages/products/products-table.tsx | 174 ++
frontend/src/pages/products/products-view.tsx | 215 +++
frontend/src/pages/reviews/[reviewsId].tsx | 177 ++
frontend/src/pages/reviews/reviews-edit.tsx | 175 ++
frontend/src/pages/reviews/reviews-list.tsx | 171 ++
frontend/src/pages/reviews/reviews-new.tsx | 138 ++
frontend/src/pages/reviews/reviews-table.tsx | 170 ++
frontend/src/pages/reviews/reviews-view.tsx | 119 ++
frontend/src/pages/roles/roles-view.tsx | 8 +
frontend/src/pages/users/[usersId].tsx | 12 +
frontend/src/pages/users/users-edit.tsx | 12 +
frontend/src/pages/users/users-list.tsx | 2 +
frontend/src/pages/users/users-new.tsx | 12 +
frontend/src/pages/users/users-table.tsx | 2 +
frontend/src/pages/users/users-view.tsx | 244 ++-
frontend/src/pages/web_pages/pricing.tsx | 2 +-
frontend/src/pages/web_pages/products.tsx | 4 +-
.../conversationparticipantsSlice.ts | 254 +++
.../conversations/conversationsSlice.ts | 250 +++
.../src/stores/locations/locationsSlice.ts | 236 +++
frontend/src/stores/messages/messagesSlice.ts | 236 +++
frontend/src/stores/photos/photosSlice.ts | 236 +++
frontend/src/stores/products/productsSlice.ts | 236 +++
frontend/src/stores/reviews/reviewsSlice.ts | 236 +++
frontend/src/stores/store.ts | 26 +-
242 files changed, 29897 insertions(+), 1938 deletions(-)
create mode 100644 backend/src/db/api/conversationparticipants.js
create mode 100644 backend/src/db/api/conversations.js
create mode 100644 backend/src/db/api/locations.js
create mode 100644 backend/src/db/api/messages.js
create mode 100644 backend/src/db/api/photos.js
create mode 100644 backend/src/db/api/products.js
create mode 100644 backend/src/db/api/reviews.js
create mode 100644 backend/src/db/migrations/1756232919789.js
create mode 100644 backend/src/db/migrations/1756232938531.js
create mode 100644 backend/src/db/migrations/1756232961734.js
create mode 100644 backend/src/db/migrations/1756232993973.js
create mode 100644 backend/src/db/migrations/1756233026221.js
create mode 100644 backend/src/db/migrations/1756233047456.js
create mode 100644 backend/src/db/migrations/1756233083420.js
create mode 100644 backend/src/db/migrations/1756233114262.js
create mode 100644 backend/src/db/migrations/1756233134331.js
create mode 100644 backend/src/db/migrations/1756233161698.js
create mode 100644 backend/src/db/migrations/1756233186114.js
create mode 100644 backend/src/db/migrations/1756233219308.js
create mode 100644 backend/src/db/migrations/1756233254738.js
create mode 100644 backend/src/db/migrations/1756233282118.js
create mode 100644 backend/src/db/migrations/1756233314139.js
create mode 100644 backend/src/db/migrations/1756233343308.js
create mode 100644 backend/src/db/migrations/1756233376774.js
create mode 100644 backend/src/db/migrations/1756233417905.js
create mode 100644 backend/src/db/migrations/1756233460513.js
create mode 100644 backend/src/db/migrations/1756233493388.js
create mode 100644 backend/src/db/migrations/1756233535083.js
create mode 100644 backend/src/db/migrations/1756233583823.js
create mode 100644 backend/src/db/migrations/1756233847816.js
create mode 100644 backend/src/db/migrations/1756233872842.js
create mode 100644 backend/src/db/migrations/1756233895732.js
create mode 100644 backend/src/db/migrations/1756233923545.js
create mode 100644 backend/src/db/migrations/1756233946141.js
create mode 100644 backend/src/db/migrations/1756233969555.js
create mode 100644 backend/src/db/migrations/1756233991015.js
create mode 100644 backend/src/db/migrations/1756234013059.js
create mode 100644 backend/src/db/migrations/1756234037032.js
create mode 100644 backend/src/db/migrations/1756234060391.js
create mode 100644 backend/src/db/migrations/1756234087868.js
create mode 100644 backend/src/db/migrations/1756234116962.js
create mode 100644 backend/src/db/migrations/1756234140311.js
create mode 100644 backend/src/db/migrations/1756234165049.js
create mode 100644 backend/src/db/migrations/1756234187286.js
create mode 100644 backend/src/db/migrations/1756234208774.js
create mode 100644 backend/src/db/migrations/1756234237031.js
create mode 100644 backend/src/db/migrations/1756234265034.js
create mode 100644 backend/src/db/migrations/1756234286983.js
create mode 100644 backend/src/db/migrations/1756234308313.js
create mode 100644 backend/src/db/migrations/1756234331914.js
create mode 100644 backend/src/db/migrations/1756234359186.js
create mode 100644 backend/src/db/migrations/1756234382031.js
create mode 100644 backend/src/db/migrations/1756234410684.js
create mode 100644 backend/src/db/migrations/1756234436241.js
create mode 100644 backend/src/db/migrations/1756234471854.js
create mode 100644 backend/src/db/migrations/1756234493098.js
create mode 100644 backend/src/db/migrations/1756234515450.js
create mode 100644 backend/src/db/migrations/1756234537835.js
create mode 100644 backend/src/db/migrations/1756234563408.js
create mode 100644 backend/src/db/migrations/1756234596232.js
create mode 100644 backend/src/db/migrations/1756234618834.js
create mode 100644 backend/src/db/migrations/1756234642956.js
create mode 100644 backend/src/db/migrations/1756234669525.js
create mode 100644 backend/src/db/migrations/1756234695418.js
create mode 100644 backend/src/db/migrations/1756234721487.js
create mode 100644 backend/src/db/migrations/1756234750821.js
create mode 100644 backend/src/db/migrations/1756234775299.js
create mode 100644 backend/src/db/migrations/1756234799569.js
create mode 100644 backend/src/db/migrations/1756234822685.js
create mode 100644 backend/src/db/migrations/1756234849724.js
create mode 100644 backend/src/db/models/conversationparticipants.js
create mode 100644 backend/src/db/models/conversations.js
create mode 100644 backend/src/db/models/locations.js
create mode 100644 backend/src/db/models/messages.js
create mode 100644 backend/src/db/models/photos.js
create mode 100644 backend/src/db/models/products.js
create mode 100644 backend/src/db/models/reviews.js
create mode 100644 backend/src/db/seeders/20250826182953.js
create mode 100644 backend/src/db/seeders/20250826183154.js
create mode 100644 backend/src/db/seeders/20250826183241.js
create mode 100644 backend/src/db/seeders/20250826183442.js
create mode 100644 backend/src/db/seeders/20250826183740.js
create mode 100644 backend/src/db/seeders/20250826184717.js
create mode 100644 backend/src/db/seeders/20250826185515.js
create mode 100644 backend/src/db/seeders/20250826185749.js
create mode 100644 backend/src/db/seeders/20250826185841.js
create mode 100644 backend/src/db/seeders/20250826185959.js
create mode 100644 backend/src/routes/conversationparticipants.js
create mode 100644 backend/src/routes/conversations.js
create mode 100644 backend/src/routes/locations.js
create mode 100644 backend/src/routes/messages.js
create mode 100644 backend/src/routes/photos.js
create mode 100644 backend/src/routes/products.js
create mode 100644 backend/src/routes/reviews.js
create mode 100644 backend/src/services/conversationparticipants.js
create mode 100644 backend/src/services/conversations.js
create mode 100644 backend/src/services/locations.js
create mode 100644 backend/src/services/messages.js
create mode 100644 backend/src/services/photos.js
create mode 100644 backend/src/services/products.js
create mode 100644 backend/src/services/reviews.js
create mode 100644 frontend/src/components/Conversationparticipants/CardConversationparticipants.tsx
create mode 100644 frontend/src/components/Conversationparticipants/ListConversationparticipants.tsx
create mode 100644 frontend/src/components/Conversationparticipants/TableConversationparticipants.tsx
create mode 100644 frontend/src/components/Conversationparticipants/configureConversationparticipantsCols.tsx
create mode 100644 frontend/src/components/Conversations/CardConversations.tsx
create mode 100644 frontend/src/components/Conversations/ListConversations.tsx
create mode 100644 frontend/src/components/Conversations/TableConversations.tsx
create mode 100644 frontend/src/components/Conversations/configureConversationsCols.tsx
create mode 100644 frontend/src/components/Locations/CardLocations.tsx
create mode 100644 frontend/src/components/Locations/ListLocations.tsx
create mode 100644 frontend/src/components/Locations/TableLocations.tsx
create mode 100644 frontend/src/components/Locations/configureLocationsCols.tsx
create mode 100644 frontend/src/components/Messages/CardMessages.tsx
create mode 100644 frontend/src/components/Messages/ListMessages.tsx
create mode 100644 frontend/src/components/Messages/TableMessages.tsx
create mode 100644 frontend/src/components/Messages/configureMessagesCols.tsx
create mode 100644 frontend/src/components/Photos/CardPhotos.tsx
create mode 100644 frontend/src/components/Photos/ListPhotos.tsx
create mode 100644 frontend/src/components/Photos/TablePhotos.tsx
create mode 100644 frontend/src/components/Photos/configurePhotosCols.tsx
create mode 100644 frontend/src/components/Products/CardProducts.tsx
create mode 100644 frontend/src/components/Products/ListProducts.tsx
create mode 100644 frontend/src/components/Products/TableProducts.tsx
create mode 100644 frontend/src/components/Products/configureProductsCols.tsx
create mode 100644 frontend/src/components/Reviews/CardReviews.tsx
create mode 100644 frontend/src/components/Reviews/ListReviews.tsx
create mode 100644 frontend/src/components/Reviews/TableReviews.tsx
create mode 100644 frontend/src/components/Reviews/configureReviewsCols.tsx
create mode 100644 frontend/src/pages/conversationparticipants/[conversationparticipantsId].tsx
create mode 100644 frontend/src/pages/conversationparticipants/conversationparticipants-edit.tsx
create mode 100644 frontend/src/pages/conversationparticipants/conversationparticipants-list.tsx
create mode 100644 frontend/src/pages/conversationparticipants/conversationparticipants-new.tsx
create mode 100644 frontend/src/pages/conversationparticipants/conversationparticipants-table.tsx
create mode 100644 frontend/src/pages/conversationparticipants/conversationparticipants-view.tsx
create mode 100644 frontend/src/pages/conversations/[conversationsId].tsx
create mode 100644 frontend/src/pages/conversations/conversations-edit.tsx
create mode 100644 frontend/src/pages/conversations/conversations-list.tsx
create mode 100644 frontend/src/pages/conversations/conversations-new.tsx
create mode 100644 frontend/src/pages/conversations/conversations-table.tsx
create mode 100644 frontend/src/pages/conversations/conversations-view.tsx
create mode 100644 frontend/src/pages/locations/[locationsId].tsx
create mode 100644 frontend/src/pages/locations/locations-edit.tsx
create mode 100644 frontend/src/pages/locations/locations-list.tsx
create mode 100644 frontend/src/pages/locations/locations-new.tsx
create mode 100644 frontend/src/pages/locations/locations-table.tsx
create mode 100644 frontend/src/pages/locations/locations-view.tsx
create mode 100644 frontend/src/pages/messages/[messagesId].tsx
create mode 100644 frontend/src/pages/messages/messages-edit.tsx
create mode 100644 frontend/src/pages/messages/messages-list.tsx
create mode 100644 frontend/src/pages/messages/messages-new.tsx
create mode 100644 frontend/src/pages/messages/messages-table.tsx
create mode 100644 frontend/src/pages/messages/messages-view.tsx
create mode 100644 frontend/src/pages/photos/[photosId].tsx
create mode 100644 frontend/src/pages/photos/photos-edit.tsx
create mode 100644 frontend/src/pages/photos/photos-list.tsx
create mode 100644 frontend/src/pages/photos/photos-new.tsx
create mode 100644 frontend/src/pages/photos/photos-table.tsx
create mode 100644 frontend/src/pages/photos/photos-view.tsx
create mode 100644 frontend/src/pages/products/[productsId].tsx
create mode 100644 frontend/src/pages/products/products-edit.tsx
create mode 100644 frontend/src/pages/products/products-list.tsx
create mode 100644 frontend/src/pages/products/products-new.tsx
create mode 100644 frontend/src/pages/products/products-table.tsx
create mode 100644 frontend/src/pages/products/products-view.tsx
create mode 100644 frontend/src/pages/reviews/[reviewsId].tsx
create mode 100644 frontend/src/pages/reviews/reviews-edit.tsx
create mode 100644 frontend/src/pages/reviews/reviews-list.tsx
create mode 100644 frontend/src/pages/reviews/reviews-new.tsx
create mode 100644 frontend/src/pages/reviews/reviews-table.tsx
create mode 100644 frontend/src/pages/reviews/reviews-view.tsx
create mode 100644 frontend/src/stores/conversationparticipants/conversationparticipantsSlice.ts
create mode 100644 frontend/src/stores/conversations/conversationsSlice.ts
create mode 100644 frontend/src/stores/locations/locationsSlice.ts
create mode 100644 frontend/src/stores/messages/messagesSlice.ts
create mode 100644 frontend/src/stores/photos/photosSlice.ts
create mode 100644 frontend/src/stores/products/productsSlice.ts
create mode 100644 frontend/src/stores/reviews/reviewsSlice.ts
diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json
index 9b58d57..c88a816 100644
--- a/app-shell/src/_schema.json
+++ b/app-shell/src/_schema.json
@@ -2,5 +2,6 @@
"Initial version": "{\"iv\":\"ZtPqmPO7LWw2Twsd\",\"encryptedData\":\"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\"}",
"Initial": "{\"iv\":\"hjlaCxCrMgn6YyLE\",\"encryptedData\":\"T6kxkgVCYvBuvzYbUbsWefz3fDuIhE4e43AgkEvwPkeXQqCyxkP0WYFoslQlRFmPAZbxwTSZXES+ClgOt7iBnn6hZnDfzNnMksnQjCdGrrJsJBJz2Zu9lFlCdRJsPi+YZMcF4pumpb/RZGJ4Gkej+/nBpfd93PVXTjTXU6WFiqYnCsNUKfAABW/mSUm50+ew5rdo2COdo5hfsMqVDZy5pMFwF0jc4c3EbSdg5stDso+bHeST4yO1xchgfkqTC3neuIUDpCD/lrw/Z7NEhg1odh52Wv404Za/3pYDERRC6wI4GFn5mrPt+I8PTJxn28dlV90EZmm6KajMqThFt1m7kf3MMDKn+GIdsftBmFx6bsHV8UtwYQluJQSpf8PaJIXx6OZ6WhyCMZeNV3fJUynHjmc09RwYYiw8rcUw7b1pGeaXs1tl+dav6zH8as0B66rV6wMvdKLqt3XVci9Bevfd6MFYLHyctecsUDroO8EXRyyUOWQEU2A24eOf6OTn2RMv6vzjENd+bQJPqxZEJQfaoFiduwN3HglPJr+9rZdlhu0oont75Md/kxE+ZeiGjJrIK/xjUsVmqowHD5Z/jSOIv93nglNqBh5/oDaHTN39EK+9oedpQ3+QhnKSYYOxa79vQwCe/RT2euzG4mAIAIen5Gua58BQd6AFMbF9jr4P09Iu1esu/qK/IXSprMi1MFa4sRTmMtPHP2pkh5Pzq5X4D+G3N/v19O3Bh/BrEEYxXFEXAzjrIpa+gTWgInKa5HJqgW52ji4eXnMfSteV7PJ7lVnJKs+Kf5JL1W+a0AJGfOZ3/1T6DK+sa6xqAyez004RT/30ALO6+/DW1cPvV5+Vj61Eqwtd3ErgzoPZUPr8fon8JseO0ASTXBqU2rFxoXmDGs0QLywYvPGqhpZfkg5dRg0A4wR4DIzDy1aY2PG7rSZ587THdy7HkqoxGrHbbay0biVVDxtqLz/MTIjXBi6zT9Ic2lAlLJNxKNVBNkFNlzjeE0IMB5Tsqc88yzGnI3oIYnSnQxMhvtLVEsazx1ys9+/tGAV48xN7q+epA+vliPaaoIMxxdlvMh+2FgtJ0fHuTu/A0Uy1sHg/5DNdE1r4L5GaBhgk2gsucQTiVjsq2JCP95O6Y+PRYcQnt99hGyrVgk223xAoiAyS0sHMWfj+AI4InGjoA+CXT3lmkLJkGiRzUTT90gaWm8ai/GklruHPSYhHOQXfFvE1cmeCm32ZIDon9LJenzWGx71IEcM96iFph9O+Jk5aJxotRxi8DSdpnYACUomdaJ0Rq+7AhvAXBZpXcwQUDgkLtmqxl6+FkislKRfTnJk7eyAb3Pvv4/uCUnkBV4X9cA7ee10UaknB3woKicWZm2aU7ngW+0mhcP5fgY9zqr8Sqj3qsnVRvzNZJB7ZDcC7V+6kB725oiJFyjHg8wip9qpQY5tYNebuyzO2wDk4FmoUS9FFQR/RPwtlLBECJv+Yb5xpkxRVOY6jOFhO0m52A5vl2T4taXhEfqNBTH77wdFZIg5zkeE80j8yjFqG5tLjgY+dXtgR7wA1gCfALq/3gz7Wj3OG0pJnuayPKNtPWT7TVkW7cWvH/nqU5CPGPP/rHpo46tibBoWi9j7ZpCrP8jprqLI9CnZ3JhQr3aADhtP1aQwLJqmGLCcwitLo9/Tx8fO3RMk6teLvjeIYpjd06fbUkFMLVxHud604dw00FHrSA6U6xqvtHPWMSmZqa2IDFDXw12CYAFnvOny9xjlBGTQfsNJlC22/VokFYKKpK5xZXydiezG4/xxpzr2O9oasZbSEuk+sVJ+VKXh0WxRSoeUB6VhLiO42axnppsNWrxWVVVOIdWxibJRsyiTQyMtX/em0775cOMgUbe95mnMjEyx7vWNb9MK1Nm0opoHqmrBIvMbwP/LRJyGZPIuxrXYLA/FHCyHTfZeW62VGEhey5TMKEbA/e3RYWmv8j+kwUGAlzj9I/FRJYjwcuSRYkRxW2OPXPbn5jN/wCJu14anGU7mNiQmcBHIqX2OWttGB6dlstea5u/jcYyr4Vzxz36Dz02VwAhZiQQmBOIzPRku6iGg0vwm4tIINaIlpnqm9PEw/mLbQ6rJYkvsv/fjhuiUa8Lcmh1LS6utRT1Ke5XL8KnZesWBMaF/lBciaMBb0u1Ss3flXmjScd4w1TfaLP7OLzOAq57oS44vAfhKyMRH9YTLHgUMGT0dXgxjR1e+x+FyppxWcnvKcpKhp9uOLHig6ABPgpolV4xEYBDdij7No+L6gZZp6oNRfJ1MYrc7abjIAAloM5BT++YGov6ETy6cm1ODxOFYu7HMY7eXFIwbHUBNThT/MD8QARomZ2SZkct8gKjVbeh042MVvxuqTKWWhjIY0znMalf0DRxvjp8xLyL+FP7pLnRLNLB8GZX1Jo1ne1aBxi0IhcI0wtY5QIIsmHUJ5kzMXfBIugNIU6V3E4lCJnR3YilhhYDItXDu5wk1iieX9p/QSWT9Qm7259MvkG/ahKmOKgAbG5UK6x5+Dq4QdUw+RnwI8FYvV9iEuypEoE+HdFvZD9BX04Ulj7zpi8Dc5gqcMGRx4PjWxG1wdFEusRWpX6zD0M1NlzwEW4NDAyUW295T6xuDaJE8lLeTYDwsNX0oXdQOcJSL6w+N6V9nvikH+MOEMLoSxTzVp1aybUMtXusCJHJMlbFWdpn4XxR0TN/m3HaMiYkQh8QslTxoJUW8WuA5oGqTLd8SF8rR6Uyygg99l9vHccHe6WiaGDQOoYNKUQ4cltw60yKN+hzEcCcGpXt0gl1Fk3UlBOb68KzJSkPQKn/KqvAmPTBeFuCxsDzx+vRxFiQQ3j089rEUPVOdkkYDaAp4mZfGp72NKkJDGvhDdyY0vTkPoYuTYI6MNlhq2O6LRAn2+S/Wofqbwa05fpw25ZGy6KmmZ7h1a6+s89Gjs3rcftLkWf/yS12npnrYamQHap+EUmsl6tVu6n/ozplfotNeB7JoeXBFJnyAGEbHWmzhdg2uTL9zGiQl7L46GZnpukiGeoJU1oP1SLKpV2Sf9BKomxfbdkGme5Y2ep/2ISGY1BY4yl6DhvIrkwikD23l1pZ3fFOgGhHh4leTH6mv9krEOOn4EjjKsfl1lU7TjE9xOI8KlZbdYh8c8aaLYPW8/J9OytDe8VtVDMt8QFUJHmELDwzuLdHDpyaHLIQFBjRkfC/F7B1GMXPvBTxXC8qYpcjDrufVODaEzm1Md4/UBW/ac9PLS3xTf1sIjL2xqt6vgR5zUX38r5TFrHVOUF8DiMmJQfPjuw1ur6ena9v1XBtdee1+hkmprUp1UJRpCVsOE4wpLQoME8m/m/T37lOyGIR4jDYaUfD4CWZTJH4cGeGk14ntAH659XmKB4PKvtTbZsCZhjf+GAi6fD5fAWsI82/ejgnS53Y5GxAeJH5xNUOztUwaKL0pIgobWBDEJBjqfFm1TrgtY+0whssxNZThygwFBepH4/Lh9b0080AaDXAZ0D8Lq0ZsSphCwO07vrDVpVNRvNlomfvwYfRWs5S2A922LQNoIRhO0g/Z6kvk+Ze/2vP+ACPlKm4Iprx56WsRCH3AbuMNX6V+uOXktnoJYw7yVzuWK9dwWGtHR2vM8DPpmd9YqdwKPQhVosp9/0cpMbLLojs2XV/MqcOwRIlBuuSNUctQP66Xw3NPjpVJElXtMYaCOyotpDEQ8TYfG0sTqpnW/N5a5gdjxefN17cJoM56ojn230ZNRYEn88EdC8N8v7Hm0qokm9cII1f+QPX4DCs0b7sJkXq+/4jVLD2B6ZkF5aAFBwrgFqffAJFH90Kk3I2XR/QI7f1/ONbaoET3Qmt5LyqfERYLir2iez7Z+54w0cLAI8rQl1WCDNdF6mNgcMgKAhf4N1R9Qap5ptaYRCEu4DD5ZKlpZzHg5sWKwstFiV5u6Ry3d1tCs8WY4RkwKVI3wkiK032SVegxFnkWICNZqk1UpiepiXu7rjb75BcHlF8hNnUphJ8rAvEOjvY1OP1h0kXhF0eMyjf+n1HI6tJWFh4DdVljlz3+J9gP9WHPJtM6NrEcdWbkg3J2p8TvF0aUFI8YQGCtsQHuZM7wHeK7fjOZkazYzGuV6G3hOdnbAJH59VuuMklAa5IVLgfCWJ3p0n2aO72os+axUV+xf4mTL/W97Y7ltPIQCdVggla/Vad7sAul92DIDkEEuqIKCePSAWCmWObZijplm0daPVFdqB/TRF9qzMWpPhpxS+pGt29laYmOWMzUE+oIi5i3fY9xnMLr5HKcC8ShkALfyNCxO9mhVY1R2aqOV5tNmKagC/szp/vpkSapHoaDaVBSu0JSWRxpJWVR/NJnJwA2Mi4hTy3LUr0Y2nLz3MwVkMN8z2u5zuSsGePZ/pumGK6OXOpPFQOt9u4kI5eYMDjiNTpVeHXcFTOcVULk2m2UD/4GXB09JoUcgbC1n1ZcrXwczOnqQ3IPIeLE+C0X73ymbFiKQppVoDqvWcnYPWOCYAupsF5wsDpRY159+PXvDGhjBI2IM6bu84fqpMhfbLdRnJvwNcNZDc3CJN1qV//RGJHJUbR0oM+zcjT/nVOOtaINXSCJXQDran1Urh9ooY2BFwvV7H6c70jZuSUhfsp1MTq2ryCM1sIEZmv7QJDefxIUKRnRG6RrstkgajCojRO92iu+PZolzlJVQj9QNWqQ+Bl33g22PqycztuG1AuAi9alGty5H6HKNgSLwQm5jnu/ZLZrE02x3M8eVQeAdHqtQI3aa+anjEGa3TMPgerWSbck5a164rJa0d3/j/Ihjf8s5Qg20KkA5kR2RML1gJFntnkF1OR/N+0gCY9sKJ9ziGpnImu4KnubU9dxzGI6ZRXZfMGilXJPi2KYId5JL7l4ECmurFP9ngPyn2mj7MZ4fStFPa9nIOwT3L6HyB3Jj9Gkz/uIS5Powb0PdScHyGmNYA9mZ8Zk9mE2D4P1xDZXQZkYTpWApH5HqH0PsKRzXbs7j31H/dHf956TbGey703bHhMc4XAbH6hkKb+2uLcWBUNKCm4qaqE7lBWGwJLMYiD8C7i+eStl7xaImmMmJwH/Qm8lQ+nD1jlYWwcCBXL0bYLqYUGv5Ki6AMeDUJXNsODoLewA/MZ6IcvROzfe9ahM5ePqA33KzjYO0+5N2lZV5u1/qDvtVWWdnIm9DZc0zx+F/T28FxXUTkB4L6iLpZce8fscKvHoRrLaOpJStLD2V/g5eHlb/USVprwTJG/ccWqCuN7GSEUeYw1b9eNXEsiC0PeDZPuOpxbgqVztejXL0t2HJMiHUDozylpFMO8w+wjOoqBu9Y2syyd0oAOVeD53rbulcyI+RVKl7TSTuUKE74LrTzX/FIuApWAkdVD4CShw5Y2q+HxHtAgO5LS3ApbrWGTavuBkVZmGsKnLctVLj/t/KyX7cQPbjMEmdGOwGmhYpBJV2c5Sf5PmrMoBNnX8O5Y4q2Y7imG8Kn470zq/lihtOCBXKh6uoMRW87LMNUYlaQfezTytGOcPiOLSCDeanxxLc8j7pykqW4qRNyJ3tLGvgsvkTZte6Ycbk63p6VLaBsFTskLzBx+qxT8NhKNiXFRsqZ1SYRker4QAWbRzIe3SING6NNRPNuxN8q7lp8Ghm4HpN+WEkazMDc/GZzsLaoWuRfipjzr4+t9eobRFunfsyrqJUvefeFWnTBsmiiTEO8+84NqHQPfSxGAQsW9it+Gou4yrxmQ4b76+anPkQ+EUkE0HHN4VAwDU+X9DKRMEvQDVxDB0oEeZtCGwYNLtKC5yzXDsPQ465e8d8i9f59MHnBadvpDkdvwdQ4WvLkXoYJ17hCso6hgtNCweR2CXCHN0yswzNuFdX5Sfm3DvYnylmuFoIy8Py8QTg/sOLcGgc887SuPia3C5RykACWNoRxrYUQLS/ms6BezjYwrnCEOSqkVzUvr+1ahmjHf+RPyA7uZO9emCocNRcSq+XqW9YRCC19fPWk0lvDauVRuaniUXrccCFvOuMnLMViDOq4xiQBwz19f5CnLfDaPqyc635CjVz0VVx3KS1m2ZYuJS0KKK+nrVewNfok4qZsYxOhQotliVRrIZjwCuBe4NAH1rtJ2nXz4JHt/Bx7d+oCMG7cKBBpmSVwVzoZsv0B/fL2CfKGCfyZvvc2r16omDZX5mb1JVn9sBehTxJ9lI0lB5wFJ568N3clO4Ak7vsaSaOk9AnZ9NZCrP99MCOHgRaVfebSD3efBLjMQOTx5NPR6JqWKs718mBmoQW6QShu+YKxW467m6ZlisE6+aR6i4I02vWEWwenbcxb6CFuWvTGTFTDz6Ey2YPg9JKhpoezKMPj7GLfOcjAzaJ4zgYEIpqKbiFPR01UHhuMwpUuybANv3WfBLIDII62k+INJUsqCAWOt2e1QSJ2eKsfMEBv3TJjim0U3drFoeYEBF89JdrecI76QKpit5v61tf7eL1/8i9hrvtnaHn49Oz9jMDrBKgh/X7NgvPOh8DaHCBdV+9dBx8CGhah2YyUJNCheRCHsoaFpiMzk9Oei1Jzki5XJxUR0k/sVJFlbzU2CiBv7ljFun8jnEFrZVQwArdK7c73Lp6SrmTiTh/dcTtnrvOShp4zBnyHWUS1RCGOQvvZO5wzshcCiKiXSeGmwzfoNXBDlj0MvbMGtGp+DPClQKxJTOK1jVL4Vlw+Nm+DCIW+fV4EALUGKu4+UEXjNSYO0r/rJt12q4jYgxB3NDM4nNbYTAV0AwGGLPGJPSbEOHWU/qpjUYemujeNethD6xw2xvJdd3etLaM5felA2YuPj3g6vojlTC8caOhNOVgEfh7btHR1S0YjLOBRt/USLvWtKNK7irhC45D1XtrsKwTNhXp3EXK0vG+fJVH6nTvj/iXqk36ysdtkD9QyGfCFcYAp7kr5TbOFoghJnK6DkcmhXsSISZGSfu7jhtctF8PZllJ0VCUGcKpQSKSOkBW2j46SOTYaouYLuZSqHjRjUxlbJMupac7lXG239cEpIdRsmrxvUgQp3tr7gqpJ48SpgsXssF5WlhtJeWzN4/PKjGpflLjJ1+mJTfRs9TjkzFnk+/Y4y1+wpIGkUQTLlczhuiIzsGUdlTstRKRHZxLQB8M1YMj4yR9HYATeYTqEF3khJTD6Xa53wB0KXvfGn5rvdUlJUReWru1jXylbHvofVW2yGjQJhnJzdXjZZf8T7MQEx8Pm/+zbR6rgdKoZ7XYVOJZbKIfAX49+VS/e4h0hXFDquyjsoZKWq+S88CDJpLzO0+caQxgZ5xFUqyNkNQmr96FJcUfeOJSyEHo+5zqaSKuQ/nIUyJPraP+hS+O0KyFF7mcnZa6nP1akdhGYamquOfHuPbvD+xsWeuaKcXJMZFb/Sa9dHGm3YwGEh3K7XUzN3rFQXLydb5Ay4RgM6HxIUECwoReLh6HneeuW/G1HfUpVbaKZHFcgyBpSIzzZcNIeJYD/TYQWIW3oZawE0OXPhH8ngo05I9QBej1bMM0d4LqMNow/rLqb/OokhNgMEAAsXRnICuEB8HlMYTbEQuEZox8rBC5xj73us85xoYuet/kMqzoGQ10HoXzt5YsvsTySmZlai42OoxrS3MwpRWiz1k8TmDhYT6ByRVL5N1wHeSnGTpkFa3bo6i5vcx2f7iWp4ZOiqpooZHsZ+6fx7WU/HX1dEDJmP7uO7gW+IwFvzDybZ4itLdW7JTcNBk0S0UPIL0UJZe03YsMvAf1V0/0FPfrCnHYBrav25UE4uC0O7RAcxDhFfNaAtOA+BWykvEo805/eeYBu+GxoXeex3RYCsSwPTT2cVN6yYrRTGmH5Wo3QhX+nD3XCTaT5GqbDKy3oooXR2fg/ew9F7dNyldS1JnZsvWoCwp6GJV3H29jFcFsKBn4tER26y2pMsvLLF3A/LGq7D6VnobBNAVJ7k1UfX5PHjfouunJ+yVtZSFI+qX3rusAl6WkJIsYOgmAO3qz0YPM+pXtjP8XyNrS1vHjSeCsUAc10diuzrfG6SpN6izC7epeQ8KNfxkv6D8fBjK3fmj421/GGJCmtvFc5BrNuXf18NYmNaYTebCJmSCzrP37AxxAYAU5MVAJhlLSv34kR0/vwPyRSZrH/5FYl8v4qTD07eNQ6gsXFpxEwqn4h8Jdk4cvXsUU59Dvj13grW42nbVOY6Pe5fsrkF1ctmKhhfW6jo7zwLGmlkiRksrt1ZE38PuiKbPJQOYtaD15rnjVsQYL8DyoLHOAe/Yh+QSzaG39h2yKLUCX4KpSxvTL9hsmuB4wZGQ7i2ZuGG9nHKX1ug0xFFEy7eArp7CaIyVRKAIby/uAlIDrmnxTHsBqPadG1WsNXoJc6j5IfztOnR02raZQiDoXmuM9w/+QBNZYTXsjmJ7PF3KjagNcStV0U/V4BLvdYnV5W1iMvtjwYbcCA5YhocaIOBiZ6qGDuhMvZetGDKvvehwcJlSHUlRzJE14KcMqyUr2ik1w8nLOO+Ni+YcYfxV23xmLg8sLAs55H85mqvZDhMTsIGQrslxzFsS2SgPJxAnCOkx5TFxmo+883Y7Vg40c4uTb3q6lS8AA/r0GHU00rEb6cTvMWY2A4HI9oEVwzHI0TDE0DxpnxPjpxooMeHAP4rFPsUi3aLKYfH2dcbpb9JvL4qdYGQisROGHdJlv8kQWfbumUjhCuz9ic9yU9mu5M3V7RYlX9athFkn1OVcfX6CFmV51hSsw3/QdiqJybKFc0KbeRMYAHyLpPNK430l06aZD9WxxPnMqYxuEHesywsgtl5HyeszJSnveHNbwb1o8hHqnA2CQxWM3uEt5auu6S63rweqtaMBjDk3pwyeszIogbHLWlIzuaYhJf++sYVFBTed01cf/x7O4ZRKNsZdZPILb47H4EqWyooBZPRWicpeXyONZCMn2+X7gUJBUcY9yix8jhzHtxbSKPp1O5bTaJx7elwWtcMW+h8MTQJlYzcaKKUrYKe/n5EOC70anGa1VBguMLroo1pH1YbluD81zKh6NO8XgRrTxoNV3j1kibnYxxZ4Luv+y/SC+QJcClA/HQKufVxJ7dzJcXLzg9ZPaIUVReoGzqsgkBXMBjW7/l9x9XjzMOTmBCbM3hBNDy5Y9gp2/KUhcLLbxZmEvU8Dgu3432tBNE1ELgBiaSZcJmAz9k8xdRVYS/GtjCiwLVnb7hdHN4zsJcGBmtMMo9BFaxcVToH7tClAatilJ2HNR7A1bHqgp0l6qKJP6++JsvY0wwwzsOW3PoHJ5N+xfntWNMDSl9OBYTDo5x5jasunp8vcjBJ7vC5RY7ey3Z2hNyHBZnaFFoviWeLj1yEV0/t0qYcANc/ZSbPL2KxBFL3Fhv2wV+oefDbCzZMYs3u7FIMuyhCifk7+KJcmHQq5DrLTbFGJGMJsiom8KaFcBhy/z3NeN4RWh8h66XNq3AALOiAiDoID5GtrgAXmTWozZv5O9tpxSzaWpeQzvLTu1nI5Ie9nHJD6jHPm26h+mvGcFC2tLYJP6/2r1NOn4nyYDKD6kYwrpAhh7i6z/bKugEguWqNtylFBtteEja4CnVCPbFJiJAGaVZADnG3tFe5cfySWfZpjKNlNCszD/3P6DpQ+OqFXG/y50u2Mu28gEGKZ15eaW9Pk8MR1chzSp92J/xtWXELcUTern80JGzykT+dNS/s3WJbRUneZwKzFpQ0WFyrMwpJ9NtkgoXD1Ces9Y91UP2WCGV7Quy8LYNGfDvL/21xk92jMLm3kWdWhEDw2rWOwCbkXmxO3KXfX1B6lFLCL/4gHVcz+J3FNmOcoC72igXUwHsUw05OXEEQS0xVEzMS3a258K3/hc5zfS8oM8pBU4ILkkRSjWCHFPgFLDSrbeD/DPeaRmb9ZtP3sdQwTceGpgJa2JvTlnZ1viEuMzLTCyxOFtxLHIDl1F58U+qWMpy/GoeNXiS8kVce9pNk1eajo/H0gryM/8Q4/Cb6NLbYOY0IbbPIH4SDbfwiQV/LMcDc6lXYymr8XQFt89UCIsumBV6/CH0OX8epN9QTYcsnNuBxBEUblmUlul8WAP3N/S8oKqmsESEL9DbCjbDkZQ6tzVMZUyreZjjUcGADtDrfLdu1xKTvvTiIzWMuBmpxGIwYBy4Ix/OKHG7/2K1oANhxPXrft+NMhE4RPRzITyE03oCv1bU/PLijsrErkDj6Hkovo+yT2XF9k1ZgJCDF9RFcMIGZZztKTpDNmluOk12dTuaO7GCbv7Oqhp+PWY7w2HZWsbwCV4QzVGXnqwLaKZ9D762NnB8dSRou9lkRTvzYer69SRXRvByjPFwjlv3oqVG4Okwjk41Xy+PDv+jv6GniD4ORva8SB8fL08arQIKIkNJhoUsE8VJPjH3hHq64SPNLFIlsuFbCchmig43QMnBXtqKR/F4FTnH5/FafV0/i3OrzEV04sWKg2Sy9QkSEbcMnDmoXjRpv5edaSHWhYah+5PasahuC0HTuOcsCkfVQ1nodWBNB2OadfZDJxbERs+pKf2E+7qjcMUWqf7WO98iWXvu4T/uQHCEr/1792LYkt7rg5/5pi13RAvpxv6ZDTZTjD/AoOXNTKtgpLueBxDj/hKzze2DWcajL0wSbNV8v7LsDcGL9iB0etTNqRqsA7ZUF5PxZbIhTq6japf5uqixvjg+in7evaSOjYffOHI9uBqNkVDCJ2cL1KPaDBT16CkrB20EUzYuZ9CgmSa/O3Gz3xOU/9fCpzm6JE3qaqNNULpol/3dItDVQWRO7R1yOzIhBhWVz86aprdSYUCLExJgw25kh4p7JtMqk64ka2BK5AyoHO24YYLXPSW7MfVa5j3jLlLXSDJ2/WsPDS63iOmYveXxxZWyGHjs/HptWGoXqYpf9B35u8PfYArY0igfZgCLruf8lNv7kWPx5JDW+j6mMjKNhAxHyUrLxheR4Y7N80AJcgSEwl4b7KKVpJmk2uc8CXv99iCVCjqp4VtyACuMeido2Vye2kAoP6hblbttC8JkWzPm7OkpZ+LKOA+64aB+9u5PSSpDj+nXwRi89P0EQkKf0sUiuCzaze8NVbqYZGiF74/BRGYL848vPIEWt7rScZz2Boj6h0f7KZM+I32pdjXxGeXCsSHb+h0AbNWN9Koc4xCAnqgfAEL5v/LlAyAvxZp1Md74HWEJNa303Hx7bZPujNo+cd1T0hX3d0d4/JyM17yXKQ1mDm6PjhPg//8CzG9uiukP4NS2PppaXNnjthUVxeuzzmSRWmXjcyIe6hJ3+WQ2F7/K1ZHIj5a+BuP95DcbumFyHo8C+rB2ZTdoAxvx+bHqE7B6wm4KlLY0o49VOdq56G4ACrjoF2RWfEdKtjPvDuH7uRqprs7/HEs+A51Z30mAbf+DG1NyINSD0AiyQ8/uTXsHrtJPG+RtDwjI1WoJqNkbChcACGBiUuIBZOxC1UizKBaPyJMjSMJzBbK7y+nTsGNO9H8U8YhcTodiln6y/DAImk+mbS38yVsm8yWlrV+ix0vPhwYKUpJmaBOO05xHEg1NjFe/HOuZeg3+qKJDqqM+OMltVQ8n6GLDrd1fta+R9Zot4VT8KKQYD1DeSImsLjxTW5VrvGxj3TU6uQ+GVfJhiopPT6PyelBqzH/kVl0+kSh2j9l0rgC/u7VaV7Tm6OpxwtW+iuJfcXALmlUsTjlfxgPqpCmVj2lKOuW2w4oUjP8ZA8KMreHACtei5llY5nTbgp8i7OR/1lXhQuJNWSZ5BHzwPlTDj820NRySW2wrxLOTE1TYw6/ZEqGC2Ci3Kx5k6HtVrp5GK3n0RbKDWoLxQF0a6kxw/+lOW464KFRhuBbLMYaxbJ5Iu/VxoHHNuquODZlc3UDyykNwCReCfvBfa7EozkyvR4adPd/ihjN/zUU5biNEu7kQRDyHf7LdmDK65+b3teif+B031nOW1it87XpSdcvgEgGrdG7BP96tHKBn0lQ0XkpVoiA6+DyzVeLpHCg09aR+5t85po3L9JFQCs9ojEVskFlibyDseJsSQxCn7/GZ4Z4JR6q9G9/v1EogKbGQGwE2BvlLgyNA0rrtlKepTEti8vnxjPnM/rcG0n8uGA2dy8rrFUZlIiZb+WSNAgbTDOd/TOegFDRbT8f+lR522dZNqQphKFeoa/4A5YVEPwM3g+231sqNmEhphx1Ajwiiw20seIRVRX4nIizjpbVmT28Jq0UMWAQuX1hxZFbDOa2HED9tugrBXRcR326TJbTs+Ou8iJXDgP/O+1jZgY+0yhhijCu2ABYesq+pQkaSMm0G2gf4pMy+FxHOixBS+8i6rTRRptQOc0ezGA+TkSZMvXxyDZR0LRuhUQH9/ktTeGY0LBLSuISRx15jCH4QM4/08Jdw2Ct5ClvTc7UcdGrbW/GjFXojvLHnf0Ypg8qBMm6QlOiwJLXa5phDy3AUVUH6u2frqMLsg31qZ9FscHRivR32PJx1XNL9MUXcCIaUY5pJM7D3+2HfdYvqFuhWB55GlWbA/KDYmav/FO1bikaiIA5fCkZ8SpzuvzH/jTK3V8v8bz9MzrmNGe12gLU92UAtGKs/gl8eWz+Cf2hMjRXbljU8vnq9EnAlGNZkn6QyEP3j1j0baYtct08yRn1/ZwPwnfk3YQKIVuq3/l+xEWEp1hqBu885DlivuDlgjv+n5kdraaoz0Xt7IJ9CgAevX8t8oi57yOlGnDjFvoW+Vdpwo6yL/0mJNNyyaYZjbikfIJrxs8brjwgSy+p9VaV7g2CXKOkwxrERdIVPIkSdEGg6Ok1qUtzyxwrLTWaMksezGNuiPKFW89Iw0/fSc0No7E20VCfcR7iuxc3bUOjmmGgfiavTjFEMxyRxsBVXxw2mqdAlBK/4oEniGsCFxUsuJ2Q71BYbylLgkDHT17YpxZdlex6yZU4JhRlmgMv32f/013CDbeSyabjnCYc8CDLI2/j17zJ9nGb7Ib/Ff5k1q+CVgFyad+MjKy6iY68pjooLMYvyU1Bycn3pL8g8Ql6xe7DW6RIsURtK4+hjyMSljqdwxA2/h9VUGubIyJASdq7WI+iOBjnbc6bpakJn5I0QJvI1kX1ussHyGdy1Il2tUKhBoohHI/ARFHbk7mAfKKsj1iXZ/dtccNoXcy/Bs4yneRFWlhvPHQInNQcshpXtSG0sD4ddkOQVEI+nk6KxzKQ7IM8vUzwREP/oBgM1v7CGl4qeqGiqkgVyB4CRjYlzquXahtrPMmbSPiXOsYDBKpheUb4ZmcUm0Xz2fbm5FlDke8FhYjYEpj2bU8mruEN3/crN1e4KZykz/BTAmsTP73rb1PSGAxOZmcAHXK8xrA7Li2KzzcFMkkYKmRQ0J5b6ZZ2Kuh6Gl7Jxk+QKNO6512x9VuMkJ+QHo1KSPFYUXwrtkI2DBsXhyhVA4nTV80DI2tc8ijN7DrunXYSrNkejFUitBMY2jJHbyYRPq6EqOTbcMp+7K8m3zO3DaahqqpQVH/Y5FdsaEMM/tlrDD6/bdUEU1yCTULP66G2vaL3iR0ntNOmzmy4fzbXt4Z4zuE6UbjclcfiE3ql4hJPZwv6ovCA0FwlhepLi/RpglQoNkj42y6QgZHIMLfZVlmp0A5/Dq9NhZwCF5eJUo2tj1thUQMifji2GWI4ktVzKVtaJxhCTR1cpOEX+894orx70K9+M06z0RlUUYkHf5GtqQmdPjZ3gwF5ks5TRdAg3/ngKWQxXBKBbhnhqTQKKJe4HKqS6xFuaZLSrqNDHhyC6ZWhnMWEocpjUC1e6NXScPwO+W6nTuxi+FSrHyG+XIcxgsXFm9c9AGRfUvjb5n3tVLtxG7o4CTyQgRAR84rBGyCp2jekyYpvvoXJVqPpD+ZZPw2LLXrtUw1Bf3YjlhwnNHCS3N1IiZ5vdefBAY1CuFtsMmod/H3TbByJoCaORONbdCRmAhTRMFGBmocQ5JrJjdeSEDaQZaUbWcLYXydQ+2eo4IlKsMpi2MdOLe1vSsWQzaXygomfraql6ouYJKPgIRtjyOfv+D5agxgzlc5eGvFURX7ieDSH7Vo/K0D5j4FINaR2d4SighlYQYQSlcHpL4Mz82mtEvY7iI4AMCaSy8Zxz0wJhFhQQ3ilk9DRil6hQeZyP4/O36QQp10Hmk0sZDVqhyRswVI6g7hCgQ+RAfBCPZB+oxrOUGG7no8ELYgO1IvFT3ARphPzVcUn1HQxVuQWHjIDBRKBKANPtGRXcJvS0+1io1ZOCbbfNUDbR8zeuoUlBP2KXCKt28BRKP6Gu4KYj3N1Le3/kJC+CsBhziovzGo+yXTp8XXYQVAEDcOCPHtwqPKgYS5mKzk/irf4tNHUzUIkY2GsXaceb3e1MmHx4ZOLgskTWP8G0A86wKjEUFVXviO9n95Fef+cKiU5eSTT4vdiRNPPmzcZx2JdUwwCAD6wsquVYKpXznG4fsc1heycKLviTu9LwPaR+nw/HunCnuk0LuQY3mcqdmaE/7rjyxEa/4mF5BmN91Nb71mXR9fDSpjpqQr6haHmXyVZ1g1t1SJg2JnUwq94lVtHR6ve6piLzN/2/viMtpS0UHN2dDxry5MYiMGttwElzqMNRb4IxtEUu8iEm9mfnlK5d6zQmiKJk56qNM4SScDZLXBHvJDP0z0LEu0QmzAy7+NsjWy/5cMFV5YbU2B6QWRoCcpmWfjVZsWx98uxakOr4HVwvM7wLDKsV6ToMFm+hnLVpp7CAPgbXNzQMlYF+XGdKifk+rcMx1jxA0GWoHHM2vwGxwpCXjBxsSlx2Y1emNc1wzY141ZgsvNFV6hahVIh3mgelrn9TtITvSMwxdvQc7n/dI9DbgRvbuG3XwZKqV+BaKeKshcTLcltE6orQL1RqQmSYwla17mTB5E9ErWV0RGW2JSXTBxlZt2cgWtIGTrYBEZmrm5nXB24U1nkxn03zkuxNLOR9LWEcYit0h5b3qIn9aY6w/SkWAeXBSQEwbMbehwP0oetBWzzQYKiZUXYP2b89ULT7X7PxM3VsoYyTJb+4BbODd8QqBO6Fn/AprCiXcg/JbMITP/7dQkqufqhiv7EEClszZM/etVtfmjNKZYLaguccAF58TIdisiWAdMgic6TtbhyJkD/0wZsBNLMaX9OLGowjXHNsIysqy26ial2i7IqJ+FH0bh47WecBF09eV75LKBTSJFcJLlBxKxuNcTMBv5m1fpsXWQ7B8A/UAULlmaj3PZJlwdb/QU2hY3vboU1B5G6Ra4mckk9jDXrhPzw8/dqbTW3xqnq5hDuQIns5CEZcz1iYvjsFzp9AFsSWAG/B2QuVduV21y7t7Kz+Bd4tqukvma2JYz2v4lp+JmDXNAfuousVOl1qDxznmgM0AvwFZ7ejVD7p70Ygi/mGco/C1zWJCBcqyXS4Mt5oPpphsrBQiHzlAGkXtb5WShMAW2NFrS5iydaYZppYtQVDEGOXg4IAq/ooE/Et2v2nb8RzBiDRwYxYm5p2n1Xd7sBEVJDGKnh41ubtFYYQW4OdBbL2Klllvsa2pp4B7HQHYnaW8/BGdzaerLdt2D6Zta90h/AD04ssDtQ6cJeUVMUF4qtBsY2fHDzE5ixPEeO1wcroH+E4/Cjd+8FzP6s8yYk32J77zLliuDlJtZrBl55rERli0ECD7230n74Hr4HBAAXXjQmOiuFti4NUEhOz4ImSL/Xhrq+jW3IcJSC0BzNto0/e0vqzIOfDwKaeNNPPVePC8YLgy6b/bWPIP39gciLWpisD8AeJeJF3mSKjUuiIDo9HWA5dRpzGjywptBaGIahYGv28tQGsPyJBN36Exo5inM/kTeE1ilj8OBnA5k98s5kX6GspSeZ3SlcZYyqTZr2H+kXEUG/Wyu6exqR1CJV8SHFjBBg0Xc3MfPlY/NORwzFhPI4caRYdHUcYqro9b5Uzc3D2fCDOZbgXeiSqeR82OvbQISNMLwQT/eMyINyiqJm+/OInwo1WRLDRz4VX1lW9v+KWVfHAdxfwa6k7TcuSGF1JTIoVPZvFCz9+hSRcte3a5R7Y92TBN6GDZSl4cRsBDRLwg6CiLraPiML4bBsnWXaspHcJAL/mN0R4qUYf9XfBtx5po4CXChi3Q352bvPxrGFzZJLIDNAdc3C/6kHYwctSpuF7HHlB3UguMAvmneY6WC2MONZwOgKqQ3rGRjLwBLbOH7e35LbWR7XJTwCRe0d6VtZNkg22yCvQo3Z1ScBrz7h60D4eTiXul5YfzW3SD5Z505B7eUWU0E2syuzWzv4XQBzareOdX5kh6LC/tx/zjAQzl92UckKK9DhQMyJreT81LeWVGVUfqgyoq4sk+VnmXl6NoWNppYBl3MZYYPbtUgSXVnLzmY1xiJZPHO36bZJ0BRnXGtf3oVGefz+ma3OorazDIU8XBaMEoL79ZVHpsYvnQlX4QhwNk3AUfoC2HaX4RyjcxnAkV1ue6ELO9d9d55yzg1j3KF47DElpUXvjDsXA+sMiyHNUYbv6t3ZTrB09c8NXAiZNVeUUoXF+v58SGrWM06eBqQWxkTPrMJzkHTXSAHK4YEsIv9Bqi948IGJ7VzTIyUXDtvXpZaX2pYbiJ9DuFPwPWbrG0agOTQNPdBbg2Jt7Qb6nzDio7QoQ+W6Vpk/WrZdpG0Ujz7mncnHL5N8nofRJiUfgeD5iIB5ZQL0FWoG90KOp62DAMYdtaC6eAaLe4Ly6L+poZod2GXbM7QB/b21I1FqwqH5Fa0zZ0/LZeT4F96Y62nIyGlBWWgdtdQZb5bq5JU70qFi+EiBm1WfFor9+/emfckBWv6BvLv48pMIvUEwb9B9MYd+rTvEvBEYbLb/PbaNuZOSqcOpAzHdRMWEiY6w9qTrARvA8DKks3luexPAmQcMoGQrc/NjyQadsm4i7oB/HTyDuLXvp00r7Tie6udZD5cuVl05QeTgUMHd4KeaNVmZHmHbnbZ48DroH/VzoqYuGsvxtxfRaxh0tV9niTGRYGFfqiXLTDTyQ1gs1VgrHljWyqZlNne1TIWC6ULXdJFii0oW5dwEYVwGLqVoKmuvCMRkfUvR7f4i4aj3z4wZmfmyZaX8WIr+kK0t4EZ8pJUqwyaqVfXgRao5kcZyNnyJRRrqkp/ETKw25qIENLLg8GtIdTsh6F1i3m/iGSIu/cqWqrwKc4bhGSUaohBSspz/Socd21aelGYHfJlc1SMjuAuYkd50tmKktSJ7fpWJ+BxVCXie3cKeRtGq3mo+RBUaoUF7ffWxYgnS8tqq6NjekS238rTBKmD9+OwyYgRdYO5BiyHuSj62pwsZn6fCyEhBl47tCbrlL/4SRv0D7auR1Yhdy2PR6wU+GO1Yln0U5ICcIObK5xNRjIkfyZclu4kv7A+/fydOYd5Ga1v4CEQIhrmWhwf9zVDgfKABN5hAFuJG067EjUlbLuItStWx/tlPfmrnL/OMkMoFQ8JNNHqGMsRvv5RlKr2J2+8b4bmm+taIweJTTPvsHm6EgeAaKR/xJuDvVjWlQXvTfnTaBDqACH3yFC+n9W9KtjBBvTN0Pynn22nKIkWbkjVZCiibW+J+eoExC1FJbhoDLdZYcUsDU67exiG6q9/DyV/gCsscZBncnJbPzd6Nr/zKHRjGRmlv/vq2+/v/2VB6bFfZ8PKynMz53oMZXPSHixL9D/CRKOZRdqHSpSdjgDAwqKG7OU8FelgTIrDY2XAk1Xu5VWIrH3nRu8RQZLD0zNUU5EO+v4zWM5uu3HSrYyRgMTUrmxGT+Ybj0raPgw/InDiz/8BYQjI+koWvA69F7DDiMWAbtYef7AWmtxZBdEKsFPGMkXQkTXr5sLLLlEne5DxyOG9ccn3X9/YKvvwyoWFO5I4qvyp3VevEK2iloc+IHttJsFVJzrrX8jHlIPSrNH0Ui+Hc2slFpSBYNVj493M+praOn9tqZdVplKf7w8+uQfPOaHsHBqdRJI4qszmiISZoZdQzIeK1qSMvp+si8YW6eBJ4+qDOOD74y+k8jNpQc9RwhGOXUc5rxpngWoWa1AFzlyoOY2btgheNQtpWBqZOU24SVVAke2mNJ/OTLbsJdEXMHM983A4vPDrkEb54eewq/AjNrgM0HO36v2NWLsjpQ0ag42HtwZLIqKgQTymTMzHwV/lKhUN80ybm1ag8PLtcHWOwtX+0QvnZbcWBjpG4nbKupSyDzGVU+rAEPx3WAsMXvRDIMf6AYw26lAKZmJcykzVZmu1pKZn/v6iGJIjHffLXPKbATyK+1xweZLmj259mDmnfsFg9xJR+IGXfEip+Apkshojt2fP/+XhxnSxo++7PVz2aKHTEJAih+ItyoXTqTvErfQcFYjlh/dh+s9kThdx1WbsaF7cgaEjH+9D8Z5nxwLukbfYEADA93S2ZlpjV+Hdee+1RbviUumu408Qdud9uoS4FSgUndYvuFhauPcnuCZOp0GKRBzYIdh5ydRudpA==\"}",
"Added image support": "{\"iv\":\"lJDa9VFj/KwUB999\",\"encryptedData\":\"dyeJ7w0PWYBATEdCpOvLvhDN06aFii6cSwYSMRyf87vNzHsJQBtcuGHut1rwjRwfS98QrC5Q7EVi14VCSQObHfezKxdrARgmaXCr4L88gsxhWwGZxowaYuuUqpL8/mA6JSkb/S4S1XOTKcWToQGNSayYPoKkSVUEzgTSHj+NkYfjKrI7RabnmjJDhQCjV3wgBMjlh900ddAq71/9sdCkIf87VO5jkfqJZg6qJGlfc3YiySlSMmChAT+8LNNUwewK3FzsucWFaz+qPB0jQYP0ru4UfbJXK1lvHN+pxtkDO8WkaTC39oqmvV6e1iqRpdsfF0euoqR1wXjfdslJt7IvKBNek/qe1s2nsISxl5IIs5l3M6wutMYHd2uV1vzhMHSrGM5aevlF1u3bQjRYsBArb2OKmKv2VG6NqSttnx6WvogYs4s1WKnUthb+OU2vMwnWc96rcFth3HkJLtVG2Ut6/c1DAYhMRFUJ+gsH/NDNbESbvGkomk0DTVNqdy2E7zW7QaVOTG404LsHpMklSzvk+E/BCyKMJAPycb96REx4jZlWCBhIJ26sdVJiY917D4QnTvSI0YMgf9PhdXOXHGhWGNWC8SscfvhvuRwh0N5TFCSyoERYA6mn+wS/XR7pNVKVBrHszYJFHRHMngyLwxMNyoD3JzlUp6fBIvs7Smb0RRxfd9bu/0Cq+o8Ic9GknyujpBd0Sgyrq+4XgDxr6WWzoMO2wLDqbr/H2vuDb6qnuLCWuU0NTMO6AOe3upy3wHnUHTlUU+ubRSB9L+WBMqcx/cFiyy52g+AlrVV4bY+npMv6fNKtDaQxQcQNX1Lbonq+4EmKUKL3gRy8Ipteey4UzWpQY0ZKEvkk6732hr8OH4AWLZklMNtnF7/p0DwIj0ebQHeqLKFY6j71Q9Cd7MQnpoQeuGS/bTIKBshNZhyaRMRO8gBZky4W+ECU/NjYLabkT0mbn3pjMyszxbRz2YPAg/uX6VSU4NiisGpZpoWb1/FmZKNuYbDraIYouixCTvDjVqvETaeYtjAFo7XqqEMCWt8yx8gPPuGWC5PAXNb6o15JqVi0/x3prwkEvLadfwzF2AEtpjjwPVT55rPG8QYZ1OJz3x4a0UqRBEZAvlC1f/AWLLIxUey1tGoIMujLNIQhoLWtX7ewbA9snAsG4s1MkHEPIxwIECsPkDhGUvoLSf/Dn5l5TJyJ6olDQnx06cYsOL+1UbFP7hXOVq0ZGJK5O2lofVKdlD5hC9rRyOz9OKn9E69cH7zUYv4UmMaLyrh4MSefr0kxtayDh1q3JQh0BuKQi4ykzsXu6MfVRDRSbvG7arW/p8FyyLUwexvR5+SqG1yRkKdGBlQhyg3tmM41vtL4kWxw+lN7MLfaIfOMUoIQpVnrzZ0RBMAfG5aftMYWum74Va3OFarrV/t3PqnKtw3nHkMfLWCO0/jkEUapDHlvUVRvvjnTKG7fOZUzPuaxXhnbghl1GZt+YGWEY2uHBhKuxMcrXliF3I/SXaACo03FBlvgTuMDNxdeZ6g41D4Xg+lCynkzLXxm873vqZTVZ5JPb6amJxjL3D/DCroDzfugxO7tD4UTQ43qL8qDvkgOchkacqHbx4LYLo6I5fKOJkUAfJqoH8FPJZ5A9Z7/LqGdeeewFF3QremzS4tkUOY6rI2otXbrw6PWdn0a/Yrl/IFa5Mm40hPKtjcslHB+XhcvV6NdgO0NBve5Ih06zu9g3daXdTu2MuLN/LNkvoWNUSkxbhbrXraTokqF+jD9iFP9cz/CudzMiTjPqCoremYE/2RbBhwYOCU4iGJZ3UAearNfrY8+sxiEPdvYOwFtjgs9lK7GTy/chtkgMrzY83lpGrYa5I7J9mWpVQjxgvp6H6taNi2V3+TRip5STGCjP/zEDKyxufh9WFEk8zx6Bbe2MQFZRtIJl2y/jgKKf75fGlpbhn4m/bZVW0KaAIyyVnCgth4F7aZwvbuBWj/Bbe2x1Wb7OZUpF3ZbV5yFMkbeg49PxnJbnuKreOtShDQ4YZvrrzUG+aAhF2zqyqR5khzII5jdLFMTflQ97rOEYR+MiPDYhyVwXNooTnukhOPwMmjUOBr0H4n/7HlyQPyIB9DZKSIUvq0RUrpEPUXneMNduyDk0nOdZ2QCpLUZXOnm04pRxn0lqHD2C3eV8yw+CyHF+s/fLX4tUWgQKWEsXMF/VvTldzrqrDLzfUNhp8oqMoyMCktPRhfmScfh8ztn85r+mtDAeuJ+93aTCfNcVugDMvGVf6oYm4Bz+UGwDqf+auDmqxQ7W+/8gDns6QNbdvuT35emkpezSUzc6ZtgpNKR6xZUoLSQSJpg1VwxHvkGt9fYLpjdU6LJC0uN6lgcmLicvij0BG0PPgJ/GAnLfi+6AYJwaEYVANhuc22c4C2LoYvREWoQVC6K4V9MGrb9opYO0UNI495ur58Cbdp5bTh2Dk2LLHiHxxmDgjCFOQKsMhfKbnFiF3zNRPfKGW/NEs1pShf5gbg9hkLHofJg+krJm7uqDjbbW4SetpHIhxT7oMD3l1YSQtbNcTKEFEMuw+o1tjYIGtrmO5pCn177d0JYgqdd/GbPONjpCnuFXBmCjPnS42ZuOK7vOK5gs5pEcHGJwQ0vf3nBQPCjK4IFqqep7kW1s+SAlVaJmgsp5prnbPiyUghxj6L/YM9cHbWGe8et3wTC1HwcHfeJ6To47bTraWuI8WjP9Pe2f3Q7nADv4Glz0sTBnwuZjG4MfKa1sYjDhY4XJMoXYVcp/3fUCUGArOnrNAkOHk/BeoBEqs+Ne6gB91BBBR0ymholz9xnvLThnGUShNDugkJS3e74a2IODhNDLaw/jREmVMjpG0a41raDcaDtnJcFsh60b0JP/J39xnxiUmhWXmOyrtNQxJUa47oJW+sIROVvA4SG9DDYjdgNQdX9vAj36uHJKH9zEH6SPxZnv7hZdwsXvEjkztaUhb2KMgWg1XFrOOnQ+C263emZ99r/PPWSCTh/P6Bzgk2Y7JhDlfuZ5oh8suUEJ9EpdvppRyWGUdIyRvLlUEg2Z/mOOVlf2zaZsIkW4M/6xjmPT1FabkWTRz5chi7SO0E3ytQJ7KKqWK0zWjNgkncJRxKN5eGay2FA6d6MrQrk1ry+q+3onGRN//UOf2TaH9E6JZ/VxKbSXJ+0vfYsdTma0EZ7C7tzkTRE3t3H6426JsOXGcFUwWSKD9XRYo+n+FIUOG/OVEZ+CNFZRKg3G1EKLGUCWS/kPydKHTE8X8hb1A/q4V+paMZf/P/7liTG6+wlezcSTucb3RT2bDhcNUqIPi4K7yYD9cS/mMTbd3/MR1OwY2DK+jAHtTixKqy4llhk95adCsLS6eprborqTfSHs2LOh0B0eTDz2KzsgAtRVppwqITr6tTAMB8N/3vUZtwTwtkjE/PgeosiUc9aA6c7WN8hU0EXiS1nz/RTIVnCI92G8r7D5Lpygb6KFYt4l97pjujaiaV7b99WWECJT27ygTu22397Z8GsgzIRmsZCv5VXQU0tjUT+xkwS9AFmNYYUTY+Qq/roRyMbXagEO46zcklKUIQtHEV4jt3QJZcPH/7VwvGJLnZAozQhUn5SGpu8Gywt88TdvkDqJcECv0zA+oA3t/U4IEmdUk/ebqLUY9P3QRo6RD0L8ArozQizIl8ADhwcg/9m2cO9ftQA9o5MIzBWocZlgfDKv8bRxWjEN+aOm9vSqDxTOdCaXAZdpKmEnqnDd60M6YVnSvkO9dA10+t/6quzUXW5pEVzkjxNfe2A614OBkhE2FgwAnQYrL5KR+54DiU1xQecXJqJv/1+L60cMM+a9uU3DOgsXyYp9iGEKjCzbGhUR4KD+sOLARrBBfy4ZZ1qJV35h93MVFMtHx6NywHjZjbYmFbg60G5/mQQleVzqjECdNtCpXVIGnIHIo2SSjZ6/fQigghbfda8xu4PzPM072WVx8dc3MiEboXn9zQFDHLkGSIB9gJq4eQZVISjOnuvVrWGSdjzhsViBNlKOVvJTbh4GtPdkdwPwYqaRTkThHfI5Shq/wDNtPlRCWW4Ciz2Qwi06oF9jd9l0cf9jPl1d4c3APb4YPJvRFeZYYFXgpDPDj4YmcGCJEiblRi+swqzIFjAKYTg72uLZ2mnF4tcAYlSvKsCAb37RCwG0v7yWxIQfayLjnntm5iYwHD5jl6wdwHaaypc270WV+DGwVyY4+m/fhO8ludXMaQ8CjRJr5H0iOUQqDmM9Ohnr2BuyJSrq8Re+JWmKRSS3PgKs6EBtAvorBZJqhEsyqPHrcLhBDt/f7QFLoIYfl3PNHNRL20e8Ojr25wfyzbDKlGaxZsBOEWh7Fgq7SmpP6Yy3qsFNHhrgi8fO48Vlpi3KVN4hyQ9NQP2VRVwbBIvntQ5bRiLEhKkN6rPzyIhs4tXlWASfio3JJVUuWddj3hBZJ1QQRy8ANtKErHVcQLJtkiG544iP8Xc3uGQR0HPfoOb3kpQv8rdqvCLYDQVx/HmPvKTN2tRW/yTNmfEbx2+7ZL1y2c6o/FwVSjAoSPWPCLHz+Mmck44QNJ2ovmZ1liPyCAkKCQyZ9mdUcoaSc47s7Lml9G9oeaioronZav11weDyK7dsIe6389QBTnA4nfsFeigVambWpBKCXlrb3H/vwjwmcxnSMIoLmsApdqZavRq2FfkWLBmBxoC1FhVeGcvn2gGJS9yFpPBVH3TU3+Fxg0Q9727Lf3dz4Km9L+QKlvJ9afzRdTHALrX7U+dSvfMoKFOkSgfxsPW+yHXk4fqog2/0PGYfsz82ug26/zTbMKdA3F/4g9inS18fX1SPQAiEZgbIIpkzNxngfgz8919DMb6lQSBgDxadxU1frK7VHDi4UVIIW8GlU66qIdjZFtkzTT7pk2cKZeW3TgGn3gc6glZ5hqx7cO4xHy2fE6nOMrZhl8WQ6NNPw/vf3+ObbbKeGw5ouA65vK0llLZlL5O4+dlijTNti7vIJLIHXky0Uh8d1+8Y3OLJqPZky3sDWRyBVn5DJaU7KDRfZ3meGxaVvxuxWp2yNuwzIopNNFQnEjzwSuALdWgArN6EKBcRmbPzqtMjTk31V5NiSxUojitz4ynzqLhZpsaoOnUSYevehDp09dU7bLoNY1UIz3Nfh8vO+rOMiFcNiARrcYJwNAoKZlIE1dLDnMv4ED/nyWWvprf2LGRGBzQAdtdIh+xeTznSLgGHZIBZi1KWBdo41OCccoo+VeCUrwEtrkn8RcEVif3QD8xafErTvBgkf3B7WPvKBKFnP1EZAE5tvdfmEfoWXpKTkMBOBMMhSeMjuF2RcdscwYoPB3tGykazwaN6IkqVqjDvQMhG90HycQBkcDXX6f3mCQdVGFif93pwUxjlZ/XCxTMh9VNEE79S8DpBjOejnBQMAGyOf2aDSBwPwPDemlcSiCZrprLgMIHDOu6clcRY80FAH29sKQg1aqfvFUh+8R4GhI4EILWlkrFfUX3Cwxo1vmSAbzygM79GiFw4SLb0K0UJsDtnJ7Ge0MCIf5KtiedNL5p2DdeNne7dzGAdjIJjgFD4p7GvjBLUJ8Kfn9ygUA9FQ2uygltkmT9xKphR7pXRgCxa1sqfkGme1GBOqlAbCzsrcdWtCRX+KGFck4tV0jmsGa3Pa69TdtlaNAFIuEgquarCZW5R0fPISDkFXpPMzIfk0jBxfloGeDtVqqIxFARV6pMKncS3bLFaPiH88RdqrD+ryrRk5JKm318O78NWlyPPBsGA7Xix19Ifeq75FR+o6SRvDPZ/vx1ZevRMn3KUVgKarPjf6aDwWDzeBXx3HwgjVS6KLo0eXZSkZdtLf0/EwphGB6EjCVYD7EzQPSCb96TEHWH2EISxolfXuAgKAR/spCJpsyLOo/jUuWHTQUAcOgk7CVGviLUeQcWfl1Ik4c+IW4r6JEQpO3en9kNEvCkP/A6MdYrVaTw70UYY0EJ86qIXBgwaAKjlBAfQEuCd7hajnjYDUe6zZKiHWV1TWERxrPx24HwQJ4JERN4qqQr/EU3Wvie0Fpyc13nauYl0AlSnHAqBIlbPz3bzeZ6JedFo6j8cTbGuoQ3B78qzbe3H+Mib/kZwwmios3xw5xbURMbdCRjRINLX6F9IonF6L6YV0qvuz9OZOimVTSusc16C+Cy1t7GWIeSFNcDCae21HkeJWlyi+LXgCKs3MPPLAHaW+D67pI7PO+/zqW/P7RHBNkk3U24Rm1PMNDy8CdpIvtlY8ctdZp4wj0m9zHOSV9+CctZu5/HSWhG16p0eCRMA/oUpUbs1Om0YhmADrqe3G9K2A/NNlUJu1xKWm7bGSKdGh8U+Ck6ZyTopY+PZN5JnRPyvqIz4OZcU5OoOIIHkAcEr9cvprdujMFMJBanonWFE59QdiciUWy4bp7qxFCig9nPe3nmLKEXxj1p0aHeC8EXlmZyNt/F8+B/LbUHIm0Sc8vvfdUYjtBoHfvj1LiJl1JkwfMxyvwLivMV42aF6MxIIgREzDUI0J4qkv6FEEb4TKN/rpj/9uWbJP49Lx/ACUOaLjUwRgCZENqRswPEUTgAtF4PpxyjGbd0W+YpMvrs5tOywMunmvHqFthPxCVcoQieg+dM7Fx67bCbf2mMmWkMM5Ebbw9UAkR4TO4gUhZ/SOcXD+/Iu7gApnaIxDNdn+OhpyFWOHYe5Rm0NiHZO6+tT9XzwTZlPmSecYVs3pKaILjZKbpiGvNcpgm6NPQo0DikcYNgbdB30O8QTJn/3GuLymko/YqlBkMFKcjS5SzQkyHWtZlfRx2KDlg/npXxvLDEN8BEIifiXPanYF1ogcZ/NWpkILYA5HkzMZ8G8TJ5wtIi+WJ0ZvfYXsV0ohKmUk2WUbYWF2VXBL/98yvfgucXh91mvE7puZj7So0pnBw5NseniJiITgpIx1cAOVyiLnPmU7btBpMwWGTHIGoXJoYSwD+aT4xhU8Iu9KHuc3zLyFQPXhm7wc8HsOmv+upxXdsvDjRaQDPVbbTaRyvsmxhDhsr8K0J5UCdgxQrGyqTLYxAqDsCWA9ftigIS9BHekLi1KOcbViLg4umOJrIDamJdM9MO9qsB5R1YF/GCyBoyroEUYZCXmZgM7WnolvlpsLNYL86ktDTHy2n60hVl198+hmWQNxxwFJF6PieeWZEbbC8Qz1b/30I5TkROwjh//4i+bmyDReH0J/hlNnGbrB0kLf20mt4vqTFBmvxjja/OGtfBjqsO243qDFfREZTvQ60uVbXC1uqNC72fQkqfruD+SDHEE8nbItWj5D/+RrTlbbw0PlyRxV6q52k4MpzgZIVAdo8twlc3TRZqN/6StxewoARxL0c/UPcxJ8/4AeoKNcSeo6+LuiPot+r6zajhTvmQ6QM+nNzmIDksSSKcoeOuu8cGnyuRxtlAQ/EdRO6M8Pa9chFP52hLoQBVbhQ5jK9dylpATKRuFzJNNZtWEVk+Rk7RPx+Jg/xe53zUAOsCJ4epz7eTs9eb8ZkED4kmPru3tXeU3ohqXT9eDku14fa19INyVdzY82tIcgbS6+f2grvlR+/lVxULtqPGd0lyhRkTlG3zsEOA2iluNHrc5CA8Idf3GB+3gTk720+u3/AooXOMESHV9cELGmonSBHkp3bE3T0f7HVXd0y21Q1qGzZAVTPkTI7oFYcDMWTpeWjZCyIxhfKTZ7hqwINn8DORi+RobFnBAwDLoygmPyoqn5x6+AX49azQM3t9IYJfAQ4wZHUW2+gAhHqwLMl/ZiEcazXcbeOBVh6x6KWVHI8T8XR7J+D7/vXNn74YpS5AGnzZ/+HuwyPYrawGz3pX7uXWL1o/z8UabJjXj0KDytcprlNNbAZXCuvemqdNx+wftzfnbkL/YsM6Fmo8pyuT9KMcL71/vV++JV0gnc038jQ2wcrT9c9mDNexQOw7FCVfyYZAP9Sdwqkpn3S2YEF3TkK4mvYNvBKIoQroxrrGcuFgdUwYT0U1ZuhmxnmLpvFHc2mdRcN4xjlN89FpW47lYiEKqSAcdcNKBG0zhNelHWlqcd/EDbQ1AgKu8/qNYi8LvPp7Nr7/9GqFjBgBOoByZbMFEQH5YpqG/13m7FVqdS3/9NP0n33dt5N9NKzqEcIgKHtFUmZ8cZwPENzkC1ymoQh6IiRgYKTe6Y3UY6lwgCpOBc3dXwzugnbqr1NY/y5k4eCSvEoWUz5ctuW+YyS6886E17Wfm5mRIKX/xyfGKTnfxETQdOr/9ClVFNe1CKRjmvRe3TvIxtvROv63Sf1LUsxgrpzTe6Gk19xlEh3/iaWmmPM4Ep0VwfItwIqtGf+xS8HOdHqcBT16dNvdvpvU65qPWYXvBbC5A1w0Uag1+tQv8Wpiw6ArUDk23xVIfDNan2W4d1gtA0Fw2KQZaCRzWGI/Jj2Cl21JSefJl2Lgsgi+fWbw0Y/ohemAyyreVy1t/9vsekspDcx63mwP9Amjo4CbpEZocSZtlU8Xrxkjw6iQf0VQhmMH+mZBHcMcgMke/cOHRkqInN0MMbIDhZP5TgjS/45gb6oTpD6dtMuhXgNNPV/+uUd2Bvj2x6MD+dRE4lGHlDywmGbV7RIhhA33nV8Sc5L1MAsJ7kxs5dB3a5ChJaADfukflQDeiTYSRZHI/kasIrXIUEZNJzZwN9S8BwIlEVBOnUTETO709zoN8SWlgjxyaqUp9I5RmAuw/zAX4qq3w+uKQbRxKvXTjeMrxTvJz0Vcp/kbsfeVuWmlvp/gFNeNUQHyoA7GWvPm0sS/rwIIsC19rdvbzXcAgsf2FDRltGL3hsjIqWseA6M7Q+h0K65yq53SvB9KVRl3id5dfOYjO3wbDYK84tkPknUNrcFJ6YwdgqmRRkF5xhNh+mDIQ0Vg8bWEebf0wilU8pWceKNt7xFsJnY2MfqXoLdchRAKCiquPUdca0It0jBf4K3K3nZLyza+afqTZSILkt54uF4d32RW1fOna9Q4pxvCMaXoZ1UUEuQIAf+5naWECMTvhzSaO18EIEr0URJ0CBkFcwsNAuu9DLP8TCFVEQAGx3j4K767uyC6Wox3atMNHmcelKsfCHFZBLIzvtSw1cR+FrQBop3Noe/BFYmmVMnLnCWfr5pN3mciBCAjtTJLR5S1Xqi+PUDu8QSHGpkLvHlIPzTD9QlcXMidReASAWjb8NapXhGUBtVJkLvmc3RFwNsCmOtZoJzzmGVrhQa/kSbmR/VP5+kPuwW/jOO6teKlsuXUV35VcLjXoEibLMcE4I0BbNzvxLTeLm9zS/EzD0i+phVwFr8qCDVg0kVE8JlEc2k3qQQ6XkPoC0doSnOUTXA3K1nxEZuf5zj9p1u5+G3V6s5Jqb1VTQR1Iyh5cyXpWDv3sWYtS8OxrNQU1kyFI58F46S/UC6UEEUNcLpuHJpqP8WSqQFVu2QApgAOKv+XrCGnqHJ9Qz7+P4HvYxm3l/DHF+NNCJVsbP1TrKirgcR6kuyoC3kzct26afZ+aprCVceGVOyOi/fCSbpKnzI/xNJ813kpxhOuRh9A7bx8oLw+wp976E7QopPELQvTIuJZ+7xV61Jo/Pb7R3bgh2bmcY0o6RKscgL4gxVKE9VGru2oQbeCOfFy1efPlZyP7TlATgbh9DsyXKvU4/ESmZAjErhdJzRZvnlrEbmVosMS6r510JMf/Oq+2ICfDHDggwMHtejDfIf1TTlFvFO8e321FVb1U5PqhFYi+CqFIZzqoqTmapg9gm56EFSP2b4mVrpisDoygTxyTF2nv5DiaWpuIoGmmXn+UPz5urseEyMy9NTz4tSrvTXrBCNS8f20PEJB3Z6twRE+sSiSUU7e7xk/cGWro0/uYqBwnf1T+VZWHRIwL5vA4xSeIFOTgOpHBsK7OGX1wmuKSNlX0qLW++4Rtq8SWQPTs4wx2Be8E20E0Nt3HXUeG/0Q0FRVr+4TA6CliWFtg6D3SWshKq7BDTQgtDYpIRQ6GD7jYYP8hECDH5BDfz+Oh/tOQCkI8zj5duJMVHs1mBb6dpkv4e/fbnZEA1ngmJVZxNMabAhft8kTav76aRPVN7zTwHue590Z5+X/ljFZA3EgScs9pIRZTt/CujtGuLbdEyPoyWr8qDOiPz1I2K1T/LWis16AQbBDDH8vw5jGMm6OXjoKGy6dciUMho5fj65buKtXGx5UmvutLxDdHv5sXSfpJs7B9uehRUg2mmFNHC5mdz4eiKHIdk4/aF9jPo1xFmqjiykEqINPSPBsDvw4lOlkdN7Vaz6QBxoAhuD5gFitnuBDqWQtlqDKK0KSgm2Rrhq/a6cPkveAHRO1/xqrK47XA/GahBfCzPmVFNNZFC2MkFieveCAAdDwWbhlCQVzz0e9Ofw98sqb1a8BvVX8ZAOpExOjweLiaL5IU+WSHLlV3+pRMfY6LA5wy/bgJ00QNl0uzc31NeqbVo1vHDAJG7F1vV7yNFT6oySI0upsvF0BwRuqWdJ37m3UMuysio0VIbt+0/ZAodwhEMricNjVJFoQgS7LB8ecqsKO0P2AMi1MzaTRfZTdbMXAaN04r8V+2Wz1kOi93Os1XbT43o3Oob6SREIfUyobjlyGwVk11+SHtlYQ5UVOVrlcf9wSlr/rQFNoqFVswxVtx8t4QDXqVpf2eZIUYvsGZBFF0FR8H6dmIwfBgPonl8vecBm106ZI4z15oz+Xu1EAsR4SfIhqjGKZz8koF6gA/S6u8vhf3b61Rsw0jMukRw0Wi4MiDwmiVPyh+bTJG9K/fZ4Jt3nfwOvuxaffid29BPlsYKrFTaahZo7fBOXu3cXNeT7jm/e4t61/YHYoBIQU65ZapBjh1qJAnoFX8xRS/X92Ne6jxQDfJ/C4yB3xGohVi/V5IGYhYYkmJ8+HgUX494Uzpwue8//2iVR3uE6DbHGGiAA4xZ3Q3cwQip+f7nexlc5sHaaSTndx7TvCIovzNPjUrq591Rx68ah9gU7PCN4E/1Ja6FfN+ldZvv/tDD7qoEs44FQxPMj85aiRihNrKR66qLWXI9Cjhv0lLK1luK3B6tb4gMxtPRjXPFgetBiCFvKWoKlXsMyPbp+bJuLXVIZDbFR3BG5DW8c8e5T7bBMGgdQoo0dZRWFiWIScsvF2t8USCsb1cCpofFze/6ZXL3WegKnFeYKcVgdy4DXy4OJRMJjdz5mYbaHDdOwsLPiSg2sjfP1WM9zfvEkwaYr3s9mfOHgog1n+dHx/ipZRlrnMhbzhuCWVSnmUDLuAzQ53v+USo+YCgyJA5n9Gykuv7Ov5DJQ2lva8hzUBhomwi62LxW8n6YI3rPDS2/iJkvbtQciyR2qxtOvfNJx3CMb/4kwk+cMhBf3lctrI4AXk/YZNzr/dU4f7GWkyhx1tYndPyRZjMD5hHCwrXh144rEmU1kiWgGC4o/ZERfNJGF/MEiahmQ1kNrMK/ZT++Sc4ZnQYHa9WBMC4tlvKoGeIwIHWhs1gS3SY82w2dQ2eoDxI2U+5OC2GRXzB66gdimHCOr77RiEyZDQ4hvSpTFPd6+gwKJZn37tRW/VIVn6IQd4vFyqrfj8VbQOSPJef7sa0kX02PwpHRvlMeeZTy8PZOhPoD1W4Xxks2tl6BgbZmF3rlK5gvbEYY6rNBogQj5efB3e4v/pYFwfDYJKPpUKElv63HNdpMM0oEJy/oay7Mi9Fqe6VjcUwioTznlerNg5O1Fu5A/2UTKM1EL7Bx4m4HntU+HrRfbnl/hu/UGjMGvBrUpFUXskvu41xk3+8MRxFKbf0uMwFECjhJ+5Mq4JlGjyLWcCL6OfWUbSb9o8Ikj9/FYeYiRsVg1L/p02CVwBtC3JttrwrLAoPMjmhkBcWvsVRefU43MwO9r6my6uIMI4we2o50jht3BtDd2cbtG/2H3+LUGkLnL0z4mOnvosgW+kGnPMJxV4pQB+JUPbL1enAUhgLyXecHHwIbL2wfLy/madFzdMjjQekzN9LyJ5BVlpOA3L6Z5sraN+pA25Fq9aPLfyGWIkKu6eIZOowfvBuzHmIS5vEMXbthCcmVRzgQEZKDt3vRgvVShtwB/NJHU0gA0NBal/eErCXsHOeQvsruBgMLrM2pHOgYp3ft6N3i/2CiPiw1A9k7NJwEf3pRxefz4wEr8P2Tn/3b5hELD1mC7xFP1LBDVNj0I64Rfgg7bvTIY4HxRGS11BsGU97u23slgsB1bxEBjYzIiaZUTfaf9iNplsrlvtpk93/JExlXWpmaX6p0FpfywXEnbxgS7/1MNTNJOc2uvtKimKnhL/oQ0X/kVSK5aCR6w/JpQPS9SbxywXhz9wwFPEYfR5ZM85wiOrZzjLIXVVBQXZN8YnGqj5k4ZPzxb00dLtQXqwKGOMAxrpfebNkJgPNtfJFDvdrEWzqRfHsCUAIGvFNjgtMq8sacaBSpqzN6HM+WS1TGibzJNLwV6j4Q4FYtSortEeAZF2DmGozF8E4VDR8ljvB+x39S3kFrduR3pRR4sQBQhqZfNgEhYFVsGI4prOD13Cg6X9OHy+wK8lbFtP7KBafcnyRGidHv7KZW6xx/XjEAr/EKhgFdePhwuwt6W0ov4HXVp2tRbl7mlfK5gp1CMGpC82N6q5CJmy45bevVLP98hrRZHhpL3q6SqyU6tIMHoxBP2lD5C9eApbKj/GXIPU2hj1yaJup1csLDcfhfvIXJdA+omQJleq3oaVsYLFQmHmrLMdvEKzZoeCPHyX9BK+oWjE2qNXWVVN2uSDJS65z9Lu8sz7dKfWOfP/HY/4yPocZuBI43E5kY+o1pE40wXV3hh0wAln3YtdwBW1ZWizEAiRS0dSXsGv4cDm0j9RJxs9KB15oIvpL6qSOQ7xSTpR9CZkL4tgIuNPVB3MCTjfu+2xzw5yG6TP7bmKZCQ1r+7Lxi8+0RJIaYPaeZI/8p+fPw7Tq2pio6qlaH4aQWLD06neO1Gj2GWBPJy7sALvGLu7TXVOJzo59QhR5qAvuT9L7syFgdHAZLPB1XNO4DIDINqVMGKDrNIXWAJ2DlMshZajIFTzERlMHWk8Fcm9u29ArfyBcJFp0turJZSBx7xJO0ZRjM4NmuKY1gTMj3/Ma2vR98Scn85sHOQ5kR3C8pIyGuJcldIkgKXjL2TqnR+ESilsytF+mpx2V0Ss/Ouza/gSs6zksgc4k5muARfQbJ1FKHhQCyut4QLHItS99VtprZfCBMgwxeEtm9kVbAMQiMsqb8hiRcueZIYIH9iJ63zFRuwAogzTZ2y/cr/idhXdPn17LoWVf6shCJFf+c6sbxxsWoE/uuTumaaYCV1FJWF92He7T+XRkUOk7i1JVGsHdzh0bDCkV6Y86Tk3yw1yschl5gVaQNL6SxVIUL9DhxbNq7E1H1GNocJznlHTrSNgiTMkVmQTgj05F+mVJ5RNqNfNz0XoMZ9eU/20eDH4y2lO4I9qxti64SB1TnXpcoDENW7Zz5Y0zov4lGsnyjEPZ1MEcGtUZQM+m9VyxY3MYPk5rkPoO4sTNrAmE/aoSi8go55q6dV+U8qgkomBlha6EXaHzIp9il2ZGfAoN4jKXThP8L7aBWA2ptum6m7J9eMkH08plSmohFLsKzibdI1sbP/VZp3PXrzEVXKYucnSKqaetFZqMXX+CC86wwUusVSNhC8mfq7CwhlfnMo84oWTCjscEt/kmcpHuWjS1vsmypU7K1535RQHbOCfKCAAB9GQT/UJ9HTeFOBgCo39/KaTrMyFfQPo8sDkRY1XJN4CicS7jSUaidsEA20Y6gfPpdMaf9ev7X9YgZt7MWz3N1/GpVXiUPKaG4BqMBLjxAeV2vBVbRm4Petv0lYDVl9AA+p2hW8vrlwJsu/tY4AN1hCKbBNVcOXS1NBvI4x+TpxZfo+6IsaACV0FWfYc923Oo24awbwA+eF14IaynezUa95JPtU+M0/NA4A4QwojkL++bQzvveVPNAZGFFn0vf20A67zE/tr/cx7ttRLg8iu3+uGkHX3j2MzMaH23VnG1hviZEQNUIbZDmi3zyt4FRwmYeUXlJesFFVPsAZYcTfKrVFgWIpmwzeT/Q8RoxpEBRa66ZSsK562TxtkeeQJca9waKiCLiodBw2V7FmqeoF+qRHAzvziPfvrlYfXv0qDYJ3SEvasWgZJ8rEa4rRT4whON2k8vTwhjvVtJHovTjFb13JDDXzY2GBPsdW7hq3TnmI05QhOJxgwJ+E3wpBpFQsesbEnXDUJ1EjIFS+Jx6oDTax3y1yxE0nYARrtQAqNQRGO7KhVphS/JPmCEgNkZYbTPtvypLhS+twUYJVkpPWv19A3xck05D3Yx+yH9COzu8Mkf/JB7ZMwo781Ag9ANNRm5F+oPEgRGvwu8MS6HgtQGiIbwvrMdV8cDhUmxR51kdbFyRqt121yM9RaC/DRoAbtWq90082Pyx3MIcAS+3xYnICaUMDIlm+gaGvPMC2sbnRk4o41vjKRWDbutg8b7A4jbCI0ehfXfPjuA1X2C5+gszCRSHp1Ksrj9ddUUDXbsgKF0qhH+vGoNu/O4NTF8irNWDVhS+NOTD+CKCIyO/1qSn/hORANjT5Iwa7Q0h428OPiWyy7IQJcP7gdXZaSDDrlKwCDtXzdprXDT/QvqlySRz6wJcj5CcWUKVtDIx9YSMD4tZszlvjCShS25SyaaQvzK61VDwjWBHzgZPC+WBAfXTIqETT2F8F+j7yqlPjfeDAwe1/Q8D0jP2Kx3YxtfNnq0bqZ5/ZXaHrHBp90MJfcZujh2HWiQUZX+r0SEoEv+PS4ExAI82nDAnpbtBPFTp7uiiuqBTZNe5GeIVMiOLHVAL0u267TSdTycz4HbNuo5SK3Jx66hpCt/RESCRPJ60vqOMwUztPEasJ/nXTxeYRLx6bO9lEuIrW6Schrq3dvLzMayfAtmQAkIBxAe60ojERjBp8eU+so2qOLzgloxCSsSJhpiEqJE5cuz86aaxhJsSDdi15ggmgsiY/iuQHiwHSCiyfAHNkbGao56QMpMV+rQR0ZK5F77GY/+44Qqmo9F+XHHSgP80WKTkBAv1gbI8+wQfGMbaOYrWFNU9d50K+l1WAva0AA9fwLDOJ511XTXpEoFeecJaulArEE/GmTOaA7/GJVIyAYvn18RaQmmVhlycgbfzpVqasgfRq1QyynKNVdwPf1ti3/bPb2Ufnzm3w2ZKr2QryiaZ9Cp+3kDjcysAWXWxFOX70pNMvTvipooBplSUF4p+fMYTb7gG27FQYE4sN9OFx5G2oPGSCXyRqKm85f1ag2y8cGqGXJy0pDIGrWKubV225RPNKek94B4L60bl4aL4U/ej4Uu5EMVtVYPmxsI87LuEnmYTvmLHOJYZbHdO4H+9V9VYiX1kosstSVts8MKwuS5/SxNdi/UnDJ2VOOmnxOySTJNW7frvNEdv+D8AIaOu9/84Z64xISTFEAwvPby7pb+WN/6+UjpGBEBX45PAMKwJVPM85oAiUzcIKiw/TGo2wGlGlJNq3PrZhOWkr9Nc9qSdr23qeH0fSI/jSzEKI9h6WpwHXRlnhsZchpovLmIex82yRmJLdVeMl08jCrRB7pv29CayfSuwW5z0FfLLx2UABhPXOGWuZnmU+dhswQGwOHZ9ATgnDsKGMbTrnpFoDpYHx92Bu10TTp94GFOpr9w+VpfCD+gSGE64gChbTW4MXt6QNopcapGi93w3JKmQUMxo6FYJEhg9Xw08npnmpQsl60yKQta+diyJptfPD44PvCeNeFiRedOObOObiI0kUlf/4qaeKrCpf+iIqVASkfK0rF2Cfkiat9opGPeMJK/zLtY+FmmZjdpIrQurg3XJiqYrMp01JqoCfsaaSm2WGfBaiDaneyqOMh6WTcBYVCl2G5aakkGVx40y2RS/ncsaut9VCDnGBGVenMLnKVite3tM5zd/VEl64jkgHe69dpC8r2MQrcxBmf3VE9HGE59jhCLV5ZWqN8DzzMqD7Adh0Eg9KSp11ZberYt0FGe6L+L8ADAwxnT90Nq3x5WqX0viB+4lsjA059DUkQYSAkjaUnrJiKwi+2Sf6jf2vI3zjQW20bTHJYruFAiXi9wLHCuL6nnVDValBHuYn80gU2GrSbsmxsez3aueOniDVbIG1kYyZjMR8zrKVA3kwqOCZA/JtWqBEcyFWLLQn6Wn6CgO/WMSwIOzCAbki2zQfzCMgTDi6/IAK18plfZ9BiZQHXQUAlm/PDs65Gj+gQBP3Xr+SEEQVxqk+L9hEuCUj4781SnQtXW7OGjMEy2uQXuSPfj7hbOeoPzAmNakScSyf6xnrEvP7zkk520GY/Upq1skQ+8CRJhtJSooDRaWLFh+1FyoHKZk0gcZRSHeIKxXptXvUSEyTzPrL0MHwn2TeNjRir7nxyU6I54eYCeL3ws+ipDw2Tq27E0+KFLQ/T0wqXITmdWrL/KTe7Dl7087T4zRHSl0kNII3sJKiIhJKOpbRcEHjUDKgI75LYeXv8pgtMm2nUObL2tcN7Kts7MZ2iBaXHONKctEADEm+O5/XNTrXwEFD0oJPb6p8+gI/dk7EMfAgtL2x+fMEqLdQpJFnqWgn1G3zOGZN3DIaWLwwRT2CgCmlzv5DY9Z4wv7H3cvasZLIe8W/AC+DSMgxkdE5ud9D68WlfJ+8BKxicR3u4RqwbKEtc1pisD7ZXr5cTn/KGWsfHF1PlNxjx0ath7tkRboRNqqx4gI95BsoDj06eGdliZ4aL4A1rnX6/JGMaBV4wr2r+cTSVq6vPgKkO/A8dkQibcfSBIFEpp2E3wP3S/m8yOlimUS7uOOU92eAQwH57/Qnas2zADMG3zDaHj9thJFvkcAqvp+WS7w5HA5xkMwSYFoqEhYH6wGKk5f0y0hamufc95iwdasAMsDlQf5Wl4mwixThxD3J8/NpzrvLnfv9LUSi+AljqCXPZrP/HXeo1XSC9qI+P+Hs0W5zRk1VhsLrN+TKsLbBJCVqBuLL4/bD0xkuMwTV42+FqTa10VxGmBWpY2U9NpgQpIYt2+XI7lqky5XV3XxJsSYevBNJOYLERxtgo+uyxZGJcDckRvntjPLxeKdmFMukIYDs/AwMNwzovcFiiOPBSqCYMlxExRhBogFTCgRFtQG77AI/dirOqq/9UxGeCxwsRn8J68yPQh8Y/NRK/IhraOoOPOtLtw/sUJP4VR3xLWoc5kdfKEfacV4E3DwCY4uemudmLvY5qxNHPzAKe3Ug2ug5fagfp4Eo6Ib6jO3mxb20N73WwYokhUlBJgdpNBlv2pYymKJCic0zMl12YTl35Qd6OhK4T/aqLDd9X7IaCPTlF73B3XNlIcVoWTPDQGP5lLIxnxiOgMBTwY2WwstK5EIGdNa1FGTb/vM1s59o4K4EAJS+cPO07iB+GigaEKlOXk+mgOnLqz7f439rGMyU9A0Em++TJEWzMIv1Ljqo4oX0jjslyeyK+s0QdbCz/0U6OJnReNsDWY4XFfxTobJbOJac6HrZIKhWo+5EI9nVwMd0EQtHRS0OvLv1T/l4ZbSRNxyxREwBYh9wBboFBixYSTt0hJ90yrtCpTZ3PofzGcIAH4MpVpDP74fRGRrkUvLBTlJIGuy9cTi8OmUrAzSFqNnbhJSr66DrSdSRHMo5rK3lIWcowOqe3KGEfPlhqco6xb9Q8wRPD7v0ABrphS2VAnAIneivOlQtp9YnPpLO4sO5mHeYE3/gcFuYiqSK/n6tfmKgebEilMab20LPgYv+f//XJH9RoWGhLQ/M/kU18uLdt56oMQ6dCjhYzATNDWqPscr8lAImzilMHiDpY+Dnfj2wiCs9cWq3EAmhFTBNuDt+BU3bnSiZMS4pz6FMNpbj+BVGgVM4hXZypn7zNx1u0QAq7gYD/eZldMPGkMIdfAeWWveRv/n0YOvfgMfFohYhMjxTvWM+KR0de1ajnwROYrnBffPbXq50FkNiDIDCoGDS1vBnbGXETz5tn1kzlKjrQAC8ln6cJNk4X9mIVp5ZsKHC4+ENMe9anTaTW1pwO+uTG3yD9B4LmBXNLw3acE36MOXhBwFVgT8yOrHXYV2mgGhAwJuRjpXxjIkHUOVswLKDvTFoFMRZW/TYjBu2m57eRmpLmMkdqw7lebRijYDS/Mbo0tZBn7zp43RTyLX4NBbV+VDB6RYDGjATPGj71vXGD8eZECcwjWYYAGkEui7T4TyUYyhxoS8BLItg9xRVTXoHCE0EFIIPA68sduWerrhQe022V1B4sJClu94PxKtXLQkFtHgUfatgFdwA5FDCaaMLIgiijELZqXE2pLsTFDnE4hKIDL9DS6dPnkVi8xaX39t1Bgj44qu+hXPnQvX3z5BtAmz34WPgzphdIHGaeEwJjP9S8/MpAeFCa35gDB0s2e4bVG5aCgRsR6euKNem2OVvo016KhkPOJmNjg+0zC8hJlauYrDYWIZxaCo7vG+uNKlvqgw==\"}",
- "Rolled back prior to loading image separate": "{\"iv\":\"PM3C5UNaRNdeI4cU\",\"encryptedData\":\"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\"}"
+ "Rolled back prior to loading image separate": "{\"iv\":\"PM3C5UNaRNdeI4cU\",\"encryptedData\":\"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\"}",
+ "Complete restart": "{\"iv\":\"uOqGpgOKQ/o5v8Q9\",\"encryptedData\":\"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\"}"
}
\ No newline at end of file
diff --git a/backend/src/db/api/bottles.js b/backend/src/db/api/bottles.js
index a0f63d8..f981c14 100644
--- a/backend/src/db/api/bottles.js
+++ b/backend/src/db/api/bottles.js
@@ -15,18 +15,20 @@ module.exports = class BottlesDBApi {
{
id: data.id || undefined,
- name: data.name || null,
proof: data.proof || null,
- type: data.type || null,
- notes: data.notes || null,
- tasting_notes: data.tasting_notes || null,
- msrp_range: data.msrp_range || null,
- secondary_value_range: data.secondary_value_range || null,
- opened_bottle_indicator: data.opened_bottle_indicator || false,
-
- quantity: data.quantity || null,
- barcode: data.barcode || null,
age: data.age || null,
+ rating: data.rating || null,
+ collectable: data.collectable || false,
+
+ rickhouse: data.rickhouse || null,
+ rack: data.rack || null,
+ release: data.release || null,
+ barrelnumber: data.barrelnumber || null,
+ barreleddate: data.barreleddate || null,
+ bottlenumber: data.bottlenumber || null,
+ dateacquired: data.dateacquired || null,
+ volume: data.volume || null,
+ notes: data.notes || null,
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -34,27 +36,25 @@ module.exports = class BottlesDBApi {
{ transaction },
);
- await bottles.setBrand(data.brand || null, {
- transaction,
- });
-
- await bottles.setDistillery(data.distillery || null, {
- transaction,
- });
-
await bottles.setUser(data.user || null, {
transaction,
});
- await FileDBApi.replaceRelationFiles(
- {
- belongsTo: db.bottles.getTableName(),
- belongsToColumn: 'picture',
- belongsToId: bottles.id,
- },
- data.picture,
- options,
- );
+ await bottles.setProduct(data.product || null, {
+ transaction,
+ });
+
+ await bottles.setLocation(data.location || null, {
+ transaction,
+ });
+
+ await bottles.setPhotofront(data.photofront || null, {
+ transaction,
+ });
+
+ await bottles.setPhotoback(data.photoback || null, {
+ transaction,
+ });
return bottles;
}
@@ -67,18 +67,20 @@ module.exports = class BottlesDBApi {
const bottlesData = data.map((item, index) => ({
id: item.id || undefined,
- name: item.name || null,
proof: item.proof || null,
- type: item.type || null,
- notes: item.notes || null,
- tasting_notes: item.tasting_notes || null,
- msrp_range: item.msrp_range || null,
- secondary_value_range: item.secondary_value_range || null,
- opened_bottle_indicator: item.opened_bottle_indicator || false,
-
- quantity: item.quantity || null,
- barcode: item.barcode || null,
age: item.age || null,
+ rating: item.rating || null,
+ collectable: item.collectable || false,
+
+ rickhouse: item.rickhouse || null,
+ rack: item.rack || null,
+ release: item.release || null,
+ barrelnumber: item.barrelnumber || null,
+ barreleddate: item.barreleddate || null,
+ bottlenumber: item.bottlenumber || null,
+ dateacquired: item.dateacquired || null,
+ volume: item.volume || null,
+ notes: item.notes || null,
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -90,18 +92,6 @@ module.exports = class BottlesDBApi {
// For each item created, replace relation files
- for (let i = 0; i < bottles.length; i++) {
- await FileDBApi.replaceRelationFiles(
- {
- belongsTo: db.bottles.getTableName(),
- belongsToColumn: 'picture',
- belongsToId: bottles[i].id,
- },
- data[i].picture,
- options,
- );
- }
-
return bottles;
}
@@ -113,52 +103,41 @@ module.exports = class BottlesDBApi {
const updatePayload = {};
- if (data.name !== undefined) updatePayload.name = data.name;
-
if (data.proof !== undefined) updatePayload.proof = data.proof;
- if (data.type !== undefined) updatePayload.type = data.type;
+ if (data.age !== undefined) updatePayload.age = data.age;
+
+ if (data.rating !== undefined) updatePayload.rating = data.rating;
+
+ if (data.collectable !== undefined)
+ updatePayload.collectable = data.collectable;
+
+ if (data.rickhouse !== undefined) updatePayload.rickhouse = data.rickhouse;
+
+ if (data.rack !== undefined) updatePayload.rack = data.rack;
+
+ if (data.release !== undefined) updatePayload.release = data.release;
+
+ if (data.barrelnumber !== undefined)
+ updatePayload.barrelnumber = data.barrelnumber;
+
+ if (data.barreleddate !== undefined)
+ updatePayload.barreleddate = data.barreleddate;
+
+ if (data.bottlenumber !== undefined)
+ updatePayload.bottlenumber = data.bottlenumber;
+
+ if (data.dateacquired !== undefined)
+ updatePayload.dateacquired = data.dateacquired;
+
+ if (data.volume !== undefined) updatePayload.volume = data.volume;
if (data.notes !== undefined) updatePayload.notes = data.notes;
- if (data.tasting_notes !== undefined)
- updatePayload.tasting_notes = data.tasting_notes;
-
- if (data.msrp_range !== undefined)
- updatePayload.msrp_range = data.msrp_range;
-
- if (data.secondary_value_range !== undefined)
- updatePayload.secondary_value_range = data.secondary_value_range;
-
- if (data.opened_bottle_indicator !== undefined)
- updatePayload.opened_bottle_indicator = data.opened_bottle_indicator;
-
- if (data.quantity !== undefined) updatePayload.quantity = data.quantity;
-
- if (data.barcode !== undefined) updatePayload.barcode = data.barcode;
-
- if (data.age !== undefined) updatePayload.age = data.age;
-
updatePayload.updatedById = currentUser.id;
await bottles.update(updatePayload, { transaction });
- if (data.brand !== undefined) {
- await bottles.setBrand(
- data.brand,
-
- { transaction },
- );
- }
-
- if (data.distillery !== undefined) {
- await bottles.setDistillery(
- data.distillery,
-
- { transaction },
- );
- }
-
if (data.user !== undefined) {
await bottles.setUser(
data.user,
@@ -167,15 +146,37 @@ module.exports = class BottlesDBApi {
);
}
- await FileDBApi.replaceRelationFiles(
- {
- belongsTo: db.bottles.getTableName(),
- belongsToColumn: 'picture',
- belongsToId: bottles.id,
- },
- data.picture,
- options,
- );
+ if (data.product !== undefined) {
+ await bottles.setProduct(
+ data.product,
+
+ { transaction },
+ );
+ }
+
+ if (data.location !== undefined) {
+ await bottles.setLocation(
+ data.location,
+
+ { transaction },
+ );
+ }
+
+ if (data.photofront !== undefined) {
+ await bottles.setPhotofront(
+ data.photofront,
+
+ { transaction },
+ );
+ }
+
+ if (data.photoback !== undefined) {
+ await bottles.setPhotoback(
+ data.photoback,
+
+ { transaction },
+ );
+ }
return bottles;
}
@@ -238,15 +239,7 @@ module.exports = class BottlesDBApi {
const output = bottles.get({ plain: true });
- output.brand = await bottles.getBrand({
- transaction,
- });
-
- output.picture = await bottles.getPicture({
- transaction,
- });
-
- output.distillery = await bottles.getDistillery({
+ output.reviews_bottle = await bottles.getReviews_bottle({
transaction,
});
@@ -254,6 +247,22 @@ module.exports = class BottlesDBApi {
transaction,
});
+ output.product = await bottles.getProduct({
+ transaction,
+ });
+
+ output.location = await bottles.getLocation({
+ transaction,
+ });
+
+ output.photofront = await bottles.getPhotofront({
+ transaction,
+ });
+
+ output.photoback = await bottles.getPhotoback({
+ transaction,
+ });
+
return output;
}
@@ -270,58 +279,6 @@ module.exports = class BottlesDBApi {
const transaction = (options && options.transaction) || undefined;
let include = [
- {
- model: db.brands,
- as: 'brand',
-
- where: filter.brand
- ? {
- [Op.or]: [
- {
- id: {
- [Op.in]: filter.brand
- .split('|')
- .map((term) => Utils.uuid(term)),
- },
- },
- {
- name: {
- [Op.or]: filter.brand
- .split('|')
- .map((term) => ({ [Op.iLike]: `%${term}%` })),
- },
- },
- ],
- }
- : {},
- },
-
- {
- model: db.distilleries,
- as: 'distillery',
-
- where: filter.distillery
- ? {
- [Op.or]: [
- {
- id: {
- [Op.in]: filter.distillery
- .split('|')
- .map((term) => Utils.uuid(term)),
- },
- },
- {
- name: {
- [Op.or]: filter.distillery
- .split('|')
- .map((term) => ({ [Op.iLike]: `%${term}%` })),
- },
- },
- ],
- }
- : {},
- },
-
{
model: db.users,
as: 'user',
@@ -349,8 +306,107 @@ module.exports = class BottlesDBApi {
},
{
- model: db.file,
- as: 'picture',
+ model: db.products,
+ as: 'product',
+
+ where: filter.product
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.product
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ name: {
+ [Op.or]: filter.product
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
+ {
+ model: db.locations,
+ as: 'location',
+
+ where: filter.location
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.location
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ name: {
+ [Op.or]: filter.location
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
+ {
+ model: db.photos,
+ as: 'photofront',
+
+ where: filter.photofront
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.photofront
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ image: {
+ [Op.or]: filter.photofront
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
+ {
+ model: db.photos,
+ as: 'photoback',
+
+ where: filter.photoback
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.photoback
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ image: {
+ [Op.or]: filter.photoback
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
},
];
@@ -362,10 +418,38 @@ module.exports = class BottlesDBApi {
};
}
- if (filter.name) {
+ if (filter.rickhouse) {
where = {
...where,
- [Op.and]: Utils.ilike('bottles', 'name', filter.name),
+ [Op.and]: Utils.ilike('bottles', 'rickhouse', filter.rickhouse),
+ };
+ }
+
+ if (filter.rack) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('bottles', 'rack', filter.rack),
+ };
+ }
+
+ if (filter.release) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('bottles', 'release', filter.release),
+ };
+ }
+
+ if (filter.barrelnumber) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('bottles', 'barrelnumber', filter.barrelnumber),
+ };
+ }
+
+ if (filter.bottlenumber) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('bottles', 'bottlenumber', filter.bottlenumber),
};
}
@@ -376,42 +460,6 @@ module.exports = class BottlesDBApi {
};
}
- if (filter.tasting_notes) {
- where = {
- ...where,
- [Op.and]: Utils.ilike(
- 'bottles',
- 'tasting_notes',
- filter.tasting_notes,
- ),
- };
- }
-
- if (filter.msrp_range) {
- where = {
- ...where,
- [Op.and]: Utils.ilike('bottles', 'msrp_range', filter.msrp_range),
- };
- }
-
- if (filter.secondary_value_range) {
- where = {
- ...where,
- [Op.and]: Utils.ilike(
- 'bottles',
- 'secondary_value_range',
- filter.secondary_value_range,
- ),
- };
- }
-
- if (filter.barcode) {
- where = {
- ...where,
- [Op.and]: Utils.ilike('bottles', 'barcode', filter.barcode),
- };
- }
-
if (filter.proofRange) {
const [start, end] = filter.proofRange;
@@ -436,30 +484,6 @@ module.exports = class BottlesDBApi {
}
}
- if (filter.quantityRange) {
- const [start, end] = filter.quantityRange;
-
- if (start !== undefined && start !== null && start !== '') {
- where = {
- ...where,
- quantity: {
- ...where.quantity,
- [Op.gte]: start,
- },
- };
- }
-
- if (end !== undefined && end !== null && end !== '') {
- where = {
- ...where,
- quantity: {
- ...where.quantity,
- [Op.lte]: end,
- },
- };
- }
- }
-
if (filter.ageRange) {
const [start, end] = filter.ageRange;
@@ -484,6 +508,102 @@ module.exports = class BottlesDBApi {
}
}
+ if (filter.ratingRange) {
+ const [start, end] = filter.ratingRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ rating: {
+ ...where.rating,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ rating: {
+ ...where.rating,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
+ if (filter.barreleddateRange) {
+ const [start, end] = filter.barreleddateRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ barreleddate: {
+ ...where.barreleddate,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ barreleddate: {
+ ...where.barreleddate,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
+ if (filter.dateacquiredRange) {
+ const [start, end] = filter.dateacquiredRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ dateacquired: {
+ ...where.dateacquired,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ dateacquired: {
+ ...where.dateacquired,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
+ if (filter.volumeRange) {
+ const [start, end] = filter.volumeRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ volume: {
+ ...where.volume,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ volume: {
+ ...where.volume,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
if (filter.active !== undefined) {
where = {
...where,
@@ -491,17 +611,10 @@ module.exports = class BottlesDBApi {
};
}
- if (filter.type) {
+ if (filter.collectable) {
where = {
...where,
- type: filter.type,
- };
- }
-
- if (filter.opened_bottle_indicator) {
- where = {
- ...where,
- opened_bottle_indicator: filter.opened_bottle_indicator,
+ collectable: filter.collectable,
};
}
@@ -567,22 +680,22 @@ module.exports = class BottlesDBApi {
where = {
[Op.or]: [
{ ['id']: Utils.uuid(query) },
- Utils.ilike('bottles', 'name', query),
+ Utils.ilike('bottles', 'id', query),
],
};
}
const records = await db.bottles.findAll({
- attributes: ['id', 'name'],
+ attributes: ['id', 'id'],
where,
limit: limit ? Number(limit) : undefined,
offset: offset ? Number(offset) : undefined,
- orderBy: [['name', 'ASC']],
+ orderBy: [['id', 'ASC']],
});
return records.map((record) => ({
id: record.id,
- label: record.name,
+ label: record.id,
}));
}
};
diff --git a/backend/src/db/api/brands.js b/backend/src/db/api/brands.js
index df9fa52..2170498 100644
--- a/backend/src/db/api/brands.js
+++ b/backend/src/db/api/brands.js
@@ -16,6 +16,7 @@ module.exports = class BrandsDBApi {
id: data.id || undefined,
name: data.name || null,
+ status: data.status || null,
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -39,6 +40,7 @@ module.exports = class BrandsDBApi {
id: item.id || undefined,
name: item.name || null,
+ status: item.status || null,
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -63,6 +65,8 @@ module.exports = class BrandsDBApi {
if (data.name !== undefined) updatePayload.name = data.name;
+ if (data.status !== undefined) updatePayload.status = data.status;
+
updatePayload.updatedById = currentUser.id;
await brands.update(updatePayload, { transaction });
@@ -136,7 +140,7 @@ module.exports = class BrandsDBApi {
const output = brands.get({ plain: true });
- output.bottles_brand = await brands.getBottles_brand({
+ output.products_brand = await brands.getProducts_brand({
transaction,
});
@@ -202,6 +206,30 @@ module.exports = class BrandsDBApi {
};
}
+ if (filter.statusRange) {
+ const [start, end] = filter.statusRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ status: {
+ ...where.status,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ status: {
+ ...where.status,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
if (filter.active !== undefined) {
where = {
...where,
diff --git a/backend/src/db/api/conversationparticipants.js b/backend/src/db/api/conversationparticipants.js
new file mode 100644
index 0000000..f0e41ec
--- /dev/null
+++ b/backend/src/db/api/conversationparticipants.js
@@ -0,0 +1,335 @@
+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 ConversationparticipantsDBApi {
+ static async create(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversationparticipants = await db.conversationparticipants.create(
+ {
+ id: data.id || undefined,
+
+ importHash: data.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ },
+ { transaction },
+ );
+
+ await conversationparticipants.setConversation(data.conversation || null, {
+ transaction,
+ });
+
+ await conversationparticipants.setUser(data.user || null, {
+ transaction,
+ });
+
+ return conversationparticipants;
+ }
+
+ 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 conversationparticipantsData = data.map((item, index) => ({
+ id: item.id || undefined,
+
+ importHash: item.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ createdAt: new Date(Date.now() + index * 1000),
+ }));
+
+ // Bulk create items
+ const conversationparticipants =
+ await db.conversationparticipants.bulkCreate(
+ conversationparticipantsData,
+ { transaction },
+ );
+
+ // For each item created, replace relation files
+
+ return conversationparticipants;
+ }
+
+ static async update(id, data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversationparticipants = await db.conversationparticipants.findByPk(
+ id,
+ {},
+ { transaction },
+ );
+
+ const updatePayload = {};
+
+ updatePayload.updatedById = currentUser.id;
+
+ await conversationparticipants.update(updatePayload, { transaction });
+
+ if (data.conversation !== undefined) {
+ await conversationparticipants.setConversation(
+ data.conversation,
+
+ { transaction },
+ );
+ }
+
+ if (data.user !== undefined) {
+ await conversationparticipants.setUser(
+ data.user,
+
+ { transaction },
+ );
+ }
+
+ return conversationparticipants;
+ }
+
+ static async deleteByIds(ids, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversationparticipants = await db.conversationparticipants.findAll({
+ where: {
+ id: {
+ [Op.in]: ids,
+ },
+ },
+ transaction,
+ });
+
+ await db.sequelize.transaction(async (transaction) => {
+ for (const record of conversationparticipants) {
+ await record.update({ deletedBy: currentUser.id }, { transaction });
+ }
+ for (const record of conversationparticipants) {
+ await record.destroy({ transaction });
+ }
+ });
+
+ return conversationparticipants;
+ }
+
+ static async remove(id, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversationparticipants = await db.conversationparticipants.findByPk(
+ id,
+ options,
+ );
+
+ await conversationparticipants.update(
+ {
+ deletedBy: currentUser.id,
+ },
+ {
+ transaction,
+ },
+ );
+
+ await conversationparticipants.destroy({
+ transaction,
+ });
+
+ return conversationparticipants;
+ }
+
+ static async findBy(where, options) {
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversationparticipants = await db.conversationparticipants.findOne(
+ { where },
+ { transaction },
+ );
+
+ if (!conversationparticipants) {
+ return conversationparticipants;
+ }
+
+ const output = conversationparticipants.get({ plain: true });
+
+ output.conversation = await conversationparticipants.getConversation({
+ transaction,
+ });
+
+ output.user = await conversationparticipants.getUser({
+ transaction,
+ });
+
+ return output;
+ }
+
+ static async findAll(filter, options) {
+ const limit = filter.limit || 0;
+ let offset = 0;
+ let where = {};
+ const currentPage = +filter.page;
+
+ offset = currentPage * limit;
+
+ const orderBy = null;
+
+ const transaction = (options && options.transaction) || undefined;
+
+ let include = [
+ {
+ model: db.conversations,
+ as: 'conversation',
+
+ where: filter.conversation
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.conversation
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ id: {
+ [Op.or]: filter.conversation
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
+ {
+ model: db.users,
+ as: 'user',
+
+ where: filter.user
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.user
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ firstName: {
+ [Op.or]: filter.user
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+ ];
+
+ if (filter) {
+ if (filter.id) {
+ where = {
+ ...where,
+ ['id']: Utils.uuid(filter.id),
+ };
+ }
+
+ if (filter.active !== undefined) {
+ where = {
+ ...where,
+ active: filter.active === true || filter.active === 'true',
+ };
+ }
+
+ if (filter.createdAtRange) {
+ const [start, end] = filter.createdAtRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+ }
+
+ const queryOptions = {
+ where,
+ include,
+ distinct: true,
+ order:
+ filter.field && filter.sort
+ ? [[filter.field, filter.sort]]
+ : [['createdAt', 'desc']],
+ transaction: options?.transaction,
+ logging: console.log,
+ };
+
+ if (!options?.countOnly) {
+ queryOptions.limit = limit ? Number(limit) : undefined;
+ queryOptions.offset = offset ? Number(offset) : undefined;
+ }
+
+ try {
+ const { rows, count } = await db.conversationparticipants.findAndCountAll(
+ queryOptions,
+ );
+
+ return {
+ rows: options?.countOnly ? [] : rows,
+ count: count,
+ };
+ } catch (error) {
+ console.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ static async findAllAutocomplete(query, limit, offset) {
+ let where = {};
+
+ if (query) {
+ where = {
+ [Op.or]: [
+ { ['id']: Utils.uuid(query) },
+ Utils.ilike('conversationparticipants', 'id', query),
+ ],
+ };
+ }
+
+ const records = await db.conversationparticipants.findAll({
+ attributes: ['id', 'id'],
+ where,
+ limit: limit ? Number(limit) : undefined,
+ offset: offset ? Number(offset) : undefined,
+ orderBy: [['id', 'ASC']],
+ });
+
+ return records.map((record) => ({
+ id: record.id,
+ label: record.id,
+ }));
+ }
+};
diff --git a/backend/src/db/api/conversations.js b/backend/src/db/api/conversations.js
new file mode 100644
index 0000000..22f27ce
--- /dev/null
+++ b/backend/src/db/api/conversations.js
@@ -0,0 +1,285 @@
+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 ConversationsDBApi {
+ static async create(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversations = await db.conversations.create(
+ {
+ id: data.id || undefined,
+
+ createdat: data.createdat || null,
+ importHash: data.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ },
+ { transaction },
+ );
+
+ return conversations;
+ }
+
+ 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 conversationsData = data.map((item, index) => ({
+ id: item.id || undefined,
+
+ createdat: item.createdat || null,
+ importHash: item.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ createdAt: new Date(Date.now() + index * 1000),
+ }));
+
+ // Bulk create items
+ const conversations = await db.conversations.bulkCreate(conversationsData, {
+ transaction,
+ });
+
+ // For each item created, replace relation files
+
+ return conversations;
+ }
+
+ static async update(id, data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversations = await db.conversations.findByPk(
+ id,
+ {},
+ { transaction },
+ );
+
+ const updatePayload = {};
+
+ if (data.createdat !== undefined) updatePayload.createdat = data.createdat;
+
+ updatePayload.updatedById = currentUser.id;
+
+ await conversations.update(updatePayload, { transaction });
+
+ return conversations;
+ }
+
+ static async deleteByIds(ids, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversations = await db.conversations.findAll({
+ where: {
+ id: {
+ [Op.in]: ids,
+ },
+ },
+ transaction,
+ });
+
+ await db.sequelize.transaction(async (transaction) => {
+ for (const record of conversations) {
+ await record.update({ deletedBy: currentUser.id }, { transaction });
+ }
+ for (const record of conversations) {
+ await record.destroy({ transaction });
+ }
+ });
+
+ return conversations;
+ }
+
+ static async remove(id, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversations = await db.conversations.findByPk(id, options);
+
+ await conversations.update(
+ {
+ deletedBy: currentUser.id,
+ },
+ {
+ transaction,
+ },
+ );
+
+ await conversations.destroy({
+ transaction,
+ });
+
+ return conversations;
+ }
+
+ static async findBy(where, options) {
+ const transaction = (options && options.transaction) || undefined;
+
+ const conversations = await db.conversations.findOne(
+ { where },
+ { transaction },
+ );
+
+ if (!conversations) {
+ return conversations;
+ }
+
+ const output = conversations.get({ plain: true });
+
+ output.messages_conversation = await conversations.getMessages_conversation(
+ {
+ transaction,
+ },
+ );
+
+ output.conversationparticipants_conversation =
+ await conversations.getConversationparticipants_conversation({
+ transaction,
+ });
+
+ return output;
+ }
+
+ static async findAll(filter, options) {
+ const limit = filter.limit || 0;
+ let offset = 0;
+ let where = {};
+ const currentPage = +filter.page;
+
+ offset = currentPage * limit;
+
+ const orderBy = null;
+
+ const transaction = (options && options.transaction) || undefined;
+
+ let include = [];
+
+ if (filter) {
+ if (filter.id) {
+ where = {
+ ...where,
+ ['id']: Utils.uuid(filter.id),
+ };
+ }
+
+ if (filter.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 (filter.active !== undefined) {
+ where = {
+ ...where,
+ active: filter.active === true || filter.active === 'true',
+ };
+ }
+
+ if (filter.createdAtRange) {
+ const [start, end] = filter.createdAtRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+ }
+
+ const queryOptions = {
+ where,
+ include,
+ distinct: true,
+ order:
+ filter.field && filter.sort
+ ? [[filter.field, filter.sort]]
+ : [['createdAt', 'desc']],
+ transaction: options?.transaction,
+ logging: console.log,
+ };
+
+ if (!options?.countOnly) {
+ queryOptions.limit = limit ? Number(limit) : undefined;
+ queryOptions.offset = offset ? Number(offset) : undefined;
+ }
+
+ try {
+ const { rows, count } = await db.conversations.findAndCountAll(
+ queryOptions,
+ );
+
+ return {
+ rows: options?.countOnly ? [] : rows,
+ count: count,
+ };
+ } catch (error) {
+ console.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ static async findAllAutocomplete(query, limit, offset) {
+ let where = {};
+
+ if (query) {
+ where = {
+ [Op.or]: [
+ { ['id']: Utils.uuid(query) },
+ Utils.ilike('conversations', 'id', query),
+ ],
+ };
+ }
+
+ const records = await db.conversations.findAll({
+ attributes: ['id', 'id'],
+ where,
+ limit: limit ? Number(limit) : undefined,
+ offset: offset ? Number(offset) : undefined,
+ orderBy: [['id', 'ASC']],
+ });
+
+ return records.map((record) => ({
+ id: record.id,
+ label: record.id,
+ }));
+ }
+};
diff --git a/backend/src/db/api/distilleries.js b/backend/src/db/api/distilleries.js
index c0255bf..9e5fc0b 100644
--- a/backend/src/db/api/distilleries.js
+++ b/backend/src/db/api/distilleries.js
@@ -16,6 +16,9 @@ module.exports = class DistilleriesDBApi {
id: data.id || undefined,
name: data.name || null,
+ city: data.city || null,
+ state: data.state || null,
+ status: data.status || null,
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -35,6 +38,9 @@ module.exports = class DistilleriesDBApi {
id: item.id || undefined,
name: item.name || null,
+ city: item.city || null,
+ state: item.state || null,
+ status: item.status || null,
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -65,6 +71,12 @@ module.exports = class DistilleriesDBApi {
if (data.name !== undefined) updatePayload.name = data.name;
+ if (data.city !== undefined) updatePayload.city = data.city;
+
+ if (data.state !== undefined) updatePayload.state = data.state;
+
+ if (data.status !== undefined) updatePayload.status = data.status;
+
updatePayload.updatedById = currentUser.id;
await distilleries.update(updatePayload, { transaction });
@@ -133,10 +145,6 @@ module.exports = class DistilleriesDBApi {
const output = distilleries.get({ plain: true });
- output.bottles_distillery = await distilleries.getBottles_distillery({
- transaction,
- });
-
output.brands_distillery = await distilleries.getBrands_distillery({
transaction,
});
@@ -173,6 +181,44 @@ module.exports = class DistilleriesDBApi {
};
}
+ if (filter.city) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('distilleries', 'city', filter.city),
+ };
+ }
+
+ if (filter.state) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('distilleries', 'state', filter.state),
+ };
+ }
+
+ if (filter.statusRange) {
+ const [start, end] = filter.statusRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ status: {
+ ...where.status,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ status: {
+ ...where.status,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
if (filter.active !== undefined) {
where = {
...where,
diff --git a/backend/src/db/api/locations.js b/backend/src/db/api/locations.js
new file mode 100644
index 0000000..0f9ff2e
--- /dev/null
+++ b/backend/src/db/api/locations.js
@@ -0,0 +1,294 @@
+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 LocationsDBApi {
+ static async create(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const locations = await db.locations.create(
+ {
+ id: data.id || undefined,
+
+ name: data.name || null,
+ importHash: data.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ },
+ { transaction },
+ );
+
+ await locations.setUser(data.user || null, {
+ transaction,
+ });
+
+ return locations;
+ }
+
+ 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 locationsData = data.map((item, index) => ({
+ id: item.id || undefined,
+
+ name: item.name || null,
+ importHash: item.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ createdAt: new Date(Date.now() + index * 1000),
+ }));
+
+ // Bulk create items
+ const locations = await db.locations.bulkCreate(locationsData, {
+ transaction,
+ });
+
+ // For each item created, replace relation files
+
+ return locations;
+ }
+
+ static async update(id, data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const locations = await db.locations.findByPk(id, {}, { transaction });
+
+ const updatePayload = {};
+
+ if (data.name !== undefined) updatePayload.name = data.name;
+
+ updatePayload.updatedById = currentUser.id;
+
+ await locations.update(updatePayload, { transaction });
+
+ if (data.user !== undefined) {
+ await locations.setUser(
+ data.user,
+
+ { transaction },
+ );
+ }
+
+ return locations;
+ }
+
+ static async deleteByIds(ids, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const locations = await db.locations.findAll({
+ where: {
+ id: {
+ [Op.in]: ids,
+ },
+ },
+ transaction,
+ });
+
+ await db.sequelize.transaction(async (transaction) => {
+ for (const record of locations) {
+ await record.update({ deletedBy: currentUser.id }, { transaction });
+ }
+ for (const record of locations) {
+ await record.destroy({ transaction });
+ }
+ });
+
+ return locations;
+ }
+
+ static async remove(id, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const locations = await db.locations.findByPk(id, options);
+
+ await locations.update(
+ {
+ deletedBy: currentUser.id,
+ },
+ {
+ transaction,
+ },
+ );
+
+ await locations.destroy({
+ transaction,
+ });
+
+ return locations;
+ }
+
+ static async findBy(where, options) {
+ const transaction = (options && options.transaction) || undefined;
+
+ const locations = await db.locations.findOne({ where }, { transaction });
+
+ if (!locations) {
+ return locations;
+ }
+
+ const output = locations.get({ plain: true });
+
+ output.bottles_location = await locations.getBottles_location({
+ transaction,
+ });
+
+ output.user = await locations.getUser({
+ transaction,
+ });
+
+ return output;
+ }
+
+ static async findAll(filter, options) {
+ const limit = filter.limit || 0;
+ let offset = 0;
+ let where = {};
+ const currentPage = +filter.page;
+
+ offset = currentPage * limit;
+
+ const orderBy = null;
+
+ const transaction = (options && options.transaction) || undefined;
+
+ let include = [
+ {
+ model: db.users,
+ as: 'user',
+
+ where: filter.user
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.user
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ firstName: {
+ [Op.or]: filter.user
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+ ];
+
+ if (filter) {
+ if (filter.id) {
+ where = {
+ ...where,
+ ['id']: Utils.uuid(filter.id),
+ };
+ }
+
+ if (filter.name) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('locations', 'name', filter.name),
+ };
+ }
+
+ if (filter.active !== undefined) {
+ where = {
+ ...where,
+ active: filter.active === true || filter.active === 'true',
+ };
+ }
+
+ if (filter.createdAtRange) {
+ const [start, end] = filter.createdAtRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+ }
+
+ const queryOptions = {
+ where,
+ include,
+ distinct: true,
+ order:
+ filter.field && filter.sort
+ ? [[filter.field, filter.sort]]
+ : [['createdAt', 'desc']],
+ transaction: options?.transaction,
+ logging: console.log,
+ };
+
+ if (!options?.countOnly) {
+ queryOptions.limit = limit ? Number(limit) : undefined;
+ queryOptions.offset = offset ? Number(offset) : undefined;
+ }
+
+ try {
+ const { rows, count } = await db.locations.findAndCountAll(queryOptions);
+
+ return {
+ rows: options?.countOnly ? [] : rows,
+ count: count,
+ };
+ } catch (error) {
+ console.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ static async findAllAutocomplete(query, limit, offset) {
+ let where = {};
+
+ if (query) {
+ where = {
+ [Op.or]: [
+ { ['id']: Utils.uuid(query) },
+ Utils.ilike('locations', 'name', query),
+ ],
+ };
+ }
+
+ const records = await db.locations.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/messages.js b/backend/src/db/api/messages.js
new file mode 100644
index 0000000..cc04107
--- /dev/null
+++ b/backend/src/db/api/messages.js
@@ -0,0 +1,321 @@
+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 MessagesDBApi {
+ static async create(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const messages = await db.messages.create(
+ {
+ id: data.id || undefined,
+
+ importHash: data.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ },
+ { transaction },
+ );
+
+ await messages.setConversation(data.conversation || null, {
+ transaction,
+ });
+
+ await messages.setSender(data.sender || null, {
+ transaction,
+ });
+
+ return messages;
+ }
+
+ 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 messagesData = data.map((item, index) => ({
+ id: item.id || undefined,
+
+ importHash: item.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ createdAt: new Date(Date.now() + index * 1000),
+ }));
+
+ // Bulk create items
+ const messages = await db.messages.bulkCreate(messagesData, {
+ transaction,
+ });
+
+ // For each item created, replace relation files
+
+ return messages;
+ }
+
+ static async update(id, data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const messages = await db.messages.findByPk(id, {}, { transaction });
+
+ const updatePayload = {};
+
+ updatePayload.updatedById = currentUser.id;
+
+ await messages.update(updatePayload, { transaction });
+
+ if (data.conversation !== undefined) {
+ await messages.setConversation(
+ data.conversation,
+
+ { transaction },
+ );
+ }
+
+ if (data.sender !== undefined) {
+ await messages.setSender(
+ data.sender,
+
+ { transaction },
+ );
+ }
+
+ return messages;
+ }
+
+ static async deleteByIds(ids, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const messages = await db.messages.findAll({
+ where: {
+ id: {
+ [Op.in]: ids,
+ },
+ },
+ transaction,
+ });
+
+ await db.sequelize.transaction(async (transaction) => {
+ for (const record of messages) {
+ await record.update({ deletedBy: currentUser.id }, { transaction });
+ }
+ for (const record of messages) {
+ await record.destroy({ transaction });
+ }
+ });
+
+ return messages;
+ }
+
+ static async remove(id, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const messages = await db.messages.findByPk(id, options);
+
+ await messages.update(
+ {
+ deletedBy: currentUser.id,
+ },
+ {
+ transaction,
+ },
+ );
+
+ await messages.destroy({
+ transaction,
+ });
+
+ return messages;
+ }
+
+ static async findBy(where, options) {
+ const transaction = (options && options.transaction) || undefined;
+
+ const messages = await db.messages.findOne({ where }, { transaction });
+
+ if (!messages) {
+ return messages;
+ }
+
+ const output = messages.get({ plain: true });
+
+ output.conversation = await messages.getConversation({
+ transaction,
+ });
+
+ output.sender = await messages.getSender({
+ transaction,
+ });
+
+ return output;
+ }
+
+ static async findAll(filter, options) {
+ const limit = filter.limit || 0;
+ let offset = 0;
+ let where = {};
+ const currentPage = +filter.page;
+
+ offset = currentPage * limit;
+
+ const orderBy = null;
+
+ const transaction = (options && options.transaction) || undefined;
+
+ let include = [
+ {
+ model: db.conversations,
+ as: 'conversation',
+
+ where: filter.conversation
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.conversation
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ id: {
+ [Op.or]: filter.conversation
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
+ {
+ model: db.users,
+ as: 'sender',
+
+ where: filter.sender
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.sender
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ firstName: {
+ [Op.or]: filter.sender
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+ ];
+
+ if (filter) {
+ if (filter.id) {
+ where = {
+ ...where,
+ ['id']: Utils.uuid(filter.id),
+ };
+ }
+
+ if (filter.active !== undefined) {
+ where = {
+ ...where,
+ active: filter.active === true || filter.active === 'true',
+ };
+ }
+
+ if (filter.createdAtRange) {
+ const [start, end] = filter.createdAtRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+ }
+
+ const queryOptions = {
+ where,
+ include,
+ distinct: true,
+ order:
+ filter.field && filter.sort
+ ? [[filter.field, filter.sort]]
+ : [['createdAt', 'desc']],
+ transaction: options?.transaction,
+ logging: console.log,
+ };
+
+ if (!options?.countOnly) {
+ queryOptions.limit = limit ? Number(limit) : undefined;
+ queryOptions.offset = offset ? Number(offset) : undefined;
+ }
+
+ try {
+ const { rows, count } = await db.messages.findAndCountAll(queryOptions);
+
+ return {
+ rows: options?.countOnly ? [] : rows,
+ count: count,
+ };
+ } catch (error) {
+ console.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ static async findAllAutocomplete(query, limit, offset) {
+ let where = {};
+
+ if (query) {
+ where = {
+ [Op.or]: [
+ { ['id']: Utils.uuid(query) },
+ Utils.ilike('messages', 'id', query),
+ ],
+ };
+ }
+
+ const records = await db.messages.findAll({
+ attributes: ['id', 'id'],
+ where,
+ limit: limit ? Number(limit) : undefined,
+ offset: offset ? Number(offset) : undefined,
+ orderBy: [['id', 'ASC']],
+ });
+
+ return records.map((record) => ({
+ id: record.id,
+ label: record.id,
+ }));
+ }
+};
diff --git a/backend/src/db/api/photos.js b/backend/src/db/api/photos.js
new file mode 100644
index 0000000..6a5133e
--- /dev/null
+++ b/backend/src/db/api/photos.js
@@ -0,0 +1,303 @@
+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 PhotosDBApi {
+ static async create(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const photos = await db.photos.create(
+ {
+ id: data.id || undefined,
+
+ phototype: data.phototype || null,
+ importHash: data.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ },
+ { transaction },
+ );
+
+ await FileDBApi.replaceRelationFiles(
+ {
+ belongsTo: db.photos.getTableName(),
+ belongsToColumn: 'image',
+ belongsToId: photos.id,
+ },
+ data.image,
+ options,
+ );
+
+ return photos;
+ }
+
+ 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 photosData = data.map((item, index) => ({
+ id: item.id || undefined,
+
+ phototype: item.phototype || null,
+ importHash: item.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ createdAt: new Date(Date.now() + index * 1000),
+ }));
+
+ // Bulk create items
+ const photos = await db.photos.bulkCreate(photosData, { transaction });
+
+ // For each item created, replace relation files
+
+ for (let i = 0; i < photos.length; i++) {
+ await FileDBApi.replaceRelationFiles(
+ {
+ belongsTo: db.photos.getTableName(),
+ belongsToColumn: 'image',
+ belongsToId: photos[i].id,
+ },
+ data[i].image,
+ options,
+ );
+ }
+
+ return photos;
+ }
+
+ static async update(id, data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const photos = await db.photos.findByPk(id, {}, { transaction });
+
+ const updatePayload = {};
+
+ if (data.phototype !== undefined) updatePayload.phototype = data.phototype;
+
+ updatePayload.updatedById = currentUser.id;
+
+ await photos.update(updatePayload, { transaction });
+
+ await FileDBApi.replaceRelationFiles(
+ {
+ belongsTo: db.photos.getTableName(),
+ belongsToColumn: 'image',
+ belongsToId: photos.id,
+ },
+ data.image,
+ options,
+ );
+
+ return photos;
+ }
+
+ static async deleteByIds(ids, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const photos = await db.photos.findAll({
+ where: {
+ id: {
+ [Op.in]: ids,
+ },
+ },
+ transaction,
+ });
+
+ await db.sequelize.transaction(async (transaction) => {
+ for (const record of photos) {
+ await record.update({ deletedBy: currentUser.id }, { transaction });
+ }
+ for (const record of photos) {
+ await record.destroy({ transaction });
+ }
+ });
+
+ return photos;
+ }
+
+ static async remove(id, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const photos = await db.photos.findByPk(id, options);
+
+ await photos.update(
+ {
+ deletedBy: currentUser.id,
+ },
+ {
+ transaction,
+ },
+ );
+
+ await photos.destroy({
+ transaction,
+ });
+
+ return photos;
+ }
+
+ static async findBy(where, options) {
+ const transaction = (options && options.transaction) || undefined;
+
+ const photos = await db.photos.findOne({ where }, { transaction });
+
+ if (!photos) {
+ return photos;
+ }
+
+ const output = photos.get({ plain: true });
+
+ output.products_photofront = await photos.getProducts_photofront({
+ transaction,
+ });
+
+ output.products_photoback = await photos.getProducts_photoback({
+ transaction,
+ });
+
+ output.bottles_photofront = await photos.getBottles_photofront({
+ transaction,
+ });
+
+ output.bottles_photoback = await photos.getBottles_photoback({
+ transaction,
+ });
+
+ output.image = await photos.getImage({
+ transaction,
+ });
+
+ return output;
+ }
+
+ static async findAll(filter, options) {
+ const limit = filter.limit || 0;
+ let offset = 0;
+ let where = {};
+ const currentPage = +filter.page;
+
+ offset = currentPage * limit;
+
+ const orderBy = null;
+
+ const transaction = (options && options.transaction) || undefined;
+
+ let include = [
+ {
+ model: db.file,
+ as: 'image',
+ },
+ ];
+
+ if (filter) {
+ if (filter.id) {
+ where = {
+ ...where,
+ ['id']: Utils.uuid(filter.id),
+ };
+ }
+
+ if (filter.phototype) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('photos', 'phototype', filter.phototype),
+ };
+ }
+
+ if (filter.active !== undefined) {
+ where = {
+ ...where,
+ active: filter.active === true || filter.active === 'true',
+ };
+ }
+
+ if (filter.createdAtRange) {
+ const [start, end] = filter.createdAtRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+ }
+
+ const queryOptions = {
+ where,
+ include,
+ distinct: true,
+ order:
+ filter.field && filter.sort
+ ? [[filter.field, filter.sort]]
+ : [['createdAt', 'desc']],
+ transaction: options?.transaction,
+ logging: console.log,
+ };
+
+ if (!options?.countOnly) {
+ queryOptions.limit = limit ? Number(limit) : undefined;
+ queryOptions.offset = offset ? Number(offset) : undefined;
+ }
+
+ try {
+ const { rows, count } = await db.photos.findAndCountAll(queryOptions);
+
+ return {
+ rows: options?.countOnly ? [] : rows,
+ count: count,
+ };
+ } catch (error) {
+ console.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ static async findAllAutocomplete(query, limit, offset) {
+ let where = {};
+
+ if (query) {
+ where = {
+ [Op.or]: [
+ { ['id']: Utils.uuid(query) },
+ Utils.ilike('photos', 'image', query),
+ ],
+ };
+ }
+
+ const records = await db.photos.findAll({
+ attributes: ['id', 'image'],
+ where,
+ limit: limit ? Number(limit) : undefined,
+ offset: offset ? Number(offset) : undefined,
+ orderBy: [['image', 'ASC']],
+ });
+
+ return records.map((record) => ({
+ id: record.id,
+ label: record.image,
+ }));
+ }
+};
diff --git a/backend/src/db/api/products.js b/backend/src/db/api/products.js
new file mode 100644
index 0000000..51381e7
--- /dev/null
+++ b/backend/src/db/api/products.js
@@ -0,0 +1,484 @@
+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 ProductsDBApi {
+ static async create(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const products = await db.products.create(
+ {
+ id: data.id || undefined,
+
+ name: data.name || null,
+ proof: data.proof || null,
+ age: data.age || null,
+ barcode: data.barcode || null,
+ notes: data.notes || null,
+ status: data.status || null,
+ importHash: data.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ },
+ { transaction },
+ );
+
+ await products.setBrand(data.brand || null, {
+ transaction,
+ });
+
+ await products.setPhotofront(data.photofront || null, {
+ transaction,
+ });
+
+ await products.setPhotoback(data.photoback || null, {
+ transaction,
+ });
+
+ return products;
+ }
+
+ 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 productsData = data.map((item, index) => ({
+ id: item.id || undefined,
+
+ name: item.name || null,
+ proof: item.proof || null,
+ age: item.age || null,
+ barcode: item.barcode || null,
+ notes: item.notes || null,
+ status: item.status || null,
+ importHash: item.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ createdAt: new Date(Date.now() + index * 1000),
+ }));
+
+ // Bulk create items
+ const products = await db.products.bulkCreate(productsData, {
+ transaction,
+ });
+
+ // For each item created, replace relation files
+
+ return products;
+ }
+
+ static async update(id, data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const products = await db.products.findByPk(id, {}, { transaction });
+
+ const updatePayload = {};
+
+ if (data.name !== undefined) updatePayload.name = data.name;
+
+ if (data.proof !== undefined) updatePayload.proof = data.proof;
+
+ if (data.age !== undefined) updatePayload.age = data.age;
+
+ if (data.barcode !== undefined) updatePayload.barcode = data.barcode;
+
+ if (data.notes !== undefined) updatePayload.notes = data.notes;
+
+ if (data.status !== undefined) updatePayload.status = data.status;
+
+ updatePayload.updatedById = currentUser.id;
+
+ await products.update(updatePayload, { transaction });
+
+ if (data.brand !== undefined) {
+ await products.setBrand(
+ data.brand,
+
+ { transaction },
+ );
+ }
+
+ if (data.photofront !== undefined) {
+ await products.setPhotofront(
+ data.photofront,
+
+ { transaction },
+ );
+ }
+
+ if (data.photoback !== undefined) {
+ await products.setPhotoback(
+ data.photoback,
+
+ { transaction },
+ );
+ }
+
+ return products;
+ }
+
+ static async deleteByIds(ids, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const products = await db.products.findAll({
+ where: {
+ id: {
+ [Op.in]: ids,
+ },
+ },
+ transaction,
+ });
+
+ await db.sequelize.transaction(async (transaction) => {
+ for (const record of products) {
+ await record.update({ deletedBy: currentUser.id }, { transaction });
+ }
+ for (const record of products) {
+ await record.destroy({ transaction });
+ }
+ });
+
+ return products;
+ }
+
+ static async remove(id, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const products = await db.products.findByPk(id, options);
+
+ await products.update(
+ {
+ deletedBy: currentUser.id,
+ },
+ {
+ transaction,
+ },
+ );
+
+ await products.destroy({
+ transaction,
+ });
+
+ return products;
+ }
+
+ static async findBy(where, options) {
+ const transaction = (options && options.transaction) || undefined;
+
+ const products = await db.products.findOne({ where }, { transaction });
+
+ if (!products) {
+ return products;
+ }
+
+ const output = products.get({ plain: true });
+
+ output.bottles_product = await products.getBottles_product({
+ transaction,
+ });
+
+ output.brand = await products.getBrand({
+ transaction,
+ });
+
+ output.photofront = await products.getPhotofront({
+ transaction,
+ });
+
+ output.photoback = await products.getPhotoback({
+ transaction,
+ });
+
+ return output;
+ }
+
+ static async findAll(filter, options) {
+ const limit = filter.limit || 0;
+ let offset = 0;
+ let where = {};
+ const currentPage = +filter.page;
+
+ offset = currentPage * limit;
+
+ const orderBy = null;
+
+ const transaction = (options && options.transaction) || undefined;
+
+ let include = [
+ {
+ model: db.brands,
+ as: 'brand',
+
+ where: filter.brand
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.brand
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ name: {
+ [Op.or]: filter.brand
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
+ {
+ model: db.photos,
+ as: 'photofront',
+
+ where: filter.photofront
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.photofront
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ image: {
+ [Op.or]: filter.photofront
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
+ {
+ model: db.photos,
+ as: 'photoback',
+
+ where: filter.photoback
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.photoback
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ image: {
+ [Op.or]: filter.photoback
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+ ];
+
+ if (filter) {
+ if (filter.id) {
+ where = {
+ ...where,
+ ['id']: Utils.uuid(filter.id),
+ };
+ }
+
+ if (filter.name) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('products', 'name', filter.name),
+ };
+ }
+
+ if (filter.barcode) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('products', 'barcode', filter.barcode),
+ };
+ }
+
+ if (filter.notes) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('products', 'notes', filter.notes),
+ };
+ }
+
+ if (filter.proofRange) {
+ const [start, end] = filter.proofRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ proof: {
+ ...where.proof,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ proof: {
+ ...where.proof,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
+ if (filter.ageRange) {
+ const [start, end] = filter.ageRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ age: {
+ ...where.age,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ age: {
+ ...where.age,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
+ if (filter.statusRange) {
+ const [start, end] = filter.statusRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ status: {
+ ...where.status,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ status: {
+ ...where.status,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
+ if (filter.active !== undefined) {
+ where = {
+ ...where,
+ active: filter.active === true || filter.active === 'true',
+ };
+ }
+
+ if (filter.createdAtRange) {
+ const [start, end] = filter.createdAtRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+ }
+
+ const queryOptions = {
+ where,
+ include,
+ distinct: true,
+ order:
+ filter.field && filter.sort
+ ? [[filter.field, filter.sort]]
+ : [['createdAt', 'desc']],
+ transaction: options?.transaction,
+ logging: console.log,
+ };
+
+ if (!options?.countOnly) {
+ queryOptions.limit = limit ? Number(limit) : undefined;
+ queryOptions.offset = offset ? Number(offset) : undefined;
+ }
+
+ try {
+ const { rows, count } = await db.products.findAndCountAll(queryOptions);
+
+ return {
+ rows: options?.countOnly ? [] : rows,
+ count: count,
+ };
+ } catch (error) {
+ console.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ static async findAllAutocomplete(query, limit, offset) {
+ let where = {};
+
+ if (query) {
+ where = {
+ [Op.or]: [
+ { ['id']: Utils.uuid(query) },
+ Utils.ilike('products', 'name', query),
+ ],
+ };
+ }
+
+ const records = await db.products.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/reviews.js b/backend/src/db/api/reviews.js
new file mode 100644
index 0000000..109957f
--- /dev/null
+++ b/backend/src/db/api/reviews.js
@@ -0,0 +1,386 @@
+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 ReviewsDBApi {
+ static async create(data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const reviews = await db.reviews.create(
+ {
+ id: data.id || undefined,
+
+ rating: data.rating || null,
+ notes: data.notes || null,
+ createdat: data.createdat || null,
+ importHash: data.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ },
+ { transaction },
+ );
+
+ await reviews.setUser(data.user || null, {
+ transaction,
+ });
+
+ await reviews.setBottle(data.bottle || null, {
+ transaction,
+ });
+
+ return reviews;
+ }
+
+ 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 reviewsData = data.map((item, index) => ({
+ id: item.id || undefined,
+
+ rating: item.rating || null,
+ notes: item.notes || null,
+ createdat: item.createdat || null,
+ importHash: item.importHash || null,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ createdAt: new Date(Date.now() + index * 1000),
+ }));
+
+ // Bulk create items
+ const reviews = await db.reviews.bulkCreate(reviewsData, { transaction });
+
+ // For each item created, replace relation files
+
+ return reviews;
+ }
+
+ static async update(id, data, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const reviews = await db.reviews.findByPk(id, {}, { transaction });
+
+ const updatePayload = {};
+
+ if (data.rating !== undefined) updatePayload.rating = data.rating;
+
+ if (data.notes !== undefined) updatePayload.notes = data.notes;
+
+ if (data.createdat !== undefined) updatePayload.createdat = data.createdat;
+
+ updatePayload.updatedById = currentUser.id;
+
+ await reviews.update(updatePayload, { transaction });
+
+ if (data.user !== undefined) {
+ await reviews.setUser(
+ data.user,
+
+ { transaction },
+ );
+ }
+
+ if (data.bottle !== undefined) {
+ await reviews.setBottle(
+ data.bottle,
+
+ { transaction },
+ );
+ }
+
+ return reviews;
+ }
+
+ static async deleteByIds(ids, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const reviews = await db.reviews.findAll({
+ where: {
+ id: {
+ [Op.in]: ids,
+ },
+ },
+ transaction,
+ });
+
+ await db.sequelize.transaction(async (transaction) => {
+ for (const record of reviews) {
+ await record.update({ deletedBy: currentUser.id }, { transaction });
+ }
+ for (const record of reviews) {
+ await record.destroy({ transaction });
+ }
+ });
+
+ return reviews;
+ }
+
+ static async remove(id, options) {
+ const currentUser = (options && options.currentUser) || { id: null };
+ const transaction = (options && options.transaction) || undefined;
+
+ const reviews = await db.reviews.findByPk(id, options);
+
+ await reviews.update(
+ {
+ deletedBy: currentUser.id,
+ },
+ {
+ transaction,
+ },
+ );
+
+ await reviews.destroy({
+ transaction,
+ });
+
+ return reviews;
+ }
+
+ static async findBy(where, options) {
+ const transaction = (options && options.transaction) || undefined;
+
+ const reviews = await db.reviews.findOne({ where }, { transaction });
+
+ if (!reviews) {
+ return reviews;
+ }
+
+ const output = reviews.get({ plain: true });
+
+ output.user = await reviews.getUser({
+ transaction,
+ });
+
+ output.bottle = await reviews.getBottle({
+ transaction,
+ });
+
+ return output;
+ }
+
+ static async findAll(filter, options) {
+ const limit = filter.limit || 0;
+ let offset = 0;
+ let where = {};
+ const currentPage = +filter.page;
+
+ offset = currentPage * limit;
+
+ const orderBy = null;
+
+ const transaction = (options && options.transaction) || undefined;
+
+ let include = [
+ {
+ model: db.users,
+ as: 'user',
+
+ where: filter.user
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.user
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ firstName: {
+ [Op.or]: filter.user
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+
+ {
+ model: db.bottles,
+ as: 'bottle',
+
+ where: filter.bottle
+ ? {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: filter.bottle
+ .split('|')
+ .map((term) => Utils.uuid(term)),
+ },
+ },
+ {
+ id: {
+ [Op.or]: filter.bottle
+ .split('|')
+ .map((term) => ({ [Op.iLike]: `%${term}%` })),
+ },
+ },
+ ],
+ }
+ : {},
+ },
+ ];
+
+ if (filter) {
+ if (filter.id) {
+ where = {
+ ...where,
+ ['id']: Utils.uuid(filter.id),
+ };
+ }
+
+ if (filter.notes) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('reviews', 'notes', filter.notes),
+ };
+ }
+
+ if (filter.ratingRange) {
+ const [start, end] = filter.ratingRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ rating: {
+ ...where.rating,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ rating: {
+ ...where.rating,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+
+ 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 (filter.active !== undefined) {
+ where = {
+ ...where,
+ active: filter.active === true || filter.active === 'true',
+ };
+ }
+
+ if (filter.createdAtRange) {
+ const [start, end] = filter.createdAtRange;
+
+ if (start !== undefined && start !== null && start !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.gte]: start,
+ },
+ };
+ }
+
+ if (end !== undefined && end !== null && end !== '') {
+ where = {
+ ...where,
+ ['createdAt']: {
+ ...where.createdAt,
+ [Op.lte]: end,
+ },
+ };
+ }
+ }
+ }
+
+ const queryOptions = {
+ where,
+ include,
+ distinct: true,
+ order:
+ filter.field && filter.sort
+ ? [[filter.field, filter.sort]]
+ : [['createdAt', 'desc']],
+ transaction: options?.transaction,
+ logging: console.log,
+ };
+
+ if (!options?.countOnly) {
+ queryOptions.limit = limit ? Number(limit) : undefined;
+ queryOptions.offset = offset ? Number(offset) : undefined;
+ }
+
+ try {
+ const { rows, count } = await db.reviews.findAndCountAll(queryOptions);
+
+ return {
+ rows: options?.countOnly ? [] : rows,
+ count: count,
+ };
+ } catch (error) {
+ console.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ static async findAllAutocomplete(query, limit, offset) {
+ let where = {};
+
+ if (query) {
+ where = {
+ [Op.or]: [
+ { ['id']: Utils.uuid(query) },
+ Utils.ilike('reviews', 'id', query),
+ ],
+ };
+ }
+
+ const records = await db.reviews.findAll({
+ attributes: ['id', 'id'],
+ where,
+ limit: limit ? Number(limit) : undefined,
+ offset: offset ? Number(offset) : undefined,
+ orderBy: [['id', 'ASC']],
+ });
+
+ return records.map((record) => ({
+ id: record.id,
+ label: record.id,
+ }));
+ }
+};
diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js
index 45e0429..f2abbd8 100644
--- a/backend/src/db/api/users.js
+++ b/backend/src/db/api/users.js
@@ -34,6 +34,8 @@ module.exports = class UsersDBApi {
passwordResetTokenExpiresAt:
data.data.passwordResetTokenExpiresAt || null,
provider: data.data.provider || null,
+ address: data.data.address || null,
+ address2: data.data.address2 || null,
importHash: data.data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -96,6 +98,8 @@ module.exports = class UsersDBApi {
passwordResetToken: item.passwordResetToken || null,
passwordResetTokenExpiresAt: item.passwordResetTokenExpiresAt || null,
provider: item.provider || null,
+ address: item.address || null,
+ address2: item.address2 || null,
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@@ -178,6 +182,10 @@ module.exports = class UsersDBApi {
if (data.provider !== undefined) updatePayload.provider = data.provider;
+ if (data.address !== undefined) updatePayload.address = data.address;
+
+ if (data.address2 !== undefined) updatePayload.address2 = data.address2;
+
updatePayload.updatedById = currentUser.id;
await users.update(updatePayload, { transaction });
@@ -267,10 +275,27 @@ module.exports = class UsersDBApi {
const output = users.get({ plain: true });
+ output.locations_user = await users.getLocations_user({
+ transaction,
+ });
+
output.bottles_user = await users.getBottles_user({
transaction,
});
+ output.reviews_user = await users.getReviews_user({
+ transaction,
+ });
+
+ output.messages_sender = await users.getMessages_sender({
+ transaction,
+ });
+
+ output.conversationparticipants_user =
+ await users.getConversationparticipants_user({
+ transaction,
+ });
+
output.avatar = await users.getAvatar({
transaction,
});
@@ -415,6 +440,20 @@ module.exports = class UsersDBApi {
};
}
+ if (filter.address) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('users', 'address', filter.address),
+ };
+ }
+
+ if (filter.address2) {
+ where = {
+ ...where,
+ [Op.and]: Utils.ilike('users', 'address2', filter.address2),
+ };
+ }
+
if (filter.emailVerificationTokenExpiresAtRange) {
const [start, end] = filter.emailVerificationTokenExpiresAtRange;
diff --git a/backend/src/db/migrations/1756232919789.js b/backend/src/db/migrations/1756232919789.js
new file mode 100644
index 0000000..f06dac3
--- /dev/null
+++ b/backend/src/db/migrations/1756232919789.js
@@ -0,0 +1,72 @@
+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.dropTable('bottles', { 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.createTable(
+ 'bottles',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756232938531.js b/backend/src/db/migrations/1756232938531.js
new file mode 100644
index 0000000..63abd52
--- /dev/null
+++ b/backend/src/db/migrations/1756232938531.js
@@ -0,0 +1,72 @@
+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.dropTable('brands', { 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.createTable(
+ 'brands',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756232961734.js b/backend/src/db/migrations/1756232961734.js
new file mode 100644
index 0000000..e150bf9
--- /dev/null
+++ b/backend/src/db/migrations/1756232961734.js
@@ -0,0 +1,72 @@
+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.dropTable('distilleries', { 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.createTable(
+ 'distilleries',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756232993973.js b/backend/src/db/migrations/1756232993973.js
new file mode 100644
index 0000000..562b081
--- /dev/null
+++ b/backend/src/db/migrations/1756232993973.js
@@ -0,0 +1,72 @@
+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(
+ 'distilleries',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('distilleries', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233026221.js b/backend/src/db/migrations/1756233026221.js
new file mode 100644
index 0000000..0a363c4
--- /dev/null
+++ b/backend/src/db/migrations/1756233026221.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(
+ 'distilleries',
+ '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('distilleries', 'name', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233047456.js b/backend/src/db/migrations/1756233047456.js
new file mode 100644
index 0000000..e9567fd
--- /dev/null
+++ b/backend/src/db/migrations/1756233047456.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(
+ 'distilleries',
+ 'city',
+ {
+ 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('distilleries', 'city', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233083420.js b/backend/src/db/migrations/1756233083420.js
new file mode 100644
index 0000000..e4bb435
--- /dev/null
+++ b/backend/src/db/migrations/1756233083420.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(
+ 'distilleries',
+ 'state',
+ {
+ 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('distilleries', 'state', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233114262.js b/backend/src/db/migrations/1756233114262.js
new file mode 100644
index 0000000..82251fc
--- /dev/null
+++ b/backend/src/db/migrations/1756233114262.js
@@ -0,0 +1,72 @@
+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(
+ 'brands',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('brands', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233134331.js b/backend/src/db/migrations/1756233134331.js
new file mode 100644
index 0000000..061ee56
--- /dev/null
+++ b/backend/src/db/migrations/1756233134331.js
@@ -0,0 +1,47 @@
+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(
+ 'brands',
+ '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('brands', 'name', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233161698.js b/backend/src/db/migrations/1756233161698.js
new file mode 100644
index 0000000..769c7d5
--- /dev/null
+++ b/backend/src/db/migrations/1756233161698.js
@@ -0,0 +1,72 @@
+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(
+ 'photos',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('photos', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233186114.js b/backend/src/db/migrations/1756233186114.js
new file mode 100644
index 0000000..80b2ebb
--- /dev/null
+++ b/backend/src/db/migrations/1756233186114.js
@@ -0,0 +1,47 @@
+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(
+ 'photos',
+ 'phototype',
+ {
+ 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('photos', 'phototype', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233219308.js b/backend/src/db/migrations/1756233219308.js
new file mode 100644
index 0000000..e6bfba3
--- /dev/null
+++ b/backend/src/db/migrations/1756233219308.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/1756233254738.js b/backend/src/db/migrations/1756233254738.js
new file mode 100644
index 0000000..701dadb
--- /dev/null
+++ b/backend/src/db/migrations/1756233254738.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(
+ 'distilleries',
+ 'status',
+ {
+ 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('distilleries', 'status', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233282118.js b/backend/src/db/migrations/1756233282118.js
new file mode 100644
index 0000000..2915c22
--- /dev/null
+++ b/backend/src/db/migrations/1756233282118.js
@@ -0,0 +1,72 @@
+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(
+ 'products',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('products', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233314139.js b/backend/src/db/migrations/1756233314139.js
new file mode 100644
index 0000000..e361ebd
--- /dev/null
+++ b/backend/src/db/migrations/1756233314139.js
@@ -0,0 +1,47 @@
+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(
+ 'users',
+ 'address',
+ {
+ type: Sequelize.DataTypes.TEXT,
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.removeColumn('users', 'address', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233343308.js b/backend/src/db/migrations/1756233343308.js
new file mode 100644
index 0000000..3fa32ef
--- /dev/null
+++ b/backend/src/db/migrations/1756233343308.js
@@ -0,0 +1,47 @@
+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(
+ 'users',
+ 'address2',
+ {
+ 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('users', 'address2', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233376774.js b/backend/src/db/migrations/1756233376774.js
new file mode 100644
index 0000000..586ae85
--- /dev/null
+++ b/backend/src/db/migrations/1756233376774.js
@@ -0,0 +1,52 @@
+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(
+ 'products',
+ 'brandId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'brands',
+ 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('products', 'brandId', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233417905.js b/backend/src/db/migrations/1756233417905.js
new file mode 100644
index 0000000..ca81d21
--- /dev/null
+++ b/backend/src/db/migrations/1756233417905.js
@@ -0,0 +1,47 @@
+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(
+ 'brands',
+ 'status',
+ {
+ 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('brands', 'status', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233460513.js b/backend/src/db/migrations/1756233460513.js
new file mode 100644
index 0000000..eabce8c
--- /dev/null
+++ b/backend/src/db/migrations/1756233460513.js
@@ -0,0 +1,72 @@
+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(
+ 'locations',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('locations', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233493388.js b/backend/src/db/migrations/1756233493388.js
new file mode 100644
index 0000000..82ea551
--- /dev/null
+++ b/backend/src/db/migrations/1756233493388.js
@@ -0,0 +1,47 @@
+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(
+ 'locations',
+ '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('locations', 'name', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233535083.js b/backend/src/db/migrations/1756233535083.js
new file mode 100644
index 0000000..40c0006
--- /dev/null
+++ b/backend/src/db/migrations/1756233535083.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(
+ 'brands',
+ 'distilleryId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'distilleries',
+ 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('brands', 'distilleryId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233583823.js b/backend/src/db/migrations/1756233583823.js
new file mode 100644
index 0000000..3c339da
--- /dev/null
+++ b/backend/src/db/migrations/1756233583823.js
@@ -0,0 +1,47 @@
+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(
+ 'products',
+ '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('products', 'name', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233847816.js b/backend/src/db/migrations/1756233847816.js
new file mode 100644
index 0000000..c235ffc
--- /dev/null
+++ b/backend/src/db/migrations/1756233847816.js
@@ -0,0 +1,47 @@
+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(
+ 'products',
+ 'proof',
+ {
+ 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('products', 'proof', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233872842.js b/backend/src/db/migrations/1756233872842.js
new file mode 100644
index 0000000..0720527
--- /dev/null
+++ b/backend/src/db/migrations/1756233872842.js
@@ -0,0 +1,47 @@
+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(
+ 'products',
+ 'age',
+ {
+ 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('products', 'age', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233895732.js b/backend/src/db/migrations/1756233895732.js
new file mode 100644
index 0000000..5f1e108
--- /dev/null
+++ b/backend/src/db/migrations/1756233895732.js
@@ -0,0 +1,47 @@
+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(
+ 'products',
+ 'barcode',
+ {
+ 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('products', 'barcode', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233923545.js b/backend/src/db/migrations/1756233923545.js
new file mode 100644
index 0000000..883c0c2
--- /dev/null
+++ b/backend/src/db/migrations/1756233923545.js
@@ -0,0 +1,47 @@
+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(
+ 'products',
+ 'notes',
+ {
+ 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('products', 'notes', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233946141.js b/backend/src/db/migrations/1756233946141.js
new file mode 100644
index 0000000..13a8f82
--- /dev/null
+++ b/backend/src/db/migrations/1756233946141.js
@@ -0,0 +1,47 @@
+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(
+ 'products',
+ 'status',
+ {
+ 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('products', 'status', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233969555.js b/backend/src/db/migrations/1756233969555.js
new file mode 100644
index 0000000..4232aa9
--- /dev/null
+++ b/backend/src/db/migrations/1756233969555.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(
+ 'products',
+ 'photofrontId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'photos',
+ 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('products', 'photofrontId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756233991015.js b/backend/src/db/migrations/1756233991015.js
new file mode 100644
index 0000000..82cd1bf
--- /dev/null
+++ b/backend/src/db/migrations/1756233991015.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(
+ 'products',
+ 'photobackId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'photos',
+ 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('products', 'photobackId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234013059.js b/backend/src/db/migrations/1756234013059.js
new file mode 100644
index 0000000..f821a9e
--- /dev/null
+++ b/backend/src/db/migrations/1756234013059.js
@@ -0,0 +1,52 @@
+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(
+ 'locations',
+ 'userId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'users',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.removeColumn('locations', 'userId', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234037032.js b/backend/src/db/migrations/1756234037032.js
new file mode 100644
index 0000000..666e9b7
--- /dev/null
+++ b/backend/src/db/migrations/1756234037032.js
@@ -0,0 +1,72 @@
+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(
+ 'bottles',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('bottles', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234060391.js b/backend/src/db/migrations/1756234060391.js
new file mode 100644
index 0000000..e97bc9a
--- /dev/null
+++ b/backend/src/db/migrations/1756234060391.js
@@ -0,0 +1,52 @@
+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(
+ 'bottles',
+ 'userId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'users',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.removeColumn('bottles', 'userId', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234087868.js b/backend/src/db/migrations/1756234087868.js
new file mode 100644
index 0000000..d96e2ec
--- /dev/null
+++ b/backend/src/db/migrations/1756234087868.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(
+ 'bottles',
+ 'productId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'products',
+ 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('bottles', 'productId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234116962.js b/backend/src/db/migrations/1756234116962.js
new file mode 100644
index 0000000..9997349
--- /dev/null
+++ b/backend/src/db/migrations/1756234116962.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(
+ 'bottles',
+ 'locationId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'locations',
+ 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('bottles', 'locationId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234140311.js b/backend/src/db/migrations/1756234140311.js
new file mode 100644
index 0000000..6a9e4a0
--- /dev/null
+++ b/backend/src/db/migrations/1756234140311.js
@@ -0,0 +1,47 @@
+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(
+ 'bottles',
+ 'proof',
+ {
+ 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('bottles', 'proof', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234165049.js b/backend/src/db/migrations/1756234165049.js
new file mode 100644
index 0000000..658fb3e
--- /dev/null
+++ b/backend/src/db/migrations/1756234165049.js
@@ -0,0 +1,47 @@
+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(
+ 'bottles',
+ 'age',
+ {
+ 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('bottles', 'age', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234187286.js b/backend/src/db/migrations/1756234187286.js
new file mode 100644
index 0000000..662ea89
--- /dev/null
+++ b/backend/src/db/migrations/1756234187286.js
@@ -0,0 +1,47 @@
+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(
+ 'bottles',
+ 'rating',
+ {
+ 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('bottles', 'rating', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234208774.js b/backend/src/db/migrations/1756234208774.js
new file mode 100644
index 0000000..865c06d
--- /dev/null
+++ b/backend/src/db/migrations/1756234208774.js
@@ -0,0 +1,52 @@
+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(
+ 'bottles',
+ 'collectable',
+ {
+ type: Sequelize.DataTypes.BOOLEAN,
+
+ defaultValue: false,
+ allowNull: false,
+ },
+ { 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('bottles', 'collectable', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234237031.js b/backend/src/db/migrations/1756234237031.js
new file mode 100644
index 0000000..c50a3c5
--- /dev/null
+++ b/backend/src/db/migrations/1756234237031.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(
+ 'bottles',
+ 'rickhouse',
+ {
+ 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('bottles', 'rickhouse', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234265034.js b/backend/src/db/migrations/1756234265034.js
new file mode 100644
index 0000000..9b5b449
--- /dev/null
+++ b/backend/src/db/migrations/1756234265034.js
@@ -0,0 +1,47 @@
+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(
+ 'bottles',
+ 'rack',
+ {
+ 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('bottles', 'rack', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234286983.js b/backend/src/db/migrations/1756234286983.js
new file mode 100644
index 0000000..5562351
--- /dev/null
+++ b/backend/src/db/migrations/1756234286983.js
@@ -0,0 +1,47 @@
+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(
+ 'bottles',
+ 'release',
+ {
+ 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('bottles', 'release', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234308313.js b/backend/src/db/migrations/1756234308313.js
new file mode 100644
index 0000000..b8f7114
--- /dev/null
+++ b/backend/src/db/migrations/1756234308313.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(
+ 'bottles',
+ 'barrelnumber',
+ {
+ 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('bottles', 'barrelnumber', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234331914.js b/backend/src/db/migrations/1756234331914.js
new file mode 100644
index 0000000..d6aa1e3
--- /dev/null
+++ b/backend/src/db/migrations/1756234331914.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(
+ 'bottles',
+ 'barreleddate',
+ {
+ type: Sequelize.DataTypes.DATEONLY,
+ },
+ { 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('bottles', 'barreleddate', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234359186.js b/backend/src/db/migrations/1756234359186.js
new file mode 100644
index 0000000..0639bf1
--- /dev/null
+++ b/backend/src/db/migrations/1756234359186.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(
+ 'bottles',
+ 'bottlenumber',
+ {
+ 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('bottles', 'bottlenumber', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234382031.js b/backend/src/db/migrations/1756234382031.js
new file mode 100644
index 0000000..9f99184
--- /dev/null
+++ b/backend/src/db/migrations/1756234382031.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(
+ 'bottles',
+ 'dateacquired',
+ {
+ type: Sequelize.DataTypes.DATEONLY,
+ },
+ { 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('bottles', 'dateacquired', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234410684.js b/backend/src/db/migrations/1756234410684.js
new file mode 100644
index 0000000..d124d78
--- /dev/null
+++ b/backend/src/db/migrations/1756234410684.js
@@ -0,0 +1,47 @@
+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(
+ 'bottles',
+ 'volume',
+ {
+ 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('bottles', 'volume', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234436241.js b/backend/src/db/migrations/1756234436241.js
new file mode 100644
index 0000000..05a3d47
--- /dev/null
+++ b/backend/src/db/migrations/1756234436241.js
@@ -0,0 +1,47 @@
+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(
+ 'bottles',
+ 'notes',
+ {
+ 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('bottles', 'notes', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234471854.js b/backend/src/db/migrations/1756234471854.js
new file mode 100644
index 0000000..05a6e21
--- /dev/null
+++ b/backend/src/db/migrations/1756234471854.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(
+ 'bottles',
+ 'photofrontId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'photos',
+ 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('bottles', 'photofrontId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234493098.js b/backend/src/db/migrations/1756234493098.js
new file mode 100644
index 0000000..77e6572
--- /dev/null
+++ b/backend/src/db/migrations/1756234493098.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(
+ 'bottles',
+ 'photobackId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'photos',
+ 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('bottles', 'photobackId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234515450.js b/backend/src/db/migrations/1756234515450.js
new file mode 100644
index 0000000..5cac6c2
--- /dev/null
+++ b/backend/src/db/migrations/1756234515450.js
@@ -0,0 +1,72 @@
+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(
+ 'reviews',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('reviews', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234537835.js b/backend/src/db/migrations/1756234537835.js
new file mode 100644
index 0000000..5a61b7b
--- /dev/null
+++ b/backend/src/db/migrations/1756234537835.js
@@ -0,0 +1,52 @@
+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(
+ 'reviews',
+ 'userId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'users',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.removeColumn('reviews', 'userId', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234563408.js b/backend/src/db/migrations/1756234563408.js
new file mode 100644
index 0000000..70f5ab7
--- /dev/null
+++ b/backend/src/db/migrations/1756234563408.js
@@ -0,0 +1,52 @@
+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(
+ 'reviews',
+ 'bottleId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'bottles',
+ 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('reviews', 'bottleId', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234596232.js b/backend/src/db/migrations/1756234596232.js
new file mode 100644
index 0000000..30dea79
--- /dev/null
+++ b/backend/src/db/migrations/1756234596232.js
@@ -0,0 +1,47 @@
+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(
+ 'reviews',
+ 'rating',
+ {
+ 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('reviews', 'rating', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234618834.js b/backend/src/db/migrations/1756234618834.js
new file mode 100644
index 0000000..e5e9102
--- /dev/null
+++ b/backend/src/db/migrations/1756234618834.js
@@ -0,0 +1,47 @@
+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(
+ 'reviews',
+ 'notes',
+ {
+ 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('reviews', 'notes', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234642956.js b/backend/src/db/migrations/1756234642956.js
new file mode 100644
index 0000000..223b667
--- /dev/null
+++ b/backend/src/db/migrations/1756234642956.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(
+ 'reviews',
+ 'createdat',
+ {
+ 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('reviews', 'createdat', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234669525.js b/backend/src/db/migrations/1756234669525.js
new file mode 100644
index 0000000..5c4ab61
--- /dev/null
+++ b/backend/src/db/migrations/1756234669525.js
@@ -0,0 +1,72 @@
+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(
+ 'conversations',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('conversations', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234695418.js b/backend/src/db/migrations/1756234695418.js
new file mode 100644
index 0000000..12f4d29
--- /dev/null
+++ b/backend/src/db/migrations/1756234695418.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(
+ 'conversations',
+ 'createdat',
+ {
+ 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('conversations', 'createdat', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234721487.js b/backend/src/db/migrations/1756234721487.js
new file mode 100644
index 0000000..85fbc72
--- /dev/null
+++ b/backend/src/db/migrations/1756234721487.js
@@ -0,0 +1,72 @@
+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(
+ 'messages',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('messages', { transaction });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234750821.js b/backend/src/db/migrations/1756234750821.js
new file mode 100644
index 0000000..11ea395
--- /dev/null
+++ b/backend/src/db/migrations/1756234750821.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(
+ 'messages',
+ 'conversationId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'conversations',
+ 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('messages', 'conversationId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234775299.js b/backend/src/db/migrations/1756234775299.js
new file mode 100644
index 0000000..dfb96d7
--- /dev/null
+++ b/backend/src/db/migrations/1756234775299.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(
+ 'messages',
+ 'senderId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'users',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.removeColumn('messages', 'senderId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234799569.js b/backend/src/db/migrations/1756234799569.js
new file mode 100644
index 0000000..bbe7b2e
--- /dev/null
+++ b/backend/src/db/migrations/1756234799569.js
@@ -0,0 +1,74 @@
+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(
+ 'conversationparticipants',
+ {
+ id: {
+ type: Sequelize.DataTypes.UUID,
+ defaultValue: Sequelize.DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+ createdById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ updatedById: {
+ type: Sequelize.DataTypes.UUID,
+ references: {
+ key: 'id',
+ model: 'users',
+ },
+ },
+ createdAt: { type: Sequelize.DataTypes.DATE },
+ updatedAt: { type: Sequelize.DataTypes.DATE },
+ deletedAt: { type: Sequelize.DataTypes.DATE },
+ importHash: {
+ type: Sequelize.DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.dropTable('conversationparticipants', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234822685.js b/backend/src/db/migrations/1756234822685.js
new file mode 100644
index 0000000..1fef055
--- /dev/null
+++ b/backend/src/db/migrations/1756234822685.js
@@ -0,0 +1,56 @@
+module.exports = {
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async up(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.addColumn(
+ 'conversationparticipants',
+ 'conversationId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'conversations',
+ 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(
+ 'conversationparticipants',
+ 'conversationId',
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/migrations/1756234849724.js b/backend/src/db/migrations/1756234849724.js
new file mode 100644
index 0000000..d47441a
--- /dev/null
+++ b/backend/src/db/migrations/1756234849724.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(
+ 'conversationparticipants',
+ 'userId',
+ {
+ type: Sequelize.DataTypes.UUID,
+
+ references: {
+ model: 'users',
+ key: 'id',
+ },
+ },
+ { transaction },
+ );
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+ /**
+ * @param {QueryInterface} queryInterface
+ * @param {Sequelize} Sequelize
+ * @returns {Promise}
+ */
+ async down(queryInterface, Sequelize) {
+ /**
+ * @type {Transaction}
+ */
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.removeColumn('conversationparticipants', 'userId', {
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
diff --git a/backend/src/db/models/bottles.js b/backend/src/db/models/bottles.js
index 23d8edf..27d0c76 100644
--- a/backend/src/db/models/bottles.js
+++ b/backend/src/db/models/bottles.js
@@ -14,53 +14,71 @@ module.exports = function (sequelize, DataTypes) {
primaryKey: true,
},
- name: {
- type: DataTypes.TEXT,
- },
-
proof: {
type: DataTypes.DECIMAL,
},
- type: {
- type: DataTypes.ENUM,
-
- values: ['Bourbon', 'Scotch', 'Rye', 'Irish', 'Other'],
+ age: {
+ type: DataTypes.INTEGER,
},
- notes: {
- type: DataTypes.TEXT,
+ rating: {
+ type: DataTypes.INTEGER,
},
- tasting_notes: {
- type: DataTypes.TEXT,
- },
-
- msrp_range: {
- type: DataTypes.TEXT,
- },
-
- secondary_value_range: {
- type: DataTypes.TEXT,
- },
-
- opened_bottle_indicator: {
+ collectable: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
- quantity: {
- type: DataTypes.INTEGER,
- },
-
- barcode: {
+ rickhouse: {
type: DataTypes.TEXT,
},
- age: {
- type: DataTypes.DECIMAL,
+ rack: {
+ type: DataTypes.TEXT,
+ },
+
+ release: {
+ type: DataTypes.TEXT,
+ },
+
+ barrelnumber: {
+ type: DataTypes.TEXT,
+ },
+
+ barreleddate: {
+ type: DataTypes.DATEONLY,
+
+ get: function () {
+ return this.getDataValue('barreleddate')
+ ? moment.utc(this.getDataValue('barreleddate')).format('YYYY-MM-DD')
+ : null;
+ },
+ },
+
+ bottlenumber: {
+ type: DataTypes.TEXT,
+ },
+
+ dateacquired: {
+ type: DataTypes.DATEONLY,
+
+ get: function () {
+ return this.getDataValue('dateacquired')
+ ? moment.utc(this.getDataValue('dateacquired')).format('YYYY-MM-DD')
+ : null;
+ },
+ },
+
+ volume: {
+ type: DataTypes.INTEGER,
+ },
+
+ notes: {
+ type: DataTypes.TEXT,
},
importHash: {
@@ -79,24 +97,16 @@ module.exports = function (sequelize, DataTypes) {
bottles.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+ db.bottles.hasMany(db.reviews, {
+ as: 'reviews_bottle',
+ foreignKey: {
+ name: 'bottleId',
+ },
+ constraints: false,
+ });
+
//end loop
- db.bottles.belongsTo(db.brands, {
- as: 'brand',
- foreignKey: {
- name: 'brandId',
- },
- constraints: false,
- });
-
- db.bottles.belongsTo(db.distilleries, {
- as: 'distillery',
- foreignKey: {
- name: 'distilleryId',
- },
- constraints: false,
- });
-
db.bottles.belongsTo(db.users, {
as: 'user',
foreignKey: {
@@ -105,14 +115,36 @@ module.exports = function (sequelize, DataTypes) {
constraints: false,
});
- db.bottles.hasMany(db.file, {
- as: 'picture',
- foreignKey: 'belongsToId',
- constraints: false,
- scope: {
- belongsTo: db.bottles.getTableName(),
- belongsToColumn: 'picture',
+ db.bottles.belongsTo(db.products, {
+ as: 'product',
+ foreignKey: {
+ name: 'productId',
},
+ constraints: false,
+ });
+
+ db.bottles.belongsTo(db.locations, {
+ as: 'location',
+ foreignKey: {
+ name: 'locationId',
+ },
+ constraints: false,
+ });
+
+ db.bottles.belongsTo(db.photos, {
+ as: 'photofront',
+ foreignKey: {
+ name: 'photofrontId',
+ },
+ constraints: false,
+ });
+
+ db.bottles.belongsTo(db.photos, {
+ as: 'photoback',
+ foreignKey: {
+ name: 'photobackId',
+ },
+ constraints: false,
});
db.bottles.belongsTo(db.users, {
diff --git a/backend/src/db/models/brands.js b/backend/src/db/models/brands.js
index e30b44b..0b57391 100644
--- a/backend/src/db/models/brands.js
+++ b/backend/src/db/models/brands.js
@@ -18,6 +18,10 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.TEXT,
},
+ status: {
+ type: DataTypes.INTEGER,
+ },
+
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
@@ -34,8 +38,8 @@ module.exports = function (sequelize, DataTypes) {
brands.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
- db.brands.hasMany(db.bottles, {
- as: 'bottles_brand',
+ db.brands.hasMany(db.products, {
+ as: 'products_brand',
foreignKey: {
name: 'brandId',
},
diff --git a/backend/src/db/models/conversationparticipants.js b/backend/src/db/models/conversationparticipants.js
new file mode 100644
index 0000000..2222a13
--- /dev/null
+++ b/backend/src/db/models/conversationparticipants.js
@@ -0,0 +1,61 @@
+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 conversationparticipants = sequelize.define(
+ 'conversationparticipants',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+
+ importHash: {
+ type: DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ freezeTableName: true,
+ },
+ );
+
+ conversationparticipants.associate = (db) => {
+ /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+
+ //end loop
+
+ db.conversationparticipants.belongsTo(db.conversations, {
+ as: 'conversation',
+ foreignKey: {
+ name: 'conversationId',
+ },
+ constraints: false,
+ });
+
+ db.conversationparticipants.belongsTo(db.users, {
+ as: 'user',
+ foreignKey: {
+ name: 'userId',
+ },
+ constraints: false,
+ });
+
+ db.conversationparticipants.belongsTo(db.users, {
+ as: 'createdBy',
+ });
+
+ db.conversationparticipants.belongsTo(db.users, {
+ as: 'updatedBy',
+ });
+ };
+
+ return conversationparticipants;
+};
diff --git a/backend/src/db/models/conversations.js b/backend/src/db/models/conversations.js
new file mode 100644
index 0000000..da1cd2e
--- /dev/null
+++ b/backend/src/db/models/conversations.js
@@ -0,0 +1,65 @@
+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 conversations = sequelize.define(
+ 'conversations',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+
+ createdat: {
+ type: DataTypes.DATE,
+ },
+
+ importHash: {
+ type: DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ freezeTableName: true,
+ },
+ );
+
+ conversations.associate = (db) => {
+ /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+
+ db.conversations.hasMany(db.messages, {
+ as: 'messages_conversation',
+ foreignKey: {
+ name: 'conversationId',
+ },
+ constraints: false,
+ });
+
+ db.conversations.hasMany(db.conversationparticipants, {
+ as: 'conversationparticipants_conversation',
+ foreignKey: {
+ name: 'conversationId',
+ },
+ constraints: false,
+ });
+
+ //end loop
+
+ db.conversations.belongsTo(db.users, {
+ as: 'createdBy',
+ });
+
+ db.conversations.belongsTo(db.users, {
+ as: 'updatedBy',
+ });
+ };
+
+ return conversations;
+};
diff --git a/backend/src/db/models/distilleries.js b/backend/src/db/models/distilleries.js
index 88e041e..fe721ca 100644
--- a/backend/src/db/models/distilleries.js
+++ b/backend/src/db/models/distilleries.js
@@ -18,6 +18,18 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.TEXT,
},
+ city: {
+ type: DataTypes.TEXT,
+ },
+
+ state: {
+ type: DataTypes.TEXT,
+ },
+
+ status: {
+ type: DataTypes.INTEGER,
+ },
+
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
@@ -34,14 +46,6 @@ module.exports = function (sequelize, DataTypes) {
distilleries.associate = (db) => {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
- db.distilleries.hasMany(db.bottles, {
- as: 'bottles_distillery',
- foreignKey: {
- name: 'distilleryId',
- },
- constraints: false,
- });
-
db.distilleries.hasMany(db.brands, {
as: 'brands_distillery',
foreignKey: {
diff --git a/backend/src/db/models/locations.js b/backend/src/db/models/locations.js
new file mode 100644
index 0000000..c186da1
--- /dev/null
+++ b/backend/src/db/models/locations.js
@@ -0,0 +1,65 @@
+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 locations = sequelize.define(
+ 'locations',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+
+ name: {
+ type: DataTypes.TEXT,
+ },
+
+ importHash: {
+ type: DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ freezeTableName: true,
+ },
+ );
+
+ locations.associate = (db) => {
+ /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+
+ db.locations.hasMany(db.bottles, {
+ as: 'bottles_location',
+ foreignKey: {
+ name: 'locationId',
+ },
+ constraints: false,
+ });
+
+ //end loop
+
+ db.locations.belongsTo(db.users, {
+ as: 'user',
+ foreignKey: {
+ name: 'userId',
+ },
+ constraints: false,
+ });
+
+ db.locations.belongsTo(db.users, {
+ as: 'createdBy',
+ });
+
+ db.locations.belongsTo(db.users, {
+ as: 'updatedBy',
+ });
+ };
+
+ return locations;
+};
diff --git a/backend/src/db/models/messages.js b/backend/src/db/models/messages.js
new file mode 100644
index 0000000..8a776f9
--- /dev/null
+++ b/backend/src/db/models/messages.js
@@ -0,0 +1,61 @@
+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 messages = sequelize.define(
+ 'messages',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+
+ importHash: {
+ type: DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ freezeTableName: true,
+ },
+ );
+
+ messages.associate = (db) => {
+ /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+
+ //end loop
+
+ db.messages.belongsTo(db.conversations, {
+ as: 'conversation',
+ foreignKey: {
+ name: 'conversationId',
+ },
+ constraints: false,
+ });
+
+ db.messages.belongsTo(db.users, {
+ as: 'sender',
+ foreignKey: {
+ name: 'senderId',
+ },
+ constraints: false,
+ });
+
+ db.messages.belongsTo(db.users, {
+ as: 'createdBy',
+ });
+
+ db.messages.belongsTo(db.users, {
+ as: 'updatedBy',
+ });
+ };
+
+ return messages;
+};
diff --git a/backend/src/db/models/photos.js b/backend/src/db/models/photos.js
new file mode 100644
index 0000000..af905bc
--- /dev/null
+++ b/backend/src/db/models/photos.js
@@ -0,0 +1,91 @@
+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 photos = sequelize.define(
+ 'photos',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+
+ phototype: {
+ type: DataTypes.TEXT,
+ },
+
+ importHash: {
+ type: DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ freezeTableName: true,
+ },
+ );
+
+ photos.associate = (db) => {
+ /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+
+ db.photos.hasMany(db.products, {
+ as: 'products_photofront',
+ foreignKey: {
+ name: 'photofrontId',
+ },
+ constraints: false,
+ });
+
+ db.photos.hasMany(db.products, {
+ as: 'products_photoback',
+ foreignKey: {
+ name: 'photobackId',
+ },
+ constraints: false,
+ });
+
+ db.photos.hasMany(db.bottles, {
+ as: 'bottles_photofront',
+ foreignKey: {
+ name: 'photofrontId',
+ },
+ constraints: false,
+ });
+
+ db.photos.hasMany(db.bottles, {
+ as: 'bottles_photoback',
+ foreignKey: {
+ name: 'photobackId',
+ },
+ constraints: false,
+ });
+
+ //end loop
+
+ db.photos.hasMany(db.file, {
+ as: 'image',
+ foreignKey: 'belongsToId',
+ constraints: false,
+ scope: {
+ belongsTo: db.photos.getTableName(),
+ belongsToColumn: 'image',
+ },
+ });
+
+ db.photos.belongsTo(db.users, {
+ as: 'createdBy',
+ });
+
+ db.photos.belongsTo(db.users, {
+ as: 'updatedBy',
+ });
+ };
+
+ return photos;
+};
diff --git a/backend/src/db/models/products.js b/backend/src/db/models/products.js
new file mode 100644
index 0000000..ff187a5
--- /dev/null
+++ b/backend/src/db/models/products.js
@@ -0,0 +1,101 @@
+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 products = sequelize.define(
+ 'products',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+
+ name: {
+ type: DataTypes.TEXT,
+ },
+
+ proof: {
+ type: DataTypes.DECIMAL,
+ },
+
+ age: {
+ type: DataTypes.INTEGER,
+ },
+
+ barcode: {
+ type: DataTypes.TEXT,
+ },
+
+ notes: {
+ type: DataTypes.TEXT,
+ },
+
+ status: {
+ type: DataTypes.INTEGER,
+ },
+
+ importHash: {
+ type: DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ freezeTableName: true,
+ },
+ );
+
+ products.associate = (db) => {
+ /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+
+ db.products.hasMany(db.bottles, {
+ as: 'bottles_product',
+ foreignKey: {
+ name: 'productId',
+ },
+ constraints: false,
+ });
+
+ //end loop
+
+ db.products.belongsTo(db.brands, {
+ as: 'brand',
+ foreignKey: {
+ name: 'brandId',
+ },
+ constraints: false,
+ });
+
+ db.products.belongsTo(db.photos, {
+ as: 'photofront',
+ foreignKey: {
+ name: 'photofrontId',
+ },
+ constraints: false,
+ });
+
+ db.products.belongsTo(db.photos, {
+ as: 'photoback',
+ foreignKey: {
+ name: 'photobackId',
+ },
+ constraints: false,
+ });
+
+ db.products.belongsTo(db.users, {
+ as: 'createdBy',
+ });
+
+ db.products.belongsTo(db.users, {
+ as: 'updatedBy',
+ });
+ };
+
+ return products;
+};
diff --git a/backend/src/db/models/reviews.js b/backend/src/db/models/reviews.js
new file mode 100644
index 0000000..1e9d916
--- /dev/null
+++ b/backend/src/db/models/reviews.js
@@ -0,0 +1,73 @@
+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 reviews = sequelize.define(
+ 'reviews',
+ {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true,
+ },
+
+ rating: {
+ type: DataTypes.INTEGER,
+ },
+
+ notes: {
+ type: DataTypes.TEXT,
+ },
+
+ createdat: {
+ type: DataTypes.DATE,
+ },
+
+ importHash: {
+ type: DataTypes.STRING(255),
+ allowNull: true,
+ unique: true,
+ },
+ },
+ {
+ timestamps: true,
+ paranoid: true,
+ freezeTableName: true,
+ },
+ );
+
+ reviews.associate = (db) => {
+ /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+
+ //end loop
+
+ db.reviews.belongsTo(db.users, {
+ as: 'user',
+ foreignKey: {
+ name: 'userId',
+ },
+ constraints: false,
+ });
+
+ db.reviews.belongsTo(db.bottles, {
+ as: 'bottle',
+ foreignKey: {
+ name: 'bottleId',
+ },
+ constraints: false,
+ });
+
+ db.reviews.belongsTo(db.users, {
+ as: 'createdBy',
+ });
+
+ db.reviews.belongsTo(db.users, {
+ as: 'updatedBy',
+ });
+ };
+
+ return reviews;
+};
diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js
index af07cd6..4414c07 100644
--- a/backend/src/db/models/users.js
+++ b/backend/src/db/models/users.js
@@ -68,6 +68,14 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.TEXT,
},
+ address: {
+ type: DataTypes.TEXT,
+ },
+
+ address2: {
+ type: DataTypes.TEXT,
+ },
+
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
@@ -102,6 +110,14 @@ module.exports = function (sequelize, DataTypes) {
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
+ db.users.hasMany(db.locations, {
+ as: 'locations_user',
+ foreignKey: {
+ name: 'userId',
+ },
+ constraints: false,
+ });
+
db.users.hasMany(db.bottles, {
as: 'bottles_user',
foreignKey: {
@@ -110,6 +126,30 @@ module.exports = function (sequelize, DataTypes) {
constraints: false,
});
+ db.users.hasMany(db.reviews, {
+ as: 'reviews_user',
+ foreignKey: {
+ name: 'userId',
+ },
+ constraints: false,
+ });
+
+ db.users.hasMany(db.messages, {
+ as: 'messages_sender',
+ foreignKey: {
+ name: 'senderId',
+ },
+ constraints: false,
+ });
+
+ db.users.hasMany(db.conversationparticipants, {
+ as: 'conversationparticipants_user',
+ foreignKey: {
+ name: 'userId',
+ },
+ constraints: false,
+ });
+
//end loop
db.users.belongsTo(db.roles, {
diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js
index 67a934b..3b54060 100644
--- a/backend/src/db/seeders/20200430130760-user-roles.js
+++ b/backend/src/db/seeders/20200430130760-user-roles.js
@@ -105,11 +105,18 @@ module.exports = {
const entities = [
'users',
- 'bottles',
- 'brands',
- 'distilleries',
'roles',
'permissions',
+ 'distilleries',
+ 'brands',
+ 'photos',
+ 'products',
+ 'locations',
+ 'bottles',
+ 'reviews',
+ 'conversations',
+ 'messages',
+ 'conversationparticipants',
,
];
await queryInterface.bulkInsert(
@@ -211,91 +218,63 @@ primary key ("roles_permissionsId", "permissionId")
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyMaster'),
- permissionId: getId('CREATE_BOTTLES'),
+ permissionId: getId('CREATE_DISTILLERIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyMaster'),
- permissionId: getId('READ_BOTTLES'),
+ permissionId: getId('READ_DISTILLERIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyMaster'),
- permissionId: getId('UPDATE_BOTTLES'),
+ permissionId: getId('UPDATE_DISTILLERIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyMaster'),
- permissionId: getId('DELETE_BOTTLES'),
+ permissionId: getId('DELETE_DISTILLERIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('InventoryManager'),
- permissionId: getId('CREATE_BOTTLES'),
+ permissionId: getId('READ_DISTILLERIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('InventoryManager'),
- permissionId: getId('READ_BOTTLES'),
- },
-
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('InventoryManager'),
- permissionId: getId('UPDATE_BOTTLES'),
- },
-
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('InventoryManager'),
- permissionId: getId('DELETE_BOTTLES'),
+ permissionId: getId('UPDATE_DISTILLERIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('TastingExpert'),
- permissionId: getId('READ_BOTTLES'),
- },
-
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('TastingExpert'),
- permissionId: getId('UPDATE_BOTTLES'),
+ permissionId: getId('READ_DISTILLERIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('BottleCollector'),
- permissionId: getId('READ_BOTTLES'),
- },
-
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('BottleCollector'),
- permissionId: getId('UPDATE_BOTTLES'),
+ permissionId: getId('READ_DISTILLERIES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyEnthusiast'),
- permissionId: getId('READ_BOTTLES'),
+ permissionId: getId('READ_DISTILLERIES'),
},
{
@@ -365,63 +344,91 @@ primary key ("roles_permissionsId", "permissionId")
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyMaster'),
- permissionId: getId('CREATE_DISTILLERIES'),
+ permissionId: getId('CREATE_BOTTLES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyMaster'),
- permissionId: getId('READ_DISTILLERIES'),
+ permissionId: getId('READ_BOTTLES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyMaster'),
- permissionId: getId('UPDATE_DISTILLERIES'),
+ permissionId: getId('UPDATE_BOTTLES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyMaster'),
- permissionId: getId('DELETE_DISTILLERIES'),
+ permissionId: getId('DELETE_BOTTLES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('InventoryManager'),
- permissionId: getId('READ_DISTILLERIES'),
+ permissionId: getId('CREATE_BOTTLES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('InventoryManager'),
- permissionId: getId('UPDATE_DISTILLERIES'),
+ permissionId: getId('READ_BOTTLES'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('InventoryManager'),
+ permissionId: getId('UPDATE_BOTTLES'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('InventoryManager'),
+ permissionId: getId('DELETE_BOTTLES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('TastingExpert'),
- permissionId: getId('READ_DISTILLERIES'),
+ permissionId: getId('READ_BOTTLES'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('TastingExpert'),
+ permissionId: getId('UPDATE_BOTTLES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('BottleCollector'),
- permissionId: getId('READ_DISTILLERIES'),
+ permissionId: getId('READ_BOTTLES'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('BottleCollector'),
+ permissionId: getId('UPDATE_BOTTLES'),
},
{
createdAt,
updatedAt,
roles_permissionsId: getId('WhiskeyEnthusiast'),
- permissionId: getId('READ_DISTILLERIES'),
+ permissionId: getId('READ_BOTTLES'),
},
{
@@ -484,81 +491,6 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_USERS'),
},
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('CREATE_BOTTLES'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('READ_BOTTLES'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('UPDATE_BOTTLES'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('DELETE_BOTTLES'),
- },
-
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('CREATE_BRANDS'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('READ_BRANDS'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('UPDATE_BRANDS'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('DELETE_BRANDS'),
- },
-
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('CREATE_DISTILLERIES'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('READ_DISTILLERIES'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('UPDATE_DISTILLERIES'),
- },
- {
- createdAt,
- updatedAt,
- roles_permissionsId: getId('Administrator'),
- permissionId: getId('DELETE_DISTILLERIES'),
- },
-
{
createdAt,
updatedAt,
@@ -609,6 +541,256 @@ primary key ("roles_permissionsId", "permissionId")
permissionId: getId('DELETE_PERMISSIONS'),
},
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_DISTILLERIES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_DISTILLERIES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_DISTILLERIES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_DISTILLERIES'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_BRANDS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_BRANDS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_BRANDS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_BRANDS'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_PHOTOS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_PHOTOS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_PHOTOS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_PHOTOS'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_PRODUCTS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_PRODUCTS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_PRODUCTS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_PRODUCTS'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_LOCATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_LOCATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_LOCATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_LOCATIONS'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_BOTTLES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_BOTTLES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_BOTTLES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_BOTTLES'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_REVIEWS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_REVIEWS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_REVIEWS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_REVIEWS'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_CONVERSATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_CONVERSATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_CONVERSATIONS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_CONVERSATIONS'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_MESSAGES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_MESSAGES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_MESSAGES'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_MESSAGES'),
+ },
+
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('CREATE_CONVERSATIONPARTICIPANTS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('READ_CONVERSATIONPARTICIPANTS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('UPDATE_CONVERSATIONPARTICIPANTS'),
+ },
+ {
+ createdAt,
+ updatedAt,
+ roles_permissionsId: getId('Administrator'),
+ permissionId: getId('DELETE_CONVERSATIONPARTICIPANTS'),
+ },
+
{
createdAt,
updatedAt,
diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js
index 207d2f7..435ae40 100644
--- a/backend/src/db/seeders/20231127130745-sample-data.js
+++ b/backend/src/db/seeders/20231127130745-sample-data.js
@@ -1,171 +1,75 @@
const db = require('../models');
const Users = db.users;
-const Bottles = db.bottles;
+const Distilleries = db.distilleries;
const Brands = db.brands;
-const Distilleries = db.distilleries;
+const Photos = db.photos;
-const BottlesData = [
+const Products = db.products;
+
+const Locations = db.locations;
+
+const Bottles = db.bottles;
+
+const Reviews = db.reviews;
+
+const Conversations = db.conversations;
+
+const Messages = db.messages;
+
+const Conversationparticipants = db.conversationparticipants;
+
+const DistilleriesData = [
{
- name: 'Old Forester 1920',
+ name: 'Old Forester',
- // type code here for "relation_one" field
+ city: 'James Clerk Maxwell',
- proof: 115,
+ state: 'Alfred Kinsey',
- type: 'Rye',
-
- notes: 'Rich and full-bodied',
-
- tasting_notes: 'Caramel, vanilla, and oak',
-
- msrp_range: '$60-$70',
-
- secondary_value_range: '$80-$100',
-
- opened_bottle_indicator: false,
-
- quantity: 1,
-
- barcode: '123456789012',
-
- // type code here for "images" field
-
- age: 4,
-
- // type code here for "relation_one" field
-
- // type code here for "relation_one" field
+ status: 6,
},
{
- name: 'Lagavulin 16',
+ name: 'Lagavulin',
- // type code here for "relation_one" field
+ city: 'J. Robert Oppenheimer',
- proof: 86,
+ state: 'Galileo Galilei',
- type: 'Bourbon',
-
- notes: 'Peaty and smoky',
-
- tasting_notes: 'Smoke, seaweed, and vanilla',
-
- msrp_range: '$100-$120',
-
- secondary_value_range: '$150-$180',
-
- opened_bottle_indicator: true,
-
- quantity: 1,
-
- barcode: '234567890123',
-
- // type code here for "images" field
-
- age: 16,
-
- // type code here for "relation_one" field
-
- // type code here for "relation_one" field
+ status: 1,
},
{
name: 'Buffalo Trace',
- // type code here for "relation_one" field
+ city: 'Comte de Buffon',
- proof: 90,
+ state: 'Thomas Hunt Morgan',
- type: 'Bourbon',
-
- notes: 'Smooth and balanced',
-
- tasting_notes: 'Vanilla, caramel, and spice',
-
- msrp_range: '$25-$35',
-
- secondary_value_range: '$40-$50',
-
- opened_bottle_indicator: false,
-
- quantity: 2,
-
- barcode: '345678901234',
-
- // type code here for "images" field
-
- age: 8,
-
- // type code here for "relation_one" field
-
- // type code here for "relation_one" field
+ status: 4,
},
{
- name: 'Redbreast 12',
+ name: 'Midleton',
- // type code here for "relation_one" field
+ city: 'Linus Pauling',
- proof: 80,
+ state: 'Edward Teller',
- type: 'Rye',
-
- notes: 'Rich and complex',
-
- tasting_notes: 'Sherry, fruit, and spice',
-
- msrp_range: '$50-$60',
-
- secondary_value_range: '$70-$90',
-
- opened_bottle_indicator: true,
-
- quantity: 1,
-
- barcode: '456789012345',
-
- // type code here for "images" field
-
- age: 12,
-
- // type code here for "relation_one" field
-
- // type code here for "relation_one" field
+ status: 5,
},
{
- name: 'Pappy Van Winkle 15',
+ name: 'Old Rip Van Winkle',
- // type code here for "relation_one" field
+ city: 'Archimedes',
- proof: 107,
+ state: 'Werner Heisenberg',
- type: 'Bourbon',
-
- notes: 'Rare and exquisite',
-
- tasting_notes: 'Vanilla, oak, and spice',
-
- msrp_range: '$120-$150',
-
- secondary_value_range: '$2000-$2500',
-
- opened_bottle_indicator: true,
-
- quantity: 1,
-
- barcode: '567890123456',
-
- // type code here for "images" field
-
- age: 15,
-
- // type code here for "relation_one" field
-
- // type code here for "relation_one" field
+ status: 8,
},
];
@@ -173,169 +77,824 @@ const BrandsData = [
{
name: 'Old Forester',
+ status: 4,
+
// type code here for "relation_one" field
},
{
name: 'Lagavulin',
+ status: 7,
+
// type code here for "relation_one" field
},
{
name: 'Buffalo Trace',
+ status: 1,
+
// type code here for "relation_one" field
},
{
name: 'Redbreast',
+ status: 4,
+
// type code here for "relation_one" field
},
{
name: 'Pappy Van Winkle',
+ status: 6,
+
// type code here for "relation_one" field
},
];
-const DistilleriesData = [
+const PhotosData = [
{
- name: 'Old Forester',
+ phototype: 'William Bayliss',
+
+ // type code here for "images" field
},
{
- name: 'Lagavulin',
+ phototype: 'William Harvey',
+
+ // type code here for "images" field
},
{
- name: 'Buffalo Trace',
+ phototype: 'George Gaylord Simpson',
+
+ // type code here for "images" field
},
{
- name: 'Midleton',
+ phototype: 'Marcello Malpighi',
+
+ // type code here for "images" field
},
{
- name: 'Old Rip Van Winkle',
+ phototype: 'Claude Levi-Strauss',
+
+ // type code here for "images" field
+ },
+];
+
+const ProductsData = [
+ {
+ // type code here for "relation_one" field
+
+ name: 'Max von Laue',
+
+ proof: 64.07,
+
+ age: 4,
+
+ barcode: 'George Gaylord Simpson',
+
+ notes: 'Ernest Rutherford',
+
+ status: 2,
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ name: 'John Bardeen',
+
+ proof: 14.86,
+
+ age: 7,
+
+ barcode: 'Alfred Binet',
+
+ notes: 'James Clerk Maxwell',
+
+ status: 4,
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ name: 'John von Neumann',
+
+ proof: 52.62,
+
+ age: 3,
+
+ barcode: 'Richard Feynman',
+
+ notes: 'Karl Landsteiner',
+
+ status: 9,
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ name: 'Heike Kamerlingh Onnes',
+
+ proof: 87.82,
+
+ age: 8,
+
+ barcode: 'J. Robert Oppenheimer',
+
+ notes: 'Charles Lyell',
+
+ status: 6,
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ name: 'Max Planck',
+
+ proof: 76.09,
+
+ age: 6,
+
+ barcode: 'Ernst Haeckel',
+
+ notes: 'Ludwig Boltzmann',
+
+ status: 3,
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+];
+
+const LocationsData = [
+ {
+ name: 'George Gaylord Simpson',
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ name: 'Galileo Galilei',
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ name: 'Anton van Leeuwenhoek',
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ name: 'Emil Kraepelin',
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ name: 'James Watson',
+
+ // type code here for "relation_one" field
+ },
+];
+
+const BottlesData = [
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ proof: 115,
+
+ age: 4,
+
+ rating: 8,
+
+ collectable: false,
+
+ rickhouse: 'Enrico Fermi',
+
+ rack: 'Paul Ehrlich',
+
+ release: 'Michael Faraday',
+
+ barrelnumber: 'J. Robert Oppenheimer',
+
+ barreleddate: new Date(Date.now()),
+
+ bottlenumber: 'Albrecht von Haller',
+
+ dateacquired: new Date(Date.now()),
+
+ volume: 9,
+
+ notes: 'Rich and full-bodied',
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ proof: 86,
+
+ age: 16,
+
+ rating: 2,
+
+ collectable: false,
+
+ rickhouse: 'William Bayliss',
+
+ rack: 'Johannes Kepler',
+
+ release: 'Sheldon Glashow',
+
+ barrelnumber: 'Claude Levi-Strauss',
+
+ barreleddate: new Date(Date.now()),
+
+ bottlenumber: 'William Herschel',
+
+ dateacquired: new Date(Date.now()),
+
+ volume: 5,
+
+ notes: 'Peaty and smoky',
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ proof: 90,
+
+ age: 8,
+
+ rating: 1,
+
+ collectable: false,
+
+ rickhouse: 'Jean Baptiste Lamarck',
+
+ rack: 'Frederick Gowland Hopkins',
+
+ release: 'Emil Kraepelin',
+
+ barrelnumber: 'Jean Piaget',
+
+ barreleddate: new Date(Date.now()),
+
+ bottlenumber: 'William Herschel',
+
+ dateacquired: new Date(Date.now()),
+
+ volume: 3,
+
+ notes: 'Smooth and balanced',
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ proof: 80,
+
+ age: 12,
+
+ rating: 4,
+
+ collectable: false,
+
+ rickhouse: 'Frederick Gowland Hopkins',
+
+ rack: 'Alexander Fleming',
+
+ release: 'Ernst Mayr',
+
+ barrelnumber: 'James Clerk Maxwell',
+
+ barreleddate: new Date(Date.now()),
+
+ bottlenumber: 'Francis Galton',
+
+ dateacquired: new Date(Date.now()),
+
+ volume: 8,
+
+ notes: 'Rich and complex',
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ proof: 107,
+
+ age: 15,
+
+ rating: 3,
+
+ collectable: true,
+
+ rickhouse: 'Heike Kamerlingh Onnes',
+
+ rack: 'Trofim Lysenko',
+
+ release: 'Willard Libby',
+
+ barrelnumber: 'Enrico Fermi',
+
+ barreleddate: new Date(Date.now()),
+
+ bottlenumber: 'Max von Laue',
+
+ dateacquired: new Date(Date.now()),
+
+ volume: 2,
+
+ notes: 'Rare and exquisite',
+
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+ },
+];
+
+const ReviewsData = [
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ rating: 7,
+
+ notes: 'Sheldon Glashow',
+
+ createdat: new Date(Date.now()),
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ rating: 6,
+
+ notes: 'James Watson',
+
+ createdat: new Date(Date.now()),
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ rating: 2,
+
+ notes: 'Anton van Leeuwenhoek',
+
+ createdat: new Date(Date.now()),
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ rating: 1,
+
+ notes: 'Karl Landsteiner',
+
+ createdat: new Date(Date.now()),
+ },
+
+ {
+ // type code here for "relation_one" field
+
+ // type code here for "relation_one" field
+
+ rating: 3,
+
+ notes: 'Edward O. Wilson',
+
+ createdat: new Date(Date.now()),
+ },
+];
+
+const ConversationsData = [
+ {
+ createdat: new Date(Date.now()),
+ },
+
+ {
+ createdat: new Date(Date.now()),
+ },
+
+ {
+ createdat: new Date(Date.now()),
+ },
+
+ {
+ createdat: new Date(Date.now()),
+ },
+
+ {
+ createdat: new Date(Date.now()),
+ },
+];
+
+const MessagesData = [
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+];
+
+const ConversationparticipantsData = [
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
+ },
+
+ {
+ // type code here for "relation_one" field
+ // type code here for "relation_one" field
},
];
// Similar logic for "relation_many"
-async function associateBottleWithBrand() {
- const relatedBrand0 = await Brands.findOne({
- offset: Math.floor(Math.random() * (await Brands.count())),
- });
- const Bottle0 = await Bottles.findOne({
- order: [['id', 'ASC']],
- offset: 0,
- });
- if (Bottle0?.setBrand) {
- await Bottle0.setBrand(relatedBrand0);
- }
-
- const relatedBrand1 = await Brands.findOne({
- offset: Math.floor(Math.random() * (await Brands.count())),
- });
- const Bottle1 = await Bottles.findOne({
- order: [['id', 'ASC']],
- offset: 1,
- });
- if (Bottle1?.setBrand) {
- await Bottle1.setBrand(relatedBrand1);
- }
-
- const relatedBrand2 = await Brands.findOne({
- offset: Math.floor(Math.random() * (await Brands.count())),
- });
- const Bottle2 = await Bottles.findOne({
- order: [['id', 'ASC']],
- offset: 2,
- });
- if (Bottle2?.setBrand) {
- await Bottle2.setBrand(relatedBrand2);
- }
-
- const relatedBrand3 = await Brands.findOne({
- offset: Math.floor(Math.random() * (await Brands.count())),
- });
- const Bottle3 = await Bottles.findOne({
- order: [['id', 'ASC']],
- offset: 3,
- });
- if (Bottle3?.setBrand) {
- await Bottle3.setBrand(relatedBrand3);
- }
-
- const relatedBrand4 = await Brands.findOne({
- offset: Math.floor(Math.random() * (await Brands.count())),
- });
- const Bottle4 = await Bottles.findOne({
- order: [['id', 'ASC']],
- offset: 4,
- });
- if (Bottle4?.setBrand) {
- await Bottle4.setBrand(relatedBrand4);
- }
-}
-
-async function associateBottleWithDistillery() {
+async function associateBrandWithDistillery() {
const relatedDistillery0 = await Distilleries.findOne({
offset: Math.floor(Math.random() * (await Distilleries.count())),
});
- const Bottle0 = await Bottles.findOne({
+ const Brand0 = await Brands.findOne({
order: [['id', 'ASC']],
offset: 0,
});
- if (Bottle0?.setDistillery) {
- await Bottle0.setDistillery(relatedDistillery0);
+ if (Brand0?.setDistillery) {
+ await Brand0.setDistillery(relatedDistillery0);
}
const relatedDistillery1 = await Distilleries.findOne({
offset: Math.floor(Math.random() * (await Distilleries.count())),
});
- const Bottle1 = await Bottles.findOne({
+ const Brand1 = await Brands.findOne({
order: [['id', 'ASC']],
offset: 1,
});
- if (Bottle1?.setDistillery) {
- await Bottle1.setDistillery(relatedDistillery1);
+ if (Brand1?.setDistillery) {
+ await Brand1.setDistillery(relatedDistillery1);
}
const relatedDistillery2 = await Distilleries.findOne({
offset: Math.floor(Math.random() * (await Distilleries.count())),
});
- const Bottle2 = await Bottles.findOne({
+ const Brand2 = await Brands.findOne({
order: [['id', 'ASC']],
offset: 2,
});
- if (Bottle2?.setDistillery) {
- await Bottle2.setDistillery(relatedDistillery2);
+ if (Brand2?.setDistillery) {
+ await Brand2.setDistillery(relatedDistillery2);
}
const relatedDistillery3 = await Distilleries.findOne({
offset: Math.floor(Math.random() * (await Distilleries.count())),
});
- const Bottle3 = await Bottles.findOne({
+ const Brand3 = await Brands.findOne({
order: [['id', 'ASC']],
offset: 3,
});
- if (Bottle3?.setDistillery) {
- await Bottle3.setDistillery(relatedDistillery3);
+ if (Brand3?.setDistillery) {
+ await Brand3.setDistillery(relatedDistillery3);
}
const relatedDistillery4 = await Distilleries.findOne({
offset: Math.floor(Math.random() * (await Distilleries.count())),
});
- const Bottle4 = await Bottles.findOne({
+ const Brand4 = await Brands.findOne({
order: [['id', 'ASC']],
offset: 4,
});
- if (Bottle4?.setDistillery) {
- await Bottle4.setDistillery(relatedDistillery4);
+ if (Brand4?.setDistillery) {
+ await Brand4.setDistillery(relatedDistillery4);
+ }
+}
+
+async function associateProductWithBrand() {
+ const relatedBrand0 = await Brands.findOne({
+ offset: Math.floor(Math.random() * (await Brands.count())),
+ });
+ const Product0 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Product0?.setBrand) {
+ await Product0.setBrand(relatedBrand0);
+ }
+
+ const relatedBrand1 = await Brands.findOne({
+ offset: Math.floor(Math.random() * (await Brands.count())),
+ });
+ const Product1 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Product1?.setBrand) {
+ await Product1.setBrand(relatedBrand1);
+ }
+
+ const relatedBrand2 = await Brands.findOne({
+ offset: Math.floor(Math.random() * (await Brands.count())),
+ });
+ const Product2 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Product2?.setBrand) {
+ await Product2.setBrand(relatedBrand2);
+ }
+
+ const relatedBrand3 = await Brands.findOne({
+ offset: Math.floor(Math.random() * (await Brands.count())),
+ });
+ const Product3 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Product3?.setBrand) {
+ await Product3.setBrand(relatedBrand3);
+ }
+
+ const relatedBrand4 = await Brands.findOne({
+ offset: Math.floor(Math.random() * (await Brands.count())),
+ });
+ const Product4 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Product4?.setBrand) {
+ await Product4.setBrand(relatedBrand4);
+ }
+}
+
+async function associateProductWithPhotofront() {
+ const relatedPhotofront0 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product0 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Product0?.setPhotofront) {
+ await Product0.setPhotofront(relatedPhotofront0);
+ }
+
+ const relatedPhotofront1 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product1 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Product1?.setPhotofront) {
+ await Product1.setPhotofront(relatedPhotofront1);
+ }
+
+ const relatedPhotofront2 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product2 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Product2?.setPhotofront) {
+ await Product2.setPhotofront(relatedPhotofront2);
+ }
+
+ const relatedPhotofront3 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product3 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Product3?.setPhotofront) {
+ await Product3.setPhotofront(relatedPhotofront3);
+ }
+
+ const relatedPhotofront4 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product4 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Product4?.setPhotofront) {
+ await Product4.setPhotofront(relatedPhotofront4);
+ }
+}
+
+async function associateProductWithPhotoback() {
+ const relatedPhotoback0 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product0 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Product0?.setPhotoback) {
+ await Product0.setPhotoback(relatedPhotoback0);
+ }
+
+ const relatedPhotoback1 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product1 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Product1?.setPhotoback) {
+ await Product1.setPhotoback(relatedPhotoback1);
+ }
+
+ const relatedPhotoback2 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product2 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Product2?.setPhotoback) {
+ await Product2.setPhotoback(relatedPhotoback2);
+ }
+
+ const relatedPhotoback3 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product3 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Product3?.setPhotoback) {
+ await Product3.setPhotoback(relatedPhotoback3);
+ }
+
+ const relatedPhotoback4 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Product4 = await Products.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Product4?.setPhotoback) {
+ await Product4.setPhotoback(relatedPhotoback4);
+ }
+}
+
+async function associateLocationWithUser() {
+ const relatedUser0 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Location0 = await Locations.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Location0?.setUser) {
+ await Location0.setUser(relatedUser0);
+ }
+
+ const relatedUser1 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Location1 = await Locations.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Location1?.setUser) {
+ await Location1.setUser(relatedUser1);
+ }
+
+ const relatedUser2 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Location2 = await Locations.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Location2?.setUser) {
+ await Location2.setUser(relatedUser2);
+ }
+
+ const relatedUser3 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Location3 = await Locations.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Location3?.setUser) {
+ await Location3.setUser(relatedUser3);
+ }
+
+ const relatedUser4 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Location4 = await Locations.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Location4?.setUser) {
+ await Location4.setUser(relatedUser4);
}
}
@@ -396,89 +955,654 @@ async function associateBottleWithUser() {
}
}
-async function associateBrandWithDistillery() {
- const relatedDistillery0 = await Distilleries.findOne({
- offset: Math.floor(Math.random() * (await Distilleries.count())),
+async function associateBottleWithProduct() {
+ const relatedProduct0 = await Products.findOne({
+ offset: Math.floor(Math.random() * (await Products.count())),
});
- const Brand0 = await Brands.findOne({
+ const Bottle0 = await Bottles.findOne({
order: [['id', 'ASC']],
offset: 0,
});
- if (Brand0?.setDistillery) {
- await Brand0.setDistillery(relatedDistillery0);
+ if (Bottle0?.setProduct) {
+ await Bottle0.setProduct(relatedProduct0);
}
- const relatedDistillery1 = await Distilleries.findOne({
- offset: Math.floor(Math.random() * (await Distilleries.count())),
+ const relatedProduct1 = await Products.findOne({
+ offset: Math.floor(Math.random() * (await Products.count())),
});
- const Brand1 = await Brands.findOne({
+ const Bottle1 = await Bottles.findOne({
order: [['id', 'ASC']],
offset: 1,
});
- if (Brand1?.setDistillery) {
- await Brand1.setDistillery(relatedDistillery1);
+ if (Bottle1?.setProduct) {
+ await Bottle1.setProduct(relatedProduct1);
}
- const relatedDistillery2 = await Distilleries.findOne({
- offset: Math.floor(Math.random() * (await Distilleries.count())),
+ const relatedProduct2 = await Products.findOne({
+ offset: Math.floor(Math.random() * (await Products.count())),
});
- const Brand2 = await Brands.findOne({
+ const Bottle2 = await Bottles.findOne({
order: [['id', 'ASC']],
offset: 2,
});
- if (Brand2?.setDistillery) {
- await Brand2.setDistillery(relatedDistillery2);
+ if (Bottle2?.setProduct) {
+ await Bottle2.setProduct(relatedProduct2);
}
- const relatedDistillery3 = await Distilleries.findOne({
- offset: Math.floor(Math.random() * (await Distilleries.count())),
+ const relatedProduct3 = await Products.findOne({
+ offset: Math.floor(Math.random() * (await Products.count())),
});
- const Brand3 = await Brands.findOne({
+ const Bottle3 = await Bottles.findOne({
order: [['id', 'ASC']],
offset: 3,
});
- if (Brand3?.setDistillery) {
- await Brand3.setDistillery(relatedDistillery3);
+ if (Bottle3?.setProduct) {
+ await Bottle3.setProduct(relatedProduct3);
}
- const relatedDistillery4 = await Distilleries.findOne({
- offset: Math.floor(Math.random() * (await Distilleries.count())),
+ const relatedProduct4 = await Products.findOne({
+ offset: Math.floor(Math.random() * (await Products.count())),
});
- const Brand4 = await Brands.findOne({
+ const Bottle4 = await Bottles.findOne({
order: [['id', 'ASC']],
offset: 4,
});
- if (Brand4?.setDistillery) {
- await Brand4.setDistillery(relatedDistillery4);
+ if (Bottle4?.setProduct) {
+ await Bottle4.setProduct(relatedProduct4);
+ }
+}
+
+async function associateBottleWithLocation() {
+ const relatedLocation0 = await Locations.findOne({
+ offset: Math.floor(Math.random() * (await Locations.count())),
+ });
+ const Bottle0 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Bottle0?.setLocation) {
+ await Bottle0.setLocation(relatedLocation0);
+ }
+
+ const relatedLocation1 = await Locations.findOne({
+ offset: Math.floor(Math.random() * (await Locations.count())),
+ });
+ const Bottle1 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Bottle1?.setLocation) {
+ await Bottle1.setLocation(relatedLocation1);
+ }
+
+ const relatedLocation2 = await Locations.findOne({
+ offset: Math.floor(Math.random() * (await Locations.count())),
+ });
+ const Bottle2 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Bottle2?.setLocation) {
+ await Bottle2.setLocation(relatedLocation2);
+ }
+
+ const relatedLocation3 = await Locations.findOne({
+ offset: Math.floor(Math.random() * (await Locations.count())),
+ });
+ const Bottle3 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Bottle3?.setLocation) {
+ await Bottle3.setLocation(relatedLocation3);
+ }
+
+ const relatedLocation4 = await Locations.findOne({
+ offset: Math.floor(Math.random() * (await Locations.count())),
+ });
+ const Bottle4 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Bottle4?.setLocation) {
+ await Bottle4.setLocation(relatedLocation4);
+ }
+}
+
+async function associateBottleWithPhotofront() {
+ const relatedPhotofront0 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle0 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Bottle0?.setPhotofront) {
+ await Bottle0.setPhotofront(relatedPhotofront0);
+ }
+
+ const relatedPhotofront1 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle1 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Bottle1?.setPhotofront) {
+ await Bottle1.setPhotofront(relatedPhotofront1);
+ }
+
+ const relatedPhotofront2 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle2 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Bottle2?.setPhotofront) {
+ await Bottle2.setPhotofront(relatedPhotofront2);
+ }
+
+ const relatedPhotofront3 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle3 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Bottle3?.setPhotofront) {
+ await Bottle3.setPhotofront(relatedPhotofront3);
+ }
+
+ const relatedPhotofront4 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle4 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Bottle4?.setPhotofront) {
+ await Bottle4.setPhotofront(relatedPhotofront4);
+ }
+}
+
+async function associateBottleWithPhotoback() {
+ const relatedPhotoback0 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle0 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Bottle0?.setPhotoback) {
+ await Bottle0.setPhotoback(relatedPhotoback0);
+ }
+
+ const relatedPhotoback1 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle1 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Bottle1?.setPhotoback) {
+ await Bottle1.setPhotoback(relatedPhotoback1);
+ }
+
+ const relatedPhotoback2 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle2 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Bottle2?.setPhotoback) {
+ await Bottle2.setPhotoback(relatedPhotoback2);
+ }
+
+ const relatedPhotoback3 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle3 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Bottle3?.setPhotoback) {
+ await Bottle3.setPhotoback(relatedPhotoback3);
+ }
+
+ const relatedPhotoback4 = await Photos.findOne({
+ offset: Math.floor(Math.random() * (await Photos.count())),
+ });
+ const Bottle4 = await Bottles.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Bottle4?.setPhotoback) {
+ await Bottle4.setPhotoback(relatedPhotoback4);
+ }
+}
+
+async function associateReviewWithUser() {
+ const relatedUser0 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Review0 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Review0?.setUser) {
+ await Review0.setUser(relatedUser0);
+ }
+
+ const relatedUser1 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Review1 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Review1?.setUser) {
+ await Review1.setUser(relatedUser1);
+ }
+
+ const relatedUser2 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Review2 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Review2?.setUser) {
+ await Review2.setUser(relatedUser2);
+ }
+
+ const relatedUser3 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Review3 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Review3?.setUser) {
+ await Review3.setUser(relatedUser3);
+ }
+
+ const relatedUser4 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Review4 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Review4?.setUser) {
+ await Review4.setUser(relatedUser4);
+ }
+}
+
+async function associateReviewWithBottle() {
+ const relatedBottle0 = await Bottles.findOne({
+ offset: Math.floor(Math.random() * (await Bottles.count())),
+ });
+ const Review0 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Review0?.setBottle) {
+ await Review0.setBottle(relatedBottle0);
+ }
+
+ const relatedBottle1 = await Bottles.findOne({
+ offset: Math.floor(Math.random() * (await Bottles.count())),
+ });
+ const Review1 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Review1?.setBottle) {
+ await Review1.setBottle(relatedBottle1);
+ }
+
+ const relatedBottle2 = await Bottles.findOne({
+ offset: Math.floor(Math.random() * (await Bottles.count())),
+ });
+ const Review2 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Review2?.setBottle) {
+ await Review2.setBottle(relatedBottle2);
+ }
+
+ const relatedBottle3 = await Bottles.findOne({
+ offset: Math.floor(Math.random() * (await Bottles.count())),
+ });
+ const Review3 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Review3?.setBottle) {
+ await Review3.setBottle(relatedBottle3);
+ }
+
+ const relatedBottle4 = await Bottles.findOne({
+ offset: Math.floor(Math.random() * (await Bottles.count())),
+ });
+ const Review4 = await Reviews.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Review4?.setBottle) {
+ await Review4.setBottle(relatedBottle4);
+ }
+}
+
+async function associateMessageWithConversation() {
+ const relatedConversation0 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Message0 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Message0?.setConversation) {
+ await Message0.setConversation(relatedConversation0);
+ }
+
+ const relatedConversation1 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Message1 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Message1?.setConversation) {
+ await Message1.setConversation(relatedConversation1);
+ }
+
+ const relatedConversation2 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Message2 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Message2?.setConversation) {
+ await Message2.setConversation(relatedConversation2);
+ }
+
+ const relatedConversation3 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Message3 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Message3?.setConversation) {
+ await Message3.setConversation(relatedConversation3);
+ }
+
+ const relatedConversation4 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Message4 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Message4?.setConversation) {
+ await Message4.setConversation(relatedConversation4);
+ }
+}
+
+async function associateMessageWithSender() {
+ const relatedSender0 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Message0 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Message0?.setSender) {
+ await Message0.setSender(relatedSender0);
+ }
+
+ const relatedSender1 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Message1 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Message1?.setSender) {
+ await Message1.setSender(relatedSender1);
+ }
+
+ const relatedSender2 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Message2 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Message2?.setSender) {
+ await Message2.setSender(relatedSender2);
+ }
+
+ const relatedSender3 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Message3 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Message3?.setSender) {
+ await Message3.setSender(relatedSender3);
+ }
+
+ const relatedSender4 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Message4 = await Messages.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Message4?.setSender) {
+ await Message4.setSender(relatedSender4);
+ }
+}
+
+async function associateConversationparticipantWithConversation() {
+ const relatedConversation0 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Conversationparticipant0 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Conversationparticipant0?.setConversation) {
+ await Conversationparticipant0.setConversation(relatedConversation0);
+ }
+
+ const relatedConversation1 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Conversationparticipant1 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Conversationparticipant1?.setConversation) {
+ await Conversationparticipant1.setConversation(relatedConversation1);
+ }
+
+ const relatedConversation2 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Conversationparticipant2 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Conversationparticipant2?.setConversation) {
+ await Conversationparticipant2.setConversation(relatedConversation2);
+ }
+
+ const relatedConversation3 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Conversationparticipant3 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Conversationparticipant3?.setConversation) {
+ await Conversationparticipant3.setConversation(relatedConversation3);
+ }
+
+ const relatedConversation4 = await Conversations.findOne({
+ offset: Math.floor(Math.random() * (await Conversations.count())),
+ });
+ const Conversationparticipant4 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Conversationparticipant4?.setConversation) {
+ await Conversationparticipant4.setConversation(relatedConversation4);
+ }
+}
+
+async function associateConversationparticipantWithUser() {
+ const relatedUser0 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Conversationparticipant0 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 0,
+ });
+ if (Conversationparticipant0?.setUser) {
+ await Conversationparticipant0.setUser(relatedUser0);
+ }
+
+ const relatedUser1 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Conversationparticipant1 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 1,
+ });
+ if (Conversationparticipant1?.setUser) {
+ await Conversationparticipant1.setUser(relatedUser1);
+ }
+
+ const relatedUser2 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Conversationparticipant2 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 2,
+ });
+ if (Conversationparticipant2?.setUser) {
+ await Conversationparticipant2.setUser(relatedUser2);
+ }
+
+ const relatedUser3 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Conversationparticipant3 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 3,
+ });
+ if (Conversationparticipant3?.setUser) {
+ await Conversationparticipant3.setUser(relatedUser3);
+ }
+
+ const relatedUser4 = await Users.findOne({
+ offset: Math.floor(Math.random() * (await Users.count())),
+ });
+ const Conversationparticipant4 = await Conversationparticipants.findOne({
+ order: [['id', 'ASC']],
+ offset: 4,
+ });
+ if (Conversationparticipant4?.setUser) {
+ await Conversationparticipant4.setUser(relatedUser4);
}
}
module.exports = {
up: async (queryInterface, Sequelize) => {
- await Bottles.bulkCreate(BottlesData);
+ await Distilleries.bulkCreate(DistilleriesData);
await Brands.bulkCreate(BrandsData);
- await Distilleries.bulkCreate(DistilleriesData);
+ await Photos.bulkCreate(PhotosData);
+
+ await Products.bulkCreate(ProductsData);
+
+ await Locations.bulkCreate(LocationsData);
+
+ await Bottles.bulkCreate(BottlesData);
+
+ await Reviews.bulkCreate(ReviewsData);
+
+ await Conversations.bulkCreate(ConversationsData);
+
+ await Messages.bulkCreate(MessagesData);
+
+ await Conversationparticipants.bulkCreate(ConversationparticipantsData);
await Promise.all([
// Similar logic for "relation_many"
- await associateBottleWithBrand(),
+ await associateBrandWithDistillery(),
- await associateBottleWithDistillery(),
+ await associateProductWithBrand(),
+
+ await associateProductWithPhotofront(),
+
+ await associateProductWithPhotoback(),
+
+ await associateLocationWithUser(),
await associateBottleWithUser(),
- await associateBrandWithDistillery(),
+ await associateBottleWithProduct(),
+
+ await associateBottleWithLocation(),
+
+ await associateBottleWithPhotofront(),
+
+ await associateBottleWithPhotoback(),
+
+ await associateReviewWithUser(),
+
+ await associateReviewWithBottle(),
+
+ await associateMessageWithConversation(),
+
+ await associateMessageWithSender(),
+
+ await associateConversationparticipantWithConversation(),
+
+ await associateConversationparticipantWithUser(),
]);
},
down: async (queryInterface, Sequelize) => {
- await queryInterface.bulkDelete('bottles', null, {});
+ await queryInterface.bulkDelete('distilleries', null, {});
await queryInterface.bulkDelete('brands', null, {});
- await queryInterface.bulkDelete('distilleries', null, {});
+ await queryInterface.bulkDelete('photos', null, {});
+
+ await queryInterface.bulkDelete('products', null, {});
+
+ await queryInterface.bulkDelete('locations', null, {});
+
+ await queryInterface.bulkDelete('bottles', null, {});
+
+ await queryInterface.bulkDelete('reviews', null, {});
+
+ await queryInterface.bulkDelete('conversations', null, {});
+
+ await queryInterface.bulkDelete('messages', null, {});
+
+ await queryInterface.bulkDelete('conversationparticipants', null, {});
},
};
diff --git a/backend/src/db/seeders/20250826182953.js b/backend/src/db/seeders/20250826182953.js
new file mode 100644
index 0000000..b2ab8cb
--- /dev/null
+++ b/backend/src/db/seeders/20250826182953.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 = ['distilleries'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826183154.js b/backend/src/db/seeders/20250826183154.js
new file mode 100644
index 0000000..11a8ae8
--- /dev/null
+++ b/backend/src/db/seeders/20250826183154.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 = ['brands'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826183241.js b/backend/src/db/seeders/20250826183241.js
new file mode 100644
index 0000000..b656561
--- /dev/null
+++ b/backend/src/db/seeders/20250826183241.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 = ['photos'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826183442.js b/backend/src/db/seeders/20250826183442.js
new file mode 100644
index 0000000..8d90697
--- /dev/null
+++ b/backend/src/db/seeders/20250826183442.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 = ['products'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826183740.js b/backend/src/db/seeders/20250826183740.js
new file mode 100644
index 0000000..962dba6
--- /dev/null
+++ b/backend/src/db/seeders/20250826183740.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 = ['locations'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826184717.js b/backend/src/db/seeders/20250826184717.js
new file mode 100644
index 0000000..332f374
--- /dev/null
+++ b/backend/src/db/seeders/20250826184717.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 = ['bottles'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826185515.js b/backend/src/db/seeders/20250826185515.js
new file mode 100644
index 0000000..8647e0a
--- /dev/null
+++ b/backend/src/db/seeders/20250826185515.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 = ['reviews'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826185749.js b/backend/src/db/seeders/20250826185749.js
new file mode 100644
index 0000000..2133d5d
--- /dev/null
+++ b/backend/src/db/seeders/20250826185749.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 = ['conversations'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826185841.js b/backend/src/db/seeders/20250826185841.js
new file mode 100644
index 0000000..be900db
--- /dev/null
+++ b/backend/src/db/seeders/20250826185841.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 = ['messages'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/db/seeders/20250826185959.js b/backend/src/db/seeders/20250826185959.js
new file mode 100644
index 0000000..5425655
--- /dev/null
+++ b/backend/src/db/seeders/20250826185959.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 = ['conversationparticipants'];
+
+ const createdPermissions = entities.flatMap(createPermissions);
+
+ // Add permissions to database
+ await queryInterface.bulkInsert('permissions', createdPermissions);
+ // Get permissions ids
+ const permissionsIds = createdPermissions.map((p) => p.id);
+ // Get admin role
+ const adminRole = await db.roles.findOne({
+ where: { name: config.roles.admin },
+ });
+
+ if (adminRole) {
+ // Add permissions to admin role if it exists
+ await adminRole.addPermissions(permissionsIds);
+ }
+ },
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.bulkDelete(
+ 'permissions',
+ entities.flatMap(createPermissions),
+ );
+ },
+};
diff --git a/backend/src/index.js b/backend/src/index.js
index 48986df..17cf23e 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -21,16 +21,30 @@ const contactFormRoutes = require('./routes/contactForm');
const usersRoutes = require('./routes/users');
-const bottlesRoutes = require('./routes/bottles');
-
-const brandsRoutes = require('./routes/brands');
-
-const distilleriesRoutes = require('./routes/distilleries');
-
const rolesRoutes = require('./routes/roles');
const permissionsRoutes = require('./routes/permissions');
+const distilleriesRoutes = require('./routes/distilleries');
+
+const brandsRoutes = require('./routes/brands');
+
+const photosRoutes = require('./routes/photos');
+
+const productsRoutes = require('./routes/products');
+
+const locationsRoutes = require('./routes/locations');
+
+const bottlesRoutes = require('./routes/bottles');
+
+const reviewsRoutes = require('./routes/reviews');
+
+const conversationsRoutes = require('./routes/conversations');
+
+const messagesRoutes = require('./routes/messages');
+
+const conversationparticipantsRoutes = require('./routes/conversationparticipants');
+
const getBaseUrl = (url) => {
if (!url) return '';
return url.endsWith('/api') ? url.slice(0, -4) : url;
@@ -102,24 +116,6 @@ app.use(
usersRoutes,
);
-app.use(
- '/api/bottles',
- passport.authenticate('jwt', { session: false }),
- bottlesRoutes,
-);
-
-app.use(
- '/api/brands',
- passport.authenticate('jwt', { session: false }),
- brandsRoutes,
-);
-
-app.use(
- '/api/distilleries',
- passport.authenticate('jwt', { session: false }),
- distilleriesRoutes,
-);
-
app.use(
'/api/roles',
passport.authenticate('jwt', { session: false }),
@@ -132,6 +128,66 @@ app.use(
permissionsRoutes,
);
+app.use(
+ '/api/distilleries',
+ passport.authenticate('jwt', { session: false }),
+ distilleriesRoutes,
+);
+
+app.use(
+ '/api/brands',
+ passport.authenticate('jwt', { session: false }),
+ brandsRoutes,
+);
+
+app.use(
+ '/api/photos',
+ passport.authenticate('jwt', { session: false }),
+ photosRoutes,
+);
+
+app.use(
+ '/api/products',
+ passport.authenticate('jwt', { session: false }),
+ productsRoutes,
+);
+
+app.use(
+ '/api/locations',
+ passport.authenticate('jwt', { session: false }),
+ locationsRoutes,
+);
+
+app.use(
+ '/api/bottles',
+ passport.authenticate('jwt', { session: false }),
+ bottlesRoutes,
+);
+
+app.use(
+ '/api/reviews',
+ passport.authenticate('jwt', { session: false }),
+ reviewsRoutes,
+);
+
+app.use(
+ '/api/conversations',
+ passport.authenticate('jwt', { session: false }),
+ conversationsRoutes,
+);
+
+app.use(
+ '/api/messages',
+ passport.authenticate('jwt', { session: false }),
+ messagesRoutes,
+);
+
+app.use(
+ '/api/conversationparticipants',
+ passport.authenticate('jwt', { session: false }),
+ conversationparticipantsRoutes,
+);
+
app.use(
'/api/openai',
passport.authenticate('jwt', { session: false }),
diff --git a/backend/src/routes/bottles.js b/backend/src/routes/bottles.js
index f66fb05..5ae1ee2 100644
--- a/backend/src/routes/bottles.js
+++ b/backend/src/routes/bottles.js
@@ -20,37 +20,39 @@ router.use(checkCrudPermissions('bottles'));
* type: object
* properties:
- * name:
+ * rickhouse:
* type: string
- * default: name
+ * default: rickhouse
+ * rack:
+ * type: string
+ * default: rack
+ * release:
+ * type: string
+ * default: release
+ * barrelnumber:
+ * type: string
+ * default: barrelnumber
+ * bottlenumber:
+ * type: string
+ * default: bottlenumber
* notes:
* type: string
* default: notes
- * tasting_notes:
- * type: string
- * default: tasting_notes
- * msrp_range:
- * type: string
- * default: msrp_range
- * secondary_value_range:
- * type: string
- * default: secondary_value_range
- * barcode:
- * type: string
- * default: barcode
- * quantity:
+ * age:
+ * type: integer
+ * format: int64
+ * rating:
+ * type: integer
+ * format: int64
+ * volume:
* type: integer
* format: int64
* proof:
* type: integer
* format: int64
- * age:
- * type: integer
- * format: int64
- *
*/
/**
@@ -333,15 +335,19 @@ router.get(
if (filetype && filetype === 'csv') {
const fields = [
'id',
- 'name',
+ 'rickhouse',
+ 'rack',
+ 'release',
+ 'barrelnumber',
+ 'bottlenumber',
'notes',
- 'tasting_notes',
- 'msrp_range',
- 'secondary_value_range',
- 'barcode',
- 'quantity',
- 'proof',
'age',
+ 'rating',
+ 'volume',
+ 'proof',
+
+ 'barreleddate',
+ 'dateacquired',
];
const opts = { fields };
try {
diff --git a/backend/src/routes/brands.js b/backend/src/routes/brands.js
index f5a6622..c5f16a5 100644
--- a/backend/src/routes/brands.js
+++ b/backend/src/routes/brands.js
@@ -24,6 +24,10 @@ router.use(checkCrudPermissions('brands'));
* type: string
* default: name
+ * status:
+ * type: integer
+ * format: int64
+
*/
/**
@@ -299,7 +303,7 @@ router.get(
const currentUser = req.currentUser;
const payload = await BrandsDBApi.findAll(req.query, { currentUser });
if (filetype && filetype === 'csv') {
- const fields = ['id', 'name'];
+ const fields = ['id', 'name', 'status'];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
diff --git a/backend/src/routes/conversationparticipants.js b/backend/src/routes/conversationparticipants.js
new file mode 100644
index 0000000..3b5148d
--- /dev/null
+++ b/backend/src/routes/conversationparticipants.js
@@ -0,0 +1,449 @@
+const express = require('express');
+
+const ConversationparticipantsService = require('../services/conversationparticipants');
+const ConversationparticipantsDBApi = require('../db/api/conversationparticipants');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const { parse } = require('json2csv');
+
+const { checkCrudPermissions } = require('../middlewares/check-permissions');
+
+router.use(checkCrudPermissions('conversationparticipants'));
+
+/**
+ * @swagger
+ * components:
+ * schemas:
+ * Conversationparticipants:
+ * type: object
+ * properties:
+
+ */
+
+/**
+ * @swagger
+ * tags:
+ * name: Conversationparticipants
+ * description: The Conversationparticipants managing API
+ */
+
+/**
+ * @swagger
+ * /api/conversationparticipants:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversationparticipants]
+ * 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/Conversationparticipants"
+ * responses:
+ * 200:
+ * description: The item was successfully added
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Conversationparticipants"
+ * 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 ConversationparticipantsService.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: [Conversationparticipants]
+ * 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/Conversationparticipants"
+ * responses:
+ * 200:
+ * description: The items were successfully imported
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Conversationparticipants"
+ * 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 ConversationparticipantsService.bulkImport(req, res, true, link.host);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversationparticipants/{id}:
+ * put:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversationparticipants]
+ * 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/Conversationparticipants"
+ * required:
+ * - id
+ * responses:
+ * 200:
+ * description: The item data was successfully updated
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Conversationparticipants"
+ * 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 ConversationparticipantsService.update(
+ req.body.data,
+ req.body.id,
+ req.currentUser,
+ );
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversationparticipants/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversationparticipants]
+ * 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/Conversationparticipants"
+ * 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 ConversationparticipantsService.remove(
+ req.params.id,
+ req.currentUser,
+ );
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversationparticipants/deleteByIds:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversationparticipants]
+ * 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/Conversationparticipants"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Items not found
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/deleteByIds',
+ wrapAsync(async (req, res) => {
+ await ConversationparticipantsService.deleteByIds(
+ req.body.data,
+ req.currentUser,
+ );
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversationparticipants:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversationparticipants]
+ * summary: Get all conversationparticipants
+ * description: Get all conversationparticipants
+ * responses:
+ * 200:
+ * description: Conversationparticipants list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Conversationparticipants"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/',
+ wrapAsync(async (req, res) => {
+ const filetype = req.query.filetype;
+
+ const currentUser = req.currentUser;
+ const payload = await ConversationparticipantsDBApi.findAll(req.query, {
+ currentUser,
+ });
+ if (filetype && filetype === 'csv') {
+ const fields = ['id'];
+ 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/conversationparticipants/count:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversationparticipants]
+ * summary: Count all conversationparticipants
+ * description: Count all conversationparticipants
+ * responses:
+ * 200:
+ * description: Conversationparticipants count successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Conversationparticipants"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/count',
+ wrapAsync(async (req, res) => {
+ const currentUser = req.currentUser;
+ const payload = await ConversationparticipantsDBApi.findAll(
+ req.query,
+ null,
+ { countOnly: true, currentUser },
+ );
+
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversationparticipants/autocomplete:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversationparticipants]
+ * summary: Find all conversationparticipants that match search criteria
+ * description: Find all conversationparticipants that match search criteria
+ * responses:
+ * 200:
+ * description: Conversationparticipants list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Conversationparticipants"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get('/autocomplete', async (req, res) => {
+ const payload = await ConversationparticipantsDBApi.findAllAutocomplete(
+ req.query.query,
+ req.query.limit,
+ req.query.offset,
+ );
+
+ res.status(200).send(payload);
+});
+
+/**
+ * @swagger
+ * /api/conversationparticipants/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversationparticipants]
+ * 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/Conversationparticipants"
+ * 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 ConversationparticipantsDBApi.findBy({
+ id: req.params.id,
+ });
+
+ res.status(200).send(payload);
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/backend/src/routes/conversations.js b/backend/src/routes/conversations.js
new file mode 100644
index 0000000..0f681ef
--- /dev/null
+++ b/backend/src/routes/conversations.js
@@ -0,0 +1,440 @@
+const express = require('express');
+
+const ConversationsService = require('../services/conversations');
+const ConversationsDBApi = require('../db/api/conversations');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const { parse } = require('json2csv');
+
+const { checkCrudPermissions } = require('../middlewares/check-permissions');
+
+router.use(checkCrudPermissions('conversations'));
+
+/**
+ * @swagger
+ * components:
+ * schemas:
+ * Conversations:
+ * type: object
+ * properties:
+
+ */
+
+/**
+ * @swagger
+ * tags:
+ * name: Conversations
+ * description: The Conversations managing API
+ */
+
+/**
+ * @swagger
+ * /api/conversations:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversations]
+ * 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/Conversations"
+ * responses:
+ * 200:
+ * description: The item was successfully added
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Conversations"
+ * 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 ConversationsService.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: [Conversations]
+ * 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/Conversations"
+ * responses:
+ * 200:
+ * description: The items were successfully imported
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Conversations"
+ * 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 ConversationsService.bulkImport(req, res, true, link.host);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversations/{id}:
+ * put:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversations]
+ * 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/Conversations"
+ * required:
+ * - id
+ * responses:
+ * 200:
+ * description: The item data was successfully updated
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Conversations"
+ * 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 ConversationsService.update(
+ req.body.data,
+ req.body.id,
+ req.currentUser,
+ );
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversations/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversations]
+ * 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/Conversations"
+ * 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 ConversationsService.remove(req.params.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversations/deleteByIds:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversations]
+ * 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/Conversations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Items not found
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/deleteByIds',
+ wrapAsync(async (req, res) => {
+ await ConversationsService.deleteByIds(req.body.data, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversations:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversations]
+ * summary: Get all conversations
+ * description: Get all conversations
+ * responses:
+ * 200:
+ * description: Conversations list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Conversations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/',
+ wrapAsync(async (req, res) => {
+ const filetype = req.query.filetype;
+
+ const currentUser = req.currentUser;
+ const payload = await ConversationsDBApi.findAll(req.query, {
+ currentUser,
+ });
+ if (filetype && filetype === 'csv') {
+ const fields = ['id', 'createdat'];
+ 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/conversations/count:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversations]
+ * summary: Count all conversations
+ * description: Count all conversations
+ * responses:
+ * 200:
+ * description: Conversations count successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Conversations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/count',
+ wrapAsync(async (req, res) => {
+ const currentUser = req.currentUser;
+ const payload = await ConversationsDBApi.findAll(req.query, null, {
+ countOnly: true,
+ currentUser,
+ });
+
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/conversations/autocomplete:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversations]
+ * summary: Find all conversations that match search criteria
+ * description: Find all conversations that match search criteria
+ * responses:
+ * 200:
+ * description: Conversations list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Conversations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get('/autocomplete', async (req, res) => {
+ const payload = await ConversationsDBApi.findAllAutocomplete(
+ req.query.query,
+ req.query.limit,
+ req.query.offset,
+ );
+
+ res.status(200).send(payload);
+});
+
+/**
+ * @swagger
+ * /api/conversations/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Conversations]
+ * 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/Conversations"
+ * 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 ConversationsDBApi.findBy({ id: req.params.id });
+
+ res.status(200).send(payload);
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/backend/src/routes/distilleries.js b/backend/src/routes/distilleries.js
index 78d04b0..5c8959e 100644
--- a/backend/src/routes/distilleries.js
+++ b/backend/src/routes/distilleries.js
@@ -23,6 +23,16 @@ router.use(checkCrudPermissions('distilleries'));
* name:
* type: string
* default: name
+ * city:
+ * type: string
+ * default: city
+ * state:
+ * type: string
+ * default: state
+
+ * status:
+ * type: integer
+ * format: int64
*/
@@ -308,7 +318,7 @@ router.get(
const currentUser = req.currentUser;
const payload = await DistilleriesDBApi.findAll(req.query, { currentUser });
if (filetype && filetype === 'csv') {
- const fields = ['id', 'name'];
+ const fields = ['id', 'name', 'city', 'state', 'status'];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
diff --git a/backend/src/routes/locations.js b/backend/src/routes/locations.js
new file mode 100644
index 0000000..6859012
--- /dev/null
+++ b/backend/src/routes/locations.js
@@ -0,0 +1,438 @@
+const express = require('express');
+
+const LocationsService = require('../services/locations');
+const LocationsDBApi = require('../db/api/locations');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const { parse } = require('json2csv');
+
+const { checkCrudPermissions } = require('../middlewares/check-permissions');
+
+router.use(checkCrudPermissions('locations'));
+
+/**
+ * @swagger
+ * components:
+ * schemas:
+ * Locations:
+ * type: object
+ * properties:
+
+ * name:
+ * type: string
+ * default: name
+
+ */
+
+/**
+ * @swagger
+ * tags:
+ * name: Locations
+ * description: The Locations managing API
+ */
+
+/**
+ * @swagger
+ * /api/locations:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Locations]
+ * 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/Locations"
+ * responses:
+ * 200:
+ * description: The item was successfully added
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Locations"
+ * 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 LocationsService.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: [Locations]
+ * 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/Locations"
+ * responses:
+ * 200:
+ * description: The items were successfully imported
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Locations"
+ * 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 LocationsService.bulkImport(req, res, true, link.host);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/locations/{id}:
+ * put:
+ * security:
+ * - bearerAuth: []
+ * tags: [Locations]
+ * 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/Locations"
+ * required:
+ * - id
+ * responses:
+ * 200:
+ * description: The item data was successfully updated
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Locations"
+ * 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 LocationsService.update(req.body.data, req.body.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/locations/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags: [Locations]
+ * 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/Locations"
+ * 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 LocationsService.remove(req.params.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/locations/deleteByIds:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Locations]
+ * 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/Locations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Items not found
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/deleteByIds',
+ wrapAsync(async (req, res) => {
+ await LocationsService.deleteByIds(req.body.data, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/locations:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Locations]
+ * summary: Get all locations
+ * description: Get all locations
+ * responses:
+ * 200:
+ * description: Locations list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Locations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/',
+ wrapAsync(async (req, res) => {
+ const filetype = req.query.filetype;
+
+ const currentUser = req.currentUser;
+ const payload = await LocationsDBApi.findAll(req.query, { currentUser });
+ if (filetype && filetype === 'csv') {
+ const fields = ['id', 'name'];
+ 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/locations/count:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Locations]
+ * summary: Count all locations
+ * description: Count all locations
+ * responses:
+ * 200:
+ * description: Locations count successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Locations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/count',
+ wrapAsync(async (req, res) => {
+ const currentUser = req.currentUser;
+ const payload = await LocationsDBApi.findAll(req.query, null, {
+ countOnly: true,
+ currentUser,
+ });
+
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/locations/autocomplete:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Locations]
+ * summary: Find all locations that match search criteria
+ * description: Find all locations that match search criteria
+ * responses:
+ * 200:
+ * description: Locations list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Locations"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get('/autocomplete', async (req, res) => {
+ const payload = await LocationsDBApi.findAllAutocomplete(
+ req.query.query,
+ req.query.limit,
+ req.query.offset,
+ );
+
+ res.status(200).send(payload);
+});
+
+/**
+ * @swagger
+ * /api/locations/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Locations]
+ * 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/Locations"
+ * 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 LocationsDBApi.findBy({ id: req.params.id });
+
+ res.status(200).send(payload);
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/backend/src/routes/messages.js b/backend/src/routes/messages.js
new file mode 100644
index 0000000..7722f84
--- /dev/null
+++ b/backend/src/routes/messages.js
@@ -0,0 +1,434 @@
+const express = require('express');
+
+const MessagesService = require('../services/messages');
+const MessagesDBApi = require('../db/api/messages');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const { parse } = require('json2csv');
+
+const { checkCrudPermissions } = require('../middlewares/check-permissions');
+
+router.use(checkCrudPermissions('messages'));
+
+/**
+ * @swagger
+ * components:
+ * schemas:
+ * Messages:
+ * type: object
+ * properties:
+
+ */
+
+/**
+ * @swagger
+ * tags:
+ * name: Messages
+ * description: The Messages managing API
+ */
+
+/**
+ * @swagger
+ * /api/messages:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Messages]
+ * 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/Messages"
+ * responses:
+ * 200:
+ * description: The item was successfully added
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Messages"
+ * 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 MessagesService.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: [Messages]
+ * 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/Messages"
+ * responses:
+ * 200:
+ * description: The items were successfully imported
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Messages"
+ * 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 MessagesService.bulkImport(req, res, true, link.host);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/messages/{id}:
+ * put:
+ * security:
+ * - bearerAuth: []
+ * tags: [Messages]
+ * 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/Messages"
+ * required:
+ * - id
+ * responses:
+ * 200:
+ * description: The item data was successfully updated
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Messages"
+ * 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 MessagesService.update(req.body.data, req.body.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/messages/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags: [Messages]
+ * 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/Messages"
+ * 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 MessagesService.remove(req.params.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/messages/deleteByIds:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Messages]
+ * 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/Messages"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Items not found
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/deleteByIds',
+ wrapAsync(async (req, res) => {
+ await MessagesService.deleteByIds(req.body.data, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/messages:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Messages]
+ * summary: Get all messages
+ * description: Get all messages
+ * responses:
+ * 200:
+ * description: Messages list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Messages"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/',
+ wrapAsync(async (req, res) => {
+ const filetype = req.query.filetype;
+
+ const currentUser = req.currentUser;
+ const payload = await MessagesDBApi.findAll(req.query, { currentUser });
+ if (filetype && filetype === 'csv') {
+ const fields = ['id'];
+ 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/messages/count:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Messages]
+ * summary: Count all messages
+ * description: Count all messages
+ * responses:
+ * 200:
+ * description: Messages count successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Messages"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/count',
+ wrapAsync(async (req, res) => {
+ const currentUser = req.currentUser;
+ const payload = await MessagesDBApi.findAll(req.query, null, {
+ countOnly: true,
+ currentUser,
+ });
+
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/messages/autocomplete:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Messages]
+ * summary: Find all messages that match search criteria
+ * description: Find all messages that match search criteria
+ * responses:
+ * 200:
+ * description: Messages list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Messages"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get('/autocomplete', async (req, res) => {
+ const payload = await MessagesDBApi.findAllAutocomplete(
+ req.query.query,
+ req.query.limit,
+ req.query.offset,
+ );
+
+ res.status(200).send(payload);
+});
+
+/**
+ * @swagger
+ * /api/messages/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Messages]
+ * 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/Messages"
+ * 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 MessagesDBApi.findBy({ id: req.params.id });
+
+ res.status(200).send(payload);
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/backend/src/routes/photos.js b/backend/src/routes/photos.js
new file mode 100644
index 0000000..c862d62
--- /dev/null
+++ b/backend/src/routes/photos.js
@@ -0,0 +1,433 @@
+const express = require('express');
+
+const PhotosService = require('../services/photos');
+const PhotosDBApi = require('../db/api/photos');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const { parse } = require('json2csv');
+
+const { checkCrudPermissions } = require('../middlewares/check-permissions');
+
+router.use(checkCrudPermissions('photos'));
+
+/**
+ * @swagger
+ * components:
+ * schemas:
+ * Photos:
+ * type: object
+ * properties:
+
+ * phototype:
+ * type: string
+ * default: phototype
+
+ */
+
+/**
+ * @swagger
+ * tags:
+ * name: Photos
+ * description: The Photos managing API
+ */
+
+/**
+ * @swagger
+ * /api/photos:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Photos]
+ * 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/Photos"
+ * responses:
+ * 200:
+ * description: The item was successfully added
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Photos"
+ * 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 PhotosService.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: [Photos]
+ * 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/Photos"
+ * responses:
+ * 200:
+ * description: The items were successfully imported
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Photos"
+ * 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 PhotosService.bulkImport(req, res, true, link.host);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/photos/{id}:
+ * put:
+ * security:
+ * - bearerAuth: []
+ * tags: [Photos]
+ * 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/Photos"
+ * required:
+ * - id
+ * responses:
+ * 200:
+ * description: The item data was successfully updated
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Photos"
+ * 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 PhotosService.update(req.body.data, req.body.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/photos/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags: [Photos]
+ * 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/Photos"
+ * 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 PhotosService.remove(req.params.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/photos/deleteByIds:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Photos]
+ * 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/Photos"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Items not found
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/deleteByIds',
+ wrapAsync(async (req, res) => {
+ await PhotosService.deleteByIds(req.body.data, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/photos:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Photos]
+ * summary: Get all photos
+ * description: Get all photos
+ * responses:
+ * 200:
+ * description: Photos list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Photos"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/',
+ wrapAsync(async (req, res) => {
+ const filetype = req.query.filetype;
+
+ const currentUser = req.currentUser;
+ const payload = await PhotosDBApi.findAll(req.query, { currentUser });
+ if (filetype && filetype === 'csv') {
+ const fields = ['id', 'phototype'];
+ 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/photos/count:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Photos]
+ * summary: Count all photos
+ * description: Count all photos
+ * responses:
+ * 200:
+ * description: Photos count successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Photos"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/count',
+ wrapAsync(async (req, res) => {
+ const currentUser = req.currentUser;
+ const payload = await PhotosDBApi.findAll(req.query, null, {
+ countOnly: true,
+ currentUser,
+ });
+
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/photos/autocomplete:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Photos]
+ * summary: Find all photos that match search criteria
+ * description: Find all photos that match search criteria
+ * responses:
+ * 200:
+ * description: Photos list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Photos"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get('/autocomplete', async (req, res) => {
+ const payload = await PhotosDBApi.findAllAutocomplete(
+ req.query.query,
+ req.query.limit,
+ req.query.offset,
+ );
+
+ res.status(200).send(payload);
+});
+
+/**
+ * @swagger
+ * /api/photos/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Photos]
+ * 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/Photos"
+ * 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 PhotosDBApi.findBy({ id: req.params.id });
+
+ res.status(200).send(payload);
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/backend/src/routes/products.js b/backend/src/routes/products.js
new file mode 100644
index 0000000..a4ffa79
--- /dev/null
+++ b/backend/src/routes/products.js
@@ -0,0 +1,463 @@
+const express = require('express');
+
+const ProductsService = require('../services/products');
+const ProductsDBApi = require('../db/api/products');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const { parse } = require('json2csv');
+
+const { checkCrudPermissions } = require('../middlewares/check-permissions');
+
+router.use(checkCrudPermissions('products'));
+
+/**
+ * @swagger
+ * components:
+ * schemas:
+ * Products:
+ * type: object
+ * properties:
+
+ * name:
+ * type: string
+ * default: name
+ * barcode:
+ * type: string
+ * default: barcode
+ * notes:
+ * type: string
+ * default: notes
+
+ * age:
+ * type: integer
+ * format: int64
+ * status:
+ * type: integer
+ * format: int64
+
+ * proof:
+ * type: integer
+ * format: int64
+
+ */
+
+/**
+ * @swagger
+ * tags:
+ * name: Products
+ * description: The Products managing API
+ */
+
+/**
+ * @swagger
+ * /api/products:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Products]
+ * 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/Products"
+ * responses:
+ * 200:
+ * description: The item was successfully added
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Products"
+ * 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 ProductsService.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: [Products]
+ * 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/Products"
+ * responses:
+ * 200:
+ * description: The items were successfully imported
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Products"
+ * 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 ProductsService.bulkImport(req, res, true, link.host);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/products/{id}:
+ * put:
+ * security:
+ * - bearerAuth: []
+ * tags: [Products]
+ * 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/Products"
+ * required:
+ * - id
+ * responses:
+ * 200:
+ * description: The item data was successfully updated
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Products"
+ * 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 ProductsService.update(req.body.data, req.body.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/products/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags: [Products]
+ * 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/Products"
+ * 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 ProductsService.remove(req.params.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/products/deleteByIds:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Products]
+ * 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/Products"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Items not found
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/deleteByIds',
+ wrapAsync(async (req, res) => {
+ await ProductsService.deleteByIds(req.body.data, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/products:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Products]
+ * summary: Get all products
+ * description: Get all products
+ * responses:
+ * 200:
+ * description: Products list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Products"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/',
+ wrapAsync(async (req, res) => {
+ const filetype = req.query.filetype;
+
+ const currentUser = req.currentUser;
+ const payload = await ProductsDBApi.findAll(req.query, { currentUser });
+ if (filetype && filetype === 'csv') {
+ const fields = [
+ 'id',
+ 'name',
+ 'barcode',
+ 'notes',
+ 'age',
+ 'status',
+ 'proof',
+ ];
+ 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/products/count:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Products]
+ * summary: Count all products
+ * description: Count all products
+ * responses:
+ * 200:
+ * description: Products count successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Products"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/count',
+ wrapAsync(async (req, res) => {
+ const currentUser = req.currentUser;
+ const payload = await ProductsDBApi.findAll(req.query, null, {
+ countOnly: true,
+ currentUser,
+ });
+
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/products/autocomplete:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Products]
+ * summary: Find all products that match search criteria
+ * description: Find all products that match search criteria
+ * responses:
+ * 200:
+ * description: Products list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Products"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get('/autocomplete', async (req, res) => {
+ const payload = await ProductsDBApi.findAllAutocomplete(
+ req.query.query,
+ req.query.limit,
+ req.query.offset,
+ );
+
+ res.status(200).send(payload);
+});
+
+/**
+ * @swagger
+ * /api/products/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Products]
+ * 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/Products"
+ * 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 ProductsDBApi.findBy({ id: req.params.id });
+
+ res.status(200).send(payload);
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/backend/src/routes/reviews.js b/backend/src/routes/reviews.js
new file mode 100644
index 0000000..95e9665
--- /dev/null
+++ b/backend/src/routes/reviews.js
@@ -0,0 +1,442 @@
+const express = require('express');
+
+const ReviewsService = require('../services/reviews');
+const ReviewsDBApi = require('../db/api/reviews');
+const wrapAsync = require('../helpers').wrapAsync;
+
+const router = express.Router();
+
+const { parse } = require('json2csv');
+
+const { checkCrudPermissions } = require('../middlewares/check-permissions');
+
+router.use(checkCrudPermissions('reviews'));
+
+/**
+ * @swagger
+ * components:
+ * schemas:
+ * Reviews:
+ * type: object
+ * properties:
+
+ * notes:
+ * type: string
+ * default: notes
+
+ * rating:
+ * type: integer
+ * format: int64
+
+ */
+
+/**
+ * @swagger
+ * tags:
+ * name: Reviews
+ * description: The Reviews managing API
+ */
+
+/**
+ * @swagger
+ * /api/reviews:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Reviews]
+ * 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/Reviews"
+ * responses:
+ * 200:
+ * description: The item was successfully added
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Reviews"
+ * 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 ReviewsService.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: [Reviews]
+ * 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/Reviews"
+ * responses:
+ * 200:
+ * description: The items were successfully imported
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Reviews"
+ * 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 ReviewsService.bulkImport(req, res, true, link.host);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/reviews/{id}:
+ * put:
+ * security:
+ * - bearerAuth: []
+ * tags: [Reviews]
+ * 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/Reviews"
+ * required:
+ * - id
+ * responses:
+ * 200:
+ * description: The item data was successfully updated
+ * content:
+ * application/json:
+ * schema:
+ * $ref: "#/components/schemas/Reviews"
+ * 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 ReviewsService.update(req.body.data, req.body.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/reviews/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags: [Reviews]
+ * 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/Reviews"
+ * 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 ReviewsService.remove(req.params.id, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/reviews/deleteByIds:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags: [Reviews]
+ * 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/Reviews"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Items not found
+ * 500:
+ * description: Some server error
+ */
+router.post(
+ '/deleteByIds',
+ wrapAsync(async (req, res) => {
+ await ReviewsService.deleteByIds(req.body.data, req.currentUser);
+ const payload = true;
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/reviews:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Reviews]
+ * summary: Get all reviews
+ * description: Get all reviews
+ * responses:
+ * 200:
+ * description: Reviews list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Reviews"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/',
+ wrapAsync(async (req, res) => {
+ const filetype = req.query.filetype;
+
+ const currentUser = req.currentUser;
+ const payload = await ReviewsDBApi.findAll(req.query, { currentUser });
+ if (filetype && filetype === 'csv') {
+ const fields = ['id', 'notes', 'rating', 'createdat'];
+ 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/reviews/count:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Reviews]
+ * summary: Count all reviews
+ * description: Count all reviews
+ * responses:
+ * 200:
+ * description: Reviews count successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Reviews"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get(
+ '/count',
+ wrapAsync(async (req, res) => {
+ const currentUser = req.currentUser;
+ const payload = await ReviewsDBApi.findAll(req.query, null, {
+ countOnly: true,
+ currentUser,
+ });
+
+ res.status(200).send(payload);
+ }),
+);
+
+/**
+ * @swagger
+ * /api/reviews/autocomplete:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Reviews]
+ * summary: Find all reviews that match search criteria
+ * description: Find all reviews that match search criteria
+ * responses:
+ * 200:
+ * description: Reviews list successfully received
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: "#/components/schemas/Reviews"
+ * 401:
+ * $ref: "#/components/responses/UnauthorizedError"
+ * 404:
+ * description: Data not found
+ * 500:
+ * description: Some server error
+ */
+router.get('/autocomplete', async (req, res) => {
+ const payload = await ReviewsDBApi.findAllAutocomplete(
+ req.query.query,
+ req.query.limit,
+ req.query.offset,
+ );
+
+ res.status(200).send(payload);
+});
+
+/**
+ * @swagger
+ * /api/reviews/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags: [Reviews]
+ * 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/Reviews"
+ * 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 ReviewsDBApi.findBy({ id: req.params.id });
+
+ res.status(200).send(payload);
+ }),
+);
+
+router.use('/', require('../helpers').commonErrorHandler);
+
+module.exports = router;
diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js
index a6d214f..6d500e7 100644
--- a/backend/src/routes/users.js
+++ b/backend/src/routes/users.js
@@ -32,6 +32,12 @@ router.use(checkCrudPermissions('users'));
* email:
* type: string
* default: email
+ * address:
+ * type: string
+ * default: address
+ * address2:
+ * type: string
+ * default: address2
*/
@@ -308,7 +314,15 @@ router.get(
const currentUser = req.currentUser;
const payload = await UsersDBApi.findAll(req.query, { currentUser });
if (filetype && filetype === 'csv') {
- const fields = ['id', 'firstName', 'lastName', 'phoneNumber', 'email'];
+ const fields = [
+ 'id',
+ 'firstName',
+ 'lastName',
+ 'phoneNumber',
+ 'email',
+ 'address',
+ 'address2',
+ ];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);
diff --git a/backend/src/services/conversationparticipants.js b/backend/src/services/conversationparticipants.js
new file mode 100644
index 0000000..8d003be
--- /dev/null
+++ b/backend/src/services/conversationparticipants.js
@@ -0,0 +1,118 @@
+const db = require('../db/models');
+const ConversationparticipantsDBApi = require('../db/api/conversationparticipants');
+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 ConversationparticipantsService {
+ static async create(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ await ConversationparticipantsDBApi.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 ConversationparticipantsDBApi.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 conversationparticipants = await ConversationparticipantsDBApi.findBy(
+ { id },
+ { transaction },
+ );
+
+ if (!conversationparticipants) {
+ throw new ValidationError('conversationparticipantsNotFound');
+ }
+
+ const updatedConversationparticipants =
+ await ConversationparticipantsDBApi.update(id, data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ return updatedConversationparticipants;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async deleteByIds(ids, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await ConversationparticipantsDBApi.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 ConversationparticipantsDBApi.remove(id, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+};
diff --git a/backend/src/services/conversations.js b/backend/src/services/conversations.js
new file mode 100644
index 0000000..5a67c76
--- /dev/null
+++ b/backend/src/services/conversations.js
@@ -0,0 +1,117 @@
+const db = require('../db/models');
+const ConversationsDBApi = require('../db/api/conversations');
+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 ConversationsService {
+ static async create(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ await ConversationsDBApi.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 ConversationsDBApi.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 conversations = await ConversationsDBApi.findBy(
+ { id },
+ { transaction },
+ );
+
+ if (!conversations) {
+ throw new ValidationError('conversationsNotFound');
+ }
+
+ const updatedConversations = await ConversationsDBApi.update(id, data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ return updatedConversations;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async deleteByIds(ids, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await ConversationsDBApi.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 ConversationsDBApi.remove(id, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+};
diff --git a/backend/src/services/locations.js b/backend/src/services/locations.js
new file mode 100644
index 0000000..9c8082b
--- /dev/null
+++ b/backend/src/services/locations.js
@@ -0,0 +1,114 @@
+const db = require('../db/models');
+const LocationsDBApi = require('../db/api/locations');
+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 LocationsService {
+ static async create(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ await LocationsDBApi.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 LocationsDBApi.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 locations = await LocationsDBApi.findBy({ id }, { transaction });
+
+ if (!locations) {
+ throw new ValidationError('locationsNotFound');
+ }
+
+ const updatedLocations = await LocationsDBApi.update(id, data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ return updatedLocations;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async deleteByIds(ids, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await LocationsDBApi.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 LocationsDBApi.remove(id, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+};
diff --git a/backend/src/services/messages.js b/backend/src/services/messages.js
new file mode 100644
index 0000000..bdc275c
--- /dev/null
+++ b/backend/src/services/messages.js
@@ -0,0 +1,114 @@
+const db = require('../db/models');
+const MessagesDBApi = require('../db/api/messages');
+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 MessagesService {
+ static async create(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ await MessagesDBApi.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 MessagesDBApi.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 messages = await MessagesDBApi.findBy({ id }, { transaction });
+
+ if (!messages) {
+ throw new ValidationError('messagesNotFound');
+ }
+
+ const updatedMessages = await MessagesDBApi.update(id, data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ return updatedMessages;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async deleteByIds(ids, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await MessagesDBApi.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 MessagesDBApi.remove(id, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+};
diff --git a/backend/src/services/photos.js b/backend/src/services/photos.js
new file mode 100644
index 0000000..18bcf10
--- /dev/null
+++ b/backend/src/services/photos.js
@@ -0,0 +1,114 @@
+const db = require('../db/models');
+const PhotosDBApi = require('../db/api/photos');
+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 PhotosService {
+ static async create(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ await PhotosDBApi.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 PhotosDBApi.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 photos = await PhotosDBApi.findBy({ id }, { transaction });
+
+ if (!photos) {
+ throw new ValidationError('photosNotFound');
+ }
+
+ const updatedPhotos = await PhotosDBApi.update(id, data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ return updatedPhotos;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async deleteByIds(ids, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await PhotosDBApi.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 PhotosDBApi.remove(id, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+};
diff --git a/backend/src/services/products.js b/backend/src/services/products.js
new file mode 100644
index 0000000..0f7a762
--- /dev/null
+++ b/backend/src/services/products.js
@@ -0,0 +1,114 @@
+const db = require('../db/models');
+const ProductsDBApi = require('../db/api/products');
+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 ProductsService {
+ static async create(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ await ProductsDBApi.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 ProductsDBApi.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 products = await ProductsDBApi.findBy({ id }, { transaction });
+
+ if (!products) {
+ throw new ValidationError('productsNotFound');
+ }
+
+ const updatedProducts = await ProductsDBApi.update(id, data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ return updatedProducts;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async deleteByIds(ids, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await ProductsDBApi.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 ProductsDBApi.remove(id, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+};
diff --git a/backend/src/services/reviews.js b/backend/src/services/reviews.js
new file mode 100644
index 0000000..2ea9dd6
--- /dev/null
+++ b/backend/src/services/reviews.js
@@ -0,0 +1,114 @@
+const db = require('../db/models');
+const ReviewsDBApi = require('../db/api/reviews');
+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 ReviewsService {
+ static async create(data, currentUser) {
+ const transaction = await db.sequelize.transaction();
+ try {
+ await ReviewsDBApi.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 ReviewsDBApi.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 reviews = await ReviewsDBApi.findBy({ id }, { transaction });
+
+ if (!reviews) {
+ throw new ValidationError('reviewsNotFound');
+ }
+
+ const updatedReviews = await ReviewsDBApi.update(id, data, {
+ currentUser,
+ transaction,
+ });
+
+ await transaction.commit();
+ return updatedReviews;
+ } catch (error) {
+ await transaction.rollback();
+ throw error;
+ }
+ }
+
+ static async deleteByIds(ids, currentUser) {
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ await ReviewsDBApi.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 ReviewsDBApi.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 4f1772b..f238b62 100644
--- a/backend/src/services/search.js
+++ b/backend/src/services/search.js
@@ -41,28 +41,56 @@ module.exports = class SearchService {
throw new ValidationError('iam.errors.searchQueryRequired');
}
const tableColumns = {
- users: ['firstName', 'lastName', 'phoneNumber', 'email'],
+ users: [
+ 'firstName',
- bottles: [
- 'name',
+ 'lastName',
- 'notes',
+ 'phoneNumber',
- 'tasting_notes',
+ 'email',
- 'msrp_range',
+ 'address',
- 'secondary_value_range',
-
- 'barcode',
+ 'address2',
],
+ distilleries: ['name', 'city', 'state'],
+
brands: ['name'],
- distilleries: ['name'],
+ photos: ['phototype'],
+
+ products: ['name', 'barcode', 'notes'],
+
+ locations: ['name'],
+
+ bottles: [
+ 'rickhouse',
+
+ 'rack',
+
+ 'release',
+
+ 'barrelnumber',
+
+ 'bottlenumber',
+
+ 'notes',
+ ],
+
+ reviews: ['notes'],
};
const columnsInt = {
- bottles: ['proof', 'quantity', 'age'],
+ distilleries: ['status'],
+
+ brands: ['status'],
+
+ products: ['proof', 'age', 'status'],
+
+ bottles: ['proof', 'age', 'rating', 'volume'],
+
+ reviews: ['rating'],
};
let allFoundRecords = [];
diff --git a/frontend/src/components/Bottles/CardBottles.tsx b/frontend/src/components/Bottles/CardBottles.tsx
index b0318f2..720b17e 100644
--- a/frontend/src/components/Bottles/CardBottles.tsx
+++ b/frontend/src/components/Bottles/CardBottles.tsx
@@ -56,22 +56,16 @@ const CardBottles = ({
}`}
>
-
-
{item.name}
+ {item.id}
-
+
-
- Name
+
- User
-
-
{item.name}
+
+ {dataFormatter.usersOneListFormatter(item.user)}
+
-
- Brand
+
-
+ Product
+
-
- {dataFormatter.brandsOneListFormatter(item.brand)}
+ {dataFormatter.productsOneListFormatter(item.product)}
+
+
+
+
+
+
-
+ Location
+
+
-
+
+ {dataFormatter.locationsOneListFormatter(item.location)}
@@ -106,9 +115,115 @@ const CardBottles = ({
-
Type
+
Age
- {item.type}
+ {item.age}
+
+
+
+
+
+ Rating
+
+
+
+ {item.rating}
+
+
+
+
+
+
+ Collectable
+
+
+
+ {dataFormatter.booleanFormatter(item.collectable)}
+
+
+
+
+
+
+ Rickhouse
+
+
+
+ {item.rickhouse}
+
+
+
+
+
+
Rack
+
+ {item.rack}
+
+
+
+
+
+ Release
+
+
+
+ {item.release}
+
+
+
+
+
+
+ Barrelnumber
+
+
+
+ {item.barrelnumber}
+
+
+
+
+
+
+ Barreleddate
+
+
+
+ {dataFormatter.dateFormatter(item.barreleddate)}
+
+
+
+
+
+
+ Bottlenumber
+
+
+
+ {item.bottlenumber}
+
+
+
+
+
+
+ Dateacquired
+
+
+
+ {dataFormatter.dateFormatter(item.dateacquired)}
+
+
+
+
+
+
+ Volume
+
+
+
+ {item.volume}
+
@@ -121,112 +236,22 @@ const CardBottles = ({
- TastingNotes
+ Photofront
- {item.tasting_notes}
+ {dataFormatter.photosOneListFormatter(item.photofront)}
- MSRPRange
+ Photoback
- {item.msrp_range}
-
-
-
-
-
-
- SecondaryValueRange
-
-
-
- {item.secondary_value_range}
-
-
-
-
-
-
- OpenedBottleIndicator
-
-
-
- {dataFormatter.booleanFormatter(
- item.opened_bottle_indicator,
- )}
-
-
-
-
-
-
- Quantity
-
-
-
- {item.quantity}
-
-
-
-
-
-
- Barcode
-
-
-
- {item.barcode}
-
-
-
-
-
-
- Picture
-
-
-
-
-
-
-
-
-
-
-
-
- Distillery
-
-
-
- {dataFormatter.distilleriesOneListFormatter(
- item.distillery,
- )}
-
-
-
-
-
-
User
-
-
- {dataFormatter.usersOneListFormatter(item.user)}
+ {dataFormatter.photosOneListFormatter(item.photoback)}
diff --git a/frontend/src/components/Bottles/ListBottles.tsx b/frontend/src/components/Bottles/ListBottles.tsx
index 9b33565..6f8205c 100644
--- a/frontend/src/components/Bottles/ListBottles.tsx
+++ b/frontend/src/components/Bottles/ListBottles.tsx
@@ -45,15 +45,6 @@ const ListBottles = ({
-
-
-
Name
-
{item.name}
+
User
+
+ {dataFormatter.usersOneListFormatter(item.user)}
+
-
Brand
+
Product
- {dataFormatter.brandsOneListFormatter(item.brand)}
+ {dataFormatter.productsOneListFormatter(item.product)}
+
+
+
+
+
Location
+
+ {dataFormatter.locationsOneListFormatter(item.location)}
@@ -78,8 +78,64 @@ const ListBottles = ({
-
Type
-
{item.type}
+
Age
+
{item.age}
+
+
+
+
Rating
+
{item.rating}
+
+
+
+
Collectable
+
+ {dataFormatter.booleanFormatter(item.collectable)}
+
+
+
+
+
Rickhouse
+
{item.rickhouse}
+
+
+
+
+
+
Release
+
{item.release}
+
+
+
+
Barrelnumber
+
{item.barrelnumber}
+
+
+
+
Barreleddate
+
+ {dataFormatter.dateFormatter(item.barreleddate)}
+
+
+
+
+
Bottlenumber
+
{item.bottlenumber}
+
+
+
+
Dateacquired
+
+ {dataFormatter.dateFormatter(item.dateacquired)}
+
+
+
+
@@ -88,72 +144,16 @@ const ListBottles = ({
-
TastingNotes
-
{item.tasting_notes}
-
-
-
-
MSRPRange
-
{item.msrp_range}
-
-
-
-
- SecondaryValueRange
-
+
Photofront
- {item.secondary_value_range}
+ {dataFormatter.photosOneListFormatter(item.photofront)}
-
- OpenedBottleIndicator
-
+
Photoback
- {dataFormatter.booleanFormatter(
- item.opened_bottle_indicator,
- )}
-
-
-
-
-
Quantity
-
{item.quantity}
-
-
-
-
Barcode
-
{item.barcode}
-
-
-
-
-
-
-
-
Distillery
-
- {dataFormatter.distilleriesOneListFormatter(
- item.distillery,
- )}
-
-
-
-
-
User
-
- {dataFormatter.usersOneListFormatter(item.user)}
+ {dataFormatter.photosOneListFormatter(item.photoback)}
diff --git a/frontend/src/components/Bottles/TableBottles.tsx b/frontend/src/components/Bottles/TableBottles.tsx
index 20d442b..3935e3a 100644
--- a/frontend/src/components/Bottles/TableBottles.tsx
+++ b/frontend/src/components/Bottles/TableBottles.tsx
@@ -20,8 +20,6 @@ import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter';
import { dataGridStyles } from '../../styles';
-import CardBottles from './CardBottles';
-
const perPage = 10;
const TableSampleBottles = ({
@@ -463,18 +461,7 @@ const TableSampleBottles = ({
Are you sure you want to delete this item?
- {bottles && Array.isArray(bottles) && !showGrid && (
-
- )}
-
- {showGrid && dataGrid}
+ {dataGrid}
{selectedRows.length > 0 &&
createPortal(
diff --git a/frontend/src/components/Bottles/configureBottlesCols.tsx b/frontend/src/components/Bottles/configureBottlesCols.tsx
index 66a8bf0..e6d8678 100644
--- a/frontend/src/components/Bottles/configureBottlesCols.tsx
+++ b/frontend/src/components/Bottles/configureBottlesCols.tsx
@@ -39,20 +39,8 @@ export const loadColumns = async (
return [
{
- field: 'name',
- headerName: 'Name',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: hasUpdatePermission,
- },
-
- {
- field: 'brand',
- headerName: 'Brand',
+ field: 'user',
+ headerName: 'User',
flex: 1,
minWidth: 120,
filterable: false,
@@ -65,7 +53,47 @@ export const loadColumns = async (
type: 'singleSelect',
getOptionValue: (value: any) => value?.id,
getOptionLabel: (value: any) => value?.label,
- valueOptions: await callOptionsApi('brands'),
+ valueOptions: await callOptionsApi('users'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'product',
+ headerName: 'Product',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('products'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'location',
+ headerName: 'Location',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('locations'),
valueGetter: (params: GridValueGetterParams) =>
params?.value?.id ?? params?.value,
},
@@ -85,8 +113,8 @@ export const loadColumns = async (
},
{
- field: 'type',
- headerName: 'Type',
+ field: 'age',
+ headerName: 'Age',
flex: 1,
minWidth: 120,
filterable: false,
@@ -94,6 +122,142 @@ export const loadColumns = async (
cellClassName: 'datagrid--cell',
editable: hasUpdatePermission,
+
+ type: 'number',
+ },
+
+ {
+ field: 'rating',
+ headerName: 'Rating',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'number',
+ },
+
+ {
+ field: 'collectable',
+ headerName: 'Collectable',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'boolean',
+ },
+
+ {
+ field: 'rickhouse',
+ headerName: 'Rickhouse',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'rack',
+ headerName: 'Rack',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'release',
+ headerName: 'Release',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'barrelnumber',
+ headerName: 'Barrelnumber',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'barreleddate',
+ headerName: 'Barreleddate',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'date',
+ valueGetter: (params: GridValueGetterParams) =>
+ new Date(params.row.barreleddate),
+ },
+
+ {
+ field: 'bottlenumber',
+ headerName: 'Bottlenumber',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'dateacquired',
+ headerName: 'Dateacquired',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'date',
+ valueGetter: (params: GridValueGetterParams) =>
+ new Date(params.row.dateacquired),
+ },
+
+ {
+ field: 'volume',
+ headerName: 'Volume',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'number',
},
{
@@ -109,118 +273,8 @@ export const loadColumns = async (
},
{
- field: 'tasting_notes',
- headerName: 'TastingNotes',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: hasUpdatePermission,
- },
-
- {
- field: 'msrp_range',
- headerName: 'MSRPRange',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: hasUpdatePermission,
- },
-
- {
- field: 'secondary_value_range',
- headerName: 'SecondaryValueRange',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: hasUpdatePermission,
- },
-
- {
- field: 'opened_bottle_indicator',
- headerName: 'OpenedBottleIndicator',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: hasUpdatePermission,
-
- type: 'boolean',
- },
-
- {
- field: 'quantity',
- headerName: 'Quantity',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: hasUpdatePermission,
-
- type: 'number',
- },
-
- {
- field: 'barcode',
- headerName: 'Barcode',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: hasUpdatePermission,
- },
-
- {
- field: 'picture',
- headerName: 'Picture',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: false,
- sortable: false,
- renderCell: (params: GridValueGetterParams) => (
-
- ),
- },
-
- {
- field: 'age',
- headerName: 'Age',
- flex: 1,
- minWidth: 120,
- filterable: false,
- headerClassName: 'datagrid--header',
- cellClassName: 'datagrid--cell',
-
- editable: hasUpdatePermission,
-
- type: 'number',
- },
-
- {
- field: 'distillery',
- headerName: 'Distillery',
+ field: 'photofront',
+ headerName: 'Photofront',
flex: 1,
minWidth: 120,
filterable: false,
@@ -233,14 +287,14 @@ export const loadColumns = async (
type: 'singleSelect',
getOptionValue: (value: any) => value?.id,
getOptionLabel: (value: any) => value?.label,
- valueOptions: await callOptionsApi('distilleries'),
+ valueOptions: await callOptionsApi('photos'),
valueGetter: (params: GridValueGetterParams) =>
params?.value?.id ?? params?.value,
},
{
- field: 'user',
- headerName: 'User',
+ field: 'photoback',
+ headerName: 'Photoback',
flex: 1,
minWidth: 120,
filterable: false,
@@ -253,7 +307,7 @@ export const loadColumns = async (
type: 'singleSelect',
getOptionValue: (value: any) => value?.id,
getOptionLabel: (value: any) => value?.label,
- valueOptions: await callOptionsApi('users'),
+ valueOptions: await callOptionsApi('photos'),
valueGetter: (params: GridValueGetterParams) =>
params?.value?.id ?? params?.value,
},
diff --git a/frontend/src/components/Brands/CardBrands.tsx b/frontend/src/components/Brands/CardBrands.tsx
index 6c02f89..1574059 100644
--- a/frontend/src/components/Brands/CardBrands.tsx
+++ b/frontend/src/components/Brands/CardBrands.tsx
@@ -83,6 +83,17 @@ const CardBrands = ({
+
+
+ Status
+
+
+
+ {item.status}
+
+
+
+
Distillery
diff --git a/frontend/src/components/Brands/ListBrands.tsx b/frontend/src/components/Brands/ListBrands.tsx
index 8c508c7..0d30be2 100644
--- a/frontend/src/components/Brands/ListBrands.tsx
+++ b/frontend/src/components/Brands/ListBrands.tsx
@@ -56,6 +56,11 @@ const ListBrands = ({
{item.name}
+
+
Status
+
{item.status}
+
+
Distillery
diff --git a/frontend/src/components/Brands/TableBrands.tsx b/frontend/src/components/Brands/TableBrands.tsx
index 17e3c07..afc54d8 100644
--- a/frontend/src/components/Brands/TableBrands.tsx
+++ b/frontend/src/components/Brands/TableBrands.tsx
@@ -20,8 +20,6 @@ import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter';
import { dataGridStyles } from '../../styles';
-import ListBrands from './ListBrands';
-
const perPage = 10;
const TableSampleBrands = ({
@@ -463,18 +461,7 @@ const TableSampleBrands = ({
Are you sure you want to delete this item?
- {brands && Array.isArray(brands) && !showGrid && (
-
- )}
-
- {showGrid && dataGrid}
+ {dataGrid}
{selectedRows.length > 0 &&
createPortal(
diff --git a/frontend/src/components/Brands/configureBrandsCols.tsx b/frontend/src/components/Brands/configureBrandsCols.tsx
index 2674b8f..826f965 100644
--- a/frontend/src/components/Brands/configureBrandsCols.tsx
+++ b/frontend/src/components/Brands/configureBrandsCols.tsx
@@ -50,6 +50,20 @@ export const loadColumns = async (
editable: hasUpdatePermission,
},
+ {
+ field: 'status',
+ headerName: 'Status',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'number',
+ },
+
{
field: 'distillery',
headerName: 'Distillery',
diff --git a/frontend/src/components/Conversationparticipants/CardConversationparticipants.tsx b/frontend/src/components/Conversationparticipants/CardConversationparticipants.tsx
new file mode 100644
index 0000000..4a0e2c6
--- /dev/null
+++ b/frontend/src/components/Conversationparticipants/CardConversationparticipants.tsx
@@ -0,0 +1,123 @@
+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 = {
+ conversationparticipants: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const CardConversationparticipants = ({
+ conversationparticipants,
+ 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_CONVERSATIONPARTICIPANTS',
+ );
+
+ return (
+
+ {loading &&
}
+
+ {!loading &&
+ conversationparticipants.map((item, index) => (
+ -
+
+
+ {item.id}
+
+
+
+
+
+
+
+
+
-
+ Conversation
+
+
-
+
+ {dataFormatter.conversationsOneListFormatter(
+ item.conversation,
+ )}
+
+
+
+
+
+
- User
+
-
+
+ {dataFormatter.usersOneListFormatter(item.user)}
+
+
+
+
+
+ ))}
+ {!loading && conversationparticipants.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default CardConversationparticipants;
diff --git a/frontend/src/components/Conversationparticipants/ListConversationparticipants.tsx b/frontend/src/components/Conversationparticipants/ListConversationparticipants.tsx
new file mode 100644
index 0000000..f103ba2
--- /dev/null
+++ b/frontend/src/components/Conversationparticipants/ListConversationparticipants.tsx
@@ -0,0 +1,101 @@
+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 = {
+ conversationparticipants: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const ListConversationparticipants = ({
+ conversationparticipants,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(
+ currentUser,
+ 'UPDATE_CONVERSATIONPARTICIPANTS',
+ );
+
+ const corners = useAppSelector((state) => state.style.corners);
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+
+ return (
+ <>
+
+ {loading &&
}
+ {!loading &&
+ conversationparticipants.map((item) => (
+
+
+
+
dark:divide-dark-700 overflow-x-auto'
+ }
+ >
+
+
Conversation
+
+ {dataFormatter.conversationsOneListFormatter(
+ item.conversation,
+ )}
+
+
+
+
+
User
+
+ {dataFormatter.usersOneListFormatter(item.user)}
+
+
+
+
+
+
+
+ ))}
+ {!loading && conversationparticipants.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ListConversationparticipants;
diff --git a/frontend/src/components/Conversationparticipants/TableConversationparticipants.tsx b/frontend/src/components/Conversationparticipants/TableConversationparticipants.tsx
new file mode 100644
index 0000000..26432db
--- /dev/null
+++ b/frontend/src/components/Conversationparticipants/TableConversationparticipants.tsx
@@ -0,0 +1,486 @@
+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/conversationparticipants/conversationparticipantsSlice';
+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 './configureConversationparticipantsCols';
+import _ from 'lodash';
+import dataFormatter from '../../helpers/dataFormatter';
+import { dataGridStyles } from '../../styles';
+
+const perPage = 10;
+
+const TableSampleConversationparticipants = ({
+ 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 {
+ conversationparticipants,
+ loading,
+ count,
+ notify: conversationparticipantsNotify,
+ refetch,
+ } = useAppSelector((state) => state.conversationparticipants);
+ 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 (conversationparticipantsNotify.showNotification) {
+ notify(
+ conversationparticipantsNotify.typeNotification,
+ conversationparticipantsNotify.textNotification,
+ );
+ }
+ }, [conversationparticipantsNotify.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,
+ `conversationparticipants`,
+ 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={conversationparticipants ?? []}
+ 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}
+ >
+
+
+
+ ) : null}
+
+ Are you sure you want to delete this item?
+
+
+ {dataGrid}
+
+ {selectedRows.length > 0 &&
+ createPortal(
+ onDeleteRows(selectedRows)}
+ />,
+ document.getElementById('delete-rows-button'),
+ )}
+
+ >
+ );
+};
+
+export default TableSampleConversationparticipants;
diff --git a/frontend/src/components/Conversationparticipants/configureConversationparticipantsCols.tsx b/frontend/src/components/Conversationparticipants/configureConversationparticipantsCols.tsx
new file mode 100644
index 0000000..0e86907
--- /dev/null
+++ b/frontend/src/components/Conversationparticipants/configureConversationparticipantsCols.tsx
@@ -0,0 +1,105 @@
+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_CONVERSATIONPARTICIPANTS',
+ );
+
+ return [
+ {
+ field: 'conversation',
+ headerName: 'Conversation',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('conversations'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'user',
+ headerName: 'User',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('users'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'actions',
+ type: 'actions',
+ minWidth: 30,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+ getActions: (params: GridRowParams) => {
+ return [
+
+
+
,
+ ];
+ },
+ },
+ ];
+};
diff --git a/frontend/src/components/Conversations/CardConversations.tsx b/frontend/src/components/Conversations/CardConversations.tsx
new file mode 100644
index 0000000..d864fd4
--- /dev/null
+++ b/frontend/src/components/Conversations/CardConversations.tsx
@@ -0,0 +1,112 @@
+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 = {
+ conversations: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const CardConversations = ({
+ conversations,
+ 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_CONVERSATIONS',
+ );
+
+ return (
+
+ {loading &&
}
+
+ {!loading &&
+ conversations.map((item, index) => (
+ -
+
+
+ {item.id}
+
+
+
+
+
+
+
+
+
-
+ Createdat
+
+
-
+
+ {dataFormatter.dateTimeFormatter(item.createdat)}
+
+
+
+
+
+ ))}
+ {!loading && conversations.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default CardConversations;
diff --git a/frontend/src/components/Conversations/ListConversations.tsx b/frontend/src/components/Conversations/ListConversations.tsx
new file mode 100644
index 0000000..2d9efd6
--- /dev/null
+++ b/frontend/src/components/Conversations/ListConversations.tsx
@@ -0,0 +1,92 @@
+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 = {
+ conversations: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const ListConversations = ({
+ conversations,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(
+ currentUser,
+ 'UPDATE_CONVERSATIONS',
+ );
+
+ const corners = useAppSelector((state) => state.style.corners);
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+
+ return (
+ <>
+
+ {loading &&
}
+ {!loading &&
+ conversations.map((item) => (
+
+
+
+
dark:divide-dark-700 overflow-x-auto'
+ }
+ >
+
+
Createdat
+
+ {dataFormatter.dateTimeFormatter(item.createdat)}
+
+
+
+
+
+
+
+ ))}
+ {!loading && conversations.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ListConversations;
diff --git a/frontend/src/components/Conversations/TableConversations.tsx b/frontend/src/components/Conversations/TableConversations.tsx
new file mode 100644
index 0000000..9705281
--- /dev/null
+++ b/frontend/src/components/Conversations/TableConversations.tsx
@@ -0,0 +1,484 @@
+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/conversations/conversationsSlice';
+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 './configureConversationsCols';
+import _ from 'lodash';
+import dataFormatter from '../../helpers/dataFormatter';
+import { dataGridStyles } from '../../styles';
+
+const perPage = 10;
+
+const TableSampleConversations = ({
+ 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 {
+ conversations,
+ loading,
+ count,
+ notify: conversationsNotify,
+ refetch,
+ } = useAppSelector((state) => state.conversations);
+ 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 (conversationsNotify.showNotification) {
+ notify(
+ conversationsNotify.typeNotification,
+ conversationsNotify.textNotification,
+ );
+ }
+ }, [conversationsNotify.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, `conversations`, 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={conversations ?? []}
+ 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}
+ >
+
+
+
+ ) : null}
+
+ Are you sure you want to delete this item?
+
+
+ {dataGrid}
+
+ {selectedRows.length > 0 &&
+ createPortal(
+ onDeleteRows(selectedRows)}
+ />,
+ document.getElementById('delete-rows-button'),
+ )}
+
+ >
+ );
+};
+
+export default TableSampleConversations;
diff --git a/frontend/src/components/Conversations/configureConversationsCols.tsx b/frontend/src/components/Conversations/configureConversationsCols.tsx
new file mode 100644
index 0000000..05bdfe3
--- /dev/null
+++ b/frontend/src/components/Conversations/configureConversationsCols.tsx
@@ -0,0 +1,78 @@
+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_CONVERSATIONS');
+
+ return [
+ {
+ field: 'createdat',
+ headerName: 'Createdat',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'dateTime',
+ valueGetter: (params: GridValueGetterParams) =>
+ new Date(params.row.createdat),
+ },
+
+ {
+ field: 'actions',
+ type: 'actions',
+ minWidth: 30,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+ getActions: (params: GridRowParams) => {
+ return [
+
+
+
,
+ ];
+ },
+ },
+ ];
+};
diff --git a/frontend/src/components/Distilleries/CardDistilleries.tsx b/frontend/src/components/Distilleries/CardDistilleries.tsx
index c4740e7..aa31d1d 100644
--- a/frontend/src/components/Distilleries/CardDistilleries.tsx
+++ b/frontend/src/components/Distilleries/CardDistilleries.tsx
@@ -82,6 +82,31 @@ const CardDistilleries = ({
{item.name}
+
+
+
City
+
+ {item.city}
+
+
+
+
+
State
+
+ {item.state}
+
+
+
+
+
+ Status
+
+
+
+ {item.status}
+
+
+
))}
diff --git a/frontend/src/components/Distilleries/ListDistilleries.tsx b/frontend/src/components/Distilleries/ListDistilleries.tsx
index b6686e5..6d72729 100644
--- a/frontend/src/components/Distilleries/ListDistilleries.tsx
+++ b/frontend/src/components/Distilleries/ListDistilleries.tsx
@@ -55,6 +55,21 @@ const ListDistilleries = ({
Name
{item.name}
+
+
+
+
+
+
+
Status
+
{item.status}
+
Are you sure you want to delete this item?
- {distilleries && Array.isArray(distilleries) && !showGrid && (
-
- )}
-
- {showGrid && dataGrid}
+ {dataGrid}
{selectedRows.length > 0 &&
createPortal(
diff --git a/frontend/src/components/Distilleries/configureDistilleriesCols.tsx b/frontend/src/components/Distilleries/configureDistilleriesCols.tsx
index 02fa87d..cf9af8b 100644
--- a/frontend/src/components/Distilleries/configureDistilleriesCols.tsx
+++ b/frontend/src/components/Distilleries/configureDistilleriesCols.tsx
@@ -50,6 +50,44 @@ export const loadColumns = async (
editable: hasUpdatePermission,
},
+ {
+ field: 'city',
+ headerName: 'City',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'state',
+ headerName: 'State',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'status',
+ headerName: 'Status',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'number',
+ },
+
{
field: 'actions',
type: 'actions',
diff --git a/frontend/src/components/Locations/CardLocations.tsx b/frontend/src/components/Locations/CardLocations.tsx
new file mode 100644
index 0000000..6fdee93
--- /dev/null
+++ b/frontend/src/components/Locations/CardLocations.tsx
@@ -0,0 +1,114 @@
+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 = {
+ locations: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const CardLocations = ({
+ locations,
+ 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_LOCATIONS');
+
+ return (
+
+ {loading &&
}
+
+ {!loading &&
+ locations.map((item, index) => (
+ -
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
- Name
+
-
+
{item.name}
+
+
+
+
+
- User
+
-
+
+ {dataFormatter.usersOneListFormatter(item.user)}
+
+
+
+
+
+ ))}
+ {!loading && locations.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default CardLocations;
diff --git a/frontend/src/components/Locations/ListLocations.tsx b/frontend/src/components/Locations/ListLocations.tsx
new file mode 100644
index 0000000..e34b067
--- /dev/null
+++ b/frontend/src/components/Locations/ListLocations.tsx
@@ -0,0 +1,94 @@
+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 = {
+ locations: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const ListLocations = ({
+ locations,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_LOCATIONS');
+
+ const corners = useAppSelector((state) => state.style.corners);
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+
+ return (
+ <>
+
+ {loading &&
}
+ {!loading &&
+ locations.map((item) => (
+
+
+
+
dark:divide-dark-700 overflow-x-auto'
+ }
+ >
+
+
+
+
User
+
+ {dataFormatter.usersOneListFormatter(item.user)}
+
+
+
+
+
+
+
+ ))}
+ {!loading && locations.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ListLocations;
diff --git a/frontend/src/components/Locations/TableLocations.tsx b/frontend/src/components/Locations/TableLocations.tsx
new file mode 100644
index 0000000..c0a84fc
--- /dev/null
+++ b/frontend/src/components/Locations/TableLocations.tsx
@@ -0,0 +1,484 @@
+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/locations/locationsSlice';
+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 './configureLocationsCols';
+import _ from 'lodash';
+import dataFormatter from '../../helpers/dataFormatter';
+import { dataGridStyles } from '../../styles';
+
+const perPage = 10;
+
+const TableSampleLocations = ({
+ 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 {
+ locations,
+ loading,
+ count,
+ notify: locationsNotify,
+ refetch,
+ } = useAppSelector((state) => state.locations);
+ 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 (locationsNotify.showNotification) {
+ notify(
+ locationsNotify.typeNotification,
+ locationsNotify.textNotification,
+ );
+ }
+ }, [locationsNotify.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, `locations`, 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={locations ?? []}
+ 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}
+ >
+
+
+
+ ) : null}
+
+ Are you sure you want to delete this item?
+
+
+ {dataGrid}
+
+ {selectedRows.length > 0 &&
+ createPortal(
+ onDeleteRows(selectedRows)}
+ />,
+ document.getElementById('delete-rows-button'),
+ )}
+
+ >
+ );
+};
+
+export default TableSampleLocations;
diff --git a/frontend/src/components/Locations/configureLocationsCols.tsx b/frontend/src/components/Locations/configureLocationsCols.tsx
new file mode 100644
index 0000000..ede479e
--- /dev/null
+++ b/frontend/src/components/Locations/configureLocationsCols.tsx
@@ -0,0 +1,94 @@
+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_LOCATIONS');
+
+ return [
+ {
+ field: 'name',
+ headerName: 'Name',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'user',
+ headerName: 'User',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('users'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'actions',
+ type: 'actions',
+ minWidth: 30,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+ getActions: (params: GridRowParams) => {
+ return [
+
+
+
,
+ ];
+ },
+ },
+ ];
+};
diff --git a/frontend/src/components/Messages/CardMessages.tsx b/frontend/src/components/Messages/CardMessages.tsx
new file mode 100644
index 0000000..491275b
--- /dev/null
+++ b/frontend/src/components/Messages/CardMessages.tsx
@@ -0,0 +1,122 @@
+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 = {
+ messages: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const CardMessages = ({
+ messages,
+ 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_MESSAGES');
+
+ return (
+
+ {loading &&
}
+
+ {!loading &&
+ messages.map((item, index) => (
+ -
+
+
+ {item.id}
+
+
+
+
+
+
+
+
+
-
+ Conversation
+
+
-
+
+ {dataFormatter.conversationsOneListFormatter(
+ item.conversation,
+ )}
+
+
+
+
+
+
-
+ Sender
+
+
-
+
+ {dataFormatter.usersOneListFormatter(item.sender)}
+
+
+
+
+
+ ))}
+ {!loading && messages.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default CardMessages;
diff --git a/frontend/src/components/Messages/ListMessages.tsx b/frontend/src/components/Messages/ListMessages.tsx
new file mode 100644
index 0000000..72c068c
--- /dev/null
+++ b/frontend/src/components/Messages/ListMessages.tsx
@@ -0,0 +1,98 @@
+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 = {
+ messages: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const ListMessages = ({
+ messages,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_MESSAGES');
+
+ const corners = useAppSelector((state) => state.style.corners);
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+
+ return (
+ <>
+
+ {loading &&
}
+ {!loading &&
+ messages.map((item) => (
+
+
+
+
dark:divide-dark-700 overflow-x-auto'
+ }
+ >
+
+
Conversation
+
+ {dataFormatter.conversationsOneListFormatter(
+ item.conversation,
+ )}
+
+
+
+
+
Sender
+
+ {dataFormatter.usersOneListFormatter(item.sender)}
+
+
+
+
+
+
+
+ ))}
+ {!loading && messages.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ListMessages;
diff --git a/frontend/src/components/Messages/TableMessages.tsx b/frontend/src/components/Messages/TableMessages.tsx
new file mode 100644
index 0000000..f0e476a
--- /dev/null
+++ b/frontend/src/components/Messages/TableMessages.tsx
@@ -0,0 +1,481 @@
+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/messages/messagesSlice';
+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 './configureMessagesCols';
+import _ from 'lodash';
+import dataFormatter from '../../helpers/dataFormatter';
+import { dataGridStyles } from '../../styles';
+
+const perPage = 10;
+
+const TableSampleMessages = ({
+ 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 {
+ messages,
+ loading,
+ count,
+ notify: messagesNotify,
+ refetch,
+ } = useAppSelector((state) => state.messages);
+ 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 (messagesNotify.showNotification) {
+ notify(messagesNotify.typeNotification, messagesNotify.textNotification);
+ }
+ }, [messagesNotify.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, `messages`, 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={messages ?? []}
+ 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}
+ >
+
+
+
+ ) : null}
+
+ Are you sure you want to delete this item?
+
+
+ {dataGrid}
+
+ {selectedRows.length > 0 &&
+ createPortal(
+ onDeleteRows(selectedRows)}
+ />,
+ document.getElementById('delete-rows-button'),
+ )}
+
+ >
+ );
+};
+
+export default TableSampleMessages;
diff --git a/frontend/src/components/Messages/configureMessagesCols.tsx b/frontend/src/components/Messages/configureMessagesCols.tsx
new file mode 100644
index 0000000..d6446dc
--- /dev/null
+++ b/frontend/src/components/Messages/configureMessagesCols.tsx
@@ -0,0 +1,102 @@
+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_MESSAGES');
+
+ return [
+ {
+ field: 'conversation',
+ headerName: 'Conversation',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('conversations'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'sender',
+ headerName: 'Sender',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('users'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'actions',
+ type: 'actions',
+ minWidth: 30,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+ getActions: (params: GridRowParams) => {
+ return [
+
+
+
,
+ ];
+ },
+ },
+ ];
+};
diff --git a/frontend/src/components/Photos/CardPhotos.tsx b/frontend/src/components/Photos/CardPhotos.tsx
new file mode 100644
index 0000000..051101b
--- /dev/null
+++ b/frontend/src/components/Photos/CardPhotos.tsx
@@ -0,0 +1,128 @@
+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 = {
+ photos: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const CardPhotos = ({
+ photos,
+ 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_PHOTOS');
+
+ return (
+
+ {loading &&
}
+
+ {!loading &&
+ photos.map((item, index) => (
+ -
+
+
+
+
{item.image}
+
+
+
+
+
+
+
+
+
-
+ Phototype
+
+
-
+
+ {item.phototype}
+
+
+
+
+
+
+
+ ))}
+ {!loading && photos.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default CardPhotos;
diff --git a/frontend/src/components/Photos/ListPhotos.tsx b/frontend/src/components/Photos/ListPhotos.tsx
new file mode 100644
index 0000000..2aece73
--- /dev/null
+++ b/frontend/src/components/Photos/ListPhotos.tsx
@@ -0,0 +1,105 @@
+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 = {
+ photos: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const ListPhotos = ({
+ photos,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_PHOTOS');
+
+ const corners = useAppSelector((state) => state.style.corners);
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+
+ return (
+ <>
+
+ {loading &&
}
+ {!loading &&
+ photos.map((item) => (
+
+
+
+
+
+
dark:divide-dark-700 overflow-x-auto'
+ }
+ >
+
+
Phototype
+
{item.phototype}
+
+
+
+
+
+
+
+
+ ))}
+ {!loading && photos.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ListPhotos;
diff --git a/frontend/src/components/Photos/TablePhotos.tsx b/frontend/src/components/Photos/TablePhotos.tsx
new file mode 100644
index 0000000..51fc0b9
--- /dev/null
+++ b/frontend/src/components/Photos/TablePhotos.tsx
@@ -0,0 +1,481 @@
+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/photos/photosSlice';
+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 './configurePhotosCols';
+import _ from 'lodash';
+import dataFormatter from '../../helpers/dataFormatter';
+import { dataGridStyles } from '../../styles';
+
+const perPage = 10;
+
+const TableSamplePhotos = ({
+ 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 {
+ photos,
+ loading,
+ count,
+ notify: photosNotify,
+ refetch,
+ } = useAppSelector((state) => state.photos);
+ 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 (photosNotify.showNotification) {
+ notify(photosNotify.typeNotification, photosNotify.textNotification);
+ }
+ }, [photosNotify.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, `photos`, 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={photos ?? []}
+ 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}
+ >
+
+
+
+ ) : null}
+
+ Are you sure you want to delete this item?
+
+
+ {dataGrid}
+
+ {selectedRows.length > 0 &&
+ createPortal(
+ onDeleteRows(selectedRows)}
+ />,
+ document.getElementById('delete-rows-button'),
+ )}
+
+ >
+ );
+};
+
+export default TableSamplePhotos;
diff --git a/frontend/src/components/Photos/configurePhotosCols.tsx b/frontend/src/components/Photos/configurePhotosCols.tsx
new file mode 100644
index 0000000..06c8603
--- /dev/null
+++ b/frontend/src/components/Photos/configurePhotosCols.tsx
@@ -0,0 +1,94 @@
+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_PHOTOS');
+
+ return [
+ {
+ field: 'phototype',
+ headerName: 'Phototype',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'image',
+ headerName: 'Image',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: false,
+ sortable: false,
+ renderCell: (params: GridValueGetterParams) => (
+
+ ),
+ },
+
+ {
+ field: 'actions',
+ type: 'actions',
+ minWidth: 30,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+ getActions: (params: GridRowParams) => {
+ return [
+
+
+
,
+ ];
+ },
+ },
+ ];
+};
diff --git a/frontend/src/components/Products/CardProducts.tsx b/frontend/src/components/Products/CardProducts.tsx
new file mode 100644
index 0000000..5be612c
--- /dev/null
+++ b/frontend/src/components/Products/CardProducts.tsx
@@ -0,0 +1,179 @@
+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 = {
+ products: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const CardProducts = ({
+ products,
+ 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_PRODUCTS');
+
+ return (
+
+ {loading &&
}
+
+ {!loading &&
+ products.map((item, index) => (
+ -
+
+
+ {item.name}
+
+
+
+
+
+
+
+
+
- Brand
+
-
+
+ {dataFormatter.brandsOneListFormatter(item.brand)}
+
+
+
+
+
+
- Name
+
-
+
{item.name}
+
+
+
+
+
- Proof
+
-
+
{item.proof}
+
+
+
+
+
+
+
-
+ Barcode
+
+
-
+
+ {item.barcode}
+
+
+
+
+
+
- Notes
+
-
+
{item.notes}
+
+
+
+
+
-
+ Status
+
+
-
+
+ {item.status}
+
+
+
+
+
+
-
+ Photofront
+
+
-
+
+ {dataFormatter.photosOneListFormatter(item.photofront)}
+
+
+
+
+
+
-
+ Photoback
+
+
-
+
+ {dataFormatter.photosOneListFormatter(item.photoback)}
+
+
+
+
+
+ ))}
+ {!loading && products.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default CardProducts;
diff --git a/frontend/src/components/Products/ListProducts.tsx b/frontend/src/components/Products/ListProducts.tsx
new file mode 100644
index 0000000..d3276f0
--- /dev/null
+++ b/frontend/src/components/Products/ListProducts.tsx
@@ -0,0 +1,133 @@
+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 = {
+ products: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const ListProducts = ({
+ products,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_PRODUCTS');
+
+ const corners = useAppSelector((state) => state.style.corners);
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+
+ return (
+ <>
+
+ {loading &&
}
+ {!loading &&
+ products.map((item) => (
+
+
+
+
dark:divide-dark-700 overflow-x-auto'
+ }
+ >
+
+
Brand
+
+ {dataFormatter.brandsOneListFormatter(item.brand)}
+
+
+
+
+
+
+
+
+
+
+
Barcode
+
{item.barcode}
+
+
+
+
+
+
Status
+
{item.status}
+
+
+
+
Photofront
+
+ {dataFormatter.photosOneListFormatter(item.photofront)}
+
+
+
+
+
Photoback
+
+ {dataFormatter.photosOneListFormatter(item.photoback)}
+
+
+
+
+
+
+
+ ))}
+ {!loading && products.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ListProducts;
diff --git a/frontend/src/components/Products/TableProducts.tsx b/frontend/src/components/Products/TableProducts.tsx
new file mode 100644
index 0000000..87183e1
--- /dev/null
+++ b/frontend/src/components/Products/TableProducts.tsx
@@ -0,0 +1,481 @@
+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/products/productsSlice';
+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 './configureProductsCols';
+import _ from 'lodash';
+import dataFormatter from '../../helpers/dataFormatter';
+import { dataGridStyles } from '../../styles';
+
+const perPage = 10;
+
+const TableSampleProducts = ({
+ 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 {
+ products,
+ loading,
+ count,
+ notify: productsNotify,
+ refetch,
+ } = useAppSelector((state) => state.products);
+ 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 (productsNotify.showNotification) {
+ notify(productsNotify.typeNotification, productsNotify.textNotification);
+ }
+ }, [productsNotify.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, `products`, 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={products ?? []}
+ 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}
+ >
+
+
+
+ ) : null}
+
+ Are you sure you want to delete this item?
+
+
+ {dataGrid}
+
+ {selectedRows.length > 0 &&
+ createPortal(
+ onDeleteRows(selectedRows)}
+ />,
+ document.getElementById('delete-rows-button'),
+ )}
+
+ >
+ );
+};
+
+export default TableSampleProducts;
diff --git a/frontend/src/components/Products/configureProductsCols.tsx b/frontend/src/components/Products/configureProductsCols.tsx
new file mode 100644
index 0000000..8adef37
--- /dev/null
+++ b/frontend/src/components/Products/configureProductsCols.tsx
@@ -0,0 +1,200 @@
+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_PRODUCTS');
+
+ return [
+ {
+ field: 'brand',
+ headerName: 'Brand',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('brands'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'name',
+ headerName: 'Name',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'proof',
+ headerName: 'Proof',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'number',
+ },
+
+ {
+ field: 'age',
+ headerName: 'Age',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'number',
+ },
+
+ {
+ field: 'barcode',
+ headerName: 'Barcode',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'notes',
+ headerName: 'Notes',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'status',
+ headerName: 'Status',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'number',
+ },
+
+ {
+ field: 'photofront',
+ headerName: 'Photofront',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('photos'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'photoback',
+ headerName: 'Photoback',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('photos'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'actions',
+ type: 'actions',
+ minWidth: 30,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+ getActions: (params: GridRowParams) => {
+ return [
+
+
+
,
+ ];
+ },
+ },
+ ];
+};
diff --git a/frontend/src/components/Reviews/CardReviews.tsx b/frontend/src/components/Reviews/CardReviews.tsx
new file mode 100644
index 0000000..da4b26c
--- /dev/null
+++ b/frontend/src/components/Reviews/CardReviews.tsx
@@ -0,0 +1,147 @@
+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 = {
+ reviews: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const CardReviews = ({
+ reviews,
+ 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_REVIEWS');
+
+ return (
+
+ {loading &&
}
+
+ {!loading &&
+ reviews.map((item, index) => (
+ -
+
+
+ {item.id}
+
+
+
+
+
+
+
+
+
- User
+
-
+
+ {dataFormatter.usersOneListFormatter(item.user)}
+
+
+
+
+
+
-
+ Bottle
+
+
-
+
+ {dataFormatter.bottlesOneListFormatter(item.bottle)}
+
+
+
+
+
+
-
+ Rating
+
+
-
+
+ {item.rating}
+
+
+
+
+
+
- Notes
+
-
+
{item.notes}
+
+
+
+
+
-
+ Createdat
+
+
-
+
+ {dataFormatter.dateTimeFormatter(item.createdat)}
+
+
+
+
+
+ ))}
+ {!loading && reviews.length === 0 && (
+
+ )}
+
+
+
+ );
+};
+
+export default CardReviews;
diff --git a/frontend/src/components/Reviews/ListReviews.tsx b/frontend/src/components/Reviews/ListReviews.tsx
new file mode 100644
index 0000000..4a00474
--- /dev/null
+++ b/frontend/src/components/Reviews/ListReviews.tsx
@@ -0,0 +1,113 @@
+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 = {
+ reviews: any[];
+ loading: boolean;
+ onDelete: (id: string) => void;
+ currentPage: number;
+ numPages: number;
+ onPageChange: (page: number) => void;
+};
+
+const ListReviews = ({
+ reviews,
+ loading,
+ onDelete,
+ currentPage,
+ numPages,
+ onPageChange,
+}: Props) => {
+ const currentUser = useAppSelector((state) => state.auth.currentUser);
+ const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_REVIEWS');
+
+ const corners = useAppSelector((state) => state.style.corners);
+ const bgColor = useAppSelector((state) => state.style.cardsColor);
+
+ return (
+ <>
+
+ {loading &&
}
+ {!loading &&
+ reviews.map((item) => (
+
+
+
+
dark:divide-dark-700 overflow-x-auto'
+ }
+ >
+
+
User
+
+ {dataFormatter.usersOneListFormatter(item.user)}
+
+
+
+
+
Bottle
+
+ {dataFormatter.bottlesOneListFormatter(item.bottle)}
+
+
+
+
+
Rating
+
{item.rating}
+
+
+
+
+
+
Createdat
+
+ {dataFormatter.dateTimeFormatter(item.createdat)}
+
+
+
+
+
+
+
+ ))}
+ {!loading && reviews.length === 0 && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ListReviews;
diff --git a/frontend/src/components/Reviews/TableReviews.tsx b/frontend/src/components/Reviews/TableReviews.tsx
new file mode 100644
index 0000000..5769c29
--- /dev/null
+++ b/frontend/src/components/Reviews/TableReviews.tsx
@@ -0,0 +1,481 @@
+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/reviews/reviewsSlice';
+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 './configureReviewsCols';
+import _ from 'lodash';
+import dataFormatter from '../../helpers/dataFormatter';
+import { dataGridStyles } from '../../styles';
+
+const perPage = 10;
+
+const TableSampleReviews = ({
+ 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 {
+ reviews,
+ loading,
+ count,
+ notify: reviewsNotify,
+ refetch,
+ } = useAppSelector((state) => state.reviews);
+ 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 (reviewsNotify.showNotification) {
+ notify(reviewsNotify.typeNotification, reviewsNotify.textNotification);
+ }
+ }, [reviewsNotify.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, `reviews`, 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={reviews ?? []}
+ 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}
+ >
+
+
+
+ ) : null}
+
+ Are you sure you want to delete this item?
+
+
+ {dataGrid}
+
+ {selectedRows.length > 0 &&
+ createPortal(
+ onDeleteRows(selectedRows)}
+ />,
+ document.getElementById('delete-rows-button'),
+ )}
+
+ >
+ );
+};
+
+export default TableSampleReviews;
diff --git a/frontend/src/components/Reviews/configureReviewsCols.tsx b/frontend/src/components/Reviews/configureReviewsCols.tsx
new file mode 100644
index 0000000..da2d3a7
--- /dev/null
+++ b/frontend/src/components/Reviews/configureReviewsCols.tsx
@@ -0,0 +1,144 @@
+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_REVIEWS');
+
+ return [
+ {
+ field: 'user',
+ headerName: 'User',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('users'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'bottle',
+ headerName: 'Bottle',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ sortable: false,
+ type: 'singleSelect',
+ getOptionValue: (value: any) => value?.id,
+ getOptionLabel: (value: any) => value?.label,
+ valueOptions: await callOptionsApi('bottles'),
+ valueGetter: (params: GridValueGetterParams) =>
+ params?.value?.id ?? params?.value,
+ },
+
+ {
+ field: 'rating',
+ headerName: 'Rating',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'number',
+ },
+
+ {
+ field: 'notes',
+ headerName: 'Notes',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+ },
+
+ {
+ field: 'createdat',
+ headerName: 'Createdat',
+ flex: 1,
+ minWidth: 120,
+ filterable: false,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+
+ editable: hasUpdatePermission,
+
+ type: 'dateTime',
+ valueGetter: (params: GridValueGetterParams) =>
+ new Date(params.row.createdat),
+ },
+
+ {
+ field: 'actions',
+ type: 'actions',
+ minWidth: 30,
+ headerClassName: 'datagrid--header',
+ cellClassName: 'datagrid--cell',
+ getActions: (params: GridRowParams) => {
+ return [
+
+
+
,
+ ];
+ },
+ },
+ ];
+};
diff --git a/frontend/src/components/Users/CardUsers.tsx b/frontend/src/components/Users/CardUsers.tsx
index f4ce406..718090b 100644
--- a/frontend/src/components/Users/CardUsers.tsx
+++ b/frontend/src/components/Users/CardUsers.tsx
@@ -173,6 +173,28 @@ const CardUsers = ({
+
+
+
+ Address
+
+
+
+ {item.address}
+
+
+
+
+
+
+ Address2
+
+
+
+ {item.address2}
+
+
+
))}
diff --git a/frontend/src/components/Users/ListUsers.tsx b/frontend/src/components/Users/ListUsers.tsx
index 61fe76d..76796db 100644
--- a/frontend/src/components/Users/ListUsers.tsx
+++ b/frontend/src/components/Users/ListUsers.tsx
@@ -113,6 +113,16 @@ const ListUsers = ({
.join(', ')}
+
+
+
Address
+
{item.address}
+
+
+
+
Address2
+
{item.address2}
+
state.style.websiteHeder);
const borders = useAppSelector((state) => state.style.borders);
- const style = HeaderStyle.PAGES_RIGHT;
+ const style = HeaderStyle.PAGES_LEFT;
const design = HeaderDesigns.DESIGN_DIVERSITY;
return (
diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js
index f3620b8..ee5fda9 100644
--- a/frontend/src/helpers/dataFormatter.js
+++ b/frontend/src/helpers/dataFormatter.js
@@ -58,44 +58,6 @@ export default {
return { label: val.firstName, id: val.id };
},
- brandsManyListFormatter(val) {
- if (!val || !val.length) return [];
- return val.map((item) => item.name);
- },
- brandsOneListFormatter(val) {
- if (!val) return '';
- return val.name;
- },
- brandsManyListFormatterEdit(val) {
- if (!val || !val.length) return [];
- return val.map((item) => {
- return { id: item.id, label: item.name };
- });
- },
- brandsOneListFormatterEdit(val) {
- if (!val) return '';
- return { label: val.name, id: val.id };
- },
-
- distilleriesManyListFormatter(val) {
- if (!val || !val.length) return [];
- return val.map((item) => item.name);
- },
- distilleriesOneListFormatter(val) {
- if (!val) return '';
- return val.name;
- },
- distilleriesManyListFormatterEdit(val) {
- if (!val || !val.length) return [];
- return val.map((item) => {
- return { id: item.id, label: item.name };
- });
- },
- distilleriesOneListFormatterEdit(val) {
- if (!val) return '';
- return { label: val.name, id: val.id };
- },
-
rolesManyListFormatter(val) {
if (!val || !val.length) return [];
return val.map((item) => item.name);
@@ -133,4 +95,137 @@ export default {
if (!val) return '';
return { label: val.name, id: val.id };
},
+
+ distilleriesManyListFormatter(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => item.name);
+ },
+ distilleriesOneListFormatter(val) {
+ if (!val) return '';
+ return val.name;
+ },
+ distilleriesManyListFormatterEdit(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => {
+ return { id: item.id, label: item.name };
+ });
+ },
+ distilleriesOneListFormatterEdit(val) {
+ if (!val) return '';
+ return { label: val.name, id: val.id };
+ },
+
+ brandsManyListFormatter(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => item.name);
+ },
+ brandsOneListFormatter(val) {
+ if (!val) return '';
+ return val.name;
+ },
+ brandsManyListFormatterEdit(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => {
+ return { id: item.id, label: item.name };
+ });
+ },
+ brandsOneListFormatterEdit(val) {
+ if (!val) return '';
+ return { label: val.name, id: val.id };
+ },
+
+ photosManyListFormatter(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => item.image);
+ },
+ photosOneListFormatter(val) {
+ if (!val) return '';
+ return val.image;
+ },
+ photosManyListFormatterEdit(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => {
+ return { id: item.id, label: item.image };
+ });
+ },
+ photosOneListFormatterEdit(val) {
+ if (!val) return '';
+ return { label: val.image, id: val.id };
+ },
+
+ productsManyListFormatter(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => item.name);
+ },
+ productsOneListFormatter(val) {
+ if (!val) return '';
+ return val.name;
+ },
+ productsManyListFormatterEdit(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => {
+ return { id: item.id, label: item.name };
+ });
+ },
+ productsOneListFormatterEdit(val) {
+ if (!val) return '';
+ return { label: val.name, id: val.id };
+ },
+
+ locationsManyListFormatter(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => item.name);
+ },
+ locationsOneListFormatter(val) {
+ if (!val) return '';
+ return val.name;
+ },
+ locationsManyListFormatterEdit(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => {
+ return { id: item.id, label: item.name };
+ });
+ },
+ locationsOneListFormatterEdit(val) {
+ if (!val) return '';
+ return { label: val.name, id: val.id };
+ },
+
+ bottlesManyListFormatter(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => item.id);
+ },
+ bottlesOneListFormatter(val) {
+ if (!val) return '';
+ return val.id;
+ },
+ bottlesManyListFormatterEdit(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => {
+ return { id: item.id, label: item.id };
+ });
+ },
+ bottlesOneListFormatterEdit(val) {
+ if (!val) return '';
+ return { label: val.id, id: val.id };
+ },
+
+ conversationsManyListFormatter(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => item.id);
+ },
+ conversationsOneListFormatter(val) {
+ if (!val) return '';
+ return val.id;
+ },
+ conversationsManyListFormatterEdit(val) {
+ if (!val || !val.length) return [];
+ return val.map((item) => {
+ return { id: item.id, label: item.id };
+ });
+ },
+ conversationsOneListFormatterEdit(val) {
+ if (!val) return '';
+ return { label: val.id, id: val.id };
+ },
};
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 69ad530..860a203 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -16,39 +16,6 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiAccountGroup ?? icon.mdiTable,
permissions: 'READ_USERS',
},
- {
- href: '/bottles/bottles-list',
- label: 'Bottles',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon:
- 'mdiBottleWine' in icon
- ? icon['mdiBottleWine' as keyof typeof icon]
- : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_BOTTLES',
- },
- {
- href: '/brands/brands-list',
- label: 'Brands',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon:
- 'mdiTrademark' in icon
- ? icon['mdiTrademark' as keyof typeof icon]
- : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_BRANDS',
- },
- {
- href: '/distilleries/distilleries-list',
- label: 'Distilleries',
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- icon:
- 'mdiFactory' in icon
- ? icon['mdiFactory' as keyof typeof icon]
- : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_DISTILLERIES',
- },
{
href: '/roles/roles-list',
label: 'Roles',
@@ -65,6 +32,86 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
permissions: 'READ_PERMISSIONS',
},
+ {
+ href: '/distilleries/distilleries-list',
+ label: 'Distilleries',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_DISTILLERIES',
+ },
+ {
+ href: '/brands/brands-list',
+ label: 'Brands',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_BRANDS',
+ },
+ {
+ href: '/photos/photos-list',
+ label: 'Photos',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_PHOTOS',
+ },
+ {
+ href: '/products/products-list',
+ label: 'Products',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_PRODUCTS',
+ },
+ {
+ href: '/locations/locations-list',
+ label: 'Locations',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_LOCATIONS',
+ },
+ {
+ href: '/bottles/bottles-list',
+ label: 'Bottles',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_BOTTLES',
+ },
+ {
+ href: '/reviews/reviews-list',
+ label: 'Reviews',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_REVIEWS',
+ },
+ {
+ href: '/conversations/conversations-list',
+ label: 'Conversations',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_CONVERSATIONS',
+ },
+ {
+ href: '/messages/messages-list',
+ label: 'Messages',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_MESSAGES',
+ },
+ {
+ href: '/conversationparticipants/conversationparticipants-list',
+ label: 'Conversationparticipants',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: icon.mdiTable ?? icon.mdiTable,
+ permissions: 'READ_CONVERSATIONPARTICIPANTS',
+ },
{
href: '/profile',
label: 'Profile',
diff --git a/frontend/src/pages/bottles/[bottlesId].tsx b/frontend/src/pages/bottles/[bottlesId].tsx
index 0f0d404..8625fd6 100644
--- a/frontend/src/pages/bottles/[bottlesId].tsx
+++ b/frontend/src/pages/bottles/[bottlesId].tsx
@@ -36,35 +36,41 @@ const EditBottles = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const initVals = {
- name: '',
+ user: null,
- brand: null,
+ product: null,
+
+ location: null,
proof: '',
- type: '',
+ age: '',
+
+ rating: '',
+
+ collectable: false,
+
+ rickhouse: '',
+
+ rack: '',
+
+ release: '',
+
+ barrelnumber: '',
+
+ barreleddate: new Date(),
+
+ bottlenumber: '',
+
+ dateacquired: new Date(),
+
+ volume: '',
notes: '',
- tasting_notes: '',
+ photofront: null,
- msrp_range: '',
-
- secondary_value_range: '',
-
- opened_bottle_indicator: false,
-
- quantity: '',
-
- barcode: '',
-
- picture: [],
-
- age: '',
-
- distillery: null,
-
- user: null,
+ photoback: null,
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -117,116 +123,6 @@ const EditBottles = () => {
onSubmit={(values) => handleSubmit(values)}
>