From 2fe8d709f012315170c8443c314a2263b931c1a4 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 2 Jun 2025 11:28:51 +0000 Subject: [PATCH] Updated via schema editor on 2025-06-02 11:27 --- .gitignore | 5 + app-shell/src/_schema.json | 7 +- backend/src/db/api/likes.js | 246 +++++++++ backend/src/db/migrations/1748863668173.js | 83 +++ backend/src/db/models/likes.js | 49 ++ .../db/seeders/20200430130760-user-roles.js | 26 + .../db/seeders/20231127130745-sample-data.js | 126 +---- backend/src/db/seeders/20250602112748.js | 87 ++++ backend/src/index.js | 8 + backend/src/routes/likes.js | 433 ++++++++++++++++ backend/src/services/likes.js | 114 +++++ backend/src/services/search.js | 2 + frontend/src/components/Likes/CardLikes.tsx | 109 ++++ frontend/src/components/Likes/ListLikes.tsx | 87 ++++ frontend/src/components/Likes/TableLikes.tsx | 481 ++++++++++++++++++ .../components/Likes/configureLikesCols.tsx | 74 +++ .../components/WebPageComponents/Header.tsx | 2 +- frontend/src/menuAside.ts | 8 + frontend/src/pages/dashboard.tsx | 35 ++ frontend/src/pages/likes/[likesId].tsx | 124 +++++ frontend/src/pages/likes/likes-edit.tsx | 122 +++++ frontend/src/pages/likes/likes-list.tsx | 160 ++++++ frontend/src/pages/likes/likes-new.tsx | 98 ++++ frontend/src/pages/likes/likes-table.tsx | 159 ++++++ frontend/src/pages/likes/likes-view.tsx | 81 +++ frontend/src/pages/web_pages/services.tsx | 2 +- frontend/src/stores/likes/likesSlice.ts | 236 +++++++++ frontend/src/stores/store.ts | 2 + 28 files changed, 2861 insertions(+), 105 deletions(-) create mode 100644 backend/src/db/api/likes.js create mode 100644 backend/src/db/migrations/1748863668173.js create mode 100644 backend/src/db/models/likes.js create mode 100644 backend/src/db/seeders/20250602112748.js create mode 100644 backend/src/routes/likes.js create mode 100644 backend/src/services/likes.js create mode 100644 frontend/src/components/Likes/CardLikes.tsx create mode 100644 frontend/src/components/Likes/ListLikes.tsx create mode 100644 frontend/src/components/Likes/TableLikes.tsx create mode 100644 frontend/src/components/Likes/configureLikesCols.tsx create mode 100644 frontend/src/pages/likes/[likesId].tsx create mode 100644 frontend/src/pages/likes/likes-edit.tsx create mode 100644 frontend/src/pages/likes/likes-list.tsx create mode 100644 frontend/src/pages/likes/likes-new.tsx create mode 100644 frontend/src/pages/likes/likes-table.tsx create mode 100644 frontend/src/pages/likes/likes-view.tsx create mode 100644 frontend/src/stores/likes/likesSlice.ts diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index c8ab79a..0fbf364 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"3NjWzgOw51ARgo3i\",\"encryptedData\":\"Qx8qk6tI4Byzs6kl2l6BsyrLHKUEPWqxZSB3HFSKuZaQjdiBKqA+tFP94OjlPGPTq+dkImMN/kSfEregtkFK5voIQKe4JxTJAzqIQdQu8fUfL8mW6ddQKgYAW6cb3ScZwmijHCl+yhO0aM7FIgNphho6+kYRVkZ/LA7P5bvbcywq+OD0ekZjHJBgDn06dtNEUeOlznYrk9Z6xAjRz5kO2P8kUdvqxvrcN+qnJ75TJzdWrBMLKULj9SWC7UNXSMw52K6RbMu3aWZoxopnEr6h1L11KulRPKcBsKmJN+ItioBW7iSLPJAzk4n+SR/0wq4EWTXzStK5r1chmgRCPBzVW8GbJ2QghXtYzuKUA6oevNvN9F7xVvEEnvwH/eHbSWU5fJPOCB7ro//UEQyYA2tpFnb9HOewSJH/Sm13O/gedcBMhNAhm9z4dght/08JtpEZKoHTGWgoPYmT0v8ywAGxvohrqVj9Syoow3DkUHssZmud0PKER1yC4j9Q6oBT6gpv20k7kARVX4PScxVZBoLJpKd1FQC+FpnMozcZQWCD/T9Cd4HA8tvy4OIcfTs0dgPnx7NxJ+VNCxYE1lGNtKWKc0J2xWL9b7nYSsYAdkXbzTwbhZh8uxjgUO2Bb5gIeetGQyQJ5RwSYsvKJLT4HZ3CfBHNpZWvN7fMDhkSZ91FWIq0zQaQ+jbafuakzNzv9PXUjnE5/xq1uVkJCVZzHQcCSkWsvyTdCBzDLImGKaM0wI+HUspLLpMxfZq80ntEPEzk8+cUEBWOWhnKsvwCc1Bjvg7fXqE7Pr/KBVZXNwdcUMBAzIkvH7QtnlSzyPsYKayIYInyoiuTDgqMrbVhuJNwpe+trRfV9DOhruz6QC2RCHq7Kpne7Tzy8yUiRpmO3W9H4X58INIwZAXrT46IifH+pxVg/i+szzQQxTOLvXNKYeqFFFraDDjsA9/KK8Y+rhE+hA37MekpphGGOBrXGBp5s/+CBFdM6S1g5T6jqnKYtuwyMG11NbTWo1/BQd0LecsDOwGYUf8jmdZcLqhSRUWo+g7T8UF63YHOXRvbEb4bmtBltWWmabTjVKWlygOu64NbIKe0H5uWuT0/6rNfxRCIptYRb/RUP9s2vNlniRrtDuQmNSP6g3CQqGv17W1HS64vy1dYbw+ue6lX3wwBrI4xRKNFurZB+vRQGEmuNMGQ+YoL5WORG9FrmE5dWIg1s9USsBFG2Ogj3ZlDjWowm0fOp/zgoOa8vEccJ/ajc/dSghlrDA1E3TtKQBb8QBHBBayWwaaVKWHAhsyk/dkVgAbmPjAum+ETv16HD5ucKhBzAiqGGNErzQXgp7WjBJcPIFWU/k8KHDvJIeWJ4zwaduOnyAtBAljME0zlw3ohzHjUF5LkonUwyhn/8hL8xM37xhw2ExeQD1vSipnUFkJ6VMtfxfVy6RrJnfwdrBfjZ/qQRfnSND8vpnjFXw5oBIW/rkORsYnktVeh4snuPdi4bV9MURYjdjTLWZjyJ30iqokFBgzIS8/RnqaOXxB5iROasJClS4nbI8xu8njo4rbhOal7QSjrm1F2ZigdhUjm4ViD0gNQoj3B/i82r0UKAPBqEMjRZc9E23laLAloEABMZ3GbjsBlyISL3orCS0pF/X852ifPy/P33J78BqVr8nmbaaNwKxGPin497Nb7DbnE3wO4BaLJFQ/IknbNpwtjrOqEAsQ12XmqwQbgszdmeBQKRgFAxKtsXIMl88qDG32UDJUQZqt//C3criX2r0jrVXGk6QSQnRSp9uTkYVAfH1AE9+yU+acA3LcKGDjCIOQ54f7e0ScxoBHHMwFzjCRaGMRfhxyJdIH+lRgKpzqN0XGDJjQNJ9dIl5pvQZXWYNpYZi3ksLqVC5DeYi2IJGBf7EmIPgSBYrNouZ2PYnKGVc58xgNcbB2wFbIYm83IU6vLF9+6SrAQeO4yWaRC7PrjWCuFEYiC/RDO41DbxSqrSVNchHq7GkWBT4kdJOP1iiM+zEA03X6QL0FWL/Gn9lgWx84RcuaWDwt3c4BNYj5x05w7F86iORZ70mtK3Nx4HeKyq0FqGWMusnL5sQvWsPfqshhNmaI1k0mNg9rs6LjvUENmEGkGp7GPtgj4B+qYbugX93EzbCWiJP1+UjdYPgm5SMy/zkH/4LgTa+WcUj0VJN/W0jO6I80hjQ4qZrrHEISFHndokxVQRxsfMyCAoFYM/+wKdfuIiZMoLKBj6LhMVuHoSB6xdAb4tr4G3zOctqvu+1ciPj2onq+67zabAa3/H9IE0YjXVyf1dHyJgMBG2NtpyTSLI3o3hUMXdvOMi0XnSnjI/WCT2eBmMqJiqHe90n7AeAg53FXFtwDRwhYfNKssc7HumQ5UaZCXEclDuT1ztBTo8IfMq+DoMgyBpXj0YFp+FPs86cR4tlilPXbKzQaUPNvgzatWSjo8AnIBahvbau9m0Q7ADJT9XP/lL88Ml8aAD14vvq2lU6sDObnhbO+vuw5GdM9HGxntRBk8QeUMkiP8UG3y/TwgEdQTAzMqgTuWVmNIowv+BdJq6AhNp6ItbrU4B6flZykY5rQZmNhhMWGsdkbhaGDMMOnTotT4YJYV6tO3P1w37POvvnziBORfaWBYKJqeiuDwPL+HlN3egr2GXEoLBe2gRp9G1vJkP1S4vG2p90ltQmJFmOoMdqr/h6qsCHlmlEZbgiCieUA/amGh64uFB44D5OLNstHelL1dOpFKSkK+Ic0uXGm7DX35u+eqMa38uJKfyBy+cv45QtwNaSaPf2zyEVgPIlWJSW/4Xsb8WtoxcwL2L3sUXfVdAJdygD2jeRGH2V+VLb0FUtnu8JDw5dpvz7mJDaIZ7wMFmqfZIOaWa9BGXG2WWbQ2l/Rp87qjdTIEb6Pdk7G+V8dr/CLzDvlPXtu6Qs2l6KiMjjcDdo9SZ1pUFHyCymLeOqXALOyIu0MKxg/5qUby0qNp+vEZHoAhyYFIR1PyTb/kBLEkadTfM8V6v7hGxRGTsHrTfTgo6qg/5TTfEKO2GMoz8MneMxo39XRuCwm/WCQkcptQFnOsGQRAhWL58yddJIU1F0dfE49i5oKDUiCp2Xb4ihldXyLgcR1wq6GcS3ppRh3orxJ2BpvSGbeL0u4kU8MngAZHAcNRK95WBm0iVXyZJucFl2HtNjVao/PxI6HOtYolctJwdcToH/grjbIcEPas1JO6U1RZmckVPty1szKgWmjfCydMMF8pbZtrK3p0Hq4tWY1qIhAwJo2rVHADlmIXkxHAs84mwUaxfajzSL2E7lR/5ETBmrQLczsFXxey1j4NO1cFtDNHSlqfC+R1/VFs3zkjwLW4bUCcUoEw3ahsUqoPoG3nJ3Z4b2zHPtjNsKQacUK8yVrZ+z8qUOWU0twlpUkaaxUbFgq0VTRLwRAXZZ7TiwiXYET4J7aRs5p3xtAhwxXVyzrFeRPfsuKqrAesBGuHi1uhvloIQpcjda6ovMWHGNL7SozGKlAop5r1Y3XU7Tas5LnsdAgizHT0tAmDEMB+GEqBRXEpndywckNasWGFsPVji0bJUaaNN8h8JrYNsjcnaX/9r/u0emhbVyenUYO4mGMtEA4L9J2/lKTSJaMvRbPR9ZvVvc1CEYH02a7OoNHLzVHdgzI3vg4ZOrJlFm63mjwYFqOIvTvNowHbFd7U9Ow7k6j39tca1dBmbNn3pusrzMlfASpTHliVw6oelJVx5G+nwCp9BTF2P+mEbFTUyr7x1WKDhb3EtY9tN/oi86CrWhH9UmBEB/FpJfNWk/6PPYzJyu5TlG1PcKwN/nI8EvekHny9oKUUi4QdyQxtRwsZlflXdE9lSeuWUlAY+aGaKtwNYirdwjL/9N8cytuiFS7oaydpg3Co+ptwaL0EO1gUGFDD8zQ2Q2eLzgEKncVmgH3aAMXs2fBVxNsdQMWrHQcNtRiUnC+MiabCYo69Z2D9/Fkat5BnsT+2DF8p7ZfxB1DYJyjrWwsIVO7bQY8Lo73frMe3RiNNCDzJWD0P5zOoSH58sxGfBZv4Roc36lwqTG8gDmEjeD/Cq9CHNpbyipdKMSXbzc3hKqGJ0CUH3NkXFSWzuB0cygRSxVVLCOBjVTODJ2J7TfoQk3bx8dxtPq8wYDHV1ByU/FpGzdugsPhOiLxI+FXvmYghiasfbb3JQZ0Okn1T8s0x0+cMwBpUjCnM+Yxkf2CDlexBcTQ4jmo0nDVMvaRiTPUyzhTcHMyzQ/QkUR+WVFkA+YiCNGJcqGxG5e02YiPdYLrKYVf+0hW1Q7k52jVeg95ZJYsxhO+6DRiqZAKgsZabfqswFzcWalQkxUBfmaWZEZxTHz6bBx6VFixn/zJxN8Ixv5PZjCOrXVaEB3hwhMyHcWolNZNXkftk7CnYUOXB9JQ7GiUDl4ydhv7q/b+PnTy4eE+SVFxs4aopOFhk1K2/EkqCVYxcP8kTxbgj7xfcUGKo4THebSg01RWy9ODvs+Ft9DEzRWE3vZMyWfmHizAztkIeJ7XIlUCgY8YzNl1sGnQlKDRH8b3KAQN1rh64SkPyslAuVxWTI9M/oqn9DvO1b1SE09LMD5BYdd2mfgqH0ZFf87GrbI0FOKictNlnaeb2RwC1DKzIu7EMJDk2eZuJIdvMfpkR1RVTOD8G7msfNo67bQ4xJexrK/j+cXYLaSgZ1/qWNLugTzKZTbak/NNoShnwlYcBFsKFZM29uNygHh6P9DzaxJ66YhoIf1rbPn4DCdQQK3cyLYB3XpI0A5jF9ximvvZYSsvLF9S7qSS/GC2qhqFZkKdqFNCKnzqthb4PuWpmMVJ2ttz7brJjZJ2Nk0yLnciX30t5QeWarrzHqJEqtWOr1xWz7c2Lk4bgmmSCOPQVM32txsc56xxd3rFEMT8FhpE2ut/32G6g+meRRTKXsHjJXmQvbrZOadE/NTpsXS75U8pLJfG0UZIZghHPVUMUjrc13b3FJB2BoSskCSkWCpSvCrVYycI0tdSjkvbVuvTNMGr5kGvTOgNui3/7jMQN4FJHlTfATev0ZwDiK1t6x1u+MvKjtW5jfheun4VhNPoxBD/K3UnQfVkx3qji4+fMHzq7TwgBsQROEO0ZrCcS0OntNR5Iog2c3puMF8x6TNpod0OtBXV3F1RKZCeDrD5zXSosvSf0UKQE0K7y/2LWL+j4wkRu57ZWViSRQOfTAHbDf1rNvj7EbOTRAgO2PT9EBbq2vCkMLneEgOTXVOwd7uh3mLO8Y1A2Th2up7p6U/tegJHOWdYhVhwfQ4vSqpYPcLaFgmwgtyuexo2VvXxM+C08rQrTbVS3SBo8BRxRBEt/ihOjcWWbqy0X+XAw+W2MUJQDv7p5FJcSrtdmY1mjt6xlmdV6vgAame/hOeyRIPFgWYDpnD4OzN20bbWzCoGwCQox3TX2aYI44zBhuS88KQneOa3YFZ8PZP+9drB1dCvgiPVw5VJNTSjIcZgAmZftshHJfHTZ0a9bItcpKBMXSX/7d39mxzSxkJRpPoKgfdzdvf2arPr4itf7VYzs1Ziseomhl+fVO6M7ghG4sbxpvgtlwuu7/CT8HQ8m4YWwoPKM3Z+ZVXv9PPpSWSQyolPRPaXXw9ClEd3umBfZyFnxpbr5nVgWsygOToKAEbZOXNOQnQq+s3SBWst3g+9uyaA0v/FOflRsLW2ts/dLUWTgD+yuLcQScyEwZXmJHDQIcCkRQW6Nqck2dBZCqZAqMLPCpOK9sHyEm4m5WO1Jxjx3wpZz6cWLHHuIv8WD5bpgj0NG5Wn1icGG0bWMLOjRMIZcn7iilgNqEwmKscezqA5Vlh4nXFK6lRAvx382HO1GJoFmHvQWzvt1QSfpeYBPRTYVEIvh9EbfLOyQ4yw+bU2agkVVKhChEgz1oQSbHRe7h86ZjAnU8KgvoMec16gNq51Y+zLm7iiuuSK3KvIEW5c9J+3uNJrGtjaML1LyuTp5gwYIh2278b3LU/DbruXVX+VmNFJqgkkHphmgwiQCkZPpUFt1154THYj6Oir+RF3G2Wm5Iryv6yksJgF2Lfi4fshOWf5wFzQ9wCRcT6EzjC4FkzF9ACHixXqMEgr6hQkBNvBuZok9AJdSGj0Hq5AsUolCmsNBo996hZsoJ1Zl/A5hCasJ8LD7VMl090RpYxH1MNHub/g8y69Ydf9rQd/OujL87qF5HJzrAXpiOwXr1gVDRloD4vDZpLtEfXw2CHwd53MgITJ4DxwnLVMp7PJf+e2mGvKOAIox3Qz6NoucPykysZdkJVsLxizpTEvsgKIfUcefK+UCGqxHGkEz0fiLdFtzoOxhHcD1g3rW/f1mr5byh8Ojbqu/c0NMjtZLiw+S3MMBQ+zB4p054RoTHA9WxszfFSodisqujytYpF78XtCCzhOnhR+0kGo6NtJKo/S7ZzwG1HengDRJOCUShoBvhy735C3vj/rF5NIoVW2AYQJ0Q3SR65Oc7l+R2S+JsOvE7NQM5UkMFWYbiqaJToK0OI0ReHN5bBwk0JzpldRtbkCwb8cVQiJ5NX0HfgJ5gnLVIcYbmnCRpWiaoAEmtNJxtWE1rbQJz4SMafuIhOzmGXYwdgAXFAjqBLlrQz2oDhjJRe8k9NzrrbBFHIwxQx8m1b/m+icC63Irfw40FijjRYzyxkY7GYLq5MYr9/o3D/LMg48AAViyOu3ADi0r7LoCXINuBjFmf5+9XDyrAAGTJqHZDIhq4oyBvqXB10oUa7aXxqhMO4DXx3D2vwpOjPYN48xdHFwsWcd+gMEPxOdyhew7vhjdI1s+0lp4OR8FlXMS1uSkxGMm/3rLwla9NaCsEJPle0o7nOua/870lzNDGOdikaQvog/Mj534JrAuxYfjxpKPTCubC2yYMtjOGTSvjR4+2LL7r7FXPntT1yWTMosILG3Ogjv34tTNBhphjZ7tySd57uknRWVZRwjSDOnYgQxm0yVMKwuv1/SfPeDlOqK/cRZvwRV+gMhFdWA7S15yM2KSgjtI9cm1oE+bqeI0hl4aNDeK7eNhBwafgqGpZkMMOe8i0xvkC3x5pwEn2YH9ebFCgeYyuw4RRqrWSozwPEK3ApYL4T7FJzK3FO7BeaK2JlpFnfea0t2+MtpmJMBl13i8YzuOc2wtpCCafsc+DCnHYTLDFq1rb+DGmUql2ux63ul6TRgVS5DEiVsEpbrNDPCRHSe1Qp1A6ANh8fUokUnxbVFiGaqru6euOc884AsrsDIAKLUBqOiD898jaRk58XjbFTS2MFY4dJnNaMpmR9haG7hoYSysmHIEbol6Xa3ENzYfwFAKd3VKWv6flhs9JHQh0uzUnQxsAllqnIGKs7uXC4TyQvhQwSN+lfBsjPO2idai+TRzXT1V22+TM3vVgRMCJmek8OfChP00+/6g8hDzYeyZUtgBaBd8XFdXVsC4PbOX2s1ULbxOnZKim5b+yp5TyMoQSCqdv4enX+/K4j64le/YqX5KAplxCXbxuyiYnf0Y4YHXcQwbqGL6wsuoHrwcGGp7ApVTIkHMVs+D2dqe8Y9u9dWwS1pXSAji0J3QLKLiW3fib+LBTo2lYE/ZJJwQ0ZblB78TFOJRqYFpRxCm9MeBgr++RBZY07OUa4YiyLr4cxiWM0GSPzgQTNFP72Vq/QYWVD7NFD1szS4ZPljMXe7yVDayACcjLJ+v5r3n3eUR5pVFN280qSE9hzDXjDLGXKPYLBRqr7i1CNof/STJCtQ3WeBoIQkrm1demk+RM2zaKRbFJMXlV9kPbvrYqRtp0uqYwI52uKYzwBCKnVZ+1knVD9DdxuyyV6TqOqaOr/D7b9+iIOvIAotCashrbsYrehT9pxmzz6lG7aB/II4wmKRH+JqNL4ILpE0wGdzi+KGxmmo1q1G9hTWEvhxYYldBy1YsgcAbQaP6fyYAkC5lMMQG6Wlh7rStGMIayOUODKASprzJiykCDBDUSmI0XpKuvi0dPQKSAZ9BJMzzTd9CcbCTasPGcPe48s5jKYVdCJIBFqflZwLtxCKJ2aVN94AsbPuDYPaz9mmTNMWb8aAG8eor3xqXtIQhCvUAnjPRZpMJcWpcLLsbiPTH8YXgTxrHcaQWRDCr4o2/LNSyPS2mXisltsSQeoWQ1OxTup2C+pyKH/HQWX1O62qTUe836Czz9LQbDHhqztdvVCJ63O8Vrs2nkT2VUEBPP3UJoW7rS4+p08+0kJwEawqHd8TwRAAQNVx24ZdxRz1Ab9V1qsgz9MczJJTcIj9H10nY970uav9yRF3htPHHfevLUjunzNofa36aj0nuex5mW+ILz5SGJndc+h+8+ZDc4/p1SbuMwtQih9mZLagciZ4qEWCXKA6C3VYrYuBUBv5P+J5FVq6Ee3X0/WbPcmbASTe1ex13fqBzUEBNFzMp1KQ8fzeZm0xiEmHYciPKv4fQZEEN+Y7dTysTQTIKvtAyuXSVpg1fZvOXciKXgcfTRkAGeUhwM2QW76TgYVvC4SQYcXDvSEUMl31HCgdMla+AUdlTKcjAPH9LKERn3A5qMcuH5kIi9Yle1PcnSm6cqehQl+Bwg2U29yjoc2BdST9Cv00mn7ENyA1cvrli0dQPCLmqKmtcT9jIwmt9eucDZlDYFipJsvQ9H3Gf+f+g9Ilwxlh1y4tsDe6zXaZIlmKWb2He43PTlL1unNOYayndmt1xM1c0lGXaqSuNlhVIJ0LdyZmmHYzcFAKi2oz8x3A/Oz7eR7ZbaPK+eL3k47cgAA00NmOL1quqgZyEgvZdQomC/UBls9SjppOSjgEpAsosKPDSJQ6rLsurSqzZbte9+xc+QvOSEKC+WrK+XPr3oCs94DGH+HEHlo1xG49UByNPLf6jpGKlk5b4bZUjozmcTokG6IJ1vs9zAbp2Rhn0lzKpk+XbW9mGh/8/tzH7kDS7JFP6bs72V2oAegO5IqvUaDDZg+F5rgF+dulfDh7zPZMc/V/jNlgpls4qjHpqL/uwYhTy+vexj6IhFZH41XqXhFSrKppYOvp0kAm5ujxxK7MjL/u5AUZkbK5L+iNnEDqlIguXnlGFc/VoLAIblcMr+gcEaXncd7HocIaOCZqO4wYIKfS2RqFCQg70sKsobESwCoqEGB5rnvPT2lVY8aTzlxzMwfVbq3FywUWU+XuCHIHsMDzlI9lqPS/bEu2GwyHYsvSsnnF2EYyOfI6LUVSBaeM9pM4Dh7zSHJmr8Ey0iz05GWmBhrN76ZFKWx3iZclu49PSbZMgVzRnhBWeAzLRdoEEhRp1ZhuA2ztp3LJoUWW9V++kcWeBhcpkgGq0vDgdnNNXLbuMTLlg5Z/A26bzf86jfehSWtEQ9LKuc10FC9QeVnQIsPnXVmMw4q/o8PyXss8F2u9YknXweXzkoM22MiiRqf9SNxezP/9N2Wby55uTMwkwUwB2v/81ivnXB0i48Y0RSRTjv8Uuyyzed/oDamk/gN9ayvFC/b6iFAcQ3Jglw92B5WWAA8vse1k2LumZ8RCL+RZ/FmPhMjLZUtaAEq2psQYUOqkcCa0lI803VrK6s26wsjiYhF3LFUFxzJcdMDk1xQJCjZ37PM3PkfAPKffmTVBVpGVsthCJ//8tCcO/akoUi5q/DAdx7kVFlf4RTKn305qiZ/P/rpdBypsdP4bSm6edsWjxnIyJP9n5a7yXpHeKsDdKC4XBO7XRkC7Pj70Hz7loPbWFmsCcnwzvey7eT8kGxwrUSmY471qqLvvOPqYZN9ykg4eG2zdhR4+pp5EH06vP0ijDKbF6o4bpLS6lSOpyKz2XNI/rshB0KyJ28RaWsjrpWtxixMjNuqdubQ676OfoGD1aBa8t7xFRPyUQaqugN88AD20liZoHyvrQsFe7dTtp8PLHR8iEKm+7tAXzlmDChFAQ2l51uByvX3g3SYwICjUAizqY6y2q7vBTns0rWJ5eiHYrkRaGEL0+E6vDrHZaXqcRmMbgXr0XgxlxsbkJbAxOPbjgqhuR/15pOAt5DdltiiBIUGi/1nmQ0UiqWCISAqbzAGoaH3UKK2Cbi9KpG0nc6zOERHNohJn8pvNs4EoI2rYCJllaDew6ZxztCHt8/0p5IDxiV9nIi1SsWPA5hLN3lDdRmxROQ21CfEN+O6GkdtLqPRH34QllZR6xbbJqLUIk6AN4m7OHt3omh3Bb09tfZDUvE/dmjczMJ4AI6gyTZYEgsaMLhT5dQgAUEj0MbTkxBgnXIcg9TLKiYsOCWX+jWm9QCxcuD1hNVkOEXoODhn7ezUrRslI5rHKEu/xczvNwrEP3uwVrYYU008znnA5Bm9a9l1PEOZgrX4MhWmx+yoXjb6QiwjnQojPrY4QSe6YgUK+Kvx9hoiBOsSrKfx4UfcG2+bxHYvze6X9dnPUa5dnfSUezwr6ZjTXK51pjbAuJJu/utvOSXDvCJA5dyojJUDdB/v0QrexS0EGZl1LVskmTLu/zpAJIHWDvOWjQNyMkIcuNryFALYbdbjlsosNRvgPbUmRjtoHjhLTj6aBppFGVLdxdVdsGPbjdvwCkTIygpzKy4Bz/y9gm4a/Z0tLkZ/OP3VIEZzOUEpa+lU8tVk/E74VP6yKUA6uK8Qlrkr8IdiXYFRpTRhE2yL2U3tUJSlhyprWbt8evjz83v5cwQvJPuDFug11BPrK8ksA6XO5BFLu2feTCQqSidR8A/hMWAIYm7itWgGeWhvI5upjYtbywtbvCj2Hdc7v5JYja/pF37xMkcek9lQ68YNYieJkIvh4crk01Or3K3yO6sFhQz6/X8RRyeu0wpixMBB5HcgOfAYtaZ3pDg5oN395ugSPRcv7xDZSZnueLlu6CmnONS6UDpJ3WeCS0IV0xbSDVV39xOpPF5NWEk5dbOU8Q3fwjf1zm8e2UYbzAtDY6JManxxkk4fkbdkF2pxZBTtQxqtHBKlWQlBm88WsMI9yVVmyZG2q+ownklsLQb31zlbWcakBtfYmqBFjh2S+H12hOW1GEchF3aIEZMDlnCf+EZM/8dJhMPcX2WlTEtxGYcR2rI9gtSFwG8Z1pMaMxWSSNPgynTm8TSsIuaAjNNmrJ+WIHRR6f5e/EErTegyJinlLdYki/02HLC6aZQPJ2U70Po4NDgI+s9Hi12mveFZHpMnuqCu1f2jUuPmq2Ggend+jb/nQi10r0vQ9B+K+nvUDJg0ty983LjEnZUhhi3CvbUvGBZKPpUmyS8nklS9ExsN0rtYWnqCSGRV+8Jhc84bZ9My/2x1miv4odUm94Bg5/UY0WLdOasAOl1Ea59LuXL+ajFIiTcBrYDATdAmRqEPkNCD/QUxUdmY/pj6LYsq+27rZ+8SBOXbDablWpHD1xxSoKfZSBXdvF7SWi0U4iuHnXvDyCb1mNTPFjwkgjtG6rwVarDt38I6cvdz7L2mvKgkjKUm+iJT7ufVCmHk1yMU3O6E+yfBPB5kMl3XltBvwopynprlYBGXmjCSo97GMEgfE8AlhXgi9n6KrFa/aiwQscgENJ9wOhn6W8m9Sp9RaRHHIsTkXVo2PBv+Rfeo6tZNtmfASjuTHAIHnF70e5eXrM7n5fqB8tMpnjS2yENLhaFACODEWEUDTkqz7TvSjuni2/gzpKOqdNVaOOr0LLEWmz/BViMOi4q4NRvX+VHhnxd86flxVvmZTT2MgAEDzbC+foyptPjqghs6ehDYDtRFerQK8TkwqIyUTV9S0eo9SC7YdbbVZV/L7F70tp08Ikm+GNWlefsQhMUMeatLggvdVBuN8+r2MUZpCiUnvbeNpKPH+F/wkAZhJXa8AEPwEEl4vP3hLPt+gd+u6EHulmi9eXOklEjAnoFergYDhbF624MLeFsGFWiG8Rm71Lf+oMETL6+mq0C0HUnjuiTX7sQTlJvoQoMlUYrhwwmvT5cslnA238L5/V92Bocl3a+wXfMhWBzrZpdxCMLDgIroTogMszfDfwXmFCXLuIrPAI84GfYYRkZgO5KPikpEwL+c90qiyaKfaMpBX1SMHBd+Cul5duzRu5c109XRHbzaQzsNdU0idrc2vf1TRzHRuc+mjKWItxAf8GnBat+AUbrJfoQqd1Tx+SaJjqKiCqX2zKG4Uo4PIf+mM12cL17RLpJPCGiQTudQo+oILnq0IjCcPIyIefwnmU8NvylDP66C69fhcZGesRhk4QbpuR2iAioUHKRHOMMLtZO3Xy8WIhCCE638u808tAveXxmep4roJzNxbLq44D1Qs1WgGCIQ5Y91AKZ8q7lRujbFy3IniKD9sP9ZR9bQ1CSK7Jqx9ZcWbHFCQGM/yLYK02ug45NHx8XSsTxvjkDJJpqfrVgSd8eqmg2g7U11N4m97VcgvjuLxtGEOgfHNH55vDZd5FwLU8Zx7BPbP6AdoEfw2sE7E/wqfVRi5zNfBCdxIiFtgWLUk/3ErupAfE+qLCTA4Fya8Iq6eKu8jZbWmblfWme48nGeQuNUGOpS+YrL0zlt7b+27UevJ8y3j7DbL1g8kDIz+7fdAphOMJ1I58fLdfLFKk6lwWuQaBXT4pGWVX8c7jLQv6RfjMmu1nEMIHIh8aYg77+cqq/HMfgfl3vaxy25Vnc6Oie22b+1xDwXamZ6h1dQos7N2BQ3puFqSTInIXXQMSq+nsMoG08541GKK32NaK+DgcJdeYMddaYd0ixDxOjc0fXphs0sLWRhIpzTDnkp4zreKK2hpiAPtwXfwO1D03Q7dhZWv9Kn280d5J70MhMsGKGFPGetVh59+f4UPLTT7IbgWCW86KHLBCLvRFE0H1/bN94tVyBg+skmYUg/Xd5D3C9oaL/BmnxJLJZ9CkKZPpkcYI+HJNQJCw4I30RVOT4bSA+CKC13u4GzccYEUeBr876PkyRHoae3YtRMHr7on7RHwAxeCZGEq0UFy3MyvBKhtayPZyFMG60XlepZFSq9t3pCv1oA0wYO8PkfNcjTZI3C+hQMhec+TI7GeI5r2ED9a40RrkWhYJH4FnLLEUXL01K2PX8MQezKDwJsfjip0iZiVgOVew2KbNt6uro2H58bQg4bm1F/EpPLEso6rrxaykd/fJsEN7er5Rb/AVP4JpfGP8zK8Eg3FqRluDKWZqtY1O6JY2uj6SWGdPY9zf9Ur6rGSqQX7vi93rQMYuklTFyAnpv62Mw3TYZ2BdTG7wFmdoXyBTpqqjH6Rc+3gq1dyyfU8PqwXXRgBmr4z0dX/iskl72zxk3EYRES/hYMp7RV7Lw+mcEXNncuRmhvjrLcaq5Cji40sFEsKE0X8q6ZNBI7VfLtmqKkVfoF1CAXOLop/QhkOXQyMHh6HEggi5YIBItEEVjM6loF2CI0tU4FbuOlMd5VxnK+MIRqQHGqrB+3HanbGIensDOodQ7ohGxQW9ClESL28O+ZV0LgfD+gjFjxtQv38Jxt0WVOoUvFls+81zaLFepteyBBoM3wSxfJCqxpxVaIzoPUKzi5JR30iZm9H5bpOe79iAVI36YPXHAMiXU5Xu0wb59Pyj7w5W9b4hmDAqJyzNk3N/ooVLhmy2fDTEvWHrIfQkwyIEuXw/3hMcGPnd5lLVu+CnOzGLunrxtL0sMzZKE4kbtXKZtKYLxf+Z6+pMKDwnrJ/isUcI0sOr5rR3/nfdeKgcTdwI0xdScgr7pLX32Od6ykkvssOW/+NrAjWzUo6z+AUANn0eeAPHO6QlvFeeodUhKWqfFjj8NXwE0XMyvN0mxuaZDfO0tYpbFjZLZy25Dmwz52Q2ywDrASF+kol+teDIiGDPv+wFdXkmcewfTRqAO+b8O/xNO3eztLTqfI5WH6MtOKqbgHfyGDjiP417/iwEFoa1jpPqYn5HV5fo/zMjTi5SAHi/7D/rpG0Bwrs46Rrf8ZhYIFHmG8OZhbMqmQfSTAlKlbTLba7uRb6imn2a9TmTNdxd/emDSmB9FEue9Cej4P7AqC4kDLxLYdHExnDaEVOjoHa81Cf1St71eQMggy/9LtpqgssjDb9hn4qFX5MyLfQuW91zy9F+OYAh6KxD2ulkZ2L043aabDmFRBh/BEmKAxerbPhCzOhYGBVhXms9l34ttn/szPyZ4UhWvMLrL9+BMAVYDZEjWrgrGS0ncE5rspPUW2RIMqk1zKIE0syGfXjlVyFL8hVKxy1Gn1VIIj2DK28UZXFgPLsKfR003V5ARojTo/pMOloKJDHDQE0/+yLF05i1wvRDoUp7QB0JHfMCRMYswO+5r1KkfPBMXnaSBzcBqF+w4ExeLX6ZWouowA6SVNKsZSWVLQlTmAsQPIicW63rkZr3t1LzJrBNESf49d4f+qJFNltj4yP8T2CtPzrQzzzPBPsBA28/WhQ/XT8iMDF2rgSuU+nlJXVAUV/NLMehNdLXuXW1iGtiGoK8C0da1JMg4ghvMUB7H12f8D9JTMDQvYZdIzN5OHg/zGxHUG6vS91m1rTFRn7VOPDFKemXO1PsWATxNASn/twFQWlG0RPQcav0nC7qOFMsu/ykbQTQVSKqxvdSBc6k3J5tRiIkrpcGlmiLyGKPNzaOgF7H8UxVb0Q5uRt2ShauFVpQX9qAB7hXGx+tScYO99xit+mz1DwN+TkK+I3M/rP9UlTynPt5IXqJWT4avBnaHQeYh1kzh7XsnG8emhFErFY5QILv+B9eT0WZyOQFk9XuzKcIzj/6RDXQKsKjVipO4gIueDtNN38qU6Woex9VIVx9yRYdErzCfCu5bZq2LtN1rR4FfGCBv0qUXd5uzkk0V3IkHujp8CUvMAhL5yNu69174WEU8BMJBOm1AFDBnTTGK7EWzOV/MuAFPVuWXRGbp06hPp/hBaq9sr4z9UCSqU2z/relhOzp5dYjUXhFDBYWdHBpEVJ8y+jLn/OpAIlmy295t3GrEyLisAu/gf/NEYMYXkHynuPIplfSKrUBI5Q3DNHphkzFrZ66ZckY7fqzybUSVvpgZ+o83C9ETaTQQm36TknQKqldC/5xFNJ8czb2Rauk2nywDWJ9qYyOR+EWBC4vlnm5A6kz/buorxF9+AV/o+RK8LpkuT1D0ZP1FvHDqgBzXEYY4vs30acHS+JPMoCjtB8Ua9W38YmwRxiMLEXRyflZJw10IdxtqbTOqJZQsWX4v7JF/dT6W3PPBh806sB5pJ9khuPp8aIWskspdfjBoCcV41X4J0lN10rjGK+p7HyszC2abFrFMN+4hqpTbIkXZLQ3OwO7WtgyLtbMg1nvM8Ioro/dmlgP5Z/FEK9k7NL+MN+Tl4kRst+9ZXuUuDKzugCIZcf+GMaNQlUrW8WZrTkEYI/M0DTGwtbmTgoVxfcFDfnfPokE06HP+akXfVuYMrDwgt7iIBVdXHK1+K4y6nqhJWGxC7PWAcuDxki4JAtprjrDqh0juQmgolkhJe1obDD35zJqLctqcEzlVHkdhy0DcQHwZTZ4XeVyJLJvs58SOIaB5IWc9wlAej8wMPemB2vmw3cwzytjSjlDUZlHTKrPpEiR0Id8/3XMvYpGR/v/XekV+LOmcpNMSqPvemDl4KTTp+dIuqme5Gvja1eCeQuMbfrmChViS5b6XONRfRXnc+9GMTCqKj4WLrvrRH0VfE+Lt1pB4el4w4V9Iopgi6MpfWtecjx/cgMM3UllkothFdKmQD02sGNzBTDIp9clfx7RFgZ00kXS5xDBPJByZZyfVbbVd1aFzonOuGg4ukT6YGXBZ1sjiS7w8+vrrnvkE8Vr8+7+1Dxtne5kFbI8QCmAHCrNA0NaHbmQhT4IkHxXT2WjpBvJREpM+SK7xJi6CXAplJUw7z432YYH31LKW3+khGdTFfGTu2guW4kp8dXa9GKHFOsaREmts86l4qGdA1Ux2EEXT/Bylg4hYVcoN5oKcoMGxm86M9LpTpwC1F9HOEs8SqiDKAxA+y2RUkLCUt1AANWzA9Yrw+s0LcJjb9i1B2Qp15jJIVVd0kjZXqWyt/KxS1XKb28yMB66z06sHQPfrw0J29GADVFYGty/+xRAa4cMMelJboj/bWIH4H3/lFExJnfQsHiJd3/2sg/LPr0ynzL4n1Bw2Wc52sQX4agmhmxhLxP8UB5CrD+7/pOMTQhGt1foaJV4dLuNALnMgB+ruULsFNeE1AHUX3ByYs3rMAJ202U9ARzv8mbkmXsO9KoPvDv5m1+tK6GnsRPmfgNRYOkAo+JY7KwsZkEHwz9d1pRNyfWd7m73Mvq1PEdPwFUz/op8P7Fy0Nw9O3+oV9Rysh0P1MYTGPvnu4UrzEyB/tgRLMDsyirqBEAOZz26qhVlfWNplWsXqd0+k6djRWCwPyCVmPWCB6kbeUcKQ+7L93PhlOSuvHumnL/iFUC7vSDimEnCguNL7TGJKqR6kKeVk/JKFbO1B7RgwWd8kWmgX+VJgwNhr06UUhHBOSwWY7u60n/Dd+OKnpWUhupgEEDPuXWmyfuN8t9jahg9uUnkyTTatXOudg9m68K44vmdq4UiE+sFehGtKOrQEsHtq3Q9pYJmUKC0Ve0NTlcdFCqR0TBK1pXJPmH8560Pq3auHXAka04YaOR01ZmKSJ7KMCxgKkxaJZ2uVml5FUOvM4Bsnj13FmHj1Dt5BuFRl5zAm4C6PLjTLojqWn/wJnIj5FYoBMxPHrUM+6jSDt/EZHi9DFTwtPjxd1guZmAkxyEQEhA7QWJZzRq9HLxNRltUZxuAYKV/zXxyIhSyLZ+HHG65t7kvp2QK0nzyU+HURPXuluXp8TuL7/PX6qlas3Gdr4IY8NtZGTq4hnxTi6xX3XVHhbi7sdQOxoGqZ7+rsqLpGO4FVLkMeu4iMahrbWQC7YiE+SaRwo76S2IfbHfPtxHv2qyNgh5I2piW2SpFE1wIM8Nc+BnHFTEkLgCjVkAFD+x9bNg4z44t6tWnoVXhte18NiMsYHqWxwG+HeIEz+n9TmOx8H7g1iwzFZtTfQKBuH3LSeMGm2DE/T0BDTivQgb0GscQYggp1yTxJUjBdtaJTcU60aqcOpEpTnfhHY0jVK5vg3kMpE73cFT8mjEU6IQrSmGuPcB56L/o+BBMezMoiFjOvG4bCqhtERu/lMiK6WdczP5v63AsYm0/9p8QKmXNcgx0n3R2qD/WN6xCFbIedFab3E2hEeFwQnoz1GW106uNhGhI/pUWID7tI+xPkCVIjZxenRoUDvjfK0E/M+cNIrJ1T9Wv6/Wt92Khtcu5YmYdRZDheL2gowOOrNhWOKed8Yb2hPPPs+yj9SJA6b/Grm3tbQ55MewykYlHb3fPxkHURGw4Tg31B9Zwb/bBCVVR8LVJdsqynIpVZhscm8u+CTFvbWm83XctoYa6pa2UHkoY9WV/jXbR+Fb4J3OBiO62H5GO39BskXC8Qt8bg2BNGvtLbQUUvkOchzGkdqH+9T/YExlydzom3+92mnsX8H8DAcyn3jzI1I8vXdV29EGI4cuCv4AGz9IrCA+y9XT6As8p1u4WND8vArk9CeiXKJFcRWdV0rEPLyhyyWyRUnkP6gMVabcZSObXY1rH9WYT9wOy2qKgD8IgCjoERmzC9wBwmJTzlGUuYVErBJGh/ql9egjt7oztpWo+E+yCs0YHzHHUKVL+GKKZek1TFdoIdoU1OdrE2Ul/+6DhknVIf0mSgOZvoz7JcupTZZcSGjHolThCTA82YCTlcRSPQIKO2kBhMzfpm0HHewi+IuM7j3tQDhd8fZWw/7BQUw1z9s88WoSBHo/ubo+pVHE6yiFufUcL4Tk0m62xnXgqQW3n4OXb121xFXdWsMjTjWXjtlxBygkz8H86lgRplkXcyTZxOf7Ivl9sNuN3DKTKu+gDJ0S9Kp6zU9qPdV8fLlpcjHdq9BL03isY0H5wFP6w2eri8468b9C5+9Rm4mTXQibypQN1zdWBTPx5WIppcIHXtcNgCXWZDX0mi2EGrG8BGTc2ia/WtX+cqX3Ix4TeIvOhfaTC2CVlZrJrO8QpormTNtTDkbHX5gm8zqDDocWr9TinhKwzX2u38SubDx3RHqXqxyQ2vZ5x7LQqzupFXJousx+KbPJ8IFBD6I3yc5Tbk0TTfUvogonu3a4saYIhJiH4+9bYIW1yNwpPG5+KDBczfSnf3UTDeNZYCbqGLfgAZvGVIniDP11VvAG8dea/UoLTAvR7ayvvF4+IWgViN0hBQLAGu7Enh6sH/AEWoJPipod1aucr2UKfXd1W1wwaq3gCMua9X14m6BmUYO3cEtkOSXxwj5qsaqfpv13VGy7WjkWbsRRMMR25taEALTOx4v0UBd7frh1zS5wXq385QVht6GhQRSQ9is2c0XGeMvxfrgHi5rIwsClAX1T2Fpu8w1In2FZCOoi8+247aFPp3ewY88An21buO4hecmw2jRzUm3bL3Ug2EeA80xkvVZXSyL/yDT7xvH4LC2arnvY+b/GpmCHUE0ELsq2v/6wjMyeZBdGzazzArFwbwqKOaGO4/WAIPh8tMnwZFVJzC32o8xNYY22Vj+RSU2sAnmffYYhQs4QzmYlpJWqNEsg3XDE3oMVagSJIGtIi/4MyKArVVXhHnWb9b1wu6a+LdLYZAm3WM+Us3a3L53qdrfxludWOxuk4+WYvPwIpO+vJJ+HxTIdTkFDhKA0rcYfTnix7iiJ0DqKF9Yl2CBFrzZ7kp8eBV0kkqZgCAwUpa4hxL4/2QAtjgGvnnkOrq8vqqBV8n9twNxf6uzmi0djOf6ywC09jTKrYBwrgNa3QrZ0YVFE8sXW+1LON1twBmCQCB4hFwJ/ab61lHtPunEn1LeHQxlW2yY3OWPUU3g+xcOWpNawZBgf1QteJL1Y7npCc6006BWDOB5oOhctW1rcZNk5gG7Y5r8tm0Rx/MdAgeOmGVBdUpwpwvS5Bg34TRPKQ4NpF7MEGe3QDxL1MztWL00gmRofyV7KVUk5xZh3zAYG8tnjOFzyvuSRiPUJI4Q3IzOv/vosHLcDF/YZw9A+AxdFRCQ2cXqTkdqjgQPcpExfviN0rsRaBBoQ84AnDD7XrjQTa1ukTmzaF0e17Z0krAmasGDEphEHjPKoOWn7+x8CbbMzpB/FPNS3W6vMlzZcakALe9/n3W0qT7zSwGPC0SneKkDJNRLlHiKMUO+uUuBDxd5F91JnfuN63I5DK1yl64y6H/5UgZgFDKNepQWxfeg02MQQ/y/RKiWakRs6vR5Zhz/RqWgqTfaHbYYz5UB+9DJ1YaFmEW+PcYropuq68SD1YNDuGeFx80nx/wQnUNUKSwc4tSuhdEBVQK6MaYnUmhsCA1RuRTj5iYL/2e75EsCOCibnjWkLF+UQX+ZN9ks7DuXisnx+J5iG7eAWN9Ry12oRwLFw7ByP4qnY4ItIX+/E1QG+dXGSeKjAEMGgA3+X7lriMUSXYMgt6/Gv1AZxo5E0D6TudGHK+GRa5nWixagR4zDo0ggL+/d4P/StTEdEeRncEplt15IQe46mAGtcQNuBFlDtDIw/Qapg7ihhM7umd3HCCHHnAgpRBa89I9+CrPQdGBhgVDaSyEjjNG3ESevUdR514N2NFOQmrfE1XzKsDsyAS42ByddZvBJV+X7Tw33Oh8PmiU8aPd9TaxnBChh5E2PbQLFzFoAp+qhbBqJYDrE6sBzFiq7YXQGPEwTtDXxdxgDyGLK6OlvFQI95ym7A3OPn4aZrEXTrGAMznNHtpDIU5McDCQfRfNIqJx82i2mTTyDQ49YfCYZsUW7Q9czYiStAGKDHgWtFukxVgLAi+46QIloILbstUU+APHidWyZwn5A16akklFdnyEWirom26kXHKzLRLFYOcHzN6Xs5y9w7R++Sz1Emy/Y9XeBOF5/JiSbrDVZaYEG+geAGLqGU4KvOplfCVF7kASg5j/ICMLcySEyxBZg1MmkT32s8O8RJD+8RaG8vrp1R/8BBMg0pq3pueYXrj/GNze5h7NdDISPVW3PoOd1oUlQaujWLLX+wJv4D95m6aKVEKR1VflWfQW19UwTu2pT9BaI97PLiZfh+y5a+PtdpqDbi2Fj6aQnWttD8dsOH4sjxGzTwS9HsIwkiBsSBrTYg7y/4UGpx+q1HmhbgkSKAVZfbxiArb4SO+W+ItjUMWu4RuTvNuUlit94x/g1Qv+/2zVpB6TbeNiUdfbZgpJbgpwVm5YnknfevSECaVdYzazt9AlSZMAl1W3tU5T4tZR51PnSp7BydJ/9Jtuii0xfmnsGD+BjY72NezqqhJq7vQwcbZS6AFUOCt4ZiBd3AZbcnSiFPV2w+QiQtkuLM8gxt7iBT28W/gm9hTTQWLTun5krKfzszB368GqciLsH+7sbyP+nDh8JBULzIpcmksxPmyPks9oUYeTo/J3ZGT5RWndGPmNNP1UeEyGy3bw8xxbPNhh42Tg69Rfoa4NeVrW+pQo5mWhy3C2AH2um51vm1UbioJFWW5Qxrx4z5OVNzhd6IDW7YDPQmmSuybRqCm2fe81ZE4EL+vXhcEH10VvDJvHVYQ6qCFaQtbuRJWKBXGOoVGjgRlmX6QR35t37UJXeZthGjwNUlvnocCyeDgS2Sfbf3HEW4gKUm3kOx2fVTHycrri3jb5oWGEpARNTGuALYsMm16I/mg0N+qi/Mwl/4xKm7xlaRugkSYSTfKgg45Ca/vAvxbBuJ+gFjLIuRnajbT1V5zGSCaBhS8RJwvUG4JpPiQ/Pccw16Ge66T/zW7qeB9xyYtpqlFT5NYBIo4glKm2FgyWwvUKHqh74QKu6AbT8yNPexENo5VlEeCy2Q6sI6Hmp2ua9Hl1J0y5dZ2menHzFlN4XU6+mNVkRnb3zw8SQlA6ALfG6MQRYgWZRQ/nc2g5xKy+9e/wcbWLKJ6b3l7ypxyh7OfGDw/Sqxap69d8Bjhvz4L24lSe85xH0UgJufXIYhgkue86tBRNzBxVHWP9neh03XqdCrypsZrhKtdHiMAZ2JAZx+rnS5P64wQfyrdLpzBiwv7DSmdyHAaIIQWlMVNOWnIIo5Aa06wS/QQVehOfrUnJA9C2TJbHlyZo8rnKDBYiPIjRsxTAOpixzXbR8T4BX/BBCxMjkw6eeV6zUyQbMtCfPU5zkr+uMygfmGBcmhds7dJ7XJtvmVQrMgab4WaA/vM6eYVL/Uc/9g1XcjNWxQ72f4I+RrA64nCONRzG5mV7FQMWoW+vZcvovjwIhKvcJsTEmyecT0RqOjYpqVy/oSThXRMbBACBXq3AvTNHAhkpt1CuqFiFzlCwOuVtE46Zuq4JTtzVOfxu7CyLoGcTAr8X1++tTmBI5d5Re+fMJIJahp7P2aJvMK7z2nQx9M0fZrvlxQQbsfXDPGM2m1XgYSLUc6f6KBtKC8BFwP5P1e5EuMcTmOTukfP8D1j1GRpE9h5nZyse58vQl7IEC2F+qUs9iMCbMbOw2WQhXg4lt4M+pwZNWOrhzqm3cJ9NVH6lfMXTLEdJoFA65jDPjMAUYIT3SaP/e1aSa52BJ+8Y1EAphzwfFpN0oIN1hxz1630mn3WKsd6plob3SC5xBag5+owwIhEWRk2ePEXPdEDMuLaVzeamdR4arQBsokUPfMQirhmyU3+qLQ0NI4p5mnlHpOcmzsAYVM3szMAgDL7rzgUu5zd1MV4tDDpRzm7GAMRhxiWARhIr36Bt4t4NZ9xs2KN2M/DvNoEnRarTWyv7OFLFLYerLsgvpLAKH5F/zpPwdJaraXkdfkhccFbRjNKwb20YVRVEvF81Cr6U7RMkfi14l5wusaA4evIwyIYoCF8FvObLrGjKPAcTRpgRdezLdK+jHPvgyZalnNV97JkQ/3tlGYvqVZoWteFsgpajr6nWgIv22AtngSG7EGwagDvX0Z9Wnj5TrUnqPVOPFrVrKuNskLbljycIZLUfh9O4KPMH6mIkKBdg2dOUytXthOsyiR9UjXNlIjlgFTn7Dunqy73OcYW/3p8ezpZCKaPw3xWzaOHgDzpNicJsSWWdmN9VbXkg2Eh8dTXZ1nNizxl0TWOxgRsC3ZiWyUhbddu5kneCTXt9m/g2JifDY+Q3ytm6wzRHVF/sYKiIsDdZ37/CyPNbKl2sgEZ5/3ec6wUiKkDkpgDrWYJtweFtPBIVUE4GUQqp5Q5SLgL+MGEUzLv9tobJ2FTgq+AG24O2+7zbpkoinp/zsTPdT0p95QVDArZX0iXhzCbARIOJJ6koEYMbtKiG/pgkZPi1yChdfa5Irs64YzS9E9XvrqNKFeR1WTVEybhAZOC8hxx/91LfiE3XR8YNZOFG3L/cGsLegrCRBVcyFVV/mPJ3+A2SzZC3LzeM8CSCrkpBPA86zh5SC8yRK9qRzAplVKlTBdUKUK8tFL6Xz2kxYXX8usP4WpB+zDAzYcWRFPLxAQkDUgkiXYDhFEgnanZE/unNgJGHpwDAp8iMAYYXH9Da6Dctoa6fw8epKNI9pNm26e4d7wVqUcUN66oQOzGuBUEsS0FDGu04hTICwPvOgCuU70pqnyJzmWFkKDDzskzRbDd6nfUvJ72T7Hz89muODILavrQVTM/iAKkvY6F3mO67rVHEDauf8mcNLTSt0EuFHkzDMlx2fuwBu0Ut7JLWrXaMIR9P/oBd58qdY+FXI9qWHFFC36e9lWQd0ATUdHXo+KXCxipk1rp2kK7vWoVQa6Fm3cQtohL0VSVFRWS+gwbdfwSYANNbYxL8Ec6qga7UNjedP7014Zd0Q==\"}" -} + "Initial version": "{\"iv\":\"3NjWzgOw51ARgo3i\",\"encryptedData\":\"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\"}", + "Updated via schema editor on 2025-06-02 11:27": "{\"iv\":\"IpgB+AyeLhPm34ab\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/likes.js b/backend/src/db/api/likes.js new file mode 100644 index 0000000..9c94362 --- /dev/null +++ b/backend/src/db/api/likes.js @@ -0,0 +1,246 @@ +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 LikesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.create( + { + id: data.id || undefined, + + amount: data.amount || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + return likes; + } + + 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 likesData = data.map((item, index) => ({ + id: item.id || undefined, + + amount: item.amount || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const likes = await db.likes.bulkCreate(likesData, { transaction }); + + // For each item created, replace relation files + + return likes; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.amount !== undefined) updatePayload.amount = data.amount; + + updatePayload.updatedById = currentUser.id; + + await likes.update(updatePayload, { transaction }); + + return likes; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of likes) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of likes) { + await record.destroy({ transaction }); + } + }); + + return likes; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findByPk(id, options); + + await likes.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await likes.destroy({ + transaction, + }); + + return likes; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const likes = await db.likes.findOne({ where }, { transaction }); + + if (!likes) { + return likes; + } + + const output = likes.get({ plain: true }); + + 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.amount) { + where = { + ...where, + [Op.and]: Utils.ilike('likes', 'amount', filter.amount), + }; + } + + 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.likes.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('likes', 'id', query), + ], + }; + } + + const records = await db.likes.findAll({ + attributes: ['id', 'id'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['id', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.id, + })); + } +}; diff --git a/backend/src/db/migrations/1748863668173.js b/backend/src/db/migrations/1748863668173.js new file mode 100644 index 0000000..09320c1 --- /dev/null +++ b/backend/src/db/migrations/1748863668173.js @@ -0,0 +1,83 @@ +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( + 'likes', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'likes', + 'amount', + { + 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('likes', 'amount', { transaction }); + + await queryInterface.dropTable('likes', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/likes.js b/backend/src/db/models/likes.js new file mode 100644 index 0000000..c5e8621 --- /dev/null +++ b/backend/src/db/models/likes.js @@ -0,0 +1,49 @@ +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 likes = sequelize.define( + 'likes', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + amount: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + likes.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.likes.belongsTo(db.users, { + as: 'createdBy', + }); + + db.likes.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return likes; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 6e45d21..de7a5d1 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -103,6 +103,7 @@ module.exports = { 'students', 'roles', 'permissions', + 'likes', , ]; await queryInterface.bulkInsert( @@ -957,6 +958,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_LIKES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_LIKES'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 39b17f4..cd7e018 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -13,6 +13,8 @@ const Instructors = db.instructors; const Students = db.students; +const Likes = db.likes; + const AnalyticsData = [ { // type code here for "relation_one" field @@ -53,16 +55,6 @@ const AnalyticsData = [ instructor_performance: 89, }, - - { - // type code here for "relation_one" field - - engagement_score: 88, - - completion_rate: 0.8, - - instructor_performance: 92, - }, ]; const CoursesData = [ @@ -105,16 +97,6 @@ const CoursesData = [ // type code here for "relation_many" field }, - - { - title: 'Digital Marketing Strategies', - - description: 'Master the art of digital marketing.', - - // type code here for "relation_many" field - - // type code here for "relation_many" field - }, ]; const DiscussionBoardsData = [ @@ -149,14 +131,6 @@ const DiscussionBoardsData = [ // type code here for "relation_many" field }, - - { - // type code here for "relation_one" field - - topic: 'SEO Techniques', - - // type code here for "relation_many" field - }, ]; const EnrollmentsData = [ @@ -168,6 +142,14 @@ const EnrollmentsData = [ payment_status: 'paid', }, + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + payment_status: 'pending', + }, + { // type code here for "relation_one" field @@ -183,22 +165,6 @@ const EnrollmentsData = [ payment_status: 'overdue', }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'pending', - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'pending', - }, ]; const InstructorsData = [ @@ -233,14 +199,6 @@ const InstructorsData = [ // type code here for "relation_many" field }, - - { - first_name: 'Jim', - - last_name: 'Halpert', - - // type code here for "relation_many" field - }, ]; const StudentsData = [ @@ -275,13 +233,23 @@ const StudentsData = [ // type code here for "relation_many" field }, +]; + +const LikesData = [ + { + amount: 'John von Neumann', + }, { - first_name: 'Eva', + amount: 'Murray Gell-Mann', + }, - last_name: 'Davis', + { + amount: 'Max von Laue', + }, - // type code here for "relation_many" field + { + amount: 'Isaac Newton', }, ]; @@ -331,17 +299,6 @@ async function associateAnalyticWithCourse() { if (Analytic3?.setCourse) { await Analytic3.setCourse(relatedCourse3); } - - const relatedCourse4 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Analytic4 = await Analytics.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Analytic4?.setCourse) { - await Analytic4.setCourse(relatedCourse4); - } } // Similar logic for "relation_many" @@ -392,17 +349,6 @@ async function associateDiscussionBoardWithCourse() { if (DiscussionBoard3?.setCourse) { await DiscussionBoard3.setCourse(relatedCourse3); } - - const relatedCourse4 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const DiscussionBoard4 = await DiscussionBoards.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (DiscussionBoard4?.setCourse) { - await DiscussionBoard4.setCourse(relatedCourse4); - } } // Similar logic for "relation_many" @@ -451,17 +397,6 @@ async function associateEnrollmentWithStudent() { if (Enrollment3?.setStudent) { await Enrollment3.setStudent(relatedStudent3); } - - const relatedStudent4 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Enrollment4 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Enrollment4?.setStudent) { - await Enrollment4.setStudent(relatedStudent4); - } } async function associateEnrollmentWithCourse() { @@ -508,17 +443,6 @@ async function associateEnrollmentWithCourse() { if (Enrollment3?.setCourse) { await Enrollment3.setCourse(relatedCourse3); } - - const relatedCourse4 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Enrollment4 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Enrollment4?.setCourse) { - await Enrollment4.setCourse(relatedCourse4); - } } // Similar logic for "relation_many" @@ -539,6 +463,8 @@ module.exports = { await Students.bulkCreate(StudentsData); + await Likes.bulkCreate(LikesData); + await Promise.all([ // Similar logic for "relation_many" @@ -574,5 +500,7 @@ module.exports = { await queryInterface.bulkDelete('instructors', null, {}); await queryInterface.bulkDelete('students', null, {}); + + await queryInterface.bulkDelete('likes', null, {}); }, }; diff --git a/backend/src/db/seeders/20250602112748.js b/backend/src/db/seeders/20250602112748.js new file mode 100644 index 0000000..ddd1a6e --- /dev/null +++ b/backend/src/db/seeders/20250602112748.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 = ['likes']; + + 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 d1d526e..b2e47b3 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -37,6 +37,8 @@ const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const likesRoutes = require('./routes/likes'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -156,6 +158,12 @@ app.use( permissionsRoutes, ); +app.use( + '/api/likes', + passport.authenticate('jwt', { session: false }), + likesRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/likes.js b/backend/src/routes/likes.js new file mode 100644 index 0000000..f6033ff --- /dev/null +++ b/backend/src/routes/likes.js @@ -0,0 +1,433 @@ +const express = require('express'); + +const LikesService = require('../services/likes'); +const LikesDBApi = require('../db/api/likes'); +const wrapAsync = require('../helpers').wrapAsync; + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('likes')); + +/** + * @swagger + * components: + * schemas: + * Likes: + * type: object + * properties: + + * amount: + * type: string + * default: amount + + */ + +/** + * @swagger + * tags: + * name: Likes + * description: The Likes managing API + */ + +/** + * @swagger + * /api/likes: + * post: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 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 LikesService.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: [Likes] + * 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/Likes" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 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 LikesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Likes" + * 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 LikesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * 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 LikesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await LikesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Get all likes + * description: Get all likes + * responses: + * 200: + * description: Likes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 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 LikesDBApi.findAll(req.query, { currentUser }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'amount']; + 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/likes/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Count all likes + * description: Count all likes + * responses: + * 200: + * description: Likes count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 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 LikesDBApi.findAll(req.query, null, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/likes/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * summary: Find all likes that match search criteria + * description: Find all likes that match search criteria + * responses: + * 200: + * description: Likes list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Likes" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Data not found + * 500: + * description: Some server error + */ +router.get('/autocomplete', async (req, res) => { + const payload = await LikesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/likes/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Likes] + * 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/Likes" + * 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 LikesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/likes.js b/backend/src/services/likes.js new file mode 100644 index 0000000..d8f6f3e --- /dev/null +++ b/backend/src/services/likes.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const LikesDBApi = require('../db/api/likes'); +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 LikesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await LikesDBApi.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 LikesDBApi.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 likes = await LikesDBApi.findBy({ id }, { transaction }); + + if (!likes) { + throw new ValidationError('likesNotFound'); + } + + const updatedLikes = await LikesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedLikes; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await LikesDBApi.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 LikesDBApi.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 1f5a9f5..ed6ccb9 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -50,6 +50,8 @@ module.exports = class SearchService { instructors: ['first_name', 'last_name'], students: ['first_name', 'last_name'], + + likes: ['amount'], }; const columnsInt = { analytics: [ diff --git a/frontend/src/components/Likes/CardLikes.tsx b/frontend/src/components/Likes/CardLikes.tsx new file mode 100644 index 0000000..c068030 --- /dev/null +++ b/frontend/src/components/Likes/CardLikes.tsx @@ -0,0 +1,109 @@ +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 = { + likes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardLikes = ({ + likes, + 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_LIKES'); + + return ( +
+ {loading && } +
    + {!loading && + likes.map((item, index) => ( +
  • +
    + + {item.id} + + +
    + +
    +
    +
    +
    +
    + Amount +
    +
    +
    + {item.amount} +
    +
    +
    +
    +
  • + ))} + {!loading && likes.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardLikes; diff --git a/frontend/src/components/Likes/ListLikes.tsx b/frontend/src/components/Likes/ListLikes.tsx new file mode 100644 index 0000000..90db223 --- /dev/null +++ b/frontend/src/components/Likes/ListLikes.tsx @@ -0,0 +1,87 @@ +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 = { + likes: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListLikes = ({ + likes, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_LIKES'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + likes.map((item) => ( +
+ +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Amount

+

{item.amount}

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

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListLikes; diff --git a/frontend/src/components/Likes/TableLikes.tsx b/frontend/src/components/Likes/TableLikes.tsx new file mode 100644 index 0000000..e544926 --- /dev/null +++ b/frontend/src/components/Likes/TableLikes.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/likes/likesSlice'; +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 './configureLikesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleLikes = ({ + 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 { + likes, + loading, + count, + notify: likesNotify, + refetch, + } = useAppSelector((state) => state.likes); + 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 (likesNotify.showNotification) { + notify(likesNotify.typeNotification, likesNotify.textNotification); + } + }, [likesNotify.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, `likes`, 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={likes ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleLikes; diff --git a/frontend/src/components/Likes/configureLikesCols.tsx b/frontend/src/components/Likes/configureLikesCols.tsx new file mode 100644 index 0000000..acc5007 --- /dev/null +++ b/frontend/src/components/Likes/configureLikesCols.tsx @@ -0,0 +1,74 @@ +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_LIKES'); + + return [ + { + field: 'amount', + headerName: 'Amount', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
+ +
, + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WebPageComponents/Header.tsx b/frontend/src/components/WebPageComponents/Header.tsx index 49e1347..6fe6a0f 100644 --- a/frontend/src/components/WebPageComponents/Header.tsx +++ b/frontend/src/components/WebPageComponents/Header.tsx @@ -17,7 +17,7 @@ export default function WebSiteHeader({ projectName }: WebSiteHeaderProps) { const websiteHeder = useAppSelector((state) => state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_LEFT; + const style = HeaderStyle.PAGES_RIGHT; const design = HeaderDesigns.DEFAULT_DESIGN; return ( diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 4a04dc3..fa9b4f0 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -98,6 +98,14 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, permissions: 'READ_PERMISSIONS', }, + { + href: '/likes/likes-list', + label: 'Likes', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_LIKES', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 7eee180..784a953 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -38,6 +38,7 @@ const Dashboard = () => { const [students, setStudents] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [likes, setLikes] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -58,6 +59,7 @@ const Dashboard = () => { 'students', 'roles', 'permissions', + 'likes', ]; const fns = [ setUsers, @@ -69,6 +71,7 @@ const Dashboard = () => { setStudents, setRoles, setPermissions, + setLikes, ]; const requests = entities.map((entity, index) => { @@ -496,6 +499,38 @@ const Dashboard = () => { )} + + {hasPermission(currentUser, 'READ_LIKES') && ( + +
+
+
+
+ Likes +
+
+ {likes} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/likes/[likesId].tsx b/frontend/src/pages/likes/[likesId].tsx new file mode 100644 index 0000000..5c542f6 --- /dev/null +++ b/frontend/src/pages/likes/[likesId].tsx @@ -0,0 +1,124 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/likes/likesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditLikes = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + amount: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { likes } = useAppSelector((state) => state.likes); + + const { likesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: likesId })); + }, [likesId]); + + useEffect(() => { + if (typeof likes === 'object') { + setInitialValues(likes); + } + }, [likes]); + + useEffect(() => { + if (typeof likes === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = likes[el])); + + setInitialValues(newInitialVal); + } + }, [likes]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: likesId, data })); + await router.push('/likes/likes-list'); + }; + + return ( + <> + + {getPageTitle('Edit likes')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +EditLikes.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLikes; diff --git a/frontend/src/pages/likes/likes-edit.tsx b/frontend/src/pages/likes/likes-edit.tsx new file mode 100644 index 0000000..e5626d0 --- /dev/null +++ b/frontend/src/pages/likes/likes-edit.tsx @@ -0,0 +1,122 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/likes/likesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +const EditLikesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + amount: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { likes } = useAppSelector((state) => state.likes); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof likes === 'object') { + setInitialValues(likes); + } + }, [likes]); + + useEffect(() => { + if (typeof likes === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = likes[el])); + setInitialValues(newInitialVal); + } + }, [likes]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/likes/likes-list'); + }; + + return ( + <> + + {getPageTitle('Edit likes')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +EditLikesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditLikesPage; diff --git a/frontend/src/pages/likes/likes-list.tsx b/frontend/src/pages/likes/likes-list.tsx new file mode 100644 index 0000000..06fcca2 --- /dev/null +++ b/frontend/src/pages/likes/likes-list.tsx @@ -0,0 +1,160 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableLikes from '../../components/Likes/TableLikes'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/likes/likesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LikesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Amount', title: 'amount' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LIKES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLikesCSV = async () => { + const response = await axios({ + url: '/likes?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'likesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Likes')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +LikesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesTablesPage; diff --git a/frontend/src/pages/likes/likes-new.tsx b/frontend/src/pages/likes/likes-new.tsx new file mode 100644 index 0000000..21d974e --- /dev/null +++ b/frontend/src/pages/likes/likes-new.tsx @@ -0,0 +1,98 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/likes/likesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + amount: '', +}; + +const LikesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/likes/likes-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + router.push('/likes/likes-list')} + /> + + +
+
+
+ + ); +}; + +LikesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default LikesNew; diff --git a/frontend/src/pages/likes/likes-table.tsx b/frontend/src/pages/likes/likes-table.tsx new file mode 100644 index 0000000..c7869e6 --- /dev/null +++ b/frontend/src/pages/likes/likes-table.tsx @@ -0,0 +1,159 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableLikes from '../../components/Likes/TableLikes'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/likes/likesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const LikesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([{ label: 'Amount', title: 'amount' }]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_LIKES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getLikesCSV = async () => { + const response = await axios({ + url: '/likes?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'likesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Likes')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +LikesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesTablesPage; diff --git a/frontend/src/pages/likes/likes-view.tsx b/frontend/src/pages/likes/likes-view.tsx new file mode 100644 index 0000000..9996c4f --- /dev/null +++ b/frontend/src/pages/likes/likes-view.tsx @@ -0,0 +1,81 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/likes/likesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +const LikesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { likes } = useAppSelector((state) => state.likes); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View likes')} + + + + + + +
+

Amount

+

{likes?.amount}

+
+ + + + router.push('/likes/likes-list')} + /> +
+
+ + ); +}; + +LikesView.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default LikesView; diff --git a/frontend/src/pages/web_pages/services.tsx b/frontend/src/pages/web_pages/services.tsx index e4e2168..ddbeb08 100644 --- a/frontend/src/pages/web_pages/services.tsx +++ b/frontend/src/pages/web_pages/services.tsx @@ -110,7 +110,7 @@ export default function WebSite() { { + const { id, query } = data; + const result = await axios.get(`likes${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'likes/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('likes/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'likes/deleteLikes', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`likes/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'likes/createLikes', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('likes', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'likes/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('likes/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'likes/updateLikes', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`likes/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const likesSlice = createSlice({ + name: 'likes', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.likes = action.payload.rows; + state.count = action.payload.count; + } else { + state.likes = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Likes has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Likes'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Likes'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Likes'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Likes has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = likesSlice.actions; + +export default likesSlice.reducer; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index aaf78cc..3a71946 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -13,6 +13,7 @@ import instructorsSlice from './instructors/instructorsSlice'; import studentsSlice from './students/studentsSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; +import likesSlice from './likes/likesSlice'; export const store = configureStore({ reducer: { @@ -30,6 +31,7 @@ export const store = configureStore({ students: studentsSlice, roles: rolesSlice, permissions: permissionsSlice, + likes: likesSlice, }, });