Compare commits
284 Commits
cc0b6a2547
...
7c2abeece4
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c2abeece4 | |||
| 7716ed1243 | |||
| 0f52820601 | |||
| 4652c4acc0 | |||
| 523c987722 | |||
| 55012c9b2f | |||
| 7f7db12431 | |||
| 0091863805 | |||
| ce20535624 | |||
| e07b829237 | |||
| 2808ff0f5d | |||
| 660db82197 | |||
| e9aa6b82d2 | |||
| 9daecb947e | |||
| 93eeb0c1a6 | |||
| a67e7cdef1 | |||
| c2bc4ae35e | |||
| a498707feb | |||
| 54df782eb4 | |||
| 946f3fd651 | |||
| 6215eccc37 | |||
| 5150c035be | |||
| 728fa8f5bb | |||
| 939eebe0fe | |||
| 24c17b9dcf | |||
| c5910ad1ae | |||
| 9cb9d93622 | |||
| 1a890844ef | |||
| 8ecacbe622 | |||
| 6efa4123cf | |||
| 0353fe8435 | |||
| f7eeefa226 | |||
| 6561d9f09c | |||
| b6ad281a10 | |||
| 80737c21a5 | |||
| c6315ea181 | |||
| fcc33fc458 | |||
| c712347781 | |||
| a4c3d774ff | |||
| f027c77a1e | |||
| 100df1e4aa | |||
| bd3aeb92f1 | |||
| e96d55a7e1 | |||
| 0e7d7675d3 | |||
| 13dbd0a6f5 | |||
| a6c213b083 | |||
| f492c0be1c | |||
| 23058d3a2c | |||
| 9cdf45de5e | |||
| 508ae60c18 | |||
| 167ddda2d4 | |||
| d286e0d827 | |||
| 4450f17e8d | |||
| f9332777dd | |||
| 83adb86988 | |||
| 3edc78ec2a | |||
| 598839407c | |||
| 40fd20a0cf | |||
| 8ef544cd0f | |||
| c67440813e | |||
| 47ed9325ee | |||
| d93b6a78e0 | |||
| 5d03060639 | |||
| 8047d72a35 | |||
| 977aa18d38 | |||
| cc4c3ea67e | |||
| 81eb9d8f44 | |||
| 560a72944b | |||
| 6534d160e5 | |||
| cac5fd6866 | |||
| b21d5b6683 | |||
| 6e9dafebe0 | |||
| 09f2ae9213 | |||
| 88681d1849 | |||
| 921e72da13 | |||
| 577caa201b | |||
| 64de368ca5 | |||
| a6739de9a4 | |||
| 5dc0a54ba2 | |||
| e3206291bb | |||
| 6d37b57398 | |||
| fe9f9ee19b | |||
| 7f0fd6312a | |||
| 97b2ff4ff9 | |||
| d890f635e2 | |||
| c8ee881b2c | |||
| 0442538bd5 | |||
| f167e24c4f | |||
| d9a4e3249d | |||
| bdab064b38 | |||
| c36e634b72 | |||
| 8218bec088 | |||
| 45ff766752 | |||
| 47c9fad545 | |||
| e2ccfc8aed | |||
| 5cd4b71f2c | |||
| 2d240c8950 | |||
| 3438c187b1 | |||
| 28bfdb22b1 | |||
| c2d658991c | |||
| 2536fd210a | |||
| f23cc80add | |||
| 49296478ea | |||
| 2d72efbece | |||
| 6c7e76cded | |||
| dc29f71549 | |||
| 7f39ebcf97 | |||
| 09df9a3a1d | |||
| 2fea68c37c | |||
| b3355809e6 | |||
| 28f4df254d | |||
| 14f6a5ff64 | |||
| 4e23e74892 | |||
| 1f9b0c1bb0 | |||
| 6af22284b6 | |||
| b2c61c7a4c | |||
| 19ee2cc038 | |||
| 4adfb2f9de | |||
| 33e6173e40 | |||
| b44f07a9de | |||
| 44c6b14d7f | |||
| 14e78cb255 | |||
| 8cab8aa078 | |||
| 6de067272c | |||
| 4ace9346ee | |||
| 741c51aff4 | |||
| 531f7b1d37 | |||
| eddff39719 | |||
| 8d1d4ac819 | |||
| 69dd0fd514 | |||
| f44195bf99 | |||
| b038aa664f | |||
| f36a690e04 | |||
| 5849460ca5 | |||
| c4e0c96268 | |||
| 7f8f8072d2 | |||
| 2fe93a3a09 | |||
| 207098910f | |||
| a406c149bd | |||
| 239835cc36 | |||
| 47d18c8d0c | |||
| 601cb245db | |||
| ec9e6c1d4b | |||
| c1e0d9d81d | |||
| 160a6a0e75 | |||
| 04d6f21754 | |||
| a8928020e7 | |||
| 67f35bba60 | |||
| 3b25dfc20f | |||
| 81e1da2bbc | |||
| 7d167f0463 | |||
| 900043a7af | |||
| 3233e73485 | |||
| 162720cba1 | |||
| 0341b2e01a | |||
| 5ee4765099 | |||
| ab322c92e9 | |||
| 39a190116d | |||
| 5ea220e863 | |||
| 045878523f | |||
| 3cc29760f9 | |||
| 3f4eec9229 | |||
| 1365fa467a | |||
| b1398fee2d | |||
| ca5aa30e92 | |||
| e72ecc3265 | |||
| ab13b06738 | |||
| 4cf26ce551 | |||
| 3e1f5f0076 | |||
| b5674622aa | |||
| 3ded39f6fc | |||
| d8695d62b8 | |||
| 33f7d64e7e | |||
| d3983edf11 | |||
| a84391b583 | |||
| 9f83183d2c | |||
| 8652007906 | |||
| 29c18807d8 | |||
| eb00b4e7b8 | |||
| f16bee5927 | |||
| 61082619fe | |||
| 79126ebbd7 | |||
| d87f04b76b | |||
| da476ea4d7 | |||
| 0266c0c8b3 | |||
| 5863c346d0 | |||
| b0087f2dfb | |||
| 97340b4ae8 | |||
| 7dcc5e1d29 | |||
| 240d830526 | |||
| c1e7ed9034 | |||
| 8324f9c501 | |||
| d715a61255 | |||
| eaee7f601e | |||
| 042776d491 | |||
| 900f6b8af8 | |||
| 86ada0da5d | |||
| 3d59629f7a | |||
| dffc00382a | |||
| 2dd9fc3469 | |||
| cfd0cb37f5 | |||
| d14557ac49 | |||
| e188949ac0 | |||
| d2bddb62bc | |||
| b382f6bb85 | |||
| 16ccb39b36 | |||
| 60cc8a2401 | |||
| 11715864a4 | |||
| a79febee92 | |||
| 554188e511 | |||
| a628bdee49 | |||
| 873a2aae95 | |||
| 1fe01ff893 | |||
| ceb0f11128 | |||
| bf3562c9e0 | |||
| 7e5bf55e66 | |||
| 1251d61352 | |||
| f2a58b5dcd | |||
| d74f4aee61 | |||
| 0788b6462c | |||
| 26ee9e5d72 | |||
| 4c12a3c56b | |||
| de767c90b9 | |||
| be8066de17 | |||
| 7262c66c90 | |||
| 73ff552104 | |||
| 98920a485f | |||
| 4a2d95f64f | |||
| c55b6f724e | |||
| 2ad40c4ba0 | |||
| 5cb64ad015 | |||
| a60179eb0e | |||
| 2956777cf5 | |||
| 65842c7f30 | |||
| b7b759f5e9 | |||
| 10594732ba | |||
| 36ee8bf5b7 | |||
| 3dbbe50b83 | |||
| 35d7bd493e | |||
| e4ae43f182 | |||
| cef5d3c416 | |||
| 34efa48c3e | |||
| cba850251f | |||
| 9f25f88695 | |||
| 2043afadbb | |||
| 74503d1eaa | |||
| c3a173128e | |||
| 6005955884 | |||
| 3aef093883 | |||
| fb9ec24986 | |||
| 49899defa9 | |||
| 549d12b392 | |||
| eff5ee9609 | |||
| 65f6f80303 | |||
| 93053d5077 | |||
| 4d53f8a122 | |||
| 9a3914660f | |||
| 961b4b07c4 | |||
| 0f6c68b1fc | |||
| b528e6fdbb | |||
| 2018174ed5 | |||
| 935deeca2c | |||
| 8ac79020d6 | |||
| c1972c7930 | |||
| f686003f19 | |||
| 42a494a15b | |||
| faa668550c | |||
| cb797074cb | |||
| ede195ce57 | |||
| 738dee1f0c | |||
| 7f0ea4f04c | |||
| 49d27e5849 | |||
| 03b9dfc60a | |||
| 433f8384a0 | |||
| 2c0b6356b6 | |||
| c6c984c34c | |||
| c84fb50087 | |||
| 79ce616781 | |||
| e4a1817d7f | |||
| 2a69c89065 | |||
| 0899835fab | |||
| cf5848491b | |||
| 33748e3233 | |||
| 574dd8b16b |
@@ -7,13 +7,14 @@ steps:
|
||||
- name: backend_check
|
||||
image: rust
|
||||
commands:
|
||||
- apt update && apt install -y cmake
|
||||
- rustup component add clippy
|
||||
- cd geneit_backend
|
||||
- cargo clippy -- -D warnings
|
||||
- cargo test
|
||||
|
||||
- name: app_deploy
|
||||
image: node:18
|
||||
image: node:22
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: AWS_ACCESS_KEY_ID
|
||||
|
||||
@@ -19,7 +19,7 @@ docker-compose up
|
||||
3. Install Diesel CLI:
|
||||
|
||||
```bash
|
||||
sudo apt install libpq5 libpq-dev
|
||||
sudo apt install libpq5 libpq-dev pkg-config libssl-dev cmake
|
||||
cargo install diesel_cli --no-default-features --features postgres
|
||||
```
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
REACT_APP_BACKEND=http://localhost:8000
|
||||
VITE_APP_BACKEND=http://localhost:8000
|
||||
@@ -1 +1 @@
|
||||
REACT_APP_BACKEND=https://geneit-backend.communiquons.org
|
||||
VITE_APP_BACKEND=https://geneit-backend.communiquons.org
|
||||
|
||||
1
geneit_app/.gitignore
vendored
1
geneit_app/.gitignore
vendored
@@ -10,6 +10,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
Notice the use of in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
@@ -39,5 +39,7 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
16866
geneit_app/package-lock.json
generated
16866
geneit_app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,44 +2,54 @@
|
||||
"name": "geneit_app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@emotion/react": "^11.11.0",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/roboto": "^5.0.2",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@fullcalendar/core": "^6.1.14",
|
||||
"@fullcalendar/daygrid": "^6.1.14",
|
||||
"@fullcalendar/interaction": "^6.1.14",
|
||||
"@fullcalendar/list": "^6.1.14",
|
||||
"@fullcalendar/react": "^6.1.14",
|
||||
"@mdi/js": "^7.2.96",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mui/icons-material": "^5.11.16",
|
||||
"@mui/icons-material": "^5.15.17",
|
||||
"@mui/lab": "^5.0.0-alpha.140",
|
||||
"@mui/material": "^5.14.5",
|
||||
"@mui/x-data-grid": "^6.9.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.34",
|
||||
"@types/react": "^18.2.8",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"date-and-time": "^3.0.1",
|
||||
"@mui/material": "^5.15.17",
|
||||
"@mui/x-data-grid": "^7.1.1",
|
||||
"@mui/x-date-pickers": "^7.7.0",
|
||||
"@mui/x-tree-view": "^7.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.5",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"date-and-time": "^3.2.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"email-validator": "^2.0.4",
|
||||
"family-chart": "^0.0.0-beta-1",
|
||||
"filesize": "^10.0.9",
|
||||
"filesize": "^10.1.2",
|
||||
"jspdf": "^2.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-easy-crop": "^5.0.0",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-zoom-pan-pinch": "^3.1.0",
|
||||
"svg2pdf.js": "^2.2.2",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
"mui-color-input": "^2.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-easy-crop": "^5.0.7",
|
||||
"react-qr-code": "^2.0.14",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-zoom-pan-pinch": "^3.4.4",
|
||||
"svg2pdf.js": "^2.2.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "GeneIT",
|
||||
"name": "La généalogie de votre famille simplifiée",
|
||||
"name": "La vie informatique de votre famille simplifiée",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
||||
@@ -16,26 +16,33 @@ import { NewAccountRoute } from "./routes/auth/NewAccountRoute";
|
||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
||||
import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute";
|
||||
import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute";
|
||||
import { FamilyHomeRoute } from "./routes/family/FamilyHomeRoute";
|
||||
import {
|
||||
FamilyCreateMemberRoute,
|
||||
FamilyEditMemberRoute,
|
||||
FamilyMemberRoute,
|
||||
} from "./routes/family/FamilyMemberRoute";
|
||||
import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute";
|
||||
import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute";
|
||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||
import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute";
|
||||
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
||||
import { FamilyMembersListRoute } from "./routes/family/FamilyMembersListRoute";
|
||||
import { AccommodationsHomeRoute } from "./routes/family/accommodations/AccommodationsHomeRoute";
|
||||
import { AccommodationsReservationsRoute } from "./routes/family/accommodations/AccommodationsReservationsRoute";
|
||||
import { AccommodationsSettingsRoute } from "./routes/family/accommodations/AccommodationsSettingsRoute";
|
||||
import {
|
||||
FamilyCoupleRoute,
|
||||
FamilyCreateCoupleRoute,
|
||||
FamilyEditCoupleRoute,
|
||||
} from "./routes/family/FamilyCoupleRoute";
|
||||
import { FamilyCouplesListRoute } from "./routes/family/FamilyCouplesListRoute";
|
||||
import { FamilyTreeRoute } from "./routes/family/FamilyTreeRoute";
|
||||
import { FamilyMemberTreeRoute } from "./routes/family/FamilyMemberTreeRoute";
|
||||
} from "./routes/family/genealogy/FamilyCoupleRoute";
|
||||
import { FamilyCouplesListRoute } from "./routes/family/genealogy/FamilyCouplesListRoute";
|
||||
import { FamilyHomeRoute } from "./routes/family/genealogy/FamilyHomeRoute";
|
||||
import {
|
||||
FamilyCreateMemberRoute,
|
||||
FamilyEditMemberRoute,
|
||||
FamilyMemberRoute,
|
||||
} from "./routes/family/genealogy/FamilyMemberRoute";
|
||||
import { FamilyMemberTreeRoute } from "./routes/family/genealogy/FamilyMemberTreeRoute";
|
||||
import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute";
|
||||
import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute";
|
||||
import { GenalogySettingsRoute } from "./routes/family/genealogy/GenalogySettingsRoute";
|
||||
import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute";
|
||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||
import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute";
|
||||
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
||||
import { BaseAccommodationsRoute } from "./widgets/accommodations/BaseAccommodationsRoute";
|
||||
import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute";
|
||||
|
||||
interface AuthContext {
|
||||
signedIn: boolean;
|
||||
@@ -67,12 +74,18 @@ export function App(): React.ReactElement {
|
||||
<Route path="family/:familyId/*" element={<BaseFamilyRoute />}>
|
||||
<Route path="" element={<FamilyHomeRoute />} />
|
||||
|
||||
<Route path="genealogy/*" element={<BaseGenealogyRoute />}>
|
||||
<Route path="" element={<GenealogyHomeRoute />} />
|
||||
|
||||
<Route path="members" element={<FamilyMembersListRoute />} />
|
||||
<Route
|
||||
path="member/create"
|
||||
element={<FamilyCreateMemberRoute />}
|
||||
/>
|
||||
<Route path="member/:memberId" element={<FamilyMemberRoute />} />
|
||||
<Route
|
||||
path="member/:memberId"
|
||||
element={<FamilyMemberRoute />}
|
||||
/>
|
||||
<Route
|
||||
path="member/:memberId/edit"
|
||||
element={<FamilyEditMemberRoute />}
|
||||
@@ -83,7 +96,10 @@ export function App(): React.ReactElement {
|
||||
path="couple/create"
|
||||
element={<FamilyCreateCoupleRoute />}
|
||||
/>
|
||||
<Route path="couple/:coupleId" element={<FamilyCoupleRoute />} />
|
||||
<Route
|
||||
path="couple/:coupleId"
|
||||
element={<FamilyCoupleRoute />}
|
||||
/>
|
||||
<Route
|
||||
path="couple/:coupleId/edit"
|
||||
element={<FamilyEditCoupleRoute />}
|
||||
@@ -94,6 +110,25 @@ export function App(): React.ReactElement {
|
||||
path="tree/:memberId"
|
||||
element={<FamilyMemberTreeRoute />}
|
||||
/>
|
||||
<Route path="settings" element={<GenalogySettingsRoute />} />
|
||||
<Route path="*" element={<NotFoundRoute />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="accommodations/*"
|
||||
element={<BaseAccommodationsRoute />}
|
||||
>
|
||||
<Route path="" element={<AccommodationsHomeRoute />} />
|
||||
<Route
|
||||
path="reservations"
|
||||
element={<AccommodationsReservationsRoute />}
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={<AccommodationsSettingsRoute />}
|
||||
/>
|
||||
<Route path="*" element={<NotFoundRoute />} />
|
||||
</Route>
|
||||
|
||||
<Route path="settings" element={<FamilySettingsRoute />} />
|
||||
<Route path="users" element={<FamilyUsersListRoute />} />
|
||||
|
||||
@@ -16,7 +16,7 @@ export class APIClient {
|
||||
* Get backend URL
|
||||
*/
|
||||
static backendURL(): string {
|
||||
const URL = process.env.REACT_APP_BACKEND ?? "";
|
||||
const URL = import.meta.env.VITE_APP_BACKEND ?? "";
|
||||
if (URL.length === 0) throw new Error("Backend URL undefined!");
|
||||
return URL;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { APIClient } from "./ApiClient";
|
||||
import { Couple } from "./CoupleApi";
|
||||
import { Member } from "./MemberApi";
|
||||
import { Couple } from "./genealogy/CoupleApi";
|
||||
import { Member } from "./genealogy/MemberApi";
|
||||
|
||||
interface FamilyAPI {
|
||||
user_id: number;
|
||||
@@ -60,7 +60,8 @@ export class Family implements FamilyAPI {
|
||||
*/
|
||||
memberURL(member: Member, edit?: boolean): string {
|
||||
return (
|
||||
`/family/${this.family_id}/member/${member.id}` + (edit ? "/edit" : "")
|
||||
`/family/${this.family_id}/genealogy/member/${member.id}` +
|
||||
(edit ? "/edit" : "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ export class Family implements FamilyAPI {
|
||||
* Get family tree URL for member
|
||||
*/
|
||||
familyTreeURL(member: Member | number): string {
|
||||
return `/family/${this.family_id}/tree/${
|
||||
return `/family/${this.family_id}/genealogy/tree/${
|
||||
typeof member === "number" ? member : member.id
|
||||
}`;
|
||||
}
|
||||
@@ -78,16 +79,21 @@ export class Family implements FamilyAPI {
|
||||
*/
|
||||
coupleURL(member: Couple, edit?: boolean): string {
|
||||
return (
|
||||
`/family/${this.family_id}/couple/${member.id}` + (edit ? "/edit" : "")
|
||||
`/family/${this.family_id}/genealogy/couple/${member.id}` +
|
||||
(edit ? "/edit" : "")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtendedFamilyInfo extends Family {
|
||||
public disable_couple_photos: boolean;
|
||||
public enable_genealogy: boolean;
|
||||
public enable_accommodations: boolean;
|
||||
constructor(p: any) {
|
||||
super(p);
|
||||
this.disable_couple_photos = p.disable_couple_photos;
|
||||
this.enable_genealogy = p.enable_genealogy;
|
||||
this.enable_accommodations = p.enable_accommodations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,14 +235,18 @@ export class FamilyApi {
|
||||
*/
|
||||
static async UpdateFamily(settings: {
|
||||
id: number;
|
||||
name: string;
|
||||
disable_couple_photos: boolean;
|
||||
name?: string;
|
||||
enable_genealogy?: boolean;
|
||||
enable_accommodations?: boolean;
|
||||
disable_couple_photos?: boolean;
|
||||
}): Promise<void> {
|
||||
await APIClient.exec({
|
||||
method: "PATCH",
|
||||
uri: `/family/${settings.id}`,
|
||||
jsonData: {
|
||||
name: settings.name,
|
||||
enable_genealogy: settings.enable_genealogy,
|
||||
enable_accommodations: settings.enable_accommodations,
|
||||
disable_couple_photos: settings.disable_couple_photos,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,6 +32,9 @@ interface Constraints {
|
||||
member_country: LenConstraint;
|
||||
member_sex: LenConstraint;
|
||||
member_note: LenConstraint;
|
||||
accommodation_name_len: LenConstraint;
|
||||
accommodation_description_len: LenConstraint;
|
||||
accommodation_calendar_name_len: LenConstraint;
|
||||
}
|
||||
|
||||
interface OIDCProvider {
|
||||
|
||||
124
geneit_app/src/api/accommodations/AccommodationListApi.tsx
Normal file
124
geneit_app/src/api/accommodations/AccommodationListApi.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { APIClient } from "../ApiClient";
|
||||
import { Family } from "../FamilyApi";
|
||||
|
||||
export interface Accommodation {
|
||||
id: number;
|
||||
family_id: number;
|
||||
time_create: number;
|
||||
time_update: number;
|
||||
name: string;
|
||||
need_validation: boolean;
|
||||
description?: string;
|
||||
color?: string;
|
||||
open_to_reservations: boolean;
|
||||
}
|
||||
|
||||
export class AccommodationsList {
|
||||
private list: Accommodation[];
|
||||
private map: Map<number, Accommodation>;
|
||||
|
||||
constructor(list: Accommodation[]) {
|
||||
this.list = list;
|
||||
this.map = new Map();
|
||||
|
||||
for (const m of list) {
|
||||
this.map.set(m.id, m);
|
||||
}
|
||||
|
||||
this.list.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
public get isEmpty(): boolean {
|
||||
return this.list.length === 0;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
public get fullList(): Accommodation[] {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
filter(predicate: (m: Accommodation) => boolean): Accommodation[] {
|
||||
return this.list.filter(predicate);
|
||||
}
|
||||
|
||||
get openToReservationList(): Accommodation[] {
|
||||
return this.filter((a) => a.open_to_reservations);
|
||||
}
|
||||
|
||||
get(id: number): Accommodation | undefined {
|
||||
return this.map.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpdateAccommodation {
|
||||
name: string;
|
||||
need_validation: boolean;
|
||||
description?: string;
|
||||
color?: string;
|
||||
open_to_reservations: boolean;
|
||||
}
|
||||
|
||||
export class AccommodationListApi {
|
||||
/**
|
||||
* Get the list of accommodation of a family
|
||||
*/
|
||||
static async GetListOfFamily(family: Family): Promise<AccommodationsList> {
|
||||
const data = (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: `/family/${family.family_id}/accommodations/list/list`,
|
||||
})
|
||||
).data;
|
||||
|
||||
return new AccommodationsList(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new accommodation
|
||||
*/
|
||||
static async Create(
|
||||
family: Family,
|
||||
accommodation: UpdateAccommodation
|
||||
): Promise<Accommodation> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "POST",
|
||||
uri: `/family/${family.family_id}/accommodations/list/create`,
|
||||
jsonData: accommodation,
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an accommodation
|
||||
*/
|
||||
static async Update(
|
||||
accommodation: Accommodation,
|
||||
update: UpdateAccommodation
|
||||
): Promise<Accommodation> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "PUT",
|
||||
uri: `/family/${accommodation.family_id}/accommodations/list/${accommodation.id}`,
|
||||
jsonData: update,
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an accommodation
|
||||
*/
|
||||
static async Delete(accommodation: Accommodation): Promise<Accommodation> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "DELETE",
|
||||
uri: `/family/${accommodation.family_id}/accommodations/list/${accommodation.id}`,
|
||||
})
|
||||
).data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { APIClient } from "../ApiClient";
|
||||
import { Family } from "../FamilyApi";
|
||||
|
||||
export interface NewCalendarURL {
|
||||
accommodation_id?: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AccommodationCalendarURL {
|
||||
id: number;
|
||||
family_id: number;
|
||||
accommodation_id: number;
|
||||
user_id: number;
|
||||
name: string;
|
||||
token: string;
|
||||
time_create: number;
|
||||
time_used: number;
|
||||
}
|
||||
|
||||
export class AccommodationsCalendarURLApi {
|
||||
/**
|
||||
* Create a new accommodation calendar URL
|
||||
*/
|
||||
static async Create(
|
||||
family: Family,
|
||||
calendar: NewCalendarURL
|
||||
): Promise<AccommodationCalendarURL> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "POST",
|
||||
uri: `/family/${family.family_id}/accommodations/reservations_calendars/create`,
|
||||
jsonData: calendar,
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accommodation calendar URL route
|
||||
*/
|
||||
static CalendarURL(c: AccommodationCalendarURL): string {
|
||||
return `${APIClient.backendURL()}/acccommodations_calendar/${c.token}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accommodations calendars list
|
||||
*/
|
||||
static async GetList(family: Family): Promise<AccommodationCalendarURL[]> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: `/family/${family.family_id}/accommodations/reservations_calendars/list`,
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an accommodation calendar
|
||||
*/
|
||||
static async Delete(
|
||||
calendar: AccommodationCalendarURL
|
||||
): Promise<AccommodationCalendarURL> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "DELETE",
|
||||
uri: `/family/${calendar.family_id}/accommodations/reservations_calendars/${calendar.id}`,
|
||||
})
|
||||
).data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { APIClient } from "../ApiClient";
|
||||
import { Family } from "../FamilyApi";
|
||||
import { Accommodation } from "./AccommodationListApi";
|
||||
|
||||
export enum ValidateResaResult {
|
||||
Success,
|
||||
Error,
|
||||
Conflict,
|
||||
}
|
||||
|
||||
export interface AccommodationReservation {
|
||||
id: number;
|
||||
family_id: number;
|
||||
accommodation_id: number;
|
||||
user_id: number;
|
||||
time_create: number;
|
||||
time_update: number;
|
||||
reservation_start: number;
|
||||
reservation_end: number;
|
||||
validated?: boolean;
|
||||
}
|
||||
|
||||
export class AccommodationsReservationsList {
|
||||
private list: AccommodationReservation[];
|
||||
private map: Map<number, AccommodationReservation>;
|
||||
|
||||
constructor(list: AccommodationReservation[]) {
|
||||
this.list = list;
|
||||
this.map = new Map();
|
||||
|
||||
for (const m of list) {
|
||||
this.map.set(m.id, m);
|
||||
}
|
||||
|
||||
this.list.sort((a, b) => a.reservation_start - b.reservation_start);
|
||||
}
|
||||
|
||||
public get isEmpty(): boolean {
|
||||
return this.list.length === 0;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
public get fullList(): AccommodationReservation[] {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
filter(
|
||||
predicate: (m: AccommodationReservation) => boolean
|
||||
): AccommodationReservation[] {
|
||||
return this.list.filter(predicate);
|
||||
}
|
||||
|
||||
forAccommodation(id: number): AccommodationReservation[] {
|
||||
return this.filter((a) => a.accommodation_id === id);
|
||||
}
|
||||
|
||||
get(id: number): AccommodationReservation | undefined {
|
||||
return this.map.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
export interface UpdateAccommodationReservation {
|
||||
start: number;
|
||||
end: number;
|
||||
accommodation_id: number;
|
||||
reservation_id?: number;
|
||||
}
|
||||
|
||||
export class AccommodationsReservationsApi {
|
||||
/**
|
||||
* Create a new reservation
|
||||
*/
|
||||
static async Create(
|
||||
family: Family,
|
||||
reservation: UpdateAccommodationReservation
|
||||
): Promise<AccommodationReservation> {
|
||||
return (
|
||||
await APIClient.exec({
|
||||
method: "POST",
|
||||
uri: `/family/${family.family_id}/accommodations/reservations/accommodation/${reservation.accommodation_id}/create`,
|
||||
jsonData: {
|
||||
start: reservation.start,
|
||||
end: reservation.end,
|
||||
},
|
||||
})
|
||||
).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entire list of accommodations of a family
|
||||
*/
|
||||
static async FullListOfFamily(
|
||||
family: Family
|
||||
): Promise<AccommodationsReservationsList> {
|
||||
const data = (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: `/family/${family.family_id}/accommodations/reservations/full_list`,
|
||||
})
|
||||
).data;
|
||||
|
||||
return new AccommodationsReservationsList(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reservations of a given time interval for an accommodation
|
||||
*/
|
||||
static async ReservationsForInterval(
|
||||
family: Family,
|
||||
accommodation: Accommodation,
|
||||
start: number,
|
||||
end: number
|
||||
): Promise<AccommodationsReservationsList> {
|
||||
const data = (
|
||||
await APIClient.exec({
|
||||
method: "GET",
|
||||
uri: `/family/${family.family_id}/accommodations/reservations/accommodation/${accommodation.id}/for_interval?start=${start}&end=${end}`,
|
||||
})
|
||||
).data;
|
||||
|
||||
return new AccommodationsReservationsList(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a reservation
|
||||
*/
|
||||
static async Update(
|
||||
family: Family,
|
||||
r: UpdateAccommodationReservation
|
||||
): Promise<void> {
|
||||
await APIClient.exec({
|
||||
method: "PATCH",
|
||||
uri: `/family/${family.family_id}/accommodations/reservation/${r.reservation_id}`,
|
||||
jsonData: {
|
||||
start: r.start,
|
||||
end: r.end,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a reservation
|
||||
*/
|
||||
static async Delete(r: AccommodationReservation): Promise<void> {
|
||||
await APIClient.exec({
|
||||
method: "DELETE",
|
||||
uri: `/family/${r.family_id}/accommodations/reservation/${r.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate or reject a reservation request
|
||||
*/
|
||||
static async Validate(
|
||||
r: AccommodationReservation,
|
||||
accept: boolean
|
||||
): Promise<ValidateResaResult> {
|
||||
const res = await APIClient.exec({
|
||||
method: "POST",
|
||||
uri: `/family/${r.family_id}/accommodations/reservation/${r.id}/validate`,
|
||||
jsonData: {
|
||||
validate: accept,
|
||||
},
|
||||
allowFail: true,
|
||||
});
|
||||
|
||||
if (res.status >= 200 && res.status <= 299)
|
||||
return ValidateResaResult.Success;
|
||||
if (res.status === 409) return ValidateResaResult.Conflict;
|
||||
return ValidateResaResult.Error;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { APIClient } from "./ApiClient";
|
||||
import { APIClient } from "../ApiClient";
|
||||
import { DateValue, Member } from "./MemberApi";
|
||||
import { ServerApi } from "./ServerApi";
|
||||
import { ServerApi } from "../ServerApi";
|
||||
|
||||
interface CoupleApiInterface {
|
||||
id: number;
|
||||
@@ -161,7 +161,7 @@ export class CoupleApi {
|
||||
*/
|
||||
static async Create(m: Couple): Promise<Couple> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/couple/create`,
|
||||
uri: `/family/${m.family_id}/genealogy/couple/create`,
|
||||
method: "POST",
|
||||
jsonData: m,
|
||||
});
|
||||
@@ -177,7 +177,7 @@ export class CoupleApi {
|
||||
couple_id: number
|
||||
): Promise<Couple> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/couple/${couple_id}`,
|
||||
uri: `/family/${family_id}/genealogy/couple/${couple_id}`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -189,7 +189,7 @@ export class CoupleApi {
|
||||
*/
|
||||
static async GetEntireList(family_id: number): Promise<CouplesList> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/couples`,
|
||||
uri: `/family/${family_id}/genealogy/couples`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -201,7 +201,7 @@ export class CoupleApi {
|
||||
*/
|
||||
static async Update(m: Couple): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/couple/${m.id}`,
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
|
||||
method: "PUT",
|
||||
jsonData: m,
|
||||
});
|
||||
@@ -214,7 +214,7 @@ export class CoupleApi {
|
||||
const fd = new FormData();
|
||||
fd.append("photo", b);
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/couple/${m.id}/photo`,
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`,
|
||||
method: "PUT",
|
||||
formData: fd,
|
||||
});
|
||||
@@ -225,7 +225,7 @@ export class CoupleApi {
|
||||
*/
|
||||
static async RemoveCouplePhoto(m: Couple): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/couple/${m.id}/photo`,
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
@@ -235,7 +235,7 @@ export class CoupleApi {
|
||||
*/
|
||||
static async Delete(m: Couple): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/couple/${m.id}`,
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIClient } from "./ApiClient";
|
||||
import { APIClient } from "../ApiClient";
|
||||
|
||||
/**
|
||||
* Data management api client
|
||||
@@ -9,7 +9,7 @@ export class DataApi {
|
||||
*/
|
||||
static async ExportData(family_id: number): Promise<Blob> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/data/export`,
|
||||
uri: `/family/${family_id}/genealogy/data/export`,
|
||||
method: "GET",
|
||||
});
|
||||
return res.data;
|
||||
@@ -22,7 +22,7 @@ export class DataApi {
|
||||
const fd = new FormData();
|
||||
fd.append("archive", archive);
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/data/import`,
|
||||
uri: `/family/${family_id}/genealogy/data/import`,
|
||||
method: "PUT",
|
||||
formData: fd,
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIClient } from "./ApiClient";
|
||||
import { APIClient } from "../ApiClient";
|
||||
import { Couple } from "./CoupleApi";
|
||||
|
||||
export type Sex = "M" | "F";
|
||||
@@ -278,7 +278,7 @@ export class MemberApi {
|
||||
*/
|
||||
static async Create(m: Member): Promise<Member> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/member/create`,
|
||||
uri: `/family/${m.family_id}/genealogy/member/create`,
|
||||
method: "POST",
|
||||
jsonData: m,
|
||||
});
|
||||
@@ -294,7 +294,7 @@ export class MemberApi {
|
||||
member_id: number
|
||||
): Promise<Member> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/member/${member_id}`,
|
||||
uri: `/family/${family_id}/genealogy/member/${member_id}`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -306,7 +306,7 @@ export class MemberApi {
|
||||
*/
|
||||
static async GetEntireList(family_id: number): Promise<MembersList> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/members`,
|
||||
uri: `/family/${family_id}/genealogy/members`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -318,7 +318,7 @@ export class MemberApi {
|
||||
*/
|
||||
static async Update(m: Member): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/member/${m.id}`,
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
|
||||
method: "PUT",
|
||||
jsonData: m,
|
||||
});
|
||||
@@ -331,7 +331,7 @@ export class MemberApi {
|
||||
const fd = new FormData();
|
||||
fd.append("photo", b);
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/member/${m.id}/photo`,
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`,
|
||||
method: "PUT",
|
||||
formData: fd,
|
||||
});
|
||||
@@ -342,7 +342,7 @@ export class MemberApi {
|
||||
*/
|
||||
static async RemoveMemberPhoto(m: Member): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/member/${m.id}/photo`,
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
@@ -352,7 +352,7 @@ export class MemberApi {
|
||||
*/
|
||||
static async Delete(m: Member): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/member/${m.id}`,
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { NewCalendarURL } from "../../api/accommodations/AccommodationsCalendarURLApi";
|
||||
import { checkConstraint } from "../../utils/form_utils";
|
||||
import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute";
|
||||
import { PropEdit } from "../../widgets/forms/PropEdit";
|
||||
import { PropSelect } from "../../widgets/forms/PropSelect";
|
||||
|
||||
export function CreateAccommodationCalendarURLDialog(p: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmitted: (c: NewCalendarURL) => void;
|
||||
}): React.ReactElement {
|
||||
const [calendar, setCalendar] = React.useState<NewCalendarURL>({ name: "" });
|
||||
|
||||
const accommodations = useAccommodations();
|
||||
|
||||
const nameErr = checkConstraint(
|
||||
ServerApi.Config.constraints.accommodation_calendar_name_len,
|
||||
calendar?.name
|
||||
);
|
||||
|
||||
const clearForm = () => {
|
||||
setCalendar({ name: "" });
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
clearForm();
|
||||
p.onClose();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
clearForm();
|
||||
p.onSubmitted(calendar!);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={p.open} onClose={cancel}>
|
||||
<DialogTitle>Création d'un calendrier</DialogTitle>
|
||||
<DialogContent style={{ display: "flex", flexDirection: "column" }}>
|
||||
<PropEdit
|
||||
editable
|
||||
label="Nom"
|
||||
value={calendar?.name}
|
||||
onValueChange={(s) =>
|
||||
setCalendar((a) => {
|
||||
return {
|
||||
...a!,
|
||||
name: s!,
|
||||
};
|
||||
})
|
||||
}
|
||||
size={ServerApi.Config.constraints.accommodation_calendar_name_len}
|
||||
helperText={nameErr}
|
||||
/>
|
||||
|
||||
<PropSelect
|
||||
editing
|
||||
label="Logement ciblé"
|
||||
onValueChange={(v) => {
|
||||
setCalendar((a) => {
|
||||
return {
|
||||
...a!,
|
||||
accommodation_id: v !== "A" && v ? Number(v) : undefined,
|
||||
};
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ label: "Tous les logements", value: "A" },
|
||||
...accommodations.accommodations.fullList.map((a) => {
|
||||
return { label: a.name, value: a.id.toString() };
|
||||
}),
|
||||
]}
|
||||
value={calendar.accommodation_id?.toString() ?? "A"}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={cancel}>Annuler</Button>
|
||||
<Button onClick={submit} disabled={!!nameErr}>
|
||||
Créer
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
OutlinedInput,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import QRCode from "react-qr-code";
|
||||
import {
|
||||
AccommodationCalendarURL,
|
||||
AccommodationsCalendarURLApi,
|
||||
} from "../../api/accommodations/AccommodationsCalendarURLApi";
|
||||
import { CopyToClipboard } from "../../widgets/CopyToClipboard";
|
||||
|
||||
export function InstallCalendarDialog(p: {
|
||||
cal?: AccommodationCalendarURL;
|
||||
onClose: () => void;
|
||||
}): React.ReactElement {
|
||||
if (!p.cal) return <></>;
|
||||
|
||||
return (
|
||||
<Dialog open={true} onClose={p.onClose}>
|
||||
<DialogTitle>Installation du calendrier</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<Typography>
|
||||
Afin d'installer le calendrier <i>{p.cal.name}</i> sur votre
|
||||
appareil, veuillez utiliser l'URL suivante :
|
||||
</Typography>
|
||||
<br />
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<OutlinedInput
|
||||
value={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<CopyToClipboard
|
||||
content={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
|
||||
>
|
||||
<IconButton>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</CopyToClipboard>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
margin: "20px auto",
|
||||
padding: "20px",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
<QRCode
|
||||
value={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={p.onClose}>Fermer</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { UpdateAccommodation } from "../../api/accommodations/AccommodationListApi";
|
||||
import { checkConstraint } from "../../utils/form_utils";
|
||||
import { PropCheckbox } from "../../widgets/forms/PropCheckbox";
|
||||
import { PropEdit } from "../../widgets/forms/PropEdit";
|
||||
import { PropColorPicker } from "../../widgets/forms/PropColorPicker";
|
||||
|
||||
export function UpdateAccommodationDialog(p: {
|
||||
open: boolean;
|
||||
create: boolean;
|
||||
onClose: () => void;
|
||||
onSubmitted: (c: UpdateAccommodation) => void;
|
||||
accommodation: UpdateAccommodation | undefined;
|
||||
}): React.ReactElement {
|
||||
const [accommodation, setAccommodation] = React.useState<
|
||||
UpdateAccommodation | undefined
|
||||
>();
|
||||
|
||||
const nameErr = checkConstraint(
|
||||
ServerApi.Config.constraints.accommodation_name_len,
|
||||
accommodation?.name
|
||||
);
|
||||
const descriptionErr = checkConstraint(
|
||||
ServerApi.Config.constraints.accommodation_description_len,
|
||||
accommodation?.description
|
||||
);
|
||||
|
||||
const clearForm = () => {
|
||||
setAccommodation(undefined);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
clearForm();
|
||||
p.onClose();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
clearForm();
|
||||
p.onSubmitted(accommodation!);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!accommodation) setAccommodation(p.accommodation);
|
||||
}, [p.open, p.accommodation]);
|
||||
|
||||
return (
|
||||
<Dialog open={p.open} onClose={cancel}>
|
||||
<DialogTitle>
|
||||
{p.create ? "Création" : "Mise à jour"} d'un logement
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ display: "flex", flexDirection: "column" }}>
|
||||
<PropEdit
|
||||
editable
|
||||
label="Nom"
|
||||
value={accommodation?.name}
|
||||
onValueChange={(s) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
name: s!,
|
||||
};
|
||||
})
|
||||
}
|
||||
size={ServerApi.Config.constraints.accommodation_name_len}
|
||||
helperText={nameErr}
|
||||
/>
|
||||
|
||||
<PropEdit
|
||||
editable
|
||||
label="Description"
|
||||
value={accommodation?.description}
|
||||
onValueChange={(s) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
description: s!,
|
||||
};
|
||||
})
|
||||
}
|
||||
size={ServerApi.Config.constraints.accommodation_description_len}
|
||||
helperText={descriptionErr}
|
||||
/>
|
||||
|
||||
<PropColorPicker
|
||||
editable
|
||||
label="Couleur"
|
||||
value={accommodation?.color}
|
||||
onChange={(s) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
color: s!,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<PropCheckbox
|
||||
editable
|
||||
label="Ouvert aux réservations"
|
||||
checked={accommodation?.open_to_reservations === true}
|
||||
onValueChange={(c) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
open_to_reservations: c,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
"Permet de spécifier si un administrateur de la famille doit valider manuellement les demandes de réservation pour qu'elles soient validées"
|
||||
}
|
||||
>
|
||||
<PropCheckbox
|
||||
checkboxAlwaysVisible
|
||||
editable={accommodation?.open_to_reservations === true}
|
||||
label="Validation des réservations par un administrateur requise"
|
||||
checked={accommodation?.need_validation === true}
|
||||
onValueChange={(c) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
need_validation: c,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={cancel}>Annuler</Button>
|
||||
<Button
|
||||
onClick={submit}
|
||||
disabled={
|
||||
!!nameErr || (!!accommodation?.description && !!descriptionErr)
|
||||
}
|
||||
>
|
||||
{p.create ? "Créer" : "Mettre à jour"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import {
|
||||
AccommodationReservation,
|
||||
AccommodationsReservationsApi,
|
||||
UpdateAccommodationReservation,
|
||||
} from "../../api/accommodations/AccommodationsReservationsApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { fmtUnixDate } from "../../utils/time_utils";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute";
|
||||
import { PropDateInput } from "../../widgets/forms/PropDateInput";
|
||||
import { PropSelect } from "../../widgets/forms/PropSelect";
|
||||
|
||||
export function UpdateReservationDialog(p: {
|
||||
open: boolean;
|
||||
create: boolean;
|
||||
reservation?: UpdateAccommodationReservation;
|
||||
onClose: () => void;
|
||||
onSubmitted: (c: UpdateAccommodationReservation) => void;
|
||||
}): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
|
||||
const family = useFamily();
|
||||
const accommodations = useAccommodations();
|
||||
|
||||
const [reservation, setReservation] = React.useState<
|
||||
UpdateAccommodationReservation | undefined
|
||||
>();
|
||||
|
||||
const [conflicts, setConflicts] = React.useState<
|
||||
AccommodationReservation[] | undefined
|
||||
>(undefined);
|
||||
|
||||
const clearForm = () => {
|
||||
setReservation(undefined);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
clearForm();
|
||||
p.onClose();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
clearForm();
|
||||
p.onSubmitted(reservation!);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!reservation) setReservation(p.reservation);
|
||||
}, [p.open, p.reservation]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setConflicts(undefined);
|
||||
(async () => {
|
||||
try {
|
||||
if (
|
||||
!reservation ||
|
||||
reservation.accommodation_id < 1 ||
|
||||
reservation.start < 1 ||
|
||||
reservation.start > reservation.end
|
||||
) {
|
||||
setConflicts([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setConflicts(
|
||||
(
|
||||
await AccommodationsReservationsApi.ReservationsForInterval(
|
||||
family.family,
|
||||
accommodations.accommodations.get(reservation.accommodation_id)!,
|
||||
reservation.start,
|
||||
reservation.end
|
||||
)
|
||||
).filter(
|
||||
(r) =>
|
||||
r.id !== p.reservation?.reservation_id && r.validated !== false
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(
|
||||
"Echec de la vérification de la présence de conflits de calendrier !"
|
||||
);
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
p.open,
|
||||
reservation?.accommodation_id,
|
||||
reservation?.start,
|
||||
reservation?.end,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Dialog open={p.open} onClose={cancel}>
|
||||
<DialogTitle>
|
||||
{p.create ? "Création" : "Mise à jour"} d'une réservation
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ display: "flex", flexDirection: "column" }}>
|
||||
<PropSelect
|
||||
editing={p.create}
|
||||
label="Logement ciblé"
|
||||
onValueChange={(v) => {
|
||||
setReservation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
accommodation_id: Number(v),
|
||||
};
|
||||
});
|
||||
}}
|
||||
options={accommodations.accommodations.openToReservationList.map(
|
||||
(a) => {
|
||||
return { label: a.name, value: a.id.toString() };
|
||||
}
|
||||
)}
|
||||
value={
|
||||
reservation?.accommodation_id === -1
|
||||
? ""
|
||||
: reservation?.accommodation_id?.toString()
|
||||
}
|
||||
/>
|
||||
|
||||
<PropDateInput
|
||||
editable
|
||||
label="Date de début"
|
||||
value={reservation?.start}
|
||||
onChange={(s) => {
|
||||
setReservation((r) => {
|
||||
return { ...r!, start: s ?? -1 };
|
||||
});
|
||||
}}
|
||||
minDate={Math.floor(new Date().getTime() / 1000) - 3600 * 24 * 60}
|
||||
canSetMiddleDay
|
||||
/>
|
||||
|
||||
<PropDateInput
|
||||
editable
|
||||
label="Date de fin"
|
||||
value={reservation?.end}
|
||||
lastSecOfDay={true}
|
||||
onChange={(s) => {
|
||||
setReservation((r) => {
|
||||
return { ...r!, end: s ?? -1 };
|
||||
});
|
||||
}}
|
||||
minDate={reservation?.start}
|
||||
canSetMiddleDay
|
||||
/>
|
||||
|
||||
{conflicts && conflicts.length > 0 && (
|
||||
<Alert severity="error">
|
||||
<p>
|
||||
Cette réservation est en conflit avec d'autres réservations sur
|
||||
les intervalles suivants :
|
||||
</p>
|
||||
<ul>
|
||||
{conflicts.map((c, num) => (
|
||||
<li key={num}>
|
||||
Réservation du {fmtUnixDate(c.reservation_start)} au{" "}
|
||||
{fmtUnixDate(c.reservation_end)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={cancel}>Annuler</Button>
|
||||
<Button
|
||||
onClick={submit}
|
||||
disabled={
|
||||
!(
|
||||
(reservation?.accommodation_id ?? -1) > 0 &&
|
||||
(reservation?.start ?? -1) > 0 &&
|
||||
(reservation?.end ?? -1) > (reservation?.start ?? 0) &&
|
||||
(conflicts?.length ?? 0) === 0
|
||||
)
|
||||
}
|
||||
>
|
||||
{p.create ? "Créer" : "Mettre à jour"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||
import React from "react";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { frFR as dataGridFr } from "@mui/x-data-grid";
|
||||
import { frFR as dataGridFr } from "@mui/x-data-grid/locales";
|
||||
|
||||
const localStorageKey = "dark-theme";
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { NewCalendarURL } from "../../../api/accommodations/AccommodationsCalendarURLApi";
|
||||
import { CreateAccommodationCalendarURLDialog } from "../../../dialogs/accommodations/CreateAccommodationCalendarURLDialog";
|
||||
|
||||
type DialogContext = () => Promise<NewCalendarURL | undefined>;
|
||||
|
||||
const DialogContextK = React.createContext<DialogContext | null>(null);
|
||||
|
||||
export function CreateAccommodationCalendarURLDialogProvider(
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const cb = React.useRef<null | ((a: NewCalendarURL | undefined) => void)>(
|
||||
null
|
||||
);
|
||||
|
||||
const handleClose = (res?: NewCalendarURL) => {
|
||||
setOpen(false);
|
||||
|
||||
if (cb.current !== null) cb.current(res);
|
||||
cb.current = null;
|
||||
};
|
||||
|
||||
const hook: DialogContext = () => {
|
||||
setOpen(true);
|
||||
|
||||
return new Promise((res) => {
|
||||
cb.current = res;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContextK.Provider value={hook}>
|
||||
{p.children}
|
||||
</DialogContextK.Provider>
|
||||
|
||||
{open && (
|
||||
<CreateAccommodationCalendarURLDialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onSubmitted={handleClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCreateAccommodationCalendarURL(): DialogContext {
|
||||
return React.useContext(DialogContextK)!;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { AccommodationCalendarURL } from "../../../api/accommodations/AccommodationsCalendarURLApi";
|
||||
import { InstallCalendarDialog } from "../../../dialogs/accommodations/InstallCalendarDialog";
|
||||
|
||||
type DialogContext = (cal: AccommodationCalendarURL) => Promise<void>;
|
||||
|
||||
const DialogContextK = React.createContext<DialogContext | null>(null);
|
||||
|
||||
export function InstallCalendarDialogProvider(
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [cal, setCal] = React.useState<AccommodationCalendarURL | undefined>();
|
||||
|
||||
const cb = React.useRef<null | (() => void)>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
setCal(undefined);
|
||||
|
||||
if (cb.current !== null) cb.current();
|
||||
cb.current = null;
|
||||
};
|
||||
|
||||
const hook: DialogContext = (c) => {
|
||||
setCal(c);
|
||||
|
||||
return new Promise((res) => {
|
||||
cb.current = res;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContextK.Provider value={hook}>
|
||||
{p.children}
|
||||
</DialogContextK.Provider>
|
||||
|
||||
{cal && <InstallCalendarDialog cal={cal} onClose={handleClose} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useInstallCalendarDialog(): DialogContext {
|
||||
return React.useContext(DialogContextK)!;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { UpdateAccommodation } from "../../../api/accommodations/AccommodationListApi";
|
||||
import { UpdateAccommodationDialog } from "../../../dialogs/accommodations/UpdateAccommodationDialog";
|
||||
|
||||
type DialogContext = (
|
||||
accommodation: UpdateAccommodation,
|
||||
create: boolean
|
||||
) => Promise<UpdateAccommodation | undefined>;
|
||||
|
||||
const DialogContextK = React.createContext<DialogContext | null>(null);
|
||||
|
||||
export function UpdateAccommodationDialogProvider(
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const [accommodation, setAccommodation] = React.useState<
|
||||
UpdateAccommodation | undefined
|
||||
>(undefined);
|
||||
const [create, setCreate] = React.useState(false);
|
||||
|
||||
const cb = React.useRef<
|
||||
null | ((a: UpdateAccommodation | undefined) => void)
|
||||
>(null);
|
||||
|
||||
const handleClose = (res?: UpdateAccommodation) => {
|
||||
setOpen(false);
|
||||
|
||||
if (cb.current !== null) cb.current(res);
|
||||
cb.current = null;
|
||||
};
|
||||
|
||||
const hook: DialogContext = (accommodation, create) => {
|
||||
setAccommodation(accommodation);
|
||||
setCreate(create);
|
||||
setOpen(true);
|
||||
|
||||
return new Promise((res) => {
|
||||
cb.current = res;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContextK.Provider value={hook}>
|
||||
{p.children}
|
||||
</DialogContextK.Provider>
|
||||
|
||||
{open && (
|
||||
<UpdateAccommodationDialog
|
||||
open={open}
|
||||
accommodation={accommodation}
|
||||
create={create}
|
||||
onClose={handleClose}
|
||||
onSubmitted={handleClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdateAccommodation(): DialogContext {
|
||||
return React.useContext(DialogContextK)!;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { UpdateAccommodationReservation } from "../../../api/accommodations/AccommodationsReservationsApi";
|
||||
import { UpdateReservationDialog } from "../../../dialogs/accommodations/UpdateReservationDialog";
|
||||
|
||||
type DialogContext = (
|
||||
reservation: UpdateAccommodationReservation,
|
||||
create: boolean
|
||||
) => Promise<UpdateAccommodationReservation | undefined>;
|
||||
|
||||
const DialogContextK = React.createContext<DialogContext | null>(null);
|
||||
|
||||
export function UpdateReservationDialogProvider(
|
||||
p: PropsWithChildren
|
||||
): React.ReactElement {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const [reservation, setReservation] = React.useState<
|
||||
UpdateAccommodationReservation | undefined
|
||||
>(undefined);
|
||||
const [create, setCreate] = React.useState(false);
|
||||
|
||||
const cb = React.useRef<
|
||||
null | ((a: UpdateAccommodationReservation | undefined) => void)
|
||||
>(null);
|
||||
|
||||
const handleClose = (res?: UpdateAccommodationReservation) => {
|
||||
setOpen(false);
|
||||
|
||||
if (cb.current !== null) cb.current(res);
|
||||
cb.current = null;
|
||||
};
|
||||
|
||||
const hook: DialogContext = (accommodation, create) => {
|
||||
setReservation(accommodation);
|
||||
setCreate(create);
|
||||
setOpen(true);
|
||||
|
||||
return new Promise((res) => {
|
||||
cb.current = res;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContextK.Provider value={hook}>
|
||||
{p.children}
|
||||
</DialogContextK.Provider>
|
||||
|
||||
{open && (
|
||||
<UpdateReservationDialog
|
||||
open={open}
|
||||
reservation={reservation}
|
||||
create={create}
|
||||
onClose={handleClose}
|
||||
onSubmitted={handleClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdateAccommodationReservation(): DialogContext {
|
||||
return React.useContext(DialogContextK)!;
|
||||
}
|
||||
1
geneit_app/src/react-app-env.d.ts
vendored
1
geneit_app/src/react-app-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle";
|
||||
|
||||
export function FamilyHomeRoute(): React.ReactElement {
|
||||
const family = useFamily();
|
||||
return (
|
||||
<>
|
||||
<FamilyPageTitle title="Votre famille" />
|
||||
<div style={{ margin: "20px" }}>
|
||||
<p>
|
||||
Bienvenue sur l'espace dédié à la généalogie de votre famille !
|
||||
Veuillez utiliser le menu situé à gauche pour accéder aux différentes
|
||||
sections de l'application.
|
||||
</p>
|
||||
<p>Nombre de fiches de membres: {family.members.size}</p>
|
||||
<p>Nombre de fiches de couples: {family.couples.size}</p>
|
||||
<p>
|
||||
Vous pouvez inviter d'autres personnes à rejoindre cette famille en
|
||||
leur donnant une copie du code d'invitation
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,19 @@
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { DataApi } from "../../api/DataApi";
|
||||
import { FamilyApi } from "../../api/FamilyApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
|
||||
import { downloadBlob, selectFileToUpload } from "../../utils/files_utils";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { FamilyCard } from "../../widgets/FamilyCard";
|
||||
import { formatDate } from "../../widgets/TimeWidget";
|
||||
@@ -55,7 +48,6 @@ export function FamilySettingsRoute(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<FamilySettingsCard />
|
||||
<FamilyExportCard />
|
||||
<div style={{ textAlign: "center", marginTop: "50px" }}>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -76,8 +68,11 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
const family = useFamily();
|
||||
|
||||
const [newName, setNewName] = React.useState(family.family.name);
|
||||
const [disableCouplePhotos, setDisableCouplePhotos] = React.useState(
|
||||
family.family.disable_couple_photos
|
||||
const [enableGenealogy, setEnableGenealogy] = React.useState(
|
||||
family.family.enable_genealogy
|
||||
);
|
||||
const [enableAccommodations, setEnableAccommodations] = React.useState(
|
||||
family.family.enable_accommodations
|
||||
);
|
||||
|
||||
const canEdit = family.family.is_admin;
|
||||
@@ -93,7 +88,8 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
await FamilyApi.UpdateFamily({
|
||||
id: family.family.family_id,
|
||||
name: newName,
|
||||
disable_couple_photos: disableCouplePhotos,
|
||||
enable_genealogy: enableGenealogy,
|
||||
enable_accommodations: enableAccommodations,
|
||||
});
|
||||
|
||||
family.reloadFamilyInfo();
|
||||
@@ -126,14 +122,12 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
label="Identifiant"
|
||||
value={family.family.family_id}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
label="Création de la famille"
|
||||
value={formatDate(family.family.time_create)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Nom de la famille"
|
||||
@@ -144,18 +138,26 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
maxLength: ServerApi.Config.constraints.family_name_len.max,
|
||||
}}
|
||||
/>
|
||||
<Tooltip title="Les photos de couple ne sont pas utilisées en pratique dans les arbres généalogiques. Il est possible de masquer les formulaires d'édition de photos de couple pour limiter le risque de confusion.">
|
||||
<FormControlLabel
|
||||
disabled={!canEdit}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={disableCouplePhotos}
|
||||
onChange={(_e, c) => setDisableCouplePhotos(c)}
|
||||
<Switch
|
||||
checked={enableGenealogy}
|
||||
onChange={(_e, c) => setEnableGenealogy(c)}
|
||||
/>
|
||||
}
|
||||
label="Désactiver les photos de couple"
|
||||
label="Activer le module de généalogie"
|
||||
/>
|
||||
<FormControlLabel
|
||||
disabled={!canEdit}
|
||||
control={
|
||||
<Switch
|
||||
checked={enableAccommodations}
|
||||
onChange={(_e, c) => setEnableAccommodations(c)}
|
||||
/>
|
||||
}
|
||||
label="Activer le module de réservation de logements"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
@@ -170,109 +172,3 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
</FamilyCard>
|
||||
);
|
||||
}
|
||||
|
||||
function FamilyExportCard(): React.ReactElement {
|
||||
const loading = useLoadingMessage();
|
||||
const confirm = useConfirm();
|
||||
const alert = useAlert();
|
||||
|
||||
const family = useFamily();
|
||||
|
||||
const [error, setError] = React.useState<string>();
|
||||
const [success, setSuccess] = React.useState<string>();
|
||||
|
||||
const exportData = async () => {
|
||||
loading.show("Export des données");
|
||||
try {
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
|
||||
const blob = await DataApi.ExportData(family.familyId);
|
||||
downloadBlob(blob, `Export-${new Date().getTime()}.zip`);
|
||||
|
||||
setSuccess("Export des données effectué avec succès !");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError("Echec de l'export des données de la famille !");
|
||||
}
|
||||
loading.hide();
|
||||
};
|
||||
|
||||
const importData = async () => {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Attention ! Cette opération a pour effet d'effacer toutes les données existantes en base ! Voulez-vous vraiment poursuivre l'opération ?"
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
const file = await selectFileToUpload({
|
||||
allowedTypes: ["application/zip"],
|
||||
});
|
||||
if (file === null) return;
|
||||
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
|
||||
loading.show("Restauration des données de la famille en cours...");
|
||||
|
||||
await DataApi.ImportData(family.familyId, file);
|
||||
|
||||
family.reloadFamilyInfo();
|
||||
|
||||
alert("Import des données de la famille effectué avec succès !");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(`Echec de l'import des données de la famille ! (${e})`);
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
};
|
||||
|
||||
return (
|
||||
<FamilyCard error={error} success={success}>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="div">
|
||||
Export / import des données de la famille
|
||||
</Typography>
|
||||
<p>
|
||||
Vous pouvez, à des fins de sauvegardes ou de transfert, exporter et
|
||||
importer l'ensemble des données des membres et des couples de cette
|
||||
famille, sous format ZIP.
|
||||
</p>
|
||||
|
||||
<Alert severity="warning">
|
||||
Attention ! La restauration des données de la famille provoque
|
||||
préalablement l'effacement de toutes les données enregistrées dans la
|
||||
famille ! Par ailleurs, la restauration n'est pas réversible !
|
||||
</Alert>
|
||||
|
||||
<p> </p>
|
||||
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={exportData}
|
||||
size={"large"}
|
||||
style={{ marginBottom: "10px" }}
|
||||
>
|
||||
Exporter les données de la famille
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
startIcon={<UploadIcon />}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
fullWidth
|
||||
onClick={importData}
|
||||
disabled={!family.family.is_admin}
|
||||
size={"large"}
|
||||
>
|
||||
Importer les données de la famille
|
||||
</Button>
|
||||
</CardContent>
|
||||
</FamilyCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
||||
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
|
||||
|
||||
export function AccommodationsHomeRoute(): React.ReactElement {
|
||||
const accommodations = useAccommodations();
|
||||
return (
|
||||
<>
|
||||
<FamilyPageTitle title="Réservation de logements" />
|
||||
<div style={{ margin: "20px" }}>
|
||||
<p>
|
||||
Depuis cette section de l'application, vous pouvez effectuer des
|
||||
réservations de logements.
|
||||
</p>
|
||||
<p> </p>
|
||||
<p>
|
||||
Nombre de logements définis : {accommodations.accommodations.size}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
import { DateSelectArg, EventClickArg } from "@fullcalendar/core";
|
||||
import frLocale from "@fullcalendar/core/locales/fr";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import FullCalendar from "@fullcalendar/react";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import RuleIcon from "@mui/icons-material/Rule";
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { red } from "@mui/material/colors";
|
||||
import React from "react";
|
||||
import { FamilyApi, FamilyUser } from "../../../api/FamilyApi";
|
||||
import { Accommodation } from "../../../api/accommodations/AccommodationListApi";
|
||||
import {
|
||||
AccommodationReservation,
|
||||
AccommodationsReservationsApi,
|
||||
AccommodationsReservationsList,
|
||||
ValidateResaResult,
|
||||
} from "../../../api/accommodations/AccommodationsReservationsApi";
|
||||
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
|
||||
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
|
||||
import { useUpdateAccommodationReservation } from "../../../hooks/context_providers/accommodations/UpdateReservationDialogProvider";
|
||||
import {
|
||||
fmtUnixDate,
|
||||
fmtUnixDateFullCalendar,
|
||||
} from "../../../utils/time_utils";
|
||||
import { AsyncWidget } from "../../../widgets/AsyncWidget";
|
||||
import { useUser } from "../../../widgets/BaseAuthenticatedPage";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
||||
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
|
||||
|
||||
export function AccommodationsReservationsRoute(): React.ReactElement {
|
||||
const snackbar = useSnackbar();
|
||||
const alert = useAlert();
|
||||
const confirm = useConfirm();
|
||||
const loadingMessage = useLoadingMessage();
|
||||
|
||||
const loadKey = React.useRef(1);
|
||||
|
||||
const user = useUser();
|
||||
const family = useFamily();
|
||||
const accommodations = useAccommodations();
|
||||
const updateReservation = useUpdateAccommodationReservation();
|
||||
|
||||
const [reservations, setReservations] = React.useState<
|
||||
AccommodationsReservationsList | undefined
|
||||
>();
|
||||
const [users, setUsers] = React.useState<FamilyUser[] | null>(null);
|
||||
|
||||
const [showValidated, setShowValidated] = React.useState(true);
|
||||
const [showRejected, setShowRejected] = React.useState(true);
|
||||
const [showPending, setShowPending] = React.useState(true);
|
||||
|
||||
const [hiddenPeople, setHiddenPeople] = React.useState<Set<number>>(
|
||||
new Set()
|
||||
);
|
||||
const [hiddenAccommodations, setHiddenAccommodations] = React.useState<
|
||||
Set<number>
|
||||
>(new Set());
|
||||
|
||||
const eventPopupAnchor = React.useRef<HTMLDivElement>(null);
|
||||
const [activeEvent, setActiveEvent] = React.useState<
|
||||
| undefined
|
||||
| {
|
||||
user: FamilyUser;
|
||||
accommodation: Accommodation;
|
||||
reservation: AccommodationReservation;
|
||||
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
>();
|
||||
|
||||
const [validateResaAnchorEl, setValidateResaAnchorEl] =
|
||||
React.useState<null | HTMLElement>(null);
|
||||
|
||||
const load = async () => {
|
||||
setReservations(
|
||||
await AccommodationsReservationsApi.FullListOfFamily(family.family)
|
||||
);
|
||||
setUsers(await FamilyApi.GetUsersList(family.family.family_id));
|
||||
};
|
||||
|
||||
const reload = async () => {
|
||||
loadKey.current += 1;
|
||||
setUsers(null);
|
||||
};
|
||||
|
||||
const visibleReservations = React.useMemo(() => {
|
||||
return reservations?.filter((r) => {
|
||||
if (!showValidated && r.validated === true) return false;
|
||||
if (!showPending && r.validated === null) return false;
|
||||
if (!showRejected && r.validated === false) return false;
|
||||
if (hiddenPeople.has(r.user_id)) return false;
|
||||
if (hiddenAccommodations.has(r.accommodation_id)) return false;
|
||||
return true;
|
||||
});
|
||||
}, [
|
||||
showValidated,
|
||||
showRejected,
|
||||
showPending,
|
||||
hiddenPeople,
|
||||
hiddenAccommodations,
|
||||
reservations,
|
||||
]);
|
||||
|
||||
const onSelect = async (d: DateSelectArg) => {
|
||||
try {
|
||||
const resa = await updateReservation(
|
||||
{
|
||||
accommodation_id: -1,
|
||||
start: Math.floor(d.start.getTime() / 1000),
|
||||
end: Math.floor(d.end.getTime() / 1000),
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
if (!resa) return;
|
||||
|
||||
loadingMessage.show("Création de la réservation en cours...");
|
||||
|
||||
await AccommodationsReservationsApi.Create(family.family, resa);
|
||||
|
||||
reload();
|
||||
snackbar("La réservation a été créée avec succès !");
|
||||
} catch (e) {
|
||||
console.error("Failed to create a reservation!", e);
|
||||
alert("Échec de la création de la réservation!");
|
||||
} finally {
|
||||
loadingMessage.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const onEventClick = (ev: EventClickArg) => {
|
||||
const id: number = ev.event.extendedProps.id;
|
||||
const resa = reservations?.get(id)!;
|
||||
const acc = accommodations.accommodations.get(resa.accommodation_id)!;
|
||||
|
||||
const user = users?.find((u) => u.user_id === resa.user_id);
|
||||
|
||||
if (!user) {
|
||||
console.error(`User ${resa.user_id} not found!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const loc = ev.el.getBoundingClientRect();
|
||||
setActiveEvent({
|
||||
reservation: resa,
|
||||
accommodation: acc,
|
||||
user: user,
|
||||
|
||||
x: loc.left,
|
||||
y: loc.top,
|
||||
w: loc.width,
|
||||
h: loc.height,
|
||||
});
|
||||
};
|
||||
|
||||
const respondToResaRequest = async (
|
||||
r: AccommodationReservation,
|
||||
validate: boolean
|
||||
) => {
|
||||
try {
|
||||
loadingMessage.show("Validation de la réservation en cours...");
|
||||
|
||||
setValidateResaAnchorEl(null);
|
||||
setActiveEvent(undefined);
|
||||
|
||||
const res = await AccommodationsReservationsApi.Validate(r, validate);
|
||||
|
||||
if (res === ValidateResaResult.Conflict) {
|
||||
throw new Error(
|
||||
"The reservation is in conflict with other reservations!"
|
||||
);
|
||||
} else if (res === ValidateResaResult.Error) {
|
||||
throw new Error("Failed to validate the reservation!");
|
||||
}
|
||||
|
||||
reload();
|
||||
snackbar("La réservation a été mise à jour avec succès !");
|
||||
} catch (e) {
|
||||
console.error("Failed to respond to reservation request!", e);
|
||||
alert(`Echec de l'enregistrement de la réponse à la réservation ! ${e}`);
|
||||
} finally {
|
||||
loadingMessage.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const validateReservation = async (r: AccommodationReservation) => {
|
||||
respondToResaRequest(r, true);
|
||||
};
|
||||
|
||||
const rejectReservation = async (r: AccommodationReservation) => {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment rejeter cette demande de réservation ?"
|
||||
))
|
||||
)
|
||||
return;
|
||||
respondToResaRequest(r, false);
|
||||
};
|
||||
|
||||
const changeReservation = async (r: AccommodationReservation) => {
|
||||
try {
|
||||
const ac = accommodations.accommodations.get(r.accommodation_id);
|
||||
if (
|
||||
ac?.need_validation &&
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment changer cette réservation ? Celle-ci devra être de nouveau validée !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
const newResa = await updateReservation(
|
||||
{
|
||||
reservation_id: r.id,
|
||||
accommodation_id: r.accommodation_id,
|
||||
start: r.reservation_start,
|
||||
end: r.reservation_end,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (!newResa) return;
|
||||
|
||||
setActiveEvent(undefined);
|
||||
loadingMessage.show("Mise à jour de la réservation en cours...");
|
||||
|
||||
await AccommodationsReservationsApi.Update(family.family, newResa);
|
||||
|
||||
reload();
|
||||
snackbar("La réservation a été mise à jour avec succès !");
|
||||
} catch (e) {
|
||||
console.error("Failed to update a reservation!", e);
|
||||
alert("Échec de la mise à jour de la réservation!");
|
||||
} finally {
|
||||
loadingMessage.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteReservation = async (r: AccommodationReservation) => {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment supprimer cette réservation ? L'opération n'est pas réversible !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
setActiveEvent(undefined);
|
||||
loadingMessage.show("Suppression de la réservation en cours...");
|
||||
|
||||
await AccommodationsReservationsApi.Delete(r);
|
||||
|
||||
reload();
|
||||
snackbar("La réservation a été supprimée avec succès !");
|
||||
} catch (e) {
|
||||
console.error("Failed to delete a reservation!", e);
|
||||
alert("Échec de la suppression de la réservation!");
|
||||
} finally {
|
||||
loadingMessage.hide();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FamilyPageTitle title="Réservations" />
|
||||
<AsyncWidget
|
||||
loadKey={loadKey.current}
|
||||
load={load}
|
||||
errMsg="Echec du chargement de la liste des réservations !"
|
||||
build={() => (
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<div style={{ flex: 1, maxWidth: "250px", marginRight: "20px" }}>
|
||||
<Alert severity="info">
|
||||
Cliquez sur le calendrier pour créer une réservation.
|
||||
</Alert>
|
||||
|
||||
{/* Invitation status */}
|
||||
<FormControl
|
||||
sx={{ m: 3 }}
|
||||
component="fieldset"
|
||||
variant="standard"
|
||||
>
|
||||
<FormLabel component="legend">Status</FormLabel>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={showValidated}
|
||||
onChange={(_ev, v) => setShowValidated(v)}
|
||||
color="success"
|
||||
/>
|
||||
}
|
||||
label="Validées"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={showRejected}
|
||||
onChange={(_ev, v) => setShowRejected(v)}
|
||||
color="error"
|
||||
/>
|
||||
}
|
||||
label="Rejetées"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={showPending}
|
||||
onChange={(_ev, v) => setShowPending(v)}
|
||||
color="info"
|
||||
/>
|
||||
}
|
||||
label="En attente de validation"
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
|
||||
{/* Accommodations */}
|
||||
<FormControl
|
||||
sx={{ m: 3 }}
|
||||
component="fieldset"
|
||||
variant="standard"
|
||||
>
|
||||
<FormLabel component="legend">Logements</FormLabel>
|
||||
<FormGroup>
|
||||
{accommodations.accommodations.fullList.map((a) => (
|
||||
<FormControlLabel
|
||||
key={a.id}
|
||||
control={
|
||||
<Checkbox
|
||||
sx={{
|
||||
color: "#" + a.color,
|
||||
"&.Mui-checked": {
|
||||
color: "#" + a.color,
|
||||
},
|
||||
}}
|
||||
checked={!hiddenAccommodations.has(a.id)}
|
||||
onChange={(_ev, v) => {
|
||||
if (v) hiddenAccommodations.delete(a.id);
|
||||
else hiddenAccommodations.add(a.id);
|
||||
setHiddenAccommodations(
|
||||
new Set(hiddenAccommodations)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={a.name}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
|
||||
{/* People */}
|
||||
<FormControl
|
||||
sx={{ m: 3 }}
|
||||
component="fieldset"
|
||||
variant="standard"
|
||||
>
|
||||
<FormLabel component="legend">Personnes</FormLabel>
|
||||
<FormGroup>
|
||||
{users?.map((u) => (
|
||||
<FormControlLabel
|
||||
key={u.user_id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={!hiddenPeople.has(u.user_id)}
|
||||
onChange={(_ev, v) => {
|
||||
if (v) hiddenPeople.delete(u.user_id);
|
||||
else hiddenPeople.add(u.user_id);
|
||||
setHiddenPeople(new Set(hiddenPeople));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={u.user_name}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
{/* The calendar */}
|
||||
<div style={{ flex: 5 }}>
|
||||
<FullCalendar
|
||||
editable={true}
|
||||
selectable={true}
|
||||
plugins={[dayGridPlugin, listPlugin, interactionPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
height="700px"
|
||||
locale={frLocale}
|
||||
headerToolbar={{
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: "dayGridMonth,dayGridWeek,dayGridDay,listWeek",
|
||||
}}
|
||||
select={onSelect}
|
||||
eventClick={onEventClick}
|
||||
events={visibleReservations?.map((r) => {
|
||||
const a = accommodations.accommodations.get(
|
||||
r.accommodation_id
|
||||
)!;
|
||||
const u = users?.find((u) => u.user_id === r.user_id);
|
||||
return {
|
||||
title: `${u?.user_name} - ${a.name}`,
|
||||
start: fmtUnixDateFullCalendar(r.reservation_start, false),
|
||||
end: fmtUnixDateFullCalendar(r.reservation_end, true),
|
||||
allDay: true,
|
||||
color: a.color ? "#" + a.color : undefined,
|
||||
borderColor:
|
||||
r.validated === true
|
||||
? "green"
|
||||
: r.validated === false
|
||||
? "red"
|
||||
: "grey ",
|
||||
extendedProps: {
|
||||
id: r.id,
|
||||
},
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Calendar event popover */}
|
||||
<div
|
||||
ref={eventPopupAnchor}
|
||||
id="active-event-anchor"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: activeEvent?.y + "px",
|
||||
left: activeEvent?.x + "px",
|
||||
width: activeEvent?.w + "px",
|
||||
height: activeEvent?.h + "px",
|
||||
backgroundColor: "pink",
|
||||
zIndex: 0,
|
||||
}}
|
||||
></div>
|
||||
<Popover
|
||||
open={activeEvent !== undefined}
|
||||
anchorEl={eventPopupAnchor.current}
|
||||
onClose={() => {
|
||||
setActiveEvent(undefined);
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<Card sx={{ maxWidth: 345 }} elevation={6}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar sx={{ bgcolor: red[500] }}>
|
||||
{activeEvent?.user.user_name
|
||||
.substring(0, 1)
|
||||
.toLocaleUpperCase()}
|
||||
</Avatar>
|
||||
}
|
||||
title={activeEvent?.user.user_name}
|
||||
subheader={activeEvent?.user.user_mail}
|
||||
/>
|
||||
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<p>
|
||||
Réservation de {activeEvent?.accommodation.name}
|
||||
<br />
|
||||
<em>{activeEvent?.accommodation.description}</em>
|
||||
</p>
|
||||
<p>
|
||||
Du{" "}
|
||||
{fmtUnixDate(
|
||||
activeEvent?.reservation.reservation_start ?? 0
|
||||
)}{" "}
|
||||
<br />
|
||||
Au{" "}
|
||||
{fmtUnixDate(
|
||||
activeEvent?.reservation.reservation_end ?? 0
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
{activeEvent?.reservation.validated === false ? (
|
||||
<span style={{ color: "#f44336" }}>Refusée</span>
|
||||
) : activeEvent?.reservation.validated === true ? (
|
||||
<span style={{ color: "#66bb6a" }}>Validée</span>
|
||||
) : (
|
||||
<span style={{ color: "#29b6f6" }}>
|
||||
En attente de validation
|
||||
</span>
|
||||
)}
|
||||
</strong>
|
||||
</p>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions disableSpacing>
|
||||
{activeEvent?.accommodation.need_validation &&
|
||||
family.family.is_admin && (
|
||||
<>
|
||||
<Tooltip
|
||||
title="Valider (ou rejeter) la réservation"
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
onClick={(e) =>
|
||||
setValidateResaAnchorEl(e.currentTarget)
|
||||
}
|
||||
>
|
||||
<RuleIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
anchorEl={validateResaAnchorEl}
|
||||
open={!!validateResaAnchorEl && !!activeEvent}
|
||||
onClose={() => setValidateResaAnchorEl(null)}
|
||||
>
|
||||
<MenuItem
|
||||
disabled={
|
||||
activeEvent.reservation.validated === true
|
||||
}
|
||||
onClick={() =>
|
||||
validateReservation(activeEvent.reservation)
|
||||
}
|
||||
>
|
||||
Valider
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={
|
||||
activeEvent.reservation.validated === false
|
||||
}
|
||||
onClick={() =>
|
||||
rejectReservation(activeEvent.reservation)
|
||||
}
|
||||
>
|
||||
Rejeter
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
|
||||
{user.user.id === activeEvent?.reservation.user_id && (
|
||||
<>
|
||||
<Tooltip title="Modifier les dates de réservation" arrow>
|
||||
<IconButton
|
||||
disabled={
|
||||
!activeEvent.accommodation.open_to_reservations
|
||||
}
|
||||
onClick={() =>
|
||||
changeReservation(activeEvent?.reservation)
|
||||
}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Supprimer la réservation" arrow>
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() =>
|
||||
deleteReservation(activeEvent?.reservation)
|
||||
}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import HouseIcon from "@mui/icons-material/House";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import {
|
||||
Accommodation,
|
||||
AccommodationListApi,
|
||||
} from "../../../api/accommodations/AccommodationListApi";
|
||||
import {
|
||||
AccommodationCalendarURL,
|
||||
AccommodationsCalendarURLApi,
|
||||
} from "../../../api/accommodations/AccommodationsCalendarURLApi";
|
||||
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
|
||||
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
|
||||
import { useCreateAccommodationCalendarURL } from "../../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider";
|
||||
import { useInstallCalendarDialog } from "../../../hooks/context_providers/accommodations/InstallCalendarDialogProvider";
|
||||
import { useUpdateAccommodation } from "../../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
|
||||
import { AsyncWidget } from "../../../widgets/AsyncWidget";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { FamilyCard } from "../../../widgets/FamilyCard";
|
||||
import { TimeWidget } from "../../../widgets/TimeWidget";
|
||||
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
|
||||
|
||||
const CARDS_WIDTH = "500px";
|
||||
|
||||
export function AccommodationsSettingsRoute(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<AccommodationsListCard />
|
||||
<AccommodationsCalURLsCard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AccommodationsListCard(): React.ReactElement {
|
||||
const loading = useLoadingMessage();
|
||||
const confirm = useConfirm();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const family = useFamily();
|
||||
const accommodations = useAccommodations();
|
||||
|
||||
const [error, setError] = React.useState<string>();
|
||||
const [success, setSuccess] = React.useState<string>();
|
||||
|
||||
const updateAccommodation = useUpdateAccommodation();
|
||||
|
||||
const createAccommodation = async () => {
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
try {
|
||||
const accommodation = await updateAccommodation(
|
||||
{
|
||||
name: "",
|
||||
open_to_reservations: true,
|
||||
need_validation: false,
|
||||
color: "2196f3",
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
if (!accommodation) return;
|
||||
|
||||
loading.show("Création du logement en cours...");
|
||||
|
||||
await AccommodationListApi.Create(family.family, accommodation);
|
||||
|
||||
snackbar("Le logement a été créé avec succès !");
|
||||
await accommodations.reloadAccommodationsList();
|
||||
} catch (e) {
|
||||
console.error("Failed to create accommodation!", e);
|
||||
setError(`Échec de la création du logement! ${e}`);
|
||||
} finally {
|
||||
loading.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const requestUpdateAccommodation = async (a: Accommodation) => {
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
try {
|
||||
const update = await updateAccommodation(a, false);
|
||||
if (!update) return;
|
||||
|
||||
loading.show("Mise à jour du logement en cours...");
|
||||
|
||||
await AccommodationListApi.Update(a, update);
|
||||
|
||||
snackbar("Le logement a été créé avec succès !");
|
||||
await accommodations.reloadAccommodationsList();
|
||||
} catch (e) {
|
||||
console.error("Failed to update accommodation!", e);
|
||||
setError(`Échec de la mise à jour du logement! ${e}`);
|
||||
} finally {
|
||||
loading.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAccommodation = async (a: Accommodation) => {
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
`Voulez-vous vraiment supprimer le logement '${a.name}' ? Cette opération est définitive !`
|
||||
))
|
||||
)
|
||||
return;
|
||||
loading.show("Suppression du logement en cours...");
|
||||
|
||||
await AccommodationListApi.Delete(a);
|
||||
|
||||
snackbar("Le logement a été supprimé avec succès !");
|
||||
await accommodations.reloadAccommodationsList();
|
||||
} catch (e) {
|
||||
console.error("Failed to delete accommodation!", e);
|
||||
setError(`Échec de la suppression du logement! ${e}`);
|
||||
} finally {
|
||||
loading.hide();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FamilyCard error={error} success={success} style={{ width: CARDS_WIDTH }}>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="div">
|
||||
Logements
|
||||
</Typography>
|
||||
|
||||
{/* Display the list of accommodations */}
|
||||
{accommodations.accommodations.isEmpty && (
|
||||
<div style={{ textAlign: "center", margin: "25px" }}>
|
||||
Aucun logement enregistré pour le moment !
|
||||
</div>
|
||||
)}
|
||||
{accommodations.accommodations.fullList.map((a) => (
|
||||
<AccommodationCard
|
||||
accommodation={a}
|
||||
onRequestUpdate={requestUpdateAccommodation}
|
||||
onRequestDelete={deleteAccommodation}
|
||||
/>
|
||||
))}
|
||||
|
||||
{family.family.is_admin && (
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
fullWidth
|
||||
onClick={createAccommodation}
|
||||
size={"large"}
|
||||
>
|
||||
Ajouter un logement
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</FamilyCard>
|
||||
);
|
||||
}
|
||||
|
||||
function AccommodationCard(p: {
|
||||
accommodation: Accommodation;
|
||||
onRequestUpdate: (a: Accommodation) => void;
|
||||
onRequestDelete: (a: Accommodation) => void;
|
||||
}): React.ReactElement {
|
||||
const family = useFamily();
|
||||
return (
|
||||
<Card sx={{ minWidth: 275, margin: "10px 0px" }} variant="outlined">
|
||||
<CardContent>
|
||||
<Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
|
||||
Mis à jour il y a <TimeWidget time={p.accommodation.time_update} />
|
||||
</Typography>
|
||||
<Typography variant="h5" component="div">
|
||||
<HouseIcon sx={{ color: "#" + p.accommodation.color }} />{" "}
|
||||
{p.accommodation.name}
|
||||
</Typography>
|
||||
<Typography sx={{ mb: 1.5 }} color="text.secondary">
|
||||
{p.accommodation.description}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<BoolIcon checked={p.accommodation.open_to_reservations} /> Ouvert aux
|
||||
réservations
|
||||
<br />
|
||||
<BoolIcon checked={!p.accommodation.need_validation} /> Réservation
|
||||
sans validation d'un administrateur
|
||||
</Typography>
|
||||
</CardContent>
|
||||
{family.family.is_admin && (
|
||||
<CardActions>
|
||||
<span style={{ flex: 1 }}></span>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => p.onRequestUpdate(p.accommodation)}
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => p.onRequestDelete(p.accommodation)}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</CardActions>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function BoolIcon(p: { checked?: boolean }): React.ReactElement {
|
||||
return p.checked ? (
|
||||
<CheckIcon color="success" />
|
||||
) : (
|
||||
<CloseIcon color="error" />
|
||||
);
|
||||
}
|
||||
|
||||
function AccommodationsCalURLsCard(): React.ReactElement {
|
||||
const key = React.useRef(0);
|
||||
|
||||
const confirm = useConfirm();
|
||||
const loading = useLoadingMessage();
|
||||
|
||||
const [error, setError] = React.useState<string>();
|
||||
const [success, setSuccess] = React.useState<string>();
|
||||
|
||||
const [list, setList] = React.useState<
|
||||
AccommodationCalendarURL[] | undefined
|
||||
>();
|
||||
|
||||
const family = useFamily();
|
||||
|
||||
const createCalendarURLDialog = useCreateAccommodationCalendarURL();
|
||||
const calendarURLDialog = useInstallCalendarDialog();
|
||||
|
||||
const load = async () => {
|
||||
setList(await AccommodationsCalendarURLApi.GetList(family.family));
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
key.current += 1;
|
||||
setList(undefined);
|
||||
};
|
||||
|
||||
const onRequestDelete = async (c: AccommodationCalendarURL) => {
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
`Voulez-vous vraiment supprimer le calendrier '${c.name}' ? Cette opération est définitive !`
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
loading.show("Suppression du calendrier en cours...");
|
||||
|
||||
await AccommodationsCalendarURLApi.Delete(c);
|
||||
|
||||
setSuccess("Le calendrier a été supprimé avec succès !");
|
||||
reload();
|
||||
} catch (e) {
|
||||
console.error("Failed to delete accommodation!", e);
|
||||
setError(`Échec de la suppression du logement! ${e}`);
|
||||
} finally {
|
||||
loading.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const createCalendarURL = async () => {
|
||||
try {
|
||||
const newCal = await createCalendarURLDialog();
|
||||
|
||||
if (!newCal) return;
|
||||
|
||||
loading.show("Création du calendrier en cours...");
|
||||
|
||||
const cal = await AccommodationsCalendarURLApi.Create(
|
||||
family.family,
|
||||
newCal
|
||||
);
|
||||
|
||||
setSuccess("Le calendrier a été créé avec succès !");
|
||||
|
||||
reload();
|
||||
|
||||
calendarURLDialog(cal);
|
||||
} catch (e) {
|
||||
console.error("Failed to create new accommodation calendar URL!", e);
|
||||
setError(`Échec de la création du calendrier! ${e}`);
|
||||
} finally {
|
||||
loading.hide();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FamilyCard error={error} success={success} style={{ width: CARDS_WIDTH }}>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="div">
|
||||
URL de calendriers
|
||||
</Typography>
|
||||
<Typography>
|
||||
Vous pouvez, si vous le souhaitez, importer dans votre application de
|
||||
calendrier le planning de réservation des logements. Pour ce faire, il
|
||||
vous suffit de créer une URL de calendrier.
|
||||
</Typography>
|
||||
|
||||
<Alert severity="info">
|
||||
Les calendriers créés ici ne sont visible que par vous. Vous ne pouvez
|
||||
pas manipuler les calendriers créés par les autres membres de la
|
||||
famille.
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
fullWidth
|
||||
onClick={createCalendarURL}
|
||||
size={"large"}
|
||||
>
|
||||
Créer un calendrier
|
||||
</Button>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<AsyncWidget
|
||||
ready={list !== undefined}
|
||||
loadKey={key.current}
|
||||
load={load}
|
||||
errMsg="Echec du chargement de la liste des calendriers !"
|
||||
build={() =>
|
||||
list?.length === 0 ? (
|
||||
<>
|
||||
<p style={{ textAlign: "center" }}>
|
||||
Vous n'avez créé aucun calendrier pour le moment !
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{list?.map((c) => (
|
||||
<CalendarItem c={c} onRequestDelete={onRequestDelete} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</FamilyCard>
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarItem(p: {
|
||||
c: AccommodationCalendarURL;
|
||||
onRequestDelete: (c: AccommodationCalendarURL) => void;
|
||||
}): React.ReactElement {
|
||||
const accommodations = useAccommodations();
|
||||
|
||||
const installCal = useInstallCalendarDialog();
|
||||
|
||||
return (
|
||||
<Card sx={{ minWidth: 275, margin: "10px 0px" }} variant="outlined">
|
||||
<CardContent>
|
||||
<Typography
|
||||
sx={{ fontSize: 14 }}
|
||||
color="text.secondary"
|
||||
gutterBottom
|
||||
></Typography>
|
||||
<Typography variant="h5" component="div">
|
||||
{p.c.name}
|
||||
</Typography>
|
||||
<Typography sx={{ mb: 1.5 }} color="text.secondary">
|
||||
{p.c.accommodation_id
|
||||
? accommodations.accommodations.get(p.c.accommodation_id)?.name
|
||||
: "Tous les logements"}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Créé il y a <TimeWidget time={p.c.time_create} />
|
||||
<br />
|
||||
Utilisé il y a <TimeWidget time={p.c.time_used} />
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardActions>
|
||||
<span style={{ flex: 1 }}></span>
|
||||
<Button size="small" onClick={() => installCal(p.c)}>
|
||||
Installer
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => p.onRequestDelete(p.c)}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -6,25 +6,27 @@ import SaveIcon from "@mui/icons-material/Save";
|
||||
import { Button, Grid, Stack } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Couple, CoupleApi } from "../../api/CoupleApi";
|
||||
import { Member } from "../../api/MemberApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||
import { AsyncWidget } from "../../widgets/AsyncWidget";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog";
|
||||
import { CouplePhoto } from "../../widgets/CouplePhoto";
|
||||
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle";
|
||||
import { MemberItem } from "../../widgets/MemberItem";
|
||||
import { PropertiesBox } from "../../widgets/PropertiesBox";
|
||||
import { RouterLink } from "../../widgets/RouterLink";
|
||||
import { DateInput } from "../../widgets/forms/DateInput";
|
||||
import { MemberInput } from "../../widgets/forms/MemberInput";
|
||||
import { PropSelect } from "../../widgets/forms/PropSelect";
|
||||
import { UploadPhotoButton } from "../../widgets/forms/UploadPhotoButton";
|
||||
import { useQuery } from "../../hooks/useQuery";
|
||||
import { ServerApi } from "../../../api/ServerApi";
|
||||
import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi";
|
||||
import { Member } from "../../../api/genealogy/MemberApi";
|
||||
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
|
||||
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
|
||||
import { useQuery } from "../../../hooks/useQuery";
|
||||
import { AsyncWidget } from "../../../widgets/AsyncWidget";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog";
|
||||
import { CouplePhoto } from "../../../widgets/CouplePhoto";
|
||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
||||
import { MemberItem } from "../../../widgets/MemberItem";
|
||||
import { PropertiesBox } from "../../../widgets/PropertiesBox";
|
||||
import { RouterLink } from "../../../widgets/RouterLink";
|
||||
import { DateInput } from "../../../widgets/forms/DateInput";
|
||||
import { MemberInput } from "../../../widgets/forms/MemberInput";
|
||||
import { PropSelect } from "../../../widgets/forms/PropSelect";
|
||||
import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton";
|
||||
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
|
||||
|
||||
/**
|
||||
* Create a new couple route
|
||||
@@ -35,6 +37,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
|
||||
|
||||
const [shouldQuit, setShouldQuit] = React.useState(false);
|
||||
const n = useNavigate();
|
||||
const genealogy = useGenealogy();
|
||||
const family = useFamily();
|
||||
|
||||
const params = useQuery();
|
||||
@@ -48,7 +51,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
|
||||
try {
|
||||
const r = await CoupleApi.Create(m);
|
||||
|
||||
await family.reloadCouplesList();
|
||||
await genealogy.reloadCouplesList();
|
||||
|
||||
setShouldQuit(true);
|
||||
n(family.family.coupleURL(r));
|
||||
@@ -61,7 +64,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
|
||||
|
||||
const cancel = () => {
|
||||
setShouldQuit(true);
|
||||
n(family.family.URL("couples"));
|
||||
n(family.family.URL("genealogy/couples"));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -88,6 +91,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
const { coupleId } = useParams();
|
||||
|
||||
const [couple, setCouple] = React.useState<Couple>();
|
||||
@@ -99,7 +103,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
|
||||
count.current += 1;
|
||||
setCouple(undefined);
|
||||
|
||||
await family.reloadCouplesList();
|
||||
await genealogy.reloadCouplesList();
|
||||
};
|
||||
|
||||
const deleteCouple = async () => {
|
||||
@@ -114,9 +118,9 @@ export function FamilyCoupleRoute(): React.ReactElement {
|
||||
await CoupleApi.Delete(couple!);
|
||||
|
||||
snackbar("La fiche du couple a été supprimée avec succès !");
|
||||
n(family.family.URL("couples"));
|
||||
n(family.family.URL("genealogy/couples"));
|
||||
|
||||
await family.reloadCouplesList();
|
||||
await genealogy.reloadCouplesList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Échec de la suppression du couple !");
|
||||
@@ -132,7 +136,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
|
||||
build={() => (
|
||||
<CouplePage
|
||||
couple={couple!}
|
||||
children={family.members.childrenOfCouple(couple!)}
|
||||
children={genealogy.members.childrenOfCouple(couple!)}
|
||||
creating={false}
|
||||
editing={false}
|
||||
onRequestDelete={deleteCouple}
|
||||
@@ -156,6 +160,7 @@ export function FamilyEditCoupleRoute(): React.ReactElement {
|
||||
|
||||
const [shouldQuit, setShouldQuit] = React.useState(false);
|
||||
|
||||
const genealogy = useGenealogy();
|
||||
const family = useFamily();
|
||||
|
||||
const [couple, setCouple] = React.useState<Couple>();
|
||||
@@ -165,7 +170,8 @@ export function FamilyEditCoupleRoute(): React.ReactElement {
|
||||
|
||||
const cancel = () => {
|
||||
setShouldQuit(true);
|
||||
n(-1);
|
||||
n(family.family.coupleURL(couple!));
|
||||
//n(-1);
|
||||
};
|
||||
|
||||
const save = async (c: Couple) => {
|
||||
@@ -174,7 +180,7 @@ export function FamilyEditCoupleRoute(): React.ReactElement {
|
||||
|
||||
snackbar("Les informations du couple ont été mises à jour avec succès !");
|
||||
|
||||
await family.reloadCouplesList();
|
||||
await genealogy.reloadCouplesList();
|
||||
|
||||
setShouldQuit(true);
|
||||
n(family.family.coupleURL(c, false));
|
||||
@@ -210,13 +216,14 @@ export function CouplePage(p: {
|
||||
shouldAllowLeaving?: boolean;
|
||||
children?: Member[];
|
||||
onCancel?: () => void;
|
||||
onSave?: (m: Couple) => void;
|
||||
onSave?: (m: Couple) => Promise<void>;
|
||||
onRequestEdit?: () => void;
|
||||
onRequestDelete?: () => void;
|
||||
onForceReload?: () => void;
|
||||
}): React.ReactElement {
|
||||
const confirm = useConfirm();
|
||||
const snackbar = useSnackbar();
|
||||
const loadingMessage = useLoadingMessage();
|
||||
|
||||
const family = useFamily();
|
||||
|
||||
@@ -230,8 +237,12 @@ export function CouplePage(p: {
|
||||
setCouple(new Couple(structuredClone(couple)));
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
p.onSave!(couple);
|
||||
const save = async () => {
|
||||
loadingMessage.show(
|
||||
"Enregistrement des informations du couple en cours..."
|
||||
);
|
||||
await p.onSave!(couple);
|
||||
loadingMessage.hide();
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
@@ -479,7 +490,7 @@ export function CouplePage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`member/create?mother=${couple.wife}&father=${couple.husband}`
|
||||
`genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
@@ -6,17 +6,18 @@ import { Button, TextField, Tooltip } from "@mui/material";
|
||||
import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Couple, CoupleApi } from "../../api/CoupleApi";
|
||||
import { dateTimestamp, fmtDate } from "../../api/MemberApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { CouplePhoto } from "../../widgets/CouplePhoto";
|
||||
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle";
|
||||
import { MemberPhoto } from "../../widgets/MemberPhoto";
|
||||
import { RouterLink } from "../../widgets/RouterLink";
|
||||
import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi";
|
||||
import { dateTimestamp, fmtDate } from "../../../api/genealogy/MemberApi";
|
||||
import { ServerApi } from "../../../api/ServerApi";
|
||||
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { CouplePhoto } from "../../../widgets/CouplePhoto";
|
||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
||||
import { MemberPhoto } from "../../../widgets/MemberPhoto";
|
||||
import { RouterLink } from "../../../widgets/RouterLink";
|
||||
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
|
||||
|
||||
export function FamilyCouplesListRoute(): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
@@ -24,6 +25,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
|
||||
const [filter, setFilter] = React.useState("");
|
||||
|
||||
@@ -37,7 +39,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
|
||||
return;
|
||||
|
||||
await CoupleApi.Delete(c);
|
||||
await family.reloadCouplesList();
|
||||
await genealogy.reloadCouplesList();
|
||||
|
||||
snackbar("La fiche du couple a été supprimée avec succès !");
|
||||
} catch (e) {
|
||||
@@ -63,7 +65,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
{family.couples.isEmpty ? (
|
||||
{genealogy.couples.isEmpty ? (
|
||||
<p>
|
||||
Votre famille n'a aucun couple enregistré pour le moment ! Utilisez le
|
||||
bouton situé en haut à droite pour créer le premier !
|
||||
@@ -81,16 +83,16 @@ export function FamilyCouplesListRoute(): React.ReactElement {
|
||||
<CouplesTable
|
||||
couples={
|
||||
filter === ""
|
||||
? family.couples.fullList
|
||||
: family.couples.filter(
|
||||
? genealogy.couples.fullList
|
||||
: genealogy.couples.filter(
|
||||
(m) =>
|
||||
(m.wife &&
|
||||
family.members
|
||||
genealogy.members
|
||||
.get(m.wife)!
|
||||
.fullName.toLocaleLowerCase()
|
||||
.includes(filter.toLocaleLowerCase())) ||
|
||||
(m.husband &&
|
||||
family.members
|
||||
genealogy.members
|
||||
.get(m.husband)!
|
||||
.fullName.toLocaleLowerCase()
|
||||
.includes(filter.toLocaleLowerCase())) === true
|
||||
@@ -109,14 +111,18 @@ function CouplesTable(p: {
|
||||
onDelete: (m: Couple) => void;
|
||||
}): React.ReactElement {
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
|
||||
const n = useNavigate();
|
||||
|
||||
const compareInvertedMembersNames = (
|
||||
v1: number | undefined,
|
||||
v2: number | undefined
|
||||
) => {
|
||||
const n1 = ((v1 && family.members.get(v1)?.invertedFullName) ?? "") || "";
|
||||
const n2 = ((v2 && family.members.get(v2)?.invertedFullName) ?? "") || "";
|
||||
const n1 =
|
||||
((v1 && genealogy.members.get(v1)?.invertedFullName) ?? "") || "";
|
||||
const n2 =
|
||||
((v2 && genealogy.members.get(v2)?.invertedFullName) ?? "") || "";
|
||||
|
||||
return n1?.localeCompare(n2, undefined, {
|
||||
ignorePunctuation: true,
|
||||
@@ -132,7 +138,13 @@ function CouplesTable(p: {
|
||||
sortable: false,
|
||||
width: 60,
|
||||
renderCell(params) {
|
||||
return <CouplePhoto couple={params.row} />;
|
||||
return (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", height: "100%" }}
|
||||
>
|
||||
<CouplePhoto couple={params.row} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -253,10 +265,10 @@ function CouplesTable(p: {
|
||||
}
|
||||
|
||||
function MemberCell(p: { id?: number }): React.ReactElement {
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
if (!p.id) return <></>;
|
||||
|
||||
const member = family.members.get(p.id!)!;
|
||||
const member = genealogy.members.get(p.id!)!;
|
||||
|
||||
return (
|
||||
<Tooltip title="Double-cliquez ici pour accéder à la fiche du membre">
|
||||
17
geneit_app/src/routes/family/genealogy/FamilyHomeRoute.tsx
Normal file
17
geneit_app/src/routes/family/genealogy/FamilyHomeRoute.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
||||
|
||||
export function FamilyHomeRoute(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<FamilyPageTitle title="Votre famille" />
|
||||
<div style={{ margin: "20px" }}>
|
||||
<p>
|
||||
Bienvenue sur l'espace informatique dédié à la vie de votre famille !
|
||||
Veuillez utiliser le menu situé à gauche pour accéder aux différentes
|
||||
sections de l'application.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { mdiFamilyTree } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
@@ -14,31 +16,31 @@ import {
|
||||
import * as EmailValidator from "email-validator";
|
||||
import React from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Couple } from "../../api/CoupleApi";
|
||||
import { Member, MemberApi, fmtDate } from "../../api/MemberApi";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||
import { AsyncWidget } from "../../widgets/AsyncWidget";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog";
|
||||
import { CouplePhoto } from "../../widgets/CouplePhoto";
|
||||
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle";
|
||||
import { MemberItem } from "../../widgets/MemberItem";
|
||||
import { MemberPhoto } from "../../widgets/MemberPhoto";
|
||||
import { PropertiesBox } from "../../widgets/PropertiesBox";
|
||||
import { RouterLink } from "../../widgets/RouterLink";
|
||||
import { DateInput } from "../../widgets/forms/DateInput";
|
||||
import { MemberInput } from "../../widgets/forms/MemberInput";
|
||||
import { PropCheckbox } from "../../widgets/forms/PropCheckbox";
|
||||
import { PropEdit } from "../../widgets/forms/PropEdit";
|
||||
import { PropSelect } from "../../widgets/forms/PropSelect";
|
||||
import { SexSelection } from "../../widgets/forms/SexSelection";
|
||||
import { UploadPhotoButton } from "../../widgets/forms/UploadPhotoButton";
|
||||
import { useQuery } from "../../hooks/useQuery";
|
||||
import { mdiFamilyTree } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import { ServerApi } from "../../../api/ServerApi";
|
||||
import { Couple } from "../../../api/genealogy/CoupleApi";
|
||||
import { Member, MemberApi, fmtDate } from "../../../api/genealogy/MemberApi";
|
||||
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
|
||||
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
|
||||
import { useQuery } from "../../../hooks/useQuery";
|
||||
import { AsyncWidget } from "../../../widgets/AsyncWidget";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog";
|
||||
import { CouplePhoto } from "../../../widgets/CouplePhoto";
|
||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
||||
import { MemberItem } from "../../../widgets/MemberItem";
|
||||
import { MemberPhoto } from "../../../widgets/MemberPhoto";
|
||||
import { PropertiesBox } from "../../../widgets/PropertiesBox";
|
||||
import { RouterLink } from "../../../widgets/RouterLink";
|
||||
import { DateInput } from "../../../widgets/forms/DateInput";
|
||||
import { MemberInput } from "../../../widgets/forms/MemberInput";
|
||||
import { PropCheckbox } from "../../../widgets/forms/PropCheckbox";
|
||||
import { PropEdit } from "../../../widgets/forms/PropEdit";
|
||||
import { PropSelect } from "../../../widgets/forms/PropSelect";
|
||||
import { SexSelection } from "../../../widgets/forms/SexSelection";
|
||||
import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton";
|
||||
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
|
||||
|
||||
/**
|
||||
* Create a new member route
|
||||
@@ -49,6 +51,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
|
||||
|
||||
const [shouldQuit, setShouldQuit] = React.useState(false);
|
||||
const n = useNavigate();
|
||||
const genealogy = useGenealogy();
|
||||
const family = useFamily();
|
||||
|
||||
const parameters = useQuery();
|
||||
@@ -59,10 +62,10 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
|
||||
try {
|
||||
const r = await MemberApi.Create(m);
|
||||
|
||||
await family.reloadMembersList();
|
||||
await genealogy.reloadMembersList();
|
||||
|
||||
setShouldQuit(true);
|
||||
n(family.family.URL(`member/${r.id}`));
|
||||
n(family.family.URL(`genealogy/member/${r.id}`));
|
||||
snackbar(`La fiche pour ${r.fullName} a été créée avec succès !`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -72,7 +75,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
|
||||
|
||||
const cancel = () => {
|
||||
setShouldQuit(true);
|
||||
n(family.family.URL("members"));
|
||||
n(family.family.URL("genealogy/members"));
|
||||
};
|
||||
|
||||
const member = Member.New(family.family.family_id);
|
||||
@@ -103,6 +106,7 @@ export function FamilyMemberRoute(): React.ReactElement {
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
const { memberId } = useParams();
|
||||
|
||||
const [member, setMember] = React.useState<Member>();
|
||||
@@ -114,7 +118,7 @@ export function FamilyMemberRoute(): React.ReactElement {
|
||||
count.current += 1;
|
||||
setMember(undefined);
|
||||
|
||||
await family.reloadMembersList();
|
||||
await genealogy.reloadMembersList();
|
||||
};
|
||||
|
||||
const deleteMember = async () => {
|
||||
@@ -129,9 +133,9 @@ export function FamilyMemberRoute(): React.ReactElement {
|
||||
await MemberApi.Delete(member!);
|
||||
|
||||
snackbar("La fiche de membre a été supprimée avec succès !");
|
||||
n(family.family.URL("members"));
|
||||
n(family.family.URL("genealogy/members"));
|
||||
|
||||
await family.reloadMembersList();
|
||||
await genealogy.reloadMembersList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Échec de la suppression du membre !");
|
||||
@@ -147,16 +151,14 @@ export function FamilyMemberRoute(): React.ReactElement {
|
||||
build={() => (
|
||||
<MemberPage
|
||||
member={member!}
|
||||
children={family.members.children(member!.id)}
|
||||
siblings={family.members.siblings(member!.id)}
|
||||
couples={family.couples.getAllOf(member!)}
|
||||
children={genealogy.members.children(member!.id)}
|
||||
siblings={genealogy.members.siblings(member!.id)}
|
||||
couples={genealogy.couples.getAllOf(member!)}
|
||||
creating={false}
|
||||
editing={false}
|
||||
onrequestOpenTree={() => n(family.family.familyTreeURL(member!))}
|
||||
onRequestDelete={deleteMember}
|
||||
onRequestEdit={() =>
|
||||
n(family.family.URL(`member/${member!.id}/edit`))
|
||||
}
|
||||
onRequestEdit={() => n(family.family.memberURL(member!, true))}
|
||||
onForceReload={forceReload}
|
||||
/>
|
||||
)}
|
||||
@@ -177,6 +179,7 @@ export function FamilyEditMemberRoute(): React.ReactElement {
|
||||
const [shouldQuit, setShouldQuit] = React.useState(false);
|
||||
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
|
||||
const [member, setMember] = React.useState<Member>();
|
||||
const load = async () => {
|
||||
@@ -185,8 +188,8 @@ export function FamilyEditMemberRoute(): React.ReactElement {
|
||||
|
||||
const cancel = () => {
|
||||
setShouldQuit(true);
|
||||
//n(family.family.URL(`member/${member!.id}`));
|
||||
n(-1);
|
||||
n(family.family.memberURL(member!));
|
||||
//n(-1);
|
||||
};
|
||||
|
||||
const save = async (m: Member) => {
|
||||
@@ -195,10 +198,10 @@ export function FamilyEditMemberRoute(): React.ReactElement {
|
||||
|
||||
snackbar("Les informations du membre ont été mises à jour avec succès !");
|
||||
|
||||
await family.reloadMembersList();
|
||||
await genealogy.reloadMembersList();
|
||||
|
||||
setShouldQuit(true);
|
||||
n(family.family.URL(`member/${member!.id}`));
|
||||
n(family.family.memberURL(member!));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Échec de la mise à jour des informations du membre !");
|
||||
@@ -233,7 +236,7 @@ export function MemberPage(p: {
|
||||
siblings?: Member[];
|
||||
couples?: Couple[];
|
||||
onCancel?: () => void;
|
||||
onSave?: (m: Member) => void;
|
||||
onSave?: (m: Member) => Promise<void>;
|
||||
onRequestEdit?: () => void;
|
||||
onRequestDelete?: () => void;
|
||||
onForceReload?: () => void;
|
||||
@@ -241,6 +244,7 @@ export function MemberPage(p: {
|
||||
}): React.ReactElement {
|
||||
const confirm = useConfirm();
|
||||
const snackbar = useSnackbar();
|
||||
const loadingMessage = useLoadingMessage();
|
||||
|
||||
const family = useFamily();
|
||||
|
||||
@@ -254,8 +258,12 @@ export function MemberPage(p: {
|
||||
setMember(new Member(structuredClone(member)));
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
p.onSave!(member);
|
||||
const save = async () => {
|
||||
loadingMessage.show(
|
||||
"Enregistrement des informations du membre en cours..."
|
||||
);
|
||||
await p.onSave!(member);
|
||||
loadingMessage.hide();
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
@@ -656,9 +664,9 @@ export function MemberPage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`couple/create?${member.sex === "F" ? "wife" : "husband"}=${
|
||||
member.id
|
||||
}`
|
||||
`genealogy/couple/create?${
|
||||
member.sex === "F" ? "wife" : "husband"
|
||||
}=${member.id}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
@@ -676,10 +684,7 @@ export function MemberPage(p: {
|
||||
<>Aucun enfant</>
|
||||
) : (
|
||||
p.children.map((c) => (
|
||||
<RouterLink
|
||||
key={c.id}
|
||||
to={family.family.URL(`member/${c.id}`)}
|
||||
>
|
||||
<RouterLink key={c.id} to={family.family.memberURL(c)}>
|
||||
<MemberItem member={c} />
|
||||
</RouterLink>
|
||||
))
|
||||
@@ -688,7 +693,7 @@ export function MemberPage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`member/create?${
|
||||
`genealogy/member/create?${
|
||||
member.sex === "F" ? "mother" : "father"
|
||||
}=${member.id}`
|
||||
)}
|
||||
@@ -708,10 +713,7 @@ export function MemberPage(p: {
|
||||
<>Aucun frère ou sœur</>
|
||||
) : (
|
||||
p.siblings.map((c) => (
|
||||
<RouterLink
|
||||
key={c.id}
|
||||
to={family.family.URL(`member/${c.id}`)}
|
||||
>
|
||||
<RouterLink key={c.id} to={family.family.memberURL(c)}>
|
||||
<MemberItem member={c} />
|
||||
</RouterLink>
|
||||
))
|
||||
@@ -721,7 +723,7 @@ export function MemberPage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`member/create?mother=${member.mother}&father=${member.father}`
|
||||
`genealogy/member/create?mother=${member.mother}&father=${member.father}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
@@ -743,6 +745,7 @@ function CoupleItem(p: {
|
||||
const n = useNavigate();
|
||||
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
|
||||
const statusStr = ServerApi.Config.couples_states.find(
|
||||
(c) => c.code === p.couple.state
|
||||
@@ -760,7 +763,7 @@ function CoupleItem(p: {
|
||||
const otherSpouseID =
|
||||
p.couple.wife === p.currMemberId ? p.couple.husband : p.couple.wife;
|
||||
const otherSpouse = otherSpouseID
|
||||
? family.members.get(otherSpouseID)
|
||||
? genealogy.members.get(otherSpouseID)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
@@ -21,18 +21,17 @@ import {
|
||||
buildAscendingTree,
|
||||
buildDescendingTree,
|
||||
treeHeight,
|
||||
} from "../../utils/family_tree";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { BasicFamilyTree } from "../../widgets/BasicFamilyTree";
|
||||
import { MemberItem } from "../../widgets/MemberItem";
|
||||
import { RouterLink } from "../../widgets/RouterLink";
|
||||
import { ComplexFamilyTree } from "../../widgets/complex_family_tree/ComplexFamilyTree";
|
||||
import { SimpleFamilyTree } from "../../widgets/simple_family_tree/SimpleFamilyTree";
|
||||
} from "../../../utils/family_tree";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { BasicFamilyTree } from "../../../widgets/BasicFamilyTree";
|
||||
import { MemberItem } from "../../../widgets/MemberItem";
|
||||
import { RouterLink } from "../../../widgets/RouterLink";
|
||||
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
|
||||
import { SimpleFamilyTree } from "../../../widgets/simple_family_tree/SimpleFamilyTree";
|
||||
|
||||
enum CurrTab {
|
||||
BasicTree,
|
||||
SimpleTree,
|
||||
AdvancedTree,
|
||||
}
|
||||
|
||||
enum TreeMode {
|
||||
@@ -43,22 +42,23 @@ enum TreeMode {
|
||||
export function FamilyMemberTreeRoute(): React.ReactElement {
|
||||
const { memberId } = useParams();
|
||||
|
||||
const genealogy = useGenealogy();
|
||||
const family = useFamily();
|
||||
|
||||
const [currTab, setCurrTab] = React.useState(CurrTab.SimpleTree);
|
||||
const [currMode, setCurrMode] = React.useState(TreeMode.Descending);
|
||||
|
||||
const member = family.members.get(Number(memberId));
|
||||
const member = genealogy.members.get(Number(memberId));
|
||||
|
||||
const memo: [FamilyTreeNode, number] | null = React.useMemo(() => {
|
||||
if (!member) return null;
|
||||
const tree =
|
||||
currMode === TreeMode.Ascending
|
||||
? buildAscendingTree(member.id, family.members, family.couples)
|
||||
: buildDescendingTree(member.id, family.members, family.couples);
|
||||
? buildAscendingTree(member.id, genealogy.members, genealogy.couples)
|
||||
: buildDescendingTree(member.id, genealogy.members, genealogy.couples);
|
||||
|
||||
return [tree, treeHeight(tree)];
|
||||
}, [member, currMode, family.members, family.couples]);
|
||||
}, [member, currMode, genealogy.members, genealogy.couples]);
|
||||
|
||||
const [currDepth, setCurrDepth] = React.useState(0);
|
||||
|
||||
@@ -89,7 +89,7 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
|
||||
dense
|
||||
member={member}
|
||||
secondary={
|
||||
<RouterLink to={family.family.URL("tree")}>
|
||||
<RouterLink to={family.family.URL("genealogy/tree")}>
|
||||
<IconButton>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
@@ -149,7 +149,6 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
|
||||
>
|
||||
<Tab tabIndex={CurrTab.BasicTree} label="Basique" />
|
||||
<Tab tabIndex={CurrTab.SimpleTree} label="Simple" />
|
||||
<Tab tabIndex={CurrTab.AdvancedTree} label="Avancé" />
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -157,14 +156,8 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
|
||||
<Paper style={{ flex: "1", display: "flex", flexDirection: "column" }}>
|
||||
{currTab === CurrTab.BasicTree ? (
|
||||
<BasicFamilyTree tree={tree!} depth={currDepth} />
|
||||
) : currTab === CurrTab.SimpleTree ? (
|
||||
<SimpleFamilyTree tree={tree!} depth={currDepth} />
|
||||
) : (
|
||||
<ComplexFamilyTree
|
||||
tree={tree!}
|
||||
isUp={currMode === TreeMode.Ascending}
|
||||
depth={currDepth}
|
||||
/>
|
||||
<SimpleFamilyTree tree={tree!} depth={currDepth} />
|
||||
)}
|
||||
</Paper>
|
||||
</div>
|
||||
@@ -4,24 +4,31 @@ import EditIcon from "@mui/icons-material/Edit";
|
||||
import FemaleIcon from "@mui/icons-material/Female";
|
||||
import MaleIcon from "@mui/icons-material/Male";
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import { Button, TextField, Tooltip } from "@mui/material";
|
||||
import { Button, TextField, Tooltip, Typography } from "@mui/material";
|
||||
import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Member, MemberApi, dateTimestamp, fmtDate } from "../../api/MemberApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle";
|
||||
import { MemberPhoto } from "../../widgets/MemberPhoto";
|
||||
import { RouterLink } from "../../widgets/RouterLink";
|
||||
import {
|
||||
Member,
|
||||
MemberApi,
|
||||
dateTimestamp,
|
||||
fmtDate,
|
||||
} from "../../../api/genealogy/MemberApi";
|
||||
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
||||
import { MemberPhoto } from "../../../widgets/MemberPhoto";
|
||||
import { RouterLink } from "../../../widgets/RouterLink";
|
||||
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
|
||||
|
||||
export function FamilyMembersListRoute(): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
const confirm = useConfirm();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const genealogy = useGenealogy();
|
||||
const family = useFamily();
|
||||
|
||||
const [filter, setFilter] = React.useState("");
|
||||
@@ -36,7 +43,7 @@ export function FamilyMembersListRoute(): React.ReactElement {
|
||||
return;
|
||||
|
||||
await MemberApi.Delete(m);
|
||||
await family.reloadMembersList();
|
||||
await genealogy.reloadMembersList();
|
||||
|
||||
snackbar("La fiche du membre a été supprimée avec succès !");
|
||||
} catch (e) {
|
||||
@@ -55,14 +62,14 @@ export function FamilyMembersListRoute(): React.ReactElement {
|
||||
}}
|
||||
>
|
||||
<FamilyPageTitle title="Membres de la famille" />
|
||||
<RouterLink to={family.family.URL("member/create")}>
|
||||
<RouterLink to={family.family.URL("genealogy/member/create")}>
|
||||
<Tooltip title="Créer la fiche d'un nouveau membre">
|
||||
<Button startIcon={<AddIcon />}>Nouveau</Button>
|
||||
</Tooltip>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
{family.members.isEmpty ? (
|
||||
{genealogy.members.isEmpty ? (
|
||||
<p>
|
||||
Votre famille n'a aucun membre pour le moment ! Utilisez le bouton
|
||||
situé en haut à droite pour créer le premier !
|
||||
@@ -80,8 +87,8 @@ export function FamilyMembersListRoute(): React.ReactElement {
|
||||
<MembersTable
|
||||
members={
|
||||
filter === ""
|
||||
? family.members.fullList
|
||||
: family.members.filter((m) =>
|
||||
? genealogy.members.fullList
|
||||
: genealogy.members.filter((m) =>
|
||||
m.fullName.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
}
|
||||
@@ -97,6 +104,7 @@ function MembersTable(p: {
|
||||
members: Member[];
|
||||
onDelete: (m: Member) => void;
|
||||
}): React.ReactElement {
|
||||
const genealogy = useGenealogy();
|
||||
const family = useFamily();
|
||||
const n = useNavigate();
|
||||
|
||||
@@ -108,7 +116,13 @@ function MembersTable(p: {
|
||||
sortable: false,
|
||||
width: 60,
|
||||
renderCell(params) {
|
||||
return <MemberPhoto member={params.row} />;
|
||||
return (
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", height: "100%" }}
|
||||
>
|
||||
<MemberPhoto member={params.row} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -165,8 +179,13 @@ function MembersTable(p: {
|
||||
headerName: "Père",
|
||||
flex: 5,
|
||||
renderCell(params) {
|
||||
if (!params.row.father) return <></>;
|
||||
return family.members.get(params.row.father)!.fullName;
|
||||
if (!params.row.father)
|
||||
return (
|
||||
<Typography color="red" component="span" variant="body2">
|
||||
Non renseigné
|
||||
</Typography>
|
||||
);
|
||||
return genealogy.members.get(params.row.father)!.fullName;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -174,8 +193,13 @@ function MembersTable(p: {
|
||||
headerName: "Mère",
|
||||
flex: 5,
|
||||
renderCell(params) {
|
||||
if (!params.row.mother) return <></>;
|
||||
return family.members.get(params.row.mother)!.fullName;
|
||||
if (!params.row.mother)
|
||||
return (
|
||||
<Typography color="red" component="span" variant="body2">
|
||||
Non renseignée
|
||||
</Typography>
|
||||
);
|
||||
return genealogy.members.get(params.row.mother)!.fullName;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { MemberInput } from "../../widgets/forms/MemberInput";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { MemberInput } from "../../../widgets/forms/MemberInput";
|
||||
|
||||
export function FamilyTreeRoute(): React.ReactElement {
|
||||
const n = useNavigate();
|
||||
221
geneit_app/src/routes/family/genealogy/GenalogySettingsRoute.tsx
Normal file
221
geneit_app/src/routes/family/genealogy/GenalogySettingsRoute.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CardActions,
|
||||
CardContent,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { FamilyApi } from "../../../api/FamilyApi";
|
||||
import { DataApi } from "../../../api/genealogy/DataApi";
|
||||
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
|
||||
import { downloadBlob, selectFileToUpload } from "../../../utils/files_utils";
|
||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||
import { FamilyCard } from "../../../widgets/FamilyCard";
|
||||
|
||||
export function GenalogySettingsRoute(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<GenealogySettingsCard />
|
||||
<GenealogyExportCard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GenealogySettingsCard(): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
|
||||
const family = useFamily();
|
||||
|
||||
const [disableCouplePhotos, setDisableCouplePhotos] = React.useState(
|
||||
family.family.disable_couple_photos
|
||||
);
|
||||
|
||||
const canEdit = family.family.is_admin;
|
||||
|
||||
const [error, setError] = React.useState<string>();
|
||||
const [success, setSuccess] = React.useState<string>();
|
||||
|
||||
const updateFamily = async () => {
|
||||
try {
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
|
||||
await FamilyApi.UpdateFamily({
|
||||
id: family.family.family_id,
|
||||
disable_couple_photos: disableCouplePhotos,
|
||||
});
|
||||
|
||||
family.reloadFamilyInfo();
|
||||
|
||||
alert("Les paramètres de la famille ont été mis à jour avec succès !");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError("Echec de la mise à jour des paramètres de la famille !");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FamilyCard error={error} success={success}>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="div">
|
||||
Paramètres du module de généalogie
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
sx={{
|
||||
"& .MuiTextField-root": { my: 1 },
|
||||
}}
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
>
|
||||
<Tooltip
|
||||
title="Les photos de couple ne sont pas utilisées en pratique dans les arbres généalogiques. Il est possible de masquer les formulaires d'édition de photos de couple pour limiter le risque de confusion."
|
||||
arrow
|
||||
>
|
||||
<FormControlLabel
|
||||
disabled={!canEdit}
|
||||
control={
|
||||
<Switch
|
||||
checked={disableCouplePhotos}
|
||||
onChange={(_e, c) => setDisableCouplePhotos(c)}
|
||||
/>
|
||||
}
|
||||
label="Désactiver les photos de couple"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
onClick={updateFamily}
|
||||
disabled={!canEdit}
|
||||
style={{ marginLeft: "auto" }}
|
||||
>
|
||||
Enregistrer
|
||||
</Button>
|
||||
</CardActions>
|
||||
</FamilyCard>
|
||||
);
|
||||
}
|
||||
|
||||
function GenealogyExportCard(): React.ReactElement {
|
||||
const loading = useLoadingMessage();
|
||||
const confirm = useConfirm();
|
||||
const alert = useAlert();
|
||||
|
||||
const family = useFamily();
|
||||
|
||||
const [error, setError] = React.useState<string>();
|
||||
const [success, setSuccess] = React.useState<string>();
|
||||
|
||||
const exportData = async () => {
|
||||
loading.show("Export des données");
|
||||
try {
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
|
||||
const blob = await DataApi.ExportData(family.familyId);
|
||||
downloadBlob(blob, `Export-${new Date().getTime()}.zip`);
|
||||
|
||||
setSuccess("Export des données effectué avec succès !");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError("Echec de l'export des données de la famille !");
|
||||
}
|
||||
loading.hide();
|
||||
};
|
||||
|
||||
const importData = async () => {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Attention ! Cette opération a pour effet d'effacer toutes les données existantes en base ! Voulez-vous vraiment poursuivre l'opération ?"
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
const file = await selectFileToUpload({
|
||||
allowedTypes: ["application/zip"],
|
||||
});
|
||||
if (file === null) return;
|
||||
|
||||
setError(undefined);
|
||||
setSuccess(undefined);
|
||||
|
||||
loading.show(
|
||||
"Restauration des données de généalogie de la famille en cours..."
|
||||
);
|
||||
|
||||
await DataApi.ImportData(family.familyId, file);
|
||||
|
||||
family.reloadFamilyInfo();
|
||||
|
||||
alert(
|
||||
"Import des données de généalogie de la famille effectué avec succès !"
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(
|
||||
`Echec de l'import des données de généalogie de la famille ! (${e})`
|
||||
);
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
};
|
||||
|
||||
return (
|
||||
<FamilyCard error={error} success={success}>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="div">
|
||||
Export / import des données de généalogie
|
||||
</Typography>
|
||||
<p>
|
||||
Vous pouvez, à des fins de sauvegardes ou de transfert, exporter et
|
||||
importer l'ensemble des données des membres et des couples de cette
|
||||
famille, sous format ZIP.
|
||||
</p>
|
||||
|
||||
<Alert severity="warning">
|
||||
Attention ! La restauration des données de généalogie de la famille
|
||||
provoque préalablement l'effacement de toutes les données enregistrées
|
||||
dans la famille ! Par ailleurs, la restauration n'est pas réversible !
|
||||
</Alert>
|
||||
|
||||
<p> </p>
|
||||
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={exportData}
|
||||
size={"large"}
|
||||
style={{ marginBottom: "10px" }}
|
||||
>
|
||||
Exporter les données de généalogie
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
startIcon={<UploadIcon />}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
fullWidth
|
||||
onClick={importData}
|
||||
disabled={!family.family.is_admin}
|
||||
size={"large"}
|
||||
>
|
||||
Importer les données de généalogie
|
||||
</Button>
|
||||
</CardContent>
|
||||
</FamilyCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
||||
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
|
||||
|
||||
export function GenealogyHomeRoute(): React.ReactElement {
|
||||
const genealogy = useGenealogy();
|
||||
return (
|
||||
<>
|
||||
<FamilyPageTitle title="Généalogie de votre famille" />
|
||||
<div style={{ margin: "20px" }}>
|
||||
<p>
|
||||
Depuis cette section de l'application, vous pouvez afficher et
|
||||
compléter l'abre généalogique de votre famille.
|
||||
</p>
|
||||
<p> </p>
|
||||
<p>Nombre de fiches de membres: {genealogy.members.size}</p>
|
||||
<p>Nombre de fiches de couples: {genealogy.couples.size}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export function isDebug(): boolean {
|
||||
return !process.env.NODE_ENV || process.env.NODE_ENV === "development";
|
||||
return (
|
||||
!import.meta.env.NODE_ENV || import.meta.env.NODE_ENV === "development"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Couple, CouplesList } from "../api/CoupleApi";
|
||||
import { Member, MembersList, dateTimestamp } from "../api/MemberApi";
|
||||
import { Couple, CouplesList } from "../api/genealogy/CoupleApi";
|
||||
import { Member, MembersList, dateTimestamp } from "../api/genealogy/MemberApi";
|
||||
|
||||
export interface CoupleInformation {
|
||||
couple: Couple;
|
||||
|
||||
21
geneit_app/src/utils/form_utils.ts
Normal file
21
geneit_app/src/utils/form_utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { LenConstraint } from "../api/ServerApi";
|
||||
|
||||
/**
|
||||
* Check if a constraint was respected or not
|
||||
*
|
||||
* @returns An error message appropriate for the constraint
|
||||
* violation, if any, or undefined otherwise
|
||||
*/
|
||||
export function checkConstraint(
|
||||
constraint: LenConstraint,
|
||||
value: string | undefined
|
||||
): string | undefined {
|
||||
value = value ?? "";
|
||||
if (value.length < constraint.min)
|
||||
return `Veuillez indiquer au moins ${constraint.min} caractères !`;
|
||||
|
||||
if (value.length > constraint.max)
|
||||
return `Veuillez indiquer au maximum ${constraint.max} caractères !`;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
31
geneit_app/src/utils/time_utils.ts
Normal file
31
geneit_app/src/utils/time_utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Get formatted UNIX date
|
||||
*/
|
||||
export function fmtUnixDate(time: number): string {
|
||||
return new Date(time * 1000).toLocaleString("fr-FR");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted UNIX date for Full Calendar
|
||||
*/
|
||||
export function fmtUnixDateFullCalendar(
|
||||
time: number,
|
||||
correctEnd: boolean
|
||||
): string {
|
||||
let d = new Date(time * 1000);
|
||||
|
||||
if (d.getHours() > 0 && correctEnd)
|
||||
d = new Date(time * 1000 + 3600 * 24 * 1000);
|
||||
|
||||
const s = `${d.getFullYear()}-${(d.getMonth() + 1)
|
||||
.toString(10)
|
||||
.padStart(2, "0")}-${d.getDate().toString(10).padStart(2, "0")}`; /*T${d
|
||||
.getHours()
|
||||
.toString(10)
|
||||
.padStart(2, "0")}:${d.getMinutes().toString(10).padStart(2, "0")}:${d
|
||||
.getSeconds()
|
||||
.toString(10)
|
||||
.padStart(2, "0")}`*/
|
||||
|
||||
return s;
|
||||
}
|
||||
@@ -4,12 +4,15 @@ import {
|
||||
mdiContentCopy,
|
||||
mdiCrowd,
|
||||
mdiFamilyTree,
|
||||
mdiFileTree,
|
||||
mdiHomeGroup,
|
||||
mdiHumanMaleFemale,
|
||||
mdiLockCheck,
|
||||
mdiPlus,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
import {
|
||||
Box,
|
||||
@@ -26,9 +29,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { Outlet, useLocation, useParams } from "react-router-dom";
|
||||
import { CoupleApi, CouplesList } from "../api/CoupleApi";
|
||||
import { ExtendedFamilyInfo, FamilyApi } from "../api/FamilyApi";
|
||||
import { MemberApi, MembersList } from "../api/MemberApi";
|
||||
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
|
||||
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
|
||||
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
|
||||
@@ -37,12 +38,8 @@ import { RouterLink } from "./RouterLink";
|
||||
|
||||
interface FamilyContext {
|
||||
family: ExtendedFamilyInfo;
|
||||
members: MembersList;
|
||||
couples: CouplesList;
|
||||
familyId: number;
|
||||
reloadFamilyInfo: () => void;
|
||||
reloadMembersList: () => Promise<void>;
|
||||
reloadCouplesList: () => Promise<void>;
|
||||
}
|
||||
|
||||
const FamilyContextK = React.createContext<FamilyContext | null>(null);
|
||||
@@ -54,8 +51,6 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
const confirm = useConfirm();
|
||||
|
||||
const [family, setFamily] = React.useState<null | ExtendedFamilyInfo>(null);
|
||||
const [members, setMembers] = React.useState<null | MembersList>(null);
|
||||
const [couples, setCouples] = React.useState<null | CouplesList>(null);
|
||||
|
||||
const loadKey = React.useRef(1);
|
||||
|
||||
@@ -64,15 +59,11 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
const load = async () => {
|
||||
const familyID = Number(familyId);
|
||||
setFamily(await FamilyApi.GetSingle(familyID));
|
||||
setMembers(await MemberApi.GetEntireList(familyID));
|
||||
setCouples(await CoupleApi.GetEntireList(familyID));
|
||||
};
|
||||
|
||||
const onReload = async () => {
|
||||
loadKey.current += 1;
|
||||
setFamily(null);
|
||||
setMembers(null);
|
||||
setCouples(null);
|
||||
|
||||
return new Promise<void>((res, _rej) => {
|
||||
loadPromise.current = () => res();
|
||||
@@ -106,7 +97,7 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
ready={family !== null && members !== null}
|
||||
ready={family !== null}
|
||||
loadKey={`${familyId}-${loadKey.current}`}
|
||||
load={load}
|
||||
errMsg="Échec du chargement des informations de la famille !"
|
||||
@@ -120,12 +111,8 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
<FamilyContextK.Provider
|
||||
value={{
|
||||
family: family!,
|
||||
members: members!,
|
||||
couples: couples!,
|
||||
familyId: family!.family_id,
|
||||
reloadFamilyInfo: onReload,
|
||||
reloadMembersList: onReload,
|
||||
reloadCouplesList: onReload,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -147,13 +134,25 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
|
||||
<FamilyLink icon={<HomeIcon />} label="Accueil" uri="" />
|
||||
|
||||
{family?.enable_genealogy && (
|
||||
<>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<ListSubheader component="div">Généalogie</ListSubheader>
|
||||
|
||||
<FamilyLink
|
||||
icon={<HomeIcon />}
|
||||
label="Accueil"
|
||||
uri="genealogy"
|
||||
/>
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiCrowd} size={1} />}
|
||||
label="Membres"
|
||||
uri="members"
|
||||
uri="genealogy/members"
|
||||
secondaryAction={
|
||||
<Tooltip title="Créer une nouvelle fiche de membre">
|
||||
<RouterLink to={family!.URL("member/create")}>
|
||||
<RouterLink
|
||||
to={family!.URL("genealogy/member/create")}
|
||||
>
|
||||
<IconButton>
|
||||
<Icon path={mdiPlus} size={0.75} />
|
||||
</IconButton>
|
||||
@@ -165,10 +164,12 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiHumanMaleFemale} size={1} />}
|
||||
label="Couples"
|
||||
uri="couples"
|
||||
uri="genealogy/couples"
|
||||
secondaryAction={
|
||||
<Tooltip title="Créer une nouvelle fiche de couple">
|
||||
<RouterLink to={family!.URL("couple/create")}>
|
||||
<RouterLink
|
||||
to={family!.URL("genealogy/couple/create")}
|
||||
>
|
||||
<IconButton>
|
||||
<Icon path={mdiPlus} size={0.75} />
|
||||
</IconButton>
|
||||
@@ -180,8 +181,28 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiFamilyTree} size={1} />}
|
||||
label="Arbre"
|
||||
uri="tree"
|
||||
uri="genealogy/tree"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{family?.enable_accommodations && (
|
||||
<>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<ListSubheader component="div">Logements</ListSubheader>
|
||||
|
||||
<FamilyLink
|
||||
icon={<HomeIcon />}
|
||||
label="Accueil"
|
||||
uri="accommodations"
|
||||
/>
|
||||
<FamilyLink
|
||||
icon={<CalendarMonthIcon />}
|
||||
label="Réservations"
|
||||
uri="accommodations/reservations"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<ListSubheader component="div">Administration</ListSubheader>
|
||||
@@ -198,6 +219,22 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
uri="settings"
|
||||
/>
|
||||
|
||||
{family?.enable_genealogy && (
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiFileTree} size={1} />}
|
||||
label="Généalogie"
|
||||
uri="genealogy/settings"
|
||||
/>
|
||||
)}
|
||||
|
||||
{family?.enable_accommodations && (
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiHomeGroup} size={1} />}
|
||||
label="Logements"
|
||||
uri="accommodations/settings"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Invitation code */}
|
||||
|
||||
<ListItem
|
||||
|
||||
@@ -82,7 +82,7 @@ export function BaseLoginPage() {
|
||||
variant="h6"
|
||||
style={{ margin: "10px 0px 30px 0px" }}
|
||||
>
|
||||
La généalogie de votre famille
|
||||
L'intranet de votre famille
|
||||
</Typography>
|
||||
|
||||
{/* inner page */}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { mdiBabyCarriage, mdiCross } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { TreeItem, TreeView } from "@mui/lab";
|
||||
import { TreeItem, SimpleTreeView } from "@mui/x-tree-view";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Couple } from "../api/CoupleApi";
|
||||
import { Member, fmtDate } from "../api/MemberApi";
|
||||
import { Couple } from "../api/genealogy/CoupleApi";
|
||||
import { Member, fmtDate } from "../api/genealogy/MemberApi";
|
||||
import { FamilyTreeNode } from "../utils/family_tree";
|
||||
import { useFamily } from "./BaseFamilyRoute";
|
||||
import { MemberPhoto } from "./MemberPhoto";
|
||||
@@ -16,13 +16,12 @@ export function BasicFamilyTree(p: {
|
||||
depth: number;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
<SimpleTreeView
|
||||
slots={{ collapseIcon: ExpandMoreIcon, expandIcon: ChevronRightIcon }}
|
||||
sx={{ flexGrow: 1 }}
|
||||
>
|
||||
<FamilyTreeItem n={p.tree} depth={p.depth} />
|
||||
</TreeView>
|
||||
</SimpleTreeView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +39,7 @@ function FamilyTreeItem(p: {
|
||||
|
||||
return (
|
||||
<TreeItem
|
||||
nodeId={p.n.member.id.toString()}
|
||||
itemId={p.n.member.id.toString()}
|
||||
style={{ margin: "10px" }}
|
||||
label={
|
||||
<div style={{ display: "flex" }}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
import { unstable_useBlocker as useBlocker } from "react-router-dom";
|
||||
import { useBlocker } from "react-router-dom";
|
||||
|
||||
export function ConfirmLeaveWithoutSaveDialog(p: {
|
||||
shouldBlock: boolean;
|
||||
|
||||
30
geneit_app/src/widgets/CopyToClipboard.tsx
Normal file
30
geneit_app/src/widgets/CopyToClipboard.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ButtonBase } from "@mui/material";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
|
||||
|
||||
export function CopyToClipboard(
|
||||
p: PropsWithChildren<{ content: string }>
|
||||
): React.ReactElement {
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(p.content);
|
||||
snackbar(`${p.content} a été copié dans le presse papier.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonBase
|
||||
onClick={copy}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
alignItems: "unset",
|
||||
textAlign: "unset",
|
||||
position: "relative",
|
||||
padding: "0px",
|
||||
}}
|
||||
disableRipple
|
||||
>
|
||||
{p.children}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Avatar } from "@mui/material";
|
||||
import { Couple } from "../api/CoupleApi";
|
||||
import { Couple } from "../api/genealogy/CoupleApi";
|
||||
|
||||
export function CouplePhoto(p: {
|
||||
couple: Couple;
|
||||
|
||||
@@ -2,10 +2,14 @@ import { Alert, Card } from "@mui/material";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
export function FamilyCard(
|
||||
p: PropsWithChildren<{ error?: string; success?: string }>
|
||||
p: PropsWithChildren<{
|
||||
error?: string;
|
||||
success?: string;
|
||||
style?: React.CSSProperties | undefined;
|
||||
}>
|
||||
): React.ReactElement {
|
||||
return (
|
||||
<Card style={{ margin: "10px auto", maxWidth: "450px" }}>
|
||||
<Card style={{ ...p.style, margin: "10px auto", maxWidth: "450px" }}>
|
||||
{p.error && <Alert severity="error">{p.error}</Alert>}
|
||||
{p.success && <Alert severity="success">{p.success}</Alert>}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { Member, fmtDate } from "../api/MemberApi";
|
||||
import { Member, fmtDate } from "../api/genealogy/MemberApi";
|
||||
import { MemberPhoto } from "./MemberPhoto";
|
||||
import Icon from "@mdi/react";
|
||||
import FemaleIcon from "@mui/icons-material/Female";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Avatar } from "@mui/material";
|
||||
import { Member } from "../api/MemberApi";
|
||||
import { Member } from "../api/genealogy/MemberApi";
|
||||
|
||||
export function MemberPhoto(p: {
|
||||
member?: Member;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import {
|
||||
AccommodationListApi,
|
||||
AccommodationsList,
|
||||
} from "../../api/accommodations/AccommodationListApi";
|
||||
import { CreateAccommodationCalendarURLDialogProvider } from "../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider";
|
||||
import { InstallCalendarDialogProvider } from "../../hooks/context_providers/accommodations/InstallCalendarDialogProvider";
|
||||
import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
|
||||
import { UpdateReservationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateReservationDialogProvider";
|
||||
import { AsyncWidget } from "../AsyncWidget";
|
||||
import { useFamily } from "../BaseFamilyRoute";
|
||||
|
||||
interface AccommodationsContext {
|
||||
accommodations: AccommodationsList;
|
||||
reloadAccommodationsList: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AccommodationsContextK =
|
||||
React.createContext<AccommodationsContext | null>(null);
|
||||
|
||||
export function BaseAccommodationsRoute(): React.ReactElement {
|
||||
const family = useFamily();
|
||||
|
||||
const [accommodations, setAccommodations] =
|
||||
React.useState<null | AccommodationsList>(null);
|
||||
|
||||
const loadKey = React.useRef(1);
|
||||
|
||||
const loadPromise = React.useRef<() => void>();
|
||||
|
||||
const load = async () => {
|
||||
setAccommodations(
|
||||
await AccommodationListApi.GetListOfFamily(family.family)
|
||||
);
|
||||
};
|
||||
|
||||
const onReload = async () => {
|
||||
loadKey.current += 1;
|
||||
setAccommodations(null);
|
||||
|
||||
return new Promise<void>((res, _rej) => {
|
||||
loadPromise.current = () => res();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
ready={accommodations !== null}
|
||||
loadKey={`${family.familyId}-${loadKey.current}`}
|
||||
load={load}
|
||||
errMsg="Échec du chargement des informations sur les logements de la famille !"
|
||||
build={() => {
|
||||
if (loadPromise.current != null) {
|
||||
loadPromise.current?.();
|
||||
loadPromise.current = undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccommodationsContextK.Provider
|
||||
value={{
|
||||
accommodations: accommodations!,
|
||||
reloadAccommodationsList: onReload,
|
||||
}}
|
||||
>
|
||||
<UpdateAccommodationDialogProvider>
|
||||
<CreateAccommodationCalendarURLDialogProvider>
|
||||
<InstallCalendarDialogProvider>
|
||||
<UpdateReservationDialogProvider>
|
||||
<Outlet />
|
||||
</UpdateReservationDialogProvider>
|
||||
</InstallCalendarDialogProvider>
|
||||
</CreateAccommodationCalendarURLDialogProvider>
|
||||
</UpdateAccommodationDialogProvider>
|
||||
</AccommodationsContextK.Provider>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAccommodations(): AccommodationsContext {
|
||||
return React.useContext(AccommodationsContextK)!;
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
import { mdiXml } from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import f3, { f3Data } from "family-chart";
|
||||
import { jsPDF } from "jspdf";
|
||||
import React from "react";
|
||||
import "svg2pdf.js";
|
||||
import { Couple } from "../../api/CoupleApi";
|
||||
import { Member, fmtDate } from "../../api/MemberApi";
|
||||
import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
|
||||
import {
|
||||
FamilyTreeNode,
|
||||
getAvailableMembers,
|
||||
treeHeight,
|
||||
treeWidth,
|
||||
} from "../../utils/family_tree";
|
||||
import { downloadBlob } from "../../utils/files_utils";
|
||||
import "./family-chart.css";
|
||||
|
||||
export function ComplexFamilyTree(p: {
|
||||
tree: FamilyTreeNode;
|
||||
isUp: boolean;
|
||||
depth: number;
|
||||
}): React.ReactElement {
|
||||
const darkTheme = useDarkTheme();
|
||||
|
||||
const applyTree = (container: HTMLDivElement) => {
|
||||
if (!container) return;
|
||||
|
||||
const store = f3.createStore({
|
||||
data: treeToF3Data(p.tree, p.isUp, p.depth),
|
||||
node_separation: 250,
|
||||
level_separation: 150,
|
||||
});
|
||||
const view = f3.d3AnimationView({
|
||||
store,
|
||||
cont: container,
|
||||
});
|
||||
const Card = f3.elements.Card({
|
||||
store,
|
||||
svg: view.svg,
|
||||
card_dim: {
|
||||
w: 210,
|
||||
h: 120,
|
||||
text_x: 5,
|
||||
text_y: 75,
|
||||
img_w: 60,
|
||||
img_h: 70,
|
||||
img_x: 5,
|
||||
img_y: 5,
|
||||
},
|
||||
card_display: [
|
||||
(d) =>
|
||||
`${d.data.first_name || ""} ${d.data.last_name || ""} ${
|
||||
d.data.dead ? "✝" : ""
|
||||
}`,
|
||||
(d) => {
|
||||
let birthDeath = [];
|
||||
if (d.data.birthday) birthDeath.push(d.data.birthday);
|
||||
if (d.data.deathday) birthDeath.push(d.data.deathday);
|
||||
|
||||
let s = birthDeath.join(" -> ");
|
||||
|
||||
if (d.data.wedding_state || d.data.dateOfWedding) {
|
||||
let weddingInfo = [];
|
||||
if (d.data.wedding_state) weddingInfo.push(d.data.wedding_state);
|
||||
if (d.data.dateOfWedding)
|
||||
weddingInfo.push("Mariage : " + d.data.dateOfWedding);
|
||||
s += `</tspan> <tspan x="0" dy="14" font-size="10">${weddingInfo.join(
|
||||
" - "
|
||||
)}`;
|
||||
}
|
||||
|
||||
return s;
|
||||
},
|
||||
],
|
||||
mini_tree: true,
|
||||
link_break: false,
|
||||
});
|
||||
|
||||
// Patch generated card
|
||||
const PatchedCard: f3.F3CardBuilder = (p) => {
|
||||
const res = Card(p);
|
||||
|
||||
// Patch card colors for PDF export
|
||||
res
|
||||
.querySelector(".card-male")
|
||||
?.querySelector(".card-body-rect")
|
||||
?.setAttribute("fill", "#add8e6");
|
||||
res
|
||||
.querySelector(".card-female")
|
||||
?.querySelector(".card-body-rect")
|
||||
?.setAttribute("fill", "#ffb6c1");
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
view.setCard(PatchedCard);
|
||||
store.setOnUpdate((props) => view.update(props || {}));
|
||||
store.update.tree({ initial: false, transition_time: 0 });
|
||||
};
|
||||
|
||||
const doExport = async (onlySVG: boolean) => {
|
||||
const docWidth = treeWidth(p.tree) * 65;
|
||||
const docHeight = treeHeight(p.tree) * 60;
|
||||
console.info(`Tree w=${treeWidth(p.tree)} h=${treeHeight(p.tree)}`);
|
||||
|
||||
// Clone the SVG to manipulate it
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("f3", "f3-export");
|
||||
container.style.width = docWidth + "px";
|
||||
container.style.height = docHeight + "px";
|
||||
document.body.appendChild(container);
|
||||
applyTree(container);
|
||||
|
||||
const target = container.children[0];
|
||||
|
||||
await new Promise((res) => setTimeout(() => res(null), 100));
|
||||
|
||||
// SVG manipulations (adaptations to export)
|
||||
let dstSVG = target.innerHTML.replaceAll(
|
||||
`<path class="link" fill="none" stroke="#fff"`,
|
||||
`<path class="link" fill="none" stroke="#000"`
|
||||
);
|
||||
|
||||
dstSVG = dstSVG.replaceAll(
|
||||
`class="text-overflow-mask"`,
|
||||
`class="text-overflow-mask" fill="transparent"`
|
||||
);
|
||||
|
||||
dstSVG = dstSVG.replaceAll(`>UNKNOWN<`, `fill="#000">INCONNU<`);
|
||||
|
||||
dstSVG = dstSVG.replaceAll(
|
||||
`class="card-outline`,
|
||||
`fill="transparent" class="card-outline`
|
||||
);
|
||||
|
||||
dstSVG = dstSVG.replaceAll("✝", " ");
|
||||
|
||||
// Download in SVG format
|
||||
if (onlySVG) {
|
||||
// Fix background color (first rect background)
|
||||
dstSVG = dstSVG.replace(
|
||||
`fill="transparent"></rect>`,
|
||||
`fill="white"></rect>`
|
||||
);
|
||||
|
||||
const blob = new Blob([`<svg>${dstSVG}</svg>`], {
|
||||
type: "image/svg+xml",
|
||||
});
|
||||
|
||||
downloadBlob(blob, "ArbreGenealogique.svg");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Download in PDF format
|
||||
//navigator.clipboard.writeText(dstSVG);
|
||||
target.innerHTML = dstSVG;
|
||||
|
||||
const doc = new jsPDF({
|
||||
orientation: "l",
|
||||
format: [docHeight, docWidth],
|
||||
});
|
||||
|
||||
await doc.svg(target, {
|
||||
height: docHeight,
|
||||
width: docWidth,
|
||||
});
|
||||
|
||||
container.remove();
|
||||
|
||||
// Save the created pdf
|
||||
doc.save("ArbreGenealogique.pdf");
|
||||
};
|
||||
|
||||
const exportPDF = () => doExport(false);
|
||||
|
||||
const exportSVG = () => doExport(true);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<Tooltip title="Exporter le graphique au format PDF">
|
||||
<IconButton onClick={exportPDF}>
|
||||
<PictureAsPdfIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Exporter le graphique au format SVG">
|
||||
<IconButton onClick={exportSVG}>
|
||||
<Icon path={mdiXml} size={1} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: "100%" }}
|
||||
className={`f3 ${darkTheme.enabled ? "f3-dark" : "f3-light"}`}
|
||||
id="FamilyChart"
|
||||
ref={applyTree}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function treeToF3Data(
|
||||
node: FamilyTreeNode,
|
||||
isUp: boolean,
|
||||
depth: number
|
||||
): f3Data[] {
|
||||
const availableMembers = getAvailableMembers(node, depth);
|
||||
|
||||
const list: f3Data[] = [];
|
||||
if (isUp) treeToF3DataUpRecurse(node, list, availableMembers);
|
||||
else treeToF3DataDownRecurse(node, list, availableMembers);
|
||||
return list;
|
||||
}
|
||||
|
||||
function memberData(m: Member, c?: Couple): f3.f3DataData {
|
||||
return {
|
||||
first_name: m.first_name ?? "_",
|
||||
last_name: m.last_name ?? "_",
|
||||
gender: m.sex ?? "M",
|
||||
avatar: m.thumbnailURL ?? undefined,
|
||||
dead: m.dead,
|
||||
birthday: m.dateOfBirth ? fmtDate(m.dateOfBirth) : undefined,
|
||||
deathday: m.dateOfDeath ? fmtDate(m.dateOfDeath) : undefined,
|
||||
wedding_state: c?.stateFr,
|
||||
dateOfWedding: c?.dateOfWedding ? fmtDate(c?.dateOfWedding) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function treeToF3DataUpRecurse(
|
||||
node: FamilyTreeNode,
|
||||
array: f3Data[],
|
||||
availableMembers: Set<number>,
|
||||
child?: number,
|
||||
spouses?: number[]
|
||||
) {
|
||||
if (!availableMembers.has(node.member.id)) return;
|
||||
|
||||
array.push({
|
||||
data: memberData(node.member),
|
||||
id: node.member.id.toString(),
|
||||
rels: {
|
||||
father:
|
||||
node.member.father && availableMembers.has(node.member.father)
|
||||
? node.member.father.toString()
|
||||
: undefined,
|
||||
mother:
|
||||
node.member.mother && availableMembers.has(node.member.mother)
|
||||
? node.member.mother.toString()
|
||||
: undefined,
|
||||
|
||||
spouses: spouses
|
||||
?.filter((c) => c !== node.member.id)
|
||||
.map((c) => c.toString()),
|
||||
children: child ? [child.toString()] : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const parentSpouses = node.down?.map((c) => c.member.id);
|
||||
|
||||
node.down?.forEach((d) =>
|
||||
treeToF3DataUpRecurse(
|
||||
d,
|
||||
array,
|
||||
availableMembers,
|
||||
node.member.id,
|
||||
parentSpouses
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function treeToF3DataDownRecurse(
|
||||
node: FamilyTreeNode,
|
||||
array: f3Data[],
|
||||
availableMembers: Set<number>
|
||||
) {
|
||||
if (!availableMembers.has(node.member.id)) return;
|
||||
|
||||
// Get all members ids
|
||||
let children = node?.down?.map((c) => c.member.id) ?? [];
|
||||
node.couples?.map((c) => c.down.forEach((m) => children.push(m.member.id)));
|
||||
|
||||
children = children.filter((c) => availableMembers.has(c));
|
||||
|
||||
array.push({
|
||||
data: memberData(node.member),
|
||||
id: node.member.id.toString(),
|
||||
rels: {
|
||||
father:
|
||||
node.member.father && availableMembers.has(node.member.father)
|
||||
? node.member.father.toString()
|
||||
: undefined,
|
||||
mother:
|
||||
node.member.mother && availableMembers.has(node.member.mother)
|
||||
? node.member.mother.toString()
|
||||
: undefined,
|
||||
|
||||
spouses: node.couples
|
||||
?.filter((s) => availableMembers.has(s.member.id))
|
||||
.map((c) => c.member.id.toString()),
|
||||
children: children.map((c) => c.toString()),
|
||||
},
|
||||
});
|
||||
|
||||
node?.down?.forEach((e) =>
|
||||
treeToF3DataDownRecurse(e, array, availableMembers)
|
||||
);
|
||||
|
||||
if (node.couples) {
|
||||
for (const c of node.couples) {
|
||||
if (!availableMembers.has(c.member.id)) continue;
|
||||
array.push({
|
||||
data: memberData(c.member, c.couple),
|
||||
id: c.member.id.toString(),
|
||||
rels: {
|
||||
father:
|
||||
c.member.father && availableMembers.has(c.member.father)
|
||||
? c.member.father.toString()
|
||||
: undefined,
|
||||
mother:
|
||||
c.member.mother && availableMembers.has(c.member.mother)
|
||||
? c.member.mother.toString()
|
||||
: undefined,
|
||||
spouses: [node.member.id.toString()],
|
||||
children: c.down
|
||||
.filter((c) => availableMembers.has(c.member.id))
|
||||
.map((c) => c.member.id.toString()),
|
||||
},
|
||||
});
|
||||
|
||||
c.down.forEach((e) =>
|
||||
treeToF3DataDownRecurse(e, array, availableMembers)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
.f3 {
|
||||
height: 700px;
|
||||
max-height: calc(100vh - 80px);
|
||||
width: 900px;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.f3 .cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.f3 svg.main_svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/*background-color: #3b5560;*/
|
||||
color: #3b5560;
|
||||
}
|
||||
.f3 svg.main_svg text {
|
||||
fill: currentColor;
|
||||
}
|
||||
.f3 rect.card-female,
|
||||
.f3 .card-female .card-body-rect,
|
||||
.f3 .card-female .text-overflow-mask {
|
||||
fill: lightpink;
|
||||
}
|
||||
.f3 rect.card-male,
|
||||
.f3 .card-male .card-body-rect,
|
||||
.f3 .card-male .text-overflow-mask {
|
||||
fill: lightblue;
|
||||
}
|
||||
.f3 .card-genderless .card-body-rect,
|
||||
.f3 .card-genderless .text-overflow-mask {
|
||||
fill: lightgray;
|
||||
}
|
||||
.f3 .card_add .card-body-rect {
|
||||
fill: #3b5560;
|
||||
stroke-width: 4px;
|
||||
stroke: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.f3 g.card_add text {
|
||||
fill: #fff;
|
||||
}
|
||||
.f3 .card-main {
|
||||
stroke: #000;
|
||||
}
|
||||
.f3 .card_family_tree rect {
|
||||
transition: 0.3s;
|
||||
}
|
||||
.f3 .card_family_tree:hover rect {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.f3 .card_add_relative {
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.f3 .card_add_relative circle {
|
||||
fill: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.f3 .card_add_relative:hover {
|
||||
color: black;
|
||||
}
|
||||
.f3 .card_edit.pencil_icon {
|
||||
color: #fff;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.f3 .card_edit.pencil_icon:hover {
|
||||
color: black;
|
||||
}
|
||||
.f3 .card_break_link,
|
||||
.f3 .link_upper,
|
||||
.f3 .link_lower,
|
||||
.f3 .link_particles {
|
||||
transform-origin: 50% 50%;
|
||||
transition: 1s;
|
||||
}
|
||||
.f3 .card_break_link {
|
||||
color: #fff;
|
||||
}
|
||||
.f3 .card_break_link.closed .link_upper {
|
||||
transform: translate(-140.5px, 655.6px);
|
||||
}
|
||||
.f3 .card_break_link.closed .link_upper g {
|
||||
transform: rotate(-58deg);
|
||||
}
|
||||
.f3 .card_break_link.closed .link_particles {
|
||||
transform: scale(0);
|
||||
}
|
||||
.f3 .input-field input {
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
.f3 .input-field > label:not(.label-icon).active {
|
||||
-webkit-transform: translateY(-8px) scale(0.8);
|
||||
transform: translateY(-8px) scale(0.8);
|
||||
}
|
||||
|
||||
.f3-light .link {
|
||||
stroke: black;
|
||||
}
|
||||
|
||||
.f3-export {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
/*width: 3508px;
|
||||
height: 2480px;*/
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
declare module "family-chart" {
|
||||
type f3data = any;
|
||||
type f3tree = any;
|
||||
|
||||
interface f3Rels {
|
||||
spouses?: string[];
|
||||
father?: string;
|
||||
mother?: string;
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
interface f3DataData {
|
||||
gender: "M" | "F";
|
||||
avatar?: string;
|
||||
dead: boolean;
|
||||
birthday?: string;
|
||||
deathday?: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
dateOfWedding?: string;
|
||||
wedding_state?: string;
|
||||
}
|
||||
|
||||
interface f3Data {
|
||||
id: string;
|
||||
rels: f3Rels;
|
||||
data: f3DataData;
|
||||
}
|
||||
|
||||
type f3State = {
|
||||
data: f3Data[];
|
||||
main_id?: any;
|
||||
tree?: f3tree;
|
||||
node_separation?: number;
|
||||
level_separation?: number;
|
||||
};
|
||||
|
||||
interface f3Update {
|
||||
tree: (props) => void;
|
||||
mainId: (mainId) => void;
|
||||
data: (data: f3data) => void;
|
||||
}
|
||||
|
||||
interface f3Store {
|
||||
state: f3State;
|
||||
update: f3update;
|
||||
getData: () => f3data;
|
||||
getTree: () => f3tree;
|
||||
setOnUpdate: (cb: (props) => void) => void;
|
||||
methods: any;
|
||||
}
|
||||
|
||||
function createStore(initial_state: f3State): f3Store;
|
||||
|
||||
function CalculateTree({
|
||||
data_stash,
|
||||
main_id = null,
|
||||
is_vertical = true,
|
||||
node_separation = 250,
|
||||
level_separation = 150,
|
||||
});
|
||||
|
||||
function d3AnimationView(p: {
|
||||
store: f3Store;
|
||||
cont: HTMLElement | null;
|
||||
Card?: any;
|
||||
});
|
||||
|
||||
const handlers: any;
|
||||
|
||||
type F3elements = {
|
||||
Card: (props: {
|
||||
store: f3Store;
|
||||
svg: HTMLElement;
|
||||
mini_tree: boolean;
|
||||
link_break: boolean;
|
||||
cardEditForm?: boolean;
|
||||
card_dim: {
|
||||
w: number;
|
||||
h: number;
|
||||
text_x: number;
|
||||
text_y: number;
|
||||
img_w: number;
|
||||
img_h: number;
|
||||
img_x: number;
|
||||
img_y: number;
|
||||
};
|
||||
card_display: ((data: f3Data) => string)[];
|
||||
}) => F3CardBuilder;
|
||||
};
|
||||
|
||||
type F3CardBuilder = (p: { node; d }) => HTMLElement;
|
||||
|
||||
const elements: F3elements;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Stack, TextField, Typography } from "@mui/material";
|
||||
import { NumberConstraint, ServerApi } from "../../api/ServerApi";
|
||||
import { DateValue, fmtDate } from "../../api/MemberApi";
|
||||
import { DateValue, fmtDate } from "../../api/genealogy/MemberApi";
|
||||
import { PropEdit } from "./PropEdit";
|
||||
|
||||
export function DateInput(p: {
|
||||
|
||||
@@ -2,9 +2,10 @@ import ClearIcon from "@mui/icons-material/Clear";
|
||||
import { Autocomplete, IconButton, TextField, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Member } from "../../api/MemberApi";
|
||||
import { Member } from "../../api/genealogy/MemberApi";
|
||||
import { useFamily } from "../BaseFamilyRoute";
|
||||
import { MemberItem } from "../MemberItem";
|
||||
import { useGenealogy } from "../genealogy/BaseGenealogyRoute";
|
||||
|
||||
export function MemberInput(p: {
|
||||
editable: boolean;
|
||||
@@ -15,13 +16,14 @@ export function MemberInput(p: {
|
||||
}): React.ReactElement {
|
||||
const n = useNavigate();
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
|
||||
const choices = family.members.filter(p.filter);
|
||||
const choices = genealogy.members.filter(p.filter);
|
||||
|
||||
const [inputValue, setInputValue] = React.useState("");
|
||||
|
||||
if (p.current) {
|
||||
const member = family.members.get(p.current)!;
|
||||
const member = genealogy.members.get(p.current)!;
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body2">{p.label}</Typography>
|
||||
@@ -30,7 +32,7 @@ export function MemberInput(p: {
|
||||
onClick={
|
||||
!p.editable
|
||||
? () => {
|
||||
n(family.family.URL(`member/${member.id}`));
|
||||
n(family.family.memberURL(member));
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
@@ -55,7 +57,7 @@ export function MemberInput(p: {
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
value={p.current ? family.members.get(p.current) : undefined}
|
||||
value={p.current ? genealogy.members.get(p.current) : undefined}
|
||||
onChange={(_event: any, newValue: Member | null | undefined) => {
|
||||
p.onValueChange(newValue?.id);
|
||||
}}
|
||||
|
||||
@@ -5,16 +5,20 @@ export function PropCheckbox(p: {
|
||||
label: string;
|
||||
checked: boolean | undefined;
|
||||
onValueChange: (v: boolean) => void;
|
||||
checkboxAlwaysVisible?: boolean;
|
||||
}): React.ReactElement {
|
||||
if (!p.checkboxAlwaysVisible) {
|
||||
if (!p.editable && p.checked)
|
||||
return <Typography variant="body2">{p.label}</Typography>;
|
||||
|
||||
if (!p.editable) return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
disabled={!p.editable}
|
||||
checked={p.checked}
|
||||
onChange={(e) => p.onValueChange(e.target.checked)}
|
||||
/>
|
||||
|
||||
24
geneit_app/src/widgets/forms/PropColorPicker.tsx
Normal file
24
geneit_app/src/widgets/forms/PropColorPicker.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { MuiColorInput } from "mui-color-input";
|
||||
import { PropEdit } from "./PropEdit";
|
||||
|
||||
export function PropColorPicker(p: {
|
||||
editable: boolean;
|
||||
label: string;
|
||||
value?: string;
|
||||
onChange: (v: string | undefined) => void;
|
||||
}): React.ReactElement {
|
||||
if (!p.editable) {
|
||||
if (!p.value) return <></>;
|
||||
|
||||
return <PropEdit editable={false} label={p.label} value={`#${p.value}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiColorInput
|
||||
value={"#" + (p.value ?? "")}
|
||||
fallbackValue="#ffffff"
|
||||
format="hex"
|
||||
onChange={(_v, c) => p.onChange(c.hex.substring(1))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
103
geneit_app/src/widgets/forms/PropDateInput.tsx
Normal file
103
geneit_app/src/widgets/forms/PropDateInput.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/fr";
|
||||
import { fmtUnixDate } from "../../utils/time_utils";
|
||||
import { PropEdit } from "./PropEdit";
|
||||
import { Checkbox, FormControlLabel } from "@mui/material";
|
||||
|
||||
export function PropDateInput(p: {
|
||||
editable: boolean;
|
||||
label: string;
|
||||
value: number | undefined;
|
||||
onChange: (v: number | undefined) => void;
|
||||
lastSecOfDay?: boolean;
|
||||
minDate?: number;
|
||||
maxDate?: number;
|
||||
canSetMiddleDay?: boolean;
|
||||
}): React.ReactElement {
|
||||
// Check for mid-day value
|
||||
let isMidDay = false;
|
||||
if (p.value) {
|
||||
const d = new Date(p.value * 1000);
|
||||
isMidDay =
|
||||
d.getHours() === 12 && d.getMinutes() === 0 && d.getSeconds() === 0;
|
||||
}
|
||||
|
||||
// Shift value
|
||||
let shiftV = p.value;
|
||||
if (shiftV && p.lastSecOfDay) {
|
||||
const d = new Date(shiftV * 1000);
|
||||
if (d.getHours() === 0) {
|
||||
shiftV -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!p.editable) {
|
||||
if (!shiftV) return <></>;
|
||||
|
||||
return (
|
||||
<PropEdit editable={false} label={p.label} value={fmtUnixDate(shiftV)} />
|
||||
);
|
||||
}
|
||||
|
||||
const value = dayjs(
|
||||
shiftV && p.value! > 0 ? new Date(shiftV * 1000) : undefined
|
||||
);
|
||||
|
||||
const minDate = p.minDate ? dayjs(new Date(p.minDate * 1000)) : undefined;
|
||||
const maxDate = p.maxDate ? dayjs(new Date(p.maxDate * 1000)) : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ height: "10px" }}></div>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="fr">
|
||||
<DatePicker
|
||||
label={p.label}
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
if (v && p.lastSecOfDay) {
|
||||
v = v.set("hours", 23);
|
||||
v = v.set("minutes", 59);
|
||||
v = v.set("seconds", 59);
|
||||
}
|
||||
p.onChange?.(v ? v.unix() : undefined);
|
||||
}}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
{p.canSetMiddleDay && (
|
||||
<FormControlLabel
|
||||
disabled={!p.value}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={isMidDay}
|
||||
onChange={(_ev, midDay) => {
|
||||
let v = value;
|
||||
if (midDay) {
|
||||
v = v.set("hours", 12);
|
||||
v = v.set("minutes", 0);
|
||||
v = v.set("seconds", 0);
|
||||
} else if (p.lastSecOfDay) {
|
||||
v = v.set("hours", 23);
|
||||
v = v.set("minutes", 59);
|
||||
v = v.set("seconds", 59);
|
||||
} else {
|
||||
v = v.set("hours", 0);
|
||||
v = v.set("minutes", 0);
|
||||
v = v.set("seconds", 0);
|
||||
}
|
||||
|
||||
p.onChange(v.unix());
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Mi-journée"
|
||||
/>
|
||||
)}
|
||||
<div style={{ height: "30px" }}></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export function PropEdit(p: {
|
||||
multiline?: boolean;
|
||||
minRows?: number;
|
||||
maxRows?: number;
|
||||
helperText?: string;
|
||||
}): React.ReactElement {
|
||||
if (((!p.editable && p.value) ?? "") === "") return <></>;
|
||||
|
||||
@@ -44,6 +45,7 @@ export function PropEdit(p: {
|
||||
!p.checkValue(p.value)) ||
|
||||
false
|
||||
}
|
||||
helperText={p.helperText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
|
||||
import { PropEdit } from "./PropEdit";
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
value: string | undefined;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export function PropSelect(p: {
|
||||
const value = p.options.find((o) => o.value === p.value)?.label;
|
||||
return <PropEdit label={p.label} editable={p.editing} value={value} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl fullWidth variant="filled" style={{ marginBottom: "15px" }}>
|
||||
<InputLabel>{p.label}</InputLabel>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Radio,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Sex } from "../../api/MemberApi";
|
||||
import { Sex } from "../../api/genealogy/MemberApi";
|
||||
|
||||
export function SexSelection(p: {
|
||||
readonly?: boolean;
|
||||
|
||||
73
geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx
Normal file
73
geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { CoupleApi, CouplesList } from "../../api/genealogy/CoupleApi";
|
||||
import { MemberApi, MembersList } from "../../api/genealogy/MemberApi";
|
||||
import { AsyncWidget } from "../AsyncWidget";
|
||||
import { useFamily } from "../BaseFamilyRoute";
|
||||
|
||||
interface GenealogyContext {
|
||||
members: MembersList;
|
||||
couples: CouplesList;
|
||||
reloadMembersList: () => Promise<void>;
|
||||
reloadCouplesList: () => Promise<void>;
|
||||
}
|
||||
|
||||
const GenealogyContextK = React.createContext<GenealogyContext | null>(null);
|
||||
|
||||
export function BaseGenealogyRoute(): React.ReactElement {
|
||||
const family = useFamily();
|
||||
|
||||
const [members, setMembers] = React.useState<null | MembersList>(null);
|
||||
const [couples, setCouples] = React.useState<null | CouplesList>(null);
|
||||
|
||||
const loadKey = React.useRef(1);
|
||||
|
||||
const loadPromise = React.useRef<() => void>();
|
||||
|
||||
const load = async () => {
|
||||
setMembers(await MemberApi.GetEntireList(family.familyId));
|
||||
setCouples(await CoupleApi.GetEntireList(family.familyId));
|
||||
};
|
||||
|
||||
const onReload = async () => {
|
||||
loadKey.current += 1;
|
||||
setMembers(null);
|
||||
setCouples(null);
|
||||
|
||||
return new Promise<void>((res, _rej) => {
|
||||
loadPromise.current = () => res();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
ready={members !== null && couples !== null}
|
||||
loadKey={`${family.familyId}-${loadKey.current}`}
|
||||
load={load}
|
||||
errMsg="Échec du chargement des informations de généalogie de la famille !"
|
||||
build={() => {
|
||||
if (loadPromise.current != null) {
|
||||
loadPromise.current?.();
|
||||
loadPromise.current = undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<GenealogyContextK.Provider
|
||||
value={{
|
||||
members: members!,
|
||||
couples: couples!,
|
||||
reloadMembersList: onReload,
|
||||
reloadCouplesList: onReload,
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</GenealogyContextK.Provider>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useGenealogy(): GenealogyContext {
|
||||
return React.useContext(GenealogyContextK)!;
|
||||
}
|
||||
@@ -5,14 +5,15 @@ import { IconButton, Tooltip } from "@mui/material";
|
||||
import jsPDF from "jspdf";
|
||||
import React from "react";
|
||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||
import { Couple } from "../../api/CoupleApi";
|
||||
import { Member } from "../../api/MemberApi";
|
||||
import { Couple } from "../../api/genealogy/CoupleApi";
|
||||
import { Member } from "../../api/genealogy/MemberApi";
|
||||
import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
|
||||
import { FamilyTreeNode } from "../../utils/family_tree";
|
||||
import { downloadBlob } from "../../utils/files_utils";
|
||||
import { getTextWidth } from "../../utils/render_utils";
|
||||
import "./simpletree.css";
|
||||
import "./Roboto-normal";
|
||||
import "svg2pdf.js";
|
||||
|
||||
const FACE_WIDTH = 60;
|
||||
const FACE_HEIGHT = 70;
|
||||
@@ -92,7 +93,8 @@ function buildSimpleTreeNode(
|
||||
): SimpleTreeNode {
|
||||
if (depth === 0) throw new Error("Too much recursion reached!");
|
||||
|
||||
const lastCouple = tree.couples?.[tree.couples?.length - 1 ?? 0];
|
||||
const lastCoupleId = tree.couples?.length ?? 1;
|
||||
const lastCouple = tree.couples?.[lastCoupleId - 1];
|
||||
|
||||
// Preprocess children
|
||||
let childrenToProcess = tree.down;
|
||||
@@ -254,6 +256,7 @@ function NodeArea(p: {
|
||||
let pers2 = p.node.spouse?.member;
|
||||
let didSwap = false;
|
||||
|
||||
// Show male of the left (all the time)
|
||||
if (pers2?.sex === "M") {
|
||||
let s = pers1;
|
||||
pers1 = pers2;
|
||||
@@ -293,10 +296,37 @@ function NodeArea(p: {
|
||||
let childrenLinkX: number;
|
||||
let childrenLinkY: number;
|
||||
|
||||
if (p.node.spouse) {
|
||||
// If the father is the father of all the children, while the
|
||||
// mother is not the mother of any of the children
|
||||
if (
|
||||
pers2 &&
|
||||
p.node.down.every(
|
||||
(n) => n.member.father === pers1.id && n.member.mother !== pers2!.id
|
||||
)
|
||||
) {
|
||||
childrenLinkX = parent_x_offset + Math.floor(memberCardWidth(pers1) / 2);
|
||||
childrenLinkY = p.y + CARD_HEIGHT + 2;
|
||||
}
|
||||
// If the mother is the mother of all the children, while the
|
||||
// father is not the father of any of the children
|
||||
else if (
|
||||
pers2 &&
|
||||
p.node.down.every(
|
||||
(n) => n.member.father !== pers1.id && n.member.mother === pers2!.id
|
||||
)
|
||||
) {
|
||||
childrenLinkX = beginSecondFaceX! + Math.floor(memberCardWidth(pers2) / 2);
|
||||
childrenLinkY = p.y + CARD_HEIGHT + 2;
|
||||
}
|
||||
|
||||
// Normal couple
|
||||
else if (p.node.spouse) {
|
||||
childrenLinkX = Math.floor((endFirstFaceX + beginSecondFaceX!) / 2);
|
||||
childrenLinkY = middleParentFaceY;
|
||||
} else {
|
||||
}
|
||||
|
||||
// Single person
|
||||
else {
|
||||
childrenLinkX = parent_x_offset + Math.floor(memberCardWidth(pers1) / 2);
|
||||
childrenLinkY = p.y + CARD_HEIGHT + 2;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["vite/client"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -20,7 +17,5 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ if [ ! "$DRONE_COMMIT_BRANCH" == "master" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd build && aws --endpoint-url https://s3.communiquons.org s3 sync . s3://geneit-app
|
||||
cd dist && aws --endpoint-url https://s3.communiquons.org s3 sync . s3://geneit-app
|
||||
|
||||
1
geneit_app/vite-env.d.ts
vendored
Normal file
1
geneit_app/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
15
geneit_app/vite.config.ts
Normal file
15
geneit_app/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import viteTsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
// depending on your application, base can also be "/"
|
||||
base: "/",
|
||||
plugins: [react(), viteTsconfigPaths()],
|
||||
server: {
|
||||
// this ensures that the browser opens upon server start
|
||||
open: true,
|
||||
// this sets a default port to 3000
|
||||
port: 3000,
|
||||
},
|
||||
});
|
||||
2359
geneit_backend/Cargo.lock
generated
2359
geneit_backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,34 +6,38 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.17"
|
||||
env_logger = "0.10.0"
|
||||
clap = { version = "4.3.0", features = ["derive", "env"] }
|
||||
log = "0.4.21"
|
||||
env_logger = "0.11.3"
|
||||
clap = { version = "4.5.4", features = ["derive", "env"] }
|
||||
lazy_static = "1.4.0"
|
||||
anyhow = "1.0.75"
|
||||
actix-web = "4.3.1"
|
||||
actix-cors = "0.6.4"
|
||||
actix-multipart = "0.6.1"
|
||||
lazy-regex = "3.1.0"
|
||||
anyhow = "1.0.83"
|
||||
actix-web = "4.5.1"
|
||||
actix-cors = "0.7.0"
|
||||
actix-multipart = "0.7.0"
|
||||
actix-remote-ip = "0.1.0"
|
||||
futures-util = "0.3.28"
|
||||
diesel = { version = "2.0.4", features = ["postgres"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
mailchecker = "5.0.9"
|
||||
redis = "0.23.2"
|
||||
lettre = "0.10.4"
|
||||
futures-util = "0.3.30"
|
||||
diesel = { version = "2.1.6", features = ["postgres"] }
|
||||
diesel_migrations = "2.1.0"
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
mailchecker = "6.0.4"
|
||||
redis = "0.25.3"
|
||||
lettre = "0.11.7"
|
||||
rand = "0.8.5"
|
||||
bcrypt = "0.15.0"
|
||||
light-openid = "1.0.1"
|
||||
thiserror = "1.0.40"
|
||||
serde_with = "3.1.0"
|
||||
rust_iso3166 = "0.1.10"
|
||||
rust-s3 = "0.33.0"
|
||||
sha2 = "0.10.7"
|
||||
image = "0.24.6"
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
httpdate = "1.0.2"
|
||||
zip = "0.6.6"
|
||||
bcrypt = "0.15.1"
|
||||
light-openid = "1.0.2"
|
||||
thiserror = "1.0.60"
|
||||
serde_with = "3.8.1"
|
||||
rust_iso3166 = "0.1.12"
|
||||
rust-s3 = "0.34.0"
|
||||
sha2 = "0.10.8"
|
||||
image = "0.25.1"
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
httpdate = "1.0.3"
|
||||
zip = "2.0.0"
|
||||
mime_guess = "2.0.4"
|
||||
tempfile = "3.7.1"
|
||||
base64 = "0.21.2"
|
||||
tempfile = "3.10.1"
|
||||
base64 = "0.22.0"
|
||||
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
|
||||
chrono = "0.4.38"
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libcurl4 libpq5 \
|
||||
|
||||
3
geneit_backend/build.rs
Normal file
3
geneit_backend/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
- ./storage/db:/var/lib/postgresql/data
|
||||
|
||||
mailcatcher:
|
||||
image: dockage/mailcatcher:0.8.2
|
||||
image: dockage/mailcatcher:0.9.0
|
||||
ports:
|
||||
- 1080:1080
|
||||
- 1025:1025
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Remove column to toggle genealogy
|
||||
ALTER TABLE public.families
|
||||
DROP COLUMN enable_genealogy;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Add column to toggle genealogy
|
||||
ALTER TABLE public.families
|
||||
ADD enable_genealogy boolean NOT NULL DEFAULT false;
|
||||
COMMENT
|
||||
ON COLUMN public.families.enable_genealogy IS 'Specify whether genealogy feature is enabled for the family';
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE public.families
|
||||
DROP COLUMN enable_accommodations;
|
||||
|
||||
DROP TABLE IF EXISTS accommodations_reservations_cals_urls;
|
||||
DROP TABLE IF EXISTS accommodations_reservations;
|
||||
DROP TABLE IF EXISTS accommodations_list;
|
||||
@@ -0,0 +1,52 @@
|
||||
-- Add column to toggle accommodations module
|
||||
ALTER TABLE public.families
|
||||
ADD enable_accommodations boolean NOT NULL DEFAULT false;
|
||||
COMMENT
|
||||
ON COLUMN public.families.enable_accommodations IS 'Specify whether accommodations feature is enabled for the family';
|
||||
|
||||
|
||||
-- Create tables
|
||||
CREATE TABLE IF NOT EXISTS accommodations_list
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
family_id integer NOT NULL REFERENCES families,
|
||||
time_create BIGINT NOT NULL,
|
||||
time_update BIGINT NOT NULL,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
need_validation BOOLEAN NOT NULL DEFAULT true,
|
||||
description text NULL,
|
||||
color VARCHAR(6) NULL,
|
||||
open_to_reservations BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN accommodations_list.need_validation is 'true if family admin review is required for validation. False otherwise';
|
||||
COMMENT ON COLUMN accommodations_list.open_to_reservations is 'true if reservations can be created / updated. False otherwise';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accommodations_reservations
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
family_id integer NOT NULL REFERENCES families ON DELETE CASCADE,
|
||||
accommodation_id integer NOT NULL REFERENCES accommodations_list ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
time_create BIGINT NOT NULL,
|
||||
time_update BIGINT NOT NULL,
|
||||
reservation_start BIGINT NOT NULL,
|
||||
reservation_end BIGINT NOT NULL,
|
||||
validated BOOLEAN NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN accommodations_reservations.validated is 'null if not reviewed yet. true if reservation is accepted. false if reservation is rejected';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accommodations_reservations_cals_urls
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
family_id integer NOT NULL REFERENCES families ON DELETE CASCADE,
|
||||
accommodation_id integer NULL REFERENCES accommodations_list ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
token VARCHAR(50) NOT NULL,
|
||||
time_create BIGINT NOT NULL,
|
||||
time_used BIGINT NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN accommodations_reservations_cals_urls.accommodation_id is 'null to get reservations of all accommodations. otherwise get the reservations of the specified accommodation only';
|
||||
@@ -3,9 +3,13 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use diesel::result::{DatabaseErrorKind, Error};
|
||||
use diesel::{Connection, PgConnection};
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use std::cell::RefCell;
|
||||
|
||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
|
||||
|
||||
thread_local! {
|
||||
static POSTGRES_CONNECTION: RefCell<Option<PgConnection>> = RefCell::new(None);
|
||||
static POSTGRES_CONNECTION: RefCell<Option<PgConnection>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Execute a request on the database
|
||||
@@ -39,3 +43,20 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize database connection
|
||||
pub fn initialize_conn() -> anyhow::Result<()> {
|
||||
// Run pending diesel migrations
|
||||
execute(|db| {
|
||||
let res = db
|
||||
.run_pending_migrations(MIGRATIONS)
|
||||
.expect("Failed to run database migration!");
|
||||
|
||||
for migration in res {
|
||||
log::info!("Executed database migration {migration}")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::cell::RefCell;
|
||||
use std::time::Duration;
|
||||
|
||||
thread_local! {
|
||||
static REDIS_CONNECTION: RefCell<Option<redis::Client>> = RefCell::new(None);
|
||||
static REDIS_CONNECTION: RefCell<Option<redis::Client>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Execute a request on Redis
|
||||
@@ -45,7 +45,7 @@ where
|
||||
{
|
||||
let value_str = serde_json::to_string(value)?;
|
||||
|
||||
execute_request(|conn| Ok(conn.set_ex(key, value_str, lifetime.as_secs() as usize)?))?;
|
||||
execute_request(|conn| Ok(conn.set_ex(key, value_str, lifetime.as_secs())?))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ pub async fn create_bucket_if_required() -> anyhow::Result<()> {
|
||||
log::debug!("The bucket already exists.");
|
||||
return Ok(());
|
||||
}
|
||||
Err(S3Error::Http(404, s)) if s.contains("<Code>NoSuchKey</Code>") => {
|
||||
Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchKey</Code>") => {
|
||||
log::warn!("Failed to fetch bucket location, but it seems that bucket exists.");
|
||||
return Ok(());
|
||||
}
|
||||
Err(S3Error::Http(404, s)) if s.contains("<Code>NoSuchBucket</Code>") => {
|
||||
Err(S3Error::HttpFailWithBody(404, s)) if s.contains("<Code>NoSuchBucket</Code>") => {
|
||||
log::warn!("The bucket does not seem to exists, trying to create it!")
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -60,6 +60,10 @@ pub struct StaticConstraints {
|
||||
pub member_country: SizeConstraint,
|
||||
pub member_sex: SizeConstraint,
|
||||
pub member_note: SizeConstraint,
|
||||
|
||||
pub accommodation_name_len: SizeConstraint,
|
||||
pub accommodation_description_len: SizeConstraint,
|
||||
pub accommodation_calendar_name_len: SizeConstraint,
|
||||
}
|
||||
|
||||
impl Default for StaticConstraints {
|
||||
@@ -91,6 +95,10 @@ impl Default for StaticConstraints {
|
||||
member_country: SizeConstraint::new(0, 2),
|
||||
member_sex: SizeConstraint::new(0, 1),
|
||||
member_note: SizeConstraint::new(0, 35000),
|
||||
|
||||
accommodation_name_len: SizeConstraint::new(1, 50),
|
||||
accommodation_description_len: SizeConstraint::new(0, 500),
|
||||
accommodation_calendar_name_len: SizeConstraint::new(2, 50),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,3 +142,10 @@ pub const THUMB_WIDTH: u32 = 350;
|
||||
|
||||
/// Thumbnail height
|
||||
pub const THUMB_HEIGHT: u32 = 350;
|
||||
|
||||
/// Accommodations reservations calendars tokens len
|
||||
pub const ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN: usize = 50;
|
||||
|
||||
/// Minimum interval before calendar used time update
|
||||
pub const ACCOMMODATIONS_RESERVATIONS_CAL_URL_TIME_USED_UPDATE_MIN_INTERVAL: Duration =
|
||||
Duration::from_secs(60);
|
||||
|
||||
115
geneit_backend/src/controllers/accommodations_list_controller.rs
Normal file
115
geneit_backend/src/controllers/accommodations_list_controller.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use crate::constants::StaticConstraints;
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
|
||||
use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
|
||||
use crate::models::Accommodation;
|
||||
use crate::services::accommodations_list_service;
|
||||
use actix_web::{web, HttpResponse};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum AccommodationListControllerErr {
|
||||
#[error("Invalid name length!")]
|
||||
InvalidNameLength,
|
||||
#[error("Invalid description length!")]
|
||||
InvalidDescriptionLength,
|
||||
#[error("Malformed color!")]
|
||||
MalformedColor,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
pub struct AccommodationRequest {
|
||||
pub name: String,
|
||||
pub need_validation: bool,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub open_to_reservations: bool,
|
||||
}
|
||||
|
||||
impl AccommodationRequest {
|
||||
pub async fn to_accommodation(self, accommodation: &mut Accommodation) -> anyhow::Result<()> {
|
||||
let c = StaticConstraints::default();
|
||||
|
||||
if !c.accommodation_name_len.validate(&self.name) {
|
||||
return Err(AccommodationListControllerErr::InvalidNameLength.into());
|
||||
}
|
||||
accommodation.name = self.name;
|
||||
|
||||
if let Some(d) = &self.description {
|
||||
if !c.accommodation_description_len.validate(d) {
|
||||
return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
|
||||
}
|
||||
}
|
||||
accommodation.description.clone_from(&self.description);
|
||||
|
||||
if let Some(c) = &self.color {
|
||||
if !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) {
|
||||
return Err(AccommodationListControllerErr::MalformedColor.into());
|
||||
}
|
||||
}
|
||||
accommodation.color.clone_from(&self.color);
|
||||
|
||||
accommodation.need_validation = self.need_validation;
|
||||
accommodation.open_to_reservations = self.open_to_reservations;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new accommodation
|
||||
pub async fn create(
|
||||
m: FamilyInPathWithAdminMembership,
|
||||
req: web::Json<AccommodationRequest>,
|
||||
) -> HttpResult {
|
||||
let mut accommodation = accommodations_list_service::create(m.family_id()).await?;
|
||||
|
||||
if let Err(e) = req.0.to_accommodation(&mut accommodation).await {
|
||||
log::error!("Failed to apply accommodation information! {e}");
|
||||
accommodations_list_service::delete(&mut accommodation).await?;
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
}
|
||||
|
||||
if let Err(e) = accommodations_list_service::update(&mut accommodation).await {
|
||||
log::error!("Failed to update accommodation information! {e}");
|
||||
accommodations_list_service::delete(&mut accommodation).await?;
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(accommodation))
|
||||
}
|
||||
|
||||
/// Get the full list of accommodations
|
||||
pub async fn get_full_list(m: FamilyInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(accommodations_list_service::get_all_of_family(m.family_id()).await?))
|
||||
}
|
||||
|
||||
/// Get the information of a single accommodation
|
||||
pub async fn get_single(m: FamilyAndAccommodationInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok().json(&m.to_accommodation()))
|
||||
}
|
||||
|
||||
/// Update an accommodation
|
||||
pub async fn update(
|
||||
m: FamilyAndAccommodationInPath,
|
||||
req: web::Json<AccommodationRequest>,
|
||||
_admin: FamilyInPathWithAdminMembership,
|
||||
) -> HttpResult {
|
||||
let mut accommodation = m.to_accommodation();
|
||||
|
||||
if let Err(e) = req.0.to_accommodation(&mut accommodation).await {
|
||||
log::error!("Failed to parse accommodation information! {e}");
|
||||
return Ok(HttpResponse::BadRequest().body(e.to_string()));
|
||||
}
|
||||
|
||||
accommodations_list_service::update(&mut accommodation).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
/// Delete an accommodation
|
||||
pub async fn delete(
|
||||
m: FamilyAndAccommodationInPath,
|
||||
_admin: FamilyInPathWithAdminMembership,
|
||||
) -> HttpResult {
|
||||
accommodations_list_service::delete(&mut m.to_accommodation()).await?;
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
use ical::{generator::*, *};
|
||||
|
||||
use actix_web::{web, HttpResponse};
|
||||
use chrono::DateTime;
|
||||
|
||||
use crate::constants::StaticConstraints;
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::accommodation_reservation_calendar_extractor::FamilyAndAccommodationReservationCalendarInPath;
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{AccommodationID, ReservationStatus};
|
||||
use crate::services::{
|
||||
accommodations_list_service, accommodations_reservations_calendars_service,
|
||||
accommodations_reservations_service, families_service,
|
||||
};
|
||||
use crate::utils::time_utils::time;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreateCalendarQuery {
|
||||
accommodation_id: Option<AccommodationID>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// Create a calendar
|
||||
pub async fn create(a: FamilyInPath, req: web::Json<CreateCalendarQuery>) -> HttpResult {
|
||||
let accommodation_id = match req.accommodation_id {
|
||||
Some(i) => {
|
||||
let accommodation = match accommodations_list_service::get_by_id(i).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get accommodation information! {e}");
|
||||
return Ok(HttpResponse::NotFound()
|
||||
.json("The accommodation was not found in the family!"));
|
||||
}
|
||||
};
|
||||
|
||||
if accommodation.family_id() != a.family_id() {
|
||||
return Ok(
|
||||
HttpResponse::NotFound().json("The accommodation was not found in the family!")
|
||||
);
|
||||
}
|
||||
|
||||
Some(accommodation.id())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let conf = StaticConstraints::default();
|
||||
if !conf.accommodation_calendar_name_len.validate(&req.name) {
|
||||
return Ok(HttpResponse::BadRequest().json("Invalid accommodation name!"));
|
||||
}
|
||||
|
||||
let calendar = accommodations_reservations_calendars_service::create(
|
||||
a.user_id(),
|
||||
a.family_id(),
|
||||
accommodation_id,
|
||||
&req.name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(calendar))
|
||||
}
|
||||
|
||||
/// Get the list of calendars of a user
|
||||
pub async fn get_list(a: FamilyInPath) -> HttpResult {
|
||||
let users =
|
||||
accommodations_reservations_calendars_service::get_all_of_user(a.user_id(), a.family_id())
|
||||
.await?;
|
||||
Ok(HttpResponse::Ok().json(users))
|
||||
}
|
||||
|
||||
/// Delete a calendar
|
||||
pub async fn delete(resa: FamilyAndAccommodationReservationCalendarInPath) -> HttpResult {
|
||||
accommodations_reservations_calendars_service::delete(resa.to_reservation()).await?;
|
||||
Ok(HttpResponse::Ok().json("Calendar successfully deleted"))
|
||||
}
|
||||
|
||||
fn fmt_date(time: i64) -> String {
|
||||
let res = DateTime::from_timestamp(time, 0).expect("Failed to parse date");
|
||||
|
||||
/*format!(
|
||||
"{:0>4}{:0>2}{:0>2}T{:0>2}{:0>2}",
|
||||
res.year(),
|
||||
res.month(),
|
||||
res.day(),
|
||||
res.minute(),
|
||||
res.second()
|
||||
)*/
|
||||
|
||||
res.format("%Y%m%dT%H%M%S").to_string()
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct AnonymousAccessURL {
|
||||
token: String,
|
||||
}
|
||||
|
||||
/// Get the content of the calendar
|
||||
pub async fn anonymous_access(req: web::Path<AnonymousAccessURL>) -> HttpResult {
|
||||
let mut calendar =
|
||||
match accommodations_reservations_calendars_service::get_by_token(&req.token).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!("Calendar information could not be retrieved: {e}");
|
||||
return Ok(HttpResponse::NotFound().body("Calendar not found!"));
|
||||
}
|
||||
};
|
||||
|
||||
let accommodations =
|
||||
accommodations_list_service::get_all_of_family(calendar.family_id()).await?;
|
||||
let members = families_service::get_memberships_of_family(calendar.family_id()).await?;
|
||||
|
||||
// Get calendar associated events
|
||||
let events = match calendar.accommodation_id() {
|
||||
None => {
|
||||
accommodations_reservations_service::get_all_of_family(calendar.family_id()).await?
|
||||
}
|
||||
Some(a) => accommodations_reservations_service::get_all_of_accommodation(a).await?,
|
||||
};
|
||||
|
||||
let mut cal = IcalCalendarBuilder::version("2.0")
|
||||
.gregorian()
|
||||
.prodid("-//geneit//")
|
||||
.build();
|
||||
|
||||
for ev in events {
|
||||
let accommodation = accommodations
|
||||
.iter()
|
||||
.find(|a| a.id() == ev.accommodation_id())
|
||||
.unwrap();
|
||||
let member_name = members
|
||||
.iter()
|
||||
.find(|a| a.membership.user_id() == ev.user_id())
|
||||
.map(|m| m.user_name.as_str())
|
||||
.unwrap_or("other user");
|
||||
|
||||
let event = IcalEventBuilder::tzid("Europe/Paris")
|
||||
.uid(format!("resa-{}", ev.id().0))
|
||||
.changed(fmt_date(ev.time_update))
|
||||
.start(fmt_date(ev.reservation_start))
|
||||
.end(fmt_date(ev.reservation_end))
|
||||
.set(ical_property!("SUMMARY", member_name))
|
||||
.set(ical_property!("LOCATION", &accommodation.name))
|
||||
.set(ical_property!(
|
||||
"STATUS",
|
||||
match ev.status() {
|
||||
ReservationStatus::Pending => "TENTATIVE",
|
||||
ReservationStatus::Accepted => "CONFIRMED",
|
||||
ReservationStatus::Rejected => "CANCELLED",
|
||||
}
|
||||
))
|
||||
.build();
|
||||
cal.events.push(event);
|
||||
}
|
||||
|
||||
if calendar.should_update_last_used() {
|
||||
calendar.time_used = time() as i64;
|
||||
accommodations_reservations_calendars_service::update(&calendar).await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/calendar")
|
||||
.body(cal.generate()))
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
|
||||
use crate::extractors::accommodation_reservation_extractor::FamilyAndAccommodationReservationInPath;
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{Accommodation, AccommodationReservationID, NewAccommodationReservation};
|
||||
use crate::services::accommodations_reservations_service;
|
||||
use crate::utils::time_utils::time;
|
||||
use actix_web::{web, HttpResponse};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UpdateReservationQuery {
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl UpdateReservationQuery {
|
||||
/// Check whether a reservation request is valid or not
|
||||
async fn validate(
|
||||
&self,
|
||||
a: &Accommodation,
|
||||
resa_id: Option<AccommodationReservationID>,
|
||||
) -> anyhow::Result<Option<&str>> {
|
||||
if !a.open_to_reservations {
|
||||
return Ok(Some(
|
||||
"The accommodation is not open to reservations creation / update!",
|
||||
));
|
||||
}
|
||||
|
||||
if (self.start as i64) < (time() as i64 - 3600 * 24 * 30) {
|
||||
return Ok(Some("Start time is too far in the past!"));
|
||||
}
|
||||
|
||||
if self.start == self.end {
|
||||
return Ok(Some("Start and end time must be different!"));
|
||||
}
|
||||
|
||||
if self.start > self.end {
|
||||
return Ok(Some("End time happens before start time!"));
|
||||
}
|
||||
|
||||
let existing = accommodations_reservations_service::get_reservations_for_time_interval(
|
||||
a.id(),
|
||||
self.start,
|
||||
self.end,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if existing
|
||||
.iter()
|
||||
.any(|r| r.validated != Some(false) && resa_id != Some(r.id()))
|
||||
{
|
||||
return Ok(Some("This reservation is in conflict with another one!"));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a reservation
|
||||
pub async fn create_reservation(
|
||||
a: FamilyAndAccommodationInPath,
|
||||
req: web::Json<UpdateReservationQuery>,
|
||||
) -> HttpResult {
|
||||
if let Some(err) = req.validate(&a, None).await? {
|
||||
return Ok(HttpResponse::BadRequest().json(err));
|
||||
}
|
||||
|
||||
let mut reservation =
|
||||
accommodations_reservations_service::create(&NewAccommodationReservation {
|
||||
family_id: a.family_id().0,
|
||||
accommodation_id: a.id().0,
|
||||
user_id: a.membership().user_id().0,
|
||||
time_create: time() as i64,
|
||||
time_update: time() as i64,
|
||||
reservation_start: req.start as i64,
|
||||
reservation_end: req.end as i64,
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Auto validate reservation if requested
|
||||
if !a.need_validation {
|
||||
reservation.validated = Some(true);
|
||||
|
||||
accommodations_reservations_service::update(&mut reservation).await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json(reservation))
|
||||
}
|
||||
|
||||
/// Get the reservations for a given accommodation
|
||||
pub async fn get_accommodation_reservations(a: FamilyAndAccommodationInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(accommodations_reservations_service::get_all_of_accommodation(a.id()).await?))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CheckAvailabilityQuery {
|
||||
start: usize,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
/// Check reservation availability
|
||||
pub async fn get_accommodation_reservations_for_interval(
|
||||
a: FamilyAndAccommodationInPath,
|
||||
req: web::Query<CheckAvailabilityQuery>,
|
||||
) -> HttpResult {
|
||||
if req.start > req.end {
|
||||
return Ok(HttpResponse::BadRequest().json("start should be smaller than end!"));
|
||||
}
|
||||
|
||||
let res = accommodations_reservations_service::get_reservations_for_time_interval(
|
||||
a.id(),
|
||||
req.start,
|
||||
req.end,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(res))
|
||||
}
|
||||
|
||||
/// Get the full list of accommodations reservations for a family
|
||||
pub async fn full_list(m: FamilyInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok()
|
||||
.json(accommodations_reservations_service::get_all_of_family(m.family_id()).await?))
|
||||
}
|
||||
|
||||
/// Get a single accommodation reservation
|
||||
pub async fn get_single(m: FamilyAndAccommodationReservationInPath) -> HttpResult {
|
||||
Ok(HttpResponse::Ok().json(m.to_reservation()))
|
||||
}
|
||||
|
||||
/// Update a reservation
|
||||
pub async fn update_single(
|
||||
m: FamilyAndAccommodationReservationInPath,
|
||||
req: web::Json<UpdateReservationQuery>,
|
||||
) -> HttpResult {
|
||||
if let Some(err) = req.validate(m.as_accommodation(), Some(m.id())).await? {
|
||||
return Ok(HttpResponse::BadRequest().json(err));
|
||||
}
|
||||
|
||||
if m.membership().user_id() != m.user_id() {
|
||||
return Ok(
|
||||
HttpResponse::BadRequest().json("Only the owner of a reservation can change it!")
|
||||
);
|
||||
}
|
||||
|
||||
let need_validation = m.as_accommodation().need_validation;
|
||||
|
||||
let mut reservation = m.to_reservation();
|
||||
reservation.reservation_start = req.start as i64;
|
||||
reservation.reservation_end = req.end as i64;
|
||||
|
||||
if need_validation {
|
||||
reservation.validated = None;
|
||||
}
|
||||
|
||||
accommodations_reservations_service::update(&mut reservation).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
/// Delete a reservation
|
||||
pub async fn delete(m: FamilyAndAccommodationReservationInPath) -> HttpResult {
|
||||
if m.membership().user_id() != m.user_id() {
|
||||
return Ok(
|
||||
HttpResponse::BadRequest().json("Only the owner of a reservation can delete it!")
|
||||
);
|
||||
}
|
||||
|
||||
accommodations_reservations_service::delete(m.to_reservation()).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ValidateQuery {
|
||||
validate: bool,
|
||||
}
|
||||
|
||||
/// Validate or reject a reservation
|
||||
pub async fn validate_or_reject(
|
||||
m: FamilyAndAccommodationReservationInPath,
|
||||
q: web::Json<ValidateQuery>,
|
||||
) -> HttpResult {
|
||||
if !m.membership().is_admin {
|
||||
return Ok(
|
||||
HttpResponse::BadRequest().json("Only a family admin can validate a reservation!")
|
||||
);
|
||||
}
|
||||
|
||||
if m.validated == Some(q.validate) {
|
||||
return Ok(
|
||||
HttpResponse::AlreadyReported().json("This reservation has already been processed!")
|
||||
);
|
||||
}
|
||||
|
||||
// In case of re-validation, check that the time is still available
|
||||
if m.validated == Some(false) && q.validate {
|
||||
let potential_conflicts =
|
||||
accommodations_reservations_service::get_reservations_for_time_interval(
|
||||
m.accommodation_id(),
|
||||
m.reservation_start as usize,
|
||||
m.reservation_end as usize,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if potential_conflicts
|
||||
.iter()
|
||||
.any(|a| a.validated != Some(false))
|
||||
{
|
||||
return Ok(HttpResponse::Conflict().json(
|
||||
"This cannot be accepted as it would create a conflict with another reservation!",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Update reservation validation status
|
||||
let mut reservation = m.to_reservation();
|
||||
reservation.validated = Some(q.validate);
|
||||
accommodations_reservations_service::update(&mut reservation).await?;
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use mime_guess::Mime;
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use zip::write::FileOptions;
|
||||
use zip::write::SimpleFileOptions;
|
||||
use zip::{CompressionMethod, ZipArchive};
|
||||
|
||||
const MEMBERS_FILE: &str = "members.json";
|
||||
@@ -38,7 +38,7 @@ struct ImportCoupleRequest {
|
||||
|
||||
/// Export whole family data
|
||||
pub async fn export_family(f: FamilyInPath) -> HttpResult {
|
||||
let files_opt = FileOptions::default().compression_method(CompressionMethod::Bzip2);
|
||||
let files_opt = SimpleFileOptions::default().compression_method(CompressionMethod::Bzip2);
|
||||
|
||||
let members = members_service::get_all_of_family(f.family_id()).await?;
|
||||
let couples = couples_service::get_all_of_family(f.family_id()).await?;
|
||||
|
||||
@@ -79,6 +79,8 @@ pub async fn list(token: LoginToken) -> HttpResult {
|
||||
struct RichFamilyInfo {
|
||||
#[serde(flatten)]
|
||||
membership: FamilyMembership,
|
||||
enable_genealogy: bool,
|
||||
enable_accommodations: bool,
|
||||
disable_couple_photos: bool,
|
||||
}
|
||||
|
||||
@@ -88,6 +90,8 @@ pub async fn single_info(f: FamilyInPath) -> HttpResult {
|
||||
let family = families_service::get_by_id(f.family_id()).await?;
|
||||
Ok(HttpResponse::Ok().json(RichFamilyInfo {
|
||||
membership,
|
||||
enable_genealogy: family.enable_genealogy,
|
||||
enable_accommodations: family.enable_accommodations,
|
||||
disable_couple_photos: family.disable_couple_photos,
|
||||
}))
|
||||
}
|
||||
@@ -101,8 +105,10 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UpdateFamilyBody {
|
||||
name: String,
|
||||
disable_couple_photos: bool,
|
||||
name: Option<String>,
|
||||
enable_genealogy: Option<bool>,
|
||||
enable_accommodations: Option<bool>,
|
||||
disable_couple_photos: Option<bool>,
|
||||
}
|
||||
|
||||
/// Update a family
|
||||
@@ -110,16 +116,28 @@ pub async fn update(
|
||||
f: FamilyInPathWithAdminMembership,
|
||||
req: web::Json<UpdateFamilyBody>,
|
||||
) -> HttpResult {
|
||||
if !StaticConstraints::default()
|
||||
.family_name_len
|
||||
.validate(&req.name)
|
||||
{
|
||||
let mut family = families_service::get_by_id(f.family_id()).await?;
|
||||
|
||||
if let Some(name) = &req.name {
|
||||
if !StaticConstraints::default().family_name_len.validate(name) {
|
||||
return Ok(HttpResponse::BadRequest().body("Invalid family name!"));
|
||||
}
|
||||
|
||||
let mut family = families_service::get_by_id(f.family_id()).await?;
|
||||
family.name = req.0.name;
|
||||
family.disable_couple_photos = req.0.disable_couple_photos;
|
||||
family.name = name.to_string();
|
||||
}
|
||||
|
||||
if let Some(enable_genealogy) = req.enable_genealogy {
|
||||
family.enable_genealogy = enable_genealogy;
|
||||
}
|
||||
|
||||
if let Some(enable_accommodations) = req.enable_accommodations {
|
||||
family.enable_accommodations = enable_accommodations;
|
||||
}
|
||||
|
||||
if let Some(disable_couple_photos) = req.disable_couple_photos {
|
||||
family.disable_couple_photos = disable_couple_photos;
|
||||
}
|
||||
|
||||
families_service::update_family(&family).await?;
|
||||
|
||||
log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id());
|
||||
|
||||
@@ -103,8 +103,29 @@ fn check_opt_str_val(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn trim_opt_val(val: &mut Option<String>) {
|
||||
if let Some(s) = val {
|
||||
*val = Some(s.trim().to_string());
|
||||
}
|
||||
|
||||
if val.as_deref() == Some("") {
|
||||
*val = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl MemberRequest {
|
||||
pub async fn to_member(self, member: &mut Member) -> anyhow::Result<()> {
|
||||
pub async fn to_member(mut self, member: &mut Member) -> anyhow::Result<()> {
|
||||
// Trim values before processing
|
||||
trim_opt_val(&mut self.first_name);
|
||||
trim_opt_val(&mut self.last_name);
|
||||
trim_opt_val(&mut self.birth_last_name);
|
||||
trim_opt_val(&mut self.email);
|
||||
trim_opt_val(&mut self.country);
|
||||
trim_opt_val(&mut self.address);
|
||||
trim_opt_val(&mut self.city);
|
||||
trim_opt_val(&mut self.note);
|
||||
trim_opt_val(&mut self.phone);
|
||||
|
||||
let c = StaticConstraints::default();
|
||||
check_opt_str_val(
|
||||
&self.first_name,
|
||||
|
||||
@@ -5,6 +5,9 @@ use actix_web::HttpResponse;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use zip::result::ZipError;
|
||||
|
||||
pub mod accommodations_list_controller;
|
||||
pub mod accommodations_reservations_calendars_controller;
|
||||
pub mod accommodations_reservations_controller;
|
||||
pub mod auth_controller;
|
||||
pub mod couples_controller;
|
||||
pub mod data_controller;
|
||||
|
||||
83
geneit_backend/src/extractors/accommodation_extractor.rs
Normal file
83
geneit_backend/src/extractors/accommodation_extractor.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{Accommodation, AccommodationID, FamilyID, Membership};
|
||||
use crate::services::accommodations_list_service;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum AccommodationExtractorErr {
|
||||
#[error("Accommodation {0:?} does not belong to family {1:?}!")]
|
||||
AccommodationNotInFamily(AccommodationID, FamilyID),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FamilyAndAccommodationInPath(Membership, Accommodation);
|
||||
|
||||
impl FamilyAndAccommodationInPath {
|
||||
async fn load_accommodation_from_path(
|
||||
family: FamilyInPath,
|
||||
accommodation_id: AccommodationID,
|
||||
) -> anyhow::Result<Self> {
|
||||
let accommodation = accommodations_list_service::get_by_id(accommodation_id).await?;
|
||||
if accommodation.family_id() != family.family_id() {
|
||||
return Err(AccommodationExtractorErr::AccommodationNotInFamily(
|
||||
accommodation.id(),
|
||||
family.family_id(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
Ok(Self(family.into(), accommodation))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FamilyAndAccommodationInPath {
|
||||
type Target = Accommodation;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl FamilyAndAccommodationInPath {
|
||||
pub fn membership(&self) -> &Membership {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_accommodation(self) -> Accommodation {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AccommodationIDInPath {
|
||||
accommodation_id: AccommodationID,
|
||||
}
|
||||
|
||||
impl FromRequest for FamilyAndAccommodationInPath {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
Box::pin(async move {
|
||||
let family = FamilyInPath::extract(&req).await?;
|
||||
|
||||
let accommodation_id = actix_web::web::Path::<AccommodationIDInPath>::from_request(
|
||||
&req,
|
||||
&mut Payload::None,
|
||||
)
|
||||
.await?
|
||||
.accommodation_id;
|
||||
|
||||
Self::load_accommodation_from_path(family, accommodation_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to extract accommodation ID from URL! {}", e);
|
||||
actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{
|
||||
AccommodationReservationCalendar, AccommodationReservationCalendarID, FamilyID, Membership,
|
||||
};
|
||||
use crate::services::accommodations_reservations_calendars_service;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use serde::Deserialize;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum AccommodationCalendarExtractorErr {
|
||||
#[error("Calendar {0:?} does not belong to user or family {1:?}!")]
|
||||
CalendarNotOfUserOrFamily(AccommodationReservationCalendarID, FamilyID),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FamilyAndAccommodationReservationCalendarInPath(
|
||||
Membership,
|
||||
AccommodationReservationCalendar,
|
||||
);
|
||||
|
||||
impl FamilyAndAccommodationReservationCalendarInPath {
|
||||
async fn load_calendar_from_path(
|
||||
family: FamilyInPath,
|
||||
calendar_id: AccommodationReservationCalendarID,
|
||||
) -> anyhow::Result<Self> {
|
||||
let accommodation =
|
||||
accommodations_reservations_calendars_service::get_by_id(calendar_id).await?;
|
||||
if accommodation.family_id() != family.family_id()
|
||||
|| accommodation.user_id() != family.user_id()
|
||||
{
|
||||
return Err(
|
||||
AccommodationCalendarExtractorErr::CalendarNotOfUserOrFamily(
|
||||
accommodation.id(),
|
||||
family.family_id(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self(family.into(), accommodation))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FamilyAndAccommodationReservationCalendarInPath {
|
||||
type Target = AccommodationReservationCalendar;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl FamilyAndAccommodationReservationCalendarInPath {
|
||||
pub fn membership(&self) -> &Membership {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_reservation(self) -> AccommodationReservationCalendar {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AccommodationIDInPath {
|
||||
cal_id: AccommodationReservationCalendarID,
|
||||
}
|
||||
|
||||
impl FromRequest for FamilyAndAccommodationReservationCalendarInPath {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
Box::pin(async move {
|
||||
let family = FamilyInPath::extract(&req).await?;
|
||||
|
||||
let accommodation_id = actix_web::web::Path::<AccommodationIDInPath>::from_request(
|
||||
&req,
|
||||
&mut Payload::None,
|
||||
)
|
||||
.await?
|
||||
.cal_id;
|
||||
|
||||
Self::load_calendar_from_path(family, accommodation_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to extract calendar ID from URL! {}", e);
|
||||
actix_web::error::ErrorNotFound("Could not fetch calendar information!")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use crate::extractors::family_extractor::FamilyInPath;
|
||||
use crate::models::{
|
||||
Accommodation, AccommodationReservation, AccommodationReservationID, FamilyID, Membership,
|
||||
};
|
||||
use crate::services::{accommodations_list_service, accommodations_reservations_service};
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum AccommodationReservationExtractorErr {
|
||||
#[error("Accommodation reservation {0:?} does not belong to family {1:?}!")]
|
||||
AccommodationNotInFamily(AccommodationReservationID, FamilyID),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FamilyAndAccommodationReservationInPath(
|
||||
Membership,
|
||||
Accommodation,
|
||||
AccommodationReservation,
|
||||
);
|
||||
|
||||
impl FamilyAndAccommodationReservationInPath {
|
||||
async fn load_accommodation_reservation_from_path(
|
||||
family: FamilyInPath,
|
||||
reservation_id: AccommodationReservationID,
|
||||
) -> anyhow::Result<Self> {
|
||||
let reservation = accommodations_reservations_service::get_by_id(reservation_id).await?;
|
||||
let accommodation =
|
||||
accommodations_list_service::get_by_id(reservation.accommodation_id()).await?;
|
||||
|
||||
if accommodation.family_id() != family.family_id()
|
||||
|| reservation.family_id() != family.family_id()
|
||||
{
|
||||
return Err(
|
||||
AccommodationReservationExtractorErr::AccommodationNotInFamily(
|
||||
reservation.id(),
|
||||
family.family_id(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self(family.into(), accommodation, reservation))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FamilyAndAccommodationReservationInPath {
|
||||
type Target = AccommodationReservation;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.2
|
||||
}
|
||||
}
|
||||
|
||||
impl FamilyAndAccommodationReservationInPath {
|
||||
pub fn membership(&self) -> &Membership {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn as_accommodation(&self) -> &Accommodation {
|
||||
&self.1
|
||||
}
|
||||
|
||||
pub fn to_accommodation(self) -> Accommodation {
|
||||
self.1
|
||||
}
|
||||
|
||||
pub fn to_reservation(self) -> AccommodationReservation {
|
||||
self.2
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReservationIDInPath {
|
||||
reservation_id: AccommodationReservationID,
|
||||
}
|
||||
|
||||
impl FromRequest for FamilyAndAccommodationReservationInPath {
|
||||
type Error = actix_web::Error;
|
||||
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
let req = req.clone();
|
||||
Box::pin(async move {
|
||||
let family = FamilyInPath::extract(&req).await?;
|
||||
|
||||
let reservation_id =
|
||||
actix_web::web::Path::<ReservationIDInPath>::from_request(&req, &mut Payload::None)
|
||||
.await?
|
||||
.reservation_id;
|
||||
|
||||
Self::load_accommodation_reservation_from_path(family, reservation_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to extract accommodation ID from URL! {}", e);
|
||||
actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
pub mod accommodation_extractor;
|
||||
pub mod accommodation_reservation_calendar_extractor;
|
||||
pub mod accommodation_reservation_extractor;
|
||||
pub mod couple_extractor;
|
||||
pub mod family_extractor;
|
||||
pub mod member_extractor;
|
||||
|
||||
@@ -4,10 +4,12 @@ use actix_remote_ip::RemoteIPConfig;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use geneit_backend::app_config::AppConfig;
|
||||
use geneit_backend::connections::s3_connection;
|
||||
use geneit_backend::connections::{db_connection, s3_connection};
|
||||
use geneit_backend::controllers::{
|
||||
auth_controller, couples_controller, data_controller, families_controller, members_controller,
|
||||
photos_controller, server_controller, users_controller,
|
||||
accommodations_list_controller, accommodations_reservations_calendars_controller,
|
||||
accommodations_reservations_controller, auth_controller, couples_controller, data_controller,
|
||||
families_controller, members_controller, photos_controller, server_controller,
|
||||
users_controller,
|
||||
};
|
||||
|
||||
#[actix_web::main]
|
||||
@@ -22,6 +24,10 @@ async fn main() -> std::io::Result<()> {
|
||||
.await
|
||||
.expect("Failed to initialize S3 bucket!");
|
||||
|
||||
// Initialize database connection
|
||||
log::debug!("Initialize database connection");
|
||||
db_connection::initialize_conn().expect("Failed to initialize database connection!");
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(
|
||||
@@ -133,73 +139,146 @@ async fn main() -> std::io::Result<()> {
|
||||
"/family/{id}/user/{user_id}",
|
||||
web::delete().to(families_controller::delete_membership),
|
||||
)
|
||||
// Members controller
|
||||
// [GENEALOGY] Members controller
|
||||
.route(
|
||||
"/family/{id}/member/create",
|
||||
"/family/{id}/genealogy/member/create",
|
||||
web::post().to(members_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/members",
|
||||
"/family/{id}/genealogy/members",
|
||||
web::get().to(members_controller::get_all),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/member/{member_id}",
|
||||
"/family/{id}/genealogy/member/{member_id}",
|
||||
web::get().to(members_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/member/{member_id}",
|
||||
"/family/{id}/genealogy/member/{member_id}",
|
||||
web::put().to(members_controller::update),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/member/{member_id}",
|
||||
"/family/{id}/genealogy/member/{member_id}",
|
||||
web::delete().to(members_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/member/{member_id}/photo",
|
||||
"/family/{id}/genealogy/member/{member_id}/photo",
|
||||
web::put().to(members_controller::set_photo),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/member/{member_id}/photo",
|
||||
"/family/{id}/genealogy/member/{member_id}/photo",
|
||||
web::delete().to(members_controller::remove_photo),
|
||||
)
|
||||
// Couples controller
|
||||
// [GENEALOGY] Couples controller
|
||||
.route(
|
||||
"/family/{id}/couple/create",
|
||||
"/family/{id}/genealogy/couple/create",
|
||||
web::post().to(couples_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/couples",
|
||||
"/family/{id}/genealogy/couples",
|
||||
web::get().to(couples_controller::get_all),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/couple/{couple_id}",
|
||||
"/family/{id}/genealogy/couple/{couple_id}",
|
||||
web::get().to(couples_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/couple/{couple_id}",
|
||||
"/family/{id}/genealogy/couple/{couple_id}",
|
||||
web::put().to(couples_controller::update),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/couple/{couple_id}",
|
||||
"/family/{id}/genealogy/couple/{couple_id}",
|
||||
web::delete().to(couples_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/couple/{couple_id}/photo",
|
||||
"/family/{id}/genealogy/couple/{couple_id}/photo",
|
||||
web::put().to(couples_controller::set_photo),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/couple/{couple_id}/photo",
|
||||
"/family/{id}/genealogy/couple/{couple_id}/photo",
|
||||
web::delete().to(couples_controller::remove_photo),
|
||||
)
|
||||
// Data controller
|
||||
// [GENEALOGY] Data controller
|
||||
.route(
|
||||
"/family/{id}/data/export",
|
||||
"/family/{id}/genealogy/data/export",
|
||||
web::get().to(data_controller::export_family),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/data/import",
|
||||
"/family/{id}/genealogy/data/import",
|
||||
web::put().to(data_controller::import_family),
|
||||
)
|
||||
// [ACCOMODATIONS] List controller
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/create",
|
||||
web::post().to(accommodations_list_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/list",
|
||||
web::get().to(accommodations_list_controller::get_full_list),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::get().to(accommodations_list_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::put().to(accommodations_list_controller::update),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/list/{accommodation_id}",
|
||||
web::delete().to(accommodations_list_controller::delete),
|
||||
)
|
||||
// [ACCOMODATIONS] Reservations controller
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}",
|
||||
web::get()
|
||||
.to(accommodations_reservations_controller::get_accommodation_reservations),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval",
|
||||
web::get()
|
||||
.to(accommodations_reservations_controller::get_accommodation_reservations_for_interval),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations/full_list",
|
||||
web::get().to(accommodations_reservations_controller::full_list),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/create",
|
||||
web::post().to(accommodations_reservations_controller::create_reservation),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::get().to(accommodations_reservations_controller::get_single),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::patch().to(accommodations_reservations_controller::update_single),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}",
|
||||
web::delete().to(accommodations_reservations_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservation/{reservation_id}/validate",
|
||||
web::post().to(accommodations_reservations_controller::validate_or_reject),
|
||||
)
|
||||
// [ACCOMMODATIONS] Calendars controller
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations_calendars/create",
|
||||
web::post().to(accommodations_reservations_calendars_controller::create),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations_calendars/list",
|
||||
web::get().to(accommodations_reservations_calendars_controller::get_list),
|
||||
)
|
||||
.route(
|
||||
"/family/{id}/accommodations/reservations_calendars/{cal_id}",
|
||||
web::delete().to(accommodations_reservations_calendars_controller::delete),
|
||||
)
|
||||
.route(
|
||||
"/acccommodations_calendar/{token}",
|
||||
web::get().to(accommodations_reservations_calendars_controller::anonymous_access),
|
||||
)
|
||||
// Photos controller
|
||||
.route(
|
||||
"/photo/{id}",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::schema::{couples, families, members, memberships, photos, users};
|
||||
use crate::constants;
|
||||
use crate::schema::{
|
||||
accommodations_list, accommodations_reservations, accommodations_reservations_cals_urls,
|
||||
couples, families, members, memberships, photos, users,
|
||||
};
|
||||
use crate::utils::crypt_utils::sha256;
|
||||
use crate::utils::time_utils::time;
|
||||
use diesel::prelude::*;
|
||||
|
||||
/// User ID holder
|
||||
@@ -65,6 +70,8 @@ pub struct Family {
|
||||
pub name: String,
|
||||
pub invitation_code: String,
|
||||
pub disable_couple_photos: bool,
|
||||
pub enable_genealogy: bool,
|
||||
pub enable_accommodations: bool,
|
||||
}
|
||||
|
||||
impl Family {
|
||||
@@ -307,7 +314,7 @@ pub struct NewMember {
|
||||
pub time_update: i64,
|
||||
}
|
||||
|
||||
/// Member ID holder
|
||||
/// Couple ID holder
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||
pub struct CoupleID(pub i32);
|
||||
|
||||
@@ -440,3 +447,153 @@ pub struct NewCouple {
|
||||
pub time_create: i64,
|
||||
pub time_update: i64,
|
||||
}
|
||||
|
||||
/// Accommodation ID holder
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||
pub struct AccommodationID(pub i32);
|
||||
|
||||
#[derive(Queryable, Debug, serde::Serialize)]
|
||||
pub struct Accommodation {
|
||||
id: i32,
|
||||
family_id: i32,
|
||||
time_create: i64,
|
||||
pub time_update: i64,
|
||||
pub name: String,
|
||||
pub need_validation: bool,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub open_to_reservations: bool,
|
||||
}
|
||||
|
||||
impl Accommodation {
|
||||
pub fn id(&self) -> AccommodationID {
|
||||
AccommodationID(self.id)
|
||||
}
|
||||
|
||||
pub fn family_id(&self) -> FamilyID {
|
||||
FamilyID(self.family_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = accommodations_list)]
|
||||
pub struct NewAccommodation {
|
||||
pub family_id: i32,
|
||||
pub name: String,
|
||||
pub time_create: i64,
|
||||
pub time_update: i64,
|
||||
}
|
||||
|
||||
/// Accommodation reservation ID holder
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||
pub struct AccommodationReservationID(pub i32);
|
||||
|
||||
pub enum ReservationStatus {
|
||||
Pending,
|
||||
Accepted,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Debug, serde::Serialize)]
|
||||
pub struct AccommodationReservation {
|
||||
id: i32,
|
||||
family_id: i32,
|
||||
accommodation_id: i32,
|
||||
user_id: i32,
|
||||
time_create: i64,
|
||||
pub time_update: i64,
|
||||
pub reservation_start: i64,
|
||||
pub reservation_end: i64,
|
||||
pub validated: Option<bool>,
|
||||
}
|
||||
|
||||
impl AccommodationReservation {
|
||||
pub fn id(&self) -> AccommodationReservationID {
|
||||
AccommodationReservationID(self.id)
|
||||
}
|
||||
|
||||
pub fn accommodation_id(&self) -> AccommodationID {
|
||||
AccommodationID(self.accommodation_id)
|
||||
}
|
||||
|
||||
pub fn family_id(&self) -> FamilyID {
|
||||
FamilyID(self.family_id)
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> UserID {
|
||||
UserID(self.user_id)
|
||||
}
|
||||
|
||||
pub fn status(&self) -> ReservationStatus {
|
||||
match self.validated {
|
||||
None => ReservationStatus::Pending,
|
||||
Some(true) => ReservationStatus::Accepted,
|
||||
Some(false) => ReservationStatus::Rejected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = accommodations_reservations)]
|
||||
pub struct NewAccommodationReservation {
|
||||
pub family_id: i32,
|
||||
pub accommodation_id: i32,
|
||||
pub user_id: i32,
|
||||
pub time_create: i64,
|
||||
pub time_update: i64,
|
||||
pub reservation_start: i64,
|
||||
pub reservation_end: i64,
|
||||
}
|
||||
|
||||
/// Accommodation reservation calendar ID holder
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||
pub struct AccommodationReservationCalendarID(pub i32);
|
||||
|
||||
#[derive(Queryable, Debug, serde::Serialize)]
|
||||
pub struct AccommodationReservationCalendar {
|
||||
id: i32,
|
||||
family_id: i32,
|
||||
accommodation_id: Option<i32>,
|
||||
user_id: i32,
|
||||
name: String,
|
||||
token: String,
|
||||
pub time_create: i64,
|
||||
pub time_used: i64,
|
||||
}
|
||||
|
||||
impl AccommodationReservationCalendar {
|
||||
pub fn id(&self) -> AccommodationReservationCalendarID {
|
||||
AccommodationReservationCalendarID(self.id)
|
||||
}
|
||||
|
||||
pub fn accommodation_id(&self) -> Option<AccommodationID> {
|
||||
self.accommodation_id.map(AccommodationID)
|
||||
}
|
||||
|
||||
pub fn family_id(&self) -> FamilyID {
|
||||
FamilyID(self.family_id)
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> UserID {
|
||||
UserID(self.user_id)
|
||||
}
|
||||
|
||||
pub fn should_update_last_used(&self) -> bool {
|
||||
(self.time_used
|
||||
+ constants::ACCOMMODATIONS_RESERVATIONS_CAL_URL_TIME_USED_UPDATE_MIN_INTERVAL.as_secs()
|
||||
as i64)
|
||||
< time() as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = accommodations_reservations_cals_urls)]
|
||||
pub struct NewAccommodationReservationCalendar {
|
||||
pub family_id: i32,
|
||||
pub accommodation_id: Option<i32>,
|
||||
pub user_id: i32,
|
||||
pub name: String,
|
||||
pub token: String,
|
||||
pub time_create: i64,
|
||||
pub time_used: i64,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user