Compare commits

...
Sign in to create a new pull request.

180 commits

Author SHA1 Message Date
Jeremy Munsch
a69f51b61a
Merge branch 'trunk' 2026-10-26 12:17:59 +01:00
Jeremy Munsch
1069076e48
Merge branch 'develop' into trunk 2026-10-26 12:17:49 +01:00
Jeremy Munsch
a2ded8f2b2
build: fix manifest user to 82 2026-10-26 12:17:34 +01:00
Noah Lancien
4fa91c3c88 Merge branch 'trunk' 2026-10-22 16:52:24 +02:00
Noah Lancien
d49d28ffc6 Merge branch 'develop' into trunk 2026-10-22 16:52:02 +02:00
Noix
ca569455dd Merge branch 'bump-deps-and-docker-file' into 'develop'
Bump deps and docker file

See merge request ldc-web/projets/crm!57
2026-10-22 14:50:37 +00:00
b01f0fb9dd
chore: add missing phpstan 2026-10-22 16:40:39 +02:00
cc6ad2e6e1
chore: phpcs fix 2026-10-22 16:37:28 +02:00
ea1d6c6bba
chore : vscode config intellephnese php version 2026-10-22 16:01:36 +02:00
132c25a75e
chore: replace deprecated constant mysql in config 2026-10-22 16:01:21 +02:00
c235055b62
build: docker file bump 2026-10-22 15:58:19 +02:00
4eaebf86c0
fix: Claim factory 2026-10-22 15:58:06 +02:00
61641660e6
chore: bump composer (php8.5 with advisories) 2026-10-22 15:57:51 +02:00
29e5728220
chore: git ignore tests coverage 2026-10-22 15:57:30 +02:00
Vincent BRIDIER
feda6e7613 Merge branch 'trunk' 2026-07-29 11:48:56 +02:00
Vincent BRIDIER
07804370d5 Merge branch 'develop' into trunk 2026-07-29 11:23:25 +02:00
Vincent BRIDIER
c1a244a4f4 feat: Add k8s internal url for trusting host 2026-07-29 11:23:09 +02:00
K Muller
700992e89f Merge branch 'trunk' 2026-05-21 13:09:08 +02:00
K Muller
c26b8c071d Merge branch 'develop' into trunk 2026-05-21 13:08:39 +02:00
karl.muller
eeddf109f5 Merge branch '59-cron-job' into 'develop'
feat: ✨ run job only on weekdays

See merge request ldc-web/projets/crm!55
2026-05-21 11:07:50 +00:00
Corentin Huet
25188fcbcf feat: ✨ run job only on weekdays 2026-05-12 13:25:49 +02:00
Noah Lancien
3e954bb51b Merge branch 'trunk' 2026-04-23 12:45:18 +02:00
Noah Lancien
d8e00b3833 Merge branch 'develop' into trunk 2026-04-23 12:45:03 +02:00
Noix
820eb7b62c Merge branch 'issue-51' into 'develop'
Issue 51

See merge request ldc-web/projets/crm!54
2026-04-23 10:41:56 +00:00
Alexis Gousseau
85acd0be1e
chore: 🔖 release v4.1.10 2026-04-23 11:38:41 +02:00
Alexis Gousseau
56c96cc794
chore: 🔧 set new rediction in .htaccess 2026-04-23 11:38:24 +02:00
Alexis Gousseau
b5bf949dca
ci: 👷 Added @uwil npm config in Dockerfile 2026-04-23 11:38:24 +02:00
Alexis Gousseau
a226e37086
feat: ✨ add ckeditor input in crud of claim mail templates and claim manual response #51 2026-04-23 11:38:23 +02:00
Alexis Gousseau
49ee59295b
feat: ✨ add LogoutUnpublishedUsers middleware 2026-04-23 11:38:23 +02:00
Alexis Gousseau
8f7ef0ed92
feat: ✨ add commit lint from husky configuration 2026-04-23 11:38:23 +02:00
Noah Lancien
6e12256a33 Merge branch 'trunk' 2026-04-23 09:37:35 +02:00
Noah Lancien
5ef171c666 Merge branch 'develop' into trunk 2026-04-23 09:37:20 +02:00
Noix
15eef6a692 Merge branch 'issue-52' into 'develop'
Issue 52

See merge request ldc-web/projets/crm!53
2026-04-23 07:36:57 +00:00
Alexis Gousseau
8f5f01df8e
chore: 🔖 release v4.1.9 2026-04-15 12:23:33 +02:00
Alexis Gousseau
6ceb32fdca
fix: 🐛 set brand id from service in claim call #52 2026-04-15 12:22:56 +02:00
Noah Lancien
e6194a3d73 Merge branch 'trunk' 2026-04-13 11:30:57 +02:00
Noah Lancien
1defb88357 Merge branch 'develop' into trunk 2026-04-13 11:30:40 +02:00
Noix
c5cb57d88a Merge branch 'add-germany' into 'develop'
fix: 🏷️ add Germany to the valid countries

See merge request ldc-web/projets/crm!52
2026-04-13 09:29:59 +00:00
Noah Lancien
0787d77beb Merge branch 'trunk' 2026-04-13 09:15:24 +02:00
Corentin Huet
69b63d0aae fix: 🏷️ add Germany to the valid countries 2026-04-09 16:20:57 +02:00
Noah Lancien
01f3c8a319 Merge branch 'develop' into trunk 2026-04-09 11:11:36 +02:00
Noix
47018bad75 Merge branch 'issue-53' into 'develop'
Issue 53 - Association customer à faire côté Serveur et non client

See merge request ldc-web/projets/crm!50
2026-04-09 09:10:49 +00:00
Alexis Gousseau
b23a1ab795
chore: 🔖 release v4.1.8 2026-03-31 15:26:15 +02:00
Alexis Gousseau
c4d45c2501
fix: 🚨 resolve phpcs errors 2026-03-31 15:25:55 +02:00
Alexis Gousseau
1920acf6e6
test: ✅ update and pass all tests 2026-03-31 15:04:41 +02:00
Alexis Gousseau
2a56bf4c67
fix: 🔥 move claim customer association into server side 2026-03-31 14:59:48 +02:00
Noah Lancien
25e58a6ed2 Merge branch 'trunk' 2026-01-29 15:33:25 +01:00
Noah Lancien
67d4debcdb Merge branch 'develop' into trunk 2026-01-29 15:32:45 +01:00
Noah Lancien
e170cf351e ci: limit jobs history to 1 2026-01-29 15:32:05 +01:00
Noah Lancien
1d28cbe7cd Merge branch 'trunk' 2026-01-12 10:14:14 +01:00
Noah Lancien
cfeca2bd21 Merge branch 'develop' into trunk 2026-01-12 10:13:48 +01:00
Noah Lancien
4f21f46464 feat: updated admin-secure 2026-01-12 10:12:57 +01:00
Noah Lancien
f2965dd9ef ci: added ci variables to define pod resources 2026-01-12 10:04:08 +01:00
Noah Lancien
dac9b12106 Merge branch 'develop' into trunk 2026-01-06 17:17:07 +01:00
noah.lancien
d20d3bd710 Merge branch '54-mail-to-digital' into 'develop'
feat: send mail daily at 7am if some claims are waiting for the digital service

See merge request ldc-web/projets/crm!49
2026-01-06 16:16:11 +00:00
Noah Lancien
6e8a296205 feat: send mail daily at 7am if some claims are waiting for the digital service 2026-01-06 17:05:32 +01:00
Vincent BRIDIER
83400e1e20 Merge branch 'trunk' 2025-12-19 08:53:08 +01:00
Vincent BRIDIER
e7ca76950a Merge branch 'develop' into trunk 2025-12-19 08:52:57 +01:00
Vincent BRIDIER
ff3550893a fix: Fix Claim interface 2025-12-19 08:52:46 +01:00
Vincent BRIDIER
93202edd09 Merge branch 'trunk' 2025-12-18 14:39:30 +01:00
Vincent BRIDIER
285e12bde4 Merge branch 'develop' into trunk 2025-12-16 19:59:39 +01:00
Vincent BRIDIER
93294ef6de fix: Some layout + Claims fixes 2025-12-16 19:59:05 +01:00
Vincent BRIDIER
06f71db87b feat: Some common fixes 2025-12-16 19:56:11 +01:00
Noah Lancien
a2fe764732 Merge branch 'develop' into trunk 2025-12-08 16:50:03 +01:00
noah.lancien
a831803437 Merge branch 'claims-brand-filter' into 'develop'
fix: hide subject filter when there's no brand selected + select default brand if user only has one

See merge request ldc-web/projets/crm!48
2025-12-08 15:47:28 +00:00
Noah Lancien
e815b89e53 fix: hide subject filter when there's no brand selected + select default brand if user only has one 2025-12-08 16:35:26 +01:00
Noah Lancien
215fb1327e Merge branch 'develop' into trunk 2025-12-08 15:11:21 +01:00
Noah Lancien
92607611dd fix: 500 on users.create because of wrong condition for brands input 2025-12-08 15:10:57 +01:00
Noah Lancien
00a9fc7d76 Merge branch 'develop' into trunk 2025-12-08 15:01:27 +01:00
Noah Lancien
ffaaf1ec1b fix: increase table size in index-list of claims 2025-12-08 15:01:01 +01:00
Noah Lancien
dbd4c88e74 Merge branch 'develop' into trunk 2025-12-08 11:56:12 +01:00
Noah Lancien
f7bac9c221 fix: style update for index-list of claims 2025-12-08 11:55:45 +01:00
Noah Lancien
5354adab6e Merge branch 'develop' into trunk 2025-12-03 15:57:59 +01:00
noah.lancien
8887aabac4 Merge branch 'display-claim-file' into 'develop'
fix: claim.show file button was broken since the file is in private storage

See merge request ldc-web/projets/crm!47
2025-12-03 14:54:56 +00:00
Noah Lancien
faa63e271f fix: claim.show file button was broken since the file is in private storage 2025-12-03 10:31:18 +01:00
Noah Lancien
bd5862d592 Merge branch 'develop' into trunk 2025-12-02 11:42:59 +01:00
noah.lancien
b6fe43cdcf Merge branch '49-update-recla' into 'develop'
feat: many ui improvements for the claim part

See merge request ldc-web/projets/crm!46
2025-12-02 10:40:31 +00:00
Noah Lancien
647943da11 fix: make export more generic 2025-12-02 09:48:28 +01:00
Noah Lancien
0aea086f26 fix: make the export feature more generic 2025-12-01 16:23:45 +01:00
Noah Lancien
2b240ed4ca fix: move uploaded claims files to private storage + use .zip for some exports types 2025-11-27 16:19:48 +01:00
Noah Lancien
b7b4013897 feat: remove export mail and replaced with .csv and .pdf downloads 2025-11-25 14:57:34 +01:00
Noah Lancien
8fbaa234b4 feat: many ui improvements for the claim part 2025-11-24 12:58:47 +01:00
Vincent BRIDIER
d8796ee78a feat: Update README 2025-11-05 09:10:46 +00:00
Noah Lancien
d1d9ffd418 Merge branch 'develop' into trunk 2025-10-16 16:37:58 +02:00
noah.lancien
45849688f6 Merge branch '48-docs-reclas' into 'develop'
feat: ✨ wording update and restrict profile page for marketing role

See merge request ldc-web/projets/crm!45
2025-10-16 14:35:44 +00:00
Corentin Huet
5d5a4c828f refactor: ♻️ make user policies strict to conceptor role 2025-10-16 16:20:47 +02:00
Corentin Huet
58c116f2f0 feat: ✨ wording update and restrict profile page for marketing role 2025-10-15 15:57:38 +02:00
Noah Lancien
cc9521c233 Merge branch 'develop' into trunk 2025-10-07 12:58:21 +02:00
Noah Lancien
b4f343940e ci: added staging-resources.yaml 2025-10-07 12:57:51 +02:00
Noah Lancien
de20063c5f Merge branch 'develop' into trunk 2025-10-07 12:49:01 +02:00
noah.lancien
fd11b966e4 Merge branch 'update' into 'develop'
chore: 🔖 release v4.1.7

See merge request ldc-web/projets/crm!44
2025-10-07 10:46:17 +00:00
Alexis Gousseau
d9357de45f
chore: 🔖 release v4.1.7 2025-10-07 11:32:03 +02:00
Alexis Gousseau
aa8ac3fec5
fix: 🐛 restrict user brands associated to conceptor users 2025-10-07 11:32:03 +02:00
Alexis Gousseau
b36aa19028
fix: ✅ update and pass all tests 2025-10-07 11:32:02 +02:00
Alexis Gousseau
16e4e4ee19
fix: 🐛 update claim customer relation with hashcode 2025-10-07 11:32:02 +02:00
Alexis Gousseau
151d9e04a9
fix: ♿️ update claim status after sending manual response 2025-10-07 11:32:02 +02:00
Alexis Gousseau
bd343277cc
fix: 🛂 update claims permissions 2025-10-07 11:31:53 +02:00
Alexis Gousseau
44035c9589
fix: 🌐 update claim messages translations 2025-10-07 10:32:38 +02:00
Noah Lancien
14de384af1 Merge branch 'develop' into trunk 2025-09-17 10:52:10 +02:00
Noah Lancien
b6b06d6cf8 chore: bump laravel-common to 4.1.1 2025-09-17 10:49:43 +02:00
Noah Lancien
9c91351388 Merge branch 'trunk' 2025-09-09 09:24:53 +02:00
Noah Lancien
d5d9ef6fdc Merge branch 'develop' into trunk 2025-09-08 15:36:16 +02:00
noah.lancien
b31f87bf8b Merge branch 'claim_file' into 'develop'
fix: 🐛 use store base64 file method of laravel common to save claim file

See merge request ldc-web/projets/crm!39
2025-09-08 13:34:45 +00:00
noah.lancien
ace4e0cce8 Merge branch 'cast_model_id' into 'develop'
fix: 🚑 update casting model_id column

See merge request ldc-web/projets/crm!43
2025-09-08 13:33:34 +00:00
Alexis Gousseau
c9f25b17aa
chore: 🔖 release v4.1.5 2025-09-08 15:06:33 +02:00
Alexis Gousseau
ecc2b7059e
fix: ⚡ improve bo pagination #46 2025-09-08 15:06:00 +02:00
Alexis Gousseau
3ab1a2244e
fix: 🐛 associating answer to their question on store action #25 2025-09-08 14:59:54 +02:00
Alexis Gousseau
7a2318b6a3
fix: 🐛 use store base64 file method of laravel common to save claim file 2025-09-08 14:59:54 +02:00
Alexis Gousseau
ded7cd7266
chore: ⬆️ update laravel-common to v4.1.0 2025-09-08 14:59:54 +02:00
Alexis Gousseau
1aaadb88d0
fix: 🚨 resolve phpcs errors 2025-09-08 14:59:53 +02:00
Alexis Gousseau
b3fb0301f8
chore: 🔖 release v4.1.4 2025-09-08 14:25:24 +02:00
Alexis Gousseau
09414e228f
fix: 🚑 update casting model_id column 2025-09-08 14:25:11 +02:00
Vincent BRIDIER
ff123f6769
Merge branch 'trunk' 2025-09-03 12:42:10 +02:00
Vincent BRIDIER
811795abd8
Merge branch 'develop' into trunk 2025-09-03 12:41:44 +02:00
Vincent BRIDIER
de13bde18f Merge branch 'cast_model_id' into 'develop'
fix: 🚑 force cast to string for model_id column

See merge request ldc-web/projets/crm!42
2025-09-03 10:41:28 +00:00
Alexis Gousseau
18b83b7197
chore: 🔖 release v4.1.3 2025-09-03 10:39:52 +02:00
Alexis Gousseau
93331aaca1
fix: 🚑 force cast to string for model_id column 2025-09-03 10:39:29 +02:00
Vincent BRIDIER
d61da12e63 Merge branch 'singleoptin' into 'develop'
fix: 🐛 change the customer's opt-in value if it exists when call api optin route #27

Closes #27

See merge request ldc-web/projets/crm!38
2025-08-28 10:08:54 +00:00
Alexis Gousseau
11a999ce42
chore: 🔖 release v4.1.2 2025-08-27 11:52:09 +02:00
Alexis Gousseau
1b9a5dacf8
fix: 🐛 update questions/answers filters on index pages 2025-08-27 11:51:56 +02:00
Vincent BRIDIER
e76312fad2
Merge branch 'develop' into trunk 2025-08-27 09:41:31 +02:00
Alexis Gousseau
0f008b0db4
fix: 🐛 change the customer's opt-in value if it exists when call api optin route #27 2025-08-27 09:39:17 +02:00
Vincent BRIDIER
06694f0a48 Merge branch 'cache' into 'develop'
fix: 🐛 update saving claim file

See merge request ldc-web/projets/crm!37
2025-08-27 07:30:43 +00:00
Alexis Gousseau
448369c7ab
chore: 🔖 release v4.1.1 2025-08-26 09:38:02 +02:00
Alexis Gousseau
eeb467d1f0
fix: 🐛 update saving claim file 2025-08-26 09:34:58 +02:00
Alexis Gousseau
4a23d74917
fix: 🌐 update some claim translations 2025-08-25 14:26:32 +02:00
Alexis Gousseau
b9f43428d8
fix: 🐛 call the cache reset method on every model event for the claim subject and template 2025-08-25 14:25:38 +02:00
Noah Lancien
5527740c26 Merge branch 'trunk' 2025-08-12 08:52:49 +02:00
Vincent BRIDIER
83c4e4d7c4
Merge branch 'develop' into trunk 2025-08-07 18:08:21 +02:00
Vincent BRIDIER
2f8a53c653 Merge branch 'clean' into 'develop'
Clean

See merge request ldc-web/projets/crm!36
2025-08-07 16:07:45 +00:00
Alexis Gousseau
351126ebf8
fix: 🐛 update answer cache and cascade delete 2025-08-07 17:41:07 +02:00
Alexis Gousseau
da8074c5a4
fix: 🐛 reorder function parameters to avoid PHP 8.0+ optional parameter warning 2025-08-07 17:41:07 +02:00
Alexis Gousseau
36494a7944
feat: ✨ add brand fields (logo/domain/url/url_claim) 2025-08-07 17:40:37 +02:00
Noah Lancien
c4473115d2 Merge branch 'develop' into trunk 2025-08-06 16:23:46 +02:00
Noah Lancien
30c113a30c ci: update image version + replace 443 port by 80 in manifest 2025-08-06 16:23:30 +02:00
Noah Lancien
1ac10f9276 Merge branch 'develop' into trunk 2025-08-06 15:27:02 +02:00
noah.lancien
515bbf4758 Merge branch 'reclas' into 'develop'
Add claim functionnality

Closes #40

See merge request ldc-web/projets/crm!35
2025-08-06 13:24:18 +00:00
Alexis Gousseau
76cc3a5cd8
fix: ✅ pass all tests with client up to date 2025-08-05 15:30:34 +02:00
Alexis Gousseau
978cfc2109
fix: 🔥 remove rennokki/laravel-eloquent-query-cache package 2025-08-05 14:12:05 +02:00
Alexis Gousseau
8caaff989c
feat: ✨ adding a field to associate brands with users 2025-08-05 12:37:46 +02:00
Alexis Gousseau
fff623144a
fix: 🐛 standardize ID extraction using modelKeys() instead of pluck('id') 2025-08-05 09:49:31 +02:00
Alexis Gousseau
927ae02240
chore: 📦 update laravel common dependency (v3.4.3) 2025-08-04 16:07:31 +02:00
Alexis Gousseau
281d23b03e
feat: ✨ add cache on models 2025-08-04 16:03:46 +02:00
Alexis Gousseau
ed7ed6f55e
feat: ✨ add claim mail templates crud 2025-08-04 16:03:46 +02:00
Alexis Gousseau
7df0fe4634
feat: ✨ add claim subjects crud 2025-08-04 16:03:45 +02:00
Alexis Gousseau
ced124b60e
feat: ✨ add claims bo interface 2025-08-04 16:03:38 +02:00
Alexis Gousseau
187b96fbaf
feat: ✅ add claims api route and tests for V4 2025-07-28 09:08:01 +02:00
Alexis Gousseau
a3a84446f8
feat: 🗃️ add claims tables, seeder and relations 2025-07-28 09:07:54 +02:00
Vincent BRIDIER
af090bbef7 Merge branch 'update' into 'develop'
chore: 🔖 release v4.1.0

Closes #41

See merge request ldc-web/projets/crm!33
2025-07-25 12:54:21 +00:00
Vincent BRIDIER
a269295a79 Merge branch 'saml' into 'develop'
feat: 🛂 change BO Auth for SAML #42

Closes #42

See merge request ldc-web/projets/crm!34
2025-07-25 12:33:00 +00:00
Alexis Gousseau
c828952e7b
feat: 🛂 change BO Auth for SAML #42 2025-07-25 11:41:52 +02:00
Alexis Gousseau
6a60f00fbe
chore: 🔖 release v4.1.0 2025-07-25 09:46:12 +02:00
Alexis Gousseau
7cfe36b40f
feat: ✨ add answers filter on single optins 2025-07-25 09:46:11 +02:00
Alexis Gousseau
89609454ad
feat: ✨ segmentation optins with answers #41 2025-07-25 09:44:40 +02:00
Karl
b178539626 Merge branch 'develop' into trunk 2025-06-04 09:01:00 +02:00
Karl
6fdcdd1a2a Merge branch 'develop' into trunk 2025-06-03 17:13:51 +02:00
Karl Muller
0f1f258706 Merge branch 'develop' into trunk 2025-04-28 09:51:25 +00:00
Karl Muller
7eeeb54ccd Merge branch 'develop' into trunk 2025-04-28 09:27:33 +00:00
Vincent BRIDIER
1ea25ed3c7
Merge branch 'trunk' 2025-04-23 11:59:47 +02:00
Vincent BRIDIER
dd3a4b53c2
Merge branch 'develop' into trunk 2025-04-23 11:27:18 +02:00
Vincent BRIDIER
bddd622895
Merge branch 'develop' into trunk 2025-04-23 11:10:24 +02:00
Vincent BRIDIER
48c6b1feaf
Merge branch 'develop' into trunk 2025-04-23 11:09:04 +02:00
Vincent BRIDIER
8949a22b5f
Merge branch 'develop' into trunk 2025-04-23 10:58:00 +02:00
Vincent BRIDIER
a9f28657e5
Merge branch 'develop' into trunk 2025-04-23 10:52:48 +02:00
Vincent BRIDIER
ce931c6dcf Merge branch 'trunk' into 'master'
Trunk

See merge request ldc-web/projets/crm!27
2025-01-13 15:06:32 +00:00
Noah Lancien
5778c7aa02 Merge branch 'develop' into trunk 2025-01-13 12:41:45 +01:00
Thibault Raquet
f23aa94101 Merge branch 'develop' into 'trunk'
Develop

See merge request ldc-web/projets/crm!26
2025-01-13 10:58:16 +00:00
Vincent BRIDIER
44d60527b8
Merge branch 'trunk' 2024-10-18 14:53:27 +02:00
Vincent BRIDIER
cbbb389a47
Merge branch 'trunk' 2024-09-11 17:04:39 +02:00
Vincent BRIDIER
f56ef0999e
Merge branch 'trunk' 2024-09-09 15:46:33 +02:00
Noah Lancien
0fd22a9a82 Merge branch 'trunk' 2024-08-12 19:26:28 +02:00
Noah Lancien
05f0e2c229 Merge branch 'trunk' 2024-08-05 17:04:59 +02:00
Noah Lancien
cf2f6bbf76 Merge branch 'trunk' 2024-08-05 13:11:04 +02:00
Vincent BRIDIER
394d558cf1
Merge branch 'trunk' 2024-07-11 11:59:18 +02:00
Thibault Raquet
d60aa2c35c Merge branch 'trunk' into 'master'
Trunk

See merge request ldc-web/projets/crm!14
2024-06-28 14:32:41 +00:00
Vincent BRIDIER
fe254722b0
Merge branch 'trunk' 2024-06-28 16:09:01 +02:00
Vincent BRIDIER
3b11316017
Merge branch 'trunk' 2024-06-28 15:03:25 +02:00
Jérémy Munsch
6e548a7823
Merge branch 'trunk' 2023-07-06 11:21:30 +02:00
Vincent BRIDIER
1974969ccc
Merge branch 'trunk' 2023-07-05 17:05:03 +02:00
Jérémy Munsch
c5182b65b4
Merge branch 'hotfix' 2022-08-31 10:28:07 +02:00
734 changed files with 16938 additions and 103568 deletions

View file

@ -15,7 +15,7 @@ DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=apc
CACHE_DRIVER=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
@ -25,22 +25,27 @@ REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_HOST=dev-mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
MAIL_DIGITAL_SERVICE=""
# Sentry
SENTRY_LARAVEL_DSN=
# ADMIN SECURE
# - API ENDPOINT (Optional)
LDC_WEB_API_ENDPOINT="https://ldc-web.ldc.fr/api/ldc-networks"
# - API KEY
LDC_WEB_API_KEY=""
# UWIL ADMIN SECURE
# - LIST SEPARATED WITH ',' (Required)
UWIL_ADMIN_SECURE_CIDR_LIST=""
# - LIST SEPARATED WITH ',' (Optional)
LDC_WEB_CIDR_MANUAL_ALLOWED_LIST=""
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
# SAML
SAML2_DEFAULT_TENANT_UUID=
SAML2_SP_CERT_x509=
SAML2_SP_CERT_PRIVATEKEY=
SAML2_CONTACT_TECHNICAL_EMAIL=
SAML2_CONTACT_SUPPORT_EMAIL=

4
.gitignore vendored
View file

@ -25,3 +25,7 @@ npm-debug.log
yarn-error.log
.npmrc
.phpdoc
.zshrc
.npm/
.composer/
tests/coverage

3
.husky/commit-msg Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
npx --no -- commitlint --config node_modules/@uwil/husky-config/commitlint.config.cjs --edit $1

View file

@ -1,6 +1,5 @@
{
"phpcs.executablePath": "vendor/bin/phpcs",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#8dc952",
"activityBar.activeBorder": "#3d7fc2",
@ -22,5 +21,5 @@
"commandCenter.border": "#15202b99"
},
"peacock.remoteColor": "#73b137",
"intelephense.environment.phpVersion": "8.2"
"intelephense.environment.phpVersion": "8.5"
}

View file

@ -1,5 +1,9 @@
ARG PHP_8_5_TAG=latest
FROM composer:2.8 AS vendor
WORKDIR /app
ARG GITLAB_USER
ARG GITLAB_PASSWORD
@ -8,7 +12,7 @@ COPY database/ database/
COPY composer.json composer.json
COPY composer.lock composer.lock
RUN composer config gitlab-token.lab.s-web.fr ${GITLAB_USER} ${GITLAB_PASSWORD}
RUN composer config gitlab-token.lab.s-web.fr "${GITLAB_USER}" "${GITLAB_PASSWORD}"
RUN composer install \
--ignore-platform-reqs \
--no-interaction \
@ -28,9 +32,12 @@ COPY resources/ /app/resources/
WORKDIR /app
RUN npm config set @ldc-web:registry https://lab.s-web.fr/api/v4/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm ci && npm run prod
RUN npm config set @uwil:registry https://lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/
RUN npm config set -- '//lab.s-web.fr/api/v4/groups/ldc-web/-/packages/npm/:_authToken' "${GITLAB_PASSWORD}"
RUN npm ci --no-audit --no-fund --loglevel=error && npm run prod
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/ldcweb-production-8.3:latest
ARG PHP_8_5_TAG
FROM registry.s-web.fr/ldc-web/projets/ldc-web-images-docker/uwil-production-8.5:${PHP_8_5_TAG}
ARG USER=www-data

View file

@ -1,3 +1,10 @@
# Comment lancer les tests unitaires ?
# CRM
## Documentation
https://cdlfr.sharepoint.com/:b:/r/sites/LDCWEB/Documents%20partages/Documentations/Projets/CRM/FICHE%20FONCTIONNELLE%20OUTIL%20R%C3%89CLAMATIONS%20CONSO.pdf?csf=1&web=1&e=qktL4V
## Tests unitaire
### Comment lancer les tests unitaires ?
[Suivre le wiki dédié au tests unitaires](https://lab.s-web.fr/ldc-web/projets/crm/-/wikis/Tests-unitaires)

View file

@ -8,6 +8,7 @@ use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Str;
class UserCreate extends Command
{
@ -28,74 +29,61 @@ class UserCreate extends Command
/**
* The first name of the new user.
*
* @var string
* @var string|null
*/
protected $first_name;
protected $firstName;
/**
* The last name of the new user.
*
* @var string
* @var string|null
*/
protected $last_name;
protected $lastName;
/**
* The email of the new user.
*
* @var string
* @var string|null
*/
protected $email;
/**
* The password of the new user.
*
* @var string
*/
protected $password;
/**
* The role of the new user.
*
* @var string
* @var string|null
*/
protected $role;
/**
* Execute the console command.
*
* @return integer
* @return void
*/
public function handle(): int
public function handle(): void
{
$this->first_name = null;
$this->firstName = null;
$this->addFirstName();
$this->last_name = null;
$this->lastName = null;
$this->addLastName();
$this->email = null;
$this->addEmail();
$this->password = null;
$this->addPassword();
$this->role = null;
$this->addRole();
$user = new User([
'first_name' => $this->first_name,
'last_name' => $this->last_name,
'email' => $this->email,
'password' => $this->password,
'role' => $this->role,
'published' => true,
'first_name' => $this->firstName,
'last_name' => $this->lastName,
'email' => $this->email,
'role' => $this->role,
'published' => true,
'published_at' => Carbon::now(),
]);
$user->saveOrFail();
$this->info('User created 👌');
return 0;
}
/**
@ -105,22 +93,22 @@ class UserCreate extends Command
*/
private function addFirstName(): void
{
while (is_null($this->first_name)) {
while (is_null($this->firstName)) {
try {
$tmp = $this->ask('Type the wanted user first name', 'Admin');
$validator = Validator::make(['first_name' => $tmp], [
'first_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->first_name = $tmp;
$this->firstName = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid first name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
}
}
/**
@ -130,22 +118,22 @@ class UserCreate extends Command
*/
private function addLastName(): void
{
while (is_null($this->last_name)) {
while (is_null($this->lastName)) {
try {
$tmp = $this->ask('Type the wanted user last name', 'Saïtama');
$validator = Validator::make(['last_name' => $tmp], [
'last_name' => 'required|min:3|max:255'
]);
$validator->validate();
$this->last_name = $tmp;
$this->lastName = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please provide a valid last_name %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
}
}
/**
@ -169,24 +157,8 @@ class UserCreate extends Command
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
/**
* Add a password to the user.
*
* @return void
*/
private function addPassword(): void
{
while (
($this->password = $this->secret(
(!\is_null($this->password) ? 'Failed to confirm, ' : '') . 'Type the wanted user password'
)) !== $this->secret('Confirm the password')
) {
continue;
}; //end while
}
}
}
/**
@ -198,21 +170,23 @@ class UserCreate extends Command
{
while (is_null($this->role)) {
try {
$tmp = $this->choice(
$tmp = $this->choice(
'Select his role',
[RoleEnum::conceptor->name, RoleEnum::admin->name, RoleEnum::visitor->name],
RoleEnum::visitor->name,
$maxAttempts = null,
$allowMultipleSelections = false
[
Str::ucFirst(RoleEnum::conceptor->label()),
Str::ucFirst(RoleEnum::admin->label()),
Str::ucFirst(RoleEnum::marketing->label())
],
Str::ucFirst(RoleEnum::marketing->label()),
);
if ($tmp === RoleEnum::conceptor->name) {
if ($tmp === Str::ucFirst(RoleEnum::conceptor->label())) {
$tmp = RoleEnum::conceptor;
}
if ($tmp === RoleEnum::admin->name) {
if ($tmp === Str::ucFirst(RoleEnum::admin->label())) {
$tmp = RoleEnum::admin;
}
if ($tmp === RoleEnum::visitor->name) {
$tmp = RoleEnum::visitor;
if ($tmp === Str::ucFirst(RoleEnum::marketing->label())) {
$tmp = RoleEnum::marketing;
}
$validator = Validator::make(['role' => $tmp], [
'role' => 'required'
@ -221,11 +195,11 @@ class UserCreate extends Command
$this->role = $tmp;
} catch (ValidationException $e) {
$this->error(sprintf(
'Please choose from the selection',
'Please choose from the selection %s',
\implode(',', collect($e->errors())->flatten()->all())
));
continue;
} //end try
}; //end while
}
}
}
}

View file

@ -3,6 +3,7 @@
namespace App\Console;
use App\Console\Commands\DeleteUnconfirmedCustomers;
use App\Jobs\SendTransferredToDigitalCountMail;
use App\Jobs\StartStatJobs;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -26,6 +27,7 @@ class Kernel extends ConsoleKernel
{
// @codeCoverageIgnoreStart
$schedule->job(StartStatJobs::class)->dailyAt('03:30');
$schedule->job(SendTransferredToDigitalCountMail::class)->weekdays()->at('07:00');
$schedule->command(DeleteUnconfirmedCustomers::class)->dailyAt('05:30');
// @codeCoverageIgnoreEnd
}

View file

@ -2,7 +2,7 @@
namespace App\Enums\ActivityLogs;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ActivityLogsEventEnum: int
{

View file

@ -2,7 +2,7 @@
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ApiError: int
{

View file

@ -2,7 +2,7 @@
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ApiStatus: int
{

View file

@ -2,7 +2,7 @@
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ArcadiaMailType: int
{

View file

@ -2,7 +2,7 @@
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ArcadiaOrderType: int
{

View file

@ -0,0 +1,21 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimMessageType: int
{
use BaseEnum;
case system = 1;
case human = 2;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::system->name => 'Système',
self::human->name => 'Humain',
];
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimResponseType: int
{
use BaseEnum;
case none = 1;
case manual = 2;
case mail = 3;
case email = 4;
case phone = 5;
case other = 99;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::none->name => 'Non défini',
self::manual->name => 'Manuelle',
self::mail->name => 'Courrier',
self::email->name => 'E-mail',
self::phone->name => 'Téléphone',
self::other->name => 'Autre',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::none->name => '#b8b8b8',
self::manual->name => '#a7a7a7',
self::mail->name => '#757575',
self::email->name => '#5e5e5e',
self::phone->name => '#414141',
self::other->name => '#2b2b2b',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimStatus: int
{
use BaseEnum;
case untreated = 1;
case sentDigital = 2;
case sentMarketing = 3;
case treated = 4;
case refused = 5;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::untreated->name => 'Non traitée',
self::sentDigital->name => 'Transférée au service Digital',
self::sentMarketing->name => 'Retournée au Marketing',
self::treated->name => 'Traitée',
self::refused->name => 'Refusé de répondre',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::untreated->name => '#5a5a5a',
self::sentDigital->name => '#0033c0',
self::sentMarketing->name => '#69077c',
self::treated->name => '#188618',
self::refused->name => '#dc143c',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Enums\Claims;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ClaimType: int
{
use BaseEnum;
case classic = 1;
case br = 2;
case rgpd = 3;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::classic->name => 'Classique',
self::br->name => 'BR',
self::rgpd->name => 'RGPD',
];
/**
* Optionnal labels definition.
*/
private const COLORS = [
self::classic->name => '#08818a',
self::br->name => '#9021f8',
self::rgpd->name => '#c2237a',
];
/**
* Get Class.
*
* @return string
*/
public function color(): string
{
return self::COLORS[$this->name];
}
}

View file

@ -2,7 +2,7 @@
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum Country: int
{
@ -570,6 +570,7 @@ enum Country: int
Country::frenchPolynesia,
Country::newCaledonia,
Country::netherlands,
Country::germany,
];
}
@ -596,6 +597,7 @@ enum Country: int
'frenchPolynesia' => 'PF',
'newCaledonia' => 'NC',
'netherlands' => 'NL',
'germany' => 'DE',
];
}

View file

@ -2,7 +2,7 @@
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum CustomerExportType: int
{

View file

@ -0,0 +1,85 @@
<?php
namespace App\Enums;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ExportFormat: int
{
use BaseEnum;
case csv = 1;
case csvWithoutAttachment = 2;
case pdf = 3;
/**
* Optionnal labels definition
*/
private const LABELS = [
self::csv->name => 'csv',
self::csvWithoutAttachment->name => 'csv sans pièces jointes',
self::pdf->name => 'pdf',
];
/**
* Bootstrap color class to use for the export buttons.
*
* @return string
*/
public function class(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'success',
self::pdf => 'danger',
default => 'primary',
};
}
/**
* Font Awesome picto to use for the export buttons.
*
* @return string
*/
public function picto(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'fa-solid fa-file-csv',
self::pdf => 'fa-solid fa-file-pdf',
default => 'fa-solid fa-file',
};
}
/**
* File headers to use when the exported file is return with StreamedResponse.
*
* @return array
*/
public function fileHeaders(): array
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="export.csv"',
],
default => [],
};
}
/**
* Export file name.
*
* @return string
*/
public function fileName(): string
{
return match ($this) {
self::csv,
self::csvWithoutAttachment => 'export.csv',
self::pdf => 'export.pdf',
};
}
}

View file

@ -2,7 +2,7 @@
namespace App\Enums\Pagination;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum ItemsPerPaginationEnum: int
{

View file

@ -2,7 +2,7 @@
namespace App\Enums;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum QuestionType: int
{

View file

@ -2,7 +2,7 @@
namespace App\Enums\Theme;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum BootstrapThemeEnum: int
{

View file

@ -2,7 +2,7 @@
namespace App\Enums\Users;
use App\Traits\Enums\BaseEnum;
use Uwil\LaravelCommon\Traits\Enums\BaseEnum;
enum RoleEnum: int
{
@ -10,7 +10,7 @@ enum RoleEnum: int
case conceptor = 1;
case admin = 2;
case visitor = 3;
case marketing = 3;
/**
* Optionnal labels definition.
@ -18,6 +18,6 @@ enum RoleEnum: int
private const LABELS = [
self::conceptor->name => 'Concepteur',
self::admin->name => 'Administrateur',
self::visitor->name => 'Visiteur',
self::marketing->name => 'Marketing',
];
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Events;
use App\Models\Claim;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ClaimModelChanged
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
/** @var \App\Models\Claim $claim Current claim modified. */
public $claim;
/** @var string $field Modified field. */
public $field;
/** @var string $label Label of the modified field. */
public $label;
/** @var \App\Models\User|null $user Current authenticated user. */
public $user;
/** @var string|null $content Custom content message. */
public $content;
/**
* Create a new event instance.
*
* @param \App\Models\Claim $claim
* @param string $field
* @param string $label
* @param \App\Models\User|null $user
* @param string|null $content
* @return void
*/
public function __construct(
Claim $claim,
string $field,
string $label,
User|null $user = null,
string|null $content = null,
) {
$this->claim = $claim;
$this->field = $field;
$this->label = $label;
$this->user = $user;
$this->content = $content;
}
}

View file

@ -155,7 +155,7 @@ class Handler extends ExceptionHandler
// @codeCoverageIgnoreStart
if (config('app.env') === 'testing') {
// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
print_r((string)$e);
print_r((string) $e);
}
return $this->getReponse($request, 500, \intval($e->getCode()), $e->getMessage());
// @codeCoverageIgnoreEnd
@ -180,13 +180,13 @@ class Handler extends ExceptionHandler
): JsonResponse {
if (Helper::isRequestForApiV2($request)) {
return (new V2ApiResponse(
(object)array_merge($data, ['message' => $message, 'code' => $apiCode]),
ApiStatus::KO
ApiStatus::KO,
(object) array_merge($data, ['message' => $message, 'code' => $apiCode]),
))->toResponse($request)->setStatusCode($httpCode);
}
return (new V4ApiResponse(
(object)array_merge($data, ['message' => $message, 'code' => $apiCode]),
ApiStatus::KO
ApiStatus::KO,
(object) array_merge($data, ['message' => $message, 'code' => $apiCode]),
))->toResponse($request)->setStatusCode($httpCode);
}
}

View file

@ -5,8 +5,8 @@ namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V2\StoreCustomerAnswers;
use App\Models\Customer;
use App\Models\ModelAnswer;
use App\Models\Question;
use Illuminate\Http\JsonResponse;
class AnswerController extends Controller
{
@ -20,14 +20,26 @@ class AnswerController extends Controller
public function storeCustomerAnswers(
StoreCustomerAnswers $request,
Customer $customer
): JsonResponse {
): \Illuminate\Http\JsonResponse {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$question = Question::find($request->validated('question'));
$customer->answers()->detach($question->answers);
$customer->answers()->attach($request->validated('answers'));
// Column model_id is varchar because of singleoptin uuid, so force cast in string.
ModelAnswer::query()
->where('model_type', Customer::class)
->where('model_id', (string) $customer->getKey())
->whereIn('answer_id', $question->answers->modelKeys())
->delete();
foreach ($request->validated('answers') as $answerId) {
ModelAnswer::create([
'model_type' => Customer::class,
'model_id' => (string) $customer->getKey(),
'answer_id' => $answerId,
]);
}
return response()->json();
}

View file

@ -34,7 +34,7 @@ class CustomerController extends Controller
*/
public function store(StoreCustomerRequest $request, Service $service): JsonResource
{
$data = (object)$request->validated();
$data = (object) $request->validated();
/** @var \App\Models\Customer */
$customer = Customer::query()->where('email', $data->email)->firstOrCreate([

View file

@ -11,7 +11,6 @@ use App\Http\Requests\Api\V2\SingleOptinRequest;
use App\Http\Requests\Api\V2\SingleOptinRemoveWithHashcodeRequest;
use App\Models\CustomerInfo;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use App\Models\SingleOptin;
class OptinController extends Controller
@ -26,24 +25,31 @@ class OptinController extends Controller
* if this email already has a full account.
* @throws \App\Exceptions\Api\V2\CrmException If the single optin could not be added or removed.
*/
public function optin(SingleOptinRequest $request, Service $service): JsonResponse
public function optin(SingleOptinRequest $request, Service $service): \Illuminate\Http\JsonResponse
{
if (CustomerInfo::getFromEmail($service->brand, $request->email)->exists()) {
$validatedData = $request->validated();
if (CustomerInfo::getFromEmail($service->brand, $validatedData['email'])->exists()) {
throw new ApiErrorException(ApiError::singleOptinCustomerExists);
}
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()->where('email', $request->email)
$singleOptin = SingleOptin::query()
->where('email', $validatedData['email'])
->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id);
})->firstOrNew();
})
->firstOrNew();
if (!$this->changeOptin($service, $request, \boolval($request->opt_in), $singleOptin)) {
if (!$this->changeOptin($service, $request, \boolval($validatedData['opt_in']), $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
if (isset($validatedData['answer_id'])) {
$singleOptin->answers()->attach($validatedData['answer_id']);
}
return response()->json();
}
@ -55,13 +61,17 @@ class OptinController extends Controller
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V2\CrmException If the single optin could not be removed.
*/
public function optoutWithHashcode(SingleOptinRemoveWithHashcodeRequest $request, Service $service): JsonResponse
{
public function optoutWithHashcode(
SingleOptinRemoveWithHashcodeRequest $request,
Service $service
): \Illuminate\Http\JsonResponse {
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()->where('hashcode', $request->hashcode)
$singleOptin = SingleOptin::query()
->where('hashcode', $request->hashcode)
->whereHas('brand', function ($q) use ($service) {
$q->whereId($service->brand->id);
})->firstOrFail();
})
->firstOrFail();
if ($singleOptin and !$this->changeOptin($service, $request, \boolval($request->opt_in), $singleOptin)) {
// @codeCoverageIgnoreStart

View file

@ -8,10 +8,9 @@ use App\Http\Requests\Api\V2\GetQuestionStatsRequest;
use App\Http\Resources\Api\V2\BrandStatsResource;
use App\Http\Resources\Api\V2\QuestionStatsResource;
use App\Models\BrandStat;
use App\Models\CustomerAnswer;
use App\Models\ModelAnswer;
use App\Models\Question;
use App\Models\Service;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
@ -24,8 +23,10 @@ class StatsController extends Controller
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function brandStats(GetBrandStatsRequest $request, Service $service): ResourceCollection
{
public function brandStats(
GetBrandStatsRequest $request,
Service $service
): \Illuminate\Http\Resources\Json\ResourceCollection {
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end)
->whereBrandId($service->brand->id)->get();
@ -39,8 +40,10 @@ class StatsController extends Controller
* @param \App\Models\Question $question
* @return \App\Http\Resources\Api\V2\QuestionStatsResource
*/
public function questionStats(GetQuestionStatsRequest $request, Question $question): QuestionStatsResource
{
public function questionStats(
GetQuestionStatsRequest $request,
Question $question
): \App\Http\Resources\Api\V2\QuestionStatsResource {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('questionBelongsToService', $question);
@ -62,18 +65,22 @@ class StatsController extends Controller
* @param \Illuminate\Support\Carbon $end
* @return \Illuminate\Support\Collection
*/
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): Collection
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection
{
// * Totalize by date all answers.
$answersByDays = CustomerAnswer::query()->whereDate('created_at', '>=', $start)
$answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id'))
->get()->map(function ($custAnswer) {
->get()
->map(function ($custAnswer) {
return [
'answer_id' => $custAnswer->answer_id,
'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->isoFormat('YYYY-MM-DD'),
];
})->groupBy(['created_at', 'answer_id'])->map(function ($day) {
})
->groupBy(['created_at', 'answer_id'])
->map(function ($day) {
return $day->map(function ($custAnswers) {
return $custAnswers->count();
});

View file

@ -5,8 +5,8 @@ namespace App\Http\Controllers\Api\V4;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\StoreCustomerAnswers;
use App\Models\Customer;
use App\Models\ModelAnswer;
use App\Models\Question;
use Illuminate\Http\JsonResponse;
class AnswerController extends Controller
{
@ -20,14 +20,26 @@ class AnswerController extends Controller
public function storeCustomerAnswers(
StoreCustomerAnswers $request,
Customer $customer
): JsonResponse {
): \Illuminate\Http\JsonResponse {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$question = Question::find($request->validated('question'));
$customer->answers()->detach($question->answers);
$customer->answers()->attach($request->validated('answers'));
// Column model_id is varchar because of singleoptin uuid, so force cast in string.
ModelAnswer::query()
->where('model_type', Customer::class)
->where('model_id', (string) $customer->getKey())
->whereIn('answer_id', $question->answers->modelKeys())
->delete();
foreach ($request->validated('answers') as $answerId) {
ModelAnswer::create([
'model_type' => Customer::class,
'model_id' => (string) $customer->getKey(),
'answer_id' => $answerId,
]);
}
return response()->json();
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api\V4;
use App\Exceptions\Api\V4\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\SendClaimRequest;
use App\Models\Claim;
use App\Models\CustomerInfo;
use App\Models\Service;
use Illuminate\Support\Arr;
use Uwil\LaravelCommon\Helpers\FileHelper;
class ClaimController extends Controller
{
/**
* Just returns HTTP 200 response.
*
* @param \App\Http\Requests\Api\V4\SendClaimRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be added or removed.
*/
public function store(SendClaimRequest $request, Service $service): \Illuminate\Http\JsonResponse
{
$claimModel = (new Claim())->fill(Arr::except($request->validated(), 'file'));
if ($request->filled('file')) {
$claimModel->file = FileHelper::storeBase64File(
tableName: 'claim',
fileBase64: $request->file,
private: true,
);
}
// Add brand id from service.
$claimModel->brand_id = $service->brand->getKey();
// Add customer relation if exist.
$crmCustomer = CustomerInfo::getFromEmail($claimModel->brand, $claimModel->email)->first();
if (!is_null($crmCustomer)) {
$claimModel->hashcode = $crmCustomer->hashcode;
}
if (!$claimModel->saveOrFail()) {
throw new CrmException('Api failed to store claim.');
}
$claimModel->brand()->associate($service->brand);
return response()->json();
}
}

View file

@ -34,7 +34,7 @@ class CustomerController extends Controller
*/
public function store(StoreCustomerRequest $request, Service $service): JsonResource
{
$data = (object)$request->validated();
$data = (object) $request->validated();
/** @var \App\Models\Customer */
$customer = Customer::query()->where('email', $data->email)->firstOrCreate([

View file

@ -2,8 +2,6 @@
namespace App\Http\Controllers\Api\V4;
use App\Enums\ApiError;
use App\Exceptions\Api\V4\ApiErrorException;
use App\Exceptions\Api\V4\CrmException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V4\BaseApiRequest;
@ -11,7 +9,6 @@ use App\Http\Requests\Api\V4\SingleOptinRequest;
use App\Http\Requests\Api\V4\SingleOptinRemoveWithHashcodeRequest;
use App\Models\CustomerInfo;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use App\Models\SingleOptin;
class OptinController extends Controller
@ -22,27 +19,49 @@ class OptinController extends Controller
* @param \App\Http\Requests\Api\V4\SingleOptinRequest $request
* @param \App\Models\Service $service
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\ApiErrorException With ApiError::singleOptinCustomerExists
* if this email already has a full account.
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be added or removed.
*/
public function optin(SingleOptinRequest $request, Service $service): JsonResponse
public function optin(SingleOptinRequest $request, Service $service): \Illuminate\Http\JsonResponse
{
if (CustomerInfo::getFromEmail($service->brand, $request->email)->exists()) {
throw new ApiErrorException(ApiError::singleOptinCustomerExists);
}
$validatedData = $request->validated();
$crmCustomer = CustomerInfo::getFromEmail($service->brand, $validatedData['email'])->first();
if ($crmCustomer) {
// Update the optin field.
$crmCustomer->opt_in = \boolval($validatedData['opt_in']);
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()->where('email', $request->email)
->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id);
})->firstOrNew();
// Check the saving.
if (!$crmCustomer->saveOrFail()) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer optin.');
// @codeCoverageIgnoreEnd
}
if (!$this->changeOptin($service, $request, \boolval($request->opt_in), $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
// Associate answers if presents.
if (isset($validatedData['answer_id'])) {
$crmCustomer->answers()->attach($validatedData['answer_id']);
}
} else {
// Get the single optin if exist or create it.
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()
->where('email', $validatedData['email'])
->whereHas('brand', function ($q) use ($service) {
$q->whereKey($service->brand->getKey());
})
->firstOrNew();
// Update the optin field and check the saving.
if (!$this->changeOptin($service, $request, \boolval($validatedData['opt_in']), $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
// Associate answers if presents.
if (isset($validatedData['answer_id'])) {
$singleOptin->answers()->attach($validatedData['answer_id']);
}
} //end if
return response()->json();
}
@ -55,19 +74,37 @@ class OptinController extends Controller
* @return \Illuminate\Http\JsonResponse
* @throws \App\Exceptions\Api\V4\CrmException If the single optin could not be removed.
*/
public function optoutWithHashcode(SingleOptinRemoveWithHashcodeRequest $request, Service $service): JsonResponse
{
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()->where('hashcode', $request->hashcode)
->whereHas('brand', function ($q) use ($service) {
$q->whereId($service->brand->id);
})->firstOrFail();
public function optoutWithHashcode(
SingleOptinRemoveWithHashcodeRequest $request,
Service $service
): \Illuminate\Http\JsonResponse {
$validatedData = $request->validated();
$crmCustomer = CustomerInfo::getFromHashcode($service->brand, $validatedData['hashcode'])->first();
if ($crmCustomer) {
// Update the optin field.
$crmCustomer->opt_in = false;
if ($singleOptin and !$this->changeOptin($service, $request, \boolval($request->opt_in), $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
// Check the saving.
if (!$crmCustomer->saveOrFail()) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update customer optin.');
// @codeCoverageIgnoreEnd
}
} else {
// Get the single optin if exist or create it.
/** @var \App\Models\SingleOptin */
$singleOptin = SingleOptin::query()->where('hashcode', $validatedData['hashcode'])
->whereHas('brand', function ($q) use ($service) {
$q->whereId($service->brand->getKey());
})->firstOrFail();
// Update the optin field and check the saving.
if ($singleOptin and !$this->changeOptin($service, $request, false, $singleOptin)) {
// @codeCoverageIgnoreStart
throw new CrmException('Api failed to update single optin.');
// @codeCoverageIgnoreEnd
}
} //end if
return response()->json();
}

View file

@ -8,8 +8,6 @@ use App\Models\Answer;
use App\Models\Customer;
use App\Models\Question;
use App\Models\Service;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
class QuestionController extends Controller
{
@ -19,7 +17,7 @@ class QuestionController extends Controller
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function index(Service $service): ResourceCollection
public function index(Service $service): \Illuminate\Http\Resources\Json\ResourceCollection
{
return new QuestionsResource($service->brand->questions);
}
@ -31,8 +29,10 @@ class QuestionController extends Controller
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function indexWithResponse(Service $service, Customer $customer): ResourceCollection
{
public function indexWithResponse(
Service $service,
Customer $customer
): \Illuminate\Http\Resources\Json\ResourceCollection {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
@ -47,18 +47,22 @@ class QuestionController extends Controller
* @param \App\Models\Customer $customer
* @return \Illuminate\Http\Resources\Json\JsonResource
*/
public function unAnsweredBy(Service $service, Customer $customer): JsonResource
public function unAnsweredBy(Service $service, Customer $customer): \Illuminate\Http\Resources\Json\JsonResource
{
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('customerBelongsToService', $customer);
$questions = Question::query()->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id);
})->whereDoesntHave('answers', function ($q) use ($customer) {
$q->whereIn('id', Answer::query()->whereHas('customers', function ($q) use ($customer) {
$q->where('id', $customer->id);
})->pluck('id'));
})->with('answers')->get();
$questions = Question::query()
->whereHas('brand', function ($q) use ($service) {
$q->where('id', $service->brand->id);
})
->whereDoesntHave('answers', function ($q) use ($customer) {
$q->whereIn('id', Answer::query()->whereHas('customers', function ($q) use ($customer) {
$q->where('id', $customer->id);
})->pluck('id'));
})
->with('answers')
->get();
return new QuestionsResource($questions);
}
}

View file

@ -8,10 +8,9 @@ use App\Http\Requests\Api\V4\GetQuestionStatsRequest;
use App\Http\Resources\Api\V4\BrandStatsResource;
use App\Http\Resources\Api\V4\QuestionStatsResource;
use App\Models\BrandStat;
use App\Models\CustomerAnswer;
use App\Models\ModelAnswer;
use App\Models\Question;
use App\Models\Service;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
@ -24,8 +23,10 @@ class StatsController extends Controller
* @param \App\Models\Service $service
* @return \Illuminate\Http\Resources\Json\ResourceCollection
*/
public function brandStats(GetBrandStatsRequest $request, Service $service): ResourceCollection
{
public function brandStats(
GetBrandStatsRequest $request,
Service $service
): \Illuminate\Http\Resources\Json\ResourceCollection {
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
->whereDate('created_at', '<=', $request->end)
->whereBrandId($service->brand->id)->get();
@ -39,8 +40,10 @@ class StatsController extends Controller
* @param \App\Models\Question $question
* @return \App\Http\Resources\Api\V4\QuestionStatsResource
*/
public function questionStats(GetQuestionStatsRequest $request, Question $question): QuestionStatsResource
{
public function questionStats(
GetQuestionStatsRequest $request,
Question $question
): \App\Http\Resources\Api\V4\QuestionStatsResource {
// * Make sure we are accessing a customer that belongs to our service.
$this->authorize('questionBelongsToService', $question);
@ -62,18 +65,22 @@ class StatsController extends Controller
* @param \Illuminate\Support\Carbon $end
* @return \Illuminate\Support\Collection
*/
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): Collection
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection
{
// * Totalize by date all answers.
$answersByDays = CustomerAnswer::query()->whereDate('created_at', '>=', $start)
$answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id'))
->get()->map(function ($custAnswer) {
->get()
->map(function ($custAnswer) {
return [
'answer_id' => $custAnswer->answer_id,
'created_at' => $custAnswer->created_at->isoFormat('YYYY-MM-DD'),
];
})->groupBy(['created_at', 'answer_id'])->map(function ($day) {
})
->groupBy(['created_at', 'answer_id'])
->map(function ($day) {
return $day->map(function ($custAnswers) {
return $custAnswers->count();
});

View file

@ -13,6 +13,14 @@ class ActivityLogsController extends Controller
use ChangesModelOrder;
use UpdateModelPublished;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ActivityLog::class);
}
/**
* Display a listing of the resource.
*
@ -43,7 +51,7 @@ class ActivityLogsController extends Controller
$this->sortQuery($query);
/** Custom pagination */
$activitylogModels = $this->paginate($query);
$activitylogModels = $query->paginate($this->getPagination());
return view('bo.pages.activity_logs.index', compact('activitylogModels', 'search', 'searchFields'));
}

View file

@ -13,6 +13,14 @@ use Illuminate\Support\Str;
class AnswerController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Answer::class);
}
/**
* Display a listing of the resource.
*
@ -24,22 +32,22 @@ class AnswerController extends Controller
/** @var \App\Models\Question $questionModel */
$questionModel = $request->question;
/** @var \Illuminate\Database\Eloquent\Builder $answers */
$answers = Answer::query()->where('question_id', $questionModel->id);
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Answer::query()->where('question_id', $questionModel->id);
if ($request->search) {
$this->searchQuery(
$answers,
$query,
$request->search,
null,
'answer',
);
}
$this->sortQuery($answers);
$this->sortQuery($query);
$searchFields = trans('Réponse');
$answerModels = $this->paginate($answers);
$answerModels = $query->paginate($this->getPagination());
return view('bo.pages.answers.index', compact('answerModels', 'questionModel', 'searchFields'));
}
@ -57,8 +65,8 @@ class AnswerController extends Controller
$questionModel = $request->question;
return view('bo.pages.answers.create', [
'answerModel' => $answer,
'questionModel' => $questionModel
'answerModel' => $answer,
'questionModel' => $questionModel,
]);
}
@ -73,9 +81,11 @@ class AnswerController extends Controller
return DB::transaction(function () use ($request) {
$answer = new Answer();
$answer->fill($request->validated());
$questionModel = $request->route()->parameter('question');
$answer->question()->associate($questionModel->getKey());
if ($answer->saveOrFail()) {
return redirect()
->route('bo.answers.edit', $answer)
->route('bo.questions.answers.edit', ['question' => $questionModel, 'answer' => $answer])
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.answers'))]
@ -100,8 +110,8 @@ class AnswerController extends Controller
public function edit(Question $question, Answer $answers): \Illuminate\Contracts\View\View
{
return view('bo.pages.answers.edit', [
'answerModel' => $answers,
'questionModel' => $question
'answerModel' => $answers,
'questionModel' => $question,
]);
}
@ -146,6 +156,7 @@ class AnswerController extends Controller
*/
public function destroy(Question $question, Answer $answer): \Illuminate\Http\RedirectResponse
{
$answer->singleOptins()->detach();
if ($answer->deleteOrFail()) {
return redirect()->route('bo.questions.answers.index', $question)
->with('success', trans(

View file

@ -5,14 +5,24 @@ namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Brands\StoreBrandRequest;
use App\Http\Requests\Bo\Brands\UpdateBrandRequest;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Models\Brand;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class BrandController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Brand::class);
}
/**
* Display a listing of the resource.
*
@ -40,7 +50,7 @@ class BrandController extends Controller
$q->where('opt_in', true);
}]);
$brandModels = $this->paginate($query);
$brandModels = $query->paginate($this->getPagination());
return view('bo.pages.brands.index', compact('brandModels', 'searchFields'));
}
@ -128,16 +138,18 @@ class BrandController extends Controller
}
/**
* Store a newly created resource in storage.
* Get a paginate resource.
*
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function jsonPaginate(): \Illuminate\Http\JsonResponse
public function jsonPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () {
/** @var \App\Models\Brand */
$brandModels = Brand::paginate(12);
return \response()->json($brandModels ?? 500);
});
$models = CollectionHelper::paginateManually(
Brand::getUserAssociatedBrands(),
$this->jsonItemPerPagination,
(int) $request->validated('page'),
);
return \response()->json($models);
}
}

View file

@ -0,0 +1,298 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Claims\ClaimResponseType;
use App\Enums\Claims\ClaimStatus;
use App\Enums\ExportFormat;
use App\Enums\Users\RoleEnum;
use App\Events\ClaimModelChanged;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Claims\ExportClaimRequest;
use App\Http\Requests\Bo\Claims\SendClaimAnswerRequest;
use App\Http\Requests\Bo\Claims\UpdateClaimRequest;
use App\Http\Requests\Bo\Claims\ViewClaimAttachmentRequest;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Lib\Helpers\FileHelper;
use App\Mail\ClaimAnswerMail;
use App\Models\Brand;
use App\Models\Claim;
use App\Models\ClaimMailTemplate;
use App\Models\ClaimMessage;
use App\Models\ClaimSubject;
use App\Traits\Controllers\ExportDatas;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimController extends Controller
{
use UpdateModelPublished;
use ExportDatas;
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Claim::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Claim::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
)->when(
$userModel->role->equals(RoleEnum::marketing),
fn($q) => $q->whereHas('brand', fn($q) => $q->whereIn('id', $userModel->brands->modelKeys()))
)
->when(
$request->filled('subject'),
fn($q) => $q->whereHas('claimSubject', fn($q) => $q->where('id', $request->subject))
)
->when(
$request->filled('status'),
fn($q) => $q->whereIn('status', $request->status),
fn($q) => $q->whereNotIn('status', [ClaimStatus::treated, ClaimStatus::refused])
);
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$query,
$search,
null,
'reference',
'first_name',
'last_name',
'email',
);
}
$searchFields = \implode(', ', [
trans('validation.custom.reference'),
trans('validation.attributes.first_name'),
trans('validation.attributes.last_name'),
trans('validation.attributes.email'),
]);
/** Sort columns with a query */
$this->sortQuery($query);
/** Custom pagination */
$claimModels = $query->simplePaginate($this->getPagination());
/** Define request brand filter if user only has 1 brand */
if (auth('backend')->user()->brands()->count() === 1) {
$request->merge(['brand' => auth('backend')->user()->brands()->value('id')]);
};
$claimSubjectModels = ClaimSubject::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand)),
)
->when(
auth('backend')->user()->brands()->count() === 1,
fn($q) => $q->whereHas(
'brand',
fn($q) => $q->where('id', auth('backend')->user()->brands()->pluck('id'))
)
);
$claimSubjectModels = $request->filled('brand') || (auth('backend')->user()->brands()->count() === 1) ?
$claimSubjectModels->orderBy('label', 'ASC')->get() :
null;
return view('bo.pages.claims.index', compact('claimModels', 'search', 'searchFields', 'claimSubjectModels'));
}
/**
* Show the form for editing the specified resource.
*
* @param App\Models\Claim $claim
* @return \Illuminate\Contracts\View\View
*/
public function edit(Claim $claim): \Illuminate\Contracts\View\View
{
$customerClaimModels = !is_null($claim->customerInfos) ? Claim::query()
->where('hashcode', $claim->customerInfos->hashcode)
->whereHas('brand', fn($q) => $q->whereKey($claim->brand->getKey()))
->whereNot('id', $claim->getKey())
->paginate($this->jsonItemPerPagination) : collect();
$brandModels = CollectionHelper::paginate(
Brand::getUserAssociatedBrands(),
$this->jsonItemPerPagination,
);
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
$claimSubjectModels = CollectionHelper::paginate(
ClaimSubject::getAllModelsFromBrand($claim->brand->getKey()),
$this->jsonItemPerPagination,
);
$claimTemplateModels = CollectionHelper::paginate(
ClaimMailTemplate::getAllModelsFromBrand($claim->brand->getKey()),
$this->jsonItemPerPagination,
);
return view('bo.pages.claims.edit', [
'claimModel' => $claim->load(['brand', 'claimSubject']),
'customerClaimModels' => $customerClaimModels,
'brandModels' => $brandModels,
'claimMessageModels' => $claimMessageModels,
'claimSubjectModels' => $claimSubjectModels,
'claimTemplateModels' => $claimTemplateModels,
'customerInfoModel' => $claim->customerInfos,
]);
}
/**
* Update the specified resource.
*
* @param \App\Http\Requests\Bo\Claims\UpdateClaimRequest $request
* @param \App\Models\Claim $claim
* @return \Illuminate\Http\JsonResponse
*/
public function update(UpdateClaimRequest $request, Claim $claim): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request, $claim) {
$claim->fill($request->validated());
if ($claim->saveOrFail()) {
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Send the claim answer.
*
* @param \App\Http\Requests\Bo\Claims\SendClaimAnswerRequest $request
* @param \App\Models\Claim $claim
* @return \Illuminate\Http\JsonResponse
*/
public function answer(SendClaimAnswerRequest $request, Claim $claim): \Illuminate\Http\JsonResponse
{
return DB::transaction(function () use ($request, $claim) {
/** @var \App\Models\User $userModel */
$userModel = auth()->user();
$enum = ClaimResponseType::manual;
$claim->response_type = $enum;
$claim->status = ClaimStatus::treated;
if ($claim->saveOrFail()) {
// Get only the content of the response.
$content = $request->validated()['content'];
// Send the response to the claim author.
Mail::to($claim->email)->send(new ClaimAnswerMail($claim, $content));
// Create custom event with content message.
// @phpcs:disable Generic.Files.LineLength.TooLong
$content = sprintf(
"<b>%s %s</b> %s <i class='badge' style='background-color:{$enum->color()}'>%s</i><hr class='my-2'>%s",
$userModel->first_name,
$userModel->last_name,
trans('a modifié :field en', ['field' => trans('validation.custom.response_type')]),
$claim->response_type->label(),
clean($content),
);
event(new ClaimModelChanged($claim, '', '', null, $content));
// @phpcs:enable
// Return conversation.
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Undocumented function
*
* @param ViewClaimAttachmentRequest $request
* @param Claim $claim
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
*/
public function viewAttachment(
ViewClaimAttachmentRequest $request,
Claim $claim
): \Symfony\Component\HttpFoundation\BinaryFileResponse {
// phpcs:enable
return response()->file(Storage::disk('private')->path(FileHelper::getPrivateFilePath($claim->file)));
}
/**
* Get a paginate resource.
* Needed to overheight the controller method and add a brand condition.
*
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function jsonClaimPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{
$data = $request->validated();
$brandModelKey = $request->route()->parameter('brand')->getKey();
$models = CollectionHelper::paginateManually(
$data['targetModel']::getAllModelsFromBrand($brandModelKey),
$this->jsonItemPerPagination,
$request->validated('page'),
);
return \response()->json($models);
}
// * EXPORT
/**
* Export claim datas to a given format
*
* @param ExportClaimRequest $request
* @return \Illuminate\Http\Response|Symfony\Component\HttpFoundation\StreamedResponse|\Illuminate\Http\RedirectResponse
*/
public function export(ExportClaimRequest $request)
{
/** @var \Illuminate\Database\Eloquent\Collection<int, \App\Models\Claim> $claims*/
$claims = Claim::query()->whereIn('id', $request->validated('claimList'))->get();
$format = ExportFormat::from($request->validated('format'));
// * CSV
if ($format->equals(ExportFormat::csvWithoutAttachment, ExportFormat::csv)) {
$csv = $this->generateCsv($claims);
if (!$this->needsZip($format, $claims)) {
return new StreamedResponse(function () use ($format, $csv) {
echo $csv;
}, 200, $this->getFileHeaders($format));
}
return $this->returnExportZip($format, $claims, $csv);
// * PDF
} elseif ($format->equals(ExportFormat::pdf)) {
$pdf = $this->generatePdf($claims);
if (!$this->needsZip(ExportFormat::pdf, $claims)) {
return $pdf->download('export.pdf');
}
return $this->returnExportZip($format, $claims, $pdf->output());
}
}
}

View file

@ -0,0 +1,183 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Enums\Users\RoleEnum;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimMailTemplates\ClaimMailTemplateRequest;
use App\Models\Brand;
use App\Models\ClaimMailTemplate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimMailTemplateController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ClaimMailTemplate::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ClaimMailTemplate::query()
->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
)->when(
$userModel->role->equals(RoleEnum::marketing),
fn($q) => $q->whereHas('brand', fn($q) => $q->whereIn('id', $userModel->brands->modelKeys()))
);
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'label',
);
}
$this->sortQuery($query);
$searchFields = trans('validation.custom.label');
$claimMailTemplateModels = $query->paginate($this->getPagination());
return view('bo.pages.claim_mail_templates.index', compact('claimMailTemplateModels', 'searchFields'));
}
/**
* Show the specified resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function show(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
return view('bo.pages.claim_mail_templates.show', [
'claimMailTemplateModel' => $claim_mail_template,
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function create(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_mail_templates.create', [
'claimMailTemplateModel' => $claim_mail_template,
'brandModels' => $brandModels,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimMailTemplates\ClaimMailTemplateRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(ClaimMailTemplateRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$claimMailTemplateModel = new ClaimMailTemplate();
$claimMailTemplateModel->fill($request->validated());
if ($claimMailTemplateModel->saveOrFail()) {
return redirect()
->route('bo.claim_mail_templates.edit', $claimMailTemplateModel)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Contracts\View\View
*/
public function edit(ClaimMailTemplate $claim_mail_template): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_mail_templates.edit', [
'claimMailTemplateModel' => $claim_mail_template,
'brandModels' => $brandModels,
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimMailTemplateRequest $request
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Http\RedirectResponse
*/
public function update(
ClaimMailTemplateRequest $request,
ClaimMailTemplate $claim_mail_template
): \Illuminate\Http\RedirectResponse {
return DB::transaction(function () use ($request, $claim_mail_template) {
$claim_mail_template->fill($request->validated());
if ($claim_mail_template->saveOrFail()) {
return redirect()
->route('bo.claim_mail_templates.edit', $claim_mail_template)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.claimmailtemplates'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\ClaimMailTemplate $claim_mail_template
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(ClaimMailTemplate $claim_mail_template): \Illuminate\Http\RedirectResponse
{
if ($claim_mail_template->deleteOrFail()) {
return redirect()->route('bo.claim_mail_templates.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::of(__('models.classes.claimmailtemplates'))->singular()
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::of(__('models.classes.claimmailtemplates'))->singular()
]));
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimMessages\StoreMessageRequest;
use App\Http\Requests\Bo\ClaimMessages\UpdateMessageRequest;
use App\Models\ClaimMessage;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Support\Facades\DB;
class ClaimMessageController extends Controller
{
use UpdateModelPublished;
/**
* Store a new specific resource.
*
* @param \App\Http\Requests\Bo\ClaimMessages\StoreMessageRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function store(StoreMessageRequest $request): \Illuminate\Http\JsonResponse
{
$this->authorize('store', ClaimMessage::class);
$validatedData = $request->validated();
return DB::transaction(function () use ($validatedData) {
$claimMessage = (new ClaimMessage())->fill($validatedData);
if ($claimMessage->saveOrFail()) {
$claimMessageModels = ClaimMessage::getAllModelsFromClaim($claimMessage->claim->getKey());
return response()->json($claimMessageModels);
}
return response()->json([], 500);
});
}
/**
* Update a specified resource.
*
* @param \App\Http\Requests\Bo\ClaimMessages\UpdateMessageRequest $request
* @param \App\Models\ClaimMessage $claim_message
* @return \Illuminate\Http\JsonResponse
*/
public function update(UpdateMessageRequest $request, ClaimMessage $claim_message): \Illuminate\Http\JsonResponse
{
$this->authorize('update', ClaimMessage::class);
$claim_message->content = $request->validated()['content'];
$claim_message->saveOrFail();
return response()->json($claim_message);
}
}

View file

@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest;
use App\Models\Brand;
use App\Models\ClaimSubject;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ClaimSubjectController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(ClaimSubject::class);
}
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = ClaimSubject::query()->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'label',
);
}
$this->sortQuery($query);
$searchFields = trans('validation.custom.label');
$claimSubjectModels = $query->paginate($this->getPagination());
return view('bo.pages.claim_subjects.index', compact('claimSubjectModels', 'searchFields'));
}
/**
* Show the specified resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function show(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
return view('bo.pages.claim_subjects.show', [
'claimSubjectModel' => $claim_subject
]);
}
/**
* Show the form for creating a new resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function create(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_subjects.create', [
'claimSubjectModel' => $claim_subject,
'brandModels' => $brandModels,
]);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(ClaimSubjectRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$claimSubjectModel = new ClaimSubject();
$claimSubjectModel->fill($request->validated());
if ($claimSubjectModel->saveOrFail()) {
return redirect()
->route('bo.claim_subjects.edit', $claimSubjectModel)
->with('success', trans(
':model a été créé.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Contracts\View\View
*/
public function edit(ClaimSubject $claim_subject): \Illuminate\Contracts\View\View
{
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.claim_subjects.edit', [
'claimSubjectModel' => $claim_subject,
'brandModels' => $brandModels,
]);
}
/**
* Update a spacific resource in storage.
*
* @param \App\Http\Requests\Bo\ClaimSubjects\ClaimSubjectRequest $request
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Http\RedirectResponse
*/
public function update(ClaimSubjectRequest $request, ClaimSubject $claim_subject): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $claim_subject) {
$claim_subject->fill($request->validated());
if ($claim_subject->saveOrFail()) {
return redirect()
->route('bo.claim_subjects.edit', $claim_subject)
->with('success', trans(
':model a été mis à jour.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour.',
['model' => Str::singular(trans('models.classes.claimsubjects'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\ClaimSubject $claim_subject
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(ClaimSubject $claim_subject): \Illuminate\Http\RedirectResponse
{
if ($claim_subject->deleteOrFail()) {
return redirect()->route('bo.claim_subjects.index')
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::of(__('models.classes.claimsubjects'))->singular()
]));
}
return redirect()->back()
->with('error', __('crud.messages.cannot_be_deleted', [
'model' => Str::of(__('models.classes.claimsubjects'))->singular()
]));
}
}

View file

@ -5,15 +5,18 @@ namespace App\Http\Controllers\Bo;
use App\Enums\CustomerExportType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\CustomerExport;
use App\Models\Answer;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\CustomerInfo;
use App\Models\CustomerLogin;
use App\Models\Question;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
@ -23,6 +26,14 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
class CustomerController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Customer::class);
}
/**
* View all customers.
*
@ -31,90 +42,41 @@ class CustomerController extends Controller
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
$modelTable = (new Customer())->getTable();
$modelClass = Customer::class;
$brands = Brand::query()->select('id', 'name')->get();
$columns = ['customer_infos.lastname', 'customer_infos.firstname'];
$modelClass = Customer::class;
$modelTable = (new Customer())->getTable();
$questionModels = null;
$answerModels = null;
$searchFields = trans('Email, Prénom, Nom');
// * Jointure pour pouvoir trier les colonnes.
/** @var \Illuminate\Database\Eloquent\Builder */
$query = CustomerInfo::query()->join('customer_logins', function (JoinClause $join) {
$join->on('customer_logins.brand_id', '=', 'customer_infos.brand_id')
->on('customer_logins.customer_id', '=', 'customer_infos.customer_id');
})->join('customers', function (JoinClause $join) {
$join->on('customers.id', '=', 'customer_infos.customer_id');
})->join('brands', function (JoinClause $join) {
$join->on('brands.id', '=', 'customer_infos.brand_id');
});
if ($request->rst) {
Session::remove("crud.$modelTable.brand");
Session::remove("crud.$modelTable.search");
Session::remove("crud.$modelTable.sort_way");
Session::remove("crud.$modelTable.sort_col");
// Reset session sorts.
if ($request->rstSrts) {
$this->resetSessionSorts($modelTable);
}
// Reset session filters.
if ($request->rstFltrs) {
$this->resetSessionFilters($modelTable);
}
if (request()->brand or Session::has("crud.$modelTable.brand")) {
$brand = $request->brand ?? Session::get("crud.$modelTable.brand");
Session::put("crud.$modelTable.brand", $brand);
$query = $query->where('customer_infos.brand_id', $brand);
}
if ($request->filled('search') || Session::get("crud.$modelTable.search")) {
$columns = ['customer_infos.lastname', 'customer_infos.firstname'];
$query = $this->buildCustomerQuery();
// * Search on crud Label column.
if ($request->search or Session::has("crud.$modelTable.search")) {
$search = $request->search ?? Session::get("crud.$modelTable.search");
Session::put("crud.$modelTable.search", $search);
$query->where(function ($q) use ($columns, $search) {
foreach ($columns as $column) {
$q = $q->orWhere($column, 'LIKE', "%$search%");
}
$q = $q->orWhere('customers.email', 'LIKE', "%$search%");
$q = $q->orWhere('customer_logins.last_connection', 'LIKE', "%$search%");
});
}
// Apply filters.
$this->applyBrandFilter($request, $query, $modelTable, $questionModels, $answerModels);
$this->applySearchFilter($request, $query, $modelTable, $columns);
$this->applySort($request, $query, $modelTable);
// * Sort columns.
if ($request->sort_col) {
Session::put("crud.$modelTable.sort_col", $request->sort_col);
}
if ($request->sort_way) {
Session::put("crud.$modelTable.sort_way", $request->sort_way);
}
$customerModels = $query->with(['customer', 'brand'])->simplePaginate($this->getPagination());
} //end if
if (
Session::has("crud.$modelTable.sort_col") and
Session::has("crud.$modelTable.sort_way") and
(Schema::hasColumn($modelTable, Session::get("crud.$modelTable.sort_col")) or
in_array(Session::get("crud.$modelTable.sort_col"), [
'brands.name',
'customers.email',
'customer_infos.firstname',
'customer_infos.lastname',
'customer_infos.opt_in',
'customer_logins.last_connection',
'customer_logins.status'
]))
) {
$query = $query->orderBy(
Session::get("crud.$modelTable.sort_col"),
Session::get("crud.$modelTable.sort_way")
);
}
$searchFields = trans('Email, Prénom, Nom');
$query->with(['customer', 'brand']);
/** @var \Illuminate\Pagination\AbstractPaginator */
$customerModels = $this->paginate($query);
return view('bo.pages.customers.index', compact(
'modelClass',
'modelTable',
'customerModels',
'brands',
'searchFields'
));
return view('bo.pages.customers.index', [
'modelClass' => $modelClass,
'modelTable' => $modelTable,
'customerModels' => $customerModels ?? collect(),
'searchFields' => $searchFields,
'questionModels' => $questionModels,
'answerModels' => $answerModels,
]);
}
/**
@ -195,6 +157,7 @@ class CustomerController extends Controller
*/
public function export(): \Illuminate\Contracts\View\View
{
$this->authorize('export', Customer::class);
$brands = Brand::all();
return view('bo.pages.customers.export', compact(
'brands'
@ -303,4 +266,157 @@ class CustomerController extends Controller
});
return $response;
}
/**
* Reset all session sorts.
*
* @param string $table
* @return void
*/
private function resetSessionSorts(string $table): void
{
Session::forget([
"crud.$table.sort_col",
"crud.$table.sort_way",
]);
}
/**
* Reset all session filters.
*
* @param string $table
* @return void
*/
private function resetSessionFilters(string $table): void
{
Session::forget([
"crud.$table.brand",
"crud.$table.question",
"crud.$table.answer",
"crud.$table.search",
]);
}
/**
* Build the customer query.
*
* @return Builder
*/
private function buildCustomerQuery(): Builder
{
return CustomerInfo::query()
->join('customer_logins', function (JoinClause $join) {
$join->on('customer_logins.brand_id', '=', 'customer_infos.brand_id')
->on('customer_logins.customer_id', '=', 'customer_infos.customer_id');
})
->join('customers', fn($join) => $join->on('customers.id', '=', 'customer_infos.customer_id'))
->join('brands', fn($join) => $join->on('brands.id', '=', 'customer_infos.brand_id'));
}
/**
* Apply all filters (brand, question, answer).
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @param \Illuminate\Support\Collection|null $questionModels
* @param \Illuminate\Support\Collection|null $answerModels
* @return void
*/
private function applyBrandFilter(
Request $request,
Builder &$query,
string $table,
?Collection &$questionModels,
?Collection &$answerModels
): void {
$brand = $request->brand ?? Session::get("crud.$table.brand");
if (!(int) $brand) {
return;
}
Session::put("crud.$table.brand", $brand);
$query->where('customer_infos.brand_id', $brand);
$questionModels = Question::getAllModelsFromBrand($brand);
if (($request->filled('question') || Session::has("crud.$table.question")) && $questionModels->count()) {
$question = $request->question ?? Session::get("crud.$table.question");
Session::put("crud.$table.question", $question);
$answerModels = Answer::getAllAnswersFromQuestion($question);
$questionAnswers = Question::find($question)?->answers->modelKeys() ?? [];
$query->whereHas('customer.answers', fn($q) => $q->whereIn('answer_id', $questionAnswers));
if (
($request->filled('answer') || Session::has("crud.$table.answer"))
&& $questionModels->count()
&& $answerModels->count()
) {
$answer = $request->answer ?? Session::get("crud.$table.answer");
Session::put("crud.$table.answer", $answer);
$query->whereHas('customer.answers', fn($q) => $q->whereKey($answer));
}
}
}
/**
* Apply the search filter.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @param array $columns
* @return void
*/
private function applySearchFilter(Request $request, Builder &$query, string $table, array $columns): void
{
$search = $request->search ?? Session::get("crud.$table.search");
Session::put("crud.$table.search", $search);
$query->where(function ($q) use ($columns, $search) {
foreach ($columns as $column) {
$q->orWhere($column, 'LIKE', "%$search%");
}
$q->orWhere('customers.email', 'LIKE', "%$search%")
->orWhere('customer_logins.last_connection', 'LIKE', "%$search%");
});
}
/**
* Apply sorts.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $table
* @return void
*/
private function applySort(Request $request, Builder &$query, string $table): void
{
$sortCol = $request->sort_col ?? Session::get("crud.$table.sort_col");
$sortWay = $request->sort_way ?? Session::get("crud.$table.sort_way");
if ($sortCol) {
Session::put("crud.$table.sort_col", $sortCol);
}
if ($sortWay) {
Session::put("crud.$table.sort_way", $sortWay);
}
$sortable = [
'brands.name',
'customers.email',
'customer_infos.firstname',
'customer_infos.lastname',
'customer_infos.opt_in',
'customer_logins.last_connection',
'customer_logins.status',
];
if ($sortCol && $sortWay && (Schema::hasColumn($table, $sortCol) || in_array($sortCol, $sortable))) {
$query->orderBy($sortCol, $sortWay);
}
}
}

View file

@ -3,16 +3,15 @@
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
class DashboardController extends Controller
{
/**
* Dashboard Page
* Dashboard page.
*
* @return \Illuminate\Contracts\View\View
*/
public function index(): View
public function index(): \Illuminate\Contracts\View\View
{
return view('bo.pages.dashboard');
}

View file

@ -7,6 +7,7 @@ use App\Http\Requests\Bo\QuestionExport;
use App\Http\Requests\Bo\Questions\StoreQuestionRequest;
use App\Http\Requests\Bo\Questions\UpdateQuestionRequest;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\Question;
use Carbon\Carbon;
use Illuminate\Http\Request;
@ -17,6 +18,14 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
class QuestionController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Question::class);
}
/**
* Display a listing of the resource.
*
@ -26,11 +35,11 @@ class QuestionController extends Controller
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Question::query();
$query = Question::query()->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->brand) {
$query = $query->where('brand_id', '=', $request->brand);
}
if ($request->search) {
$this->searchQuery(
$query,
@ -46,7 +55,7 @@ class QuestionController extends Controller
$query->with(['brand', 'answers']);
/** @var \Illuminate\Pagination\AbstractPaginator */
$questionModels = $this->paginate($query);
$questionModels = $query->paginate($this->getPagination());
return view('bo.pages.questions.index', compact('questionModels', 'searchFields'));
}
@ -59,11 +68,10 @@ class QuestionController extends Controller
*/
public function create(Question $question): \Illuminate\Contracts\View\View
{
$brandModels = Brand::all();
$brandModels = Brand::query()->orderBy('name', 'ASC')->paginate($this->jsonItemPerPagination);
return view('bo.pages.questions.create', [
'questionModel' => $question,
'brandModels' => $brandModels
'brandModels' => $brandModels
]);
}
@ -103,9 +111,8 @@ class QuestionController extends Controller
*/
public function edit(Question $question): \Illuminate\Contracts\View\View
{
$brandModels = Brand::all();
$brandModels = Brand::query()->orderBy('name', 'ASC')->paginate($this->jsonItemPerPagination);
$question->load('brand');
return view('bo.pages.questions.edit', [
'questionModel' => $question,
'brandModels' => $brandModels
@ -182,6 +189,7 @@ class QuestionController extends Controller
*/
public function export(): \Illuminate\Contracts\View\View
{
$this->authorize('export', Question::class);
$brands = Brand::all();
return view('bo.pages.questions.export', compact('brands'));
}
@ -202,10 +210,12 @@ class QuestionController extends Controller
foreach ($questions as $question) {
$answersCounts = DB::query()
->selectRaw('count(*) as total, answer_id')
->from('customer_answer')
->whereIn('answer_id', $question->answers->pluck('id'))
->from('model_answer')
->where('model_type', '=', Customer::class)
->whereIn('answer_id', $question->answers->modelKeys())
->groupBy('answer_id')
->get()->keyBy('answer_id');
->get()
->keyBy('answer_id');
$results[$question->id] = [
'question' => $question->question,
@ -218,7 +228,7 @@ class QuestionController extends Controller
$answersCounts[$answer->id]->total ?? 0
];
}
}
} //end foreach
$w = Writer::createFromString('');
$w->setDelimiter(';');

View file

@ -14,9 +14,18 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class ServiceController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(Service::class);
}
/**
* Display a listing of the resource.
*
@ -26,11 +35,11 @@ class ServiceController extends Controller
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Service::query();
$query = Service::query()->when(
$request->filled('brand'),
fn($q) => $q->whereHas('brand', fn($q) => $q->where('id', $request->brand))
);
if ($request->brand) {
$query = $query->where('brand_id', '=', $request->brand);
}
if ($request->search) {
$this->searchQuery(
$query,
@ -45,7 +54,7 @@ class ServiceController extends Controller
$query->with(['brand']);
$serviceModels = $this->paginate($query);
$serviceModels = $query->paginate($this->getPagination());
return view('bo.pages.services.index', compact('serviceModels', 'searchFields'));
}
@ -58,8 +67,7 @@ class ServiceController extends Controller
*/
public function create(Service $service): \Illuminate\Contracts\View\View
{
$brandModels = Brand::all();
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.services.create', [
'serviceModel' => $service,
'brandModels' => $brandModels
@ -104,8 +112,7 @@ class ServiceController extends Controller
*/
public function edit(Service $service): \Illuminate\Contracts\View\View
{
$brandModels = Brand::all();
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.services.edit', [
'serviceModel' => $service,
'brandModels' => $brandModels

View file

@ -3,13 +3,24 @@
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Models\Answer;
use App\Models\Question;
use App\Models\SingleOptin;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class SingleOptinController extends Controller
{
/**
* Create the controller instance.
*/
public function __construct()
{
$this->authorizeResource(SingleOptin::class);
}
/**
* Display a listing of the resource.
*
@ -18,34 +29,30 @@ class SingleOptinController extends Controller
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = SingleOptin::query();
$questionModels = null;
$answerModels = null;
$searchFields = trans('E-mail');
if ($request->brand) {
$query = $query->whereHas('brand', function (Builder $q) use ($request) {
$q->where('id', $request->brand);
});
}
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'email',
);
if ($request->filled('search')) {
$query = SingleOptin::query();
$this->applyBrandAndAnswerFilters($request, $query, $questionModels, $answerModels);
$this->applySearch($request, $query);
$this->sortQuery($query);
$query->with('brand');
$singleoptinModels = $query->simplePaginate($this->getPagination());
}
$this->sortQuery($query);
$searchFields = trans('E-mail');
$query->with(['brand']);
/** @var \Illuminate\Pagination\AbstractPaginator */
$singleoptinModels = $this->paginate($query);
return view('bo.pages.singleoptins.index', compact('singleoptinModels', 'searchFields'));
return view('bo.pages.singleoptins.index', [
'singleoptinModels' => $singleoptinModels ?? collect(),
'searchFields' => $searchFields,
'questionModels' => $questionModels,
'answerModels' => $answerModels,
]);
}
/**
* Remove the specified resource from storage.
*
@ -54,6 +61,7 @@ class SingleOptinController extends Controller
*/
public function destroy(SingleOptin $singleoptin): \Illuminate\Http\RedirectResponse
{
$singleoptin->answers()->detach();
if ($singleoptin->deleteOrFail()) {
return redirect()->route('bo.single_optins.index')
->with('success', trans(
@ -67,4 +75,59 @@ class SingleOptinController extends Controller
['model' => Str::singular(trans('models.classes.singleoptins'))]
));
}
/**
* Undocumented function
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Support\Collection|null $questionModels
* @param \Illuminate\Support\Collection|null $answerModels
* @return void
*/
private function applyBrandAndAnswerFilters(
Request $request,
Builder $query,
?Collection &$questionModels,
?Collection &$answerModels
): void {
if (!$request->filled('brand') || !(int) $request->brand) {
return;
}
$brandId = $request->brand;
$query->whereHas('brand', fn($q) => $q->whereKey($brandId));
$questionModels = Question::getAllModelsFromBrand($brandId);
if (!$request->filled('question')) {
return;
}
$questionId = $request->question;
$question = Question::find($questionId);
if ($question) {
$questionAnswers = $question->answers->modelKeys();
$query->whereHas('answers', fn($q) => $q->whereIn('answer_id', $questionAnswers));
$answerModels = Answer::getAllAnswersFromQuestion($questionId);
}
if ($request->filled('answer')) {
$query->whereHas('answers', fn($q) => $q->whereKey($request->answer));
}
}
/**
* Apply search filter.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
private function applySearch(Request $request, Builder $query): void
{
if ($request->filled('search')) {
$this->searchQuery($query, $request->search, null, 'email');
}
}
}

View file

@ -7,10 +7,11 @@ use App\Http\Requests\Bo\StatsByBrandRequest;
use App\Http\Requests\Bo\StatsByQuestionRequest;
use App\Models\Brand;
use App\Models\BrandStat;
use App\Models\CustomerAnswer;
use App\Models\ModelAnswer;
use App\Models\Question;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use League\Csv\Writer;
@ -24,6 +25,7 @@ class StatsController extends Controller
*/
public function viewStatsByBrand(StatsByBrandRequest $request)
{
Gate::authorize('viewStatsByBrand');
$stats = null;
if ($request->post()) {
$stats = BrandStat::query()->whereDate('created_at', '>=', $request->start)
@ -68,7 +70,7 @@ class StatsController extends Controller
}
$w->insertOne($cols);
}
return response((string)$w, 200, [
return response((string) $w, 200, [
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
// phpcs:ignore Generic.Files.LineLength.TooLong
@ -85,6 +87,7 @@ class StatsController extends Controller
*/
public function viewByQuestion(StatsByQuestionRequest $request): \Illuminate\Contracts\View\View
{
Gate::authorize('viewByQuestion');
$question = null;
$answers = null;
$answersByDays = null;
@ -132,7 +135,7 @@ class StatsController extends Controller
$dayAnswers->values()->all()
));
}
return response((string)$w, 200, [
return response((string) $w, 200, [
'Content-Encoding' => 'none',
'Content-Type' => 'text/csv; charset=UTF-8',
// phpcs:ignore Generic.Files.LineLength.TooLong
@ -152,10 +155,12 @@ class StatsController extends Controller
*/
private function answersByDays(Collection $answers, Carbon $start, Carbon $end): \Illuminate\Support\Collection
{
Gate::authorize('answersByDays');
// * Totalize by date all answers.
$answersByDays = CustomerAnswer::query()->whereDate('created_at', '>=', $start)
$answersByDays = ModelAnswer::query()
->whereDate('created_at', '>=', $start)
->whereDate('created_at', '<=', $end)
->whereIn('answer_id', $answers->pluck('id'))
->whereIn('answer_id', $answers->modelKeys())
->get()->map(function ($custAnswer) {
return [
'answer_id' => $custAnswer->answer_id,

View file

@ -5,13 +5,15 @@ namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Users\StoreUserRequest;
use App\Http\Requests\Bo\Users\UpdateUserRequest;
use App\Models\Brand;
use App\Models\User;
use App\Traits\Controllers\UpdateModelPublished;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Uwil\LaravelCommon\Helpers\CollectionHelper;
class UsersController extends Controller
class UserController extends Controller
{
use UpdateModelPublished;
@ -31,14 +33,14 @@ class UsersController extends Controller
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $userModels */
$userModels = User::query();
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = User::query();
/** @var string $search Search field */
$search = $request->search;
if ($search) {
$this->searchQuery(
$userModels,
$query,
$search,
null,
'first_name',
@ -55,10 +57,10 @@ class UsersController extends Controller
]);
/** Sort columns with a query */
$this->sortQuery($userModels);
$this->sortQuery($query);
/** Custom pagination */
$userModels = $this->paginate($userModels);
$userModels = $query->paginate($this->getPagination());
return view('bo.pages.users.index', compact('userModels', 'search', 'searchFields'));
}
@ -72,7 +74,8 @@ class UsersController extends Controller
*/
public function create(User $user): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.create', ['userModel' => $user]);
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.users.create', ['userModel' => $user, 'brandModels' => $brandModels]);
}
/**
@ -88,6 +91,7 @@ class UsersController extends Controller
$user->fill($request->validated());
if ($user->saveOrFail()) {
$user->brands()->sync(collect($request->validated()['brand_id'])->pluck('id')->toArray());
return redirect()->route('bo.users.edit', $user)
->with('success', __('crud.messages.has_been_created', [
'model' => Str::ucfirst(__('models.classes.user'))
@ -108,7 +112,8 @@ class UsersController extends Controller
*/
public function edit(User $user): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.edit', ['userModel' => $user]);
$brandModels = CollectionHelper::paginate(Brand::getUserAssociatedBrands(), $this->jsonItemPerPagination);
return view('bo.pages.users.edit', ['userModel' => $user, 'brandModels' => $brandModels]);
}
/**
@ -124,6 +129,7 @@ class UsersController extends Controller
$user->fill($request->validated());
if ($user->saveOrFail()) {
$user->brands()->sync(collect($request->validated()['brand_id'])->pluck('id')->toArray());
return redirect()->route('bo.users.edit', $user)
->with('success', __('crud.messages.has_been_updated', [
'model' => Str::ucfirst(__('models.classes.user'))

View file

@ -4,11 +4,12 @@ namespace App\Http\Controllers;
use App\Enums\Pagination\ItemsPerPaginationEnum;
use App\Enums\Theme\BootstrapThemeEnum;
use App\Http\Requests\Bo\PaginateModelsRequest;
use App\Http\Requests\Bo\ThemeRequest;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
@ -20,6 +21,9 @@ class Controller extends BaseController
use AuthorizesRequests;
use ValidatesRequests;
/** @var integer $itemPerPagination */
public $jsonItemPerPagination = 5;
/**
* Build Search query on specified fields
* splitting search into words or using whole sentence
@ -156,7 +160,7 @@ class Controller extends BaseController
Session::put("$rName.sort_way", request()->sort_way);
Session::put("$rName.sorted", true);
}
if (request()->rst) {
if (request()->rstSrts) {
Session::remove("$rName.sorted");
Session::remove("$rName.sort_col");
Session::remove("$rName.sort_way");
@ -185,16 +189,13 @@ class Controller extends BaseController
}
/**
* Customize pagination with cache or config (default).
* Get the number of items per page.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \App\Enums\Pagination\ItemsPerPaginationEnum|null $pagination
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
* @return integer
*/
protected function paginate(
Builder $query,
ItemsPerPaginationEnum $pagination = null
): \Illuminate\Contracts\Pagination\LengthAwarePaginator {
protected function getPagination(ItemsPerPaginationEnum $pagination = null): int
{
$currentRoutePath = Str::slug(request()->route()->getName());
$cacheKey = "pagination.{$currentRoutePath}";
$pagination = $pagination ?? ItemsPerPaginationEnum::twelve;
@ -206,20 +207,35 @@ class Controller extends BaseController
if (Cache::get($cacheKey) !== $pagination) {
Cache::put($cacheKey, $pagination);
}
$query = $query->paginate($pagination);
return $query;
return $pagination;
}
/**
* Get a paginate resource.
*
* @param \App\Http\Requests\Bo\PaginateModelsRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function jsonPaginateModels(PaginateModelsRequest $request): \Illuminate\Http\JsonResponse
{
/** @var \Illuminate\Database\Eloquent\Model $models */
$models = $request->targetModel::query()
->where($request->input('fieldName') ?? 'name', 'like', "%{$request->input('search')}%")
->orderBy($request->input('fieldName') ?? 'name', 'ASC')
->paginate($request->input('paginate') ?? 10);
return \response()->json($models);
}
/**
* Set the bootstrap theme or light (default).
*
* @param \Illuminate\Http\Request $request
* @param \App\Http\Requests\Bo\ThemeRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function setTheme(Request $request): \Illuminate\Http\RedirectResponse
protected function setTheme(ThemeRequest $request): \Illuminate\Http\RedirectResponse
{
$theme = ToolboxHelper::getValidatedEnum(
$request->theme ?: Cache::get('theme', BootstrapThemeEnum::light->value()),
$request->validated('theme') ?: Cache::get('theme', BootstrapThemeEnum::light->value()),
'theme',
'\App\Http\Controllers\BootstrapThemeEnum',
);

View file

@ -20,7 +20,6 @@ class Kernel extends HttpKernel
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\HtmlSanitizer::class
];
/**
@ -39,7 +38,6 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
],
'api' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\App\Http\Middleware\JsonOnly::class,
@ -47,6 +45,11 @@ class Kernel extends HttpKernel
\App\Http\Middleware\ApiCheckTimeStamps::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class
],
'saml' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
],
];
/**
@ -57,16 +60,17 @@ class Kernel extends HttpKernel
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
'logoutusers' => \App\Http\Middleware\LogoutUnpublishedUsers::class,
];
}

View file

@ -57,9 +57,8 @@ class ApiResponse
$status = intval($response->getStatusCode() / 100) == 2 ? ApiStatus::OK : ApiStatus::KO;
if (Helper::isRequestForApiV2($request)) {
return $response->setData(new V2ApiResponse((object)$data, $status));
return $response->setData(new V2ApiResponse($status, (object) $data));
}
return $response->setData(new V4ApiResponse((object)$data, $status));
return $response->setData(new V4ApiResponse($status, (object) $data));
}
}

View file

@ -31,7 +31,7 @@ class Authenticate extends Middleware
}
}
}
return route('login');
return route('bo.login');
}
return null;

View file

@ -4,7 +4,6 @@ namespace App\Http\Middleware;
use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class HtmlSanitizer
{
@ -16,8 +15,11 @@ class HtmlSanitizer
* @param string|array ...$params
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, \Closure $next, string|array ...$params): Response
{
public function handle(
Request $request,
\Closure $next,
string|array ...$params
): \Symfony\Component\HttpFoundation\Response {
$inputs = $request->all();
/** @var callable|null */
$cleanInput = null;

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LogoutUnpublishedUsers
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
/** @var \App\Models\User|null $userModel */
$userModel = Auth::user();
if (!is_null($userModel) && !$userModel->published) {
Auth::logout();
return redirect()->route('bo.login')->with('warning', trans('auth.unpublished_user'));
}
return $next($request);
}
}

View file

@ -15,6 +15,7 @@ class TrustHosts extends Middleware
{
return [
$this->allSubdomainsOfApplicationUrl(),
config('app.k8s_internal_url'),
];
}
}

View file

@ -15,8 +15,10 @@ class SingleOptinRequest extends BaseApiRequest
public function rules(): array
{
return [
'email' => 'required|email:rfc,dns,spoof,filter',
'opt_in' => 'required|boolean'
'email' => 'required|email:rfc,dns,spoof,filter',
'opt_in' => 'required|boolean',
'answer_id' => 'sometimes|array|min:1',
'answer_id.*' => 'sometimes|integer|exists:answers,id',
];
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace App\Http\Requests\Api\V4;
use App\Enums\Country;
use App\Http\Sanitizers\RemoveSpaces;
use ArondeParon\RequestSanitizer\Sanitizers\Capitalize;
use ArondeParon\RequestSanitizer\Sanitizers\Lowercase;
use ArondeParon\RequestSanitizer\Sanitizers\Trim;
use ArondeParon\RequestSanitizer\Sanitizers\TrimDuplicateSpaces;
class SendClaimRequest extends BaseApiRequest
{
/**
* Using these sanitizers before proceeding the request.
*
* @var array
*/
protected $sanitizers = [
'first_name' => [Trim::class, TrimDuplicateSpaces::class, Capitalize::class],
'last_name' => [Trim::class, TrimDuplicateSpaces::class, Capitalize::class],
'email' => [Trim::class, RemoveSpaces::class, Lowercase::class],
'address' => [Trim::class, TrimDuplicateSpaces::class],
'city' => [Trim::class, TrimDuplicateSpaces::class],
'postcode' => [Trim::class],
'file' => [Trim::class],
'message' => [Trim::class],
];
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
$countryList = ($this->currentCountry && $this->currentCountry->get('iso')) ?
$this->currentCountry->get('iso') : implode(',', Country::getValidIsoAplha2Codes());
return [
'first_name' => 'required|string|between:3,255',
'last_name' => 'required|string|between:3,255',
'email' => 'required|email:rfc,dns,spoof,filter|max:255',
'address' => 'sometimes|nullable|string|max:255',
'city' => 'sometimes|nullable|string|between:2,255',
'postcode' => ['sometimes', 'nullable', 'string', "postal_code:{$countryList}"],
'file' => [
'sometimes',
'nullable',
'string',
// @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
function ($attribute, $value, $fail) {
// Base64 validation.
$decoded = base64_decode($value, true);
if ($decoded === false) {
return $fail('Le fichier doit être un encodage base64 valide.');
}
// Check the size (5 MB max = 5 * 1024 * 1024).
if (strlen($decoded) > 5 * 1024 * 1024) {
return $fail('Le fichier est trop volumineux (max 5 Mo).');
}
// Check the MIME type.
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo, $decoded);
$allowed = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($mime, $allowed)) {
return $fail('Le fichier doit être une image JPG/PNG ou un PDF.');
}
}
],
'message' => 'required|string|min:3',
'subject_id' => 'required|integer|exists:claim_subjects,id',
];
}
}

View file

@ -12,8 +12,10 @@ class SingleOptinRequest extends BaseApiRequest
public function rules(): array
{
return [
'email' => 'required|email:rfc,dns,spoof,filter',
'opt_in' => 'required|boolean'
'email' => 'required|email:rfc,dns,spoof,filter',
'opt_in' => 'required|boolean',
'answer_id' => 'sometimes|array|min:1',
'answer_id.*' => 'sometimes|integer|exists:answers,id',
];
}
}

View file

@ -3,9 +3,12 @@
namespace App\Http\Requests\Bo\Brands;
use Illuminate\Foundation\Http\FormRequest;
use Uwil\LaravelCommon\Traits\Requests\HasPicture;
class StoreBrandRequest extends FormRequest
{
use HasPicture;
/**
* Determine if the user is authorized to make this request.
*
@ -16,6 +19,16 @@ class StoreBrandRequest extends FormRequest
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->mergePicture('logo');
}
/**
* Get the validation rules that apply to the request.
*
@ -23,9 +36,16 @@ class StoreBrandRequest extends FormRequest
*/
public function rules(): array
{
return [
'name' => 'required|string|min:5|max:255',
$rules = [
'name' => 'required|unique:brands,name|string|min:5|max:255',
'url' => 'required|url|min:5|max:255',
'url_claim' => 'required|url|min:5|max:255',
'domain' => 'required|unique:brands,domain|string|min:5|max:255',
];
return \array_merge(
$rules,
$this->pictureRules('logo', 550, 550, 550, 550),
);
}
/**
@ -36,7 +56,11 @@ class StoreBrandRequest extends FormRequest
public function attributes(): array
{
return [
'name' => trans('Nom de la marque'),
'name' => trans('validation.attributes.name'),
'logo' => trans('validation.custom.logo'),
'url' => trans('validation.custom.url'),
'url_claim' => trans('validation.custom.url_claim'),
'domain' => trans('validation.custom.domain'),
];
}
}

View file

@ -4,4 +4,34 @@ namespace App\Http\Requests\Bo\Brands;
class UpdateBrandRequest extends StoreBrandRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
/** @var integer $brandModelKey */
$brandModelKey = request()->route()->parameter('brand')->getKey();
$rules = [
'name' => [
'required',
"unique:brands,name,$brandModelKey",
'string',
'min:5',
'max:255',
],
'domain' => [
'required',
"unique:brands,domain,$brandModelKey",
'string',
'min:5',
'max:255',
],
];
return \array_merge(
parent::rules(),
$rules,
);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\Bo\ClaimMailTemplates;
use Illuminate\Foundation\Http\FormRequest;
use Uwil\LaravelCommon\Rules\WysiwygLength;
class ClaimMailTemplateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'brand_id' => 'required|exists:brands,id',
'label' => 'required|string|min:3|max:255',
'content' => ['required', 'string', new WysiwygLength()],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'brand_id' => trans('models.classes.brands'),
'label' => trans('validation.custom.label'),
'content' => trans('validation.attributes.content'),
];
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Http\Requests\Bo\ClaimMessages;
use App\Enums\Claims\ClaimMessageType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
class StoreMessageRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
$this->merge([
'user_id' => $userModel->getKey(),
'type' => ClaimMessageType::human->value(),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'claim_id' => 'required|numeric|exists:claims,id',
'user_id' => 'required|numeric|exists:users,id',
'content' => 'required|string|between:3,255',
'type' => ['required', new Enum(ClaimMessageType::class)],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string,\Illuminate\Contracts\Translation\Translator|string|array<mixed>|null>
*/
public function attributes(): array
{
return [
'claim_id' => trans('models.classes.claims'),
'user_id' => trans('models.classes.users'),
'content' => trans('validation.attributes.content'),
'type' => trans('validation.custom.type'),
];
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\Bo\ClaimMessages;
use Illuminate\Foundation\Http\FormRequest;
class UpdateMessageRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'claim_message_id' => 'required|numeric|exists:claim_messages,id',
'content' => 'required|string|between:3,255',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string,\Illuminate\Contracts\Translation\Translator|string|array<mixed>|null>
*/
public function attributes(): array
{
return [
'claim_message_id' => trans('models.classes.claim_messages'),
'content' => trans('validation.attributes.content'),
];
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\Bo\ClaimSubjects;
use Illuminate\Foundation\Http\FormRequest;
class ClaimSubjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'brand_id' => 'required|exists:brands,id',
'label' => 'required|string|min:3|max:255',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'brand_id' => trans('models.classes.brands'),
'label' => trans('validation.custom.label'),
];
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Http\Requests\Bo\Claims;
use App\Enums\ExportFormat;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ExportClaimRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
public function prepareForValidation()
{
$this->merge([
'claimList' => json_decode($this->input('claimList'), true)
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'format' => [
'required',
Rule::enum(ExportFormat::class)
],
'claimList' => 'required|array',
'claimList.*' => 'required|numeric|exists:claims,id',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string,\Illuminate\Contracts\Translation\Translator|string|array<mixed>|null>
*/
public function attributes(): array
{
return [
'brand_id' => trans('models.classes.brands'),
'subject_id' => trans('models.classes.claimsubjects'),
'type' => trans('validation.custom.type'),
'status' => trans('validation.custom.status'),
'response_type' => trans('validation.custom.response_type'),
];
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\Bo\Claims;
use Illuminate\Foundation\Http\FormRequest;
class SendClaimAnswerRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'claim_id' => 'required|numeric|exists:claims,id',
'content' => 'required|string|between:3,2000',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string,\Illuminate\Contracts\Translation\Translator|string|array<mixed>|null>
*/
public function attributes(): array
{
return [
'claim_id' => trans('models.classes.claims'),
'content' => trans('validation.attributes.content'),
];
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Http\Requests\Bo\Claims;
use App\Enums\Claims\ClaimResponseType;
use App\Enums\Claims\ClaimStatus;
use App\Enums\Claims\ClaimType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
class UpdateClaimRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth('backend')->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'brand_id' => 'required|numeric|exists:brands,id',
'subject_id' => 'required|numeric|exists:claim_subjects,id',
'type' => ['required', new Enum(ClaimType::class)],
'status' => ['required', new Enum(ClaimStatus::class)],
'response_type' => ['present', 'nullable', new Enum(ClaimResponseType::class)],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string,\Illuminate\Contracts\Translation\Translator|string|array<mixed>|null>
*/
public function attributes(): array
{
return [
'brand_id' => trans('models.classes.brands'),
'subject_id' => trans('models.classes.claimsubjects'),
'type' => trans('validation.custom.type'),
'status' => trans('validation.custom.status'),
'response_type' => trans('validation.custom.response_type'),
];
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Bo\Claims;
use App\Enums\Users\RoleEnum;
use App\Policies\Rules\UserStaticRules;
use Illuminate\Foundation\Http\FormRequest;
class ViewClaimAttachmentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
/** @var \App\Models\User $user */
$user = auth('backend')->user();
return auth('backend')->check() &&
($user->brands->contains($this->claim->brand->id) ||
UserStaticRules::atLeastRole($user, RoleEnum::admin));
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [];
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests\Bo;
use Illuminate\Foundation\Http\FormRequest;
class PaginateModelsRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string,\Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'targetModel' => [
'required',
'string',
'starts_with:App\\Models\\',
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
function ($attribute, $value, $fail) {
if (!class_exists($value)) {
$fail("Le modèle spécifié n'existe pas.");
return;
}
if (!is_subclass_of($value, \Illuminate\Database\Eloquent\Model::class)) {
$fail("Le modèle spécifié n'est pas un modèle Eloquent.");
}
},
],
'page' => 'required|integer|min:1',
'paginate' => 'required|integer|min:1',
'fieldName' => 'required|string|alpha_dash|max:255',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string,\Illuminate\Contracts\Translation\Translator|string|array<mixed>|null>
*/
public function attributes(): array
{
return [
'targetModel' => trans('validation.custom.targetModel'),
'page' => trans('validation.custom.page'),
'paginate' => trans('validation.custom.paginate'),
'fieldName' => trans('validation.custom.fieldName'),
];
}
}

View file

@ -18,18 +18,6 @@ class StoreQuestionRequest extends FormRequest
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'brand_id' => collect($this->brand)->pluck('id')->first()
]);
}
/**
* Get the validation rules that apply to the request.
*

View file

@ -16,18 +16,6 @@ class StoreServiceRequest extends FormRequest
return auth('backend')->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'brand_id' => collect($this->brand)->pluck('id')->first()
]);
}
/**
* Get the validation rules that apply to the request.
*

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Bo;
use App\Enums\Theme\BootstrapThemeEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
class ThemeRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string,\Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'theme' => ['required', new Enum(BootstrapThemeEnum::class)],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string,\Illuminate\Contracts\Translation\Translator|string|array<mixed>|null>
*/
public function attributes(): array
{
return [
'theme' => trans('validation.custom.theme'),
];
}
}

View file

@ -100,11 +100,11 @@ abstract class UpdateMiniCrudRequest extends FormRequest
}) : $input['dimensions'];
// * Adapt dimensions
$sizes = (object)[
$sizes = (object) [
'minHeight' => $dim['height'],
'maxHeight' => $dim['height'],
'minWith' => $dim['width'],
'maxWith' => $dim['width'],
'minWith' => $dim['width'],
'maxWith' => $dim['width'],
];
$rules = array_merge($rules, [
"data.$iptName.value" => [

View file

@ -5,7 +5,6 @@ namespace App\Http\Requests\Bo\Users;
use App\Enums\Users\RoleEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Password;
class StoreUserRequest extends FormRequest
{
@ -24,10 +23,11 @@ class StoreUserRequest extends FormRequest
*
* @return void
*/
protected function prepareForValidation()
protected function prepareForValidation(): void
{
$this->merge([
'published' => $this->published ? true : false
'published' => $this->input('published') ? true : false,
'brand_id' => $this->input('brand_id') ?: [],
]);
}
@ -39,19 +39,14 @@ class StoreUserRequest extends FormRequest
public function rules(): array
{
return [
'first_name' => 'required|string|min:3|max:255',
'last_name' => 'required|string|min:3|max:255',
'email' => 'required|email:rfc,strict,dns,spoof,filter|unique:users,email',
'password' => [
'required', 'required_with:password_confirmation', 'confirmed', 'max:255', Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
'role' => ['required', new Enum(RoleEnum::class)],
'published' => 'required|boolean',
'first_name' => 'required|string|min:3|max:255',
'last_name' => 'required|string|min:3|max:255',
'email' => 'required|email:rfc,strict,dns,spoof,filter|unique:users,email',
'role' => ['required', new Enum(RoleEnum::class)],
'published' => 'required|boolean',
'brand_id' => 'present|array',
'brand_id.*' => 'sometimes|nullable|array',
'brand_id.*.id' => 'sometimes|nullable|numeric|exists:brands,id',
];
}
@ -63,12 +58,14 @@ class StoreUserRequest extends FormRequest
public function attributes(): array
{
return [
'first_name' => trans('validation.attributes.first_name'),
'last_name' => trans('validation.attributes.last_name'),
'email' => trans('validation.attributes.email'),
'password' => trans('validation.attributes.password'),
'role' => trans('validation.attributes.role'),
'published' => trans('validation.custom.publication')
'first_name' => trans('validation.attributes.first_name'),
'last_name' => trans('validation.attributes.last_name'),
'email' => trans('validation.attributes.email'),
'role' => trans('validation.attributes.role'),
'published' => trans('validation.custom.publication'),
'brand_id' => trans('models.classes.brands'),
'brand_id.*' => trans('models.classes.brands'),
'brand_id.*.id' => trans('models.classes.brands'),
];
}
}

View file

@ -2,8 +2,6 @@
namespace App\Http\Requests\Bo\Users;
use Illuminate\Validation\Rules\Password;
class UpdateUserRequest extends StoreUserRequest
{
/**
@ -11,14 +9,11 @@ class UpdateUserRequest extends StoreUserRequest
*
* @return void
*/
protected function prepareForValidation()
protected function prepareForValidation(): void
{
$this->merge([
'published' => $this->published ? true : false,
'password' => $this->password ? $this->password : $this->route('user')->password,
'password_confirmation' => $this->password_confirmation ?
$this->password_confirmation :
$this->route('user')->password
'published' => $this->input('published') ? true : false,
'brand_id' => $this->input('brand_id') ?: [],
]);
}
@ -29,19 +24,15 @@ class UpdateUserRequest extends StoreUserRequest
*/
public function rules(): array
{
/** @var \App\Models\User $userModel */
$userModel = request()->route()->parameter('user');
return array_merge(parent::rules(), [
'email' => [
'email' => [
'required',
'string',
'email:rfc,strict,dns,spoof,filter',
'unique:users,email,' . $this->route('user')->id . ',id',
],
'password' => [
'required', 'required_with:password_confirmation', 'confirmed', 'max:255', Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
'unique:users,email,' . $userModel->getKey(),
],
]);
}

View file

@ -16,10 +16,10 @@ class ApiResponse extends JsonResource
protected $status;
/**
* @param \stdClass|null $data
* @param \App\Enums\ApiStatus $status
* @param \stdClass|null $data
*/
public function __construct(stdClass|null $data = null, ApiStatus $status)
public function __construct(ApiStatus $status, stdClass|null $data = null)
{
$this->status = $status;
$this->data = $data;
@ -37,7 +37,7 @@ class ApiResponse extends JsonResource
//phpcs:enable
return [
'status' => $this->status->value(),
'data' => (array)($this->data ?? []),
'data' => (array) ($this->data ?? []),
'version' => '3.0.6'
];
}

View file

@ -11,20 +11,20 @@ use Illuminate\Support\Collection;
class QuestionsResource extends JsonResourceCollection
{
/** @var \Illuminate\Support\Collection<\App\Models\CustomerAnswer> */
protected $userAnwsers = null;
protected $userAnswers = null;
/**
* Create a new resource instance.
*
* @param mixed $resource
* @param \Illuminate\Support\Collection<\App\Models\CustomerAnswer> $userAnwsers
* @param \Illuminate\Support\Collection<\App\Models\CustomerAnswer> $userAnswers
* @return void
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function __construct(mixed $resource, Collection $userAnwsers = null)
public function __construct(mixed $resource, Collection $userAnswers = null)
{
//phpcs:enable
$this->userAnwsers = $userAnwsers;
$this->userAnswers = $userAnswers;
parent::__construct($resource);
}
@ -47,9 +47,9 @@ class QuestionsResource extends JsonResourceCollection
'question' => $question->question,
'answers' => new AnswersResource($answers)
];
if ($this->userAnwsers) {
if ($this->userAnswers) {
$question['customerAnswers'] = new AnswersResource($answers->filter(function (Answer $answer) {
return $this->userAnwsers->keyBy('id')->has($answer->id);
return $this->userAnswers->keyBy('id')->has($answer->id);
}));
}
return $question;

View file

@ -16,10 +16,10 @@ class ApiResponse extends JsonResource
protected $status;
/**
* @param \stdClass|null $data
* @param \App\Enums\ApiStatus $status
* @param \stdClass|null $data
*/
public function __construct(stdClass|null $data = null, ApiStatus $status)
public function __construct(ApiStatus $status, stdClass|null $data = null)
{
$this->status = $status;
$this->data = $data;
@ -36,8 +36,8 @@ class ApiResponse extends JsonResource
{
//phpcs:enable
return [
'status' => $this->status->value(),
'data' => (array)($this->data ?? []),
'status' => $this->status->value(),
'data' => (array) ($this->data ?? []),
'version' => config('api.version')
];
}

View file

@ -11,20 +11,20 @@ use Illuminate\Support\Collection;
class QuestionsResource extends JsonResourceCollection
{
/** @var \Illuminate\Support\Collection<\App\Models\CustomerAnswer> */
protected $userAnwsers = null;
protected $userAnswers = null;
/**
* Create a new resource instance.
*
* @param mixed $resource
* @param \Illuminate\Support\Collection<\App\Models\CustomerAnswer> $userAnwsers
* @param \Illuminate\Support\Collection<\App\Models\CustomerAnswer> $userAnswers
* @return void
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function __construct(mixed $resource, Collection $userAnwsers = null)
public function __construct(mixed $resource, Collection $userAnswers = null)
{
//phpcs:enable
$this->userAnwsers = $userAnwsers;
$this->userAnswers = $userAnswers;
parent::__construct($resource);
}
@ -47,9 +47,9 @@ class QuestionsResource extends JsonResourceCollection
'question' => $question->question,
'answers' => new AnswersResource($answers)
];
if ($this->userAnwsers) {
if ($this->userAnswers) {
$question['customerAnswers'] = new AnswersResource($answers->filter(function (Answer $answer) {
return $this->userAnwsers->keyBy('id')->has($answer->id);
return $this->userAnswers->keyBy('id')->has($answer->id);
}));
}
return $question;

View file

@ -0,0 +1,34 @@
<?php
namespace App\Interfaces;
interface IsExportable
{
/**
* Get the formatted csv line for the current model.
*
* @return array
*/
public function getFormattedCsvLine(): array;
/**
* Get the csv headers for this model.
*
* @return array
*/
public static function getCsvHeaders(): array;
/**
* 'file_extension' virtual property.
*
* @return string
*/
public function getFileExtensionAttribute(): string;
/**
* 'export_file_name' virtual property.
*
* @return string
*/
public function getExportFileNameAttribute(): string;
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Jobs;
use App\Enums\Claims\ClaimStatus;
use App\Mail\TransferredToDigitalCountMail;
use App\Models\Claim;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class SendTransferredToDigitalCountMail implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
$count = Claim::query()->where('status', ClaimStatus::sentDigital)->count();
if ($count <= 0) {
return;
}
Mail::to(config('mail.digital_service'))
->send(new TransferredToDigitalCountMail($count));
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Lib\Helpers;
class FileHelper
{
/**
* Get file path in private storage from stored path.
*
* @param string $basePath
* @return string
*/
public static function getPrivateFilePath(string $basePath): string
{
return str($basePath)->replaceFirst('storage/app/private', '');
}
}

View file

@ -369,7 +369,7 @@ class ToolboxHelper
{
// Only process if it's an object or array being passed to the function.
if (is_object($input) or is_array($input)) {
$ret = (array)$input;
$ret = (array) $input;
foreach ($ret as &$item) {
// Recursively process EACH element only if not scalar.
$item = (is_object($item) or is_array($item)) ?
@ -392,7 +392,7 @@ class ToolboxHelper
{
// Only process if it's an object or array being passed to the function.
if (is_object($input) or is_array($input)) {
$ret = (object)$input;
$ret = (object) $input;
foreach ($ret as &$item) {
// Recursively process EACH element only if not scalar.
$item = (is_object($item) or is_array($item)) ?

View file

@ -0,0 +1,125 @@
<?php
namespace App\Listeners;
use App\Enums\Claims\ClaimMessageType;
use App\Events\ClaimModelChanged;
use App\Models\ClaimMessage;
use Illuminate\Support\Str;
class AddClaimMessageSystem
{
/**
* Handle the event.
*
* @param \App\Events\ClaimModelChanged $event
* @return void
*/
public function handle(ClaimModelChanged $event): void
{
$message = $this->createClaimMessage($event);
// Assign content based on whether there is a custom message or default handling.
$message->content = $event->content ? $event->content : $this->generateMessageContent($event);
$message->saveOrFail();
}
/**
* Create a new ClaimMessage instance.
*
* @param \App\Events\ClaimModelChanged $event
* @return \App\Models\ClaimMessage
*/
private function createClaimMessage(ClaimModelChanged $event): \App\Models\ClaimMessage
{
return new ClaimMessage([
'claim_id' => $event->claim->getKey(),
'user_id' => null,
'type' => ClaimMessageType::system,
]);
}
/**
* Generate the message content based on the event data.
*
* @param \App\Events\ClaimModelChanged $event
* @return string
*/
private function generateMessageContent(ClaimModelChanged $event): string
{
// Check for Enum type and generate content accordingly.
$castType = $event->claim->getCasts()[$event->field] ?? null;
if ($castType && enum_exists($castType) && str_starts_with($castType, "App\\Enums\\Claims\\")) {
return $this->generateEnumMessageContent($event);
}
// Default field message content.
return $this->generateDefaultFieldMessageContent($event);
}
/**
* Generate message content for Enum type fields.
*
* @param \App\Events\ClaimModelChanged $event
* @return string
*/
private function generateEnumMessageContent(ClaimModelChanged $event): string
{
$enumLabel = $event->claim->{$event->field}->label();
$enumColor = $event->claim->{$event->field}->color();
$baseMessage = trans('Modification :field en', ['field' => $event->label]);
// If the user is available, include user details in the message.
if ($event->user) {
return sprintf(
"<b>%s %s</b> %s <i class='badge' style='background-color:%s'>%s</i>",
$event->user->first_name,
$event->user->last_name,
trans('a modifié :field en', ['field' => $event->label]),
$enumColor,
$enumLabel,
);
}
// User is not available, return a simple enum change message.
return sprintf(
"%s : <i class='badge' style='background-color:%s'>%s</i>",
$baseMessage,
$enumColor,
$enumLabel,
);
}
/**
* Generate message content for default field types.
*
* @param \App\Events\ClaimModelChanged $event
* @return string
*/
private function generateDefaultFieldMessageContent(ClaimModelChanged $event): string
{
$field = $event->claim->{$event->field};
$label = Str::of($event->label)->lower()->singular();
$baseMessage = trans('Modification :field en', ['field' => $label]);
$fieldLabel = $field->label ?? $field->name;
// If the user is available, include user details in the message.
if ($event->user) {
return sprintf(
"<b>%s %s</b> %s <b>%s</b>",
$event->user->first_name,
$event->user->last_name,
trans('a modifié :field en', ['field' => $label]),
$fieldLabel,
);
}
// User is not available, return a simple field change message.
return sprintf(
"%s : <b>%s</b>",
$baseMessage,
$fieldLabel,
);
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace App\Mail;
use App\Models\Claim;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class ClaimAnswerMail extends Mailable implements ShouldQueue
{
use Queueable;
use SerializesModels;
/**
* The current claim.
*
* @var \App\Models\Claim
*/
protected $claimModel;
/**
* The answer of the current claim.
*
* @var string
*/
protected $answer;
/**
* Create a new message instance.
*
* @param \App\Models\Claim $claimModel
* @param string $answer
* @return void
*/
public function __construct(Claim $claimModel, string $answer)
{
$this->claimModel = $claimModel;
$this->answer = $answer;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this
->from("no-reply@{$this->claimModel->brand->domain}")
->subject(sprintf(
'Réponse réclamation %s - %s',
$this->claimModel->reference,
$this->claimModel->brand->name
))
->markdown('mail.send-claim-answer')
->text('mail.send-claim-answer-plain')
->with([
'customHeader' => view('vendor.mail.html.header-claim-answer', [
'brandModel' => $this->claimModel->brand,
])->render(),
'customFooter' => view('vendor.mail.html.footer-claim-answer', [
'brandModel' => $this->claimModel->brand,
])->render(),
'claimModel' => $this->claimModel,
'answer' => $this->answer,
]);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Mail;
use Illuminate\Mail\Mailable;
class TransferredToDigitalCountMail extends Mailable
{
/**
* Create a new message instance.
*
* @param integer $count
* @return void
*/
public function __construct(private int $count)
{
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this
->subject(sprintf('%s réclamations vous attendent', $this->count))
->view('mail.transferred-claim-count')
->text('mail.transferred-claim-count-plain')
->with('count', $this->count);
}
}

View file

@ -51,7 +51,7 @@ class ActivityLog extends Model
/**
* The attributes that should be cast.
*
* @var array<string, string>
* @var array<string, mixed>
*/
protected $casts = [
'is_anonymous' => 'bool',

View file

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
/**
* @property integer $id Id.
@ -13,8 +14,7 @@ use Illuminate\Database\Eloquent\Model;
* @property-read \Illuminate\Support\Carbon $updated_at Updated date.
*
* @property-read \App\Models\Question $question Question BelongsTo relation.
* @property-read \Illuminate\Database\Eloquent\Collection<App\Models\Customer>
* $customers Customer BelongsToMany relation.
* @property-read \Illuminate\Database\Eloquent\Collection<Illuminate\Database\Eloquent\Model> $model Morph to relation.
*/
class Answer extends Model
{
@ -23,16 +23,65 @@ class Answer extends Model
/**
* The attributes that are mass assignable.
*
* @var string[]
* @var array<int, string>
*/
protected $fillable = [
'answer',
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::created(function (self $model) {
self::resetCache($model);
});
static::updated(function (self $model) {
self::resetCache($model);
});
static::deleted(function (self $model) {
self::resetCache($model);
});
}
// * METHODS
/**
* Reset all cache elements related to this model.
*
* @param self $model
* @return void
*/
private static function resetCache(self $model): void
{
$questionModelKey = $model->question->getKey();
Cache::forget("question-{$questionModelKey}-answers");
self::getAllAnswersFromQuestion($questionModelKey);
}
/**
* Get all answers.
*
* @param integer $questionModelKey
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getAllAnswersFromQuestion(int $questionModelKey): \Illuminate\Database\Eloquent\Collection
{
return Cache::remember("question-{$questionModelKey}-answers", 86400, function () use ($questionModelKey) {
return self::query()
->whereHas('question', fn($q) => $q->whereKey($questionModelKey))
->orderBy('answer', 'ASC')
->get();
});
}
// * RELATIONS
/**
* Question BelongsTo relation.
* Question belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
@ -42,12 +91,22 @@ class Answer extends Model
}
/**
* Customer belongsToMany relation.
* Customer morph to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function customers(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
public function customers(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->belongsToMany(Customer::class, 'customer_answer');
return $this->morphedByMany(Customer::class, 'model', 'model_answer', 'answer_id', 'model_id');
}
/**
* Customer morph to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function singleOptins(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->morphedByMany(SingleOptin::class, 'model', 'model_answer', 'answer_id', 'model_id');
}
}

View file

@ -2,16 +2,22 @@
namespace App\Models;
use App\Enums\Users\RoleEnum;
use App\Traits\Models\ActivityLog;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Rennokki\QueryCache\Traits\QueryCacheable;
use Illuminate\Support\Facades\Cache;
use Uwil\LaravelCommon\Helpers\FileHelper;
/**
* @property integer $id Id.
* @property string $name Name.
* @property-read \Illuminate\Support\Carbon $created_at Created date.
* @property-read \Illuminate\Support\Carbon $updated_at Updated date.
* @property integer $id Brand id.
* @property string $name Brand name.
* @property string $logo Brand logo.
* @property string $url Brand url link.
* @property string $url_claim Brand associative url claim.
* @property string $domain Brand domain.
* @property-read \Illuminate\Support\Carbon $created_at Brand created date.
* @property-read \Illuminate\Support\Carbon $updated_at Brand updated date.
*
* @property-read \Illuminate\Database\Eloquent\Collection<App\Models\Service>
* $services Service HasMany relation.
@ -28,51 +34,20 @@ class Brand extends Model
{
use ActivityLog;
use HasFactory;
use QueryCacheable;
/**
* The timeout cache for this model.
*
* @var integer
*/
protected $cacheFor = 0;
/**
* The attributes that are mass assignable.
*
* @var string[]
* @var array<int, string>
*/
protected $fillable = [
'name',
'logo',
'url',
'url_claim',
'domain',
];
/**
* Brand
*
* @param array $attributes
*/
public function __construct(array $attributes = [])
{
$this->cacheFor = config('api.servicesDBCache') * 60;
parent::__construct($attributes);
}
/**
* Set the base cache tags that will be present
* on all queries.
*
* ! This is used to flush caching
*
* @return array
*/
protected function getCacheBaseTags(): array
{
return [
'crm_brands',
];
}
/**
* Perform any actions required after the model boots.
*
@ -80,70 +55,23 @@ class Brand extends Model
*/
protected static function booted(): void
{
static::creating(function (self $model) {
$model->logo = FileHelper::storeFile($model, $model->logo, true);
});
static::created(function () {
// * Flush Eloquent cache.
self::flushQueryCache();
self::resetCache();
});
static::updated(function () {
// * Flush Eloquent cache.
self::flushQueryCache();
static::updating(function (self $model) {
$model->logo = FileHelper::storeFile($model, $model->logo, true);
});
static::deleted(function () {
// * Flush Eloquent cache.
self::flushQueryCache();
static::updated(function (self $model) {
self::resetCache();
FileHelper::removeOldFile($model, 'logo');
});
static::deleted(function (self $model) {
self::resetCache();
FileHelper::removeOldFile($model, 'logo');
});
}
// * RELATIONS
/**
* Service hasMany relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function services(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Service::class);
}
/**
* Question hasMany relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function questions(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Question::class);
}
/**
* CustomerInfo hasMany relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function customerInfos(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(CustomerInfo::class);
}
/**
* ServiceStat hasMany relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function servicesStats(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ServiceStat::class);
}
/**
* SingleOptin hasMany relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function singleOptins(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(SingleOptin::class);
}
// * METHODS
@ -167,4 +95,141 @@ class Brand extends Model
{
return $this->customerInfos()->where('opt_in', 1)->count() + $this->singleOptins()->count();
}
/**
* Get all brands associated to the auth user.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getUserAssociatedBrands(): \Illuminate\Database\Eloquent\Collection
{
/** @var \App\Models\User $userModel */
$userModel = auth('backend')->user();
$brandModels = self::getBrands();
if ($userModel->role->equals(RoleEnum::marketing)) {
$brandIds = $userModel->brands->modelKeys();
$brandModels = $brandModels->filter(function ($brand) use ($brandIds) {
return in_array($brand->getKey(), $brandIds);
})->values();
}
return $brandModels;
}
/**
* Reset all cache elements related to this model.
*
* @return void
*/
private static function resetCache(): void
{
Cache::forget('brands');
self::getBrands();
}
/**
* Get all brands.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getBrands(): \Illuminate\Database\Eloquent\Collection
{
return Cache::remember('brands', 86400, function () {
return self::query()->orderBy('name', 'ASC')->get();
});
}
// * RELATIONS
/**
* Service has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function services(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Service::class);
}
/**
* Question has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function questions(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Question::class);
}
/**
* CustomerInfo has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function customerInfos(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(CustomerInfo::class);
}
/**
* ServiceStat has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function servicesStats(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ServiceStat::class);
}
/**
* SingleOptin has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function singleOptins(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(SingleOptin::class);
}
/**
* Users many to many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function users(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Brand::class, 'brand_users', 'brand_id', 'user_id');
}
/**
* Claim has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function claims(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Claim::class);
}
/**
* ClaimSubject has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function claimSubjects(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ClaimSubject::class);
}
/**
* ClaimMailTemplate has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function claimMailTemplates(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ClaimMailTemplate::class);
}
}

View file

@ -28,7 +28,7 @@ class BrandStat extends Model
/**
* The attributes that are mass assignable.
*
* @var string[]
* @var array<int, string>
*/
protected $fillable = [
'active',
@ -42,7 +42,7 @@ class BrandStat extends Model
/**
* The attributes that should be cast to native types.
*
* @var array
* @var array<string, mixed>
*/
public $casts = [
'created_at' => 'date',
@ -65,7 +65,7 @@ class BrandStat extends Model
// * RELATIONS
/**
* Brand hasMany relation.
* Brand belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/

294
app/Models/Claim.php Normal file
View file

@ -0,0 +1,294 @@
<?php
namespace App\Models;
use App\Enums\Claims\ClaimResponseType;
use App\Enums\Claims\ClaimStatus;
use App\Enums\Claims\ClaimType;
use App\Events\ClaimModelChanged;
use App\Interfaces\IsExportable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
/**
* @property integer $id Claim id.
* @property string $reference Claim reference.
* @property string $first_name Claimant\'s first name.
* @property string $last_name Claimant\'s last name.
* @property string $email Claimant\'s email.
* @property string|null $address Claimant\'s address.
* @property string|null $city Claimant\'s city.
* @property string|null $postcode Claimant\'s postcode.
* @property string|null $file Claimant\'s file.
* @property string $message Claimant\'s message.
* @property \App\Enums\Claims\ClaimResponseType $response_type Claim type (ClaimResponseType enum).
* @property \App\Enums\Claims\ClaimStatus $status Claim status (ClaimStatus enum).
* @property \App\Enums\Claims\ClaimType $type Claim type (ClaimType enum).
* @property \App\Models\Brand $brand_id Claim associated Brand.
* @property \App\Models\ClaimSubject $subject_id Claim associated Subject.
* @property string|null $hashcode Claim associated Customer hashcode.
* @property-read \Illuminate\Support\Carbon $created_at Claim created date.
* @property-read \Illuminate\Support\Carbon $updated_at Claim updated date.
*
* @property-read string $file_extension Extension of the file.
* @property-read string $export_file_name Name of the exported file.
*/
class Claim extends Model implements IsExportable
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'reference',
'first_name',
'last_name',
'email',
'address',
'city',
'postcode',
'file',
'message',
'response_type',
'status',
'type',
'brand_id',
'subject_id',
'hashcode',
];
/**
* The attributes that should be cast.
*
* @var array<string, mixed>
*/
protected $casts = [
'response_type' => ClaimResponseType::class,
'status' => ClaimStatus::class,
'type' => ClaimType::class,
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::created(function (self $model) {
$model->reference = self::encodeIntegerToBase36($model->getKey());
$model->saveOrFail();
});
static::updated(function (self $model) {
$user = auth()->user();
if (!$user instanceof \App\Models\User) {
$user = null;
}
if (isset($model->getChanges()['brand_id'])) {
event(new ClaimModelChanged(
$model->load('brand'),
'brand',
Str::singular(trans('models.classes.brands')),
$user,
));
}
if (isset($model->getChanges()['subject_id'])) {
event(new ClaimModelChanged(
$model->load('claimSubject'),
'claimSubject',
trans('models.classes.claimsubjects'),
$user,
));
}
if (isset($model->getChanges()['status'])) {
event(new ClaimModelChanged(
$model,
'status',
trans('validation.custom.status'),
$user,
));
}
if (isset($model->getChanges()['type'])) {
event(new ClaimModelChanged(
$model,
'type',
trans('validation.custom.type'),
$user,
));
}
if (
isset($model->getChanges()['response_type'])
&& !$model->response_type->equals(ClaimResponseType::manual)
) {
event(new ClaimModelChanged(
$model,
'response_type',
trans('validation.custom.response_type'),
$user,
));
}
});
}
// * RELATIONS
/**
* Brand belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function brand(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Brand::class);
}
/**
* ClaimSubject subject belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function claimSubject(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(ClaimSubject::class, 'subject_id');
}
/**
* ClaimMessage has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function claimMessages(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ClaimMessage::class);
}
// * METHODS
/**
* Get CustomerInfos
*
* If Customer Haschcode <> null
*
* @return CustomerInfo|null
*/
public function getCustomerInfosAttribute(): CustomerInfo|null
{
if (!is_null($this->hashcode)) {
return CustomerInfo::query()->where('hashcode', $this->hashcode)->first();
}
return null;
}
/**
* Convert the integer to base 36 and make uppercase.
*
* @param integer $int
* @return string
*/
public static function encodeIntegerToBase36(int $int): string
{
return strtoupper(base_convert($int, 10, 36));
}
/**
* Convert back to base 10 from base 36, case insensitive.
*
* @param string $str
* @return string
*/
public static function decodeBase36ToInteger(string $str): string
{
return base_convert(strtolower($str), 36, 10);
}
/**
* Get formated address.
*
* @return string
*/
public function getFormatedAddress(): string
{
return implode(', ', array_filter([
$this->address,
trim(implode(' ', array_filter([$this->postcode, $this->city])))
]));
}
/**
* Get the formmated csv line for the current model.
*
* @return array
*/
public function getFormattedCsvLine(): array
{
return [
$this->reference,
$this->created_at->toDateTimeString(),
sprintf('%s %s', $this->first_name, Str::upper($this->last_name)),
$this->email,
sprintf('%s, %s, %s', $this->address, $this->postcode, $this->city),
$this->status->label(),
$this->claimSubject->label,
$this->type->label(),
$this->brand->name,
$this->message,
$this->export_file_name,
];
}
/**
* Get the csv headers for this model.
*
* @return array
*/
public static function getCsvHeaders(): array
{
return [
'Référence',
'Date de création',
'Prénom NOM',
'Email',
'Adresse',
'Statut',
'Sujet',
'Type',
'Marque',
'Message',
'Pièce jointe',
];
}
// * VIRTUAL ATTRIBUTES
/**
* 'file_extension' virtual property.
*
* @return string
*/
public function getFileExtensionAttribute(): string
{
return File::extension($this->file);
}
/**
* 'export_file_name' virtual property.
*
* @return string
*/
public function getExportFileNameAttribute(): string
{
return $this->file ?
sprintf(
'%s.%s',
$this->reference,
$this->file_extension,
) :
__('Aucune pièce jointe');
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
/**
* @property integer $id Mail template id.
* @property string $label Mail template label.
* @property string $content Mail template content.
* @property \App\Models\Brand $brand_id Mail template associated Brand.
* @property-read \Illuminate\Support\Carbon $created_at Mail template created date.
* @property-read \Illuminate\Support\Carbon $updated_at Mail template updated date.
*/
class ClaimMailTemplate extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'label',
'content',
'brand_id',
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::created(function () {
self::resetCache();
});
static::updated(function () {
self::resetCache();
});
static::deleted(function () {
self::resetCache();
});
}
// * METHODS
/**
* Get all templates associated to the band.
*
* @param integer $brandModelKey
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getAllModelsFromBrand(int $brandModelKey): \Illuminate\Database\Eloquent\Collection
{
return self::getTemplates()->filter(
fn($model) => $model->brand->getKey() === $brandModelKey
)->values();
}
/**
* Reset all cache elements related to this model.
*
* @return void
*/
private static function resetCache(): void
{
Cache::forget('templates');
self::getTemplates();
}
/**
* Get all templates.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getTemplates(): \Illuminate\Database\Eloquent\Collection
{
return Cache::remember('templates', 86400, function () {
return self::query()->orderBy('label', 'ASC')->get();
});
}
// * RELATIONS
/**
* Brand belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function brand(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Brand::class);
}
}

119
app/Models/ClaimMessage.php Normal file
View file

@ -0,0 +1,119 @@
<?php
namespace App\Models;
use App\Enums\Claims\ClaimMessageType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
/**
* @property integer $id Subject id.
* @property string $label Subject label.
* @property \App\Models\Claim $claim_id Subject associated Claim.
* @property-read \Illuminate\Support\Carbon $created_at Subject created date.
* @property-read \Illuminate\Support\Carbon $updated_at Subject updated date.
*/
class ClaimMessage extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'content',
'type',
'claim_id',
'user_id',
];
/**
* The attributes that should be cast.
*
* @var array<string, mixed>
*/
protected $casts = [
'type' => ClaimMessageType::class,
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::created(function () {
self::resetCache();
});
static::updated(function () {
self::resetCache();
});
static::deleted(function () {
self::resetCache();
});
}
// * METHODS
/**
* Get all brands associated to the auth user.
*
* @param integer $claimModelKey
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getAllModelsFromClaim(int $claimModelKey): \Illuminate\Database\Eloquent\Collection
{
return self::getClaimMessages()->filter(
fn($model) => $model->claim->getKey() === $claimModelKey
)->values();
}
/**
* Reset all cache elements related to this model.
*
* @return void
*/
private static function resetCache(): void
{
Cache::forget('claimMessages');
self::getClaimMessages();
}
/**
* Get all brands.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getClaimMessages(): \Illuminate\Database\Eloquent\Collection
{
return Cache::remember('claimMessages', 86400, function () {
return self::query()->with('user')->orderby('created_at', 'ASC')->get();
});
}
// * RELATIONS
/**
* Claim belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function claim(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Claim::class);
}
/**
* User belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);
}
}

107
app/Models/ClaimSubject.php Normal file
View file

@ -0,0 +1,107 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
/**
* @property integer $id Subject id.
* @property string $label Subject label.
* @property \App\Models\Brand $brand_id Subject associated Brand.
* @property-read \Illuminate\Support\Carbon $created_at Subject created date.
* @property-read \Illuminate\Support\Carbon $updated_at Subject updated date.
*/
class ClaimSubject extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'label',
'brand_id',
];
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::created(function () {
self::resetCache();
});
static::updated(function () {
self::resetCache();
});
static::deleted(function () {
self::resetCache();
});
}
// * METHODS
/**
* Get all subjects associated to the band.
*
* @param integer $brandModelKey
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getAllModelsFromBrand(int $brandModelKey): \Illuminate\Database\Eloquent\Collection
{
return self::getSubjects()->filter(
fn($model) => $model->brand->getKey() === $brandModelKey
)->values();
}
/**
* Reset all cache elements related to this model.
*
* @return void
*/
private static function resetCache(): void
{
Cache::forget('subjects');
self::getSubjects();
}
/**
* Get all subjects.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public static function getSubjects(): \Illuminate\Database\Eloquent\Collection
{
return Cache::remember('subjects', 86400, function () {
return self::query()->orderBy('label', 'ASC')->get();
});
}
// * RELATIONS
/**
* Brand belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function brand(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Brand::class);
}
/**
* Claim has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function claims(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Claim::class);
}
}

View file

@ -3,7 +3,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
@ -24,7 +23,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property-read \Illuminate\Database\Eloquent\Collection<App\Models\Brand>
* $brands Brand BelongsToMany relation.
* @property-read \Illuminate\Database\Eloquent\Collection<App\Models\Answer>
* $answers Answer BelongsToMany relation.
* $answers Answer morph many relation.
*/
class Customer extends Model
{
@ -37,7 +36,7 @@ class Customer extends Model
/**
* The attributes that are mass assignable.
*
* @var string[]
* @var array<int, string>
*/
protected $fillable = [
'email',
@ -46,7 +45,7 @@ class Customer extends Model
// * RELATIONS
/**
* CustomerInfo hasMany relation.
* CustomerInfo has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -56,7 +55,7 @@ class Customer extends Model
}
/**
* CustomerLogin hasMany relation.
* CustomerLogin has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -66,7 +65,7 @@ class Customer extends Model
}
/**
* CustomerOptInHistory hasMany relation.
* CustomerOptInHistory has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -76,7 +75,7 @@ class Customer extends Model
}
/**
* Customer Brand belongsToMany relation.
* Customer Brand belongs to many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
@ -86,13 +85,23 @@ class Customer extends Model
}
/**
* Answers belongsToMany relation.
* Answers morph to many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
*/
public function answers(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
public function answers(): \Illuminate\Database\Eloquent\Relations\MorphToMany
{
return $this->belongsToMany(Answer::class, 'customer_answer');
return $this->morphToMany(Answer::class, 'model', 'model_answer', 'model_id', 'answer_id');
}
/**
* Claim has many relation.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function claims(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Claim::class);
}
// * METHODS
@ -217,14 +226,17 @@ class Customer extends Model
* @param \App\Models\Service $service
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getAnswers(Service $service): Collection
public function getAnswers(Service $service): \Illuminate\Database\Eloquent\Collection
{
return Answer::query()->whereHas('customers', function (Builder $q) {
$q->where('id', $this->id);
})->whereHas('question', function (Builder $q) use ($service) {
$q->whereHas('brand', function (Builder $q) use ($service) {
$q->where('id', $service->brand->id);
});
})->get();
return Answer::query()
->whereHas('customers', function (Builder $q) {
$q->where('id', $this->id);
})
->whereHas('question', function (Builder $q) use ($service) {
$q->whereHas('brand', function (Builder $q) use ($service) {
$q->where('id', $service->brand->id);
});
})
->get();
}
}

View file

@ -1,49 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property integer $answer_id Foreign key Answer.
* @property integer $customer_id Foreign key Customer.
* @property-read \Illuminate\Support\Carbon $created_at Created date.
*
* @property-read \App\Models\Customer $customer Customer BelongsTo relation.
* @property-read \App\Models\Answer $answer Answer BelongsTo relation.
*/
class CustomerAnswer extends Pivot
{
/**
* Inhibit update_at column as we only have
* created_at.
*
* @return null
*/
public function getUpdatedAtColumn()
{
return null;
}
// * RELATIONS
/**
* Customer belongsTo relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function customer(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Customer::class);
}
/**
* Customer belongsTo relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function answer(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Answer::class);
}
}

View file

@ -36,7 +36,7 @@ class CustomerInfo extends Model
/**
* The attributes that are mass assignable.
*
* @var string[]
* @var array<int, string>
*/
protected $fillable = [
'firstname',
@ -54,7 +54,7 @@ class CustomerInfo extends Model
/**
* The attributes that should be cast to native types.
*
* @var array
* @var array<string, mixed>
*/
protected $casts = [
'country' => Country::class,
@ -83,7 +83,7 @@ class CustomerInfo extends Model
// * RELATIONS
/**
* Customer BelongsTo relation.
* Customer belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
@ -93,7 +93,7 @@ class CustomerInfo extends Model
}
/**
* Brand BelongsTo relation.
* Brand belongs to relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/

Some files were not shown because too many files have changed in this diff Show more