284 Commits

Author SHA1 Message Date
7c2abeece4 Update Rust crate actix-multipart to 0.7.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-07 00:21:18 +00:00
7716ed1243 Update dependency @mui/icons-material to v5.16.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-06 00:28:37 +00:00
0f52820601 Update dependency @mui/lab to v5.0.0-alpha.171
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-06 00:20:19 +00:00
4652c4acc0 Update dependency @mui/x-data-grid to v7.8.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-05 00:32:09 +00:00
523c987722 Update dependency vite to v5.3.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-05 00:20:26 +00:00
55012c9b2f Update dependency react-router-dom to v6.24.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-04 00:32:53 +00:00
7f7db12431 Update Rust crate serde_with to v3.8.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-04 00:20:47 +00:00
0091863805 Merge pull request 'Update dependency react-zoom-pan-pinch to v3.6.1' (#242) from renovate/react-zoom-pan-pinch-3.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #242
2024-07-03 09:19:00 +00:00
ce20535624 Update dependency typescript to v5.5.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-03 00:20:23 +00:00
e07b829237 Update dependency react-zoom-pan-pinch to v3.6.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-07-03 00:20:19 +00:00
2808ff0f5d Update dependency @mui/material to v5.15.21
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-02 00:41:56 +00:00
660db82197 Update Rust crate serde_json to v1.0.120
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-02 00:20:49 +00:00
e9aa6b82d2 Update Rust crate serde_with to v3.8.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-01 00:22:46 +00:00
9daecb947e Update Rust crate serde_json to v1.0.119
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-01 00:06:13 +00:00
93eeb0c1a6 Update dependency @mui/icons-material to v5.15.21
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-30 00:20:59 +00:00
a67e7cdef1 Update Rust crate mime_guess to v2.0.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-30 00:06:16 +00:00
c2bc4ae35e Merge pull request 'Update Rust crate clap to v4.5.8' (#234) from renovate/clap-4.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #234
2024-06-29 08:41:53 +00:00
a498707feb Update Rust crate log to v0.4.22
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-29 00:15:27 +00:00
54df782eb4 Update Rust crate clap to v4.5.8
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-29 00:15:23 +00:00
946f3fd651 Update dependency react-zoom-pan-pinch to v3.6.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-28 00:26:41 +00:00
6215eccc37 Update dependency vite to v5.3.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-28 00:14:31 +00:00
5150c035be Update Rust crate serde_json to v1.0.118
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-26 00:14:49 +00:00
728fa8f5bb Update dependency react-router-dom to v6.24.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-25 00:29:25 +00:00
939eebe0fe Update Rust crate uuid to v1.9.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-25 00:15:09 +00:00
24c17b9dcf Update dependency react-zoom-pan-pinch to v3.5.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-24 00:14:19 +00:00
c5910ad1ae Update dependency react-qr-code to v2.0.15
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-23 00:21:54 +00:00
9cb9d93622 Update dependency @mui/x-date-pickers to v7.7.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-23 00:14:18 +00:00
1a890844ef Add an accommodations reservations module (#188)
All checks were successful
continuous-integration/drone/push Build is passing
Add a new module to enable accommodations reservation

![](https://gitea.communiquons.org/attachments/de1f5b12-0a93-40f8-b29d-97665daa6fd5)

Reviewed-on: #188
2024-06-22 21:30:26 +00:00
8ecacbe622 Update dependency @mui/x-tree-view to v7.7.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-22 00:28:16 +00:00
6efa4123cf Update dependency @mui/x-data-grid to v7.7.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-22 00:14:48 +00:00
0353fe8435 Update dependency typescript to v5.5.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-21 00:29:18 +00:00
f7eeefa226 Update Rust crate actix-web to v4.8.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-21 00:14:49 +00:00
6561d9f09c Update dependency svg2pdf.js to v2.2.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-18 00:15:27 +00:00
b6ad281a10 Update dependency vite to v5.3.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-16 00:14:24 +00:00
80737c21a5 Update Rust crate rust-s3 to 0.34.0 (#218)
All checks were successful
continuous-integration/drone/push Build is passing
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rust-s3](https://github.com/durch/rust-s3) | dependencies | minor | `0.33.0` -> `0.34.0` |

---

### Release Notes

<details>
<summary>durch/rust-s3 (rust-s3)</summary>

### [`v0.34.0`](https://github.com/durch/rust-s3/releases/tag/v0.34.0)

[Compare Source](https://github.com/durch/rust-s3/compare/v0.33.0...v0.34.0)

#### What's Changed

-   Fix etag generation for small files by [@&#8203;theCapypara](https://github.com/theCapypara) in https://github.com/durch/rust-s3/pull/324
-   add support for new region eu-central-2 by [@&#8203;robinfriedli](https://github.com/robinfriedli) in https://github.com/durch/rust-s3/pull/314
-   fix: presigned custom header order by [@&#8203;zifeo](https://github.com/zifeo) in https://github.com/durch/rust-s3/pull/309
-   refactor(s3): don't depend on reqwest by [@&#8203;tomkarw](https://github.com/tomkarw) in https://github.com/durch/rust-s3/pull/299
-   Remove superfluous println by [@&#8203;zaninime](https://github.com/zaninime) in https://github.com/durch/rust-s3/pull/345
-   Move to the smaller, cargo-team maintained `home` crate by [@&#8203;utkarshgupta137](https://github.com/utkarshgupta137) in https://github.com/durch/rust-s3/pull/338
-   remove misleading comment from GCP example by [@&#8203;pascualex](https://github.com/pascualex) in https://github.com/durch/rust-s3/pull/356
-   Add il-central-1 region by [@&#8203;ArielHorwitz](https://github.com/ArielHorwitz) in https://github.com/durch/rust-s3/pull/359
-   Add feature-gate on Credentials::refresh by [@&#8203;olback](https://github.com/olback) in https://github.com/durch/rust-s3/pull/361
-   Only send metadata on Initiate of multipart upload by [@&#8203;jcrossley3](https://github.com/jcrossley3) in https://github.com/durch/rust-s3/pull/353
-   Fix put_object_stream sync for small files by [@&#8203;jsurkont](https://github.com/jsurkont) in https://github.com/durch/rust-s3/pull/357
-   Rework presign_post to be correct by [@&#8203;urkle](https://github.com/urkle) in https://github.com/durch/rust-s3/pull/358
-   Implement a new `ListBuckets` command and its consumer API. by [@&#8203;aalekhpatel07](https://github.com/aalekhpatel07) in https://github.com/durch/rust-s3/pull/348
-   feat: re-expose 'http-credentials' from 's3' by [@&#8203;woshilapin](https://github.com/woshilapin) in https://github.com/durch/rust-s3/pull/364
-   Fix Bytes version requirement by [@&#8203;Ten0](https://github.com/Ten0) in https://github.com/durch/rust-s3/pull/365
-   Fix fail-on-err description by [@&#8203;randomairborne](https://github.com/randomairborne) in https://github.com/durch/rust-s3/pull/371

#### New Contributors

-   [@&#8203;theCapypara](https://github.com/theCapypara) made their first contribution in https://github.com/durch/rust-s3/pull/324
-   [@&#8203;robinfriedli](https://github.com/robinfriedli) made their first contribution in https://github.com/durch/rust-s3/pull/314
-   [@&#8203;zifeo](https://github.com/zifeo) made their first contribution in https://github.com/durch/rust-s3/pull/309
-   [@&#8203;tomkarw](https://github.com/tomkarw) made their first contribution in https://github.com/durch/rust-s3/pull/299
-   [@&#8203;zaninime](https://github.com/zaninime) made their first contribution in https://github.com/durch/rust-s3/pull/345
-   [@&#8203;utkarshgupta137](https://github.com/utkarshgupta137) made their first contribution in https://github.com/durch/rust-s3/pull/338
-   [@&#8203;pascualex](https://github.com/pascualex) made their first contribution in https://github.com/durch/rust-s3/pull/356
-   [@&#8203;ArielHorwitz](https://github.com/ArielHorwitz) made their first contribution in https://github.com/durch/rust-s3/pull/359
-   [@&#8203;olback](https://github.com/olback) made their first contribution in https://github.com/durch/rust-s3/pull/361
-   [@&#8203;jcrossley3](https://github.com/jcrossley3) made their first contribution in https://github.com/durch/rust-s3/pull/353
-   [@&#8203;jsurkont](https://github.com/jsurkont) made their first contribution in https://github.com/durch/rust-s3/pull/357
-   [@&#8203;urkle](https://github.com/urkle) made their first contribution in https://github.com/durch/rust-s3/pull/358
-   [@&#8203;aalekhpatel07](https://github.com/aalekhpatel07) made their first contribution in https://github.com/durch/rust-s3/pull/348
-   [@&#8203;woshilapin](https://github.com/woshilapin) made their first contribution in https://github.com/durch/rust-s3/pull/364
-   [@&#8203;randomairborne](https://github.com/randomairborne) made their first contribution in https://github.com/durch/rust-s3/pull/371

**Full Changelog**: https://github.com/durch/rust-s3/compare/v0.33.0...v0.34.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MDguMSIsInVwZGF0ZWRJblZlciI6IjM3LjQwOC4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: #218
2024-06-15 08:06:21 +00:00
c6315ea181 Update dependency @mui/x-tree-view to v7.7.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-15 00:14:49 +00:00
fcc33fc458 Merge pull request 'Update Rust crate diesel to v2.2.1' (#216) from renovate/diesel-2.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #216
2024-06-14 16:47:27 +00:00
c712347781 Update dependency @mui/x-data-grid to v7.7.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-14 00:14:45 +00:00
a4c3d774ff Update Rust crate diesel to v2.2.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-14 00:14:32 +00:00
f027c77a1e Merge pull request 'Update dependency @mui/material to v5.15.20' (#215) from renovate/mui-material-5.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #215
2024-06-13 17:00:47 +00:00
100df1e4aa Update dependency @mui/material to v5.15.20
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-13 00:12:24 +00:00
bd3aeb92f1 Update dependency @mui/icons-material to v5.15.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-13 00:12:21 +00:00
e96d55a7e1 Update dependency @vitejs/plugin-react to v4.3.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-12 00:11:50 +00:00
0e7d7675d3 Update dependency @testing-library/jest-dom to v6.4.6
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-11 00:29:42 +00:00
13dbd0a6f5 Update Rust crate clap to v4.5.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-11 00:12:35 +00:00
a6c213b083 Update Rust crate actix-web to v4.7.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-09 00:28:11 +00:00
f492c0be1c Update Rust crate actix-multipart to v0.6.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-09 00:13:04 +00:00
23058d3a2c Update dependency vite to v5.2.13
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-08 00:19:36 +00:00
9cdf45de5e Update dependency @mui/x-tree-view to v7.6.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-08 00:11:53 +00:00
508ae60c18 Update dependency @mui/x-data-grid to v7.6.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-07 00:24:09 +00:00
167ddda2d4 Update Rust crate clap to v4.5.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-07 00:12:26 +00:00
d286e0d827 Merge pull request 'Update dependency @testing-library/react to v16' (#203) from renovate/testing-library-react-16.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #203
2024-06-06 13:16:15 +00:00
4450f17e8d Update Rust crate zip to v2.1.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-06 00:12:44 +00:00
f9332777dd Update dependency @testing-library/react to v16
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-04 00:12:10 +00:00
83adb86988 Update Rust crate zip to v2.1.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-03 00:12:03 +00:00
3edc78ec2a Update dependency @mui/x-tree-view to v7.6.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-01 00:19:47 +00:00
598839407c Update Rust crate diesel_migrations to v2.2.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-31 21:17:34 +00:00
40fd20a0cf Update dependency @mui/x-data-grid to v7.6.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-31 21:11:57 +00:00
8ef544cd0f Update dependency @mui/x-data-grid to v7.6.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-31 00:28:15 +00:00
c67440813e Update dependency @mui/material to v5.15.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-31 00:19:44 +00:00
47ed9325ee Merge pull request 'Update dependency @mui/icons-material to v5.15.19' (#196) from renovate/mui-icons-material-5.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #196
2024-05-30 19:59:20 +00:00
d93b6a78e0 Update dependency @mui/icons-material to v5.15.19
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-30 00:20:19 +00:00
5d03060639 Update Rust crate redis to v0.25.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-30 00:20:12 +00:00
8047d72a35 Update Rust crate zip to v2.1.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-29 00:28:22 +00:00
977aa18d38 Update dependency vite to v5.2.12
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-29 00:19:48 +00:00
cc4c3ea67e Merge pull request 'Update Rust crate zip to v2' (#191) from renovate/zip-2.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #191
2024-05-28 19:30:46 +00:00
81eb9d8f44 Update Rust crate serde to v1.0.203
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-26 00:25:25 +00:00
560a72944b Update Rust crate zip to v2
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-25 00:24:55 +00:00
6534d160e5 Merge pull request 'Update node Docker tag to v22' (#174) from renovate/node-22.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #174
2024-05-24 13:13:22 +00:00
cac5fd6866 Update dependency @mui/x-tree-view to v7.5.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-24 00:50:59 +00:00
b21d5b6683 Update node Docker tag to v22
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-24 00:25:29 +00:00
6e9dafebe0 Update dependency @mui/x-data-grid to v7.5.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-24 00:25:22 +00:00
09f2ae9213 Merge pull request 'Update Rust crate anyhow to v1.0.86' (#181) from renovate/anyhow-1.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #181
2024-05-23 09:22:11 +00:00
88681d1849 Merge pull request 'Update dependency @vitejs/plugin-react to v4.3.0' (#187) from renovate/vitejs-plugin-react-4.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #187
2024-05-23 09:21:28 +00:00
921e72da13 Update dependency @vitejs/plugin-react to v4.3.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-23 00:24:29 +00:00
577caa201b Update dependency date-and-time to v3.3.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-21 00:32:04 +00:00
64de368ca5 Update dependency @mui/x-tree-view to v7.5.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-21 00:24:28 +00:00
a6739de9a4 Update dependency @mui/x-data-grid to v7.5.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-20 00:34:30 +00:00
5dc0a54ba2 Update Rust crate actix-web to v4.6.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-20 00:25:34 +00:00
e3206291bb Update Rust crate mailchecker to v6.0.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-19 00:24:59 +00:00
6d37b57398 Update Rust crate anyhow to v1.0.86
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-19 00:24:54 +00:00
fe9f9ee19b Update Rust crate thiserror to v1.0.61
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-18 00:35:15 +00:00
7f0fd6312a Update Rust crate anyhow to v1.0.85
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-18 00:24:46 +00:00
97b2ff4ff9 Update dependency @mui/material to v5.15.18
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-17 00:33:19 +00:00
d890f635e2 Update dependency @mui/icons-material to v5.15.18
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-17 00:24:52 +00:00
c8ee881b2c Genealogy as a feature (#175)
All checks were successful
continuous-integration/drone/push Build is passing
Start our journey into turning GeneIT as afully featured family intranet by making genealogy a feature that can be disabled by family admins

Reviewed-on: #175
2024-05-16 19:15:15 +00:00
0442538bd5 Update Rust crate serde to v1.0.202
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-16 00:20:45 +00:00
f167e24c4f Database migration are now automatically applied
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-14 21:36:49 +02:00
d9a4e3249d Genealogy -> intranet
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-14 21:19:14 +02:00
bdab064b38 Update frontend dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-14 20:53:10 +02:00
c36e634b72 Attempt to fix build config
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-14 19:39:35 +02:00
8218bec088 Attempt to fix build config
Some checks failed
continuous-integration/drone/push Build is failing
2024-05-14 19:39:00 +02:00
45ff766752 Update backend dependencies
Some checks failed
continuous-integration/drone/push Build is failing
2024-05-14 19:34:35 +02:00
47c9fad545 Merge pull request 'Update Rust crate thiserror to v1.0.59' (#169) from renovate/thiserror-1.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #169
2024-05-14 17:19:32 +00:00
e2ccfc8aed Merge pull request 'Update Rust crate rust_iso3166 to v0.1.13' (#171) from renovate/rust_iso3166-0.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #171
2024-05-14 17:19:22 +00:00
5cd4b71f2c Update dependency @mui/x-data-grid to v7.4.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-11 00:24:30 +00:00
2d240c8950 Update Rust crate rust_iso3166 to v0.1.13
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-09 00:24:54 +00:00
3438c187b1 Update Rust crate serde to v1.0.201
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-08 00:24:39 +00:00
28bfdb22b1 Update Rust crate thiserror to v1.0.59
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-07 00:04:50 +00:00
c2d658991c Update Rust crate serde to v1.0.200
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-05 00:10:04 +00:00
2536fd210a Update Rust crate base64 to v0.22.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-05 00:05:01 +00:00
f23cc80add Update dependency vite to v5.2.11
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-03 00:09:43 +00:00
49296478ea Update dependency @mui/x-data-grid to v7.3.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-03 00:04:14 +00:00
2d72efbece Update dependency @mui/x-data-grid to v7.3.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-27 00:16:33 +00:00
6c7e76cded Update Rust crate lettre to 0.11.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-24 00:16:59 +00:00
dc29f71549 Update Rust crate mailchecker to 6.0.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-23 00:10:38 +00:00
7f39ebcf97 Update dependency vite to v5.2.10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-21 00:10:47 +00:00
09df9a3a1d Update Rust crate diesel to 2.1.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-20 00:10:45 +00:00
2fea68c37c Update dependency @mui/x-data-grid to v7.3.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-19 00:10:03 +00:00
b3355809e6 Update dependency vite to v5.2.9
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-18 00:23:00 +00:00
28f4df254d Update dependency @testing-library/react to v15.0.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-18 00:20:30 +00:00
14f6a5ff64 Update Rust crate serde_json to 1.0.116
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-18 00:10:34 +00:00
4e23e74892 Merge pull request 'Update dependency @mui/x-tree-view to v7.2.0' (#147) from renovate/mui-x-tree-view-7.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #147
2024-04-17 06:01:16 +00:00
1f9b0c1bb0 Merge pull request 'Update dependency @testing-library/react to v15' (#145) from renovate/testing-library-react-15.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #145
2024-04-17 06:01:06 +00:00
6af22284b6 Update Rust crate serde to 1.0.198
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-17 00:10:41 +00:00
b2c61c7a4c Update dependency @mui/x-tree-view to v7.2.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-13 00:10:09 +00:00
19ee2cc038 Update dependency @mui/x-data-grid to v7.2.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-13 00:10:03 +00:00
4adfb2f9de Update dependency @testing-library/react to v15
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-12 00:10:28 +00:00
33e6173e40 Merge pull request 'Update node Docker tag to v21' (#141) from renovate/node-21.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #141
2024-04-11 08:21:52 +00:00
b44f07a9de Update dependency typescript to v5.4.5
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-11 00:25:06 +00:00
44c6b14d7f Update Rust crate anyhow to 1.0.82
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-11 00:10:40 +00:00
14e78cb255 Update node Docker tag to v21
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-09 00:12:02 +00:00
8cab8aa078 Update project dependencies (#140)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #140
Co-authored-by: Pierre HUBERT <pierre.git@communiquons.org>
Co-committed-by: Pierre HUBERT <pierre.git@communiquons.org>
2024-04-08 16:24:09 +00:00
6de067272c Merge pull request 'Update Rust crate image to 0.25.1' (#128) from renovate/image-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #128
2024-04-08 11:51:32 +00:00
4ace9346ee Merge pull request 'Update Rust crate light-openid to 1.0.2' (#134) from renovate/light-openid-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #134
2024-04-08 11:51:23 +00:00
741c51aff4 Merge pull request 'Update Rust crate redis to 0.25.3' (#135) from renovate/redis-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #135
2024-04-08 11:51:12 +00:00
531f7b1d37 Merge pull request 'Update dependency date-and-time to v3.1.1' (#139) from renovate/date-and-time-3.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #139
2024-04-08 11:51:03 +00:00
eddff39719 Update dependency date-and-time to v3.1.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-08 00:10:49 +00:00
8d1d4ac819 Update dependency @mui/material to v5.15.15
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-08 00:10:42 +00:00
69dd0fd514 Update dependency @mui/lab to v5.0.0-alpha.170
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-07 00:18:40 +00:00
f44195bf99 Update dependency @mui/x-data-grid to v6.19.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-06 00:11:46 +00:00
b038aa664f Update Rust crate redis to 0.25.3
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-06 00:11:37 +00:00
f36a690e04 Update Rust crate light-openid to 1.0.2
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-06 00:11:31 +00:00
5849460ca5 Update Rust crate image to 0.25.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-06 00:11:24 +00:00
c4e0c96268 Merge pull request 'Update dependency @testing-library/jest-dom to v6' (#33) from renovate/testing-library-jest-dom-6.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #33
2024-04-05 05:57:07 +00:00
7f8f8072d2 Merge pull request 'Update dependency @testing-library/user-event to v14' (#51) from renovate/testing-library-user-event-14.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #51
2024-04-05 05:56:58 +00:00
2fe93a3a09 Merge pull request 'Update Rust crate lettre to 0.11.6' (#123) from renovate/lettre-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #123
2024-04-05 05:56:47 +00:00
207098910f Update dependency @mui/icons-material to v5.15.15
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-05 00:05:56 +00:00
a406c149bd Update dependency vite to v5.2.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-04 00:06:34 +00:00
239835cc36 Update dependency @testing-library/react to v14.2.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-03 00:05:25 +00:00
47d18c8d0c Update dependency @mdi/js to v7.4.47
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-02 00:07:33 +00:00
601cb245db Update Rust crate uuid to 1.8.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-01 00:25:57 +00:00
ec9e6c1d4b Update dependency vite to v5.2.7
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-31 00:36:02 +00:00
c1e0d9d81d Update dependency @emotion/styled to v11.11.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-31 00:24:56 +00:00
160a6a0e75 Update dependency react-easy-crop to v5.0.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-30 00:19:49 +00:00
04d6f21754 Update Rust crate serde_json to 1.0.115
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-29 00:20:16 +00:00
a8928020e7 Update Rust crate lettre to 0.11.6
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-29 00:20:06 +00:00
67f35bba60 Merge pull request 'Update Rust crate anyhow to 1.0.81' (#119) from renovate/anyhow-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #119
2024-03-28 21:28:30 +00:00
3b25dfc20f Update Rust crate lettre to 0.11.5
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-27 00:36:44 +00:00
81e1da2bbc Update Rust crate clap to 4.5.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-27 00:24:26 +00:00
7d167f0463 Update dependency @testing-library/user-event to v14
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-26 00:25:26 +00:00
900043a7af Update dependency @testing-library/jest-dom to v6
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-26 00:25:15 +00:00
3233e73485 Update Rust crate bcrypt to 0.15.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-26 00:24:52 +00:00
162720cba1 Update Rust crate anyhow to 1.0.81
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-26 00:24:45 +00:00
0341b2e01a Merge pull request 'Update dependency @mui/lab to v5.0.0-alpha.169' (#75) from renovate/mui-lab-5.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #75
2024-03-25 09:50:05 +00:00
5ee4765099 Merge pull request 'Update dependency @mui/material to v5.15.14' (#39) from renovate/mui-material-5.x-lockfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #39
2024-03-25 09:49:42 +00:00
ab322c92e9 Merge pull request 'Update dependency @types/jest to v29' (#55) from renovate/jest-29.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #55
2024-03-25 09:49:22 +00:00
39a190116d Update dependency vite to v5.2.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-25 00:24:15 +00:00
5ea220e863 Update dependency @types/jest to v29
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-23 00:26:07 +00:00
045878523f Update dependency @mui/material to v5.15.14
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-23 00:25:44 +00:00
3cc29760f9 Update dependency vite to v5.2.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-23 00:25:35 +00:00
3f4eec9229 Update dependency @mui/lab to v5.0.0-alpha.169
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-23 00:25:22 +00:00
1365fa467a Merge pull request 'Update dependency @testing-library/react to v14' (#34) from renovate/testing-library-react-14.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #34
2024-03-22 10:44:08 +00:00
b1398fee2d Update dependency filesize to v10.1.1 (#116)
All checks were successful
continuous-integration/drone/push Build is passing
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [filesize](https://filesizejs.com) ([source](https://github.com/avoidwork/filesize.js)) | dependencies | patch | [`10.1.0` -> `10.1.1`](https://renovatebot.com/diffs/npm/filesize/10.1.0/10.1.1) |

---

### Release Notes

<details>
<summary>avoidwork/filesize.js (filesize)</summary>

### [`v10.1.1`](https://github.com/avoidwork/filesize.js/blob/HEAD/CHANGELOG.md#1011)

[Compare Source](https://github.com/avoidwork/filesize.js/compare/10.1.0...10.1.1)

-   Rework types to allow Parameters\<typeof filesize> to function properly [`#180`](https://github.com/avoidwork/filesize.js/pull/180)
-   Bump [@&#8203;babel/traverse](https://github.com/babel/traverse) from 7.23.0 to 7.23.2 [`#178`](https://github.com/avoidwork/filesize.js/pull/178)
-   change: rework types to allow usages like Parameters\<typeof filesize> [`fbfc87f`](fbfc87f4d2)
-   Generating CHANGELOG.md [`d80c457`](d80c4579e1)
-   fix: types for partial now return a higher order function [`af4a6ef`](af4a6efe8e)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjI2NS4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIn0=-->

Reviewed-on: #116
2024-03-22 10:38:13 +00:00
ca5aa30e92 Update dependency vite to v5.2.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-21 00:10:31 +00:00
e72ecc3265 Update dependency @mui/x-data-grid to v6.19.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-21 00:04:19 +00:00
ab13b06738 Update dependency @mui/icons-material to v5.15.14
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-20 00:05:01 +00:00
4cf26ce551 Update Rust crate diesel to 2.1.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-16 00:06:06 +00:00
3e1f5f0076 Update dependency @mui/icons-material to v5.15.13
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-14 00:04:13 +00:00
b5674622aa Update Rust crate thiserror to 1.0.58
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-13 00:04:38 +00:00
3ded39f6fc Update dependency vite to v5.1.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-12 00:09:38 +00:00
d8695d62b8 Update backend dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-11 18:25:47 +01:00
33f7d64e7e Merge pull request 'Update Rust crate log to 0.4.21' (#102) from renovate/log-0.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #102
2024-03-11 17:20:27 +00:00
d3983edf11 Update dependency @mui/icons-material to v5.15.12
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-06 00:16:53 +00:00
a84391b583 Update dependency vite to v5.1.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-05 00:12:02 +00:00
9f83183d2c Update Rust crate redis to 0.24.0 (#45)
All checks were successful
continuous-integration/drone/push Build is passing
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [redis](https://github.com/redis-rs/redis-rs) | dependencies | minor | `0.23.3` -> `0.24.0` |

---

### Release Notes

<details>
<summary>redis-rs/redis-rs (redis)</summary>

### [`v0.24.0`](https://github.com/redis-rs/redis-rs/releases/tag/redis-0.24.0): v0.24.0

[Compare Source](https://github.com/redis-rs/redis-rs/compare/redis-0.23.4...redis-0.24.0)

##### 0.24.0 (2023-12-05)

##### Features

-   **Breaking change**: Support Mutual TLS ([#&#8203;858](https://github.com/redis-rs/redis-rs/pull/858) [@&#8203;sp-angel](https://github.com/sp-angel))
-   Implement `FromRedisValue` for `Box<[T]>` and `Arc<[T]>` ([#&#8203;799](https://github.com/redis-rs/redis-rs/pull/799) [@&#8203;JOT85](https://github.com/JOT85))
-   Sync Cluster: support multi-slot operations. ([#&#8203;967](https://github.com/redis-rs/redis-rs/pull/967))
-   Execute multi-node requests using try_request. ([#&#8203;919](https://github.com/redis-rs/redis-rs/pull/919))
-   Sorted set blocking commands ([#&#8203;962](https://github.com/redis-rs/redis-rs/pull/962) [@&#8203;gheorghitamutu](https://github.com/gheorghitamutu))
-   Allow passing routing information to cluster. ([#&#8203;899](https://github.com/redis-rs/redis-rs/pull/899))
-   Add `tcp_nodelay` feature ([#&#8203;941](https://github.com/redis-rs/redis-rs/pull/941) [@&#8203;PureWhiteWu](https://github.com/PureWhiteWu))
-   Add support for multi-shard commands. ([#&#8203;900](https://github.com/redis-rs/redis-rs/pull/900))

##### Changes

-   Order in usage of ClusterParams. ([#&#8203;997](https://github.com/redis-rs/redis-rs/pull/997))
-   **Breaking change**: Fix StreamId::contains_key signature ([#&#8203;783](https://github.com/redis-rs/redis-rs/pull/783) [@&#8203;Ayush1325](https://github.com/Ayush1325))
-   **Breaking change**: Update Command expiration values to be an appropriate type ([#&#8203;589](https://github.com/redis-rs/redis-rs/pull/589) [@&#8203;joshleeb](https://github.com/joshleeb))
-   **Breaking change**: Bump aHash to v0.8.6 ([#&#8203;966](https://github.com/redis-rs/redis-rs/pull/966) [@&#8203;aumetra](https://github.com/aumetra))
-   Fix features for `load_native_certs`. ([#&#8203;996](https://github.com/redis-rs/redis-rs/pull/996))
-   Revert redis-test versioning changes ([#&#8203;993](https://github.com/redis-rs/redis-rs/pull/993))
-   Tests: Add retries to test cluster creation ([#&#8203;994](https://github.com/redis-rs/redis-rs/pull/994))
-   Fix sync cluster behavior with transactions. ([#&#8203;983](https://github.com/redis-rs/redis-rs/pull/983))
-   Sync Pub/Sub - cache received pub/sub messages. ([#&#8203;910](https://github.com/redis-rs/redis-rs/pull/910))
-   Prefer routing to primary in a transaction. ([#&#8203;986](https://github.com/redis-rs/redis-rs/pull/986))
-   Accept iterator at `ClusterClient` initialization ([#&#8203;987](https://github.com/redis-rs/redis-rs/pull/987) [@&#8203;ruanpetterson](https://github.com/ruanpetterson))
-   **Breaking change**: Change timeouts from usize and isize to f64 ([#&#8203;988](https://github.com/redis-rs/redis-rs/pull/988) [@&#8203;eythorhel19](https://github.com/eythorhel19))
-   Update minimal rust version to 1.6.5 ([#&#8203;982](https://github.com/redis-rs/redis-rs/pull/982))
-   Disable JSON module tests for redis 6.2.4. ([#&#8203;980](https://github.com/redis-rs/redis-rs/pull/980))
-   Add connection string examples ([#&#8203;976](https://github.com/redis-rs/redis-rs/pull/976) [@&#8203;NuclearOreo](https://github.com/NuclearOreo))
-   Move response policy into multi-node routing. ([#&#8203;952](https://github.com/redis-rs/redis-rs/pull/952))
-   Added functions that allow tests to check version. ([#&#8203;963](https://github.com/redis-rs/redis-rs/pull/963))
-   Fix XREADGROUP command ordering as per Redis Docs, and compatibility with Upstash Redis ([#&#8203;960](https://github.com/redis-rs/redis-rs/pull/960) [@&#8203;prabhpreet](https://github.com/prabhpreet))
-   Optimize make_pipeline_results by pre-allocate memory ([#&#8203;957](https://github.com/redis-rs/redis-rs/pull/957) [@&#8203;PureWhiteWu](https://github.com/PureWhiteWu))
-   Run module tests sequentially.  ([#&#8203;956](https://github.com/redis-rs/redis-rs/pull/956))
-   Log cluster creation output in tests. ([#&#8203;955](https://github.com/redis-rs/redis-rs/pull/955))
-   CI: Update and use better maintained github actions. ([#&#8203;954](https://github.com/redis-rs/redis-rs/pull/954))
-   Call CLIENT SETINFO on new connections. ([#&#8203;945](https://github.com/redis-rs/redis-rs/pull/945))
-   Deprecate functions that erroneously use `tokio` in their name. ([#&#8203;913](https://github.com/redis-rs/redis-rs/pull/913))
-   CI: Increase timeouts and use newer redis. ([#&#8203;949](https://github.com/redis-rs/redis-rs/pull/949))
-   Remove redis version from redis-test. ([#&#8203;943](https://github.com/redis-rs/redis-rs/pull/943))

### [`v0.23.4`](https://github.com/redis-rs/redis-rs/compare/redis-0.23.3...redis-0.23.4)

[Compare Source](https://github.com/redis-rs/redis-rs/compare/redis-0.23.3...redis-0.23.4)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy44OS40IiwidXBkYXRlZEluVmVyIjoiMzcuOTEuNCIsInRhcmdldEJyYW5jaCI6Im1hc3RlciJ9-->

Reviewed-on: #45
2024-03-04 09:06:19 +00:00
8652007906 Update dependency @fontsource/roboto to v5.0.12
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-04 00:11:35 +00:00
29c18807d8 Update Rust crate base64 to 0.22.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-03 00:21:16 +00:00
eb00b4e7b8 Update dependency @mui/x-data-grid to v6.19.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-03 00:12:39 +00:00
f16bee5927 Update Rust crate log to 0.4.21
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-29 00:12:16 +00:00
61082619fe Update Rust crate tempfile to 3.10.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-27 00:12:21 +00:00
79126ebbd7 Update dependency @mui/icons-material to v5.15.11
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-25 00:11:44 +00:00
d87f04b76b Update dependency @mui/x-data-grid to v6.19.5
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-24 00:18:24 +00:00
da476ea4d7 Update Rust crate image to 0.24.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-24 00:12:14 +00:00
0266c0c8b3 Update dependency vite to v5.1.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-22 00:11:53 +00:00
5863c346d0 Update dependency vite to v5.1.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-17 00:12:29 +00:00
b0087f2dfb Update dependency vite to v5.1.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-15 00:11:50 +00:00
97340b4ae8 Update dependency @mui/icons-material to v5.15.10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-13 00:12:14 +00:00
7dcc5e1d29 Update Rust crate thiserror to 1.0.57
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-12 00:12:44 +00:00
240d830526 Update dependency vite to v5.1.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-10 00:20:06 +00:00
c1e7ed9034 Update dependency @mui/x-data-grid to v6.19.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-10 00:12:35 +00:00
8324f9c501 Update dependency vite to v5.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-09 00:18:19 +00:00
d715a61255 Update dependency @mui/icons-material to v5.15.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-09 00:12:25 +00:00
eaee7f601e Update dependency @mui/icons-material to v5.15.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-07 00:12:02 +00:00
042776d491 Update Rust crate tempfile to 3.10.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-06 00:12:34 +00:00
900f6b8af8 Update Rust crate actix-web to 4.5.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-05 00:31:52 +00:00
86ada0da5d Update dependency @mui/x-data-grid to v6.19.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-02 00:33:46 +00:00
3d59629f7a Update dependency @mui/icons-material to v5.15.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-02-01 00:30:54 +00:00
dffc00382a Update dependency react-easy-crop to v5.0.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-31 00:30:57 +00:00
2dd9fc3469 Update dependency @mui/x-data-grid to v6.19.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-26 00:31:35 +00:00
cfd0cb37f5 Update dependency @mui/icons-material to v5.15.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-23 00:31:43 +00:00
d14557ac49 Update dependency @mui/x-data-grid to v6.19.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-20 00:40:40 +00:00
e188949ac0 Update dependency vite to v5.0.12
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-20 00:31:40 +00:00
d2bddb62bc Update dependency @mui/icons-material to v5.15.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-18 00:31:02 +00:00
b382f6bb85 Merge pull request 'Update Rust crate serde_json to 1.0.110' (#65) from renovate/serde_json-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #65
2024-01-16 18:32:11 +00:00
16ccb39b36 Update Rust crate image to 0.24.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-14 00:26:28 +00:00
60cc8a2401 Update Rust crate base64 to 0.21.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-12 00:09:51 +00:00
11715864a4 Update dependency @mui/icons-material to v5.15.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-11 00:10:35 +00:00
a79febee92 Update Rust crate base64 to 0.21.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-09 00:09:37 +00:00
554188e511 Update Rust crate actix-cors to 0.7.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-07 00:10:00 +00:00
a628bdee49 Update dependency vite to v5.0.11
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-06 00:14:51 +00:00
873a2aae95 Update dependency @mui/x-data-grid to v6.18.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-06 00:09:36 +00:00
1fe01ff893 Update dependency @mui/lab to v5.0.0-alpha.159
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-05 00:09:42 +00:00
ceb0f11128 Update dependency @mui/x-data-grid to v6.18.6
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 00:38:54 +00:00
bf3562c9e0 Update dependency @mui/icons-material to v5.15.3
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 00:35:43 +00:00
7e5bf55e66 Update dependency @testing-library/react to v14
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-01-04 00:29:10 +00:00
1251d61352 Update Rust crate thiserror to 1.0.56
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-04 00:28:26 +00:00
f2a58b5dcd Update Rust crate serde_json to 1.0.110
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-01-04 00:28:14 +00:00
d74f4aee61 Merge pull request 'Update Rust crate clap to 4.4.12' (#61) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #61
2024-01-03 10:43:56 +00:00
0788b6462c Merge pull request 'Update Rust crate serde to 1.0.194' (#66) from renovate/serde-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #66
2024-01-03 10:43:17 +00:00
26ee9e5d72 Update Rust crate serde to 1.0.194
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-03 00:20:02 +00:00
4c12a3c56b Update Rust crate thiserror to 1.0.53
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-31 00:19:17 +00:00
de767c90b9 Update Rust crate tempfile to 3.9.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-30 00:18:35 +00:00
be8066de17 Update dependency @mui/lab to v5.0.0-alpha.158
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-29 00:20:32 +00:00
7262c66c90 Update Rust crate clap to 4.4.12
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-12-29 00:20:09 +00:00
73ff552104 Update dependency @mui/icons-material to v5.15.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-28 00:31:26 +00:00
98920a485f Update Rust crate thiserror to 1.0.52
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-28 00:20:03 +00:00
4a2d95f64f Update Rust crate actix-web to 4.4.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-25 00:18:47 +00:00
c55b6f724e Update dependency @mui/lab to v5.0.0-alpha.157
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-20 00:28:12 +00:00
2ad40c4ba0 Update dependency @mui/icons-material to v5.15.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-20 00:11:12 +00:00
5cb64ad015 Update Rust crate thiserror to 1.0.51
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-17 00:13:23 +00:00
a60179eb0e Update dependency vite to v5.0.10
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-16 00:38:36 +00:00
2956777cf5 Update dependency react-easy-crop to v5.0.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-16 00:14:24 +00:00
65842c7f30 Update dependency vite to v5.0.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-15 00:11:55 +00:00
b7b759f5e9 Update dependency @mui/icons-material to v5.15.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-14 00:29:04 +00:00
10594732ba Update dependency @mui/lab to v5.0.0-alpha.156
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-14 00:11:37 +00:00
36ee8bf5b7 Update Rust crate clap to 4.4.11
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-13 00:29:36 +00:00
3dbbe50b83 Update Rust crate actix-cors to 0.6.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-13 00:13:48 +00:00
35d7bd493e Update installation instructions
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-12 19:06:22 +01:00
e4ae43f182 Update Docker image
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-12 19:01:41 +01:00
cef5d3c416 Finish vite migration
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-12 19:45:54 +01:00
34efa48c3e WIP vite migration 2023-12-12 19:37:41 +01:00
cba850251f Fix issue 2023-12-12 18:30:19 +01:00
9f25f88695 Updated frontend dependencies 2023-12-12 18:27:57 +01:00
2043afadbb Updated backend dependencies 2023-12-12 18:21:35 +01:00
74503d1eaa Update dependency @mui/lab to v5.0.0-alpha.155
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-07 00:11:47 +00:00
c3a173128e Update Rust crate tempfile to 3.8.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-12-06 00:17:20 +00:00
6005955884 Update Rust crate diesel to 2.1.4
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-30 00:12:53 +00:00
3aef093883 Update Rust crate serde_json to 1.0.108
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-29 00:08:19 +00:00
fb9ec24986 Update dependency @mui/x-tree-view to v6.0.0-beta.0
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2023-10-19 00:06:55 +00:00
49899defa9 Update dependency @mui/lab to v5.0.0-alpha.149
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2023-10-18 00:04:39 +00:00
549d12b392 Update Rust crate diesel to 2.1.3
Some checks failed
continuous-integration/drone/push Build is failing
2023-10-17 00:07:29 +00:00
eff5ee9609 Merge pull request 'Update Rust crate clap to 4.4.3' (#35) from renovate/clap-4.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #35
2023-10-16 16:40:17 +00:00
65f6f80303 Merge pull request 'Update Rust crate thiserror to 1.0.48' (#20) from renovate/thiserror-1.x into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #20
2023-10-16 12:00:25 +00:00
93053d5077 Merge pull request 'Update Rust crate httpdate to 1.0.3' (#3) from renovate/httpdate-1.x into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #3
2023-10-16 12:00:08 +00:00
4d53f8a122 Update Rust crate clap to 4.4.3
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2023-09-13 00:28:34 +00:00
9a3914660f Update dockage/mailcatcher Docker tag to v0.9.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-11 00:26:36 +00:00
961b4b07c4 Update dependency @mui/x-data-grid to v6.13.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-10 01:09:27 +00:00
0f6c68b1fc Update Rust crate tempfile to 3.8.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2023-09-10 00:27:25 +00:00
b528e6fdbb Update dependency react-easy-crop to v5.0.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-09 01:02:50 +00:00
2018174ed5 Update dependency @mui/x-tree-view to v6.0.0-alpha.2
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2023-09-09 00:26:01 +00:00
935deeca2c Switch tree view to its new package
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-08 11:28:24 +02:00
8ac79020d6 Update Rust crate diesel to 2.1.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-08 00:31:35 +00:00
c1972c7930 Update dependency react-easy-crop to v5.0.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-07 00:24:24 +00:00
f686003f19 Update dependency @mui/material to v5.14.8
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-06 01:20:18 +00:00
42a494a15b Update dependency @mui/lab to v5.0.0-alpha.143
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2023-09-06 00:35:11 +00:00
faa668550c Update Rust crate clap to 4.4.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-05 00:35:43 +00:00
cb797074cb Update Rust crate actix-web to 4.4.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-04 00:32:51 +00:00
ede195ce57 Update dependency filesize to v10.0.12
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-03 00:26:40 +00:00
738dee1f0c Update Rust crate thiserror to 1.0.48
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-09-03 00:26:17 +00:00
7f0ea4f04c Update Rust crate redis to 0.23.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-09-02 00:27:12 +00:00
49d27e5849 Update dependency @types/node to v16.18.46
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-01 01:17:31 +00:00
03b9dfc60a Update dependency @mui/material to v5.14.7
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2023-09-01 00:35:31 +00:00
433f8384a0 Emphasis missing mother and father
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-31 20:39:17 +02:00
2c0b6356b6 Fix bug 2023-08-31 20:36:33 +02:00
c6c984c34c Handle particular cases in simple family tree
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-31 20:26:34 +02:00
c84fb50087 Fix type declaration issue 2023-08-31 20:14:36 +02:00
79ce616781 Show a feedback when saving couple information
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-31 19:30:16 +02:00
e4a1817d7f Simplify navigation to set member photo 2023-08-31 19:27:14 +02:00
2a69c89065 Automatically trim values before saving 2023-08-31 19:23:21 +02:00
0899835fab Update dependency @mui/lab to v5.0.0-alpha.142
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-31 00:58:23 +00:00
cf5848491b Update dependency @fontsource/roboto to v5.0.8
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2023-08-31 00:26:52 +00:00
33748e3233 Update Rust crate thiserror to 1.0.47
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-30 00:55:47 +00:00
574dd8b16b Update Rust crate httpdate to 1.0.3
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-08-26 00:32:00 +00:00
107 changed files with 8836 additions and 16516 deletions

View File

@@ -7,13 +7,14 @@ steps:
- name: backend_check - name: backend_check
image: rust image: rust
commands: commands:
- apt update && apt install -y cmake
- rustup component add clippy - rustup component add clippy
- cd geneit_backend - cd geneit_backend
- cargo clippy -- -D warnings - cargo clippy -- -D warnings
- cargo test - cargo test
- name: app_deploy - name: app_deploy
image: node:18 image: node:22
environment: environment:
AWS_ACCESS_KEY_ID: AWS_ACCESS_KEY_ID:
from_secret: AWS_ACCESS_KEY_ID from_secret: AWS_ACCESS_KEY_ID

View File

@@ -19,7 +19,7 @@ docker-compose up
3. Install Diesel CLI: 3. Install Diesel CLI:
```bash ```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 cargo install diesel_cli --no-default-features --features postgres
``` ```
@@ -51,4 +51,4 @@ evan@qlik.example
franklin@qlik.example franklin@qlik.example
``` ```
Password: `Password1!` Password: `Password1!`

View File

@@ -1 +1 @@
REACT_APP_BACKEND=http://localhost:8000 VITE_APP_BACKEND=http://localhost:8000

View File

@@ -1 +1 @@
REACT_APP_BACKEND=https://geneit-backend.communiquons.org VITE_APP_BACKEND=https://geneit-backend.communiquons.org

View File

@@ -10,6 +10,7 @@
# production # production
/build /build
/dist
# misc # misc
.DS_Store .DS_Store

View File

@@ -2,25 +2,25 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <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="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" 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 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/ 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. 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. 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. 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`. 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 begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
<script type="module" src="/src/index.tsx"></script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -2,44 +2,54 @@
"name": "geneit_app", "name": "geneit_app",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"dependencies": { "dependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@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", "@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/js": "^7.2.96",
"@mdi/react": "^1.6.1", "@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/lab": "^5.0.0-alpha.140",
"@mui/material": "^5.14.5", "@mui/material": "^5.15.17",
"@mui/x-data-grid": "^6.9.2", "@mui/x-data-grid": "^7.1.1",
"@testing-library/jest-dom": "^5.16.5", "@mui/x-date-pickers": "^7.7.0",
"@testing-library/react": "^13.4.0", "@mui/x-tree-view": "^7.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/jest-dom": "^6.4.5",
"@types/jest": "^27.5.2", "@testing-library/react": "^16.0.0",
"@types/node": "^16.18.34", "@testing-library/user-event": "^14.0.0",
"@types/react": "^18.2.8", "@types/jest": "^29.0.0",
"@types/react-dom": "^18.2.4", "@types/react": "^18.3.2",
"date-and-time": "^3.0.1", "@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", "email-validator": "^2.0.4",
"family-chart": "^0.0.0-beta-1", "filesize": "^10.1.2",
"filesize": "^10.0.9",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"react": "^18.2.0", "mui-color-input": "^2.0.3",
"react-dom": "^18.2.0", "react": "^18.3.1",
"react-easy-crop": "^5.0.0", "react-dom": "^18.3.1",
"react-router-dom": "^6.11.2", "react-easy-crop": "^5.0.7",
"react-scripts": "^5.0.1", "react-qr-code": "^2.0.14",
"react-zoom-pan-pinch": "^3.1.0", "react-router-dom": "^6.23.1",
"svg2pdf.js": "^2.2.2", "react-zoom-pan-pinch": "^3.4.4",
"typescript": "^4.9.5", "svg2pdf.js": "^2.2.3",
"web-vitals": "^2.1.4" "typescript": "^5.4.5",
"vite": "^5.2.11",
"vite-tsconfig-paths": "^4.3.2",
"web-vitals": "^3.5.2"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "vite",
"build": "react-scripts build", "build": "tsc && vite build",
"test": "react-scripts test", "preview": "vite preview"
"eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@@ -1,6 +1,6 @@
{ {
"short_name": "GeneIT", "short_name": "GeneIT",
"name": "La généalogie de votre famille simplifiée", "name": "La vie informatique de votre famille simplifiée",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

View File

@@ -16,26 +16,33 @@ import { NewAccountRoute } from "./routes/auth/NewAccountRoute";
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute"; import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute";
import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute"; 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 { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute";
import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute"; import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute";
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; import { AccommodationsHomeRoute } from "./routes/family/accommodations/AccommodationsHomeRoute";
import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute"; import { AccommodationsReservationsRoute } from "./routes/family/accommodations/AccommodationsReservationsRoute";
import { BaseLoginPage } from "./widgets/BaseLoginpage"; import { AccommodationsSettingsRoute } from "./routes/family/accommodations/AccommodationsSettingsRoute";
import { FamilyMembersListRoute } from "./routes/family/FamilyMembersListRoute";
import { import {
FamilyCoupleRoute, FamilyCoupleRoute,
FamilyCreateCoupleRoute, FamilyCreateCoupleRoute,
FamilyEditCoupleRoute, FamilyEditCoupleRoute,
} from "./routes/family/FamilyCoupleRoute"; } from "./routes/family/genealogy/FamilyCoupleRoute";
import { FamilyCouplesListRoute } from "./routes/family/FamilyCouplesListRoute"; import { FamilyCouplesListRoute } from "./routes/family/genealogy/FamilyCouplesListRoute";
import { FamilyTreeRoute } from "./routes/family/FamilyTreeRoute"; import { FamilyHomeRoute } from "./routes/family/genealogy/FamilyHomeRoute";
import { FamilyMemberTreeRoute } from "./routes/family/FamilyMemberTreeRoute"; 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 { interface AuthContext {
signedIn: boolean; signedIn: boolean;
@@ -67,33 +74,61 @@ export function App(): React.ReactElement {
<Route path="family/:familyId/*" element={<BaseFamilyRoute />}> <Route path="family/:familyId/*" element={<BaseFamilyRoute />}>
<Route path="" element={<FamilyHomeRoute />} /> <Route path="" element={<FamilyHomeRoute />} />
<Route path="members" element={<FamilyMembersListRoute />} /> <Route path="genealogy/*" element={<BaseGenealogyRoute />}>
<Route <Route path="" element={<GenealogyHomeRoute />} />
path="member/create"
element={<FamilyCreateMemberRoute />}
/>
<Route path="member/:memberId" element={<FamilyMemberRoute />} />
<Route
path="member/:memberId/edit"
element={<FamilyEditMemberRoute />}
/>
<Route path="couples" element={<FamilyCouplesListRoute />} /> <Route path="members" element={<FamilyMembersListRoute />} />
<Route <Route
path="couple/create" path="member/create"
element={<FamilyCreateCoupleRoute />} element={<FamilyCreateMemberRoute />}
/> />
<Route path="couple/:coupleId" element={<FamilyCoupleRoute />} /> <Route
<Route path="member/:memberId"
path="couple/:coupleId/edit" element={<FamilyMemberRoute />}
element={<FamilyEditCoupleRoute />} />
/> <Route
path="member/:memberId/edit"
element={<FamilyEditMemberRoute />}
/>
<Route path="couples" element={<FamilyCouplesListRoute />} />
<Route
path="couple/create"
element={<FamilyCreateCoupleRoute />}
/>
<Route
path="couple/:coupleId"
element={<FamilyCoupleRoute />}
/>
<Route
path="couple/:coupleId/edit"
element={<FamilyEditCoupleRoute />}
/>
<Route path="tree" element={<FamilyTreeRoute />} />
<Route
path="tree/:memberId"
element={<FamilyMemberTreeRoute />}
/>
<Route path="settings" element={<GenalogySettingsRoute />} />
<Route path="*" element={<NotFoundRoute />} />
</Route>
<Route path="tree" element={<FamilyTreeRoute />} />
<Route <Route
path="tree/:memberId" path="accommodations/*"
element={<FamilyMemberTreeRoute />} 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="settings" element={<FamilySettingsRoute />} />
<Route path="users" element={<FamilyUsersListRoute />} /> <Route path="users" element={<FamilyUsersListRoute />} />

View File

@@ -16,7 +16,7 @@ export class APIClient {
* Get backend URL * Get backend URL
*/ */
static backendURL(): string { 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!"); if (URL.length === 0) throw new Error("Backend URL undefined!");
return URL; return URL;
} }

View File

@@ -1,6 +1,6 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
import { Couple } from "./CoupleApi"; import { Couple } from "./genealogy/CoupleApi";
import { Member } from "./MemberApi"; import { Member } from "./genealogy/MemberApi";
interface FamilyAPI { interface FamilyAPI {
user_id: number; user_id: number;
@@ -60,7 +60,8 @@ export class Family implements FamilyAPI {
*/ */
memberURL(member: Member, edit?: boolean): string { memberURL(member: Member, edit?: boolean): string {
return ( 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 * Get family tree URL for member
*/ */
familyTreeURL(member: Member | number): string { familyTreeURL(member: Member | number): string {
return `/family/${this.family_id}/tree/${ return `/family/${this.family_id}/genealogy/tree/${
typeof member === "number" ? member : member.id typeof member === "number" ? member : member.id
}`; }`;
} }
@@ -78,16 +79,21 @@ export class Family implements FamilyAPI {
*/ */
coupleURL(member: Couple, edit?: boolean): string { coupleURL(member: Couple, edit?: boolean): string {
return ( 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 { export class ExtendedFamilyInfo extends Family {
public disable_couple_photos: boolean; public disable_couple_photos: boolean;
public enable_genealogy: boolean;
public enable_accommodations: boolean;
constructor(p: any) { constructor(p: any) {
super(p); super(p);
this.disable_couple_photos = p.disable_couple_photos; 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: { static async UpdateFamily(settings: {
id: number; id: number;
name: string; name?: string;
disable_couple_photos: boolean; enable_genealogy?: boolean;
enable_accommodations?: boolean;
disable_couple_photos?: boolean;
}): Promise<void> { }): Promise<void> {
await APIClient.exec({ await APIClient.exec({
method: "PATCH", method: "PATCH",
uri: `/family/${settings.id}`, uri: `/family/${settings.id}`,
jsonData: { jsonData: {
name: settings.name, name: settings.name,
enable_genealogy: settings.enable_genealogy,
enable_accommodations: settings.enable_accommodations,
disable_couple_photos: settings.disable_couple_photos, disable_couple_photos: settings.disable_couple_photos,
}, },
}); });

View File

@@ -32,6 +32,9 @@ interface Constraints {
member_country: LenConstraint; member_country: LenConstraint;
member_sex: LenConstraint; member_sex: LenConstraint;
member_note: LenConstraint; member_note: LenConstraint;
accommodation_name_len: LenConstraint;
accommodation_description_len: LenConstraint;
accommodation_calendar_name_len: LenConstraint;
} }
interface OIDCProvider { interface OIDCProvider {

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,6 +1,6 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "../ApiClient";
import { DateValue, Member } from "./MemberApi"; import { DateValue, Member } from "./MemberApi";
import { ServerApi } from "./ServerApi"; import { ServerApi } from "../ServerApi";
interface CoupleApiInterface { interface CoupleApiInterface {
id: number; id: number;
@@ -161,7 +161,7 @@ export class CoupleApi {
*/ */
static async Create(m: Couple): Promise<Couple> { static async Create(m: Couple): Promise<Couple> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${m.family_id}/couple/create`, uri: `/family/${m.family_id}/genealogy/couple/create`,
method: "POST", method: "POST",
jsonData: m, jsonData: m,
}); });
@@ -177,7 +177,7 @@ export class CoupleApi {
couple_id: number couple_id: number
): Promise<Couple> { ): Promise<Couple> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/couple/${couple_id}`, uri: `/family/${family_id}/genealogy/couple/${couple_id}`,
method: "GET", method: "GET",
}); });
@@ -189,7 +189,7 @@ export class CoupleApi {
*/ */
static async GetEntireList(family_id: number): Promise<CouplesList> { static async GetEntireList(family_id: number): Promise<CouplesList> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/couples`, uri: `/family/${family_id}/genealogy/couples`,
method: "GET", method: "GET",
}); });
@@ -201,7 +201,7 @@ export class CoupleApi {
*/ */
static async Update(m: Couple): Promise<void> { static async Update(m: Couple): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/couple/${m.id}`, uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
method: "PUT", method: "PUT",
jsonData: m, jsonData: m,
}); });
@@ -214,7 +214,7 @@ export class CoupleApi {
const fd = new FormData(); const fd = new FormData();
fd.append("photo", b); fd.append("photo", b);
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/couple/${m.id}/photo`, uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`,
method: "PUT", method: "PUT",
formData: fd, formData: fd,
}); });
@@ -225,7 +225,7 @@ export class CoupleApi {
*/ */
static async RemoveCouplePhoto(m: Couple): Promise<void> { static async RemoveCouplePhoto(m: Couple): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/couple/${m.id}/photo`, uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`,
method: "DELETE", method: "DELETE",
}); });
} }
@@ -235,7 +235,7 @@ export class CoupleApi {
*/ */
static async Delete(m: Couple): Promise<void> { static async Delete(m: Couple): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/couple/${m.id}`, uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
method: "DELETE", method: "DELETE",
}); });
} }

View File

@@ -1,4 +1,4 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "../ApiClient";
/** /**
* Data management api client * Data management api client
@@ -9,7 +9,7 @@ export class DataApi {
*/ */
static async ExportData(family_id: number): Promise<Blob> { static async ExportData(family_id: number): Promise<Blob> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/data/export`, uri: `/family/${family_id}/genealogy/data/export`,
method: "GET", method: "GET",
}); });
return res.data; return res.data;
@@ -22,7 +22,7 @@ export class DataApi {
const fd = new FormData(); const fd = new FormData();
fd.append("archive", archive); fd.append("archive", archive);
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/data/import`, uri: `/family/${family_id}/genealogy/data/import`,
method: "PUT", method: "PUT",
formData: fd, formData: fd,
}); });

View File

@@ -1,4 +1,4 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "../ApiClient";
import { Couple } from "./CoupleApi"; import { Couple } from "./CoupleApi";
export type Sex = "M" | "F"; export type Sex = "M" | "F";
@@ -278,7 +278,7 @@ export class MemberApi {
*/ */
static async Create(m: Member): Promise<Member> { static async Create(m: Member): Promise<Member> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${m.family_id}/member/create`, uri: `/family/${m.family_id}/genealogy/member/create`,
method: "POST", method: "POST",
jsonData: m, jsonData: m,
}); });
@@ -294,7 +294,7 @@ export class MemberApi {
member_id: number member_id: number
): Promise<Member> { ): Promise<Member> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/member/${member_id}`, uri: `/family/${family_id}/genealogy/member/${member_id}`,
method: "GET", method: "GET",
}); });
@@ -306,7 +306,7 @@ export class MemberApi {
*/ */
static async GetEntireList(family_id: number): Promise<MembersList> { static async GetEntireList(family_id: number): Promise<MembersList> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/members`, uri: `/family/${family_id}/genealogy/members`,
method: "GET", method: "GET",
}); });
@@ -318,7 +318,7 @@ export class MemberApi {
*/ */
static async Update(m: Member): Promise<void> { static async Update(m: Member): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/member/${m.id}`, uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
method: "PUT", method: "PUT",
jsonData: m, jsonData: m,
}); });
@@ -331,7 +331,7 @@ export class MemberApi {
const fd = new FormData(); const fd = new FormData();
fd.append("photo", b); fd.append("photo", b);
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/member/${m.id}/photo`, uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`,
method: "PUT", method: "PUT",
formData: fd, formData: fd,
}); });
@@ -342,7 +342,7 @@ export class MemberApi {
*/ */
static async RemoveMemberPhoto(m: Member): Promise<void> { static async RemoveMemberPhoto(m: Member): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/member/${m.id}/photo`, uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`,
method: "DELETE", method: "DELETE",
}); });
} }
@@ -352,7 +352,7 @@ export class MemberApi {
*/ */
static async Delete(m: Member): Promise<void> { static async Delete(m: Member): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/member/${m.id}`, uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
method: "DELETE", method: "DELETE",
}); });
} }

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -1,7 +1,7 @@
import { ThemeProvider, createTheme } from "@mui/material/styles"; import { ThemeProvider, createTheme } from "@mui/material/styles";
import React from "react"; import React from "react";
import { PropsWithChildren } 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"; const localStorageKey = "dark-theme";

View File

@@ -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)!;
}

View File

@@ -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)!;
}

View File

@@ -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)!;
}

View File

@@ -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)!;
}

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -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>
</>
);
}

View File

@@ -1,26 +1,19 @@
import DownloadIcon from "@mui/icons-material/Download";
import UploadIcon from "@mui/icons-material/Upload";
import { import {
Alert,
Box, Box,
Button, Button,
CardActions, CardActions,
CardContent, CardContent,
Checkbox,
FormControlLabel, FormControlLabel,
Switch,
TextField, TextField,
Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { DataApi } from "../../api/DataApi";
import { FamilyApi } from "../../api/FamilyApi"; import { FamilyApi } from "../../api/FamilyApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../api/ServerApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; 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 { useFamily } from "../../widgets/BaseFamilyRoute";
import { FamilyCard } from "../../widgets/FamilyCard"; import { FamilyCard } from "../../widgets/FamilyCard";
import { formatDate } from "../../widgets/TimeWidget"; import { formatDate } from "../../widgets/TimeWidget";
@@ -55,7 +48,6 @@ export function FamilySettingsRoute(): React.ReactElement {
return ( return (
<> <>
<FamilySettingsCard /> <FamilySettingsCard />
<FamilyExportCard />
<div style={{ textAlign: "center", marginTop: "50px" }}> <div style={{ textAlign: "center", marginTop: "50px" }}>
<Button <Button
size="small" size="small"
@@ -76,8 +68,11 @@ function FamilySettingsCard(): React.ReactElement {
const family = useFamily(); const family = useFamily();
const [newName, setNewName] = React.useState(family.family.name); const [newName, setNewName] = React.useState(family.family.name);
const [disableCouplePhotos, setDisableCouplePhotos] = React.useState( const [enableGenealogy, setEnableGenealogy] = React.useState(
family.family.disable_couple_photos family.family.enable_genealogy
);
const [enableAccommodations, setEnableAccommodations] = React.useState(
family.family.enable_accommodations
); );
const canEdit = family.family.is_admin; const canEdit = family.family.is_admin;
@@ -93,7 +88,8 @@ function FamilySettingsCard(): React.ReactElement {
await FamilyApi.UpdateFamily({ await FamilyApi.UpdateFamily({
id: family.family.family_id, id: family.family.family_id,
name: newName, name: newName,
disable_couple_photos: disableCouplePhotos, enable_genealogy: enableGenealogy,
enable_accommodations: enableAccommodations,
}); });
family.reloadFamilyInfo(); family.reloadFamilyInfo();
@@ -126,14 +122,12 @@ function FamilySettingsCard(): React.ReactElement {
label="Identifiant" label="Identifiant"
value={family.family.family_id} value={family.family.family_id}
/> />
<TextField <TextField
disabled disabled
fullWidth fullWidth
label="Création de la famille" label="Création de la famille"
value={formatDate(family.family.time_create)} value={formatDate(family.family.time_create)}
/> />
<TextField <TextField
fullWidth fullWidth
label="Nom de la famille" label="Nom de la famille"
@@ -144,18 +138,26 @@ function FamilySettingsCard(): React.ReactElement {
maxLength: ServerApi.Config.constraints.family_name_len.max, 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
<FormControlLabel disabled={!canEdit}
disabled={!canEdit} control={
control={ <Switch
<Checkbox checked={enableGenealogy}
checked={disableCouplePhotos} onChange={(_e, c) => setEnableGenealogy(c)}
onChange={(_e, c) => setDisableCouplePhotos(c)} />
/> }
} label="Activer le module de généalogie"
label="Désactiver les photos de couple" />
/> <FormControlLabel
</Tooltip> disabled={!canEdit}
control={
<Switch
checked={enableAccommodations}
onChange={(_e, c) => setEnableAccommodations(c)}
/>
}
label="Activer le module de réservation de logements"
/>
</Box> </Box>
</CardContent> </CardContent>
<CardActions> <CardActions>
@@ -170,109 +172,3 @@ function FamilySettingsCard(): React.ReactElement {
</FamilyCard> </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>&nbsp;</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>
);
}

View File

@@ -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>&nbsp;</p>
<p>
Nombre de logements définis : {accommodations.accommodations.size}
</p>
</div>
</>
);
}

View File

@@ -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>
)}
/>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -6,25 +6,27 @@ import SaveIcon from "@mui/icons-material/Save";
import { Button, Grid, Stack } from "@mui/material"; import { Button, Grid, Stack } from "@mui/material";
import React from "react"; import React from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Couple, CoupleApi } from "../../api/CoupleApi"; import { ServerApi } from "../../../api/ServerApi";
import { Member } from "../../api/MemberApi"; import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi";
import { ServerApi } from "../../api/ServerApi"; import { Member } from "../../../api/genealogy/MemberApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
import { AsyncWidget } from "../../widgets/AsyncWidget"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useQuery } from "../../../hooks/useQuery";
import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog"; import { AsyncWidget } from "../../../widgets/AsyncWidget";
import { CouplePhoto } from "../../widgets/CouplePhoto"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog";
import { MemberItem } from "../../widgets/MemberItem"; import { CouplePhoto } from "../../../widgets/CouplePhoto";
import { PropertiesBox } from "../../widgets/PropertiesBox"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
import { RouterLink } from "../../widgets/RouterLink"; import { MemberItem } from "../../../widgets/MemberItem";
import { DateInput } from "../../widgets/forms/DateInput"; import { PropertiesBox } from "../../../widgets/PropertiesBox";
import { MemberInput } from "../../widgets/forms/MemberInput"; import { RouterLink } from "../../../widgets/RouterLink";
import { PropSelect } from "../../widgets/forms/PropSelect"; import { DateInput } from "../../../widgets/forms/DateInput";
import { UploadPhotoButton } from "../../widgets/forms/UploadPhotoButton"; import { MemberInput } from "../../../widgets/forms/MemberInput";
import { useQuery } from "../../hooks/useQuery"; import { PropSelect } from "../../../widgets/forms/PropSelect";
import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton";
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
/** /**
* Create a new couple route * Create a new couple route
@@ -35,6 +37,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
const [shouldQuit, setShouldQuit] = React.useState(false); const [shouldQuit, setShouldQuit] = React.useState(false);
const n = useNavigate(); const n = useNavigate();
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const params = useQuery(); const params = useQuery();
@@ -48,7 +51,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
try { try {
const r = await CoupleApi.Create(m); const r = await CoupleApi.Create(m);
await family.reloadCouplesList(); await genealogy.reloadCouplesList();
setShouldQuit(true); setShouldQuit(true);
n(family.family.coupleURL(r)); n(family.family.coupleURL(r));
@@ -61,7 +64,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
const cancel = () => { const cancel = () => {
setShouldQuit(true); setShouldQuit(true);
n(family.family.URL("couples")); n(family.family.URL("genealogy/couples"));
}; };
return ( return (
@@ -88,6 +91,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const { coupleId } = useParams(); const { coupleId } = useParams();
const [couple, setCouple] = React.useState<Couple>(); const [couple, setCouple] = React.useState<Couple>();
@@ -99,7 +103,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
count.current += 1; count.current += 1;
setCouple(undefined); setCouple(undefined);
await family.reloadCouplesList(); await genealogy.reloadCouplesList();
}; };
const deleteCouple = async () => { const deleteCouple = async () => {
@@ -114,9 +118,9 @@ export function FamilyCoupleRoute(): React.ReactElement {
await CoupleApi.Delete(couple!); await CoupleApi.Delete(couple!);
snackbar("La fiche du couple a été supprimée avec succès !"); 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) { } catch (e) {
console.error(e); console.error(e);
alert("Échec de la suppression du couple !"); alert("Échec de la suppression du couple !");
@@ -132,7 +136,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
build={() => ( build={() => (
<CouplePage <CouplePage
couple={couple!} couple={couple!}
children={family.members.childrenOfCouple(couple!)} children={genealogy.members.childrenOfCouple(couple!)}
creating={false} creating={false}
editing={false} editing={false}
onRequestDelete={deleteCouple} onRequestDelete={deleteCouple}
@@ -156,6 +160,7 @@ export function FamilyEditCoupleRoute(): React.ReactElement {
const [shouldQuit, setShouldQuit] = React.useState(false); const [shouldQuit, setShouldQuit] = React.useState(false);
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const [couple, setCouple] = React.useState<Couple>(); const [couple, setCouple] = React.useState<Couple>();
@@ -165,7 +170,8 @@ export function FamilyEditCoupleRoute(): React.ReactElement {
const cancel = () => { const cancel = () => {
setShouldQuit(true); setShouldQuit(true);
n(-1); n(family.family.coupleURL(couple!));
//n(-1);
}; };
const save = async (c: Couple) => { 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 !"); snackbar("Les informations du couple ont été mises à jour avec succès !");
await family.reloadCouplesList(); await genealogy.reloadCouplesList();
setShouldQuit(true); setShouldQuit(true);
n(family.family.coupleURL(c, false)); n(family.family.coupleURL(c, false));
@@ -210,13 +216,14 @@ export function CouplePage(p: {
shouldAllowLeaving?: boolean; shouldAllowLeaving?: boolean;
children?: Member[]; children?: Member[];
onCancel?: () => void; onCancel?: () => void;
onSave?: (m: Couple) => void; onSave?: (m: Couple) => Promise<void>;
onRequestEdit?: () => void; onRequestEdit?: () => void;
onRequestDelete?: () => void; onRequestDelete?: () => void;
onForceReload?: () => void; onForceReload?: () => void;
}): React.ReactElement { }): React.ReactElement {
const confirm = useConfirm(); const confirm = useConfirm();
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const loadingMessage = useLoadingMessage();
const family = useFamily(); const family = useFamily();
@@ -230,8 +237,12 @@ export function CouplePage(p: {
setCouple(new Couple(structuredClone(couple))); setCouple(new Couple(structuredClone(couple)));
}; };
const save = () => { const save = async () => {
p.onSave!(couple); loadingMessage.show(
"Enregistrement des informations du couple en cours..."
);
await p.onSave!(couple);
loadingMessage.hide();
}; };
const cancel = async () => { const cancel = async () => {
@@ -479,7 +490,7 @@ export function CouplePage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( to={family.family.URL(
`member/create?mother=${couple.wife}&father=${couple.husband}` `genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`
)} )}
> >
<Button>Nouveau</Button> <Button>Nouveau</Button>

View File

@@ -6,17 +6,18 @@ import { Button, TextField, Tooltip } from "@mui/material";
import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid"; import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Couple, CoupleApi } from "../../api/CoupleApi"; import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi";
import { dateTimestamp, fmtDate } from "../../api/MemberApi"; import { dateTimestamp, fmtDate } from "../../../api/genealogy/MemberApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../../api/ServerApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { CouplePhoto } from "../../widgets/CouplePhoto"; import { CouplePhoto } from "../../../widgets/CouplePhoto";
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
import { MemberPhoto } from "../../widgets/MemberPhoto"; import { MemberPhoto } from "../../../widgets/MemberPhoto";
import { RouterLink } from "../../widgets/RouterLink"; import { RouterLink } from "../../../widgets/RouterLink";
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
export function FamilyCouplesListRoute(): React.ReactElement { export function FamilyCouplesListRoute(): React.ReactElement {
const alert = useAlert(); const alert = useAlert();
@@ -24,6 +25,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const [filter, setFilter] = React.useState(""); const [filter, setFilter] = React.useState("");
@@ -37,7 +39,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
return; return;
await CoupleApi.Delete(c); await CoupleApi.Delete(c);
await family.reloadCouplesList(); await genealogy.reloadCouplesList();
snackbar("La fiche du couple a été supprimée avec succès !"); snackbar("La fiche du couple a été supprimée avec succès !");
} catch (e) { } catch (e) {
@@ -63,7 +65,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
</RouterLink> </RouterLink>
</div> </div>
{family.couples.isEmpty ? ( {genealogy.couples.isEmpty ? (
<p> <p>
Votre famille n'a aucun couple enregistré pour le moment ! Utilisez le Votre famille n'a aucun couple enregistré pour le moment ! Utilisez le
bouton situé en haut à droite pour créer le premier ! bouton situé en haut à droite pour créer le premier !
@@ -81,16 +83,16 @@ export function FamilyCouplesListRoute(): React.ReactElement {
<CouplesTable <CouplesTable
couples={ couples={
filter === "" filter === ""
? family.couples.fullList ? genealogy.couples.fullList
: family.couples.filter( : genealogy.couples.filter(
(m) => (m) =>
(m.wife && (m.wife &&
family.members genealogy.members
.get(m.wife)! .get(m.wife)!
.fullName.toLocaleLowerCase() .fullName.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())) || .includes(filter.toLocaleLowerCase())) ||
(m.husband && (m.husband &&
family.members genealogy.members
.get(m.husband)! .get(m.husband)!
.fullName.toLocaleLowerCase() .fullName.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())) === true .includes(filter.toLocaleLowerCase())) === true
@@ -109,14 +111,18 @@ function CouplesTable(p: {
onDelete: (m: Couple) => void; onDelete: (m: Couple) => void;
}): React.ReactElement { }): React.ReactElement {
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const n = useNavigate(); const n = useNavigate();
const compareInvertedMembersNames = ( const compareInvertedMembersNames = (
v1: number | undefined, v1: number | undefined,
v2: number | undefined v2: number | undefined
) => { ) => {
const n1 = ((v1 && family.members.get(v1)?.invertedFullName) ?? "") || ""; const n1 =
const n2 = ((v2 && family.members.get(v2)?.invertedFullName) ?? "") || ""; ((v1 && genealogy.members.get(v1)?.invertedFullName) ?? "") || "";
const n2 =
((v2 && genealogy.members.get(v2)?.invertedFullName) ?? "") || "";
return n1?.localeCompare(n2, undefined, { return n1?.localeCompare(n2, undefined, {
ignorePunctuation: true, ignorePunctuation: true,
@@ -132,7 +138,13 @@ function CouplesTable(p: {
sortable: false, sortable: false,
width: 60, width: 60,
renderCell(params) { 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 { function MemberCell(p: { id?: number }): React.ReactElement {
const family = useFamily(); const genealogy = useGenealogy();
if (!p.id) return <></>; if (!p.id) return <></>;
const member = family.members.get(p.id!)!; const member = genealogy.members.get(p.id!)!;
return ( return (
<Tooltip title="Double-cliquez ici pour accéder à la fiche du membre"> <Tooltip title="Double-cliquez ici pour accéder à la fiche du membre">

View 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>
</>
);
}

View File

@@ -1,3 +1,5 @@
import { mdiFamilyTree } from "@mdi/js";
import Icon from "@mdi/react";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
@@ -14,31 +16,31 @@ import {
import * as EmailValidator from "email-validator"; import * as EmailValidator from "email-validator";
import React from "react"; import React from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Couple } from "../../api/CoupleApi"; import { ServerApi } from "../../../api/ServerApi";
import { Member, MemberApi, fmtDate } from "../../api/MemberApi"; import { Couple } from "../../../api/genealogy/CoupleApi";
import { ServerApi } from "../../api/ServerApi"; import { Member, MemberApi, fmtDate } from "../../../api/genealogy/MemberApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
import { AsyncWidget } from "../../widgets/AsyncWidget"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useQuery } from "../../../hooks/useQuery";
import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog"; import { AsyncWidget } from "../../../widgets/AsyncWidget";
import { CouplePhoto } from "../../widgets/CouplePhoto"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog";
import { MemberItem } from "../../widgets/MemberItem"; import { CouplePhoto } from "../../../widgets/CouplePhoto";
import { MemberPhoto } from "../../widgets/MemberPhoto"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
import { PropertiesBox } from "../../widgets/PropertiesBox"; import { MemberItem } from "../../../widgets/MemberItem";
import { RouterLink } from "../../widgets/RouterLink"; import { MemberPhoto } from "../../../widgets/MemberPhoto";
import { DateInput } from "../../widgets/forms/DateInput"; import { PropertiesBox } from "../../../widgets/PropertiesBox";
import { MemberInput } from "../../widgets/forms/MemberInput"; import { RouterLink } from "../../../widgets/RouterLink";
import { PropCheckbox } from "../../widgets/forms/PropCheckbox"; import { DateInput } from "../../../widgets/forms/DateInput";
import { PropEdit } from "../../widgets/forms/PropEdit"; import { MemberInput } from "../../../widgets/forms/MemberInput";
import { PropSelect } from "../../widgets/forms/PropSelect"; import { PropCheckbox } from "../../../widgets/forms/PropCheckbox";
import { SexSelection } from "../../widgets/forms/SexSelection"; import { PropEdit } from "../../../widgets/forms/PropEdit";
import { UploadPhotoButton } from "../../widgets/forms/UploadPhotoButton"; import { PropSelect } from "../../../widgets/forms/PropSelect";
import { useQuery } from "../../hooks/useQuery"; import { SexSelection } from "../../../widgets/forms/SexSelection";
import { mdiFamilyTree } from "@mdi/js"; import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton";
import Icon from "@mdi/react"; import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
/** /**
* Create a new member route * Create a new member route
@@ -49,6 +51,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
const [shouldQuit, setShouldQuit] = React.useState(false); const [shouldQuit, setShouldQuit] = React.useState(false);
const n = useNavigate(); const n = useNavigate();
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const parameters = useQuery(); const parameters = useQuery();
@@ -59,10 +62,10 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
try { try {
const r = await MemberApi.Create(m); const r = await MemberApi.Create(m);
await family.reloadMembersList(); await genealogy.reloadMembersList();
setShouldQuit(true); 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 !`); snackbar(`La fiche pour ${r.fullName} a été créée avec succès !`);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -72,7 +75,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
const cancel = () => { const cancel = () => {
setShouldQuit(true); setShouldQuit(true);
n(family.family.URL("members")); n(family.family.URL("genealogy/members"));
}; };
const member = Member.New(family.family.family_id); const member = Member.New(family.family.family_id);
@@ -103,6 +106,7 @@ export function FamilyMemberRoute(): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const { memberId } = useParams(); const { memberId } = useParams();
const [member, setMember] = React.useState<Member>(); const [member, setMember] = React.useState<Member>();
@@ -114,7 +118,7 @@ export function FamilyMemberRoute(): React.ReactElement {
count.current += 1; count.current += 1;
setMember(undefined); setMember(undefined);
await family.reloadMembersList(); await genealogy.reloadMembersList();
}; };
const deleteMember = async () => { const deleteMember = async () => {
@@ -129,9 +133,9 @@ export function FamilyMemberRoute(): React.ReactElement {
await MemberApi.Delete(member!); await MemberApi.Delete(member!);
snackbar("La fiche de membre a été supprimée avec succès !"); 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) { } catch (e) {
console.error(e); console.error(e);
alert("Échec de la suppression du membre !"); alert("Échec de la suppression du membre !");
@@ -147,16 +151,14 @@ export function FamilyMemberRoute(): React.ReactElement {
build={() => ( build={() => (
<MemberPage <MemberPage
member={member!} member={member!}
children={family.members.children(member!.id)} children={genealogy.members.children(member!.id)}
siblings={family.members.siblings(member!.id)} siblings={genealogy.members.siblings(member!.id)}
couples={family.couples.getAllOf(member!)} couples={genealogy.couples.getAllOf(member!)}
creating={false} creating={false}
editing={false} editing={false}
onrequestOpenTree={() => n(family.family.familyTreeURL(member!))} onrequestOpenTree={() => n(family.family.familyTreeURL(member!))}
onRequestDelete={deleteMember} onRequestDelete={deleteMember}
onRequestEdit={() => onRequestEdit={() => n(family.family.memberURL(member!, true))}
n(family.family.URL(`member/${member!.id}/edit`))
}
onForceReload={forceReload} onForceReload={forceReload}
/> />
)} )}
@@ -177,6 +179,7 @@ export function FamilyEditMemberRoute(): React.ReactElement {
const [shouldQuit, setShouldQuit] = React.useState(false); const [shouldQuit, setShouldQuit] = React.useState(false);
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const [member, setMember] = React.useState<Member>(); const [member, setMember] = React.useState<Member>();
const load = async () => { const load = async () => {
@@ -185,8 +188,8 @@ export function FamilyEditMemberRoute(): React.ReactElement {
const cancel = () => { const cancel = () => {
setShouldQuit(true); setShouldQuit(true);
//n(family.family.URL(`member/${member!.id}`)); n(family.family.memberURL(member!));
n(-1); //n(-1);
}; };
const save = async (m: Member) => { 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 !"); snackbar("Les informations du membre ont été mises à jour avec succès !");
await family.reloadMembersList(); await genealogy.reloadMembersList();
setShouldQuit(true); setShouldQuit(true);
n(family.family.URL(`member/${member!.id}`)); n(family.family.memberURL(member!));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
alert("Échec de la mise à jour des informations du membre !"); alert("Échec de la mise à jour des informations du membre !");
@@ -233,7 +236,7 @@ export function MemberPage(p: {
siblings?: Member[]; siblings?: Member[];
couples?: Couple[]; couples?: Couple[];
onCancel?: () => void; onCancel?: () => void;
onSave?: (m: Member) => void; onSave?: (m: Member) => Promise<void>;
onRequestEdit?: () => void; onRequestEdit?: () => void;
onRequestDelete?: () => void; onRequestDelete?: () => void;
onForceReload?: () => void; onForceReload?: () => void;
@@ -241,6 +244,7 @@ export function MemberPage(p: {
}): React.ReactElement { }): React.ReactElement {
const confirm = useConfirm(); const confirm = useConfirm();
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const loadingMessage = useLoadingMessage();
const family = useFamily(); const family = useFamily();
@@ -254,8 +258,12 @@ export function MemberPage(p: {
setMember(new Member(structuredClone(member))); setMember(new Member(structuredClone(member)));
}; };
const save = () => { const save = async () => {
p.onSave!(member); loadingMessage.show(
"Enregistrement des informations du membre en cours..."
);
await p.onSave!(member);
loadingMessage.hide();
}; };
const cancel = async () => { const cancel = async () => {
@@ -656,9 +664,9 @@ export function MemberPage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( to={family.family.URL(
`couple/create?${member.sex === "F" ? "wife" : "husband"}=${ `genealogy/couple/create?${
member.id member.sex === "F" ? "wife" : "husband"
}` }=${member.id}`
)} )}
> >
<Button>Nouveau</Button> <Button>Nouveau</Button>
@@ -676,10 +684,7 @@ export function MemberPage(p: {
<>Aucun enfant</> <>Aucun enfant</>
) : ( ) : (
p.children.map((c) => ( p.children.map((c) => (
<RouterLink <RouterLink key={c.id} to={family.family.memberURL(c)}>
key={c.id}
to={family.family.URL(`member/${c.id}`)}
>
<MemberItem member={c} /> <MemberItem member={c} />
</RouterLink> </RouterLink>
)) ))
@@ -688,7 +693,7 @@ export function MemberPage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( to={family.family.URL(
`member/create?${ `genealogy/member/create?${
member.sex === "F" ? "mother" : "father" member.sex === "F" ? "mother" : "father"
}=${member.id}` }=${member.id}`
)} )}
@@ -708,10 +713,7 @@ export function MemberPage(p: {
<>Aucun frère ou sœur</> <>Aucun frère ou sœur</>
) : ( ) : (
p.siblings.map((c) => ( p.siblings.map((c) => (
<RouterLink <RouterLink key={c.id} to={family.family.memberURL(c)}>
key={c.id}
to={family.family.URL(`member/${c.id}`)}
>
<MemberItem member={c} /> <MemberItem member={c} />
</RouterLink> </RouterLink>
)) ))
@@ -721,7 +723,7 @@ export function MemberPage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( to={family.family.URL(
`member/create?mother=${member.mother}&father=${member.father}` `genealogy/member/create?mother=${member.mother}&father=${member.father}`
)} )}
> >
<Button>Nouveau</Button> <Button>Nouveau</Button>
@@ -743,6 +745,7 @@ function CoupleItem(p: {
const n = useNavigate(); const n = useNavigate();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const statusStr = ServerApi.Config.couples_states.find( const statusStr = ServerApi.Config.couples_states.find(
(c) => c.code === p.couple.state (c) => c.code === p.couple.state
@@ -760,7 +763,7 @@ function CoupleItem(p: {
const otherSpouseID = const otherSpouseID =
p.couple.wife === p.currMemberId ? p.couple.husband : p.couple.wife; p.couple.wife === p.currMemberId ? p.couple.husband : p.couple.wife;
const otherSpouse = otherSpouseID const otherSpouse = otherSpouseID
? family.members.get(otherSpouseID) ? genealogy.members.get(otherSpouseID)
: undefined; : undefined;
return ( return (

View File

@@ -21,18 +21,17 @@ import {
buildAscendingTree, buildAscendingTree,
buildDescendingTree, buildDescendingTree,
treeHeight, treeHeight,
} from "../../utils/family_tree"; } from "../../../utils/family_tree";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { BasicFamilyTree } from "../../widgets/BasicFamilyTree"; import { BasicFamilyTree } from "../../../widgets/BasicFamilyTree";
import { MemberItem } from "../../widgets/MemberItem"; import { MemberItem } from "../../../widgets/MemberItem";
import { RouterLink } from "../../widgets/RouterLink"; import { RouterLink } from "../../../widgets/RouterLink";
import { ComplexFamilyTree } from "../../widgets/complex_family_tree/ComplexFamilyTree"; import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
import { SimpleFamilyTree } from "../../widgets/simple_family_tree/SimpleFamilyTree"; import { SimpleFamilyTree } from "../../../widgets/simple_family_tree/SimpleFamilyTree";
enum CurrTab { enum CurrTab {
BasicTree, BasicTree,
SimpleTree, SimpleTree,
AdvancedTree,
} }
enum TreeMode { enum TreeMode {
@@ -43,22 +42,23 @@ enum TreeMode {
export function FamilyMemberTreeRoute(): React.ReactElement { export function FamilyMemberTreeRoute(): React.ReactElement {
const { memberId } = useParams(); const { memberId } = useParams();
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const [currTab, setCurrTab] = React.useState(CurrTab.SimpleTree); const [currTab, setCurrTab] = React.useState(CurrTab.SimpleTree);
const [currMode, setCurrMode] = React.useState(TreeMode.Descending); 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(() => { const memo: [FamilyTreeNode, number] | null = React.useMemo(() => {
if (!member) return null; if (!member) return null;
const tree = const tree =
currMode === TreeMode.Ascending currMode === TreeMode.Ascending
? buildAscendingTree(member.id, family.members, family.couples) ? buildAscendingTree(member.id, genealogy.members, genealogy.couples)
: buildDescendingTree(member.id, family.members, family.couples); : buildDescendingTree(member.id, genealogy.members, genealogy.couples);
return [tree, treeHeight(tree)]; return [tree, treeHeight(tree)];
}, [member, currMode, family.members, family.couples]); }, [member, currMode, genealogy.members, genealogy.couples]);
const [currDepth, setCurrDepth] = React.useState(0); const [currDepth, setCurrDepth] = React.useState(0);
@@ -89,7 +89,7 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
dense dense
member={member} member={member}
secondary={ secondary={
<RouterLink to={family.family.URL("tree")}> <RouterLink to={family.family.URL("genealogy/tree")}>
<IconButton> <IconButton>
<ClearIcon /> <ClearIcon />
</IconButton> </IconButton>
@@ -149,7 +149,6 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
> >
<Tab tabIndex={CurrTab.BasicTree} label="Basique" /> <Tab tabIndex={CurrTab.BasicTree} label="Basique" />
<Tab tabIndex={CurrTab.SimpleTree} label="Simple" /> <Tab tabIndex={CurrTab.SimpleTree} label="Simple" />
<Tab tabIndex={CurrTab.AdvancedTree} label="Avancé" />
</Tabs> </Tabs>
</div> </div>
@@ -157,14 +156,8 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
<Paper style={{ flex: "1", display: "flex", flexDirection: "column" }}> <Paper style={{ flex: "1", display: "flex", flexDirection: "column" }}>
{currTab === CurrTab.BasicTree ? ( {currTab === CurrTab.BasicTree ? (
<BasicFamilyTree tree={tree!} depth={currDepth} /> <BasicFamilyTree tree={tree!} depth={currDepth} />
) : currTab === CurrTab.SimpleTree ? (
<SimpleFamilyTree tree={tree!} depth={currDepth} />
) : ( ) : (
<ComplexFamilyTree <SimpleFamilyTree tree={tree!} depth={currDepth} />
tree={tree!}
isUp={currMode === TreeMode.Ascending}
depth={currDepth}
/>
)} )}
</Paper> </Paper>
</div> </div>

View File

@@ -4,24 +4,31 @@ import EditIcon from "@mui/icons-material/Edit";
import FemaleIcon from "@mui/icons-material/Female"; import FemaleIcon from "@mui/icons-material/Female";
import MaleIcon from "@mui/icons-material/Male"; import MaleIcon from "@mui/icons-material/Male";
import VisibilityIcon from "@mui/icons-material/Visibility"; 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 { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Member, MemberApi, dateTimestamp, fmtDate } from "../../api/MemberApi"; import {
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; Member,
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; MemberApi,
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; dateTimestamp,
import { useFamily } from "../../widgets/BaseFamilyRoute"; fmtDate,
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; } from "../../../api/genealogy/MemberApi";
import { MemberPhoto } from "../../widgets/MemberPhoto"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { RouterLink } from "../../widgets/RouterLink"; 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 { export function FamilyMembersListRoute(): React.ReactElement {
const alert = useAlert(); const alert = useAlert();
const confirm = useConfirm(); const confirm = useConfirm();
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const [filter, setFilter] = React.useState(""); const [filter, setFilter] = React.useState("");
@@ -36,7 +43,7 @@ export function FamilyMembersListRoute(): React.ReactElement {
return; return;
await MemberApi.Delete(m); await MemberApi.Delete(m);
await family.reloadMembersList(); await genealogy.reloadMembersList();
snackbar("La fiche du membre a été supprimée avec succès !"); snackbar("La fiche du membre a été supprimée avec succès !");
} catch (e) { } catch (e) {
@@ -55,14 +62,14 @@ export function FamilyMembersListRoute(): React.ReactElement {
}} }}
> >
<FamilyPageTitle title="Membres de la famille" /> <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"> <Tooltip title="Créer la fiche d'un nouveau membre">
<Button startIcon={<AddIcon />}>Nouveau</Button> <Button startIcon={<AddIcon />}>Nouveau</Button>
</Tooltip> </Tooltip>
</RouterLink> </RouterLink>
</div> </div>
{family.members.isEmpty ? ( {genealogy.members.isEmpty ? (
<p> <p>
Votre famille n'a aucun membre pour le moment ! Utilisez le bouton Votre famille n'a aucun membre pour le moment ! Utilisez le bouton
situé en haut à droite pour créer le premier ! situé en haut à droite pour créer le premier !
@@ -80,8 +87,8 @@ export function FamilyMembersListRoute(): React.ReactElement {
<MembersTable <MembersTable
members={ members={
filter === "" filter === ""
? family.members.fullList ? genealogy.members.fullList
: family.members.filter((m) => : genealogy.members.filter((m) =>
m.fullName.toLowerCase().includes(filter.toLowerCase()) m.fullName.toLowerCase().includes(filter.toLowerCase())
) )
} }
@@ -97,6 +104,7 @@ function MembersTable(p: {
members: Member[]; members: Member[];
onDelete: (m: Member) => void; onDelete: (m: Member) => void;
}): React.ReactElement { }): React.ReactElement {
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const n = useNavigate(); const n = useNavigate();
@@ -108,7 +116,13 @@ function MembersTable(p: {
sortable: false, sortable: false,
width: 60, width: 60,
renderCell(params) { 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", headerName: "Père",
flex: 5, flex: 5,
renderCell(params) { renderCell(params) {
if (!params.row.father) return <></>; if (!params.row.father)
return family.members.get(params.row.father)!.fullName; 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", headerName: "Mère",
flex: 5, flex: 5,
renderCell(params) { renderCell(params) {
if (!params.row.mother) return <></>; if (!params.row.mother)
return family.members.get(params.row.mother)!.fullName; return (
<Typography color="red" component="span" variant="body2">
Non renseignée
</Typography>
);
return genealogy.members.get(params.row.mother)!.fullName;
}, },
}, },
{ {

View File

@@ -1,6 +1,6 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { MemberInput } from "../../widgets/forms/MemberInput"; import { MemberInput } from "../../../widgets/forms/MemberInput";
export function FamilyTreeRoute(): React.ReactElement { export function FamilyTreeRoute(): React.ReactElement {
const n = useNavigate(); const n = useNavigate();

View 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>&nbsp;</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>
);
}

View File

@@ -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>&nbsp;</p>
<p>Nombre de fiches de membres: {genealogy.members.size}</p>
<p>Nombre de fiches de couples: {genealogy.couples.size}</p>
</div>
</>
);
}

View File

@@ -1,3 +1,5 @@
export function isDebug(): boolean { 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"
);
} }

View File

@@ -1,5 +1,5 @@
import { Couple, CouplesList } from "../api/CoupleApi"; import { Couple, CouplesList } from "../api/genealogy/CoupleApi";
import { Member, MembersList, dateTimestamp } from "../api/MemberApi"; import { Member, MembersList, dateTimestamp } from "../api/genealogy/MemberApi";
export interface CoupleInformation { export interface CoupleInformation {
couple: Couple; couple: Couple;

View 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;
}

View 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;
}

View File

@@ -4,12 +4,15 @@ import {
mdiContentCopy, mdiContentCopy,
mdiCrowd, mdiCrowd,
mdiFamilyTree, mdiFamilyTree,
mdiFileTree,
mdiHomeGroup,
mdiHumanMaleFemale, mdiHumanMaleFemale,
mdiLockCheck, mdiLockCheck,
mdiPlus, mdiPlus,
mdiRefresh, mdiRefresh,
} from "@mdi/js"; } from "@mdi/js";
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";
import HomeIcon from "@mui/icons-material/Home"; import HomeIcon from "@mui/icons-material/Home";
import { import {
Box, Box,
@@ -26,9 +29,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { Outlet, useLocation, useParams } from "react-router-dom"; import { Outlet, useLocation, useParams } from "react-router-dom";
import { CoupleApi, CouplesList } from "../api/CoupleApi";
import { ExtendedFamilyInfo, FamilyApi } from "../api/FamilyApi"; import { ExtendedFamilyInfo, FamilyApi } from "../api/FamilyApi";
import { MemberApi, MembersList } from "../api/MemberApi";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
@@ -37,12 +38,8 @@ import { RouterLink } from "./RouterLink";
interface FamilyContext { interface FamilyContext {
family: ExtendedFamilyInfo; family: ExtendedFamilyInfo;
members: MembersList;
couples: CouplesList;
familyId: number; familyId: number;
reloadFamilyInfo: () => void; reloadFamilyInfo: () => void;
reloadMembersList: () => Promise<void>;
reloadCouplesList: () => Promise<void>;
} }
const FamilyContextK = React.createContext<FamilyContext | null>(null); const FamilyContextK = React.createContext<FamilyContext | null>(null);
@@ -54,8 +51,6 @@ export function BaseFamilyRoute(): React.ReactElement {
const confirm = useConfirm(); const confirm = useConfirm();
const [family, setFamily] = React.useState<null | ExtendedFamilyInfo>(null); 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); const loadKey = React.useRef(1);
@@ -64,15 +59,11 @@ export function BaseFamilyRoute(): React.ReactElement {
const load = async () => { const load = async () => {
const familyID = Number(familyId); const familyID = Number(familyId);
setFamily(await FamilyApi.GetSingle(familyID)); setFamily(await FamilyApi.GetSingle(familyID));
setMembers(await MemberApi.GetEntireList(familyID));
setCouples(await CoupleApi.GetEntireList(familyID));
}; };
const onReload = async () => { const onReload = async () => {
loadKey.current += 1; loadKey.current += 1;
setFamily(null); setFamily(null);
setMembers(null);
setCouples(null);
return new Promise<void>((res, _rej) => { return new Promise<void>((res, _rej) => {
loadPromise.current = () => res(); loadPromise.current = () => res();
@@ -106,7 +97,7 @@ export function BaseFamilyRoute(): React.ReactElement {
return ( return (
<AsyncWidget <AsyncWidget
ready={family !== null && members !== null} ready={family !== null}
loadKey={`${familyId}-${loadKey.current}`} loadKey={`${familyId}-${loadKey.current}`}
load={load} load={load}
errMsg="Échec du chargement des informations de la famille !" errMsg="Échec du chargement des informations de la famille !"
@@ -120,12 +111,8 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyContextK.Provider <FamilyContextK.Provider
value={{ value={{
family: family!, family: family!,
members: members!,
couples: couples!,
familyId: family!.family_id, familyId: family!.family_id,
reloadFamilyInfo: onReload, reloadFamilyInfo: onReload,
reloadMembersList: onReload,
reloadCouplesList: onReload,
}} }}
> >
<Box <Box
@@ -147,41 +134,75 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyLink icon={<HomeIcon />} label="Accueil" uri="" /> <FamilyLink icon={<HomeIcon />} label="Accueil" uri="" />
<FamilyLink {family?.enable_genealogy && (
icon={<Icon path={mdiCrowd} size={1} />} <>
label="Membres" <Divider sx={{ my: 1 }} />
uri="members" <ListSubheader component="div">Généalogie</ListSubheader>
secondaryAction={
<Tooltip title="Créer une nouvelle fiche de membre">
<RouterLink to={family!.URL("member/create")}>
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink <FamilyLink
icon={<Icon path={mdiHumanMaleFemale} size={1} />} icon={<HomeIcon />}
label="Couples" label="Accueil"
uri="couples" uri="genealogy"
secondaryAction={ />
<Tooltip title="Créer une nouvelle fiche de couple"> <FamilyLink
<RouterLink to={family!.URL("couple/create")}> icon={<Icon path={mdiCrowd} size={1} />}
<IconButton> label="Membres"
<Icon path={mdiPlus} size={0.75} /> uri="genealogy/members"
</IconButton> secondaryAction={
</RouterLink> <Tooltip title="Créer une nouvelle fiche de membre">
</Tooltip> <RouterLink
} to={family!.URL("genealogy/member/create")}
/> >
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink <FamilyLink
icon={<Icon path={mdiFamilyTree} size={1} />} icon={<Icon path={mdiHumanMaleFemale} size={1} />}
label="Arbre" label="Couples"
uri="tree" uri="genealogy/couples"
/> secondaryAction={
<Tooltip title="Créer une nouvelle fiche de couple">
<RouterLink
to={family!.URL("genealogy/couple/create")}
>
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink
icon={<Icon path={mdiFamilyTree} size={1} />}
label="Arbre"
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 }} /> <Divider sx={{ my: 1 }} />
<ListSubheader component="div">Administration</ListSubheader> <ListSubheader component="div">Administration</ListSubheader>
@@ -198,6 +219,22 @@ export function BaseFamilyRoute(): React.ReactElement {
uri="settings" 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 */} {/* Invitation code */}
<ListItem <ListItem

View File

@@ -82,7 +82,7 @@ export function BaseLoginPage() {
variant="h6" variant="h6"
style={{ margin: "10px 0px 30px 0px" }} style={{ margin: "10px 0px 30px 0px" }}
> >
La généalogie de votre famille L'intranet de votre famille
</Typography> </Typography>
{/* inner page */} {/* inner page */}

View File

@@ -2,11 +2,11 @@ import { mdiBabyCarriage, mdiCross } from "@mdi/js";
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; 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 React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Couple } from "../api/CoupleApi"; import { Couple } from "../api/genealogy/CoupleApi";
import { Member, fmtDate } from "../api/MemberApi"; import { Member, fmtDate } from "../api/genealogy/MemberApi";
import { FamilyTreeNode } from "../utils/family_tree"; import { FamilyTreeNode } from "../utils/family_tree";
import { useFamily } from "./BaseFamilyRoute"; import { useFamily } from "./BaseFamilyRoute";
import { MemberPhoto } from "./MemberPhoto"; import { MemberPhoto } from "./MemberPhoto";
@@ -16,13 +16,12 @@ export function BasicFamilyTree(p: {
depth: number; depth: number;
}): React.ReactElement { }): React.ReactElement {
return ( return (
<TreeView <SimpleTreeView
defaultCollapseIcon={<ExpandMoreIcon />} slots={{ collapseIcon: ExpandMoreIcon, expandIcon: ChevronRightIcon }}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
> >
<FamilyTreeItem n={p.tree} depth={p.depth} /> <FamilyTreeItem n={p.tree} depth={p.depth} />
</TreeView> </SimpleTreeView>
); );
} }
@@ -40,7 +39,7 @@ function FamilyTreeItem(p: {
return ( return (
<TreeItem <TreeItem
nodeId={p.n.member.id.toString()} itemId={p.n.member.id.toString()}
style={{ margin: "10px" }} style={{ margin: "10px" }}
label={ label={
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>

View File

@@ -11,7 +11,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { unstable_useBlocker as useBlocker } from "react-router-dom"; import { useBlocker } from "react-router-dom";
export function ConfirmLeaveWithoutSaveDialog(p: { export function ConfirmLeaveWithoutSaveDialog(p: {
shouldBlock: boolean; shouldBlock: boolean;

View 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>
);
}

View File

@@ -1,5 +1,5 @@
import { Avatar } from "@mui/material"; import { Avatar } from "@mui/material";
import { Couple } from "../api/CoupleApi"; import { Couple } from "../api/genealogy/CoupleApi";
export function CouplePhoto(p: { export function CouplePhoto(p: {
couple: Couple; couple: Couple;

View File

@@ -2,10 +2,14 @@ import { Alert, Card } from "@mui/material";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
export function FamilyCard( export function FamilyCard(
p: PropsWithChildren<{ error?: string; success?: string }> p: PropsWithChildren<{
error?: string;
success?: string;
style?: React.CSSProperties | undefined;
}>
): React.ReactElement { ): React.ReactElement {
return ( 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.error && <Alert severity="error">{p.error}</Alert>}
{p.success && <Alert severity="success">{p.success}</Alert>} {p.success && <Alert severity="success">{p.success}</Alert>}

View File

@@ -5,7 +5,7 @@ import {
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemText, ListItemText,
} from "@mui/material"; } from "@mui/material";
import { Member, fmtDate } from "../api/MemberApi"; import { Member, fmtDate } from "../api/genealogy/MemberApi";
import { MemberPhoto } from "./MemberPhoto"; import { MemberPhoto } from "./MemberPhoto";
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import FemaleIcon from "@mui/icons-material/Female"; import FemaleIcon from "@mui/icons-material/Female";

View File

@@ -1,5 +1,5 @@
import { Avatar } from "@mui/material"; import { Avatar } from "@mui/material";
import { Member } from "../api/MemberApi"; import { Member } from "../api/genealogy/MemberApi";
export function MemberPhoto(p: { export function MemberPhoto(p: {
member?: Member; member?: Member;

View File

@@ -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)!;
}

View File

@@ -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)
);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,6 +1,6 @@
import { Stack, TextField, Typography } from "@mui/material"; import { Stack, TextField, Typography } from "@mui/material";
import { NumberConstraint, ServerApi } from "../../api/ServerApi"; import { NumberConstraint, ServerApi } from "../../api/ServerApi";
import { DateValue, fmtDate } from "../../api/MemberApi"; import { DateValue, fmtDate } from "../../api/genealogy/MemberApi";
import { PropEdit } from "./PropEdit"; import { PropEdit } from "./PropEdit";
export function DateInput(p: { export function DateInput(p: {

View File

@@ -2,9 +2,10 @@ import ClearIcon from "@mui/icons-material/Clear";
import { Autocomplete, IconButton, TextField, Typography } from "@mui/material"; import { Autocomplete, IconButton, TextField, Typography } from "@mui/material";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Member } from "../../api/MemberApi"; import { Member } from "../../api/genealogy/MemberApi";
import { useFamily } from "../BaseFamilyRoute"; import { useFamily } from "../BaseFamilyRoute";
import { MemberItem } from "../MemberItem"; import { MemberItem } from "../MemberItem";
import { useGenealogy } from "../genealogy/BaseGenealogyRoute";
export function MemberInput(p: { export function MemberInput(p: {
editable: boolean; editable: boolean;
@@ -15,13 +16,14 @@ export function MemberInput(p: {
}): React.ReactElement { }): React.ReactElement {
const n = useNavigate(); const n = useNavigate();
const family = useFamily(); 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(""); const [inputValue, setInputValue] = React.useState("");
if (p.current) { if (p.current) {
const member = family.members.get(p.current)!; const member = genealogy.members.get(p.current)!;
return ( return (
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2">{p.label}</Typography> <Typography variant="body2">{p.label}</Typography>
@@ -30,7 +32,7 @@ export function MemberInput(p: {
onClick={ onClick={
!p.editable !p.editable
? () => { ? () => {
n(family.family.URL(`member/${member.id}`)); n(family.family.memberURL(member));
} }
: undefined : undefined
} }
@@ -55,7 +57,7 @@ export function MemberInput(p: {
return ( return (
<Autocomplete <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) => { onChange={(_event: any, newValue: Member | null | undefined) => {
p.onValueChange(newValue?.id); p.onValueChange(newValue?.id);
}} }}

View File

@@ -5,16 +5,20 @@ export function PropCheckbox(p: {
label: string; label: string;
checked: boolean | undefined; checked: boolean | undefined;
onValueChange: (v: boolean) => void; onValueChange: (v: boolean) => void;
checkboxAlwaysVisible?: boolean;
}): React.ReactElement { }): React.ReactElement {
if (!p.editable && p.checked) if (!p.checkboxAlwaysVisible) {
return <Typography variant="body2">{p.label}</Typography>; if (!p.editable && p.checked)
return <Typography variant="body2">{p.label}</Typography>;
if (!p.editable) return <></>; if (!p.editable) return <></>;
}
return ( return (
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
disabled={!p.editable}
checked={p.checked} checked={p.checked}
onChange={(e) => p.onValueChange(e.target.checked)} onChange={(e) => p.onValueChange(e.target.checked)}
/> />

View 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))}
/>
);
}

View 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>
</>
);
}

View File

@@ -14,6 +14,7 @@ export function PropEdit(p: {
multiline?: boolean; multiline?: boolean;
minRows?: number; minRows?: number;
maxRows?: number; maxRows?: number;
helperText?: string;
}): React.ReactElement { }): React.ReactElement {
if (((!p.editable && p.value) ?? "") === "") return <></>; if (((!p.editable && p.value) ?? "") === "") return <></>;
@@ -44,6 +45,7 @@ export function PropEdit(p: {
!p.checkValue(p.value)) || !p.checkValue(p.value)) ||
false false
} }
helperText={p.helperText}
/> />
); );
} }

View File

@@ -2,7 +2,7 @@ import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import { PropEdit } from "./PropEdit"; import { PropEdit } from "./PropEdit";
export interface SelectOption { export interface SelectOption {
value: string; value: string | undefined;
label: string; label: string;
} }
@@ -19,6 +19,7 @@ export function PropSelect(p: {
const value = p.options.find((o) => o.value === p.value)?.label; const value = p.options.find((o) => o.value === p.value)?.label;
return <PropEdit label={p.label} editable={p.editing} value={value} />; return <PropEdit label={p.label} editable={p.editing} value={value} />;
} }
return ( return (
<FormControl fullWidth variant="filled" style={{ marginBottom: "15px" }}> <FormControl fullWidth variant="filled" style={{ marginBottom: "15px" }}>
<InputLabel>{p.label}</InputLabel> <InputLabel>{p.label}</InputLabel>

View File

@@ -6,7 +6,7 @@ import {
Radio, Radio,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { Sex } from "../../api/MemberApi"; import { Sex } from "../../api/genealogy/MemberApi";
export function SexSelection(p: { export function SexSelection(p: {
readonly?: boolean; readonly?: boolean;

View 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)!;
}

View File

@@ -5,14 +5,15 @@ import { IconButton, Tooltip } from "@mui/material";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import React from "react"; import React from "react";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import { Couple } from "../../api/CoupleApi"; import { Couple } from "../../api/genealogy/CoupleApi";
import { Member } from "../../api/MemberApi"; import { Member } from "../../api/genealogy/MemberApi";
import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider"; import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
import { FamilyTreeNode } from "../../utils/family_tree"; import { FamilyTreeNode } from "../../utils/family_tree";
import { downloadBlob } from "../../utils/files_utils"; import { downloadBlob } from "../../utils/files_utils";
import { getTextWidth } from "../../utils/render_utils"; import { getTextWidth } from "../../utils/render_utils";
import "./simpletree.css"; import "./simpletree.css";
import "./Roboto-normal"; import "./Roboto-normal";
import "svg2pdf.js";
const FACE_WIDTH = 60; const FACE_WIDTH = 60;
const FACE_HEIGHT = 70; const FACE_HEIGHT = 70;
@@ -92,7 +93,8 @@ function buildSimpleTreeNode(
): SimpleTreeNode { ): SimpleTreeNode {
if (depth === 0) throw new Error("Too much recursion reached!"); 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 // Preprocess children
let childrenToProcess = tree.down; let childrenToProcess = tree.down;
@@ -254,6 +256,7 @@ function NodeArea(p: {
let pers2 = p.node.spouse?.member; let pers2 = p.node.spouse?.member;
let didSwap = false; let didSwap = false;
// Show male of the left (all the time)
if (pers2?.sex === "M") { if (pers2?.sex === "M") {
let s = pers1; let s = pers1;
pers1 = pers2; pers1 = pers2;
@@ -293,10 +296,37 @@ function NodeArea(p: {
let childrenLinkX: number; let childrenLinkX: number;
let childrenLinkY: 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); childrenLinkX = Math.floor((endFirstFaceX + beginSecondFaceX!) / 2);
childrenLinkY = middleParentFaceY; childrenLinkY = middleParentFaceY;
} else { }
// Single person
else {
childrenLinkX = parent_x_offset + Math.floor(memberCardWidth(pers1) / 2); childrenLinkX = parent_x_offset + Math.floor(memberCardWidth(pers1) / 2);
childrenLinkY = p.y + CARD_HEIGHT + 2; childrenLinkY = p.y + CARD_HEIGHT + 2;
} }

View File

@@ -1,11 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "ESNext",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom", "types": ["vite/client"],
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@@ -20,7 +17,5 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": ["src"]
"src"
]
} }

View File

@@ -13,4 +13,4 @@ if [ ! "$DRONE_COMMIT_BRANCH" == "master" ]; then
exit 0 exit 0
fi 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

15
geneit_app/vite.config.ts Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -6,34 +6,38 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
log = "0.4.17" log = "0.4.21"
env_logger = "0.10.0" env_logger = "0.11.3"
clap = { version = "4.3.0", features = ["derive", "env"] } clap = { version = "4.5.4", features = ["derive", "env"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
anyhow = "1.0.75" lazy-regex = "3.1.0"
actix-web = "4.3.1" anyhow = "1.0.83"
actix-cors = "0.6.4" actix-web = "4.5.1"
actix-multipart = "0.6.1" actix-cors = "0.7.0"
actix-multipart = "0.7.0"
actix-remote-ip = "0.1.0" actix-remote-ip = "0.1.0"
futures-util = "0.3.28" futures-util = "0.3.30"
diesel = { version = "2.0.4", features = ["postgres"] } diesel = { version = "2.1.6", features = ["postgres"] }
serde = { version = "1.0.188", features = ["derive"] } diesel_migrations = "2.1.0"
serde_json = "1.0.96" serde = { version = "1.0.198", features = ["derive"] }
mailchecker = "5.0.9" serde_json = "1.0.117"
redis = "0.23.2" mailchecker = "6.0.4"
lettre = "0.10.4" redis = "0.25.3"
lettre = "0.11.7"
rand = "0.8.5" rand = "0.8.5"
bcrypt = "0.15.0" bcrypt = "0.15.1"
light-openid = "1.0.1" light-openid = "1.0.2"
thiserror = "1.0.40" thiserror = "1.0.60"
serde_with = "3.1.0" serde_with = "3.8.1"
rust_iso3166 = "0.1.10" rust_iso3166 = "0.1.12"
rust-s3 = "0.33.0" rust-s3 = "0.34.0"
sha2 = "0.10.7" sha2 = "0.10.8"
image = "0.24.6" image = "0.25.1"
uuid = { version = "1.4.1", features = ["v4"] } uuid = { version = "1.8.0", features = ["v4"] }
httpdate = "1.0.2" httpdate = "1.0.3"
zip = "0.6.6" zip = "2.0.0"
mime_guess = "2.0.4" mime_guess = "2.0.4"
tempfile = "3.7.1" tempfile = "3.10.1"
base64 = "0.21.2" base64 = "0.22.0"
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
chrono = "0.4.38"

View File

@@ -1,4 +1,4 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y libcurl4 libpq5 \ && apt-get install -y libcurl4 libpq5 \

3
geneit_backend/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rerun-if-changed=migrations");
}

View File

@@ -27,7 +27,7 @@ services:
- ./storage/db:/var/lib/postgresql/data - ./storage/db:/var/lib/postgresql/data
mailcatcher: mailcatcher:
image: dockage/mailcatcher:0.8.2 image: dockage/mailcatcher:0.9.0
ports: ports:
- 1080:1080 - 1080:1080
- 1025:1025 - 1025:1025

View File

@@ -0,0 +1,3 @@
-- Remove column to toggle genealogy
ALTER TABLE public.families
DROP COLUMN enable_genealogy;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -3,9 +3,13 @@
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
use diesel::result::{DatabaseErrorKind, Error}; use diesel::result::{DatabaseErrorKind, Error};
use diesel::{Connection, PgConnection}; use diesel::{Connection, PgConnection};
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use std::cell::RefCell; use std::cell::RefCell;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
thread_local! { 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 /// 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(())
}

View File

@@ -7,7 +7,7 @@ use std::cell::RefCell;
use std::time::Duration; use std::time::Duration;
thread_local! { 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 /// Execute a request on Redis
@@ -45,7 +45,7 @@ where
{ {
let value_str = serde_json::to_string(value)?; 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(()) Ok(())
} }

View File

@@ -22,11 +22,11 @@ pub async fn create_bucket_if_required() -> anyhow::Result<()> {
log::debug!("The bucket already exists."); log::debug!("The bucket already exists.");
return Ok(()); 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."); log::warn!("Failed to fetch bucket location, but it seems that bucket exists.");
return Ok(()); 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!") log::warn!("The bucket does not seem to exists, trying to create it!")
} }
Err(e) => { Err(e) => {

View File

@@ -60,6 +60,10 @@ pub struct StaticConstraints {
pub member_country: SizeConstraint, pub member_country: SizeConstraint,
pub member_sex: SizeConstraint, pub member_sex: SizeConstraint,
pub member_note: 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 { impl Default for StaticConstraints {
@@ -91,6 +95,10 @@ impl Default for StaticConstraints {
member_country: SizeConstraint::new(0, 2), member_country: SizeConstraint::new(0, 2),
member_sex: SizeConstraint::new(0, 1), member_sex: SizeConstraint::new(0, 1),
member_note: SizeConstraint::new(0, 35000), 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 /// Thumbnail height
pub const THUMB_HEIGHT: u32 = 350; 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);

View 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())
}

View File

@@ -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()))
}

View File

@@ -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())
}

View File

@@ -14,7 +14,7 @@ use mime_guess::Mime;
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::io;
use std::io::{Cursor, Read, Write}; use std::io::{Cursor, Read, Write};
use zip::write::FileOptions; use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipArchive}; use zip::{CompressionMethod, ZipArchive};
const MEMBERS_FILE: &str = "members.json"; const MEMBERS_FILE: &str = "members.json";
@@ -38,7 +38,7 @@ struct ImportCoupleRequest {
/// Export whole family data /// Export whole family data
pub async fn export_family(f: FamilyInPath) -> HttpResult { 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 members = members_service::get_all_of_family(f.family_id()).await?;
let couples = couples_service::get_all_of_family(f.family_id()).await?; let couples = couples_service::get_all_of_family(f.family_id()).await?;

View File

@@ -79,6 +79,8 @@ pub async fn list(token: LoginToken) -> HttpResult {
struct RichFamilyInfo { struct RichFamilyInfo {
#[serde(flatten)] #[serde(flatten)]
membership: FamilyMembership, membership: FamilyMembership,
enable_genealogy: bool,
enable_accommodations: bool,
disable_couple_photos: 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?; let family = families_service::get_by_id(f.family_id()).await?;
Ok(HttpResponse::Ok().json(RichFamilyInfo { Ok(HttpResponse::Ok().json(RichFamilyInfo {
membership, membership,
enable_genealogy: family.enable_genealogy,
enable_accommodations: family.enable_accommodations,
disable_couple_photos: family.disable_couple_photos, disable_couple_photos: family.disable_couple_photos,
})) }))
} }
@@ -101,8 +105,10 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct UpdateFamilyBody { pub struct UpdateFamilyBody {
name: String, name: Option<String>,
disable_couple_photos: bool, enable_genealogy: Option<bool>,
enable_accommodations: Option<bool>,
disable_couple_photos: Option<bool>,
} }
/// Update a family /// Update a family
@@ -110,16 +116,28 @@ pub async fn update(
f: FamilyInPathWithAdminMembership, f: FamilyInPathWithAdminMembership,
req: web::Json<UpdateFamilyBody>, req: web::Json<UpdateFamilyBody>,
) -> HttpResult { ) -> HttpResult {
if !StaticConstraints::default() let mut family = families_service::get_by_id(f.family_id()).await?;
.family_name_len
.validate(&req.name) if let Some(name) = &req.name {
{ if !StaticConstraints::default().family_name_len.validate(name) {
return Ok(HttpResponse::BadRequest().body("Invalid family name!")); return Ok(HttpResponse::BadRequest().body("Invalid family name!"));
}
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;
} }
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;
families_service::update_family(&family).await?; families_service::update_family(&family).await?;
log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id()); log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id());

View File

@@ -103,8 +103,29 @@ fn check_opt_str_val(
Ok(()) 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 { 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(); let c = StaticConstraints::default();
check_opt_str_val( check_opt_str_val(
&self.first_name, &self.first_name,

View File

@@ -5,6 +5,9 @@ use actix_web::HttpResponse;
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use zip::result::ZipError; 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 auth_controller;
pub mod couples_controller; pub mod couples_controller;
pub mod data_controller; pub mod data_controller;

View 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!")
})
})
}
}

View File

@@ -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!")
})
})
}
}

View File

@@ -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!")
})
})
}
}

View File

@@ -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 couple_extractor;
pub mod family_extractor; pub mod family_extractor;
pub mod member_extractor; pub mod member_extractor;

View File

@@ -4,10 +4,12 @@ use actix_remote_ip::RemoteIPConfig;
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
use geneit_backend::app_config::AppConfig; use geneit_backend::app_config::AppConfig;
use geneit_backend::connections::s3_connection; use geneit_backend::connections::{db_connection, s3_connection};
use geneit_backend::controllers::{ use geneit_backend::controllers::{
auth_controller, couples_controller, data_controller, families_controller, members_controller, accommodations_list_controller, accommodations_reservations_calendars_controller,
photos_controller, server_controller, users_controller, accommodations_reservations_controller, auth_controller, couples_controller, data_controller,
families_controller, members_controller, photos_controller, server_controller,
users_controller,
}; };
#[actix_web::main] #[actix_web::main]
@@ -22,6 +24,10 @@ async fn main() -> std::io::Result<()> {
.await .await
.expect("Failed to initialize S3 bucket!"); .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(|| { HttpServer::new(|| {
App::new() App::new()
.wrap( .wrap(
@@ -133,73 +139,146 @@ async fn main() -> std::io::Result<()> {
"/family/{id}/user/{user_id}", "/family/{id}/user/{user_id}",
web::delete().to(families_controller::delete_membership), web::delete().to(families_controller::delete_membership),
) )
// Members controller // [GENEALOGY] Members controller
.route( .route(
"/family/{id}/member/create", "/family/{id}/genealogy/member/create",
web::post().to(members_controller::create), web::post().to(members_controller::create),
) )
.route( .route(
"/family/{id}/members", "/family/{id}/genealogy/members",
web::get().to(members_controller::get_all), web::get().to(members_controller::get_all),
) )
.route( .route(
"/family/{id}/member/{member_id}", "/family/{id}/genealogy/member/{member_id}",
web::get().to(members_controller::get_single), web::get().to(members_controller::get_single),
) )
.route( .route(
"/family/{id}/member/{member_id}", "/family/{id}/genealogy/member/{member_id}",
web::put().to(members_controller::update), web::put().to(members_controller::update),
) )
.route( .route(
"/family/{id}/member/{member_id}", "/family/{id}/genealogy/member/{member_id}",
web::delete().to(members_controller::delete), web::delete().to(members_controller::delete),
) )
.route( .route(
"/family/{id}/member/{member_id}/photo", "/family/{id}/genealogy/member/{member_id}/photo",
web::put().to(members_controller::set_photo), web::put().to(members_controller::set_photo),
) )
.route( .route(
"/family/{id}/member/{member_id}/photo", "/family/{id}/genealogy/member/{member_id}/photo",
web::delete().to(members_controller::remove_photo), web::delete().to(members_controller::remove_photo),
) )
// Couples controller // [GENEALOGY] Couples controller
.route( .route(
"/family/{id}/couple/create", "/family/{id}/genealogy/couple/create",
web::post().to(couples_controller::create), web::post().to(couples_controller::create),
) )
.route( .route(
"/family/{id}/couples", "/family/{id}/genealogy/couples",
web::get().to(couples_controller::get_all), web::get().to(couples_controller::get_all),
) )
.route( .route(
"/family/{id}/couple/{couple_id}", "/family/{id}/genealogy/couple/{couple_id}",
web::get().to(couples_controller::get_single), web::get().to(couples_controller::get_single),
) )
.route( .route(
"/family/{id}/couple/{couple_id}", "/family/{id}/genealogy/couple/{couple_id}",
web::put().to(couples_controller::update), web::put().to(couples_controller::update),
) )
.route( .route(
"/family/{id}/couple/{couple_id}", "/family/{id}/genealogy/couple/{couple_id}",
web::delete().to(couples_controller::delete), web::delete().to(couples_controller::delete),
) )
.route( .route(
"/family/{id}/couple/{couple_id}/photo", "/family/{id}/genealogy/couple/{couple_id}/photo",
web::put().to(couples_controller::set_photo), web::put().to(couples_controller::set_photo),
) )
.route( .route(
"/family/{id}/couple/{couple_id}/photo", "/family/{id}/genealogy/couple/{couple_id}/photo",
web::delete().to(couples_controller::remove_photo), web::delete().to(couples_controller::remove_photo),
) )
// Data controller // [GENEALOGY] Data controller
.route( .route(
"/family/{id}/data/export", "/family/{id}/genealogy/data/export",
web::get().to(data_controller::export_family), web::get().to(data_controller::export_family),
) )
.route( .route(
"/family/{id}/data/import", "/family/{id}/genealogy/data/import",
web::put().to(data_controller::import_family), 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 // Photos controller
.route( .route(
"/photo/{id}", "/photo/{id}",

View File

@@ -1,6 +1,11 @@
use crate::app_config::AppConfig; 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::crypt_utils::sha256;
use crate::utils::time_utils::time;
use diesel::prelude::*; use diesel::prelude::*;
/// User ID holder /// User ID holder
@@ -65,6 +70,8 @@ pub struct Family {
pub name: String, pub name: String,
pub invitation_code: String, pub invitation_code: String,
pub disable_couple_photos: bool, pub disable_couple_photos: bool,
pub enable_genealogy: bool,
pub enable_accommodations: bool,
} }
impl Family { impl Family {
@@ -307,7 +314,7 @@ pub struct NewMember {
pub time_update: i64, pub time_update: i64,
} }
/// Member ID holder /// Couple ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct CoupleID(pub i32); pub struct CoupleID(pub i32);
@@ -440,3 +447,153 @@ pub struct NewCouple {
pub time_create: i64, pub time_create: i64,
pub time_update: 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